From fe9f5709599ad7f720725477ea3b90e31bede638 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luiz=20Jo=C3=A3o=20Motta?= Date: Fri, 3 May 2024 09:57:58 -0300 Subject: [PATCH] kie-issues#1019: React-Based DMN Editor: allowedValues not allowed for custom type (#2274) --- .../dmn-editor/src/dataTypes/Constraints.tsx | 481 +++++++++++------- .../src/dataTypes/ConstraintsEnum.tsx | 3 +- .../src/dataTypes/ConstraintsExpression.tsx | 45 +- .../src/dataTypes/ConstraintsRange.tsx | 3 +- .../src/dataTypes/DataTypePanel.tsx | 1 + .../dmn-editor/src/dataTypes/DataTypes.tsx | 2 +- .../src/dataTypes/ItemComponentsTable.tsx | 34 +- .../src/dataTypes/TypeRefSelector.tsx | 35 +- .../Fields.tsx | 2 +- 9 files changed, 393 insertions(+), 213 deletions(-) diff --git a/packages/dmn-editor/src/dataTypes/Constraints.tsx b/packages/dmn-editor/src/dataTypes/Constraints.tsx index 812bb9c801d..9a2b93230af 100644 --- a/packages/dmn-editor/src/dataTypes/Constraints.tsx +++ b/packages/dmn-editor/src/dataTypes/Constraints.tsx @@ -18,7 +18,7 @@ */ import * as React from "react"; -import { useMemo, useCallback } from "react"; +import { useMemo, useCallback, useState, useEffect } from "react"; import { ConstraintsExpression } from "./ConstraintsExpression"; import { DMN15__tItemDefinition, @@ -28,9 +28,9 @@ import { DmnBuiltInDataType, generateUuid } from "@kie-tools/boxed-expression-co import { ConstraintsEnum, isEnum } from "./ConstraintsEnum"; import { ConstraintsRange, isRange } from "./ConstraintsRange"; import { KIE__tConstraintType } from "@kie-tools/dmn-marshaller/dist/schemas/kie-1_0/ts-gen/types"; -import { EditItemDefinition } from "./DataTypes"; +import { DataTypeIndex, EditItemDefinition } from "./DataTypes"; import { ToggleGroup, ToggleGroupItem } from "@patternfly/react-core/dist/js/components/ToggleGroup"; -import { constrainableBuiltInFeelTypes } from "./DataTypeSpec"; +import { constrainableBuiltInFeelTypes, isCollection, isStruct } from "./DataTypeSpec"; import moment from "moment"; import { TextInput } from "@patternfly/react-core/dist/js/components/TextInput"; import { ConstraintDate } from "./ConstraintComponents/ConstraintDate"; @@ -46,6 +46,10 @@ import { } from "./ConstraintComponents/ConstraintYearsMonthsDuration"; import { invalidInlineFeelNameStyle } from "../feel/InlineFeelNameInput"; import { ConstraintProps } from "./ConstraintComponents/Constraint"; +import { useDmnEditorStore } from "../store/StoreContext"; +import { useExternalModels } from "../includedModels/DmnEditorDependenciesContext"; +import { UniqueNameIndex } from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/Dmn15Spec"; +import { builtInFeelTypeNames } from "./BuiltInFeelTypes"; export type TypeHelper = { check: (value: string) => boolean; @@ -56,6 +60,7 @@ export type TypeHelper = { }; export interface ConstraintComponentProps { + id: string; isReadonly: boolean; value?: string; expressionValue?: string; @@ -73,31 +78,71 @@ enum ConstraintsType { NONE = "None", } -export const constraintTypeHelper = (typeRef: DmnBuiltInDataType): TypeHelper => { - return { +// Recurse the `itemDefinition` until find `typeRef` attribute +// that is part of the built in FEEL types. +// If the found `itemDefinition` is a collection, it will have a early stop. +export function recursivelyGetRootItemDefinition( + itemDefinition: DMN15__tItemDefinition, + allDataTypesById: DataTypeIndex, + allTopLevelItemDefinitionUniqueNames: UniqueNameIndex +): DMN15__tItemDefinition { + const typeRef: DmnBuiltInDataType = + (itemDefinition.typeRef?.__$$text as DmnBuiltInDataType) ?? DmnBuiltInDataType.Undefined; + + if (builtInFeelTypeNames.has(typeRef) === false) { + const parentDataType = allDataTypesById.get(allTopLevelItemDefinitionUniqueNames.get(typeRef) ?? ""); + if (parentDataType !== undefined && isCollection(parentDataType.itemDefinition)) { + // Parent `itemDefinition` is a collection. Early stop. + return parentDataType.itemDefinition; + } else if (parentDataType !== undefined) { + return recursivelyGetRootItemDefinition( + parentDataType.itemDefinition, + allDataTypesById, + allTopLevelItemDefinitionUniqueNames + ); + } + // Something wrong. Caller `itemDefinition` isn't a built-in FEEL type and doesn't have parent. + return itemDefinition; + } + // Caller `itemDefinition` is a built-in FEEL type + return itemDefinition; +} + +export const constraintTypeHelper = ( + itemDefinition: DMN15__tItemDefinition, + allDataTypesById?: DataTypeIndex, + allTopLevelItemDefinitionUniqueNames?: UniqueNameIndex +): TypeHelper => { + const typeRef = + (allDataTypesById !== undefined && allTopLevelItemDefinitionUniqueNames !== undefined + ? recursivelyGetRootItemDefinition(itemDefinition, allDataTypesById, allTopLevelItemDefinitionUniqueNames).typeRef + ?.__$$text + : itemDefinition.typeRef?.__$$text) ?? DmnBuiltInDataType.Undefined; + + const typeHelper = { // check if the value has the correct type - check: (value: string) => { - const recoveredValue = constraintTypeHelper(typeRef).recover(value); - switch (typeRef) { + check: (value: string, type?: DmnBuiltInDataType) => { + const recoveredValue = typeHelper.recover(value); + switch (type ?? typeRef) { case DmnBuiltInDataType.Any: return true; case DmnBuiltInDataType.String: if (recoveredValue === "") { return true; } - if (constraintTypeHelper(DmnBuiltInDataType.Date).check(value)) { + if (typeHelper.check(value, DmnBuiltInDataType.Date)) { return false; } - if (constraintTypeHelper(DmnBuiltInDataType.DateTime).check(value)) { + if (typeHelper.check(value, DmnBuiltInDataType.DateTime)) { return false; } - if (constraintTypeHelper(DmnBuiltInDataType.DateTimeDuration).check(value)) { + if (typeHelper.check(value, DmnBuiltInDataType.DateTimeDuration)) { return false; } - if (constraintTypeHelper(DmnBuiltInDataType.Time).check(value)) { + if (typeHelper.check(value, DmnBuiltInDataType.Time)) { return false; } - if (constraintTypeHelper(DmnBuiltInDataType.YearsMonthsDuration).check(value)) { + if (typeHelper.check(value, DmnBuiltInDataType.YearsMonthsDuration)) { return false; } return typeof recoveredValue === "string"; @@ -125,7 +170,7 @@ export const constraintTypeHelper = (typeRef: DmnBuiltInDataType): TypeHelper => // parse the value to the type // useful for comparisons parse: (value: string) => { - const recoveredValue = constraintTypeHelper(typeRef).recover(value); + const recoveredValue = typeHelper.recover(value); switch (typeRef) { case DmnBuiltInDataType.Number: return parseFloat(recoveredValue ?? ""); @@ -163,7 +208,7 @@ export const constraintTypeHelper = (typeRef: DmnBuiltInDataType): TypeHelper => } }, // recover the value before use it - recover: (value?: string) => { + recover: (value: string | undefined) => { if (value === undefined) { return undefined; } @@ -248,41 +293,39 @@ export const constraintTypeHelper = (typeRef: DmnBuiltInDataType): TypeHelper => } }, }; + return typeHelper; }; export function useConstraint({ constraint, itemDefinition, - isCollectionConstraintEnable, + isCollectionConstraintEnabled, + constraintTypeHelper, + enabledConstraints, }: { constraint: DMN15__tUnaryTests | undefined; itemDefinition: DMN15__tItemDefinition; - isCollectionConstraintEnable: boolean; + isCollectionConstraintEnabled: boolean; + constraintTypeHelper: TypeHelper; + enabledConstraints: KIE__tConstraintType[] | undefined; }) { - const constraintValue = useMemo(() => constraint?.text.__$$text, [constraint?.text.__$$text]); - const kieConstraintType = useMemo(() => constraint?.["@_kie:constraintType"], [constraint]); - const isCollection = useMemo(() => itemDefinition["@_isCollection"] ?? false, [itemDefinition]); - const itemDefinitionId = useMemo(() => itemDefinition["@_id"], [itemDefinition]); - - const typeRef: DmnBuiltInDataType = useMemo( - () => (itemDefinition?.typeRef?.__$$text as DmnBuiltInDataType) ?? DmnBuiltInDataType.Undefined, - [itemDefinition?.typeRef?.__$$text] - ); + const constraintValue = constraint?.text.__$$text; + const kieConstraintType = constraint?.["@_kie:constraintType"]; const isConstraintEnum = useMemo( () => - isCollection === true && isCollectionConstraintEnable === true // collection doesn't support enumeration constraint + isCollection(itemDefinition) === true && isCollectionConstraintEnabled === true // collection doesn't support enumeration constraint ? undefined - : isEnum(constraintValue, constraintTypeHelper(typeRef).check), - [constraintValue, isCollectionConstraintEnable, isCollection, typeRef] + : isEnum(constraintValue, constraintTypeHelper.check), + [constraintTypeHelper.check, constraintValue, isCollectionConstraintEnabled, itemDefinition] ); const isConstraintRange = useMemo( () => - isCollection === true && isCollectionConstraintEnable === true // collection doesn't support range constraint + isCollection(itemDefinition) === true && isCollectionConstraintEnabled === true // collection doesn't support range constraint ? undefined - : isRange(constraintValue, constraintTypeHelper(typeRef).check), - [constraintValue, isCollectionConstraintEnable, isCollection, typeRef] + : isRange(constraintValue, constraintTypeHelper.check), + [constraintTypeHelper.check, constraintValue, isCollectionConstraintEnabled, itemDefinition] ); const enumToKieConstraintType: (selection: ConstraintsType) => KIE__tConstraintType | undefined = useCallback( @@ -303,21 +346,20 @@ export function useConstraint({ ); const isConstraintEnabled = useMemo(() => { - const enabledConstraints = constrainableBuiltInFeelTypes.get(typeRef); return { enumeration: - !(isCollection === true && isCollectionConstraintEnable === true) && + !(isCollection(itemDefinition) === true && isCollectionConstraintEnabled === true) && (enabledConstraints ?? []).includes(enumToKieConstraintType(ConstraintsType.ENUMERATION)!), range: - !(isCollection === true && isCollectionConstraintEnable === true) && + !(isCollection(itemDefinition) === true && isCollectionConstraintEnabled === true) && (enabledConstraints ?? []).includes(enumToKieConstraintType(ConstraintsType.RANGE)!), expression: - (isCollection === true && isCollectionConstraintEnable === true) || + (isCollection(itemDefinition) === true && isCollectionConstraintEnabled === true) || (enabledConstraints ?? []).includes(enumToKieConstraintType(ConstraintsType.EXPRESSION)!), }; - }, [typeRef, isCollection, isCollectionConstraintEnable, enumToKieConstraintType]); + }, [enabledConstraints, enumToKieConstraintType, isCollectionConstraintEnabled, itemDefinition]); - const selectedConstraint = useMemo(() => { + const selectedKieConstraintType = useMemo(() => { if (isConstraintEnabled.enumeration && kieConstraintType === "enumeration") { return ConstraintsType.ENUMERATION; } @@ -347,27 +389,14 @@ export function useConstraint({ kieConstraintType, ]); - return useMemo(() => { - return { - constraintValue, - typeRef, - isConstraintEnum, - isConstraintRange, - isConstraintEnabled, - itemDefinitionId, - selectedConstraint, - enumToKieConstraintType, - }; - }, [ + return { constraintValue, - isConstraintEnabled, isConstraintEnum, isConstraintRange, - itemDefinitionId, - selectedConstraint, - typeRef, + isConstraintEnabled, + selectedKieConstraintType, enumToKieConstraintType, - ]); + }; } export function ConstraintsFromAllowedValuesAttribute({ @@ -383,100 +412,127 @@ export function ConstraintsFromAllowedValuesAttribute({ isEnumDisabled?: boolean; isRangeDisabled?: boolean; }) { + const { externalModelsByNamespace } = useExternalModels(); + const allDataTypesById = useDmnEditorStore( + (s) => s.computed(s).getDataTypes(externalModelsByNamespace).allDataTypesById + ); + const allTopLevelItemDefinitionUniqueNames = useDmnEditorStore( + (s) => s.computed(s).getDataTypes(externalModelsByNamespace).allTopLevelItemDefinitionUniqueNames + ); + const allowedValues = useMemo(() => itemDefinition?.allowedValues, [itemDefinition?.allowedValues]); + const itemDefinitionId = itemDefinition["@_id"]!; + const typeRef = (itemDefinition?.typeRef?.__$$text as DmnBuiltInDataType) ?? DmnBuiltInDataType.Undefined; + const typeRefConstraintTypeHelper = useMemo( + () => constraintTypeHelper(itemDefinition, allDataTypesById, allTopLevelItemDefinitionUniqueNames), + [allDataTypesById, allTopLevelItemDefinitionUniqueNames, itemDefinition] + ); + + const rootItemDefinition = useMemo( + () => recursivelyGetRootItemDefinition(itemDefinition, allDataTypesById, allTopLevelItemDefinitionUniqueNames), + [allDataTypesById, allTopLevelItemDefinitionUniqueNames, itemDefinition] + ); + + const enabledConstraints = useMemo( + () => + isStruct(rootItemDefinition) + ? (["expression"] as KIE__tConstraintType[]) + : constrainableBuiltInFeelTypes.get( + (rootItemDefinition.typeRef?.__$$text as DmnBuiltInDataType) ?? DmnBuiltInDataType.Undefined + ), + [rootItemDefinition] + ); + + // Collection constraint on the `allowedValues` must be enabled on cases where `rootItemDefinition` is a collection + const isCollectionConstraintEnable = useMemo(() => { + if (itemDefinitionId !== rootItemDefinition["@_id"]) { + return rootItemDefinition["@_isCollection"] ?? false; + } + return false; + }, [itemDefinitionId, rootItemDefinition]); const { constraintValue, - typeRef, isConstraintEnum, isConstraintRange, isConstraintEnabled, - itemDefinitionId, - selectedConstraint, + selectedKieConstraintType, enumToKieConstraintType, } = useConstraint({ constraint: allowedValues, itemDefinition, - isCollectionConstraintEnable: false, // allowedValues doesn't support constraint to the collection itself + isCollectionConstraintEnabled: isCollectionConstraintEnable, + constraintTypeHelper: typeRefConstraintTypeHelper, + enabledConstraints, }); const onConstraintChange = useCallback( - (value?: string) => { - editItemDefinition(itemDefinitionId!, (itemDefinition) => { - itemDefinition.allowedValues ??= { text: { __$$text: "" } }; - itemDefinition.allowedValues.text.__$$text = value ?? ""; - itemDefinition.allowedValues["@_id"] = itemDefinition.allowedValues?.["@_id"] ?? generateUuid(); - return; + (value: string | undefined, selectedConstraint: ConstraintsType) => { + editItemDefinition(itemDefinitionId, (itemDefinition) => { + if (value === "" || value === undefined) { + itemDefinition.allowedValues = undefined; + } else { + itemDefinition.allowedValues ??= { text: { __$$text: "" } }; + itemDefinition.allowedValues.text.__$$text = value; + itemDefinition.allowedValues["@_id"] = itemDefinition.allowedValues?.["@_id"] ?? generateUuid(); + itemDefinition.allowedValues["@_kie:constraintType"] = enumToKieConstraintType(selectedConstraint); + } }); }, - [editItemDefinition, itemDefinitionId] + [editItemDefinition, enumToKieConstraintType, itemDefinitionId] ); const onToggleGroupChange = useCallback( - (newSelection: boolean, event: React.KeyboardEvent | MouseEvent | React.MouseEvent) => { + (newSelection: boolean, selectedConstraint: ConstraintsType) => { if (!newSelection) { return; } - const selection = event.currentTarget.id as ConstraintsType; - if (selection === ConstraintsType.NONE) { - editItemDefinition(itemDefinitionId!, (itemDefinition) => { + + editItemDefinition(itemDefinitionId, (itemDefinition) => { + if (selectedConstraint === ConstraintsType.NONE) { itemDefinition.allowedValues = undefined; - }); - return; - } + return; + } - editItemDefinition(itemDefinitionId!, (itemDefinition) => { - itemDefinition.allowedValues ??= { text: { __$$text: "" } }; - const previousKieContraintType = itemDefinition.allowedValues["@_kie:constraintType"]; - itemDefinition.allowedValues["@_kie:constraintType"] = enumToKieConstraintType(selection); + if (itemDefinition.allowedValues) { + itemDefinition.allowedValues["@_kie:constraintType"] = enumToKieConstraintType(selectedConstraint); + } - if (selection === ConstraintsType.EXPRESSION) { + if (selectedConstraint === ConstraintsType.EXPRESSION) { return; } if ( - previousKieContraintType === "expression" && - selection === ConstraintsType.ENUMERATION && - isEnum( - itemDefinition.allowedValues.text.__$$text, - constraintTypeHelper( - (itemDefinition?.typeRef?.__$$text as DmnBuiltInDataType) ?? DmnBuiltInDataType.Undefined - ).check - ) + selectedConstraint === ConstraintsType.ENUMERATION && + isEnum(itemDefinition.allowedValues?.text.__$$text, typeRefConstraintTypeHelper.check) ) { return; } if ( - previousKieContraintType === "expression" && - selection === ConstraintsType.RANGE && - isRange( - itemDefinition.allowedValues.text.__$$text, - constraintTypeHelper( - (itemDefinition?.typeRef?.__$$text as DmnBuiltInDataType) ?? DmnBuiltInDataType.Undefined - ).check - ) + selectedConstraint === ConstraintsType.RANGE && + isRange(itemDefinition.allowedValues?.text.__$$text, typeRefConstraintTypeHelper.check) ) { return; } - itemDefinition.allowedValues.text.__$$text = ""; - return; + itemDefinition.allowedValues = undefined; }); }, - [editItemDefinition, enumToKieConstraintType, itemDefinitionId] + [editItemDefinition, itemDefinitionId, enumToKieConstraintType, typeRefConstraintTypeHelper.check] ); return ( s.computed(s).getDataTypes(externalModelsByNamespace).allDataTypesById + ); + const allTopLevelItemDefinitionUniqueNames = useDmnEditorStore( + (s) => s.computed(s).getDataTypes(externalModelsByNamespace).allTopLevelItemDefinitionUniqueNames + ); + const itemDefinitionId = itemDefinition["@_id"]!; const typeConstraint = useMemo( () => defaultsToAllowedValues @@ -505,96 +569,111 @@ export function ConstraintsFromTypeConstraintAttribute({ [defaultsToAllowedValues, itemDefinition?.allowedValues, itemDefinition?.typeConstraint] ); + const typeRef = (itemDefinition?.typeRef?.__$$text as DmnBuiltInDataType) ?? DmnBuiltInDataType.Undefined; + const typeRefConstraintTypeHelper = useMemo( + () => constraintTypeHelper(itemDefinition, allDataTypesById, allTopLevelItemDefinitionUniqueNames), + [allDataTypesById, allTopLevelItemDefinitionUniqueNames, itemDefinition] + ); + + const rootItemDefinition = useMemo( + () => recursivelyGetRootItemDefinition(itemDefinition, allDataTypesById, allTopLevelItemDefinitionUniqueNames), + [allDataTypesById, allTopLevelItemDefinitionUniqueNames, itemDefinition] + ); + + const enabledConstraints = useMemo(() => { + if (isStruct(rootItemDefinition)) { + return ["expression"] as KIE__tConstraintType[]; + } + if (isCollection(rootItemDefinition)) { + return ["expression"] as KIE__tConstraintType[]; + } + return constrainableBuiltInFeelTypes.get( + (rootItemDefinition.typeRef?.__$$text as DmnBuiltInDataType) ?? DmnBuiltInDataType.Undefined + ); + }, [rootItemDefinition]); + const { constraintValue, - typeRef, isConstraintEnum, isConstraintRange, isConstraintEnabled, - itemDefinitionId, - selectedConstraint, + selectedKieConstraintType, enumToKieConstraintType, } = useConstraint({ constraint: typeConstraint, - itemDefinition, - isCollectionConstraintEnable: true, // typeConstraint enables to add a constraint to the collection itself + itemDefinition: itemDefinition, + isCollectionConstraintEnabled: true, // typeConstraint enables to add a constraint to the collection itself + constraintTypeHelper: typeRefConstraintTypeHelper, + enabledConstraints, }); const onConstraintChange = useCallback( - (value?: string) => { - editItemDefinition(itemDefinitionId!, (itemDefinition) => { - itemDefinition.typeConstraint ??= { text: { __$$text: "" } }; - itemDefinition.typeConstraint.text.__$$text = value ?? ""; - itemDefinition.typeConstraint["@_id"] = itemDefinition.typeConstraint?.["@_id"] ?? generateUuid(); + (value: string | undefined, selectedConstraint: ConstraintsType) => { + editItemDefinition(itemDefinitionId, (itemDefinition) => { + if (value === "" || value === undefined) { + itemDefinition.typeConstraint = undefined; + } else { + itemDefinition.typeConstraint ??= { text: { __$$text: "" } }; + itemDefinition.typeConstraint.text.__$$text = value; + itemDefinition.typeConstraint["@_id"] = itemDefinition.typeConstraint?.["@_id"] ?? generateUuid(); + itemDefinition.typeConstraint["@_kie:constraintType"] = enumToKieConstraintType(selectedConstraint); + } }); }, - [editItemDefinition, itemDefinitionId] + [editItemDefinition, enumToKieConstraintType, itemDefinitionId] ); const onToggleGroupChange = useCallback( - (newSelection: boolean, event: React.KeyboardEvent | MouseEvent | React.MouseEvent) => { + (newSelection: boolean, selectedConstraint: ConstraintsType) => { if (!newSelection) { return; } - const selection = event.currentTarget.id as ConstraintsType; - if (selection === ConstraintsType.NONE) { - editItemDefinition(itemDefinitionId!, (itemDefinition) => { + + editItemDefinition(itemDefinitionId, (itemDefinition) => { + if (selectedConstraint === ConstraintsType.NONE) { itemDefinition.typeConstraint = undefined; - }); - return; - } + return; + } - editItemDefinition(itemDefinitionId!, (itemDefinition) => { - itemDefinition.typeConstraint ??= { text: { __$$text: "" } }; - const previousKieContraintType = itemDefinition.typeConstraint["@_kie:constraintType"]; - itemDefinition.typeConstraint["@_kie:constraintType"] = enumToKieConstraintType(selection); + if (itemDefinition.typeConstraint) { + itemDefinition.typeConstraint["@_kie:constraintType"] = enumToKieConstraintType(selectedConstraint); + } - if (selection === ConstraintsType.EXPRESSION) { + if (selectedConstraint === ConstraintsType.EXPRESSION) { return; } if ( - previousKieContraintType === "expression" && - selection === ConstraintsType.ENUMERATION && - isEnum( - itemDefinition.typeConstraint.text.__$$text, - constraintTypeHelper( - (itemDefinition?.typeRef?.__$$text as DmnBuiltInDataType) ?? DmnBuiltInDataType.Undefined - ).check - ) + selectedConstraint === ConstraintsType.ENUMERATION && + isEnum(itemDefinition.typeConstraint?.text.__$$text, typeRefConstraintTypeHelper.check) ) { return; } if ( - previousKieContraintType === "expression" && - selection === ConstraintsType.RANGE && - isRange( - itemDefinition.typeConstraint.text.__$$text, - constraintTypeHelper( - (itemDefinition?.typeRef?.__$$text as DmnBuiltInDataType) ?? DmnBuiltInDataType.Undefined - ).check - ) + selectedConstraint === ConstraintsType.RANGE && + isRange(itemDefinition.typeConstraint?.text.__$$text, typeRefConstraintTypeHelper.check) ) { return; } - itemDefinition.typeConstraint.text.__$$text = ""; + itemDefinition.typeConstraint = undefined; }); }, - [editItemDefinition, enumToKieConstraintType, itemDefinitionId] + [editItemDefinition, itemDefinitionId, enumToKieConstraintType, typeRefConstraintTypeHelper.check] ); return ( | MouseEvent | React.MouseEvent - ) => void; - onConstraintChange: (value?: string) => void; + onToggleGroupChange: (selected: boolean, selectedConstraint: ConstraintsType) => void; + onConstraintChange: (value: string | undefined, selectedConstraint: ConstraintsType) => void; }) { + const [internalSelectedConstraint, setInternalSelectedConstraint] = + useState(selectedKieConstraintType); + + // Updates the `selectedConstraint` only after changing the active item definition + // Both `internalSelectedConstraint` and `selectedKieConstraintType` should not be coupled together + useEffect(() => { + setInternalSelectedConstraint(selectedKieConstraintType); + // eslint-disable-next-line + }, [itemDefinitionId]); + + const onToggleGroupChangeInternal = useCallback( + (selected: boolean, event: React.KeyboardEvent | MouseEvent | React.MouseEvent) => { + const selectedConstraint = event.currentTarget.id as ConstraintsType; + setInternalSelectedConstraint(selectedConstraint); + onToggleGroupChange(selected, selectedConstraint); + }, + [onToggleGroupChange] + ); + + const onConstraintChangeInternal = useCallback( + (value: string | undefined) => { + onConstraintChange(value, internalSelectedConstraint); + }, + [onConstraintChange, internalSelectedConstraint] + ); + return ( <> {isConstraintEnabled.expression === false && @@ -656,8 +760,11 @@ export function Constraints({
- {selectedConstraint === ConstraintsType.ENUMERATION && ( + {(selectedKieConstraintType === ConstraintsType.ENUMERATION || + internalSelectedConstraint === ConstraintsType.ENUMERATION) && ( )} - {selectedConstraint === ConstraintsType.RANGE && ( + {(selectedKieConstraintType === ConstraintsType.RANGE || + internalSelectedConstraint === ConstraintsType.RANGE) && ( )} - {selectedConstraint === ConstraintsType.EXPRESSION && ( + {(selectedKieConstraintType === ConstraintsType.EXPRESSION || + internalSelectedConstraint === ConstraintsType.EXPRESSION) && ( )} - {selectedConstraint === ConstraintsType.NONE && ( -

- {`All values are allowed`} -

- )} + {selectedKieConstraintType === ConstraintsType.NONE && + internalSelectedConstraint === ConstraintsType.NONE && ( +

+ {`All values are allowed`} +

+ )}
)} diff --git a/packages/dmn-editor/src/dataTypes/ConstraintsEnum.tsx b/packages/dmn-editor/src/dataTypes/ConstraintsEnum.tsx index 2e113704b92..d92758c9aa6 100644 --- a/packages/dmn-editor/src/dataTypes/ConstraintsEnum.tsx +++ b/packages/dmn-editor/src/dataTypes/ConstraintsEnum.tsx @@ -31,6 +31,7 @@ import { ConstraintComponentProps, TypeHelper } from "./Constraints"; export const ENUM_SEPARATOR = ","; export function ConstraintsEnum({ + id, isReadonly, value, expressionValue, @@ -182,7 +183,7 @@ export function ConstraintsEnum({ <>

- + )} diff --git a/packages/dmn-editor/src/dataTypes/ConstraintsExpression.tsx b/packages/dmn-editor/src/dataTypes/ConstraintsExpression.tsx index ecd7e13727b..75d53af21b9 100644 --- a/packages/dmn-editor/src/dataTypes/ConstraintsExpression.tsx +++ b/packages/dmn-editor/src/dataTypes/ConstraintsExpression.tsx @@ -18,7 +18,7 @@ */ import * as React from "react"; -import { useMemo, useState, useCallback } from "react"; +import { useMemo, useState, useCallback, useRef, useEffect } from "react"; import { Title } from "@patternfly/react-core/dist/js/components/Title"; import { FeelInput } from "@kie-tools/feel-input-component/dist"; import "./ConstraintsExpression.css"; @@ -28,10 +28,12 @@ import { DmnBuiltInDataType } from "@kie-tools/boxed-expression-component/dist/a import { TypeHelper } from "./Constraints"; export function ConstraintsExpression({ + id, isReadonly, value, onSave, }: { + id: string; isReadonly: boolean; value?: string; savedValue?: string; @@ -41,15 +43,43 @@ export function ConstraintsExpression({ isDisabled?: boolean; }) { const [preview, setPreview] = useState(value ?? ""); - const [editingValue, setEditingValue] = useState(value); + const [isEditing, setEditing] = useState(false); + const valueCopy = useRef(value); + + const onFeelBlur = useCallback((valueOnBlur: string) => { + setEditing(false); + }, []); + const onFeelChange = useCallback( (_, content, preview) => { - onSave?.(content.trim()); setPreview(preview); + onSave?.(content.trim()); }, [onSave] ); + const onPreviewChanged = useCallback((newPreview: string) => setPreview(newPreview), []); + + useEffect(() => { + valueCopy.current = isEditing ? valueCopy.current : value; + }, [isEditing, value]); + + const onKeyDown = useCallback( + (e) => { + // When inside FEEL Input, all keyboard events should be kept inside it. + // Exceptions to this strategy are handled on `onFeelKeyDown`. + if (!isReadonly && isEditing) { + e.stopPropagation(); + } + + // This is used to start editing a cell without being in edit mode. + if (!isReadonly && !isEditing) { + setEditing(true); + } + }, + [isEditing, isReadonly] + ); + const monacoOptions = useMemo( () => ({ fixedOverflowWidgets: true, @@ -64,7 +94,9 @@ export function ConstraintsExpression({ ); return ( -
+ // FeelInput doens't react to `onFeelChange` updates + // making it necessary to add a key to force a re-render; +
{isReadonly && ( Equivalent FEEL expression: @@ -85,9 +117,10 @@ export function ConstraintsExpression({ <p style={{ fontStyle: "italic" }}>{`<None>`}</p> ))} <FeelInput - value={isReadonly ? value : editingValue} + value={isEditing ? valueCopy.current : value} onChange={onFeelChange} - onPreviewChanged={setPreview} + onBlur={onFeelBlur} + onPreviewChanged={onPreviewChanged} enabled={!isReadonly} options={monacoOptions as any} /> diff --git a/packages/dmn-editor/src/dataTypes/ConstraintsRange.tsx b/packages/dmn-editor/src/dataTypes/ConstraintsRange.tsx index d04e6c85658..b18aca1498e 100644 --- a/packages/dmn-editor/src/dataTypes/ConstraintsRange.tsx +++ b/packages/dmn-editor/src/dataTypes/ConstraintsRange.tsx @@ -31,6 +31,7 @@ const CONSTRAINT_START_ID = "start"; const CONSTRAINT_END_ID = "end"; export function ConstraintsRange({ + id, isReadonly, value, expressionValue, @@ -312,7 +313,7 @@ export function ConstraintsRange({ {!renderOnPropertiesPanel && ( <> <br /> - <ConstraintsExpression isReadonly={true} value={expressionValue ?? ""} type={type} /> + <ConstraintsExpression id={id} isReadonly={true} value={expressionValue ?? ""} type={type} /> </> )} </div> diff --git a/packages/dmn-editor/src/dataTypes/DataTypePanel.tsx b/packages/dmn-editor/src/dataTypes/DataTypePanel.tsx index 9279932c753..080955d5791 100644 --- a/packages/dmn-editor/src/dataTypes/DataTypePanel.tsx +++ b/packages/dmn-editor/src/dataTypes/DataTypePanel.tsx @@ -357,6 +357,7 @@ export function DataTypePanel({ isDisabled={isReadonly} typeRef={resolvedTypeRef} onChange={changeTypeRef} + removeDataTypes={[dataType]} /> <br /> <br /> diff --git a/packages/dmn-editor/src/dataTypes/DataTypes.tsx b/packages/dmn-editor/src/dataTypes/DataTypes.tsx index 3daf48bb213..7d950a6ee67 100644 --- a/packages/dmn-editor/src/dataTypes/DataTypes.tsx +++ b/packages/dmn-editor/src/dataTypes/DataTypes.tsx @@ -87,7 +87,7 @@ export type EditItemDefinition = ( export function DataTypes() { const thisDmnsNamespace = useDmnEditorStore((s) => s.dmn.model.definitions["@_namespace"]); const dmnEditorStoreApi = useDmnEditorStoreApi(); - const { activeItemDefinitionId } = useDmnEditorStore((s) => s.dataTypesEditor); + const activeItemDefinitionId = useDmnEditorStore((s) => s.dataTypesEditor.activeItemDefinitionId); const [filter, setFilter] = useState(""); const { externalModelsByNamespace } = useExternalModels(); diff --git a/packages/dmn-editor/src/dataTypes/ItemComponentsTable.tsx b/packages/dmn-editor/src/dataTypes/ItemComponentsTable.tsx index 7778708f412..7769fd8c709 100644 --- a/packages/dmn-editor/src/dataTypes/ItemComponentsTable.tsx +++ b/packages/dmn-editor/src/dataTypes/ItemComponentsTable.tsx @@ -54,7 +54,7 @@ import { import { getNewDmnIdRandomizer } from "../idRandomizer/dmnIdRandomizer"; import { isEnum } from "./ConstraintsEnum"; import { isRange } from "./ConstraintsRange"; -import { constraintTypeHelper } from "./Constraints"; +import { constraintTypeHelper, recursivelyGetRootItemDefinition } from "./Constraints"; import { builtInFeelTypeNames } from "./BuiltInFeelTypes"; import { useDmnEditor } from "../DmnEditorContext"; import { DMN15__tItemDefinition } from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types"; @@ -95,6 +95,9 @@ export function ItemComponentsTable({ const allTopLevelDataTypesByFeelName = useDmnEditorStore( (s) => s.computed(s).getDataTypes(externalModelsByNamespace).allTopLevelDataTypesByFeelName ); + const allTopLevelItemDefinitionUniqueNames = useDmnEditorStore( + (s) => s.computed(s).getDataTypes(externalModelsByNamespace).allTopLevelItemDefinitionUniqueNames + ); const importsByNamespace = useDmnEditorStore((s) => s.computed(s).importsByNamespace()); const thisDmnsNamespace = useDmnEditorStore((s) => s.dmn.model.definitions["@_namespace"]); @@ -288,21 +291,37 @@ export function ItemComponentsTable({ return <>Range</>; } - const constraintValue = dt.itemDefinition.allowedValues?.text.__$$text; - const typeRef = - (dt.itemDefinition.typeRef?.__$$text as DmnBuiltInDataType) ?? DmnBuiltInDataType.Undefined; + const constraintValue = + dt.itemDefinition.typeConstraint?.text.__$$text ?? dt.itemDefinition.allowedValues?.text.__$$text; + + const typeHelper = constraintTypeHelper( + dt.itemDefinition, + allDataTypesById, + allTopLevelItemDefinitionUniqueNames + ); + if (constraintValue === undefined) { return <>None</>; } - if (isEnum(constraintValue, constraintTypeHelper(typeRef).check)) { + if (isEnum(constraintValue, typeHelper.check)) { return <>Enumeration</>; } - if (isRange(constraintValue, constraintTypeHelper(typeRef).check)) { + if (isRange(constraintValue, typeHelper.check)) { return <>Range</>; } return <>Expression</>; }; + const rootItemDefinition = recursivelyGetRootItemDefinition( + dt.itemDefinition, + allDataTypesById, + allTopLevelItemDefinitionUniqueNames + ); + + const isItemComponent = !!parent.itemDefinition?.itemComponent?.find( + (ic) => ic["@_id"] === rootItemDefinition["@_id"] + ); + return ( <React.Fragment key={dt.itemDefinition["@_id"]}> {shouldShowRow && ( @@ -439,7 +458,8 @@ export function ItemComponentsTable({ /> </td> <td> - {canHaveConstraints(dt.itemDefinition) ? ( + {canHaveConstraints(rootItemDefinition) || + (isStruct(rootItemDefinition) && !isItemComponent) ? ( <Button variant={ButtonVariant.link} onClick={() => { diff --git a/packages/dmn-editor/src/dataTypes/TypeRefSelector.tsx b/packages/dmn-editor/src/dataTypes/TypeRefSelector.tsx index 4e01851dd71..42456e8b817 100644 --- a/packages/dmn-editor/src/dataTypes/TypeRefSelector.tsx +++ b/packages/dmn-editor/src/dataTypes/TypeRefSelector.tsx @@ -26,7 +26,6 @@ import { ArrowUpIcon } from "@patternfly/react-icons/dist/js/icons/arrow-up-icon import { DmnEditorTab } from "../store/Store"; import { useDmnEditorStore, useDmnEditorStoreApi } from "../store/StoreContext"; import { Button, ButtonVariant } from "@patternfly/react-core/dist/js/components/Button"; -import { Tooltip } from "@patternfly/react-core/dist/js/components/Tooltip"; import { DataType } from "./DataTypes"; import { builtInFeelTypeNames, builtInFeelTypes } from "./BuiltInFeelTypes"; import { Flex } from "@patternfly/react-core/dist/js/layouts/Flex"; @@ -48,6 +47,7 @@ export function TypeRefSelector({ menuAppendTo, onCreate, onToggle, + removeDataTypes, }: { zoom?: number; heightRef: React.RefObject<HTMLElement>; @@ -57,6 +57,7 @@ export function TypeRefSelector({ onCreate?: OnCreateDataType; onToggle?: OnToggle; menuAppendTo?: "parent"; + removeDataTypes?: DataType[]; }) { const [isOpen, setOpen] = useState(false); const { externalModelsByNamespace } = useExternalModels(); @@ -87,7 +88,9 @@ export function TypeRefSelector({ } if (s.namespace === state.dmn.model.definitions["@_namespace"]) { - customDataTypes.push(s); + if ((removeDataTypes ?? []).findIndex((removeDataType) => removeDataType.feelName === s.feelName) < 0) { + customDataTypes.push(s); + } } else { externalDataTypes.push(s); } @@ -113,21 +116,19 @@ export function TypeRefSelector({ spaceItems={{ default: "spaceItemsNone" }} > {selectedDataType?.itemDefinition && ( - <Tooltip content="Jump to definition" appendTo={() => document.getElementById(id)!}> - <Button - title={"Jump to definition"} - className={"kie-dmn-editor--data-type-jump-to-definition"} - variant={ButtonVariant.control} - onClick={(e) => - dmnEditorStoreApi.setState((state) => { - state.navigation.tab = DmnEditorTab.DATA_TYPES; - state.dataTypesEditor.activeItemDefinitionId = selectedDataType?.itemDefinition?.["@_id"]; - }) - } - > - <ArrowUpIcon /> - </Button> - </Tooltip> + <Button + title={"Jump to definition"} + className={"kie-dmn-editor--data-type-jump-to-definition"} + variant={ButtonVariant.control} + onClick={(e) => + dmnEditorStoreApi.setState((state) => { + state.navigation.tab = DmnEditorTab.DATA_TYPES; + state.dataTypesEditor.activeItemDefinitionId = selectedDataType?.itemDefinition?.["@_id"]; + }) + } + > + <ArrowUpIcon /> + </Button> )} <Select toggleRef={toggleRef} diff --git a/packages/dmn-editor/src/propertiesPanel/BoxedExpressionPropertiesPanelComponents/Fields.tsx b/packages/dmn-editor/src/propertiesPanel/BoxedExpressionPropertiesPanelComponents/Fields.tsx index 5ab9fc11355..f38004f7f21 100644 --- a/packages/dmn-editor/src/propertiesPanel/BoxedExpressionPropertiesPanelComponents/Fields.tsx +++ b/packages/dmn-editor/src/propertiesPanel/BoxedExpressionPropertiesPanelComponents/Fields.tsx @@ -18,7 +18,7 @@ */ import * as React from "react"; -import { useEffect, useState, useRef } from "react"; +import { useEffect, useState } from "react"; import { FormGroup } from "@patternfly/react-core/dist/js/components/Form"; import { InlineFeelNameInput } from "../../feel/InlineFeelNameInput"; import { TextArea } from "@patternfly/react-core/dist/js/components/TextArea";