Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Nu-5490] Expand Custom Action with display policy #5491

Closed
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import i18next from "i18next";
import { SwitchTransition } from "react-transition-group";
import { useSelector } from "react-redux";
import { RootState } from "../../../reducers";
import { getScenario, getProcessUnsavedNewName, isProcessRenamed } from "../../../reducers/selectors/graph";
import { getScenario, getProcessUnsavedNewName, isProcessRenamed, getProcessVersionId } from "../../../reducers/selectors/graph";
import { getProcessState } from "../../../reducers/selectors/scenarioState";
import { getCustomActions } from "../../../reducers/selectors/settings";
import { CssFade } from "../../CssFade";
Expand All @@ -28,6 +28,7 @@ const ProcessInfo = memo(({ id, buttonsVariant, children }: ToolbarPanelProps) =
const unsavedNewName = useSelector((state: RootState) => getProcessUnsavedNewName(state));
const processState = useSelector((state: RootState) => getProcessState(state));
const customActions = useSelector((state: RootState) => getCustomActions(state));
const versionId = useSelector(getProcessVersionId);

const description = ProcessStateUtils.getStateDescription(scenario, processState);
const transitionKey = ProcessStateUtils.getTransitionKey(scenario, processState);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { ComponentType } from "react";
import React, {ComponentType, useEffect, useState} from "react";
import { useTranslation } from "react-i18next";
import DefaultIcon from "../../../../assets/img/toolbarButtons/custom_action.svg";
import { CustomAction } from "../../../../types";
Expand All @@ -9,6 +9,9 @@ import { ToolbarButton } from "../../../toolbarComponents/toolbarButtons";
import { ToolbarButtonProps } from "../../types";
import UrlIcon from "../../../UrlIcon";
import { FallbackProps } from "react-error-boundary";
import {useSelector} from "react-redux";
import {getProcessVersionId} from "../../../../reducers/selectors/graph";
import {resolveCustomActionDisplayability} from "../../../../helpers/customActionDisplayabilityResolver";

type CustomActionProps = {
action: CustomAction;
Expand All @@ -28,7 +31,9 @@ export default function CustomActionButton(props: CustomActionProps) {
);

const statusName = processStatus?.name;
const available = !disabled && action.allowedStateStatusNames.includes(statusName);
const available = !disabled &&
action.allowedStateStatusNames.includes(statusName) &&
resolveCustomActionDisplayability(action.displayPolicy);

const toolTip = available
? null
Expand Down
19 changes: 19 additions & 0 deletions designer/client/src/helpers/customActionDisplayabilityResolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {CustomActionDisplayPolicy} from "../types";
import {useSelector} from "react-redux";
import {getProcessName, getProcessVersionId, getScenario} from "../reducers/selectors/graph";
import {ActionType} from "../components/Process/types";

export function resolveCustomActionDisplayability(displayPolicy: CustomActionDisplayPolicy){
const processVersionId = useSelector(getProcessVersionId);
const scenario = useSelector(getScenario);

switch(displayPolicy.type) {
case "UICustomActionDisplaySimplePolicy":
const { version, operator, expr } = displayPolicy;
return false;
case "UICustomActionDisplayConditionalPolicy":
return false;
default:
return false;
}
}
31 changes: 31 additions & 0 deletions designer/client/src/types/scenarioGraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,40 @@ export type ProcessAdditionalFields = {
metaDataType: string;
};

export type StatusExpr = {
type: 'UIStatusExpr';
status: string;
};

export type NodeExpr = {
type: 'UINodeExpr';
node: string;
};

type CustomActionDisplayPolicyExpr = StatusExpr | NodeExpr;

// CustomActionDisplayPolicy ADT
export type CustomActionDisplaySimplePolicy = {
type: 'UICustomActionDisplaySimplePolicy';
version: number;
operator: string;
expr: CustomActionDisplayPolicyExpr;
};

export type CustomActionDisplayConditionalPolicy = {
type: 'UICustomActionDisplayConditionalPolicy';
condition: string;
operands: CustomActionDisplayPolicy[];
};

export type CustomActionDisplayPolicy =
| CustomActionDisplaySimplePolicy
| CustomActionDisplayConditionalPolicy;

export type CustomAction = {
name: string;
allowedStateStatusNames: Array<string>;
displayPolicy?: CustomActionDisplayPolicy;
icon?: string;
parameters?: Array<CustomActionParameter>;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,22 @@ package pl.touk.nussknacker.restmodel
import io.circe.generic.JsonCodec
import io.circe.generic.extras.semiauto.{deriveConfiguredDecoder, deriveConfiguredEncoder}
import io.circe.{Decoder, Encoder}
import pl.touk.nussknacker.engine.api.component.ComponentType.ComponentType
import pl.touk.nussknacker.engine.api.component.{ComponentGroupName, ComponentId}
import pl.touk.nussknacker.engine.api.definition.ParameterEditor
import pl.touk.nussknacker.engine.api.deployment.CustomAction
import pl.touk.nussknacker.engine.api.deployment.{
CustomAction,
CustomActionDisplayConditionalPolicy,
CustomActionDisplayPolicy,
CustomActionDisplaySimplePolicy,
NodeExpr,
StatusExpr
}
import pl.touk.nussknacker.engine.api.typed.typing.TypingResult
import pl.touk.nussknacker.engine.graph.EdgeType
import pl.touk.nussknacker.engine.graph.evaluatedparam.{Parameter => NodeParameter}
import pl.touk.nussknacker.engine.graph.expression.Expression
import pl.touk.nussknacker.engine.graph.node.NodeData
import io.circe.generic.extras.{Configuration, ConfiguredJsonCodec}

import java.net.URI

Expand Down Expand Up @@ -135,19 +142,58 @@ package object definition {
def apply(action: CustomAction): UICustomAction = UICustomAction(
name = action.name,
allowedStateStatusNames = action.allowedStateStatusNames,
displayPolicy = action.displayPolicy.map(UICustomActionDisplayPolicy.fromCustomActionDisplayPolicy),
icon = action.icon,
parameters = action.parameters.map(p => UICustomActionParameter(p.name, p.editor))
)

}

implicit val configuration: Configuration = Configuration.default.withDefaults.withDiscriminator("type")

@JsonCodec final case class UICustomActionParameter(name: String, editor: ParameterEditor)

@ConfiguredJsonCodec sealed trait UICustomActionDisplayPolicyExpr

@JsonCodec case class UIStatusExpr(status: String) extends UICustomActionDisplayPolicyExpr
@JsonCodec case class UINodeExpr(node: String) extends UICustomActionDisplayPolicyExpr

@ConfiguredJsonCodec sealed trait UICustomActionDisplayPolicy

@JsonCodec case class UICustomActionDisplaySimplePolicy(
version: Long,
operator: String,
expr: UICustomActionDisplayPolicyExpr
) extends UICustomActionDisplayPolicy

object UICustomActionDisplayPolicy {

def fromCustomActionDisplayPolicy(displayPolicy: CustomActionDisplayPolicy): UICustomActionDisplayPolicy =
displayPolicy match {
case CustomActionDisplaySimplePolicy(version, operator, NodeExpr(node)) =>
UICustomActionDisplaySimplePolicy(version, operator, UINodeExpr(node))

case CustomActionDisplaySimplePolicy(version, operator, StatusExpr(status)) =>
UICustomActionDisplaySimplePolicy(version, operator, UIStatusExpr(status))

case CustomActionDisplayConditionalPolicy(condition, operands) =>
val uiOperands = operands.map(fromCustomActionDisplayPolicy)
UICustomActionDisplayConditionalPolicy(condition, uiOperands)
}

}

@JsonCodec case class UICustomActionDisplayConditionalPolicy(
condition: String,
operands: List[UICustomActionDisplayPolicy]
) extends UICustomActionDisplayPolicy

@JsonCodec final case class UICustomAction(
name: String,
allowedStateStatusNames: List[String],
displayPolicy: Option[UICustomActionDisplayPolicy],
icon: Option[URI],
parameters: List[UICustomActionParameter]
)

@JsonCodec final case class UICustomActionParameter(name: String, editor: ParameterEditor)

}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package pl.touk.nussknacker.engine.api.deployment

import pl.touk.nussknacker.engine.api.ProcessVersion
import pl.touk.nussknacker.engine.api.context.ProcessCompilationError.CustomParameterValidationError
import pl.touk.nussknacker.engine.api.definition.ParameterEditor
import pl.touk.nussknacker.engine.deployment.User

import scala.util.{Failure, Success, Try}
import java.net.URI

/*
Expand All @@ -22,6 +24,7 @@ case class CustomAction(
name: String,
// We cannot use "engine.api.deployment.StateStatus" because it can be implemented as a class containing nonconstant attributes
allowedStateStatusNames: List[String],
displayPolicy: Option[CustomActionDisplayPolicy] = None,
parameters: List[CustomActionParameter] = Nil,
icon: Option[URI] = None
)
Expand All @@ -32,3 +35,62 @@ case class CustomActionParameter(name: String, editor: ParameterEditor)
case class CustomActionRequest(name: String, processVersion: ProcessVersion, user: User, params: Map[String, String])

case class CustomActionResult(req: CustomActionRequest, msg: String)

sealed trait CustomActionDisplayPolicy
sealed trait CustomActionDisplayPolicyExpr
case class StatusExpr(status: String) extends CustomActionDisplayPolicyExpr
case class NodeExpr(node: String) extends CustomActionDisplayPolicyExpr
case class CustomActionDisplaySimplePolicy(version: Long, operator: String, expr: CustomActionDisplayPolicyExpr)
extends CustomActionDisplayPolicy
case class CustomActionDisplayConditionalPolicy(condition: String, operands: List[CustomActionDisplayPolicy])
extends CustomActionDisplayPolicy
class CustomActionDisplayPolicyError(msg: String) extends IllegalArgumentException(msg)

object CustomActionDisplayPolicy {

class CustomActionDisplayPolicyBuilder private (
private val version: Option[Long] = None,
private val operator: Option[String] = None,
private val expr: Option[CustomActionDisplayPolicyExpr] = None,
private val condition: Option[String] = None,
private val operands: List[CustomActionDisplayPolicy] = List()
) {
def withVersion(version: Long): CustomActionDisplayPolicyBuilder =
new CustomActionDisplayPolicyBuilder(version = Some(version), operator, expr, condition, operands)

def withOperator(operator: String): CustomActionDisplayPolicyBuilder =
if (operator == "is" || operator == "contains") {
new CustomActionDisplayPolicyBuilder(version, operator = Some(operator), expr, condition, operands)
} else {
throw new CustomActionDisplayPolicyError("Operator must be one of ['is', 'contains']")
}

def withExpr(expr: CustomActionDisplayPolicyExpr): CustomActionDisplayPolicyBuilder =
new CustomActionDisplayPolicyBuilder(version, operator, expr = Some(expr), condition, operands)

def withCondition(condition: String): CustomActionDisplayPolicyBuilder =
if (condition == "OR" || condition == "AND") {
new CustomActionDisplayPolicyBuilder(version, operator, expr, condition = Some(condition), operands)
} else {
throw new CustomActionDisplayPolicyError("Operator must be one of ['OR', 'AND']")
}

def withOperands(operands: CustomActionDisplayPolicy*): CustomActionDisplayPolicyBuilder =
new CustomActionDisplayPolicyBuilder(version, operator, expr, condition, operands = operands.toList)

def buildSimplePolicy(): CustomActionDisplaySimplePolicy =
CustomActionDisplaySimplePolicy(
version.getOrElse(throw new CustomActionDisplayPolicyError("version not set")),
operator.getOrElse(throw new CustomActionDisplayPolicyError("operator not set")),
expr.getOrElse(throw new CustomActionDisplayPolicyError("expr not set"))
)

def buildConditionalPolicy(): CustomActionDisplayConditionalPolicy =
CustomActionDisplayConditionalPolicy(
condition.getOrElse(throw new CustomActionDisplayPolicyError("condition not set")),
if (operands.nonEmpty) operands else throw new CustomActionDisplayPolicyError("operands list is empty")
)

}

}
Loading