Skip to content

Post Actions: parent master entity binding

jhou-pro edited this page Sep 14, 2023 · 14 revisions

Property / Entity actions on Entity Master sometimes require automatic saving of parent entity. For example, consider CertificationRevalidation entity that can either be just SAVEd or APPROVEd (i.e. SAVEd and approved with approveDate indication). When approval is performed, the user usually changes some properties and, without waiting for validation, immediately taps APPROVE button. This means that CertificationRevalidation is still in construction phase and may even become erroneous during ApproveCertificationRevalidationAction execution.

So, the entity being approved can have its own changes, initiated by the user, and changes from actual approval (e.g. approveDate populated). Both types of changes should be reflected on successful / erroneous action execution.

Since appearance of Title actions on Entity Master we have developed quite sofisticated refreshing mechanism of parent Entity Masters / Centres down to the very root of hierarchy. In case of successful approval the user would see This property has been recently changed. warning on approveDate property. This warning, however, does not add any value for the user as it is them who made the change. Also, CertificationRevalidation may even become not editable after approval and this can trigger error Certification Revalidation is approved - no changes allowed. (see #1777 for more).

To avoid these potential warnings / errors we can use BindSavedPropertyPostActionSuccess/Error post actions with the idea to capture master entity being saved in a producer of the action and then use that master entity for actual alteration and saving. So, how can this be implemented?

At first, add certificationRevalidationToBind property to ApproveCertificationRevalidationAction to hold the entity being saved.

    @IsProperty
    @SkipEntityExistsValidation // only add this annotation iff newly created entity can be saved and immediately approved
    private CertificationRevalidation certificationRevalidationToBind;

Even though we added certificationRevalidationToBind property, this property will not be used in ApproveCertificationRevalidationAction master (it is actually no-UI master). That's why no change to ApproveCertificationRevalidationActionCo.FETCH_PROVIDER is needed.

Then we need to populate certificationRevalidationToBind property. Definitely, we shouldn't execute action against invalid entity. Please, check its validity as early as possible. Also, please ask yourself whether user initiated changes should be saved as a whole with actual approval process. If not, please check the dirtiness of the entity and throw an error.

    @Override
    @Authorise(ApproveCertificationRevalidationAction_CanExecute_Token.class)
    protected ApproveCertificationRevalidationAction provideDefaultValues(final ApproveCertificationRevalidationAction entity) {
        if (masterEntityInstanceOf(CertificationRevalidation.class)) {
            // masterEntity() must be an instrumented instance from parent Entity Master
            final CertificationRevalidation masterEntity = masterEntity(CertificationRevalidation.class);
            // make sure that masterEntity is valid before continuing
            masterEntity.isValid().ifFailure(Result::throwRuntime);

            // [optional] if master entity is dirty, we may need to encourage the user to save or cancel their changes
            if (masterEntity.isDirty()) {
                 throw Result.failure("Please save or cancel changes before using this action.");
            }

            // set exact instance taken from parent master;
            // this instance will be bound to parent master in case of action error on save
            entity.setCertificationRevalidationToBind(masterEntity);
        }
        return super.provideDefaultValues(entity);
    }

The next step would be to do some manipulations with certificationRevalidationToBind property value (e.g. take care of approveDate) and actually save it.

    @Override
    @SessionRequired
    @Authorise(ApproveCertificationRevalidationAction_CanExecute_Token.class)
    public ApproveCertificationRevalidationAction save(final ApproveCertificationRevalidationAction action) {
        final CertificationRevalidation masterEntity = action.getCertificationRevalidation();

        // validate as early as possible to avoid unnecessary checks for already invalid instance;
        // the same validation is done in producer, however that is UI logic and better to be sure to perform the same in model-driven code
        masterEntity.isValid().ifFailure(Result::throwRuntime);

        masterEntity.setApproveDate(now().toDate());
        // ... other alterations

        // actually save master entity
        final CertificationRevalidation savedMasterEntity = co$(CertificationRevalidation.class).save(masterEntity);
        // it is important to set the same (in terms of equals()) entity with enforcement;
        // otherwise previous unsaved version will be preserved and it would not be suitable for parent entity master binding
        action.getProperty("certificationRevalidationToBind").setValue(savedMasterEntity, true);

        return super.save(action);
    }

And, finally, need to specify which property will be used to get binding entity for parent master.

    /**
     * Creates ApproveCertificationRevalidationAction to be used on CertificationRevalidation master.
     */
    public static EntityActionConfig createApproveAction() {
        return action(ApproveCertificationRevalidationAction.class)
            .withContext(context().withMasterEntity().build())
            // setting "certificationRevalidationToBind" in companion's `save` is required for successful binding:
            .postActionSuccess(new BindSavedPropertyPostActionSuccess("certificationRevalidationToBind"))
            // setting "certificationRevalidationToBind" in producer's `provideDefaultValues` is required for erroneous binding:
            .postActionError(new BindSavedPropertyPostActionError("certificationRevalidationToBind"))
            .shortDesc("Approve")
            .longDesc(format("Approve %s", CertificationRevalidation.ENTITY_TITLE))
            .build();
    }

What about the case of action, that has its UI and shows master entity as one of its properties? Consider example where RenumberVehicleAction should show parent Vehicle in RenumberVehicleAction master. In this case we want to have vehicle and vehicleToBind properties separate. vehicle property would need to be specified in RenumberVehicleActionCo.FETCH_PROVIDER as usually and added to master configuration. vehicleToBind property flow is to be exactly as described above (in ApproveCertificationRevalidationAction case).

Please note, that existence of UI master may require refetching of vehicleToBind property and validation for conflicts. This is because user may open an action and go for a walk / coffee without actually executing it.

    final Vehicle vehicleToBindRefetched = co$Vehicle.findByKeyAndFetch(VehicleCo.FETCH_MODEL, action.getVehicleToBind().getNumber());
    // ... validate vehicleToBindRefetched / action.getVehicleToBind() for conflicts if necessary
    vehicleToBindRefetched.setNumber(action.getNewNumber());
    final Vehicle renumberedVehicle = co$Vehicle.save(vehicleToBindRefetched);
    // it is important to set the same (in terms of equals()) entity with enforcement;
    // otherwise previous unsaved version will be preserved and it would not be suitable for parent entity master binding
    action.getProperty("vehicleToBind").setValue(renumberedVehicle, true);

Some another interesting example of parent master entity binding is where action is defined as EntityNewAction for some persistent entity. For example, we add PositionAllocation as + action on Vehicle.companyPosition editor. Creation of PositionAllocation may affect Vehicle itself (e.g. its usage property). In this case BindSavedPropertyPostActionSuccess/Error can not be used, but we can remedy warnings on usage/companyPosition editors using the following approach (not ideal though):

    .addProp(Vehicle_.companyPosition()).asAutocompleter().withAction(mkAddNewPropAction(PositionAllocation.class, format("""
        // only consider embedded PositionAllocation master successful save, not EntityNewAction master
        if (functionalEntity.type().fullClassName() === '%s') {
            // Vehicle.usage may be changed on PositionAllocation save;
            // increase client-side Vehicle version to avoid self-conflict checks in case
            //   if Vehicle.usage was indeed changed and Vehicle version indeed increased
            self._currBindingEntity['version'] += 1;
        }
    """, PositionAllocation.class.getName()))).also()
Clone this wiki locally