Using multiupload in generic frame

Hi,

I’m trying to enable the multi-upload function on a generic frame (screen) that uses a generic filesDs collection datasource. This works fine but I’m not able to link the uploaded files to the parent object as the parent entity type differs per usage.

A description of the setup/implementation is given below.

File.class

// Generic package stuff left out
public class File extends StandardEntity {
    private static final long serialVersionUID = -6474423184262399337L;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "FILE_DESCRIPTOR_ID")
    protected FileDescriptor fileDescriptor;

    @Lob
    @Column(name = "REMARKS")
    protected String remarks;

    @Column(name = "VERSION")
    protected String version;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "ENTITY_A_ID")
    private A entityA;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "ENTITY_A_ID")
    private B entityB;

   // Getters and setters left out
}

Note that entity types A and B could be anything having a many-to-one composition with File and thus imply a map field on File. Actually, there are some more entities that have such usages of File.

file-frame.xml

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<window xmlns="http://schemas.haulmont.com/cuba/window.xsd"
        class="com.company.app.web.file.FileFrame"
        messagesPack="com.company.app.web.file">
    <dsContext>
         <!-- parent datasource is to provide a filesDs -->
    </dsContext>
    <layout expand="dropZone">
        <vbox id="dropZone" margin="false,false,false,false" spacing="false" width="100%" expand="fileTable">
            <table id="fileTable"
                   width="100%">
                <actions>
                    <action id="create"/>
                    <action id="edit"/>
                    <action id="remove"/>
                </actions>
                <columns>
                    <column id="fileDescriptor"/>
                    <column id="version" width="200px"/>
                    <column id="updateTs" width="150px"/>
                    <column id="updatedBy" width="200px"/>
                    <column id="remarks"/>
                </columns>
                <rows datasource="filesDs"/>
                <buttonsPanel>
                    <button action="fileTable.create"/>
                    <button action="fileTable.edit"/>
                    <button action="fileTable.remove"/>
                    <multiUpload id="multiUploadField"  dropZone="dropZone"/>
                </buttonsPanel>
            </table>
        </vbox>
    </layout>
</window>

Note that the dropzone covers the entire table. The frame is typically included on the edit screen of the entity on one of the tabs:

A-edit.xml

            <!-- other tabs left out -->
            <tab id="tabFiles"
                 caption="msg://tab.Files"
                 expand="files"
                 margin="true,false,false,false">
                <frame id="files"
                       screen="app$File.frame"/>
            </tab>

FileFrame.class

// Generic package stuff left out
public class FileFrame extends AbstractFrame {
    @Inject
    private ComponentsFactory componentsFactory;

    @Inject
    private CollectionDatasource<File, UUID> filesDs;

    @Inject
    private Table<File> fileTable;

    @Inject
    private FileStorageService fileStorageService;

    @Inject
    private FileMultiUploadField multiUploadField;

    @Inject
    private FileUploadingAPI fileUploadingAPI;

    @Inject
    private DataSupplier dataSupplier;

    @Inject
    private Metadata metadata;

    @Override
    public void init(Map<String, Object> params) {
        // Handle drag & drop upload for files
        multiUploadField.addQueueUploadCompleteListener(() -> {
            for (Map.Entry<UUID, String> entry : multiUploadField.getUploadsMap().entrySet()) {
                UUID fileId = entry.getKey();
                String fileName = entry.getValue();
                FileDescriptor fd = fileUploadingAPI.getFileDescriptor(fileId, fileName);
                // Save file to FileStorage
                try {
                    fileUploadingAPI.putFileIntoStorage(fileId, fd);
                } catch (FileStorageException e) {
                    throw new RuntimeException("Error saving file to FileStorage", e);
                }
                // Save file descriptor to database
                dataSupplier.commit(fd);

                // Add file to list
                File f = metadata.create(File.class);
                f.setFileDescriptor(fd);

                // How to add reference to parent object ??
                // e.g. f.setEntityA(parent) or f.setEntityB(parent)

                filesDs.addItem(f);
            }
            showNotification(getMessage("Uploaded"), NotificationType.HUMANIZED);
            multiUploadField.clearUploads();
        });

        multiUploadField.addFileUploadErrorListener(event ->
                showNotification(getMessage("UploadError"), NotificationType.HUMANIZED));
     }
}

When using the normal create button for the table, the references to the parent entity are all set without any problem. However, when dragging one or more files to the dropzone, I’m unable to set the proper parent entity field (see comments).

As each file is only linked to a single parent, a generic parent field -instead of entityA and entityB fields - would help out here. However, I’m unable to have the entity types A and B (and others) to use such a field for the composition mapping.

And yes, another solution would be to have a super class that has the files composition and have entity A and B inherit from it. That may be the best solution but implies major refactoring at this point which I’m not keen of doing - apart from problems that such an approach would have as well

Any suggestions? Thanks for any help!
-b

Hi,
if you want to avoid a refactoring your the data model you may try the following.
Add a couple of methods to the FileFrame: setParentA(A parentA) and setParentB(B parentB) that will save the parent entity to the corresponding class field. In the parent screen controller, inject the FileFrame and, when the parent screen is initializing, invoke the proper setter (set parent a or b).
After that in the upload completion listener you’ll have a parent entity explicitly set and will be able to use it.

Hi Max,

Thanks for your comment. I thought of such an approach but that leaves me with adding code to a number of screens. And I very much like the current setup in which we simply add the frame and everything works fine.

Is there no option to have a many-to-one relation ship on a more generic UUID field instead of having all these reference fields on the File class?

Something like this:

public class File extends StandardEntity {
    private static final long serialVersionUID = -6474423184262399337L;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "FILE_DESCRIPTOR_ID")
    protected FileDescriptor fileDescriptor;

    @Lob
    @Column(name = "REMARKS")
    protected String remarks;

    @Column(name = "VERSION")
    protected String version;

    // Generic parent reference
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "PARENT_ID")
    private UUID parent;

}

It seems this is not possible as the actual parent types (A and B previously) do not mention the File at all but still seems to work basically because of the fields in File.class.

Thanks again.
-b

1 Like

There is another option you can think about.
Do you need the “files” collection in the A and B entities? If don’t, when you can make the parentId field in the File entity a regular column:

@Column(name = "PARENT_ID")
private UUID parentId;

When you need to display the list of files create a collection datasource wilth a query like this:

select f from my$File f where f.parentId = :ds$aDs

Hi Max,

That might actually work - the files on the parent entities are only needed for reference and are always shown using the same frame. I will try this one - thanks!

-b

Hi Max,

I’ve given this a try but now do not see an option to get the parent id transferred to the frame component. If I would need to do this through code in the edit screens of the parent types A/B I’m back at the start…

Any idea how to get the parent UUID within a frame? I guess it’s not possible but maybe you have an idea.

-b

Finally got something working. Now using the table to add a file but preventing the add dialog to popup:

    private boolean hidePopup = false;
    private File newFile;

    @Override
    public void init(Map<String, Object> params) {
 
        // Handle drag & drop upload for files
        fileTable.addAction(new CreateAction(fileTable) {
            @Override
            protected void internalOpenEditor(CollectionDatasource datasource, Entity newItem, Datasource parentDs, Map<String, Object> params) {
                if (!hidePopup)
                    super.internalOpenEditor(datasource, newItem, parentDs, params);
                newFile = (File) newItem;
            }
        });

        // Handle drag & drop upload for files
        multiUploadField.addQueueUploadCompleteListener(() -> {
            hidePopup = true;
            CommitContext cc = new CommitContext();
            for (Map.Entry<UUID, String> entry : multiUploadField.getUploadsMap().entrySet()) {
                UUID fileId = entry.getKey();
                String fileName = entry.getValue();
                FileDescriptor fd = fileUploadingAPI.getFileDescriptor(fileId, fileName);
                // Save file to FileStorage
                try {
                    fileUploadingAPI.putFileIntoStorage(fileId, fd);
                } catch (FileStorageException e) {
                    throw new RuntimeException("Error saving file to FileStorage", e);
                }
                // Save file descriptor to database
                dataSupplier.commit(fd);

                // Add file to list by initiating the add file action (this sets reference attributes)
                fileTable.getAction("create").actionPerform(fileTable);
                // newFile now holds the newly created file
                newFile.setFileFile(fd);
                cc.addInstanceToCommit(newFile);
                // Add to datasource
                filesDs.includeItem(newFile);
            }
            dm.commit(cc);
            multiUploadField.clearUploads();
            hidePopup = false;

            // Let user know the outcome
            showNotification(getMessage("Uploaded"), NotificationType.HUMANIZED);
        });

       ....

This works very nice; reference attributes get set through the table create action!

However - somehow there is always an however - when the file(s) that was added through drag & drop is removed by the user before saving the parent entity, the file is removed from the list but on reopening the parent it is shown again (thus not removed).

Not sure how to fix that. Tried to do something with the RemoveAction handler but that didn’t solve anything.’

Any ideas?
-b

If I were you, I would go the way where frame contains explicit method “setParentId”. You say, that you’ll have to spend some time for adding the code that invokes this setter on your current editor screens, but I think you would spend much less time on that than you’ve already spend on finding an alternative solution.

Here is a small sample that demonstrates how I see the solution of your problem.

file-sample.zip (91.9 KB)

Hi Max,

Thanks for your follow up. I agree that the approach that you’ve taken might be the better solution but I came across my own solution from another thing I was working on. So it didn’t take me the time you might expect.

Pros for your solution is the generic approach, pros for mine is the parent independent approach - at least code wise.

By the way, I am using frames more and more as they are a means of organizing complex screens into better understandable and stable code. For example, the generic browser+edit screen that is provided in Cuba seems to have a more elegant solution when the editor part is organised as a frame.

Recently I’ve created a complex screen that consists of a browser, editor and sub-editor (edits a child object within the main editor) where both editors are created using a frame. I actually started off with the generic browser+edit screen from Cuba but in the end had to abandon that approach as it became too complex.

At almost all locations in which I currently use a frame, I’ve come upon the need to have a means of getting a handle to the parent. Would that be something to add to a future version of the Cuba platform? It would make the use of frames a lot more powerful while it only requires a small extension (like your own example).

Anyway, thanks again.
-b

For now, you already have an access from frames to screen. You can get screen UI components and datasources, it is mentioned in the documentation. What else do you need?

Well, I maybe missing something here but there is no way to get information from/access to the screen class, is there?

For example, in the implementation I mentioned before, I need to ‘signal’ the browser screen that a new entity was saved and the frame needs to “close”. This in fact is now done through the parent by hiding the frame (parallel to the combined browser+editor screen by Cuba).

Open for suggestions but this in my opinion would be a nice addition.