Skip to content

Instantly share code, notes, and snippets.

@pfurini
Last active June 30, 2017 14:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pfurini/daf22c2cfdd146c7b9b8178c5ccf1263 to your computer and use it in GitHub Desktop.
Save pfurini/daf22c2cfdd146c7b9b8178c5ccf1263 to your computer and use it in GitHub Desktop.
CUBA framework: CompositeLookupDelegate class and associated Builder to eliminate boilerplate code for composite screens
/*
* Copyright (c) 2017 Nexbit di Paolo Furini
*
* MIT License
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package it.nexbit.sample.gui.components;
import com.haulmont.cuba.core.entity.Entity;
import com.haulmont.cuba.gui.ComponentsHelper;
import com.haulmont.cuba.gui.components.*;
import com.haulmont.cuba.gui.components.actions.CreateAction;
import com.haulmont.cuba.gui.components.actions.EditAction;
import com.haulmont.cuba.gui.components.actions.RemoveAction;
import com.haulmont.cuba.gui.data.CollectionDatasource;
import com.haulmont.cuba.gui.data.DataSupplier;
import com.haulmont.cuba.gui.data.Datasource;
import com.haulmont.cuba.security.entity.EntityOp;
import javax.validation.constraints.NotNull;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
/**
* A controller's delegate that provides common functions to composite lookup screens (those
* that combines a list component and edit controls on the same screen).
*/
@SuppressWarnings("unchecked")
public class CompositeLookupDelegate<E extends Entity> {
protected final Table<E> lookupTable;
protected final Datasource<E> editItemDs;
protected final DataSupplier dataSupplier;
protected final Component.Container editControlsContainer;
protected final Component editActionsPane;
protected final Component lookupContainer;
protected final boolean autoCommit;
protected FieldGroup firstFieldGroup;
protected AfterInitEditComponents afterInitEditComponents;
private boolean creating;
public CompositeLookupDelegate(@NotNull DataSupplier dataSupplier,
@NotNull Table<E> lookupTable,
@NotNull Datasource<E> editItemDs,
@NotNull Component.Container editControlsContainer,
@NotNull Component editActionsPane,
@NotNull Component lookupContainer) {
this(dataSupplier, lookupTable, editItemDs, editControlsContainer, editActionsPane, lookupContainer, true);
}
public CompositeLookupDelegate(@NotNull DataSupplier dataSupplier,
@NotNull Table<E> lookupTable,
@NotNull Datasource<E> editItemDs,
@NotNull Component.Container editControlsContainer,
@NotNull Component editActionsPane,
@NotNull Component lookupContainer,
boolean autoCommit) {
this.lookupTable = lookupTable;
this.editItemDs = editItemDs;
this.dataSupplier = dataSupplier;
this.editControlsContainer = editControlsContainer;
this.editActionsPane = editActionsPane;
this.lookupContainer = lookupContainer;
this.autoCommit = autoCommit;
/*
* Adding {@link com.haulmont.cuba.gui.data.Datasource.ItemChangeListener} to {@link lookupTable}'s Datasource.
* The listener reloads the selected record with the specified view and sets it to {@link editItemDs}.
*/
lookupTable.getDatasource().addItemChangeListener(e -> {
E selectedItem = (E) e.getItem();
if (selectedItem != null) {
AbstractDatasource<E> ds = (AbstractDatasource<E>) e.getDs();
if (!ds.isModified() ||
(!ds.getItemsToCreate().contains(selectedItem) && !ds.getItemsToUpdate().contains(selectedItem))) {
selectedItem = (E) this.dataSupplier.reload(e.getDs().getItem(), editItemDs.getView());
}
editItemDs.setItem(selectedItem);
}
});
/*
* Adding {@link CreateAction} to {@link lookupTable}
* The listener removes selection in {@link lookupTable}, sets a newly created item to {@link editItemDs}
* and enables controls for record editing
*/
lookupTable.addAction(new CreateAction(lookupTable) {
@Override
protected void internalOpenEditor(CollectionDatasource datasource, Entity newItem, Datasource parentDs, Map<String, Object> params) {
lookupTable.setSelected(Collections.emptyList());
editItemDs.setItem((E) newItem);
refreshOptionsForLookupFields();
enableEditControls(true);
}
});
/*
* Adding {@link EditAction} to {@link lookupTable}
* The listener enables controls for record editing
*/
lookupTable.addAction(new EditAction(lookupTable) {
@Override
protected void internalOpenEditor(CollectionDatasource datasource, Entity existingItem, Datasource parentDs, Map<String, Object> params) {
if (lookupTable.getSelected().size() == 1) {
refreshOptionsForLookupFields();
enableEditControls(false);
}
}
@Override
public void refreshState() {
if (target != null) {
CollectionDatasource ds = target.getDatasource();
if (ds != null && !captionInitialized) {
setCaption(messages.getMainMessage("actions.Edit"));
}
}
super.refreshState();
}
@Override
protected boolean isPermitted() {
CollectionDatasource ownerDatasource = target.getDatasource();
boolean entityOpPermitted = security.isEntityOpPermitted(ownerDatasource.getMetaClass(), EntityOp.UPDATE);
return entityOpPermitted && super.isPermitted();
}
});
/*
* Adding {@link RemoveAction} to {@link lookupTable} to reset record, contained in {@link editItemDs}.
*/
lookupTable.addAction(new RemoveAction(lookupTable, autoCommit) {
@Override
protected void afterRemove(Set selected) {
super.afterRemove(selected);
editItemDs.setItem(null);
}
});
disableEditControls();
}
/**
* Enabling controls for record editing.
*
* @param creating indicates if a new instance of {@link E} is being created
*/
public void enableEditControls(boolean creating) {
this.creating = creating;
initEditComponents(true);
// focus the first fieldGroup control in the editControlsContainer
getFirstFieldGroup().requestFocus();
}
/**
* Disabling editing controls.
*/
public void disableEditControls() {
initEditComponents(false);
lookupTable.requestFocus();
}
/**
* @param afterInitEditComponents handler that is invoked after initEditComponents invocation.
*/
public void setAfterInitEditComponents(AfterInitEditComponents afterInitEditComponents) {
this.afterInitEditComponents = afterInitEditComponents;
}
/**
* Initiating edit controls, depending on if they should be enabled/disabled.
*
* @param enabled if true - enables editing controls and disables controls on the left side of the splitter
* if false - visa versa
*/
protected void initEditComponents(boolean enabled) {
ComponentsHelper.walkComponents(editControlsContainer, (component, name) -> {
if (component instanceof FieldGroup) {
((FieldGroup) component).setEditable(enabled);
} else if (component instanceof Table) {
((Table) component).getActions().forEach(action -> action.setEnabled(enabled));
} else if (!(component instanceof Component.Container)) {
component.setEnabled(enabled);
}
});
editActionsPane.setVisible(enabled);
lookupContainer.setEnabled(!enabled);
if (afterInitEditComponents != null) {
afterInitEditComponents.handle(enabled, creating);
}
}
protected void refreshOptionsForLookupFields() {
for (Component component : getFirstFieldGroup().getOwnComponents()) {
if (component instanceof LookupField) {
CollectionDatasource optionsDatasource = ((LookupField) component).getOptionsDatasource();
if (optionsDatasource != null) {
optionsDatasource.refresh();
}
}
}
}
protected FieldGroup getFirstFieldGroup() {
if (firstFieldGroup == null) {
ComponentsHelper.walkComponents(editControlsContainer, (component, name) -> {
if (component instanceof FieldGroup) {
firstFieldGroup = (FieldGroup) component;
}
});
}
return firstFieldGroup;
}
public boolean isCreating() {
return creating;
}
public static <T extends Entity> Builder<T> builder(Window owningWindow) {
return new Builder<>(owningWindow);
}
public interface AfterInitEditComponents {
/**
* @param enabled if the controls were enabled or disabled
* @param creating true if creating a new entity, false if editing
*/
void handle(boolean enabled, boolean creating);
}
@SuppressWarnings("unchecked")
public static class Builder<E extends Entity> {
protected Frame owningFrame;
protected Table<E> lookupTable;
protected Datasource<E> editItemDs;
protected DataSupplier dataSupplier;
protected Component.Container editControlsContainer;
protected Component editActionsPane;
protected Component lookupContainer;
protected boolean autoCommit = true;
public Builder(@NotNull Frame owningFrame) {
this.owningFrame = owningFrame;
dataSupplier = owningFrame.getDsContext().getDataSupplier();
}
public Builder<E> lookupTable(@NotNull String id) {
lookupTable = (Table<E>) owningFrame.getComponentNN(id);
return this;
}
public Builder<E> lookupTable(@NotNull Table<E> component) {
lookupTable = component;
return this;
}
public Builder<E> editControlsContainer(@NotNull String id) {
editControlsContainer = (Component.Container) owningFrame.getComponentNN(id);
return this;
}
public Builder<E> editControlsContainer(@NotNull Component.Container container) {
editControlsContainer = container;
return this;
}
public Builder<E> editActionsPane(@NotNull String id) {
editActionsPane = owningFrame.getComponentNN(id);
return this;
}
public Builder<E> editActionsPane(@NotNull Component component) {
editActionsPane = component;
return this;
}
public Builder<E> lookupContainer(@NotNull String id) {
lookupContainer = owningFrame.getComponentNN(id);
return this;
}
public Builder<E> lookupContainer(@NotNull Component component) {
lookupContainer = component;
return this;
}
public Builder<E> editItemDatasource(@NotNull String id) {
editItemDs = owningFrame.getDsContext().getNN(id);
return this;
}
public Builder<E> editItemDatasource(@NotNull Datasource<E> datasource) {
editItemDs = datasource;
return this;
}
public Builder<E> autoCommit(boolean autoCommit) {
this.autoCommit = autoCommit;
return this;
}
public CompositeLookupDelegate<E> build() {
return new CompositeLookupDelegate<>(
dataSupplier,
lookupTable,
editItemDs,
editControlsContainer,
editActionsPane,
lookupContainer,
autoCommit
);
}
}
}
/*
* Copyright (c) 2017 Nexbit di Paolo Furini
*/
package it.nexbit.sample.web.client;
import com.haulmont.cuba.core.entity.BaseUuidEntity;
import com.haulmont.cuba.gui.components.*;
import com.haulmont.cuba.gui.components.actions.AddAction;
import com.haulmont.cuba.gui.data.CollectionDatasource;
import com.haulmont.cuba.gui.data.DataSupplier;
import com.haulmont.cuba.gui.data.Datasource;
import it.nexbit.sample.entity.Client;
import it.nexbit.sample.gui.components.CompositeLookupDelegate;
import javax.inject.Inject;
import javax.inject.Named;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* @author Paolo Furini
*/
public class ClientBrowse extends AbstractLookup {
/**
* The {@link CollectionDatasource} instance that loads a list of {@link Client} records
* to be displayed in {@link ClientBrowse#clientsTable} on the left
*/
@Inject
private CollectionDatasource<Client, UUID> clientsDs;
/**
* The {@link Datasource} instance that contains an instance of the selected entity
* in {@link ClientBrowse#clientsDs}
*/
@Inject
private Datasource<Client> clientDs;
/**
* The {@link Table} instance, containing a list of {@link Client} records,
* loaded via {@link ClientBrowse#clientsDs}
*/
@Inject
private Table<Client> clientsTable;
/**
* The {@link FieldGroup} instance that is linked to {@link ClientBrowse#clientDs}
* and shows fields of the selected {@link Client} record
*/
@Inject
private FieldGroup fieldGroup;
@Inject
private DataSupplier dataSupplier;
protected CompositeLookupDelegate<Client> compositeLookupDelegate;
@Override
public void init(Map<String, Object> params) {
compositeLookupDelegate = CompositeLookupDelegate.<Client>builder(this)
.lookupContainer("lookupBox")
.lookupTable("clientsTable")
.editControlsContainer("tabSheet")
.editActionsPane("actionsPane")
.editItemDatasource("clientDs")
.autoCommit(false) // for remove action, default is true
.build();
}
/**
* Method that is invoked by clicking Ok button after editing an existing or creating a new record
*/
public void save() {
if (!validate(Collections.singletonList(fieldGroup))) {
return;
}
getDsContext().commit();
Client editedItem = clientDs.getItem();
if (compositeLookupDelegate.isCreating()) {
clientsDs.includeItem(editedItem);
} else {
clientsDs.updateItem(editedItem);
}
clientsTable.setSelected(editedItem);
compositeLookupDelegate.disableEditControls();
}
/**
* Method that is invoked by clicking Cancel button, discards changes and disables controls for record editing
*/
public void cancel() {
Client selectedItem = clientsDs.getItem();
if (selectedItem != null) {
Client reloadedItem = dataSupplier.reload(selectedItem, clientDs.getView());
clientsDs.setItem(reloadedItem);
} else {
clientsDs.setItem(null);
}
compositeLookupDelegate.disableEditControls();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment