Create new records when searching for reference object

138 views Asked by At

In my current project I would like to be able to create new objects when searching for a reference object. This happens in several places of the application.

For example, let's assume we have a City Entity and a Country Entity. The City entity has a mandatory reference to the Country entity.

In my use case, I would like to create a new City. When I do this, I will have to assign a Country to the new City. When I click on the lookup icon, I get the selection dialog with all existent countries. But if I don't have the Country I want, I have to abort the operation, get back to the countries list and create the new one I'd like to assign to my new city.

  1. Would it be possible to create that new Country from the selection dialog with all countries?
  2. If it is possible, is the country being added to the list right after it has been created?
  3. Would it be possible to one define a range for the countries list? For example, showing only countries in Europe, if the user is in Europe.

I could imagine, that this would be a lot to ask from the framework. But I am just giving a shot and perhaps also giving a new feature idea, which would be nice to have.

1

There are 1 answers

2
Vincent Vandenschrick On

Customization of the LOV dialog :

You can easily customize the LOV dialog by creating your own class of the LOV action that is installed next to the reference fields.

  • Adding a new action in the dialog (the create action) :
public class LovActionWithCreate<E, F, G> extends LovAction<E, F, G> {

  private IDisplayableAction createAction;

  @Override
  protected void feedContextWithDialog(IReferencePropertyDescriptor<IComponent> erqDescriptor,
                                       IQueryComponent queryComponent, IView<E> lovView, IActionHandler actionHandler,
                                       Map<String, Object> context) {
    super.feedContextWithDialog(erqDescriptor, queryComponent, lovView, actionHandler, context);
    List<IDisplayableAction> defaultLovDialogActions = (List<IDisplayableAction>) context.get(
        ModalDialogAction.DIALOG_ACTIONS);
    defaultLovDialogActions.add(1, getCreateAction());
  }

  /**
   * Gets create action.
   *
   * @return the create action
   */
  protected IDisplayableAction getCreateAction() {
    return createAction;
  }

  /**
   * Sets create action.
   *
   * @param createAction
   *     the create action
   */
  public void setCreateAction(IDisplayableAction createAction) {
    this.createAction = createAction;
  }
}

The key point is to override the feedContextWithDialog method in order to install the new action into the dialog.

Next step is to install your new LOV action. You can either do it globally for whole application or per reference view :

  • replacing the LOV action globally is just a matter of declaring an action named 'lovAction' into your application frontend.groovy, i.e. :
action('lovAction', parent: 'lovActionBase', class:'test.LovActionWithCreate',
    custom: [createAction_ref:'theCreateAction']
)
  • replacing the LOV action on a certain reference field in a form can be done by using the referencePropertyView (in a form or in a table) and its 'lovAction' property, e.g. :
action('lovActionWithCreate', parent: 'lovActionBase', class:'test.LovActionWithCreate',
    custom: [createAction_ref:'theCreateAction']
)

form('ACertainForm'){
  fields {
    ...
    referencePropertyView name:'country', lovAction:'lovActionWithCreate'
    ...
  }
}

Creating an entity in the LOV dialog :

In the next step, we create the action that will be responsible for opening an extra dialog in order to create the new entity, persist it and, if successful, add it to the LOV result view. This is a little more complicated but not that much.

  • First of all, we have to open a new dialog.

For doing this, we will inherit the built-in EditComponentAction. The goal of this action is to edit a model in a modal dialog. The only difficulty here is that our model is only known at runtime. No problem though as we will use the dynamic nature of Jspresso.

public class CreateEntityFromLOVAction<E, F, G> extends EditComponentAction<E,F,G> {

  @Override
  protected Object getComponentToEdit(Map<String, Object> context) {
    IEntityFactory entityFactory = getBackendController(context).getEntityFactory();
    IQueryComponent lovQueryComponent = (IQueryComponent) context.get(IQueryComponent.QUERY_COMPONENT);
    Class<IEntity> entityToCreateContract = lovQueryComponent.getQueryContract();

    IEntity entityInstance = entityFactory.createEntityInstance(entityToCreateContract);
    setActionParameter(Arrays.asList(entityInstance), context);
    return entityInstance;
  }

  @Override
  protected IViewDescriptor getViewDescriptor(Map<String, Object> context) {
    IEntityFactory entityFactory = getBackendController(context).getEntityFactory();
    IQueryComponent lovQueryComponent = (IQueryComponent) context.get(IQueryComponent.QUERY_COMPONENT);
    Class<IEntity> entityToCreateContract = lovQueryComponent.getQueryContract();
    IComponentDescriptor<?> entityToCreateDescriptor = entityFactory.getComponentDescriptor(entityToCreateContract);

    BasicComponentViewDescriptor formViewDescriptor = new BasicComponentViewDescriptor();
    formViewDescriptor.setModelDescriptor(entityToCreateDescriptor);
    return formViewDescriptor;
  }
}

If you look at the code above, our new action takes care of the following :

  1. Get the type of entity to create from the context. For this, we are just exploring the query component which is the model of the LOV dialog.
  2. Create the entity instance and set it as action parameter in the context for the chain to continue working on it (save, close dialog).
  3. Create a form to display in the creation dialog.

Points 1 and 2 are handled by the getComponentToEdit method and point 3 by the getViewDescriptor method.

  • Next, when the user clicks Ok, we have to save the entity, add it to the LOV result list and close the creation dialog.

For this, we will create a new action and chain it to the saveAction and closeDialogAction built-in actions.

public class CreateEntityFromLOVPersistAction<E, F, G> extends FrontendAction<E,F,G> {

  @Override
  public boolean execute(IActionHandler actionHandler, Map<String, Object> context) {
    if (super.execute(actionHandler, context)) {
      IQueryComponent lovQueryComponent = (IQueryComponent) context.get(IQueryComponent.QUERY_COMPONENT);
      List<IEntity> createdEntityInstance = getActionParameter(context);

      lovQueryComponent.setQueriedComponents(createdEntityInstance);
      return true;
    }
    return false;
  }
}
  • And the final wiring in SJS frontend.groovy:
action('createEntityFromLovOkAction', parent: 'okDialogFrontAction',
        class:'test.CreateEntityFromLOVPersistAction',
       wrapped: 'saveBackAction', next: 'closeDialogAction')

action('createEntityFromLovAction', parent: 'editComponentAction',
        class: 'test.CreateEntityFromLOVAction',
       name:'add.name', custom: [
           okAction_ref: 'createEntityFromLovOkAction'
       ]
)

action('lovAction', parent: 'lovActionBase',
        class:'test.LovActionWithCreate',
    custom: [createAction_ref:'createEntityFromLovAction']
)

A long answer for less than 100 lines of code, but now you have a fully generic LOV action where the user can create any missing master data without leaving his current screen.

Presetting some data in the LOV filter depending on the user context :

For this, we generally use the initialization mapping that allows for setting some restrictions (either static or dynamic) on a reference property when it is queried in a LOV. For instance, let's consider the following use case :

  • You have 2 entities, Contract and Tariff, that are linked together through a 1-N relationship, i.e. a Contract is linked to 1 Tariff.
  • Contract and Tariff both have a country property and a Tariff can be assigned to a Contract if and only if they belong to the same country.
  • Tarrif has a status property and can only be used in a Contract if its status is ACTIVE.

You can simply enforce these rules in the LOV by setting the initialization mapping on the reference property the following way :

Entity('Contract', ...) {
  ...
  reference 'tariff', ref: 'Tariff',
       initializationMapping: [
            'country': 'country',
            'status': 'ACTIVE'
       ]
  ...
}

Thinking about it, this kind of behavior might very well find its way to the framework, so please, feel free to ope an enhancement request in the Jspresso GitHub.