Skip to content

Commit

Permalink
feat(edit-content) : Allowing Users to add separators between workflo…
Browse files Browse the repository at this point in the history
…w actions (#26390)

* feat(Workflows) : Allowing Users to add separators between actions inside a Workflow Step.

* Code refactoring. Adding Upgrade Task to create a new column: `metadata` in the `workflow_action` table.

* Adding some Javadoc.

* Adding Integration Test and minor changes.

* Implementing SonarQube feedback.

* Implementing SonarQube feedback.

* Implementing SonarQube feedback.

* Implementing SonarQube feedback.

* Fixing Upgrade Task's name so that it will get executed correctly.

* Missing Java import statement.

* Implementing SonarQube feedback.

* Implementing SonarQube feedback.

* dev: fix divider button

* dev: prevent the SEPARATOR action from being displayed and editing

* Style: set default cursor when action is not editable (SEPARATOR)

* Avoid displaying the `Separator` action in the LISTING rendering mode. Including the `metadata` attribute in the REST Endpoint.

---------

Co-authored-by: Rafael Velazco <rjvelazco21@gmail.com>
Co-authored-by: Freddy Montes <751424+fmontes@users.noreply.github.com>
  • Loading branch information
3 people authored Dec 13, 2023
1 parent c90eb48 commit ba01ad9
Show file tree
Hide file tree
Showing 20 changed files with 711 additions and 465 deletions.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import com.dotcms.contenttype.model.field.Field;
import com.dotcms.contenttype.model.type.ContentType;
import com.dotcms.contenttype.transform.field.LegacyFieldTransformer;
import com.dotcms.exception.ExceptionUtil;
import com.dotcms.mock.response.MockHttpResponse;
import com.dotcms.repackage.com.google.common.annotations.VisibleForTesting;
import com.dotcms.repackage.javax.validation.constraints.NotNull;
Expand Down Expand Up @@ -418,15 +419,20 @@ public final Response findStepsByScheme(@Context final HttpServletRequest reques
} // findSteps.

/**
* Finds the available actions for an inode
* Finds the Workflow Actions that are available for a specific Contentlet Inode. Here's an
* example of how you can use this method:
* <pre>
* GET http://localhost:8080/api/v1/workflow/contentlet/{CONTENTLET-INODE}/actions?renderMode={editing|listing}
* </pre>
*
* @param request HttpServletRequest
* @param inode String
* @param renderMode String, this is an uncase sensitive query string (?renderMode=) optional parameter.
* By default the findAvailableAction will run on WorkflowAPI.RenderMode.EDITING, if you want to run for instance on WorkflowAPI.RenderMode.LISTING
* you can send the renderMode parameter as ?renderMode=listing
* This will be used to filter the action based on the show on configuration for each action.
* @return Response
* @param request The current instance of the {@link HttpServletRequest}.
* @param inode The Inode of the Contentlet.
* @param renderMode This is a case-insensitive optional parameter. By default, this method will
* run EDITING rendering mode. The available modes are specified via the
* {{@link #validRenderModeSet}} variable.
*
* @return Response A {@link Response} object that contains the available actions for the
* specified Contentlet.
*/
@GET
@Path("/contentlet/{inode}/actions")
Expand All @@ -446,13 +452,15 @@ public final Response findAvailableActions(@Context final HttpServletRequest req
this.workflowHelper.checkRenderMode (renderMode, initDataObject.getUser(), this.validRenderModeSet);

final List<WorkflowAction> actions = this.workflowHelper.findAvailableActions(inode, initDataObject.getUser(),
LISTING.equalsIgnoreCase(renderMode)?WorkflowAPI.RenderMode.LISTING:WorkflowAPI.RenderMode.EDITING);
LISTING.equalsIgnoreCase(renderMode)
? WorkflowAPI.RenderMode.LISTING
: WorkflowAPI.RenderMode.EDITING);
return Response.ok(new ResponseEntityView<>(actions.stream()
.map(this::toWorkflowActionView).collect(Collectors.toList()))).build(); // 200
} catch (Exception e) {
Logger.error(this.getClass(),
"Exception on findAvailableActions, contentlet inode: " + inode +
", exception message: " + e.getMessage(), e);
.map(this::toWorkflowActionView).collect(Collectors.toList()))).build();
} catch (final Exception e) {
Logger.error(this.getClass(), String.format("An error occurred when finding available" +
" Workflow Actions for Contentlet Inode '%s' in mode '%s': %s", inode, renderMode,
ExceptionUtil.getErrorMessage(e)), e);
return ResponseUtil.mapExceptionResponse(e);
}
} // findAvailableActions.
Expand All @@ -462,10 +470,18 @@ private WorkflowActionView toWorkflowActionView(final WorkflowAction workflowAct
return convertToWorkflowActionView(workflowAction);
}

/**
* Takes the information from a Workflow Action and transforms it into a View object that can
* display it in JSON notation appropriately. Keep in mind that any new property you add to the
* Workflow Action class will need to be added here as well.
*
* @param workflowAction The {@link WorkflowAction} that will be transformed.
*
* @return The {@link WorkflowActionView} that contains the information from the Workflow
* Action.
*/
public static WorkflowActionView convertToWorkflowActionView(final WorkflowAction workflowAction) {

final WorkflowActionView workflowActionView = new WorkflowActionView();

workflowActionView.setId(workflowAction.getId());
workflowActionView.setName(workflowAction.getName());
workflowActionView.setStepId(workflowAction.getSchemeId());
Expand All @@ -489,7 +505,7 @@ public static WorkflowActionView convertToWorkflowActionView(final WorkflowActio
workflowActionView.setDestroyActionlet(workflowAction.hasDestroyActionlet());
workflowActionView.setShowOn(workflowAction.getShowOn());
workflowActionView.setActionInputs(createActionInputViews(workflowAction));

workflowActionView.setMetadata(workflowAction.getMetadata());
return workflowActionView;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,29 @@

import com.dotcms.repackage.javax.validation.constraints.NotNull;
import com.dotcms.rest.api.Validated;
import com.dotmarketing.business.APILocator;
import com.dotmarketing.business.Role;
import com.dotmarketing.exception.DotRuntimeException;
import com.dotmarketing.portlets.workflows.model.WorkflowAction;
import com.dotmarketing.portlets.workflows.model.WorkflowState;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import io.vavr.control.Try;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
* This class represents a Workflow Action Form used by different parts of the system, such as REST
* Endpoints. It is used to create, update and delete Workflow Actions in dotCMS.
*
* @author Jonathan Sanchez
* @since Dec 6th, 2017
*/
@JsonDeserialize(builder = WorkflowActionForm.Builder.class)
public class WorkflowActionForm extends Validated {

Expand All @@ -17,7 +33,7 @@ public class WorkflowActionForm extends Validated {
@NotNull
private final String schemeId;

// you can send an optional stepId for a new Action when you want to associated the action to the step in the same transaction.
// You can send an optional stepId for a new Action when you want to associate it to the step in the same transaction.
private final String stepId;

@NotNull
Expand All @@ -39,6 +55,8 @@ public class WorkflowActionForm extends Validated {
private final String actionNextStep;
private final String actionNextAssign;
private final String actionCondition;
private static final String METADATA_SUBTYPE_ATTR = "subtype";
private final Map<String, Object> metadata;

public String getStepId() {
return stepId;
Expand Down Expand Up @@ -100,6 +118,10 @@ public String getActionCondition() {
return actionCondition;
}

public Map<String, Object> getMetadata() {
return this.metadata;
}

@Override
public String toString() {
return "WorkflowActionForm{" +
Expand All @@ -118,6 +140,7 @@ public String toString() {
", actionNextStep='" + actionNextStep + '\'' +
", actionNextAssign='" + actionNextAssign + '\'' +
", actionCondition='" + actionCondition + '\'' +
", metadata='" + metadata + '\'' +
'}';
}

Expand All @@ -138,10 +161,11 @@ public WorkflowActionForm(final Builder builder) {
this.actionAssignable = builder.actionAssignable;
this.actionRoleHierarchyForAssign = builder.actionRoleHierarchyForAssign;
this.roleHierarchyForAssign = (actionAssignable && actionRoleHierarchyForAssign);
this.metadata = builder.metadata;
this.checkValid();
}

public static final class Builder {
public static final class Builder {

@JsonProperty()
private String actionId;
Expand All @@ -163,9 +187,14 @@ public static final class Builder {
@JsonProperty(required = true)
private boolean actionCommentable;

/**
* @deprecated This attribute is not necessary as a single workflow action can be available
* for locked and/or unlocked content now. See
* <a href="https://github.com/dotCMS/core/issues/13287">#13287</a>
*/
@Deprecated
@JsonProperty(required = true)
private boolean requiresCheckout;
private boolean requiresCheckout = false;
@JsonProperty(required = true)
private boolean actionRoleHierarchyForAssign;
@JsonProperty(required = true)
Expand All @@ -177,7 +206,8 @@ public static final class Builder {

@JsonProperty(required = true)
private Set<WorkflowState> showOn;

@JsonProperty()
private Map<String, Object> metadata;

public Builder showOn(Set<WorkflowState> showOn) {
this.showOn = showOn;
Expand Down Expand Up @@ -250,8 +280,53 @@ public Builder actionCondition(String actionCondition) {
return this;
}

/**
* Sets the metadata for this Workflow Action. This is a Map of key/value pairs that may
* include different custom properties that define the behavior of an action.
*
* @param metadata Different custom properties for this action.
*
* @return The current {@link Builder} instance.
*/
public Builder metadata(final Map<String, Object> metadata) {
this.metadata = metadata;
return this;
}

/**
* Marks this Workflow Action as a Separator. This is a special type of action that does
* not execute any sub-actions at all, as it simply groups X number of actions together
* in the UI. The result of this may be seen as the differentiation between Primary and
* Secondary Actions.
*
* @param schemeId The ID of the Workflow Scheme that this action belongs to.
* @param stepId The ID of the Workflow Step that this action belongs to.
*
* @return The current {@link Builder} instance.
*/
public Builder separator(final String schemeId, final String stepId) {
this.schemeId(schemeId);
this.stepId(stepId);
this.actionName(WorkflowAction.SEPARATOR);
this.actionAssignable(false);
this.actionCommentable(false);
this.actionRoleHierarchyForAssign(false);
this.actionNextStep(WorkflowAction.CURRENT_STEP);
this.actionNextAssign(Try.of(() -> APILocator.getRoleAPI().loadRoleByKey(Role.CMS_ANONYMOUS_ROLE).getId())
.getOrElseThrow(e -> new DotRuntimeException("Anonymous Role ID not found in the database", e)));
this.actionCondition(WorkflowAction.SEPARATOR);
this.showOn(Arrays.stream(WorkflowState.values()).filter(state -> state != WorkflowState.LISTING).collect(java.util.stream.Collectors.toSet()));
if (null == this.metadata) {
this.metadata = new HashMap<>();
}
this.metadata.put(METADATA_SUBTYPE_ATTR, WorkflowAction.SEPARATOR);
return this;
}

public WorkflowActionForm build() {
return new WorkflowActionForm(this);
}

}

}
35 changes: 21 additions & 14 deletions dotCMS/src/main/java/com/dotcms/workflow/helper/WorkflowHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -1536,11 +1536,16 @@ private ContentType findContentType (final String variable, final User user)
}

/**
* Save a WorkflowActionForm returning the WorkflowAction created.
* A WorkflowActionForm can send a stepId in that case the Action will be associated to the Step in the same transaction.
* @param actionId When present an update operation takes place otherwise an insert is executed
* @param workflowActionForm WorkflowActionForm
* @return WorkflowAction (workflow action created)
* Saves a Workflow Action. A {@link WorkflowActionForm} object can send a Step ID, in which
* case the Action will be associated to the Step in the same transaction.
*
* @param actionId If present, an update operation takes place. Otherwise, an insert
* is executed.
* @param workflowActionForm The {@link WorkflowActionForm} object with the Workflow Action data
* that will be saved.
* @param user The {@link User} that is performing this action.
*
* @return The {@link WorkflowAction} object that was created.
*/
@WrapInTransaction
public WorkflowAction saveAction(final String actionId, final WorkflowActionForm workflowActionForm, final User user) {
Expand All @@ -1564,7 +1569,7 @@ public WorkflowAction saveAction(final String actionId, final WorkflowActionForm
newAction.setRequiresCheckout(false);
newAction.setShowOn(workflowActionForm.getShowOn());
newAction.setRoleHierarchyForAssign(workflowActionForm.isRoleHierarchyForAssign());

newAction.setMetadata(workflowActionForm.getMetadata());
try {

newAction.setNextAssign(this.resolveRole(actionNextAssign).getId());
Expand Down Expand Up @@ -1607,17 +1612,19 @@ public WorkflowAction saveAction(final String actionId, final WorkflowActionForm
workflowActionClass.setName(NotifyAssigneeActionlet.class.getDeclaredConstructor().newInstance().getName());
workflowActionClass.setOrder(0);
this.workflowAPI.saveActionClass(workflowActionClass, user);
} catch (Exception e) {
Logger.error(this.getClass(), e.getMessage());
Logger.debug(this, e.getMessage(), e);
throw new DotWorkflowException(e.getMessage(), e);
} catch (final Exception e) {
final String errorMsg = String.format("Failed to save Workflow Action Class with ID '%s': %s", newAction.getId(), ExceptionUtil.getErrorMessage(e));
Logger.error(this.getClass(), errorMsg);
Logger.debug(this, errorMsg, e);
throw new DotWorkflowException(errorMsg, e);
}
});
}
} catch (Exception e) {
Logger.error(this.getClass(), e.getMessage());
Logger.debug(this, e.getMessage(), e);
throw new DotWorkflowException(e.getMessage(), e);
} catch (final Exception e) {
final String errorMsg = String.format("Failed to save Workflow Action '%s': %s", actionId, ExceptionUtil.getErrorMessage(e));
Logger.error(this.getClass(), errorMsg);
Logger.debug(this, errorMsg, e);
throw new DotWorkflowException(errorMsg, e);
}

return newAction;
Expand Down
Loading

0 comments on commit ba01ad9

Please sign in to comment.