From 73496e4efb9481c77b526a732b63b4bbbebb3709 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Jos=C3=A9=20dos=20Santos?= Date: Thu, 14 Nov 2024 16:45:53 -0300 Subject: [PATCH 01/12] kie-issues#208: Renaming any "NamedElement" on the DMN Editor should update all references to the old name --- .../src/BoxedExpressionEditor.tsx | 16 +- .../src/BoxedExpressionEditorContext.tsx | 26 +- .../src/api/ExpressionChange.ts | 116 +++ .../src/api/index.ts | 1 + .../contextMenu/PopoverMenu/PopoverMenu.tsx | 13 +- .../ExpressionVariableCell.tsx | 68 +- .../ExpressionVariableMenu.tsx | 37 +- .../ConditionalExpression.tsx | 113 ++- .../ConditionalExpressionCell.tsx | 55 +- .../ContextEntryExpressionCell.tsx | 43 +- .../ContextExpression/ContextExpression.tsx | 219 +++-- .../ContextResultExpressionCell.tsx | 53 +- .../DecisionTableExpression.tsx | 780 +++++++++------- .../ExpressionContainer.tsx | 28 +- .../ExpressionDefinitionLogicTypeSelector.tsx | 13 +- .../FilterExpressionCollectionCell.tsx | 21 +- .../FilterExpressionComponent.tsx | 42 +- .../FilterExpressionMatchCell.tsx | 27 +- .../FeelFunctionExpression.tsx | 69 +- .../FunctionExpression/FunctionExpression.tsx | 103 ++- .../JavaFunctionExpression.tsx | 153 ++-- .../FunctionExpression/ParametersPopover.tsx | 125 +-- .../PmmlFunctionExpression.tsx | 81 +- .../ArgumentEntryExpressionCell.tsx | 29 +- .../InvocationExpression.tsx | 176 ++-- .../IteratorExpressionCell.tsx | 55 +- .../IteratorExpressionComponent.tsx | 111 ++- .../IteratorExpressionVariableCell.tsx | 33 +- .../ListExpression/ListExpression.tsx | 139 +-- .../ListExpression/ListItemCell.tsx | 23 +- .../LiteralExpression/LiteralExpression.tsx | 47 +- .../RelationExpression/RelationExpression.tsx | 356 +++++--- .../src/table/BeeTable/BeeTable.tsx | 6 +- .../BeeTable/BeeTableEditableCellContent.tsx | 10 +- .../table/BeeTable/BeeTableThResizable.tsx | 1 + .../stories/boxedExpressionStoriesWrapper.tsx | 15 +- .../stories/dev/WebApp.stories.tsx | 22 +- packages/dmn-editor/package.json | 1 + .../BoxedExpressionScreen.tsx | 167 +++- .../dmn-editor/src/dataTypes/DataTypeName.tsx | 19 +- packages/dmn-editor/src/diagram/Diagram.tsx | 13 + .../dmn-editor/src/diagram/nodes/Nodes.tsx | 425 ++++++++- .../src/includedModels/IncludedModels.tsx | 17 +- .../dmn-editor/src/mutations/renameImport.ts | 12 +- .../src/mutations/renameItemDefinition.ts | 11 +- .../dmn-editor/src/mutations/renameNode.ts | 15 +- .../src/mutations/setDrgElementExpression.ts | 58 ++ .../src/mutations/updateDrgElementType.ts | 53 ++ .../src/mutations/updateExpression.ts | 40 +- .../src/propertiesPanel/BkmProperties.tsx | 97 +- .../DecisionTableOutputHeaderCell.tsx | 109 ++- .../propertiesPanel/DecisionProperties.tsx | 94 +- .../DecisionServiceProperties.tsx | 92 +- .../propertiesPanel/InputDataProperties.tsx | 94 +- .../KnowledgeSourceProperties.tsx | 108 ++- .../propertiesPanel/SingleNodeProperties.tsx | 17 + .../refactor/RefactorConfirmationDialog.tsx | 78 ++ .../stories/dev/DevWebApp.stories.tsx | 2 +- .../stories/dev/availableModelsToInclude.ts | 4 +- .../stories/dmnEditorStoriesWrapper.tsx | 8 +- .../stories/misc/empty/Empty.stories.tsx | 2 +- .../LoanPreQualification.stories.tsx | 2 +- packages/dmn-feel-antlr4-parser/package.json | 4 +- .../src/FeelIdentifiers.ts | 45 + .../src/FeelVariables.ts | 40 - packages/dmn-feel-antlr4-parser/src/Uuid.ts | 29 + packages/dmn-feel-antlr4-parser/src/index.ts | 9 +- .../src/parser/BuiltInTypes.ts | 57 ++ .../src/parser/DataType.ts | 4 + .../{VariableOccurrence.ts => Expression.ts} | 40 +- ...eelVariable.ts => FeelIdentifiedSymbol.ts} | 32 +- ...blesParser.ts => FeelIdentifiersParser.ts} | 70 +- .../src/parser/{Variable.ts => Identifier.ts} | 26 +- ...ariableContext.ts => IdentifierContext.ts} | 18 +- .../src/parser/IdentifiersRepository.ts | 851 +++++++++++++++++ .../src/parser/ParsedExpression.ts | 4 +- .../src/parser/VariablesRepository.ts | 717 --------------- .../src/parser/grammar/BaseSymbol.ts | 17 +- .../src/parser/grammar/FunctionSymbol.ts | 17 +- ...{VariableSymbol.ts => IdentifierSymbol.ts} | 31 +- .../src/parser/grammar/MapBackedType.ts | 15 +- .../src/parser/grammar/ParserHelper.ts | 51 +- .../src/parser/grammar/ScopeImpl.ts | 10 +- .../src/parser/grammar/Type.ts | 3 + packages/dmn-language-service/src/index.ts | 1 + .../src/refactor/IdentifiersRefactor.ts | 160 ++++ .../fixtures/refactor/includeMathModel.dmn | 160 ++++ .../tests/fixtures/refactor/math.dmn | 343 +++++++ .../tests/fixtures/refactor/sampleLoan.dmn | 857 ++++++++++++++++++ .../dmn-language-service/tests/fs/fixtures.ts | 18 + .../refactor/IdentifiersRefactor.test.ts | 223 +++++ .../feel-input-component/src/FeelInput.tsx | 22 +- .../src/semanticTokensProvider.ts | 55 +- .../tests/semanticTokensProvider.test.ts | 36 +- .../stunner-editors-dmn-loader/src/index.tsx | 39 +- pnpm-lock.yaml | 237 ++++- 96 files changed, 6648 insertions(+), 2356 deletions(-) create mode 100644 packages/boxed-expression-component/src/api/ExpressionChange.ts create mode 100644 packages/dmn-editor/src/mutations/setDrgElementExpression.ts create mode 100644 packages/dmn-editor/src/mutations/updateDrgElementType.ts create mode 100644 packages/dmn-editor/src/refactor/RefactorConfirmationDialog.tsx create mode 100644 packages/dmn-feel-antlr4-parser/src/FeelIdentifiers.ts delete mode 100644 packages/dmn-feel-antlr4-parser/src/FeelVariables.ts create mode 100644 packages/dmn-feel-antlr4-parser/src/Uuid.ts rename packages/dmn-feel-antlr4-parser/src/parser/{VariableOccurrence.ts => Expression.ts} (52%) rename packages/dmn-feel-antlr4-parser/src/parser/{FeelVariable.ts => FeelIdentifiedSymbol.ts} (77%) rename packages/dmn-feel-antlr4-parser/src/parser/{FeelVariablesParser.ts => FeelIdentifiersParser.ts} (60%) rename packages/dmn-feel-antlr4-parser/src/parser/{Variable.ts => Identifier.ts} (61%) rename packages/dmn-feel-antlr4-parser/src/parser/{VariableContext.ts => IdentifierContext.ts} (79%) create mode 100644 packages/dmn-feel-antlr4-parser/src/parser/IdentifiersRepository.ts delete mode 100644 packages/dmn-feel-antlr4-parser/src/parser/VariablesRepository.ts rename packages/dmn-feel-antlr4-parser/src/parser/grammar/{VariableSymbol.ts => IdentifierSymbol.ts} (64%) create mode 100644 packages/dmn-language-service/src/refactor/IdentifiersRefactor.ts create mode 100644 packages/dmn-language-service/tests/fixtures/refactor/includeMathModel.dmn create mode 100644 packages/dmn-language-service/tests/fixtures/refactor/math.dmn create mode 100644 packages/dmn-language-service/tests/fixtures/refactor/sampleLoan.dmn create mode 100644 packages/dmn-language-service/tests/refactor/IdentifiersRefactor.test.ts diff --git a/packages/boxed-expression-component/src/BoxedExpressionEditor.tsx b/packages/boxed-expression-component/src/BoxedExpressionEditor.tsx index 920e4110257..5a33b08d0ae 100644 --- a/packages/boxed-expression-component/src/BoxedExpressionEditor.tsx +++ b/packages/boxed-expression-component/src/BoxedExpressionEditor.tsx @@ -27,12 +27,12 @@ import { boxedExpressionEditorI18nDefaults, } from "./i18n"; import { ExpressionDefinitionRoot } from "./expressions/ExpressionDefinitionRoot/ExpressionDefinitionRoot"; -import { BoxedExpressionEditorContextProvider } from "./BoxedExpressionEditorContext"; -import { FeelVariables } from "@kie-tools/dmn-feel-antlr4-parser"; +import { BoxedExpressionEditorContextProvider, OnExpressionChange } from "./BoxedExpressionEditorContext"; +import { FeelIdentifiers } from "@kie-tools/dmn-feel-antlr4-parser"; import "./base-no-reset-wrapped.css"; import "./@types/react-table"; -export type OnRequestFeelVariables = () => FeelVariables; +export type OnRequestFeelIdentifiers = () => FeelIdentifiers; export interface BoxedExpressionEditorProps { /** The API methods which BoxedExpressionEditor component can use to dialog with GWT layer. Although the GWT layer is deprecated, and the new DMN Editor does not have GWT, some methods here are still necessary. */ @@ -46,7 +46,7 @@ export interface BoxedExpressionEditorProps { /** The boxed expression itself */ expression: Normalized | undefined; /** Called every time something changes on the expression */ - onExpressionChange: React.Dispatch | undefined>>; + onExpressionChange: OnExpressionChange; /** KIE Extension to represent IDs of individual columns or expressions */ widthsById: Map; /** Called every time a width changes on the expression */ @@ -61,8 +61,8 @@ export interface BoxedExpressionEditorProps { pmmlDocuments?: PmmlDocument[]; /** The containing HTMLElement which is scrollable */ scrollableParentRef: React.RefObject; - /** Parsed variables used for syntax coloring and auto-complete */ - onRequestFeelVariables?: OnRequestFeelVariables; + /** Parsed identifiers used for syntax coloring and auto-complete */ + onRequestFeelIdentifiers?: OnRequestFeelIdentifiers; /** Hide DMN 1.4 boxed expressions */ hideDmn14BoxedExpressions?: boolean; } @@ -79,7 +79,7 @@ export function BoxedExpressionEditor({ isResetSupportedOnRootExpression, scrollableParentRef, pmmlDocuments, - onRequestFeelVariables, + onRequestFeelIdentifiers, widthsById, onWidthsChange, hideDmn14BoxedExpressions, @@ -103,7 +103,7 @@ export function BoxedExpressionEditor({ isReadOnly={isReadOnly} dataTypes={dataTypes} pmmlDocuments={pmmlDocuments} - onRequestFeelVariables={onRequestFeelVariables} + onRequestFeelIdentifiers={onRequestFeelIdentifiers} widthsById={widthsById} hideDmn14BoxedExpressions={hideDmn14BoxedExpressions} > diff --git a/packages/boxed-expression-component/src/BoxedExpressionEditorContext.tsx b/packages/boxed-expression-component/src/BoxedExpressionEditorContext.tsx index ce879f8808c..47922383b21 100644 --- a/packages/boxed-expression-component/src/BoxedExpressionEditorContext.tsx +++ b/packages/boxed-expression-component/src/BoxedExpressionEditorContext.tsx @@ -19,8 +19,8 @@ import * as React from "react"; import { useContext, useMemo, useRef, useState } from "react"; -import { BeeGwtService, BoxedExpression, DmnDataType, Normalized, PmmlDocument } from "./api"; -import { BoxedExpressionEditorProps, OnRequestFeelVariables } from "./BoxedExpressionEditor"; +import { BeeGwtService, BoxedExpression, DmnDataType, ExpressionChangedArgs, Normalized, PmmlDocument } from "./api"; +import { BoxedExpressionEditorProps, OnRequestFeelIdentifiers } from "./BoxedExpressionEditor"; import "./BoxedExpressionEditorContext.css"; export interface BoxedExpressionEditorContextType { @@ -39,13 +39,13 @@ export interface BoxedExpressionEditorContextType { currentlyOpenContextMenu: string | undefined; setCurrentlyOpenContextMenu: React.Dispatch>; - onRequestFeelVariables?: OnRequestFeelVariables; + onRequestFeelIdentifiers?: OnRequestFeelIdentifiers; widthsById: Map; hideDmn14BoxedExpressions?: boolean; } export interface BoxedExpressionEditorDispatchContextType { - setExpression: React.Dispatch>>; + setExpression: OnExpressionChange; setWidthsById: (mutation: ({ newMap }: { newMap: Map }) => void) => void; } @@ -75,7 +75,7 @@ export function BoxedExpressionEditorContextProvider({ children, pmmlDocuments, scrollableParentRef, - onRequestFeelVariables, + onRequestFeelIdentifiers, widthsById, hideDmn14BoxedExpressions, }: React.PropsWithChildren) { @@ -118,7 +118,7 @@ export function BoxedExpressionEditorContextProvider({ //state // FIXME: Move to a separate context (https://github.com/apache/incubator-kie-issues/issues/168) currentlyOpenContextMenu, setCurrentlyOpenContextMenu, - onRequestFeelVariables, + onRequestFeelIdentifiers, widthsById, hideDmn14BoxedExpressions, }} @@ -134,6 +134,12 @@ export function BoxedExpressionEditorContextProvider({ export type OnSetExpression = (args: { getNewExpression: (prev: Normalized | undefined) => Normalized | undefined; + expressionChangedArgs: ExpressionChangedArgs; +}) => void; + +export type OnExpressionChange = (args: { + setExpressionAction: React.SetStateAction | undefined>; + expressionChangedArgs: ExpressionChangedArgs; }) => void; export function NestedExpressionDispatchContextProvider({ @@ -145,12 +151,14 @@ export function NestedExpressionDispatchContextProvider({ const { setWidthsById } = useBoxedExpressionEditorDispatch(); const nestedExpressionDispatch = useMemo(() => { return { - setExpression: (newExpressionAction: React.SetStateAction>) => { + setExpression: (OnExpressionChange) => { function getNewExpression(prev: Normalized) { - return typeof newExpressionAction === "function" ? newExpressionAction(prev) : newExpressionAction; + return typeof OnExpressionChange.setExpressionAction === "function" + ? (OnExpressionChange.setExpressionAction(prev)! as Normalized) + : OnExpressionChange.setExpressionAction; } - onSetExpression({ getNewExpression }); + onSetExpression({ getNewExpression, expressionChangedArgs: OnExpressionChange.expressionChangedArgs }); }, setWidthsById, }; diff --git a/packages/boxed-expression-component/src/api/ExpressionChange.ts b/packages/boxed-expression-component/src/api/ExpressionChange.ts new file mode 100644 index 00000000000..fc6ff5b17d3 --- /dev/null +++ b/packages/boxed-expression-component/src/api/ExpressionChange.ts @@ -0,0 +1,116 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export type ExpressionChangedArgs = + | ({ + action: + | Action.ExpressionReset + | Action.ExpressionCreated + | Action.ExpressionPastedFromClipboard + | Action.DecisionTableCellsUpdated + | Action.DecisionTableHitPolicyChanged + | Action.DecisionTableBuiltInAggregatorChanged + | Action.FunctionParameterAdded + | Action.FunctionParameterTypeChanged + | Action.FunctionParameterRemoved + | Action.IteratorVariableDefined + | Action.RelationCellsUpdated + | Action.InvocationParametersChanged + | Action.ColumnChanged; + } & {}) + | ({ action: Action.RowsAdded } & RowsAddedArgs) + | ({ action: Action.RowDuplicated } & RowDuplicatedArgs) + | ({ action: Action.ColumnAdded } & ColumnsAddedArgs) + | ({ action: Action.RowRemoved } & RowRemovedArgs) + | ({ action: Action.RowReset } & RowResetArgs) + | ({ action: Action.ColumnRemoved } & ColumnRemovedArgs) + | ({ action: Action.LiteralTextExpressionChanged } & LiteralTextExpressionChangedArgs) + | ({ action: Action.FunctionKindChanged } & FunctionKindChangedArgs) + | ({ action: Action.VariableChanged } & VariableChangedArgs); + +export type VariableChangedProperty = { + from: string | undefined; + to: string | undefined; +}; + +export type VariableChangedArgs = { + typeChange?: VariableChangedProperty | undefined; + nameChange?: VariableChangedProperty | undefined; + variableUuid: string; +}; + +export interface ColumnsAddedArgs { + columnIndex: number; + columnCount: number; +} + +export interface RowsAddedArgs { + rowIndex: number; + rowsCount: number; +} + +export interface RowDuplicatedArgs { + rowIndex: number; +} + +export interface RowRemovedArgs { + rowIndex: number; +} +export interface RowResetArgs { + rowIndex: number; +} + +export interface ColumnRemovedArgs { + columnIndex: number; +} + +export interface LiteralTextExpressionChangedArgs { + from: string; + to: string; +} + +export interface FunctionKindChangedArgs { + from: string; + to: string; +} + +export enum Action { + ExpressionReset, + ExpressionCreated, + ExpressionPastedFromClipboard, + RowsAdded, + RowRemoved, + RowReset, + RowDuplicated, + ColumnAdded, + ColumnRemoved, + ColumnChanged, + VariableChanged, + LiteralTextExpressionChanged, + DecisionTableCellsUpdated, + DecisionTableHitPolicyChanged, + DecisionTableBuiltInAggregatorChanged, + FunctionKindChanged, + FunctionParameterAdded, + FunctionParameterTypeChanged, + FunctionParameterRemoved, + RelationCellsUpdated, + InvocationParametersChanged, + IteratorVariableDefined, +} diff --git a/packages/boxed-expression-component/src/api/index.ts b/packages/boxed-expression-component/src/api/index.ts index b682a12748e..26a284a7806 100644 --- a/packages/boxed-expression-component/src/api/index.ts +++ b/packages/boxed-expression-component/src/api/index.ts @@ -23,3 +23,4 @@ export * from "./DmnBuiltInDataType"; export * from "./DmnDataType"; export * from "./BoxedExpression"; export * from "./BeeTable"; +export * from "./ExpressionChange"; diff --git a/packages/boxed-expression-component/src/contextMenu/PopoverMenu/PopoverMenu.tsx b/packages/boxed-expression-component/src/contextMenu/PopoverMenu/PopoverMenu.tsx index fb03105edb8..c6688736850 100644 --- a/packages/boxed-expression-component/src/contextMenu/PopoverMenu/PopoverMenu.tsx +++ b/packages/boxed-expression-component/src/contextMenu/PopoverMenu/PopoverMenu.tsx @@ -111,13 +111,18 @@ export const PopoverMenu = React.forwardRef( if (event instanceof KeyboardEvent && NavigationKeysUtils.isEsc(event.key)) { onCancel(event); } else { - onHide(); + hideFunction?.(); } + }, + [onCancel] + ); + const onHideCallback: PopoverProps["onHide"] = useCallback( + (tip): void => { + onHide(); setCurrentlyOpenContextMenu(undefined); - hideFunction?.(); }, - [onCancel, onHide, setCurrentlyOpenContextMenu] + [onHide, setCurrentlyOpenContextMenu] ); useImperativeHandle( @@ -165,7 +170,7 @@ export const PopoverMenu = React.forwardRef( bodyContent={body} isVisible={isPopoverVisible} onShown={onPopoverShown} - onHide={shouldClose} + onHide={onHideCallback} shouldClose={shouldClose} shouldOpen={shouldOpen} flipBehavior={["bottom-start", "bottom", "bottom-end", "right-start", "left-start", "right-end", "left-end"]} diff --git a/packages/boxed-expression-component/src/expressionVariable/ExpressionVariableCell.tsx b/packages/boxed-expression-component/src/expressionVariable/ExpressionVariableCell.tsx index 5793118f1dc..b76f1729a1a 100644 --- a/packages/boxed-expression-component/src/expressionVariable/ExpressionVariableCell.tsx +++ b/packages/boxed-expression-component/src/expressionVariable/ExpressionVariableCell.tsx @@ -20,7 +20,15 @@ import { DMN15__tInformationItem } from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types"; import * as React from "react"; import { useCallback, useEffect, useMemo } from "react"; -import { BeeTableCellProps, BoxedExpression, DmnBuiltInDataType, Normalized } from "../api"; +import { + Action, + BeeTableCellProps, + BoxedExpression, + DmnBuiltInDataType, + ExpressionChangedArgs, + Normalized, + VariableChangedArgs, +} from "../api"; import { useCellWidthToFitDataRef } from "../resizing/BeeTableCellWidthToFitDataContext"; import { getCanvasFont, getTextWidth } from "../resizing/WidthsToFitData"; import { useBeeTableSelectableCellRef } from "../selection/BeeTableSelectionContext"; @@ -37,7 +45,11 @@ export interface ExpressionWithVariable { variable: Normalized; } -export type OnExpressionWithVariableUpdated = (index: number, { expression, variable }: ExpressionWithVariable) => void; +export type OnExpressionWithVariableUpdated = ( + index: number, + { expression, variable }: ExpressionWithVariable, + variableChangedArgs: VariableChangedArgs +) => void; export const ExpressionVariableCell: React.FunctionComponent< BeeTableCellProps & { @@ -50,21 +62,44 @@ export const ExpressionVariableCell: React.FunctionComponent< const onVariableUpdated = useCallback( ({ name = DEFAULT_EXPRESSION_VARIABLE_NAME, typeRef = undefined }) => { - onExpressionWithVariableUpdated(index, { - // `expression` and `variable` must always have the same `typeRef` and `name/label`, as those are dictated by `variable`. - expression: expression - ? { - ...expression, - "@_label": name, - "@_typeRef": typeRef, - } - : undefined!, // SPEC DISCREPANCY - variable: { - ...variable, - "@_name": name, - "@_typeRef": typeRef, + const variableChangedArgs: ExpressionChangedArgs = { + action: Action.VariableChanged, + variableUuid: variable["@_id"], + typeChange: + variable["@_typeRef"] !== typeRef + ? { + from: variable["@_typeRef"], + to: typeRef, + } + : undefined, + nameChange: + variable["@_name"] !== name + ? { + from: variable["@_name"], + to: name, + } + : undefined, + }; + + onExpressionWithVariableUpdated( + index, + { + // `expression` and `variable` must always have the same `typeRef` and `name/label`, as those are dictated by `variable`. + expression: expression + ? { + ...expression, + "@_label": name, + "@_typeRef": typeRef, + } + : undefined!, // SPEC DISCREPANCY + variable: { + ...variable, + "@_name": name, + "@_typeRef": typeRef, + }, }, - }); + variableChangedArgs + ); }, [onExpressionWithVariableUpdated, index, expression, variable] ); @@ -144,6 +179,7 @@ export const ExpressionVariableCell: React.FunctionComponent< selectedExpressionName={variable["@_name"]} selectedDataType={variable["@_typeRef"]} onVariableUpdated={onVariableUpdated} + variableUuid={variable["@_id"]} > {cellContent} diff --git a/packages/boxed-expression-component/src/expressionVariable/ExpressionVariableMenu.tsx b/packages/boxed-expression-component/src/expressionVariable/ExpressionVariableMenu.tsx index 03e50bfe219..47a0416b26e 100644 --- a/packages/boxed-expression-component/src/expressionVariable/ExpressionVariableMenu.tsx +++ b/packages/boxed-expression-component/src/expressionVariable/ExpressionVariableMenu.tsx @@ -28,8 +28,13 @@ import { Button } from "@patternfly/react-core/dist/js/components/Button"; import { NavigationKeysUtils } from "../keysUtils/keyUtils"; import { PopoverPosition } from "@patternfly/react-core/dist/js/components/Popover"; import "./ExpressionVariableMenu.css"; +import { Action, ExpressionChangedArgs, VariableChangedArgs } from "../api"; -export type OnExpressionVariableUpdated = (args: { name: string; typeRef: string | undefined }) => void; +export type OnExpressionVariableUpdated = (args: { + name: string; + typeRef: string | undefined; + changes: VariableChangedArgs; +}) => void; export interface ExpressionVariableMenuProps { /** Optional children element to be considered for triggering the edit expression menu */ @@ -51,6 +56,8 @@ export interface ExpressionVariableMenuProps { /** Function to be called when the expression gets updated, passing the most updated version of it */ onVariableUpdated: OnExpressionVariableUpdated; position?: PopoverPosition; + /** The UUID of the variable. */ + variableUuid: string; } export const DEFAULT_EXPRESSION_VARIABLE_NAME = "Expression Name"; @@ -65,6 +72,7 @@ export function ExpressionVariableMenu({ selectedExpressionName, onVariableUpdated, position, + variableUuid, }: ExpressionVariableMenuProps) { const { editorRef, beeGwtService } = useBoxedExpressionEditor(); const { i18n } = useBoxedExpressionEditorI18n(); @@ -100,17 +108,36 @@ export function ExpressionVariableMenu({ }, [beeGwtService]); const saveExpression = useCallback(() => { - if (expressionName !== selectedExpressionName || dataType !== selectedDataType) { - onVariableUpdated({ name: expressionName, typeRef: dataType }); + const changes: ExpressionChangedArgs = { + action: Action.VariableChanged, + variableUuid: variableUuid, + typeChange: + dataType !== selectedDataType + ? { + from: dataType, + to: selectedDataType, + } + : undefined, + nameChange: + expressionName !== selectedExpressionName + ? { + from: expressionName, + to: selectedExpressionName, + } + : undefined, + }; + + if (changes.nameChange || changes.typeChange) { + onVariableUpdated({ name: expressionName, typeRef: dataType, changes }); } - }, [expressionName, selectedExpressionName, dataType, selectedDataType, onVariableUpdated]); + }, [expressionName, selectedExpressionName, dataType, selectedDataType, variableUuid, onVariableUpdated]); const resetFormData = useCallback(() => { setExpressionName(selectedExpressionName); setDataType(selectedDataType); }, [selectedExpressionName, selectedDataType]); - // onCancel doens't prevent the onHide call + // onCancel doesn't prevent the onHide call // With this ref we ensure the "saveExpression" inside onHide will not be called const cancelEdit = useRef(false); diff --git a/packages/boxed-expression-component/src/expressions/ConditionalExpression/ConditionalExpression.tsx b/packages/boxed-expression-component/src/expressions/ConditionalExpression/ConditionalExpression.tsx index ab9871d81e8..6df6abd4079 100644 --- a/packages/boxed-expression-component/src/expressions/ConditionalExpression/ConditionalExpression.tsx +++ b/packages/boxed-expression-component/src/expressions/ConditionalExpression/ConditionalExpression.tsx @@ -18,12 +18,14 @@ */ import { + Action, BeeTableHeaderVisibility, BeeTableOperation, BeeTableOperationConfig, BeeTableProps, BoxedConditional, DmnBuiltInDataType, + ExpressionChangedArgs, generateUuid, Normalized, } from "../../api"; @@ -171,40 +173,43 @@ export function ConditionalExpression({ const onRowReset = useCallback( (args: { rowIndex: number }) => { - setExpression((prev: Normalized) => { - if (args.rowIndex === 0) { - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { - ...prev, - if: { - "@_id": generateUuid(), - expression: undefined!, - }, // SPEC DISCREPANCY - }; - return ret; - } else if (args.rowIndex === 1) { - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { - ...prev, - then: { - "@_id": generateUuid(), - expression: undefined!, - }, // SPEC DISCREPANCY - }; - return ret; - } else if (args.rowIndex === 2) { - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { - ...prev, - else: { - "@_id": generateUuid(), - expression: undefined!, - }, // SPEC DISCREPANCY - }; - return ret; - } else { - throw new Error("ConditionalExpression shouldn't have more than 3 rows."); - } + setExpression({ + setExpressionAction: (prev: Normalized) => { + if (args.rowIndex === 0) { + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: Normalized = { + ...prev, + if: { + "@_id": generateUuid(), + expression: undefined!, + }, // SPEC DISCREPANCY + }; + return ret; + } else if (args.rowIndex === 1) { + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: Normalized = { + ...prev, + then: { + "@_id": generateUuid(), + expression: undefined!, + }, // SPEC DISCREPANCY + }; + return ret; + } else if (args.rowIndex === 2) { + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: Normalized = { + ...prev, + else: { + "@_id": generateUuid(), + expression: undefined!, + }, // SPEC DISCREPANCY + }; + return ret; + } else { + throw new Error("ConditionalExpression shouldn't have more than 3 rows."); + } + }, + expressionChangedArgs: { action: Action.RowReset, rowIndex: args.rowIndex }, }); }, [setExpression] @@ -212,18 +217,40 @@ export function ConditionalExpression({ const onColumnUpdates = useCallback( ([{ name, typeRef }]: BeeTableColumnUpdate[]) => { - setExpression((prev: Normalized) => { - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { - ...prev, - "@_label": name, - "@_typeRef": typeRef, - }; + const expressionChangedArgs: ExpressionChangedArgs = { + action: Action.VariableChanged, + variableUuid: expressionHolderId, + typeChange: + typeRef !== conditionalExpression["@_typeRef"] + ? { + from: conditionalExpression["@_typeRef"] ?? "", + to: typeRef, + } + : undefined, + nameChange: + name !== conditionalExpression["@_label"] + ? { + from: conditionalExpression["@_label"] ?? "", + to: name, + } + : undefined, + }; - return ret; + setExpression({ + setExpressionAction: (prev: Normalized) => { + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: Normalized = { + ...prev, + "@_label": name, + "@_typeRef": typeRef, + }; + + return ret; + }, + expressionChangedArgs, }); }, - [setExpression] + [conditionalExpression, expressionHolderId, setExpression] ); return ( diff --git a/packages/boxed-expression-component/src/expressions/ConditionalExpression/ConditionalExpressionCell.tsx b/packages/boxed-expression-component/src/expressions/ConditionalExpression/ConditionalExpressionCell.tsx index a01c7b1d86c..fb1ddfcfaf0 100644 --- a/packages/boxed-expression-component/src/expressions/ConditionalExpression/ConditionalExpressionCell.tsx +++ b/packages/boxed-expression-component/src/expressions/ConditionalExpression/ConditionalExpressionCell.tsx @@ -38,32 +38,35 @@ export function ConditionalExpressionCell({ const { setExpression } = useBoxedExpressionEditorDispatch(); const onSetExpression = useCallback( - ({ getNewExpression }) => { - setExpression((prev: Normalized) => { - if (rowIndex === 0) { - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { - ...prev, - if: { ...prev.if, expression: getNewExpression(prev.if.expression)! }, - }; - return ret; - } else if (rowIndex === 1) { - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { - ...prev, - then: { ...prev.then, expression: getNewExpression(prev.then.expression)! }, - }; - return ret; - } else if (rowIndex === 2) { - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { - ...prev, - else: { ...prev.else, expression: getNewExpression(prev.else.expression)! }, - }; - return ret; - } else { - throw new Error("ConditionalExpression shouldn't have more than 3 rows."); - } + ({ getNewExpression, expressionChangedArgs }) => { + setExpression({ + setExpressionAction: (prev: Normalized) => { + if (rowIndex === 0) { + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: Normalized = { + ...prev, + if: { ...prev.if, expression: getNewExpression(prev.if.expression)! }, + }; + return ret; + } else if (rowIndex === 1) { + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: Normalized = { + ...prev, + then: { ...prev.then, expression: getNewExpression(prev.then.expression)! }, + }; + return ret; + } else if (rowIndex === 2) { + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: Normalized = { + ...prev, + else: { ...prev.else, expression: getNewExpression(prev.else.expression)! }, + }; + return ret; + } else { + throw new Error("ConditionalExpression shouldn't have more than 3 rows."); + } + }, + expressionChangedArgs, }); }, [rowIndex, setExpression] diff --git a/packages/boxed-expression-component/src/expressions/ContextExpression/ContextEntryExpressionCell.tsx b/packages/boxed-expression-component/src/expressions/ContextExpression/ContextEntryExpressionCell.tsx index 6139b628c01..2bb482b7d03 100644 --- a/packages/boxed-expression-component/src/expressions/ContextExpression/ContextEntryExpressionCell.tsx +++ b/packages/boxed-expression-component/src/expressions/ContextExpression/ContextEntryExpressionCell.tsx @@ -48,28 +48,31 @@ export const ContextEntryExpressionCell: React.FunctionComponent( - ({ getNewExpression }) => { - setExpression((prev: Normalized) => { - const newContextEntries = [...(prev.contextEntry ?? [])]; - const newExpression = getNewExpression(newContextEntries[index]?.expression ?? undefined); - newContextEntries[index] = { - ...newContextEntries[index], - expression: newExpression!, // SPEC DISCREPANCY: Accepting undefined expression - variable: { - ...newContextEntries[index].variable, - "@_id": newContextEntries[index]?.variable?.["@_id"] ?? generateUuid(), - "@_name": newExpression?.["@_label"] ?? newContextEntries[index].variable!["@_name"], - "@_typeRef": newExpression?.["@_typeRef"] ?? newContextEntries[index].variable!["@_typeRef"], - }, - }; + ({ getNewExpression, expressionChangedArgs }) => { + setExpression({ + setExpressionAction: (prev: Normalized) => { + const newContextEntries = [...(prev.contextEntry ?? [])]; + const newExpression = getNewExpression(newContextEntries[index]?.expression ?? undefined); + newContextEntries[index] = { + ...newContextEntries[index], + expression: newExpression!, // SPEC DISCREPANCY: Accepting undefined expression + variable: { + ...newContextEntries[index].variable, + "@_id": newContextEntries[index]?.variable?.["@_id"] ?? generateUuid(), + "@_name": newExpression?.["@_label"] ?? newContextEntries[index].variable!["@_name"], + "@_typeRef": newExpression?.["@_typeRef"] ?? newContextEntries[index].variable!["@_typeRef"], + }, + }; - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { - ...prev, - contextEntry: newContextEntries, - }; + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: Normalized = { + ...prev, + contextEntry: newContextEntries, + }; - return ret; + return ret; + }, + expressionChangedArgs, }); }, [index, setExpression] diff --git a/packages/boxed-expression-component/src/expressions/ContextExpression/ContextExpression.tsx b/packages/boxed-expression-component/src/expressions/ContextExpression/ContextExpression.tsx index ef7ff4641f3..e000c4d49ea 100644 --- a/packages/boxed-expression-component/src/expressions/ContextExpression/ContextExpression.tsx +++ b/packages/boxed-expression-component/src/expressions/ContextExpression/ContextExpression.tsx @@ -21,6 +21,7 @@ import * as React from "react"; import { useCallback, useEffect, useMemo, useState } from "react"; import * as ReactTable from "react-table"; import { + Action, BeeTableContextMenuAllowedOperationsConditions, BeeTableHeaderVisibility, BeeTableOperation, @@ -29,6 +30,7 @@ import { BoxedContext, BoxedExpression, DmnBuiltInDataType, + ExpressionChangedArgs, generateUuid, getNextAvailablePrefixedName, Normalized, @@ -185,18 +187,40 @@ export function ContextExpression({ const onColumnUpdates = useCallback( ([{ name, typeRef }]: BeeTableColumnUpdate[]) => { - setExpression((prev: Normalized) => { - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { - ...prev, - "@_label": name, - "@_typeRef": typeRef, - }; + const expressionChangedArgs: ExpressionChangedArgs = { + action: Action.VariableChanged, + variableUuid: expressionHolderId, + typeChange: + typeRef !== contextExpression["@_typeRef"] + ? { + from: contextExpression["@_typeRef"] ?? "", + to: typeRef, + } + : undefined, + nameChange: + name !== contextExpression["@_label"] + ? { + from: contextExpression["@_label"] ?? "", + to: name, + } + : undefined, + }; + + setExpression({ + setExpressionAction: (prev: Normalized) => { + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: Normalized = { + ...prev, + "@_label": name, + "@_typeRef": typeRef, + }; - return ret; + return ret; + }, + expressionChangedArgs, }); }, - [setExpression] + [contextExpression, expressionHolderId, setExpression] ); const headerVisibility = useMemo(() => { @@ -204,23 +228,25 @@ export function ContextExpression({ }, [isNested]); const updateVariable = useCallback( - (index: number, { expression, variable }: ExpressionWithVariable) => { - setExpression((prev: Normalized) => { - const contextEntries = [...(prev.contextEntry ?? [])]; - - contextEntries[index] = { - ...contextEntries[index], - expression: expression ?? undefined!, // SPEC DISCREPANCY - variable: variable, - }; - - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { - ...prev, - contextEntry: contextEntries, - }; - - return ret; + (index: number, { expression, variable }: ExpressionWithVariable, variableChangedArgs) => { + setExpression({ + setExpressionAction: (prev: Normalized) => { + const contextEntries = [...(prev.contextEntry ?? [])]; + contextEntries[index] = { + ...contextEntries[index], + expression: expression ?? undefined!, // SPEC DISCREPANCY + variable: variable, + }; + + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: Normalized = { + ...prev, + contextEntry: contextEntries, + }; + + return ret; + }, + expressionChangedArgs: variableChangedArgs, }); }, [setExpression] @@ -297,28 +323,31 @@ export function ContextExpression({ const onRowAdded = useCallback( (args: { beforeIndex: number; rowsCount: number }) => { - setExpression((prev: Normalized) => { - const newContextEntries = [...(prev.contextEntry ?? [])]; - - const newEntries = []; - const names = newContextEntries.map((e) => e.variable?.["@_name"] ?? "").filter((e) => e !== ""); - for (let i = 0; i < args.rowsCount; i++) { - const name = getNextAvailablePrefixedName(names, "ContextEntry"); - names.push(name); - newEntries.push(getDefaultContextEntry(name)); - } + setExpression({ + setExpressionAction: (prev: Normalized) => { + const newContextEntries = [...(prev.contextEntry ?? [])]; + + const newEntries = []; + const names = newContextEntries.map((e) => e.variable?.["@_name"] ?? "").filter((e) => e !== ""); + for (let i = 0; i < args.rowsCount; i++) { + const name = getNextAvailablePrefixedName(names, "ContextEntry"); + names.push(name); + newEntries.push(getDefaultContextEntry(name)); + } - for (const newEntry of newEntries) { - newContextEntries.splice(args.beforeIndex, 0, newEntry); - } + for (const newEntry of newEntries) { + newContextEntries.splice(args.beforeIndex, 0, newEntry); + } - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { - ...prev, - contextEntry: newContextEntries, - }; + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: Normalized = { + ...prev, + contextEntry: newContextEntries, + }; - return ret; + return ret; + }, + expressionChangedArgs: { action: Action.RowsAdded, rowIndex: args.beforeIndex, rowsCount: args.rowsCount }, }); }, [getDefaultContextEntry, setExpression] @@ -328,28 +357,31 @@ export function ContextExpression({ (args: { rowIndex: number }) => { let oldExpression: Normalized | undefined; - setExpression((prev: Normalized) => { - const newContextEntries = [...(prev.contextEntry ?? [])]; + setExpression({ + setExpressionAction: (prev: Normalized) => { + const newContextEntries = [...(prev.contextEntry ?? [])]; - const { isResultOperation: isDeletingResult, entryIndex } = solveResultAndEntriesIndex({ - contextEntries: newContextEntries, - rowIndex: args.rowIndex, - }); + const { isResultOperation: isDeletingResult, entryIndex } = solveResultAndEntriesIndex({ + contextEntries: newContextEntries, + rowIndex: args.rowIndex, + }); - if (isDeletingResult) { - throw new Error("It's not possible to delete the row"); - } else { - oldExpression = newContextEntries[entryIndex]?.expression; - newContextEntries.splice(entryIndex, 1); - } + if (isDeletingResult) { + throw new Error("It's not possible to delete the row"); + } else { + oldExpression = newContextEntries[entryIndex]?.expression; + newContextEntries.splice(entryIndex, 1); + } - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { - ...prev, - contextEntry: newContextEntries, - }; + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: Normalized = { + ...prev, + contextEntry: newContextEntries, + }; - return ret; + return ret; + }, + expressionChangedArgs: { action: Action.RowRemoved, rowIndex: args.rowIndex }, }); setWidthsById(({ newMap }) => { @@ -365,39 +397,42 @@ export function ContextExpression({ (args: { rowIndex: number }) => { let oldExpression: Normalized | undefined; - setExpression((prev: Normalized) => { - const newContextEntries = [...(prev.contextEntry ?? [])]; - - const { - isResultOperation: isResettingResult, - hasResultEntry: hasResultExpression, - resultIndex, - entryIndex, - } = solveResultAndEntriesIndex({ - contextEntries: newContextEntries, - rowIndex: args.rowIndex, - }); - - if (isResettingResult) { - if (hasResultExpression) { - oldExpression = newContextEntries[resultIndex]?.expression; - newContextEntries.splice(resultIndex, 1); + setExpression({ + setExpressionAction: (prev: Normalized) => { + const newContextEntries = [...(prev.contextEntry ?? [])]; + + const { + isResultOperation: isResettingResult, + hasResultEntry: hasResultExpression, + resultIndex, + entryIndex, + } = solveResultAndEntriesIndex({ + contextEntries: newContextEntries, + rowIndex: args.rowIndex, + }); + + if (isResettingResult) { + if (hasResultExpression) { + oldExpression = newContextEntries[resultIndex]?.expression; + newContextEntries.splice(resultIndex, 1); + } else { + // ignore + } } else { - // ignore + oldExpression = newContextEntries[entryIndex]?.expression; + const defaultContextEntry = getDefaultContextEntry(newContextEntries[entryIndex]?.variable?.["@_name"]); + newContextEntries.splice(entryIndex, 1, defaultContextEntry); } - } else { - oldExpression = newContextEntries[entryIndex]?.expression; - const defaultContextEntry = getDefaultContextEntry(newContextEntries[entryIndex]?.variable?.["@_name"]); - newContextEntries.splice(entryIndex, 1, defaultContextEntry); - } - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { - ...prev, - contextEntry: newContextEntries, - }; + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: Normalized = { + ...prev, + contextEntry: newContextEntries, + }; - return ret; + return ret; + }, + expressionChangedArgs: { action: Action.RowReset, rowIndex: args.rowIndex }, }); setWidthsById(({ newMap }) => { diff --git a/packages/boxed-expression-component/src/expressions/ContextExpression/ContextResultExpressionCell.tsx b/packages/boxed-expression-component/src/expressions/ContextExpression/ContextResultExpressionCell.tsx index b9519bcd355..ef0334356dd 100644 --- a/packages/boxed-expression-component/src/expressions/ContextExpression/ContextResultExpressionCell.tsx +++ b/packages/boxed-expression-component/src/expressions/ContextExpression/ContextResultExpressionCell.tsx @@ -41,35 +41,38 @@ export function ContextResultExpressionCell(props: { }); const onSetExpression = useCallback( - ({ getNewExpression }) => { - setExpression((prev: Normalized) => { - const newContextEntries = [...(prev.contextEntry ?? [])]; + ({ getNewExpression, expressionChangedArgs }) => { + setExpression({ + setExpressionAction: (prev: Normalized) => { + const newContextEntries = [...(prev.contextEntry ?? [])]; - const newExpression = getNewExpression(newContextEntries[resultIndex]?.expression); + const newExpression = getNewExpression(newContextEntries[resultIndex]?.expression); - if (resultIndex <= -1) { - newContextEntries.push({ - "@_id": generateUuid(), - expression: newExpression!, // SPEC DISCREPANCY: - }); - } else if (newExpression) { - newContextEntries.splice(resultIndex, 1, { - ...newContextEntries[resultIndex], - expression: newExpression, - }); - } else { - newContextEntries.splice(resultIndex, 1); - } + if (resultIndex <= -1) { + newContextEntries.push({ + "@_id": generateUuid(), + expression: newExpression!, // SPEC DISCREPANCY: + }); + } else if (newExpression) { + newContextEntries.splice(resultIndex, 1, { + ...newContextEntries[resultIndex], + expression: newExpression, + }); + } else { + newContextEntries.splice(resultIndex, 1); + } - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { - ...prev, - contextEntry: newContextEntries, - "@_label": newExpression?.["@_label"] ?? prev["@_label"], - "@_typeRef": newExpression?.["@_typeRef"] ?? prev["@_typeRef"], - }; + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: Normalized = { + ...prev, + contextEntry: newContextEntries, + "@_label": newExpression?.["@_label"] ?? prev["@_label"], + "@_typeRef": newExpression?.["@_typeRef"] ?? prev["@_typeRef"], + }; - return ret; + return ret; + }, + expressionChangedArgs, }); }, [resultIndex, setExpression] diff --git a/packages/boxed-expression-component/src/expressions/DecisionTableExpression/DecisionTableExpression.tsx b/packages/boxed-expression-component/src/expressions/DecisionTableExpression/DecisionTableExpression.tsx index e22e1da0ce0..d9c25fc1199 100644 --- a/packages/boxed-expression-component/src/expressions/DecisionTableExpression/DecisionTableExpression.tsx +++ b/packages/boxed-expression-component/src/expressions/DecisionTableExpression/DecisionTableExpression.tsx @@ -21,12 +21,14 @@ import * as React from "react"; import { useCallback, useMemo, useRef } from "react"; import * as ReactTable from "react-table"; import { + Action, BeeTableContextMenuAllowedOperationsConditions, BeeTableHeaderVisibility, BeeTableOperation, BeeTableOperationConfig, BoxedDecisionTable, DmnBuiltInDataType, + ExpressionChangedArgs, generateUuid, getNextAvailablePrefixedName, Normalized, @@ -440,153 +442,211 @@ export function DecisionTableExpression({ const onCellUpdates = useCallback( (cellUpdates: BeeTableCellUpdate[]) => { - setExpression((prev: Normalized) => { - let previousExpression: Normalized = { ...prev }; - - cellUpdates.forEach((cellUpdate) => { - const newRules = [...(previousExpression.rule ?? [])]; - const groupType = cellUpdate.column.groupType as DecisionTableColumnType; - switch (groupType) { - case DecisionTableColumnType.InputClause: - const newInputEntries = [...(newRules[cellUpdate.rowIndex].inputEntry ?? [])]; - newInputEntries[cellUpdate.columnIndex] = { - ...newInputEntries[cellUpdate.columnIndex], - text: { - __$$text: cellUpdate.value, - }, - }; - newRules[cellUpdate.rowIndex] = { - ...newRules[cellUpdate.rowIndex], - inputEntry: newInputEntries, - }; - break; - case DecisionTableColumnType.OutputClause: - const newOutputEntries = [...newRules[cellUpdate.rowIndex].outputEntry]; - const entryIndex = cellUpdate.columnIndex - (prev.input?.length ?? 0); - newOutputEntries[entryIndex] = { - ...newOutputEntries[entryIndex], - text: { - __$$text: cellUpdate.value, - }, - }; - newRules[cellUpdate.rowIndex] = { - ...newRules[cellUpdate.rowIndex], - outputEntry: newOutputEntries, - }; - break; - case DecisionTableColumnType.Annotation: - const newAnnotationEntries = [...(newRules[cellUpdate.rowIndex].annotationEntry ?? [])]; - const annotationIndex = cellUpdate.columnIndex - (prev.input?.length ?? 0) - (prev.output?.length ?? 0); - newAnnotationEntries[annotationIndex] = { - ...newAnnotationEntries[annotationIndex], - text: { __$$text: cellUpdate.value }, - }; - newRules[cellUpdate.rowIndex] = { - ...newRules[cellUpdate.rowIndex], - annotationEntry: newAnnotationEntries, - }; - break; - default: - assertUnreachable(groupType); - } + setExpression({ + setExpressionAction: (prev: Normalized) => { + let previousExpression: Normalized = { ...prev }; + + cellUpdates.forEach((cellUpdate) => { + const newRules = [...(previousExpression.rule ?? [])]; + const groupType = cellUpdate.column.groupType as DecisionTableColumnType; + switch (groupType) { + case DecisionTableColumnType.InputClause: + const newInputEntries = [...(newRules[cellUpdate.rowIndex].inputEntry ?? [])]; + newInputEntries[cellUpdate.columnIndex] = { + ...newInputEntries[cellUpdate.columnIndex], + text: { + __$$text: cellUpdate.value, + }, + }; + newRules[cellUpdate.rowIndex] = { + ...newRules[cellUpdate.rowIndex], + inputEntry: newInputEntries, + }; + break; + case DecisionTableColumnType.OutputClause: + const newOutputEntries = [...newRules[cellUpdate.rowIndex].outputEntry]; + const entryIndex = cellUpdate.columnIndex - (prev.input?.length ?? 0); + newOutputEntries[entryIndex] = { + ...newOutputEntries[entryIndex], + text: { + __$$text: cellUpdate.value, + }, + }; + newRules[cellUpdate.rowIndex] = { + ...newRules[cellUpdate.rowIndex], + outputEntry: newOutputEntries, + }; + break; + case DecisionTableColumnType.Annotation: + const newAnnotationEntries = [...(newRules[cellUpdate.rowIndex].annotationEntry ?? [])]; + const annotationIndex = cellUpdate.columnIndex - (prev.input?.length ?? 0) - (prev.output?.length ?? 0); + newAnnotationEntries[annotationIndex] = { + ...newAnnotationEntries[annotationIndex], + text: { __$$text: cellUpdate.value }, + }; + newRules[cellUpdate.rowIndex] = { + ...newRules[cellUpdate.rowIndex], + annotationEntry: newAnnotationEntries, + }; + break; + default: + assertUnreachable(groupType); + } - previousExpression = { - ...previousExpression, - rule: newRules, - }; - }); + previousExpression = { + ...previousExpression, + rule: newRules, + }; + }); - return previousExpression; + return previousExpression; + }, + expressionChangedArgs: { action: Action.DecisionTableCellsUpdated }, }); }, [setExpression] ); - const onColumnUpdates = useCallback( + const getExpressionChangedArgsFromColumnUpdates = useCallback( (columnUpdates: BeeTableColumnUpdate[]) => { - setExpression((prev: Normalized) => { + const updateNodeNameOrType = columnUpdates.filter( + (columnUpdate) => + columnUpdate.column.depth === 0 && + columnUpdate.column.groupType === DecisionTableColumnType.OutputClause && + (decisionTableExpression["@_label"] !== columnUpdate.name || + decisionTableExpression["@_typeRef"] !== columnUpdate.typeRef) + ); + + if (updateNodeNameOrType.length > 1) { + throw new Error("Unexpected multiple name and/or type changed simultaneously in a Decision Table."); + } + + // This is the Output column aggregator column, which represents the entire expression name and typeRef + if (updateNodeNameOrType.length === 1) { + const expressionChangedArgs: ExpressionChangedArgs = { + action: Action.VariableChanged, + variableUuid: expressionHolderId, + typeChange: + decisionTableExpression["@_typeRef"] !== updateNodeNameOrType[0].typeRef + ? { + from: decisionTableExpression["@_typeRef"], + to: updateNodeNameOrType[0].typeRef, + } + : undefined, + nameChange: + decisionTableExpression["@_label"] !== updateNodeNameOrType[0].name + ? { + from: decisionTableExpression["@_label"], + to: updateNodeNameOrType[0].name, + } + : undefined, + }; + + return expressionChangedArgs; + } else { + // Changes in other columns does not reflect in changes in variables // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { ...prev }; - for (const columnUpdate of columnUpdates) { - // This is the Output column aggregator column, which represents the entire expression name and typeRef - if ( - columnUpdate.column.depth === 0 && - columnUpdate.column.groupType === DecisionTableColumnType.OutputClause - ) { - ret["@_label"] = columnUpdate.name; - ret["@_typeRef"] = columnUpdate.typeRef; - - // Single output column is merged with the aggregator column and should have the same typeRef - if (ret.output?.length === 1) { - const newOutputs = [...(ret.output ?? [])]; - newOutputs[0] = { - ...newOutputs[0], - "@_typeRef": columnUpdate.typeRef, - "@_name": columnUpdate.name, - }; + const columnChangedAction: ExpressionChangedArgs = { action: Action.ColumnChanged }; + return columnChangedAction; + } + }, + [decisionTableExpression, expressionHolderId] + ); + + const onColumnUpdates = useCallback( + (columnUpdates: BeeTableColumnUpdate[]) => { + const expressionChangedArgs = getExpressionChangedArgsFromColumnUpdates(columnUpdates); + + setExpression({ + setExpressionAction: (prev: Normalized) => { + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: Normalized = { ...prev }; + for (const columnUpdate of columnUpdates) { + // This is the Output column aggregator column, which represents the entire expression name and typeRef + if ( + columnUpdate.column.depth === 0 && + columnUpdate.column.groupType === DecisionTableColumnType.OutputClause + ) { + ret["@_label"] = columnUpdate.name; + ret["@_typeRef"] = columnUpdate.typeRef; + + // Single output column is merged with the aggregator column and should have the same typeRef + if (ret.output?.length === 1) { + const newOutputs = [...(ret.output ?? [])]; + newOutputs[0] = { + ...newOutputs[0], + "@_typeRef": columnUpdate.typeRef, + "@_name": columnUpdate.name, + }; + } + continue; } - continue; - } - // These are the other columns. - const groupType = columnUpdate.column.groupType as DecisionTableColumnType; - switch (groupType) { - case DecisionTableColumnType.InputClause: - const newInputs = [...(ret.input ?? [])]; - newInputs[columnUpdate.columnIndex] = { - ...newInputs[columnUpdate.columnIndex], - inputExpression: { - ...newInputs[columnUpdate.columnIndex].inputExpression, + // These are the other columns. + const groupType = columnUpdate.column.groupType as DecisionTableColumnType; + switch (groupType) { + case DecisionTableColumnType.InputClause: + const newInputs = [...(ret.input ?? [])]; + newInputs[columnUpdate.columnIndex] = { + ...newInputs[columnUpdate.columnIndex], + inputExpression: { + ...newInputs[columnUpdate.columnIndex].inputExpression, + "@_typeRef": columnUpdate.typeRef, + text: { __$$text: columnUpdate.name }, + }, + }; + ret.input = newInputs; + break; + case DecisionTableColumnType.OutputClause: + const newOutputs = [...(ret.output ?? [])]; + const outputIndex = columnUpdate.columnIndex - (prev.input?.length ?? 0); + newOutputs[outputIndex] = { + ...newOutputs[outputIndex], "@_typeRef": columnUpdate.typeRef, - text: { __$$text: columnUpdate.name }, - }, - }; - ret.input = newInputs; - break; - case DecisionTableColumnType.OutputClause: - const newOutputs = [...(ret.output ?? [])]; - const outputIndex = columnUpdate.columnIndex - (prev.input?.length ?? 0); - newOutputs[outputIndex] = { - ...newOutputs[outputIndex], - "@_typeRef": columnUpdate.typeRef, - "@_name": columnUpdate.name, - }; + "@_name": columnUpdate.name, + }; - ret.output = newOutputs; - break; - case DecisionTableColumnType.Annotation: - const newAnnotations = [...(ret.annotation ?? [])]; - const annotationIndex = columnUpdate.columnIndex - (prev.input?.length ?? 0) - (prev.output?.length ?? 0); - newAnnotations[annotationIndex] = { - ...newAnnotations[annotationIndex], - "@_name": columnUpdate.name, - }; - ret.annotation = newAnnotations; - break; - default: - assertUnreachable(groupType); + ret.output = newOutputs; + break; + case DecisionTableColumnType.Annotation: + const newAnnotations = [...(ret.annotation ?? [])]; + const annotationIndex = + columnUpdate.columnIndex - (prev.input?.length ?? 0) - (prev.output?.length ?? 0); + newAnnotations[annotationIndex] = { + ...newAnnotations[annotationIndex], + "@_name": columnUpdate.name, + }; + ret.annotation = newAnnotations; + break; + default: + assertUnreachable(groupType); + } } - } - return ret; + return ret; + }, + expressionChangedArgs, }); }, - [setExpression] + [getExpressionChangedArgsFromColumnUpdates, setExpression] ); const onHitPolicySelect = useCallback( (hitPolicy: string) => { - setExpression((prev: Normalized) => { - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { - ...prev, - "@_hitPolicy": hitPolicy as DMN15__tHitPolicy, - "@_aggregation": HIT_POLICIES_THAT_SUPPORT_AGGREGATION.includes(hitPolicy) - ? (prev as BoxedDecisionTable)["@_aggregation"] - : undefined!, - }; + setExpression({ + setExpressionAction: (prev: Normalized) => { + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: Normalized = { + ...prev, + "@_hitPolicy": hitPolicy as DMN15__tHitPolicy, + "@_aggregation": HIT_POLICIES_THAT_SUPPORT_AGGREGATION.includes(hitPolicy) + ? (prev as BoxedDecisionTable)["@_aggregation"] + : undefined!, + }; - return ret; + return ret; + }, + expressionChangedArgs: { action: Action.DecisionTableHitPolicyChanged }, }); }, [setExpression] @@ -627,14 +687,17 @@ export function DecisionTableExpression({ const onBuiltInAggregatorSelect = useCallback( (aggregation: DMN15__tBuiltinAggregator) => { - setExpression((prev: Normalized) => { - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { - ...prev, - "@_aggregation": getAggregation(aggregation), - }; + setExpression({ + setExpressionAction: (prev: Normalized) => { + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: Normalized = { + ...prev, + "@_aggregation": getAggregation(aggregation), + }; - return ret; + return ret; + }, + expressionChangedArgs: { action: Action.DecisionTableBuiltInAggregatorChanged }, }); }, [getAggregation, setExpression] @@ -655,36 +718,39 @@ export function DecisionTableExpression({ const onRowAdded = useCallback( (args: { beforeIndex: number; rowsCount: number }) => { - setExpression((prev: Normalized) => { - const newRules = [...(prev.rule ?? [])]; - const newItems: Normalized[] = []; - - for (let i = 0; i < args.rowsCount; i++) { - newItems.push({ - "@_id": generateUuid(), - inputEntry: Array.from(new Array(prev.input?.length ?? 0)).map(() => { - return createInputEntry(); - }), - outputEntry: Array.from(new Array(prev.output?.length ?? 0)).map(() => { - return createOutputEntry(); - }), - annotationEntry: Array.from(new Array(prev.annotation?.length ?? 0)).map(() => { - return { text: { __$$text: DECISION_TABLE_ANNOTATION_DEFAULT_VALUE } }; - }), - }); - } + setExpression({ + setExpressionAction: (prev: Normalized) => { + const newRules = [...(prev.rule ?? [])]; + const newItems: Normalized[] = []; + + for (let i = 0; i < args.rowsCount; i++) { + newItems.push({ + "@_id": generateUuid(), + inputEntry: Array.from(new Array(prev.input?.length ?? 0)).map(() => { + return createInputEntry(); + }), + outputEntry: Array.from(new Array(prev.output?.length ?? 0)).map(() => { + return createOutputEntry(); + }), + annotationEntry: Array.from(new Array(prev.annotation?.length ?? 0)).map(() => { + return { text: { __$$text: DECISION_TABLE_ANNOTATION_DEFAULT_VALUE } }; + }), + }); + } - for (const newEntry of newItems) { - newRules.splice(args.beforeIndex, 0, newEntry); - } + for (const newEntry of newItems) { + newRules.splice(args.beforeIndex, 0, newEntry); + } - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { - ...prev, - rule: newRules, - }; + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: Normalized = { + ...prev, + rule: newRules, + }; - return ret; + return ret; + }, + expressionChangedArgs: { action: Action.RowsAdded, rowIndex: args.beforeIndex, rowsCount: args.rowsCount }, }); }, [setExpression] @@ -717,128 +783,135 @@ export function DecisionTableExpression({ const localIndexInsideGroup = getLocalIndexInsideGroupType(args.beforeIndex, groupType); - setExpression((prev: Normalized) => { - const nextRows = [...(prev.rule ?? [])]; + setExpression({ + setExpressionAction: (prev: Normalized) => { + const nextRows = [...(prev.rule ?? [])]; - switch (groupType) { - case DecisionTableColumnType.InputClause: - const inputColumnsToAdd: Normalized[] = []; + switch (groupType) { + case DecisionTableColumnType.InputClause: + const inputColumnsToAdd: Normalized[] = []; - const currentInputNames = prev.input?.map((c) => c.inputExpression.text?.__$$text ?? "") ?? []; - for (let i = 0; i < args.columnsCount; i++) { - const newName = getNextAvailablePrefixedName(currentInputNames, "Input"); - currentInputNames.push(newName); + const currentInputNames = prev.input?.map((c) => c.inputExpression.text?.__$$text ?? "") ?? []; + for (let i = 0; i < args.columnsCount; i++) { + const newName = getNextAvailablePrefixedName(currentInputNames, "Input"); + currentInputNames.push(newName); - inputColumnsToAdd.push({ - "@_id": generateUuid(), - inputExpression: { + inputColumnsToAdd.push({ "@_id": generateUuid(), - "@_typeRef": undefined, - text: { __$$text: newName }, - }, - }); - } + inputExpression: { + "@_id": generateUuid(), + "@_typeRef": undefined, + text: { __$$text: newName }, + }, + }); + } - const nextInputColumns = [...(prev.input ?? [])]; - for (/* Add new columns */ let i = 0; i < inputColumnsToAdd.length; i++) { - nextInputColumns.splice(localIndexInsideGroup + i, 0, inputColumnsToAdd[i]); - } + const nextInputColumns = [...(prev.input ?? [])]; + for (/* Add new columns */ let i = 0; i < inputColumnsToAdd.length; i++) { + nextInputColumns.splice(localIndexInsideGroup + i, 0, inputColumnsToAdd[i]); + } - for (/* Add new cells to each row */ let i = 0; i < nextRows.length; i++) { - const row = nextRows[i]; - const nextInputEntries = [...(row.inputEntry ?? [])]; + for (/* Add new cells to each row */ let i = 0; i < nextRows.length; i++) { + const row = nextRows[i]; + const nextInputEntries = [...(row.inputEntry ?? [])]; - for (/* Add new cells to row */ let j = 0; j < args.columnsCount; j++) { - nextInputEntries.splice(localIndexInsideGroup + j, 0, createInputEntry()); + for (/* Add new cells to row */ let j = 0; j < args.columnsCount; j++) { + nextInputEntries.splice(localIndexInsideGroup + j, 0, createInputEntry()); + } + nextRows[i] = { ...row, inputEntry: nextInputEntries }; } - nextRows[i] = { ...row, inputEntry: nextInputEntries }; - } - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const retInput: Normalized = { - ...prev, - input: nextInputColumns, - rule: nextRows, - }; + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const retInput: Normalized = { + ...prev, + input: nextInputColumns, + rule: nextRows, + }; - return retInput; + return retInput; - case DecisionTableColumnType.OutputClause: - const outputColumnsToAdd: Normalized[] = []; + case DecisionTableColumnType.OutputClause: + const outputColumnsToAdd: Normalized[] = []; - const currentOutputColumnNames = prev.output?.map((c) => c["@_name"] ?? "") ?? []; - for (let i = 0; i < args.columnsCount; i++) { - const name = getNextAvailablePrefixedName(currentOutputColumnNames, "Output"); - currentOutputColumnNames.push(name); - outputColumnsToAdd.push({ - "@_id": generateUuid(), - "@_name": name, - "@_typeRef": undefined, - }); - } + const currentOutputColumnNames = prev.output?.map((c) => c["@_name"] ?? "") ?? []; + for (let i = 0; i < args.columnsCount; i++) { + const name = getNextAvailablePrefixedName(currentOutputColumnNames, "Output"); + currentOutputColumnNames.push(name); + outputColumnsToAdd.push({ + "@_id": generateUuid(), + "@_name": name, + "@_typeRef": undefined, + }); + } - const nextOutputColumns = [...(prev.output ?? [])]; - for (/* Add new columns */ let i = 0; i < outputColumnsToAdd.length; i++) { - nextOutputColumns.splice(localIndexInsideGroup + i, 0, outputColumnsToAdd[i]); - } + const nextOutputColumns = [...(prev.output ?? [])]; + for (/* Add new columns */ let i = 0; i < outputColumnsToAdd.length; i++) { + nextOutputColumns.splice(localIndexInsideGroup + i, 0, outputColumnsToAdd[i]); + } - for (/* Add new cells to each row */ let i = 0; i < nextRows.length; i++) { - const row = nextRows[i]; - const nextOutputEntries = [...(row.outputEntry ?? [])]; + for (/* Add new cells to each row */ let i = 0; i < nextRows.length; i++) { + const row = nextRows[i]; + const nextOutputEntries = [...(row.outputEntry ?? [])]; - for (/* Add new cells to row */ let j = 0; j < args.columnsCount; j++) { - nextOutputEntries.splice(localIndexInsideGroup + j, 0, createOutputEntry()); - } + for (/* Add new cells to row */ let j = 0; j < args.columnsCount; j++) { + nextOutputEntries.splice(localIndexInsideGroup + j, 0, createOutputEntry()); + } - nextRows[i] = { ...row, outputEntry: nextOutputEntries }; - } + nextRows[i] = { ...row, outputEntry: nextOutputEntries }; + } - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const retOutput: Normalized = { - ...prev, - output: nextOutputColumns, - rule: nextRows, - }; + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const retOutput: Normalized = { + ...prev, + output: nextOutputColumns, + rule: nextRows, + }; - return retOutput; + return retOutput; - case DecisionTableColumnType.Annotation: - const annotationColumnsToAdd: Normalized[] = []; + case DecisionTableColumnType.Annotation: + const annotationColumnsToAdd: Normalized[] = []; - const currentAnnotationColumnNames = prev.annotation?.map((c) => c["@_name"] ?? "") ?? []; - for (let i = 0; i < args.columnsCount; i++) { - const newName = getNextAvailablePrefixedName(currentAnnotationColumnNames, "Annotations"); - currentAnnotationColumnNames.push(newName); - annotationColumnsToAdd.push({ "@_name": newName }); - } + const currentAnnotationColumnNames = prev.annotation?.map((c) => c["@_name"] ?? "") ?? []; + for (let i = 0; i < args.columnsCount; i++) { + const newName = getNextAvailablePrefixedName(currentAnnotationColumnNames, "Annotations"); + currentAnnotationColumnNames.push(newName); + annotationColumnsToAdd.push({ "@_name": newName }); + } - const nextAnnotationColumns = [...(prev.annotation ?? [])]; - for (/* Add new columns */ let i = 0; i < annotationColumnsToAdd.length; i++) { - nextAnnotationColumns.splice(localIndexInsideGroup + i, 0, annotationColumnsToAdd[i]); - } + const nextAnnotationColumns = [...(prev.annotation ?? [])]; + for (/* Add new columns */ let i = 0; i < annotationColumnsToAdd.length; i++) { + nextAnnotationColumns.splice(localIndexInsideGroup + i, 0, annotationColumnsToAdd[i]); + } - for (/* Add new cells to each row */ let i = 0; i < nextRows.length; i++) { - const row = nextRows[i]; - const nextAnnotationEntries = [...(row.annotationEntry ?? [])]; + for (/* Add new cells to each row */ let i = 0; i < nextRows.length; i++) { + const row = nextRows[i]; + const nextAnnotationEntries = [...(row.annotationEntry ?? [])]; - for (/* Add new cells to row */ let j = 0; j < args.columnsCount; j++) { - nextAnnotationEntries.splice(localIndexInsideGroup + j, 0, createAnnotationEntry()); + for (/* Add new cells to row */ let j = 0; j < args.columnsCount; j++) { + nextAnnotationEntries.splice(localIndexInsideGroup + j, 0, createAnnotationEntry()); + } + nextRows[i] = { ...row, annotationEntry: nextAnnotationEntries }; } - nextRows[i] = { ...row, annotationEntry: nextAnnotationEntries }; - } - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const retAnnotation: Normalized = { - ...prev, - annotation: nextAnnotationColumns, - rule: nextRows, - }; + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const retAnnotation: Normalized = { + ...prev, + annotation: nextAnnotationColumns, + rule: nextRows, + }; - return retAnnotation; + return retAnnotation; - default: - assertUnreachable(groupType); - } + default: + assertUnreachable(groupType); + } + }, + expressionChangedArgs: { + action: Action.ColumnAdded, + columnIndex: args.beforeIndex, + columnCount: args.columnsCount, + }, }); setWidthsById(({ newMap }) => { @@ -865,73 +938,76 @@ export function DecisionTableExpression({ const onColumnDeleted = useCallback( (args: { columnIndex: number; groupType: DecisionTableColumnType }) => { - setExpression((prev: Normalized) => { - const groupType = args.groupType; - if (!groupType) { - throw new Error("Column without groupType for Decision table."); - } + setExpression({ + setExpressionAction: (prev: Normalized) => { + const groupType = args.groupType; + if (!groupType) { + throw new Error("Column without groupType for Decision table."); + } - const localIndexInsideGroup = getLocalIndexInsideGroupType(args.columnIndex, groupType); - - switch (groupType) { - case DecisionTableColumnType.InputClause: - const newInputs = [...(prev.input ?? [])]; - newInputs.splice(localIndexInsideGroup, 1); - - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const retInput: Normalized = { - ...prev, - input: newInputs, - rule: [...(prev.rule ?? [])].map((rule) => { - const newInputEntry = [...(rule.inputEntry ?? [])]; - newInputEntry.splice(localIndexInsideGroup, 1); - return { - ...rule, - inputEntry: newInputEntry, - }; - }), - }; - return retInput; - case DecisionTableColumnType.OutputClause: - const newOutputs = [...(prev.output ?? [])]; - newOutputs.splice(localIndexInsideGroup, 1); - - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const retOutput: Normalized = { - ...prev, - output: newOutputs, - rule: [...(prev.rule ?? [])].map((rule) => { - const newOutputEntry = [...rule.outputEntry]; - newOutputEntry.splice(localIndexInsideGroup, 1); - return { - ...rule, - outputEntry: newOutputEntry, - }; - }), - }; + const localIndexInsideGroup = getLocalIndexInsideGroupType(args.columnIndex, groupType); - return retOutput; - case DecisionTableColumnType.Annotation: - const newAnnotations = [...(prev.annotation ?? [])]; - newAnnotations.splice(localIndexInsideGroup, 1); - - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const retAnnotation: Normalized = { - ...prev, - annotation: newAnnotations, - rule: [...(prev.rule ?? [])].map((rule) => { - const newAnnotationEntry = [...(rule.annotationEntry ?? [])]; - newAnnotationEntry.splice(localIndexInsideGroup, 1); - return { - ...rule, - annotationEntry: newAnnotationEntry, - }; - }), - }; - return retAnnotation; - default: - assertUnreachable(groupType); - } + switch (groupType) { + case DecisionTableColumnType.InputClause: + const newInputs = [...(prev.input ?? [])]; + newInputs.splice(localIndexInsideGroup, 1); + + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const retInput: Normalized = { + ...prev, + input: newInputs, + rule: [...(prev.rule ?? [])].map((rule) => { + const newInputEntry = [...(rule.inputEntry ?? [])]; + newInputEntry.splice(localIndexInsideGroup, 1); + return { + ...rule, + inputEntry: newInputEntry, + }; + }), + }; + return retInput; + case DecisionTableColumnType.OutputClause: + const newOutputs = [...(prev.output ?? [])]; + newOutputs.splice(localIndexInsideGroup, 1); + + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const retOutput: Normalized = { + ...prev, + output: newOutputs, + rule: [...(prev.rule ?? [])].map((rule) => { + const newOutputEntry = [...rule.outputEntry]; + newOutputEntry.splice(localIndexInsideGroup, 1); + return { + ...rule, + outputEntry: newOutputEntry, + }; + }), + }; + + return retOutput; + case DecisionTableColumnType.Annotation: + const newAnnotations = [...(prev.annotation ?? [])]; + newAnnotations.splice(localIndexInsideGroup, 1); + + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const retAnnotation: Normalized = { + ...prev, + annotation: newAnnotations, + rule: [...(prev.rule ?? [])].map((rule) => { + const newAnnotationEntry = [...(rule.annotationEntry ?? [])]; + newAnnotationEntry.splice(localIndexInsideGroup, 1); + return { + ...rule, + annotationEntry: newAnnotationEntry, + }; + }), + }; + return retAnnotation; + default: + assertUnreachable(groupType); + } + }, + expressionChangedArgs: { action: Action.ColumnRemoved, columnIndex: args.columnIndex }, }); setWidthsById(({ newMap }) => { @@ -946,16 +1022,19 @@ export function DecisionTableExpression({ const onRowDeleted = useCallback( (args: { rowIndex: number }) => { - setExpression((prev: Normalized) => { - const newRules = [...(prev.rule ?? [])]; - newRules.splice(args.rowIndex, 1); - - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { - ...prev, - rule: newRules, - }; - return ret; + setExpression({ + setExpressionAction: (prev: Normalized) => { + const newRules = [...(prev.rule ?? [])]; + newRules.splice(args.rowIndex, 1); + + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: Normalized = { + ...prev, + rule: newRules, + }; + return ret; + }, + expressionChangedArgs: { action: Action.RowRemoved, rowIndex: args.rowIndex }, }); }, [setExpression] @@ -963,30 +1042,33 @@ export function DecisionTableExpression({ const onRowDuplicated = useCallback( (args: { rowIndex: number }) => { - setExpression((prev: Normalized) => { - const duplicatedRule = { - "@_id": generateUuid(), - inputEntry: prev.rule![args.rowIndex].inputEntry?.map((input) => ({ - ...input, - "@_id": generateUuid(), - })), - outputEntry: prev.rule![args.rowIndex].outputEntry.map((output) => ({ - ...output, + setExpression({ + setExpressionAction: (prev: Normalized) => { + const duplicatedRule = { "@_id": generateUuid(), - })), - annotationEntry: prev.rule![args.rowIndex].annotationEntry?.slice(), - }; + inputEntry: prev.rule![args.rowIndex].inputEntry?.map((input) => ({ + ...input, + "@_id": generateUuid(), + })), + outputEntry: prev.rule![args.rowIndex].outputEntry.map((output) => ({ + ...output, + "@_id": generateUuid(), + })), + annotationEntry: prev.rule![args.rowIndex].annotationEntry?.slice(), + }; - const newRules = [...(prev.rule ?? [])]; - newRules.splice(args.rowIndex, 0, duplicatedRule); + const newRules = [...(prev.rule ?? [])]; + newRules.splice(args.rowIndex, 0, duplicatedRule); - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { - ...prev, - rule: newRules, - }; + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: Normalized = { + ...prev, + rule: newRules, + }; - return ret; + return ret; + }, + expressionChangedArgs: { action: Action.RowDuplicated, rowIndex: args.rowIndex }, }); }, [setExpression] diff --git a/packages/boxed-expression-component/src/expressions/ExpressionDefinitionRoot/ExpressionContainer.tsx b/packages/boxed-expression-component/src/expressions/ExpressionDefinitionRoot/ExpressionContainer.tsx index 5d2a0673c78..a8c8a7bb9af 100644 --- a/packages/boxed-expression-component/src/expressions/ExpressionDefinitionRoot/ExpressionContainer.tsx +++ b/packages/boxed-expression-component/src/expressions/ExpressionDefinitionRoot/ExpressionContainer.tsx @@ -20,7 +20,7 @@ import * as React from "react"; import { useCallback, useEffect, useRef } from "react"; import { useBoxedExpressionEditor, useBoxedExpressionEditorDispatch } from "../../BoxedExpressionEditorContext"; -import { BoxedExpression, generateUuid, Normalized } from "../../api"; +import { Action, BoxedExpression, generateUuid, Normalized } from "../../api"; import { findAllIdsDeep } from "../../ids/ids"; import { DEFAULT_EXPRESSION_VARIABLE_NAME } from "../../expressionVariable/ExpressionVariableMenu"; import { useBeeTableSelectableCellRef } from "../../selection/BeeTableSelectionContext"; @@ -67,15 +67,18 @@ export const ExpressionContainer: React.FunctionComponent) => { - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { - ...defaultExpression, - "@_id": defaultExpression["@_id"] ?? generateUuid(), - "@_label": defaultExpression["@_label"] ?? parentElementName ?? DEFAULT_EXPRESSION_VARIABLE_NAME, - }; + setExpression({ + setExpressionAction: (prev: Normalized) => { + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: Normalized = { + ...defaultExpression, + "@_id": defaultExpression["@_id"] ?? generateUuid(), + "@_label": defaultExpression["@_label"] ?? parentElementName ?? DEFAULT_EXPRESSION_VARIABLE_NAME, + }; - return ret; + return ret; + }, + expressionChangedArgs: { action: Action.ExpressionCreated }, }); setWidthsById(({ newMap }) => { @@ -94,7 +97,12 @@ export const ExpressionContainer: React.FunctionComponent { + return undefined; // SPEC DISCREPANCY: Undefined expressions gives users the ability to select the expression type. + }, + expressionChangedArgs: { action: Action.ExpressionReset }, + }); }, [expression, setExpression, setWidthsById]); const getPlacementRef = useCallback(() => containerRef.current!, []); diff --git a/packages/boxed-expression-component/src/expressions/ExpressionDefinitionRoot/ExpressionDefinitionLogicTypeSelector.tsx b/packages/boxed-expression-component/src/expressions/ExpressionDefinitionRoot/ExpressionDefinitionLogicTypeSelector.tsx index 6a2e1a052ca..dca65168c11 100644 --- a/packages/boxed-expression-component/src/expressions/ExpressionDefinitionRoot/ExpressionDefinitionLogicTypeSelector.tsx +++ b/packages/boxed-expression-component/src/expressions/ExpressionDefinitionRoot/ExpressionDefinitionLogicTypeSelector.tsx @@ -34,7 +34,7 @@ import { ResourcesAlmostEmptyIcon } from "@patternfly/react-icons/dist/js/icons/ import { ResourcesFullIcon } from "@patternfly/react-icons/dist/js/icons/resources-full-icon"; import * as React from "react"; import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; -import { BoxedExpression, Normalized } from "../../api"; +import { Action, BoxedExpression, Normalized } from "../../api"; import { useCustomContextMenuHandler } from "../../contextMenu"; import { MenuItemWithHelp } from "../../contextMenu/MenuWithHelp"; import { useBoxedExpressionEditorI18n } from "../../i18n"; @@ -320,10 +320,13 @@ export function ExpressionDefinitionLogicTypeSelector({ const newIdsByOriginalId = mutateExpressionRandomizingIds(clipboard.expression); let oldExpression: Normalized | undefined; - setExpression((prev: Normalized) => { - oldExpression = prev; - return clipboard.expression; - }); // This is mutated to have new IDs by the ID randomizer above. + setExpression({ + setExpressionAction: (prev: Normalized) => { + oldExpression = prev; + return clipboard.expression; + }, // This is mutated to have new IDs by the ID randomizer above. + expressionChangedArgs: { action: Action.ExpressionPastedFromClipboard }, + }); setWidthsById(({ newMap }) => { for (const id of findAllIdsDeep(oldExpression)) { diff --git a/packages/boxed-expression-component/src/expressions/FilterExpression/FilterExpressionCollectionCell.tsx b/packages/boxed-expression-component/src/expressions/FilterExpression/FilterExpressionCollectionCell.tsx index b1258afaec0..ecd8c78e6a0 100644 --- a/packages/boxed-expression-component/src/expressions/FilterExpression/FilterExpressionCollectionCell.tsx +++ b/packages/boxed-expression-component/src/expressions/FilterExpression/FilterExpressionCollectionCell.tsx @@ -37,15 +37,18 @@ export function FilterExpressionCollectionCell({ const { setExpression } = useBoxedExpressionEditorDispatch(); const onSetExpression = useCallback( - ({ getNewExpression }) => { - setExpression((prev: Normalized) => { - return { - ...prev, - in: { - ...prev.in, - expression: getNewExpression(prev.in.expression), - }, - }; + ({ getNewExpression, expressionChangedArgs }) => { + setExpression({ + setExpressionAction: (prev: Normalized) => { + return { + ...prev, + in: { + ...prev.in, + expression: getNewExpression(prev.in.expression), + }, + }; + }, + expressionChangedArgs, }); }, [setExpression] diff --git a/packages/boxed-expression-component/src/expressions/FilterExpression/FilterExpressionComponent.tsx b/packages/boxed-expression-component/src/expressions/FilterExpression/FilterExpressionComponent.tsx index 0ab60b7b5e8..5a992209529 100644 --- a/packages/boxed-expression-component/src/expressions/FilterExpression/FilterExpressionComponent.tsx +++ b/packages/boxed-expression-component/src/expressions/FilterExpression/FilterExpressionComponent.tsx @@ -18,6 +18,7 @@ */ import { + Action, BeeTableContextMenuAllowedOperationsConditions, BeeTableHeaderVisibility, BeeTableOperation, @@ -25,6 +26,7 @@ import { BeeTableProps, BoxedFilter, DmnBuiltInDataType, + ExpressionChangedArgs, Normalized, } from "../../api"; import { BeeTable, BeeTableColumnUpdate } from "../../table/BeeTable"; @@ -150,18 +152,40 @@ export function FilterExpressionComponent({ const onColumnUpdates = useCallback( ([{ name, typeRef }]: BeeTableColumnUpdate[]) => { - setExpression((prev: Normalized) => { - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { - ...prev, - "@_label": name, - "@_typeRef": typeRef, - }; + const expressionChangedArgs: ExpressionChangedArgs = { + action: Action.VariableChanged, + variableUuid: expressionHolderId, + typeChange: + typeRef !== filterExpression["@_typeRef"] + ? { + from: filterExpression["@_typeRef"] ?? "", + to: typeRef, + } + : undefined, + nameChange: + name !== filterExpression["@_label"] + ? { + from: filterExpression["@_label"] ?? "", + to: name, + } + : undefined, + }; + + setExpression({ + setExpressionAction: (prev: Normalized) => { + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: Normalized = { + ...prev, + "@_label": name, + "@_typeRef": typeRef, + }; - return ret; + return ret; + }, + expressionChangedArgs, }); }, - [setExpression] + [expressionHolderId, filterExpression, setExpression] ); return ( diff --git a/packages/boxed-expression-component/src/expressions/FilterExpression/FilterExpressionMatchCell.tsx b/packages/boxed-expression-component/src/expressions/FilterExpression/FilterExpressionMatchCell.tsx index 8d152a66367..5715a7b2063 100644 --- a/packages/boxed-expression-component/src/expressions/FilterExpression/FilterExpressionMatchCell.tsx +++ b/packages/boxed-expression-component/src/expressions/FilterExpression/FilterExpressionMatchCell.tsx @@ -56,20 +56,23 @@ export function FilterExpressionMatchCell({ }, [beeGwtService, expression, isActive]); const onSetExpression = useCallback( - ({ getNewExpression }) => { - setExpression((prev: Normalized) => { - const newExpression = getNewExpression(prev.match.expression); + ({ getNewExpression, expressionChangedArgs }) => { + setExpression({ + setExpressionAction: (prev: Normalized) => { + const newExpression = getNewExpression(prev.match.expression); - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { - ...prev, - match: { - ...prev.match, - expression: newExpression!, // SPEC DISCREPANCY - }, - }; + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: Normalized = { + ...prev, + match: { + ...prev.match, + expression: newExpression!, // SPEC DISCREPANCY + }, + }; - return ret; + return ret; + }, + expressionChangedArgs, }); }, [setExpression] diff --git a/packages/boxed-expression-component/src/expressions/FunctionExpression/FeelFunctionExpression.tsx b/packages/boxed-expression-component/src/expressions/FunctionExpression/FeelFunctionExpression.tsx index 2fa6dfb2bbe..47a834b142a 100644 --- a/packages/boxed-expression-component/src/expressions/FunctionExpression/FeelFunctionExpression.tsx +++ b/packages/boxed-expression-component/src/expressions/FunctionExpression/FeelFunctionExpression.tsx @@ -21,6 +21,7 @@ import * as React from "react"; import { useCallback, useMemo } from "react"; import * as ReactTable from "react-table"; import { + Action, BeeTableCellProps, BeeTableContextMenuAllowedOperationsConditions, BeeTableHeaderVisibility, @@ -31,6 +32,7 @@ import { BoxedFunction, BoxedFunctionKind, DmnBuiltInDataType, + ExpressionChangedArgs, Normalized, } from "../../api"; import { useBoxedExpressionEditorI18n } from "../../i18n"; @@ -111,18 +113,39 @@ export function FeelFunctionExpression({ const onColumnUpdates = useCallback( ([{ name, typeRef }]: BeeTableColumnUpdate[]) => { - setExpression((prev: Normalized) => { - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { - ...prev, - "@_label": name, - "@_typeRef": typeRef, - }; + const expressionChangedArgs: ExpressionChangedArgs = { + action: Action.VariableChanged, + variableUuid: expressionHolderId, + typeChange: + typeRef !== functionExpression["@_typeRef"] + ? { + from: functionExpression["@_typeRef"] ?? "", + to: typeRef, + } + : undefined, + nameChange: + name !== functionExpression["@_label"] + ? { + from: functionExpression["@_label"] ?? "", + to: name, + } + : undefined, + }; + setExpression({ + setExpressionAction: (prev: Normalized) => { + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: Normalized = { + ...prev, + "@_label": name, + "@_typeRef": typeRef, + }; - return ret; + return ret; + }, + expressionChangedArgs, }); }, - [setExpression] + [expressionHolderId, functionExpression, setExpression] ); const beeTableOperationConfig = useMemo(() => { @@ -156,9 +179,12 @@ export function FeelFunctionExpression({ const onRowReset = useCallback(() => { let oldExpression: Normalized | undefined; - setExpression((prev: Normalized) => { - oldExpression = prev.expression; - return undefined!; // SPEC DISCREPANCY + setExpression({ + setExpressionAction: (prev: Normalized) => { + oldExpression = prev.expression; + return undefined!; // SPEC DISCREPANCY + }, + expressionChangedArgs: { action: Action.RowReset, rowIndex: 0 }, }); setWidthsById(({ newMap }) => { for (const id of findAllIdsDeep(oldExpression)) { @@ -243,17 +269,22 @@ export function FeelFunctionImplementationCell({ const onSetExpression = useCallback( ({ getNewExpression, + expressionChangedArgs, }: { getNewExpression: (prev: Normalized) => Normalized; + expressionChangedArgs: ExpressionChangedArgs; }) => { - setExpression((prev: Normalized) => { - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { - ...prev, - expression: getNewExpression(prev.expression ?? undefined!), - }; + setExpression({ + setExpressionAction: (prev: Normalized) => { + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: Normalized = { + ...prev, + expression: getNewExpression(prev.expression ?? undefined!), + }; - return ret; + return ret; + }, + expressionChangedArgs, }); }, [setExpression] diff --git a/packages/boxed-expression-component/src/expressions/FunctionExpression/FunctionExpression.tsx b/packages/boxed-expression-component/src/expressions/FunctionExpression/FunctionExpression.tsx index 0ed16bae669..fcdf33754fc 100644 --- a/packages/boxed-expression-component/src/expressions/FunctionExpression/FunctionExpression.tsx +++ b/packages/boxed-expression-component/src/expressions/FunctionExpression/FunctionExpression.tsx @@ -20,7 +20,7 @@ import _ from "lodash"; import * as React from "react"; import { useCallback, useMemo } from "react"; -import { BoxedFunction, BoxedFunctionKind, DmnBuiltInDataType, generateUuid, Normalized } from "../../api"; +import { Action, BoxedFunction, BoxedFunctionKind, DmnBuiltInDataType, generateUuid, Normalized } from "../../api"; import { PopoverMenu } from "../../contextMenu/PopoverMenu"; import { useBoxedExpressionEditorI18n } from "../../i18n"; import { useBoxedExpressionEditor, useBoxedExpressionEditorDispatch } from "../../BoxedExpressionEditorContext"; @@ -77,61 +77,64 @@ export function useFunctionExpressionControllerCell(functionKind: Normalized) => { - setExpression((prev: Normalized) => { - if (kind === BoxedFunctionKind.Feel) { - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const retFeel: Normalized = { - __$$element: "functionDefinition", - "@_label": prev["@_label"], - "@_id": generateUuid(), - "@_kind": BoxedFunctionKind.Feel, - "@_typeRef": undefined, - expression: { - __$$element: "literalExpression", + setExpression({ + setExpressionAction: (prev: Normalized) => { + if (kind === BoxedFunctionKind.Feel) { + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const retFeel: Normalized = { + __$$element: "functionDefinition", + "@_label": prev["@_label"], "@_id": generateUuid(), + "@_kind": BoxedFunctionKind.Feel, "@_typeRef": undefined, - }, - formalParameter: [], - }; - return retFeel; - } else if (kind === BoxedFunctionKind.Java) { - const expressionId = generateUuid(); + expression: { + __$$element: "literalExpression", + "@_id": generateUuid(), + "@_typeRef": undefined, + }, + formalParameter: [], + }; + return retFeel; + } else if (kind === BoxedFunctionKind.Java) { + const expressionId = generateUuid(); - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const retJava: Normalized = { - __$$element: "functionDefinition", - "@_label": prev["@_label"], - "@_id": expressionId, - expression: { - __$$element: "context", - "@_id": generateUuid(), - }, - "@_kind": BoxedFunctionKind.Java, - "@_typeRef": undefined, - formalParameter: [], - }; - return retJava; - } else if (kind === BoxedFunctionKind.Pmml) { - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const retPmml: Normalized = { - __$$element: "functionDefinition", - "@_label": prev["@_label"], - "@_id": generateUuid(), - expression: { - __$$element: "context", + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const retJava: Normalized = { + __$$element: "functionDefinition", + "@_label": prev["@_label"], + "@_id": expressionId, + expression: { + __$$element: "context", + "@_id": generateUuid(), + }, + "@_kind": BoxedFunctionKind.Java, + "@_typeRef": undefined, + formalParameter: [], + }; + return retJava; + } else if (kind === BoxedFunctionKind.Pmml) { + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const retPmml: Normalized = { + __$$element: "functionDefinition", + "@_label": prev["@_label"], "@_id": generateUuid(), - }, - "@_kind": BoxedFunctionKind.Pmml, - "@_typeRef": undefined, - formalParameter: [], - }; - return retPmml; - } else { - throw new Error("Shouldn't ever reach this point."); - } + expression: { + __$$element: "context", + "@_id": generateUuid(), + }, + "@_kind": BoxedFunctionKind.Pmml, + "@_typeRef": undefined, + formalParameter: [], + }; + return retPmml; + } else { + throw new Error("Shouldn't ever reach this point."); + } + }, + expressionChangedArgs: { action: Action.FunctionKindChanged, from: functionKind, to: kind }, }); }, - [setExpression] + [functionKind, setExpression] ); return useMemo( diff --git a/packages/boxed-expression-component/src/expressions/FunctionExpression/JavaFunctionExpression.tsx b/packages/boxed-expression-component/src/expressions/FunctionExpression/JavaFunctionExpression.tsx index 5f2b882f6a8..96561ec9fe1 100644 --- a/packages/boxed-expression-component/src/expressions/FunctionExpression/JavaFunctionExpression.tsx +++ b/packages/boxed-expression-component/src/expressions/FunctionExpression/JavaFunctionExpression.tsx @@ -23,6 +23,7 @@ import * as React from "react"; import { useCallback, useEffect, useMemo } from "react"; import * as ReactTable from "react-table"; import { + Action, BeeTableCellProps, BeeTableContextMenuAllowedOperationsConditions, BeeTableHeaderVisibility, @@ -32,6 +33,7 @@ import { BoxedFunction, BoxedFunctionKind, DmnBuiltInDataType, + ExpressionChangedArgs, generateUuid, Normalized, } from "../../api"; @@ -185,17 +187,39 @@ export function JavaFunctionExpression({ const onColumnUpdates = useCallback( ([{ name, typeRef }]: BeeTableColumnUpdate[]) => { - setExpression((prev: Normalized) => { - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { - ...prev, - "@_label": name, - "@_typeRef": typeRef, - }; - return ret; + const expressionChangedArgs: ExpressionChangedArgs = { + action: Action.VariableChanged, + variableUuid: expressionHolderId, + typeChange: + typeRef !== functionExpression["@_typeRef"] + ? { + from: functionExpression["@_typeRef"] ?? "", + to: typeRef, + } + : undefined, + nameChange: + name !== functionExpression["@_label"] + ? { + from: functionExpression["@_label"] ?? "", + to: name, + } + : undefined, + }; + + setExpression({ + setExpressionAction: (prev: Normalized) => { + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: Normalized = { + ...prev, + "@_label": name, + "@_typeRef": typeRef, + }; + return ret; + }, + expressionChangedArgs, }); }, - [setExpression] + [expressionHolderId, functionExpression, setExpression] ); // It is always a Context @@ -236,14 +260,17 @@ export function JavaFunctionExpression({ }, []); const onRowReset = useCallback(() => { - setExpression((prev: Normalized) => { - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { - ...prev, - expression: undefined!, - }; + setExpression({ + setExpressionAction: (prev: Normalized) => { + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: Normalized = { + ...prev, + expression: undefined!, + }; - return ret; + return ret; + }, + expressionChangedArgs: { action: Action.RowReset, rowIndex: 0 }, }); }, [setExpression]); @@ -331,50 +358,64 @@ export function JavaFunctionExpression({ // Class if (u.rowIndex === 0) { - setExpression((prev: Normalized) => { - clazz.expression = { - ...clazz.expression, - __$$element: "literalExpression", - text: { - __$$text: u.value, - }, - }; - - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { - ...prev, - expression: { - __$$element: "context", - ...context, - contextEntry: [clazz, method], - }, - }; - - return ret; + setExpression({ + setExpressionAction: (prev: Normalized) => { + clazz.expression = { + ...clazz.expression, + __$$element: "literalExpression", + text: { + __$$text: u.value, + }, + }; + + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: Normalized = { + ...prev, + expression: { + __$$element: "context", + ...context, + contextEntry: [clazz, method], + }, + }; + + return ret; + }, + expressionChangedArgs: { + action: Action.LiteralTextExpressionChanged, + from: clazz.expression.__$$element === "literalExpression" ? clazz.expression.text?.__$$text ?? "" : "", + to: u.value, + }, }); } // Method else if (u.rowIndex === 1) { - setExpression((prev: Normalized) => { - method.expression = { - ...method.expression, - __$$element: "literalExpression", - "@_id": method.expression["@_id"] ?? generateUuid(), - text: { - __$$text: u.value, - }, - }; - - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { - ...prev, - expression: { - __$$element: "context", - ...context, - contextEntry: [clazz, method], - }, - }; - return ret; + setExpression({ + setExpressionAction: (prev: Normalized) => { + method.expression = { + ...method.expression, + __$$element: "literalExpression", + "@_id": method.expression["@_id"] ?? generateUuid(), + text: { + __$$text: u.value, + }, + }; + + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: Normalized = { + ...prev, + expression: { + __$$element: "context", + ...context, + contextEntry: [clazz, method], + }, + }; + return ret; + }, + expressionChangedArgs: { + action: Action.LiteralTextExpressionChanged, + from: method.expression.__$$element === "literalExpression" ? method.expression.text?.__$$text ?? "" : "", + to: u.value, + }, }); } } diff --git a/packages/boxed-expression-component/src/expressions/FunctionExpression/ParametersPopover.tsx b/packages/boxed-expression-component/src/expressions/FunctionExpression/ParametersPopover.tsx index e04e2b2f837..3dd25927be5 100644 --- a/packages/boxed-expression-component/src/expressions/FunctionExpression/ParametersPopover.tsx +++ b/packages/boxed-expression-component/src/expressions/FunctionExpression/ParametersPopover.tsx @@ -24,7 +24,7 @@ import { CubesIcon } from "@patternfly/react-icons/dist/js/icons/cubes-icon"; import { OutlinedTrashAltIcon } from "@patternfly/react-icons/dist/js/icons/outlined-trash-alt-icon"; import * as React from "react"; import { ChangeEvent, useCallback } from "react"; -import { BoxedFunction, generateUuid, getNextAvailablePrefixedName, Normalized } from "../../api"; +import { Action, BoxedFunction, generateUuid, getNextAvailablePrefixedName, Normalized } from "../../api"; import { useBoxedExpressionEditorI18n } from "../../i18n"; import { useBoxedExpressionEditorDispatch } from "../../BoxedExpressionEditorContext"; import { DMN15__tInformationItem } from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types"; @@ -42,26 +42,29 @@ export const ParametersPopover: React.FunctionComponent const addParameter = useCallback( (e: React.MouseEvent) => { e.stopPropagation(); - setExpression((prev: Normalized) => { - const newParameters = [ - ...(prev.formalParameter ?? []), - { - "@_id": generateUuid(), - "@_name": getNextAvailablePrefixedName( - (prev.formalParameter ?? []).map((p) => p["@_name"]), - "p" - ), - "@_typeRef": undefined, - }, - ]; + setExpression({ + setExpressionAction: (prev: Normalized) => { + const newParameters = [ + ...(prev.formalParameter ?? []), + { + "@_id": generateUuid(), + "@_name": getNextAvailablePrefixedName( + (prev.formalParameter ?? []).map((p) => p["@_name"]), + "p" + ), + "@_typeRef": undefined, + }, + ]; - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { - ...prev, - formalParameter: newParameters, - }; + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: Normalized = { + ...prev, + formalParameter: newParameters, + }; - return ret; + return ret; + }, + expressionChangedArgs: { action: Action.FunctionParameterAdded }, }); }, [setExpression] @@ -101,39 +104,52 @@ function ParameterEntry({ parameter, index }: { parameter: DMN15__tInformationIt const onNameChange = useCallback( (e: ChangeEvent) => { e.stopPropagation(); - setExpression((prev: Normalized) => { - const newParameters = [...(prev.formalParameter ?? [])]; - newParameters[index] = { - ...newParameters[index], - "@_name": e.target.value, - }; - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { - ...prev, - formalParameter: newParameters, - }; + setExpression({ + setExpressionAction: (prev: Normalized) => { + const newParameters = [...(prev.formalParameter ?? [])]; + newParameters[index] = { + ...newParameters[index], + "@_name": e.target.value, + }; + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: Normalized = { + ...prev, + formalParameter: newParameters, + }; - return ret; + return ret; + }, + expressionChangedArgs: { + action: Action.VariableChanged, + variableUuid: parameter["@_id"] ?? "", + nameChange: { + from: parameter["@_name"], + to: e.target.value, + }, + }, }); }, - [index, setExpression] + [index, parameter, setExpression] ); const onDataTypeChange = useCallback( (typeRef: string | undefined) => { - setExpression((prev: Normalized) => { - const newParameters = [...(prev.formalParameter ?? [])]; - newParameters[index] = { - ...newParameters[index], - "@_typeRef": typeRef, - }; - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { - ...prev, - formalParameter: newParameters, - }; + setExpression({ + setExpressionAction: (prev: Normalized) => { + const newParameters = [...(prev.formalParameter ?? [])]; + newParameters[index] = { + ...newParameters[index], + "@_typeRef": typeRef, + }; + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: Normalized = { + ...prev, + formalParameter: newParameters, + }; - return ret; + return ret; + }, + expressionChangedArgs: { action: Action.FunctionParameterTypeChanged }, }); }, [index, setExpression] @@ -142,16 +158,19 @@ function ParameterEntry({ parameter, index }: { parameter: DMN15__tInformationIt const onParameterRemove = useCallback( (e: React.MouseEvent) => { e.stopPropagation(); - setExpression((prev: Normalized) => { - const newParameters = [...(prev.formalParameter ?? [])]; - newParameters.splice(index, 1); - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { - ...prev, - formalParameter: newParameters, - }; + setExpression({ + setExpressionAction: (prev: Normalized) => { + const newParameters = [...(prev.formalParameter ?? [])]; + newParameters.splice(index, 1); + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: Normalized = { + ...prev, + formalParameter: newParameters, + }; - return ret; + return ret; + }, + expressionChangedArgs: { action: Action.FunctionParameterRemoved }, }); }, [index, setExpression] diff --git a/packages/boxed-expression-component/src/expressions/FunctionExpression/PmmlFunctionExpression.tsx b/packages/boxed-expression-component/src/expressions/FunctionExpression/PmmlFunctionExpression.tsx index 4534c9e258d..33da87038be 100644 --- a/packages/boxed-expression-component/src/expressions/FunctionExpression/PmmlFunctionExpression.tsx +++ b/packages/boxed-expression-component/src/expressions/FunctionExpression/PmmlFunctionExpression.tsx @@ -22,6 +22,7 @@ import * as React from "react"; import { useCallback, useEffect, useMemo } from "react"; import * as ReactTable from "react-table"; import { + Action, BeeTableCellProps, BeeTableContextMenuAllowedOperationsConditions, BeeTableHeaderVisibility, @@ -31,6 +32,7 @@ import { BoxedFunction, BoxedFunctionKind, DmnBuiltInDataType, + ExpressionChangedArgs, generateUuid, Normalized, } from "../../api"; @@ -136,18 +138,40 @@ export function PmmlFunctionExpression({ const onColumnUpdates = useCallback( ([{ name, typeRef: dataType }]: BeeTableColumnUpdate[]) => { - setExpression((prev: Normalized) => { - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { - ...prev, - "@_label": name, - "@_typeRef": dataType, - }; + const expressionChangedArgs: ExpressionChangedArgs = { + action: Action.VariableChanged, + variableUuid: expressionHolderId, + typeChange: + dataType !== functionExpression["@_typeRef"] + ? { + from: functionExpression["@_typeRef"] ?? "", + to: dataType, + } + : undefined, + nameChange: + name !== functionExpression["@_label"] + ? { + from: functionExpression["@_label"] ?? "", + to: name, + } + : undefined, + }; - return ret; + setExpression({ + setExpressionAction: (prev: Normalized) => { + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: Normalized = { + ...prev, + "@_label": name, + "@_typeRef": dataType, + }; + + return ret; + }, + expressionChangedArgs, }); }, - [setExpression] + [expressionHolderId, functionExpression, setExpression] ); const beeTableOperationConfig = useMemo(() => { @@ -201,14 +225,17 @@ export function PmmlFunctionExpression({ }, []); const onRowReset = useCallback(() => { - setExpression((prev: Normalized) => { - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { - ...prev, - expression: undefined!, - }; + setExpression({ + setExpressionAction: (prev: Normalized) => { + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: Normalized = { + ...prev, + expression: undefined!, + }; - return ret; + return ret; + }, + expressionChangedArgs: { action: Action.RowReset, rowIndex: 0 }, }); }, [setExpression]); @@ -430,11 +457,14 @@ function PmmlFunctionExpressionDocumentCell(props: React.PropsWithChildren { setSelectOpen(false); - setExpression((prev: Normalized) => { - return getUpdatedExpression(prev, newDocument, ""); + setExpression({ + setExpressionAction: (prev: Normalized) => { + return getUpdatedExpression(prev, newDocument, ""); + }, + expressionChangedArgs: { action: Action.ExpressionCreated }, }); }, - [setExpression] + [pmmlFunctionExpression, setExpression] ); const [isSelectOpen, setSelectOpen] = React.useState(false); @@ -487,12 +517,15 @@ function PmmlFunctionExpressionModelCell(props: React.PropsWithChildren { setSelectOpen(false); - setExpression((prev: Normalized) => { - const document = getDocumentEntry(prev); - const currentDocument = - document.expression?.__$$element === "literalExpression" ? document.expression.text?.__$$text ?? "" : ""; + setExpression({ + setExpressionAction: (prev: Normalized) => { + const document = getDocumentEntry(prev); + const currentDocument = + document.expression?.__$$element === "literalExpression" ? document.expression.text?.__$$text ?? "" : ""; - return getUpdatedExpression(prev, currentDocument, newModel); + return getUpdatedExpression(prev, currentDocument, newModel); + }, + expressionChangedArgs: { action: Action.LiteralTextExpressionChanged, from: "", to: newModel }, }); }, [setExpression] diff --git a/packages/boxed-expression-component/src/expressions/InvocationExpression/ArgumentEntryExpressionCell.tsx b/packages/boxed-expression-component/src/expressions/InvocationExpression/ArgumentEntryExpressionCell.tsx index c979ed4678f..183d1f775cf 100644 --- a/packages/boxed-expression-component/src/expressions/InvocationExpression/ArgumentEntryExpressionCell.tsx +++ b/packages/boxed-expression-component/src/expressions/InvocationExpression/ArgumentEntryExpressionCell.tsx @@ -46,21 +46,24 @@ export const ArgumentEntryExpressionCell: React.FunctionComponent< }, [beeGwtService, columnIndex, expression, isActive]); const onSetExpression = useCallback( - ({ getNewExpression }) => { - setExpression((prev: Normalized) => { - const newBindings = [...(prev.binding ?? [])]; - newBindings[index] = { - ...newBindings[index], - expression: getNewExpression(newBindings[index]?.expression ?? undefined!), - }; + ({ getNewExpression, expressionChangedArgs }) => { + setExpression({ + setExpressionAction: (prev: Normalized) => { + const newBindings = [...(prev.binding ?? [])]; + newBindings[index] = { + ...newBindings[index], + expression: getNewExpression(newBindings[index]?.expression ?? undefined!), + }; - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { - ...prev, - binding: newBindings, - }; + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: Normalized = { + ...prev, + binding: newBindings, + }; - return ret; + return ret; + }, + expressionChangedArgs, }); }, [index, setExpression] diff --git a/packages/boxed-expression-component/src/expressions/InvocationExpression/InvocationExpression.tsx b/packages/boxed-expression-component/src/expressions/InvocationExpression/InvocationExpression.tsx index f66e3d61a97..d9d34e427c6 100644 --- a/packages/boxed-expression-component/src/expressions/InvocationExpression/InvocationExpression.tsx +++ b/packages/boxed-expression-component/src/expressions/InvocationExpression/InvocationExpression.tsx @@ -21,6 +21,7 @@ import * as React from "react"; import { useCallback, useEffect, useMemo } from "react"; import * as ReactTable from "react-table"; import { + Action, BeeTableContextMenuAllowedOperationsConditions, BeeTableHeaderVisibility, BeeTableOperation, @@ -231,43 +232,60 @@ export function InvocationExpression({ (columnUpdates: BeeTableColumnUpdate[]) => { for (const u of columnUpdates) { if (u.column.originalId === id) { - setExpression((prev: Normalized) => ({ - ...prev, - "@_id": prev["@_id"], - name: u.name, - })); - } else if (u.column.originalId === invocationId) { - setExpression((prev: Normalized) => { - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { + setExpression({ + setExpressionAction: (prev: Normalized) => ({ ...prev, - expression: { - ...prev.expression, - "@_id": prev.expression?.["@_id"] ?? generateUuid(), - __$$element: "literalExpression", - text: { - __$$text: u.name, + "@_id": prev["@_id"], + name: u.name, + }), + expressionChangedArgs: { + action: Action.VariableChanged, + variableUuid: expressionHolderId, + nameChange: { from: invocationExpression["@_label"] ?? "", to: u.name }, + }, + }); + } else if (u.column.originalId === invocationId) { + setExpression({ + setExpressionAction: (prev: Normalized) => { + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: Normalized = { + ...prev, + expression: { + ...prev.expression, + "@_id": prev.expression?.["@_id"] ?? generateUuid(), + __$$element: "literalExpression", + text: { + __$$text: u.name, + }, }, - }, - }; - return ret; + }; + return ret; + }, + expressionChangedArgs: { + action: Action.VariableChanged, + variableUuid: expressionHolderId, + nameChange: { from: invocationExpression["@_label"] ?? "", to: u.name }, + }, }); } else { - setExpression((prev: Normalized) => { - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { - ...prev, - "@_id": prev["@_id"] ?? generateUuid(), - "@_typeRef": u.typeRef, - "@_label": u.name, - }; - - return ret; + setExpression({ + setExpressionAction: (prev: Normalized) => { + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: Normalized = { + ...prev, + "@_id": prev["@_id"] ?? generateUuid(), + "@_typeRef": u.typeRef, + "@_label": u.name, + }; + + return ret; + }, + expressionChangedArgs: { action: Action.ExpressionCreated }, }); } } }, - [setExpression, id, invocationId] + [id, invocationId, setExpression, expressionHolderId, invocationExpression] ); const headerVisibility = useMemo( @@ -281,19 +299,22 @@ export function InvocationExpression({ const updateParameter = useCallback( (index: number, { expression, variable }: ExpressionWithVariable) => { - setExpression((prev: Normalized) => { - const newArgumentEntries = [...(prev.binding ?? [])]; - newArgumentEntries[index] = { - parameter: variable, - expression: expression, - }; - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { - ...prev, - binding: newArgumentEntries, - }; + setExpression({ + setExpressionAction: (prev: Normalized) => { + const newArgumentEntries = [...(prev.binding ?? [])]; + newArgumentEntries[index] = { + parameter: variable, + expression: expression, + }; + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: Normalized = { + ...prev, + binding: newArgumentEntries, + }; - return ret; + return ret; + }, + expressionChangedArgs: { action: Action.InvocationParametersChanged }, }); }, [setExpression] @@ -359,20 +380,23 @@ export function InvocationExpression({ names.push(name); newEntries.push(getDefaultArgumentEntry(name)); } - setExpression((prev: Normalized) => { - const newArgumentEntries = [...(prev.binding ?? [])]; + setExpression({ + setExpressionAction: (prev: Normalized) => { + const newArgumentEntries = [...(prev.binding ?? [])]; - for (const newEntry of newEntries) { - newArgumentEntries.splice(args.beforeIndex, 0, newEntry); - } + for (const newEntry of newEntries) { + newArgumentEntries.splice(args.beforeIndex, 0, newEntry); + } - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { - ...prev, - binding: newArgumentEntries, - }; + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: Normalized = { + ...prev, + binding: newArgumentEntries, + }; - return ret; + return ret; + }, + expressionChangedArgs: { action: Action.RowsAdded, rowIndex: args.beforeIndex, rowsCount: args.rowsCount }, }); }, [getDefaultArgumentEntry, invocationExpression.binding, setExpression] @@ -381,17 +405,20 @@ export function InvocationExpression({ const onRowDeleted = useCallback( (args: { rowIndex: number }) => { let oldExpression: Normalized | undefined; - setExpression((prev: Normalized) => { - const newArgumentEntries = [...(prev.binding ?? [])]; - oldExpression = newArgumentEntries[args.rowIndex].expression; - newArgumentEntries.splice(args.rowIndex, 1); - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { - ...prev, - binding: newArgumentEntries, - }; + setExpression({ + setExpressionAction: (prev: Normalized) => { + const newArgumentEntries = [...(prev.binding ?? [])]; + oldExpression = newArgumentEntries[args.rowIndex].expression; + newArgumentEntries.splice(args.rowIndex, 1); + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: Normalized = { + ...prev, + binding: newArgumentEntries, + }; - return ret; + return ret; + }, + expressionChangedArgs: { action: Action.RowRemoved, rowIndex: args.rowIndex }, }); setWidthsById(({ newMap }) => { @@ -406,18 +433,21 @@ export function InvocationExpression({ const onRowReset = useCallback( (args: { rowIndex: number }) => { let oldExpression: Normalized | undefined; - setExpression((prev: Normalized) => { - const newArgumentEntries = [...(prev.binding ?? [])]; - oldExpression = newArgumentEntries[args.rowIndex].expression; - const defaultArgumentEntry = getDefaultArgumentEntry(newArgumentEntries[args.rowIndex].parameter["@_name"]); - newArgumentEntries.splice(args.rowIndex, 1, defaultArgumentEntry); - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { - ...prev, - binding: newArgumentEntries, - }; + setExpression({ + setExpressionAction: (prev: Normalized) => { + const newArgumentEntries = [...(prev.binding ?? [])]; + oldExpression = newArgumentEntries[args.rowIndex].expression; + const defaultArgumentEntry = getDefaultArgumentEntry(newArgumentEntries[args.rowIndex].parameter["@_name"]); + newArgumentEntries.splice(args.rowIndex, 1, defaultArgumentEntry); + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: Normalized = { + ...prev, + binding: newArgumentEntries, + }; - return ret; + return ret; + }, + expressionChangedArgs: { action: Action.RowReset, rowIndex: args.rowIndex }, }); setWidthsById(({ newMap }) => { diff --git a/packages/boxed-expression-component/src/expressions/IteratorExpression/IteratorExpressionCell.tsx b/packages/boxed-expression-component/src/expressions/IteratorExpression/IteratorExpressionCell.tsx index 2e6262c6ba0..4dbf5549b67 100644 --- a/packages/boxed-expression-component/src/expressions/IteratorExpression/IteratorExpressionCell.tsx +++ b/packages/boxed-expression-component/src/expressions/IteratorExpression/IteratorExpressionCell.tsx @@ -43,37 +43,40 @@ export function IteratorExpressionCell({ const { setExpression } = useBoxedExpressionEditorDispatch(); const onSetExpression = useCallback( - ({ getNewExpression }) => { - setExpression((prev: Normalized) => { - switch (rowIndex) { - case 1: - return { - ...prev, - in: { - "@_id": generateUuid(), - expression: getNewExpression(prev.in.expression), - }, - }; - case 2: - default: - if (prev.__$$element === "for") { + ({ getNewExpression, expressionChangedArgs }) => { + setExpression({ + setExpressionAction: (prev: Normalized) => { + switch (rowIndex) { + case 1: return { ...prev, - return: { + in: { "@_id": generateUuid(), - expression: getNewExpression(prev.return.expression), + expression: getNewExpression(prev.in.expression), }, }; - } else { - return { - ...prev, - satisfies: { - "@_id": generateUuid(), - expression: getNewExpression(prev.satisfies.expression), - }, - }; - } - } + case 2: + default: + if (prev.__$$element === "for") { + return { + ...prev, + return: { + "@_id": generateUuid(), + expression: getNewExpression(prev.return.expression), + }, + }; + } else { + return { + ...prev, + satisfies: { + "@_id": generateUuid(), + expression: getNewExpression(prev.satisfies.expression), + }, + }; + } + } + }, + expressionChangedArgs, }); }, [rowIndex, setExpression] diff --git a/packages/boxed-expression-component/src/expressions/IteratorExpression/IteratorExpressionComponent.tsx b/packages/boxed-expression-component/src/expressions/IteratorExpression/IteratorExpressionComponent.tsx index 9791885ccf6..066f5ea601c 100644 --- a/packages/boxed-expression-component/src/expressions/IteratorExpression/IteratorExpressionComponent.tsx +++ b/packages/boxed-expression-component/src/expressions/IteratorExpression/IteratorExpressionComponent.tsx @@ -18,6 +18,7 @@ */ import { + Action, BeeTableHeaderVisibility, BeeTableOperation, BeeTableOperationConfig, @@ -25,6 +26,7 @@ import { BoxedFor, BoxedIterator, DmnBuiltInDataType, + ExpressionChangedArgs, generateUuid, Normalized, } from "../../api"; @@ -241,66 +243,91 @@ export function IteratorExpressionComponent({ const onColumnUpdates = useCallback( ([{ name, typeRef }]: BeeTableColumnUpdate[]) => { - setExpression((prev: Normalized) => { - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { - ...prev, - "@_label": name, - "@_typeRef": typeRef, - }; + const expressionChangedArgs: ExpressionChangedArgs = { + action: Action.VariableChanged, + variableUuid: expressionHolderId, + typeChange: + typeRef !== expression["@_typeRef"] + ? { + from: expression["@_typeRef"] ?? "", + to: typeRef, + } + : undefined, + nameChange: + name !== expression["@_label"] + ? { + from: expression["@_label"] ?? "", + to: name, + } + : undefined, + }; - return ret; - }); - }, - [setExpression] - ); - const onRowReset = useCallback( - (args: { rowIndex: number }) => { - setExpression((prev: Normalized) => { - if (args.rowIndex === 0) { - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { - ...prev, - "@_iteratorVariable": undefined, - }; - return ret; - } else if (args.rowIndex === 1) { + setExpression({ + setExpressionAction: (prev: Normalized) => { // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 const ret: Normalized = { ...prev, - in: { - "@_id": generateUuid(), - expression: undefined!, - }, // SPEC DISCREPANCY + "@_label": name, + "@_typeRef": typeRef, }; + return ret; - } else if (args.rowIndex === 2) { - if (prev.__$$element === "for") { + }, + expressionChangedArgs, + }); + }, + [expression, expressionHolderId, setExpression] + ); + const onRowReset = useCallback( + (args: { rowIndex: number }) => { + setExpression({ + setExpressionAction: (prev: Normalized) => { + if (args.rowIndex === 0) { // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { + const ret: Normalized = { ...prev, - return: { - "@_id": generateUuid(), - expression: undefined!, - }, // SPEC DISCREPANCY + "@_iteratorVariable": undefined, }; return ret; - } else if (prev.__$$element === "some" || prev.__$$element === "every") { + } else if (args.rowIndex === 1) { // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const iterator: Normalized = { + const ret: Normalized = { ...prev, - satisfies: { + in: { "@_id": generateUuid(), expression: undefined!, }, // SPEC DISCREPANCY }; - return iterator; + return ret; + } else if (args.rowIndex === 2) { + if (prev.__$$element === "for") { + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: Normalized = { + ...prev, + return: { + "@_id": generateUuid(), + expression: undefined!, + }, // SPEC DISCREPANCY + }; + return ret; + } else if (prev.__$$element === "some" || prev.__$$element === "every") { + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const iterator: Normalized = { + ...prev, + satisfies: { + "@_id": generateUuid(), + expression: undefined!, + }, // SPEC DISCREPANCY + }; + return iterator; + } else { + throw new Error("Nested expression type not supported in IteratorExpression."); + } } else { - throw new Error("Nested expression type not supported in IteratorExpression."); + throw new Error("IteratorExpression shouldn't have more than 3 rows."); } - } else { - throw new Error("IteratorExpression shouldn't have more than 3 rows."); - } + }, + expressionChangedArgs: { action: Action.RowReset, rowIndex: args.rowIndex }, }); }, [setExpression] diff --git a/packages/boxed-expression-component/src/expressions/IteratorExpression/IteratorExpressionVariableCell.tsx b/packages/boxed-expression-component/src/expressions/IteratorExpression/IteratorExpressionVariableCell.tsx index 8bf8add74bb..eed07acfc52 100644 --- a/packages/boxed-expression-component/src/expressions/IteratorExpression/IteratorExpressionVariableCell.tsx +++ b/packages/boxed-expression-component/src/expressions/IteratorExpression/IteratorExpressionVariableCell.tsx @@ -19,7 +19,7 @@ import * as React from "react"; import { useEffect } from "react"; -import { BoxedIterator, Normalized } from "../../api"; +import { Action, BoxedIterator, ExpressionChangedArgs, Normalized } from "../../api"; import { useBoxedExpressionEditor, useBoxedExpressionEditorDispatch } from "../../BoxedExpressionEditorContext"; import { IteratorClause } from "./IteratorExpressionComponent"; import { InlineEditableTextInput } from "../../table/BeeTable/InlineEditableTextInput"; @@ -64,13 +64,30 @@ export function IteratorExpressionVariableCell({ { - setExpression((prev: Normalized) => { - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { - ...prev, - "@_iteratorVariable": updatedValue, - }; - return ret; + const expressionChangedArgs: ExpressionChangedArgs = + (data[rowIndex].child as string) === "" + ? { + action: Action.IteratorVariableDefined, + } + : { + action: Action.VariableChanged, + variableUuid: currentElementId, + nameChange: { + from: data[rowIndex].child as string, + to: updatedValue, + }, + }; + + setExpression({ + setExpressionAction: (prev: Normalized) => { + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: Normalized = { + ...prev, + "@_iteratorVariable": updatedValue, + }; + return ret; + }, + expressionChangedArgs, }); }} rowIndex={rowIndex} diff --git a/packages/boxed-expression-component/src/expressions/ListExpression/ListExpression.tsx b/packages/boxed-expression-component/src/expressions/ListExpression/ListExpression.tsx index f352159ba26..a037ea079c4 100644 --- a/packages/boxed-expression-component/src/expressions/ListExpression/ListExpression.tsx +++ b/packages/boxed-expression-component/src/expressions/ListExpression/ListExpression.tsx @@ -21,6 +21,7 @@ import * as React from "react"; import { useCallback, useMemo } from "react"; import * as ReactTable from "react-table"; import { + Action, BeeTableCellProps, BeeTableContextMenuAllowedOperationsConditions, BeeTableHeaderVisibility, @@ -30,6 +31,7 @@ import { BoxedExpression, BoxedList, DmnBuiltInDataType, + ExpressionChangedArgs, generateUuid, Normalized, } from "../../api"; @@ -156,25 +158,28 @@ export function ListExpression({ const onRowAdded = useCallback( (args: { beforeIndex: number; rowsCount: number }) => { - setExpression((prev: Normalized) => { - const newItems = [...(prev.expression ?? [])]; - const newListItems: Normalized[] = []; - - for (let i = 0; i < args.rowsCount; i++) { - newListItems.push(undefined!); // SPEC DISCREPANCY: Starting without an expression gives users the ability to select the expression type. - } - - for (const newEntry of newListItems) { - newItems.splice(args.beforeIndex, 0, newEntry); - } - - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { - ...prev, - expression: newItems, - }; - - return ret; + setExpression({ + setExpressionAction: (prev: Normalized) => { + const newItems = [...(prev.expression ?? [])]; + const newListItems: Normalized[] = []; + + for (let i = 0; i < args.rowsCount; i++) { + newListItems.push(undefined!); // SPEC DISCREPANCY: Starting without an expression gives users the ability to select the expression type. + } + + for (const newEntry of newListItems) { + newItems.splice(args.beforeIndex, 0, newEntry); + } + + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: Normalized = { + ...prev, + expression: newItems, + }; + + return ret; + }, + expressionChangedArgs: { action: Action.RowsAdded, rowIndex: args.beforeIndex, rowsCount: args.rowsCount }, }); }, [setExpression] @@ -183,18 +188,21 @@ export function ListExpression({ const onRowDeleted = useCallback( (args: { rowIndex: number }) => { let oldExpression: Normalized | undefined; - setExpression((prev: Normalized) => { - const newItems = [...(prev.expression ?? [])]; - oldExpression = newItems[args.rowIndex]; - newItems.splice(args.rowIndex, 1); - - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { - ...prev, - expression: newItems, - }; - - return ret; + setExpression({ + setExpressionAction: (prev: Normalized) => { + const newItems = [...(prev.expression ?? [])]; + oldExpression = newItems[args.rowIndex]; + newItems.splice(args.rowIndex, 1); + + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: Normalized = { + ...prev, + expression: newItems, + }; + + return ret; + }, + expressionChangedArgs: { action: Action.RowRemoved, rowIndex: args.rowIndex }, }); setWidthsById(({ newMap }) => { @@ -209,18 +217,21 @@ export function ListExpression({ const onRowReset = useCallback( (args: { rowIndex: number }) => { let oldExpression: Normalized | undefined; - setExpression((prev: Normalized) => { - const newItems = [...(prev.expression ?? [])]; - oldExpression = newItems[args.rowIndex]; - newItems.splice(args.rowIndex, 1, undefined!); // SPEC DISCREPANCY: Starting without an expression gives users the ability to select the expression type. - - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { - ...prev, - expression: newItems, - }; - - return ret; + setExpression({ + setExpressionAction: (prev: Normalized) => { + const newItems = [...(prev.expression ?? [])]; + oldExpression = newItems[args.rowIndex]; + newItems.splice(args.rowIndex, 1, undefined!); // SPEC DISCREPANCY: Starting without an expression gives users the ability to select the expression type. + + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: Normalized = { + ...prev, + expression: newItems, + }; + + return ret; + }, + expressionChangedArgs: { action: Action.RowReset, rowIndex: args.rowIndex }, }); setWidthsById(({ newMap }) => { @@ -238,18 +249,40 @@ export function ListExpression({ const onColumnUpdates = useCallback( ([{ name, typeRef }]: BeeTableColumnUpdate[]) => { - setExpression((prev: Normalized) => { - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { - ...prev, - "@_label": name, - "@_typeRef": typeRef, - }; - - return ret; + const expressionChangedArgs: ExpressionChangedArgs = { + action: Action.VariableChanged, + variableUuid: expressionHolderId, + typeChange: + typeRef !== listExpression["@_typeRef"] + ? { + from: listExpression["@_typeRef"] ?? "", + to: typeRef, + } + : undefined, + nameChange: + name !== listExpression["@_label"] + ? { + from: listExpression["@_label"] ?? "", + to: name, + } + : undefined, + }; + + setExpression({ + setExpressionAction: (prev: Normalized) => { + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: Normalized = { + ...prev, + "@_label": name, + "@_typeRef": typeRef, + }; + + return ret; + }, + expressionChangedArgs, }); }, - [setExpression] + [expressionHolderId, listExpression, setExpression] ); const allowedOperations = useCallback( diff --git a/packages/boxed-expression-component/src/expressions/ListExpression/ListItemCell.tsx b/packages/boxed-expression-component/src/expressions/ListExpression/ListItemCell.tsx index c2050a212db..7f8549ae86b 100644 --- a/packages/boxed-expression-component/src/expressions/ListExpression/ListItemCell.tsx +++ b/packages/boxed-expression-component/src/expressions/ListExpression/ListItemCell.tsx @@ -39,18 +39,21 @@ export function ListItemCell({ const { setExpression } = useBoxedExpressionEditorDispatch(); const onSetExpression = useCallback( - ({ getNewExpression }) => { - setExpression((prev: Normalized) => { - const newItems = [...(prev.expression ?? [])]; - newItems[rowIndex] = getNewExpression(newItems[rowIndex])!; // SPEC DISCREPANCY: Allowing undefined expression + ({ getNewExpression, expressionChangedArgs }) => { + setExpression({ + setExpressionAction: (prev: Normalized) => { + const newItems = [...(prev.expression ?? [])]; + newItems[rowIndex] = getNewExpression(newItems[rowIndex])!; // SPEC DISCREPANCY: Allowing undefined expression - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { - ...prev, - expression: newItems, - }; + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: Normalized = { + ...prev, + expression: newItems, + }; - return ret; + return ret; + }, + expressionChangedArgs, }); }, [rowIndex, setExpression] diff --git a/packages/boxed-expression-component/src/expressions/LiteralExpression/LiteralExpression.tsx b/packages/boxed-expression-component/src/expressions/LiteralExpression/LiteralExpression.tsx index 10458c7e1be..042a42d629c 100644 --- a/packages/boxed-expression-component/src/expressions/LiteralExpression/LiteralExpression.tsx +++ b/packages/boxed-expression-component/src/expressions/LiteralExpression/LiteralExpression.tsx @@ -21,11 +21,13 @@ import * as React from "react"; import { useCallback, useEffect, useMemo, useRef } from "react"; import * as ReactTable from "react-table"; import { + Action, BeeTableContextMenuAllowedOperationsConditions, BeeTableHeaderVisibility, BeeTableOperation, BoxedLiteral, DmnBuiltInDataType, + ExpressionChangedArgs, Normalized, } from "../../api"; import { useNestedExpressionContainer } from "../../resizing/NestedExpressionContainerContext"; @@ -63,10 +65,17 @@ export function LiteralExpression({ const setValue = useCallback( (value: string) => { - setExpression((prev: Normalized) => { - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { ...literalExpression, text: { __$$text: value } }; - return ret; + setExpression({ + setExpressionAction: (prev: Normalized) => { + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: Normalized = { ...literalExpression, text: { __$$text: value } }; + return ret; + }, + expressionChangedArgs: { + action: Action.LiteralTextExpressionChanged, + from: literalExpression?.text?.__$$text ?? "", + to: value, + }, }); }, [literalExpression, setExpression] @@ -86,15 +95,35 @@ export function LiteralExpression({ const onColumnUpdates = useCallback( ([{ name, typeRef }]: BeeTableColumnUpdate[]) => { - setExpression( - (): Normalized => ({ + const expressionChangedArgs: ExpressionChangedArgs = { + action: Action.VariableChanged, + variableUuid: expressionHolderId, + typeChange: + typeRef !== literalExpression["@_typeRef"] + ? { + from: literalExpression["@_typeRef"] ?? "", + to: typeRef, + } + : undefined, + nameChange: + name !== literalExpression["@_label"] + ? { + from: literalExpression["@_label"] ?? "", + to: name, + } + : undefined, + }; + + setExpression({ + setExpressionAction: (): Normalized => ({ ...literalExpression, "@_label": name, "@_typeRef": typeRef, - }) - ); + }), + expressionChangedArgs, + }); }, - [literalExpression, setExpression] + [expressionHolderId, literalExpression, setExpression] ); const setLiteralExpressionWidth = useCallback( diff --git a/packages/boxed-expression-component/src/expressions/RelationExpression/RelationExpression.tsx b/packages/boxed-expression-component/src/expressions/RelationExpression/RelationExpression.tsx index 68d81dcbaed..819c8661c17 100644 --- a/packages/boxed-expression-component/src/expressions/RelationExpression/RelationExpression.tsx +++ b/packages/boxed-expression-component/src/expressions/RelationExpression/RelationExpression.tsx @@ -22,12 +22,14 @@ import * as React from "react"; import { useCallback, useMemo, useRef } from "react"; import * as ReactTable from "react-table"; import { + Action, BeeTableContextMenuAllowedOperationsConditions, BeeTableHeaderVisibility, BeeTableOperation, BeeTableOperationConfig, BoxedRelation, DmnBuiltInDataType, + ExpressionChangedArgs, generateUuid, getNextAvailablePrefixedName, Normalized, @@ -205,65 +207,116 @@ export function RelationExpression({ const onCellUpdates = useCallback( (cellUpdates: BeeTableCellUpdate[]) => { - setExpression((prev: Normalized) => { - let previousExpression: Normalized = { ...prev }; - - cellUpdates.forEach((cellUpdate) => { - const newRows = [...(previousExpression.row ?? [])]; - const newExpressions = [...(newRows[cellUpdate.rowIndex].expression ?? [])]; - newExpressions[cellUpdate.columnIndex] = { - ...newExpressions[cellUpdate.columnIndex], - __$$element: "literalExpression", - text: { - __$$text: cellUpdate.value, - }, - }; - newRows[cellUpdate.rowIndex] = { - ...newRows[cellUpdate.rowIndex], - expression: newExpressions, - }; + setExpression({ + setExpressionAction: (prev: Normalized) => { + let previousExpression: Normalized = { ...prev }; + + cellUpdates.forEach((cellUpdate) => { + const newRows = [...(previousExpression.row ?? [])]; + const newExpressions = [...(newRows[cellUpdate.rowIndex].expression ?? [])]; + newExpressions[cellUpdate.columnIndex] = { + ...newExpressions[cellUpdate.columnIndex], + __$$element: "literalExpression", + text: { + __$$text: cellUpdate.value, + }, + }; + newRows[cellUpdate.rowIndex] = { + ...newRows[cellUpdate.rowIndex], + expression: newExpressions, + }; - previousExpression = { - ...previousExpression, - row: newRows, - }; - }); + previousExpression = { + ...previousExpression, + row: newRows, + }; + }); - return previousExpression; + return previousExpression; + }, + expressionChangedArgs: { action: Action.RelationCellsUpdated }, }); }, [setExpression] ); + const getExpressionChangedArgsFromColumnUpdates = useCallback( + (columnUpdates: BeeTableColumnUpdate[]) => { + // column.depth === 0 changes the Decision name and/or type which is a variable + const updateNodeNameOrType = columnUpdates.filter( + (update) => + update.column.depth === 0 && + (relationExpression["@_label"] !== update.name || relationExpression["@_typeRef"] !== update.typeRef) + ); + + if (updateNodeNameOrType.length > 1) { + throw new Error("Unexpected multiple name and/or type changed simultaneously in a Relation Expression."); + } + + if (updateNodeNameOrType.length === 1) { + const expressionChangedArgs: ExpressionChangedArgs = { + action: Action.VariableChanged, + variableUuid: expressionHolderId, + typeChange: + relationExpression["@_typeRef"] !== updateNodeNameOrType[0].typeRef + ? { + from: relationExpression["@_typeRef"], + to: updateNodeNameOrType[0].typeRef, + } + : undefined, + nameChange: + relationExpression["@_label"] !== updateNodeNameOrType[0].name + ? { + from: relationExpression["@_label"], + to: updateNodeNameOrType[0].name, + } + : undefined, + }; + + return expressionChangedArgs; + } else { + // Changes in other columns does not reflect in changes in variables + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const columnChangedAction: ExpressionChangedArgs = { action: Action.ColumnChanged }; + return columnChangedAction; + } + }, + [expressionHolderId, relationExpression] + ); + const onColumnUpdates = useCallback( (columnUpdates: BeeTableColumnUpdate[]) => { - setExpression((prev: Normalized) => { - const n = { ...prev }; - const newColumns = [...(prev.column ?? [])]; - - for (const u of columnUpdates) { - if (u.column.depth === 0) { - n["@_typeRef"] = u.typeRef; - n["@_label"] = u.name; - } else { - newColumns[u.columnIndex] = { - ...newColumns[u.columnIndex], - "@_name": u.name, - "@_typeRef": u.typeRef, - }; + const expressionChangedArgs = getExpressionChangedArgsFromColumnUpdates(columnUpdates); + setExpression({ + setExpressionAction: (prev: Normalized) => { + const n = { ...prev }; + const newColumns = [...(prev.column ?? [])]; + + for (const u of columnUpdates) { + if (u.column.depth === 0) { + n["@_typeRef"] = u.typeRef; + n["@_label"] = u.name; + } else { + newColumns[u.columnIndex] = { + ...newColumns[u.columnIndex], + "@_name": u.name, + "@_typeRef": u.typeRef, + }; + } } - } - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { - ...n, - column: newColumns, - }; + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: Normalized = { + ...n, + column: newColumns, + }; - return ret; + return ret; + }, + expressionChangedArgs, }); }, - [setExpression] + [getExpressionChangedArgsFromColumnUpdates, setExpression] ); const createCell = useCallback(() => { @@ -283,30 +336,33 @@ export function RelationExpression({ const onRowAdded = useCallback( (args: { beforeIndex: number; rowsCount: number }) => { - setExpression((prev: Normalized) => { - const newRows = [...(prev.row ?? [])]; - const newItems = []; - - for (let i = 0; i < args.rowsCount; i++) { - newItems.push({ - "@_id": generateUuid(), - expression: Array.from(new Array(prev.column?.length ?? 0)).map(() => { - return createCell(); - }), - }); - } + setExpression({ + setExpressionAction: (prev: Normalized) => { + const newRows = [...(prev.row ?? [])]; + const newItems = []; + + for (let i = 0; i < args.rowsCount; i++) { + newItems.push({ + "@_id": generateUuid(), + expression: Array.from(new Array(prev.column?.length ?? 0)).map(() => { + return createCell(); + }), + }); + } - for (const newEntry of newItems) { - newRows.splice(args.beforeIndex, 0, newEntry); - } + for (const newEntry of newItems) { + newRows.splice(args.beforeIndex, 0, newEntry); + } - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { - ...prev, - row: newRows, - }; + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: Normalized = { + ...prev, + row: newRows, + }; - return ret; + return ret; + }, + expressionChangedArgs: { action: Action.RowsAdded, rowsCount: args.rowsCount, rowIndex: args.beforeIndex }, }); }, [createCell, setExpression] @@ -314,44 +370,51 @@ export function RelationExpression({ const onColumnAdded = useCallback( (args: { beforeIndex: number; columnsCount: number }) => { - setExpression((prev: Normalized) => { - const newColumns = [...(prev.column ?? [])]; - - const newItems = []; - const availableNames = prev.column?.map((c) => c["@_name"]) ?? []; + setExpression({ + setExpressionAction: (prev: Normalized) => { + const newColumns = [...(prev.column ?? [])]; + + const newItems = []; + const availableNames = prev.column?.map((c) => c["@_name"]) ?? []; + + for (let i = 0; i < args.columnsCount; i++) { + const name = getNextAvailablePrefixedName(availableNames, "column"); + availableNames.push(name); + + newItems.push({ + "@_id": generateUuid(), + "@_name": name, + "@_typeRef": undefined, + }); + } - for (let i = 0; i < args.columnsCount; i++) { - const name = getNextAvailablePrefixedName(availableNames, "column"); - availableNames.push(name); + for (const newEntry of newItems) { + newColumns.splice(args.beforeIndex, 0, newEntry); + } - newItems.push({ - "@_id": generateUuid(), - "@_name": name, - "@_typeRef": undefined, + const newRows = [...(prev.row ?? [])].map((row) => { + const newCells = [...(row.expression ?? [])]; + newCells.splice(args.beforeIndex, 0, createCell()); + return { + ...row, + expression: newCells, + }; }); - } - - for (const newEntry of newItems) { - newColumns.splice(args.beforeIndex, 0, newEntry); - } - const newRows = [...(prev.row ?? [])].map((row) => { - const newCells = [...(row.expression ?? [])]; - newCells.splice(args.beforeIndex, 0, createCell()); - return { - ...row, - expression: newCells, + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: Normalized = { + ...prev, + column: newColumns, + row: newRows, }; - }); - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { - ...prev, - column: newColumns, - row: newRows, - }; - - return ret; + return ret; + }, + expressionChangedArgs: { + action: Action.ColumnAdded, + columnCount: args.columnsCount, + columnIndex: args.beforeIndex, + }, }); setWidthsById(({ newMap }) => { @@ -370,27 +433,30 @@ export function RelationExpression({ const onColumnDeleted = useCallback( (args: { columnIndex: number }) => { - setExpression((prev: Normalized) => { - const newColumns = [...(prev.column ?? [])]; - newColumns.splice(args.columnIndex, 1); - - const newRows = [...(prev.row ?? [])].map((row) => { - const newCells = [...(row.expression ?? [])]; - newCells.splice(args.columnIndex, 1); - return { - ...row, - expression: newCells, - }; - }); + setExpression({ + setExpressionAction: (prev: Normalized) => { + const newColumns = [...(prev.column ?? [])]; + newColumns.splice(args.columnIndex, 1); + + const newRows = [...(prev.row ?? [])].map((row) => { + const newCells = [...(row.expression ?? [])]; + newCells.splice(args.columnIndex, 1); + return { + ...row, + expression: newCells, + }; + }); - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { - ...prev, - column: newColumns, - row: newRows, - }; + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: Normalized = { + ...prev, + column: newColumns, + row: newRows, + }; - return ret; + return ret; + }, + expressionChangedArgs: { action: Action.ColumnRemoved, columnIndex: args.columnIndex }, }); setWidthsById(({ newMap }) => { @@ -405,17 +471,20 @@ export function RelationExpression({ const onRowDeleted = useCallback( (args: { rowIndex: number }) => { - setExpression((prev: Normalized) => { - const newRows = [...(prev.row ?? [])]; - newRows.splice(args.rowIndex, 1); - - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { - ...prev, - row: newRows, - }; + setExpression({ + setExpressionAction: (prev: Normalized) => { + const newRows = [...(prev.row ?? [])]; + newRows.splice(args.rowIndex, 1); + + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: Normalized = { + ...prev, + row: newRows, + }; - return ret; + return ret; + }, + expressionChangedArgs: { action: Action.RowRemoved, rowIndex: args.rowIndex }, }); }, [setExpression] @@ -423,25 +492,28 @@ export function RelationExpression({ const onRowDuplicated = useCallback( (args: { rowIndex: number }) => { - setExpression((prev: Normalized) => { - const duplicatedRow = { - "@_id": generateUuid(), - expression: prev.row![args.rowIndex].expression?.map((cell) => ({ - ...cell, + setExpression({ + setExpressionAction: (prev: Normalized) => { + const duplicatedRow = { "@_id": generateUuid(), - })), - }; + expression: prev.row![args.rowIndex].expression?.map((cell) => ({ + ...cell, + "@_id": generateUuid(), + })), + }; - const newRows = [...(prev.row ?? [])]; - newRows.splice(args.rowIndex, 0, duplicatedRow); + const newRows = [...(prev.row ?? [])]; + newRows.splice(args.rowIndex, 0, duplicatedRow); - // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 - const ret: Normalized = { - ...prev, - row: newRows, - }; + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: Normalized = { + ...prev, + row: newRows, + }; - return ret; + return ret; + }, + expressionChangedArgs: { action: Action.RowDuplicated, rowIndex: args.rowIndex }, }); }, [setExpression] diff --git a/packages/boxed-expression-component/src/table/BeeTable/BeeTable.tsx b/packages/boxed-expression-component/src/table/BeeTable/BeeTable.tsx index 6bd456c9f80..980751bdd50 100644 --- a/packages/boxed-expression-component/src/table/BeeTable/BeeTable.tsx +++ b/packages/boxed-expression-component/src/table/BeeTable/BeeTable.tsx @@ -44,7 +44,7 @@ import { BeeTableCellWidthsToFitDataContextProvider } from "../../resizing/BeeTa import { getOperatingSystem, OperatingSystem } from "@kie-tools-core/operating-system"; import "./BeeTable.css"; -const ROW_INDEX_COLUMN_ACCESOR = "#"; +const ROW_INDEX_COLUMN_ACCESSOR = "#"; const ROW_INDEX_SUB_COLUMN_ACCESSOR = "0"; export function getColumnsAtLastLevel | ReactTable.ColumnInstance>( @@ -84,7 +84,7 @@ export function BeeTableInternal({ onHeaderKeyUp, onDataCellClick, onDataCellKeyUp, - controllerCell = ROW_INDEX_COLUMN_ACCESOR, + controllerCell = ROW_INDEX_COLUMN_ACCESSOR, cellComponentByColumnAccessor, rows, columns, @@ -153,7 +153,7 @@ export function BeeTableInternal({ (currentControllerCell, columns) => { const rowIndexColumn: ReactTable.Column = { label: currentControllerCell as any, //FIXME: https://github.com/apache/incubator-kie-issues/issues/169 - accessor: ROW_INDEX_COLUMN_ACCESOR as any, + accessor: ROW_INDEX_COLUMN_ACCESSOR as any, width: BEE_TABLE_ROW_INDEX_COLUMN_WIDTH, minWidth: BEE_TABLE_ROW_INDEX_COLUMN_WIDTH, isRowIndexColumn: true, diff --git a/packages/boxed-expression-component/src/table/BeeTable/BeeTableEditableCellContent.tsx b/packages/boxed-expression-component/src/table/BeeTable/BeeTableEditableCellContent.tsx index 372f2578249..8de8a26396d 100644 --- a/packages/boxed-expression-component/src/table/BeeTable/BeeTableEditableCellContent.tsx +++ b/packages/boxed-expression-component/src/table/BeeTable/BeeTableEditableCellContent.tsx @@ -76,15 +76,15 @@ export function BeeTableEditableCellContent({ }, [isEditing, isReadOnly]); // FIXME: Tiago --> Temporary fix for the Boxed Expression Editor to work well. Ideally this wouldn't bee here, as the BeeTable should be decoupled from the DMN Editor's Boxed Expression Editor use-case. - const { onRequestFeelVariables } = useBoxedExpressionEditor(); + const { onRequestFeelIdentifiers } = useBoxedExpressionEditor(); - const feelVariables = useMemo(() => { + const feelIdentifiers = useMemo(() => { if (mode === Mode.Edit) { - return onRequestFeelVariables?.(); + return onRequestFeelIdentifiers?.(); } else { return undefined; } - }, [mode, onRequestFeelVariables]); + }, [mode, onRequestFeelIdentifiers]); useEffect(() => { setPreviousValue((prev) => (isEditing ? prev : value)); @@ -218,7 +218,7 @@ export function BeeTableEditableCellContent({ onPreviewChanged={setPreview} options={MONACO_OPTIONS} onBlur={onFeelBlur} - feelVariables={feelVariables} + feelIdentifiers={feelIdentifiers} expressionId={expressionId} /> diff --git a/packages/boxed-expression-component/src/table/BeeTable/BeeTableThResizable.tsx b/packages/boxed-expression-component/src/table/BeeTable/BeeTableThResizable.tsx index b74592eddb2..1b2f3979d94 100644 --- a/packages/boxed-expression-component/src/table/BeeTable/BeeTableThResizable.tsx +++ b/packages/boxed-expression-component/src/table/BeeTable/BeeTableThResizable.tsx @@ -214,6 +214,7 @@ export function BeeTableThResizable({ selectedDataType={column.dataType} onVariableUpdated={onExpressionHeaderUpdated} appendTo={getAppendToElement} + variableUuid={column.id} > {headerCellInfo} diff --git a/packages/boxed-expression-component/stories/boxedExpressionStoriesWrapper.tsx b/packages/boxed-expression-component/stories/boxedExpressionStoriesWrapper.tsx index 92276209843..d154bc686b5 100644 --- a/packages/boxed-expression-component/stories/boxedExpressionStoriesWrapper.tsx +++ b/packages/boxed-expression-component/stories/boxedExpressionStoriesWrapper.tsx @@ -24,6 +24,7 @@ import { BoxedExpressionEditor, BoxedExpressionEditorProps } from "../src/BoxedE import { BeeGwtService, BoxedExpression, DmnBuiltInDataType, generateUuid, Normalized } from "../src/api"; import { DEFAULT_EXPRESSION_VARIABLE_NAME } from "../src/expressionVariable/ExpressionVariableMenu"; import { getDefaultBoxedExpressionForStories } from "./getDefaultBoxedExpressionForStories"; +import { OnExpressionChange } from "@kie-tools/boxed-expression-component/dist/BoxedExpressionEditorContext"; export const pmmlDocuments = [ { @@ -147,6 +148,18 @@ export function BoxedExpressionEditorStory(props?: Partial( + (args) => { + const newExpression = + typeof args.setExpressionAction === "function" + ? args.setExpressionAction(expressionState) + : args.setExpressionAction; + + setExpressionState(newExpression); + }, + [expressionState] + ); + // Keep expression args in sync with state useEffect(() => { updateArgs({ expression: expressionState }); @@ -175,7 +188,7 @@ export function BoxedExpressionEditorStory(props?: Partial( + (args) => { + const newExpression = + typeof args.setExpressionAction === "function" + ? args.setExpressionAction(boxedExpression) + : args.setExpressionAction; + + setBoxedExpression(newExpression); + }, + [boxedExpression] + ); + const beeGwtService: BeeGwtService = { getDefaultExpressionDefinition(logicType, typeRef) { return { @@ -154,7 +172,7 @@ function App() { {BoxedExpressionEditorStory({ expressionHolderId: "_00000000-0000-0000-0000-000000000000", expression: boxedExpression, - onExpressionChange: setBoxedExpression, + onExpressionChange: onExpressionChange, widthsById: widthsById, onWidthsChange: setWidthsById, isResetSupportedOnRootExpression: true, diff --git a/packages/dmn-editor/package.json b/packages/dmn-editor/package.json index 3441696ef18..a1a77ba9084 100644 --- a/packages/dmn-editor/package.json +++ b/packages/dmn-editor/package.json @@ -39,6 +39,7 @@ "@kie-tools-core/switch-expression-ts": "workspace:*", "@kie-tools/boxed-expression-component": "workspace:*", "@kie-tools/dmn-feel-antlr4-parser": "workspace:*", + "@kie-tools/dmn-language-service": "workspace:*", "@kie-tools/dmn-marshaller": "workspace:*", "@kie-tools/feel-input-component": "workspace:*", "@kie-tools/i18n-common-dictionary": "workspace:*", diff --git a/packages/dmn-editor/src/boxedExpressions/BoxedExpressionScreen.tsx b/packages/dmn-editor/src/boxedExpressions/BoxedExpressionScreen.tsx index 72a355eb9a2..61e4cf79a3f 100644 --- a/packages/dmn-editor/src/boxedExpressions/BoxedExpressionScreen.tsx +++ b/packages/dmn-editor/src/boxedExpressions/BoxedExpressionScreen.tsx @@ -18,6 +18,7 @@ */ import { + Action, BeeGwtService, BoxedExpression, DmnDataType, @@ -25,11 +26,13 @@ import { PmmlDocument, } from "@kie-tools/boxed-expression-component/dist/api"; import { BoxedExpressionEditor } from "@kie-tools/boxed-expression-component/dist/BoxedExpressionEditor"; -import { FeelVariables } from "@kie-tools/dmn-feel-antlr4-parser"; +import { FeelIdentifiers } from "@kie-tools/dmn-feel-antlr4-parser"; +import { IdentifiersRefactor } from "@kie-tools/dmn-language-service"; import { DmnLatestModel } from "@kie-tools/dmn-marshaller"; import { DMN15__tBusinessKnowledgeModel, DMN15__tDecision, + DMN15__tDefinitions, DMN15__tItemDefinition, } from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types"; import { Normalized } from "@kie-tools/dmn-marshaller/dist/normalization/normalize"; @@ -65,7 +68,7 @@ import { Flex, FlexItem } from "@patternfly/react-core/dist/js/layouts/Flex"; import { ArrowRightIcon } from "@patternfly/react-icons/dist/js/icons/arrow-right-icon"; import { InfoIcon } from "@patternfly/react-icons/dist/js/icons/info-icon"; import * as React from "react"; -import { useCallback, useEffect, useMemo, useRef } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import * as RF from "reactflow"; import { builtInFeelTypes } from "../dataTypes/BuiltInFeelTypes"; import { DataTypeIndex } from "../dataTypes/DataTypes"; @@ -81,7 +84,14 @@ import { useDmnEditorStore, useDmnEditorStoreApi } from "../store/StoreContext"; import { getDefaultColumnWidth } from "./getDefaultColumnWidth"; import { getDefaultBoxedExpression } from "./getDefaultBoxedExpression"; import { useSettings } from "../settings/DmnEditorSettingsContext"; - +import { renameDrgElement } from "../mutations/renameNode"; +import { OnExpressionChange } from "@kie-tools/boxed-expression-component/dist/BoxedExpressionEditorContext"; +import { updateDrgElementType } from "../mutations/updateDrgElementType"; +import { VariableChangedArgs } from "@kie-tools/boxed-expression-component/dist/api/ExpressionChange"; +import { + isIdentifierReferencedInSomeExpression, + RefactorConfirmationDialog, +} from "../refactor/RefactorConfirmationDialog"; export function BoxedExpressionScreen({ container }: { container: React.RefObject }) { const { externalModelsByNamespace } = useExternalModels(); @@ -104,15 +114,21 @@ export function BoxedExpressionScreen({ container }: { container: React.RefObjec const isAlternativeInputDataShape = useDmnEditorStore((s) => s.computed(s).isAlternativeInputDataShape()); const drdIndex = useDmnEditorStore((s) => s.computed(s).getDrdIndex()); - const onRequestFeelVariables = useCallback(() => { - const externalModels = new Map(); + const externalDmnModelsByNamespaceMap = useMemo(() => { + const externalModels = new Map>(); for (const [key, externalDmn] of externalDmnsByNamespace) { externalModels.set(key, externalDmn.model); } + return externalModels; + }, [externalDmnsByNamespace]); - return new FeelVariables(dmnEditorStoreApi.getState().dmn.model.definitions, externalModels); - }, [dmnEditorStoreApi, externalDmnsByNamespace]); + const onRequestFeelIdentifiers = useCallback(() => { + return new FeelIdentifiers({ + _readonly_dmnDefinitions: dmnEditorStoreApi.getState().dmn.model.definitions, + _readonly_externalDefinitions: externalDmnModelsByNamespaceMap, + }); + }, [dmnEditorStoreApi, externalDmnModelsByNamespaceMap]); const drgElementIndex = useMemo(() => { if (!activeDrgElementId) { @@ -203,24 +219,106 @@ export function BoxedExpressionScreen({ container }: { container: React.RefObjec [dmnEditorStoreApi] ); - const onExpressionChange: React.Dispatch>> = useCallback( - (newExpressionAction) => { - dmnEditorStoreApi.setState((state) => { - const newExpression = - typeof newExpressionAction === "function" - ? newExpressionAction(boxedExpressionRef.current ?? undefined!) - : newExpressionAction; + const setExpression = useCallback( + (args: { definitions: Normalized; expression: Normalized }) => { + boxedExpressionRef.current = args.expression; + updateExpression({ + definitions: args.definitions, + expression: args.expression!, + drgElementIndex: expression?.drgElementIndex ?? 0, + externalDmnModelsByNamespaceMap, + }); + }, + [expression?.drgElementIndex, externalDmnModelsByNamespaceMap] + ); + + const [isRefactorModalOpen, setIsRefactorModalOpen] = useState(false); + const [variableChangedArgs, setVariableChangedArgs] = useState(); + const [newExpression, setNewExpression] = useState>(); - boxedExpressionRef.current = newExpression; + const refactor = useCallback(() => { + if (!variableChangedArgs) { + throw new Error("Can not refactor because `variableChangedArgs` are not set in BoxedExpressionScreen."); + } - updateExpression({ + dmnEditorStoreApi.setState((state) => { + const drgElement = state.dmn.model.definitions.drgElement?.[expression?.drgElementIndex ?? 0]; + if (drgElement?.["@_id"] === variableChangedArgs.variableUuid) { + renameDrgElement({ definitions: state.dmn.model.definitions, - expression: newExpression, - drgElementIndex: expression?.drgElementIndex ?? 0, + newName: newExpression?.["@_label"] ?? drgElement!["@_name"]!, + index: expression?.drgElementIndex ?? 0, + externalDmnModelsByNamespaceMap, + shouldRenameReferencedExpressions: true, + }); + } else { + const identifiersRefactor = new IdentifiersRefactor({ + writeableDmnDefinitions: state.dmn.model.definitions, + _readonly_externalDmnModelsByNamespaceMap: externalDmnModelsByNamespaceMap, }); + identifiersRefactor.rename({ + identifierUuid: variableChangedArgs.variableUuid, + newName: variableChangedArgs.nameChange?.to ?? "", + }); + } + }); + }, [ + dmnEditorStoreApi, + expression?.drgElementIndex, + externalDmnModelsByNamespaceMap, + newExpression, + variableChangedArgs, + ]); + + const onExpressionChange = useCallback( + (args) => { + dmnEditorStoreApi.setState((state) => { + const newExpression = + typeof args.setExpressionAction === "function" + ? args.setExpressionAction(boxedExpressionRef.current) + : args.setExpressionAction; + + if (!args.expressionChangedArgs) { + setExpression({ definitions: state.dmn.model.definitions, expression: newExpression }); + } else { + if (args.expressionChangedArgs.action === Action.VariableChanged) { + if (args.expressionChangedArgs.typeChange) { + const identifiersRefactor = new IdentifiersRefactor({ + writeableDmnDefinitions: state.dmn.model.definitions, + _readonly_externalDmnModelsByNamespaceMap: externalDmnModelsByNamespaceMap, + }); + identifiersRefactor.changeType({ + identifierUuid: args.expressionChangedArgs.variableUuid, + newType: args.expressionChangedArgs.typeChange.to, + }); + updateDrgElementType({ + definitions: state.dmn.model.definitions, + expression: newExpression!, + drgElementIndex: expression?.drgElementIndex ?? 0, + }); + } + if (args.expressionChangedArgs.nameChange) { + setVariableChangedArgs(args.expressionChangedArgs); + setNewExpression(newExpression); + if ( + isIdentifierReferencedInSomeExpression({ + identifierUuid: args.expressionChangedArgs.variableUuid, + dmnDefinitions: state.dmn.model.definitions, + externalDmnModelsByNamespaceMap, + }) + ) { + setIsRefactorModalOpen(true); + } else { + setExpression({ definitions: state.dmn.model.definitions, expression: newExpression }); + } + } + } else { + setExpression({ definitions: state.dmn.model.definitions, expression: newExpression }); + } + } }); }, - [dmnEditorStoreApi, expression?.drgElementIndex] + [dmnEditorStoreApi, expression?.drgElementIndex, externalDmnModelsByNamespaceMap, setExpression] ); // END (setState batching for `expression` and `widthsById`) @@ -319,6 +417,28 @@ export function BoxedExpressionScreen({ container }: { container: React.RefObjec return NodeIcon({ nodeType, isAlternativeInputDataShape }); }, [drgElement, isAlternativeInputDataShape]); + const onConfirmExpressionRefactor = useCallback(() => { + if (!variableChangedArgs) { + throw new Error( + "Can not update variable name because 'variableChangedArgs' is not set in the BoxedExpressionScreen." + ); + } + refactor(); + + setVariableChangedArgs(undefined); + setNewExpression(undefined); + setIsRefactorModalOpen(false); + }, [refactor, variableChangedArgs]); + + const onConfirmRenameOnly = useCallback(() => { + setVariableChangedArgs(undefined); + setNewExpression(undefined); + setIsRefactorModalOpen(false); + dmnEditorStoreApi.setState((state) => { + setExpression({ definitions: state.dmn.model.definitions, expression: newExpression }); + }); + }, [dmnEditorStoreApi, newExpression, setExpression]); + return ( <> <> @@ -379,6 +499,13 @@ export function BoxedExpressionScreen({ container }: { container: React.RefObjec +
s.computed(s).getDirectlyIncludedExternalModelsByNamespace(externalModelsByNamespace).dmns + ); + + const externalModelsByNamespaceMap = useMemo(() => { + const externalModels = new Map>(); + + for (const [key, externalDmn] of externalDmnsByNamespace) { + externalModels.set(key, externalDmn.model); + } + return externalModels; + }, [externalDmnsByNamespace]); + const onRenamed = useCallback( (newName) => { if (isReadOnly) { @@ -82,10 +96,11 @@ export function DataTypeName({ newName, itemDefinitionId: itemDefinition["@_id"]!, allDataTypesById: state.computed(state).getDataTypes(externalModelsByNamespace).allDataTypesById, + externalModelsByNamespaceMap, }); }); }, - [dmnEditorStoreApi, externalModelsByNamespace, isReadOnly, itemDefinition] + [dmnEditorStoreApi, externalModelsByNamespace, externalModelsByNamespaceMap, isReadOnly, itemDefinition] ); const _shouldCommitOnBlur = shouldCommitOnBlur ?? true; // Defaults to true diff --git a/packages/dmn-editor/src/diagram/Diagram.tsx b/packages/dmn-editor/src/diagram/Diagram.tsx index c42760aa796..236a4e70026 100644 --- a/packages/dmn-editor/src/diagram/Diagram.tsx +++ b/packages/dmn-editor/src/diagram/Diagram.tsx @@ -126,6 +126,7 @@ import { autoGenerateDrd } from "../normalization/autoGenerateDrd"; import OptimizeIcon from "@patternfly/react-icons/dist/js/icons/optimize-icon"; import { applyAutoLayoutToDrd } from "../mutations/applyAutoLayoutToDrd"; import { useSettings } from "../settings/DmnEditorSettingsContext"; +import { DmnLatestModel } from "@kie-tools/dmn-marshaller"; const isFirefox = typeof (window as any).InstallTrigger !== "undefined"; // See https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browsers @@ -1473,6 +1474,17 @@ function DmnDiagramEmptyState({ }) { const dmnEditorStoreApi = useDmnEditorStoreApi(); const { externalModelsByNamespace } = useExternalModels(); + const externalDmnsByNamespace = useDmnEditorStore( + (s) => s.computed(s).getDirectlyIncludedExternalModelsByNamespace(externalModelsByNamespace).dmns + ); + const externalDmnModelsByNamespaceMap = useMemo(() => { + const externalModels = new Map>(); + + for (const [key, externalDmn] of externalDmnsByNamespace) { + externalModels.set(key, externalDmn.model); + } + return externalModels; + }, [externalDmnsByNamespace]); return ( = E extends any ? E["__$$element"] extends Filter @@ -136,13 +141,64 @@ export const InputDataNode = React.memo( isAlternativeInputDataShape, }); + const { externalModelsByNamespace } = useExternalModels(); + const externalDmnsByNamespace = useDmnEditorStore( + (s) => s.computed(s).getDirectlyIncludedExternalModelsByNamespace(externalModelsByNamespace).dmns + ); + const externalDmnModelsByNamespaceMap = useMemo(() => { + const externalModels = new Map>(); + + for (const [key, externalDmn] of externalDmnsByNamespace) { + externalModels.set(key, externalDmn.model); + } + return externalModels; + }, [externalDmnsByNamespace]); + + const [isRefactorModalOpen, setIsRefactorModalOpen] = useState(false); + const [newName, setNewName] = useState(""); + const identifierId = useMemo(() => inputData["@_id"], [inputData]); + const oldName = useMemo(() => inputData["@_label"] ?? inputData["@_name"], [inputData]); + + const applyRename = useCallback( + (args: { + definitions: Normalized; + newName: string; + shouldRenameReferencedExpressions: boolean; + }) => { + renameDrgElement({ + ...args, + index, + externalDmnModelsByNamespaceMap, + }); + }, + [externalDmnModelsByNamespaceMap, index] + ); + const setName = useCallback( - (newName: string) => { + (name: string) => { + if (name === oldName) { + return; + } dmnEditorStoreApi.setState((state) => { - renameDrgElement({ definitions: state.dmn.model.definitions, newName, index }); + if ( + isIdentifierReferencedInSomeExpression({ + identifierUuid: identifierId, + dmnDefinitions: state.dmn.model.definitions, + externalDmnModelsByNamespaceMap, + }) + ) { + setNewName(name); + setIsRefactorModalOpen(true); + } else { + applyRename({ + definitions: state.dmn.model.definitions, + newName: name, + shouldRenameReferencedExpressions: false, + }); + } }); }, - [dmnEditorStoreApi, index] + [oldName, dmnEditorStoreApi, identifierId, externalDmnModelsByNamespaceMap, applyRename] ); const onTypeRefChange = useCallback( @@ -166,8 +222,6 @@ export const InputDataNode = React.memo( isEnabled: enableCustomNodeStyles, }); - const { externalModelsByNamespace } = useExternalModels(); - const isCollection = useDmnEditorStore((s) => { const { allDataTypesById, allTopLevelItemDefinitionUniqueNames } = s .computed(s) @@ -193,7 +247,7 @@ export const InputDataNode = React.memo( }px`, } as any) : undefined; - // The dependecy should be "nodeDimension" to trigger an adjustment on width changes as well. + // The dependency should be "nodeDimension" to trigger an adjustment on width changes as well. }, [isAlternativeInputDataShape, nodeDimensions, isEditingLabel, alternativeEditableNodeHeight]); const selectedAlternativeClass = useMemo( @@ -203,6 +257,31 @@ export const InputDataNode = React.memo( return ( <> + { + setIsRefactorModalOpen(false); + dmnEditorStoreApi.setState((state) => { + applyRename({ + definitions: state.dmn.model.definitions, + newName, + shouldRenameReferencedExpressions: true, + }); + }); + }} + onConfirmRenameOnly={() => { + setIsRefactorModalOpen(false); + dmnEditorStoreApi.setState((state) => { + applyRename({ + definitions: state.dmn.model.definitions, + newName, + shouldRenameReferencedExpressions: false, + }); + }); + }} + isRefactorModalOpen={isRefactorModalOpen} + fromName={oldName} + toName={newName} + /> s.computed(s).getDirectlyIncludedExternalModelsByNamespace(externalModelsByNamespace).dmns + ); + const externalDmnModelsByNamespaceMap = useMemo(() => { + const externalModels = new Map>(); + + for (const [key, externalDmn] of externalDmnsByNamespace) { + externalModels.set(key, externalDmn.model); + } + return externalModels; + }, [externalDmnsByNamespace]); + + const [isRefactorModalOpen, setIsRefactorModalOpen] = useState(false); + const [newName, setNewName] = useState(""); + const identifierId = useMemo(() => decision["@_id"], [decision]); + const oldName = useMemo(() => decision["@_label"] ?? decision["@_name"], [decision]); + + const applyRename = useCallback( + (args: { + definitions: Normalized; + newName: string; + shouldRenameReferencedExpressions: boolean; + }) => { + renameDrgElement({ + ...args, + index, + externalDmnModelsByNamespaceMap, + }); + }, + [externalDmnModelsByNamespaceMap, index] + ); + const setName = useCallback( - (newName: string) => { + (name: string) => { + if (name === oldName) { + return; + } dmnEditorStoreApi.setState((state) => { - renameDrgElement({ definitions: state.dmn.model.definitions, newName, index }); + if ( + isIdentifierReferencedInSomeExpression({ + identifierUuid: identifierId, + dmnDefinitions: state.dmn.model.definitions, + externalDmnModelsByNamespaceMap, + }) + ) { + setNewName(name); + setIsRefactorModalOpen(true); + } else { + applyRename({ + definitions: state.dmn.model.definitions, + newName: name, + shouldRenameReferencedExpressions: false, + }); + } }); }, - [dmnEditorStoreApi, index] + [oldName, dmnEditorStoreApi, identifierId, externalDmnModelsByNamespaceMap, applyRename] ); const onTypeRefChange = useCallback( @@ -389,8 +520,6 @@ export const DecisionNode = React.memo( isEnabled: enableCustomNodeStyles, }); - const { externalModelsByNamespace } = useExternalModels(); - const isCollection = useDmnEditorStore((s) => { const { allDataTypesById, allTopLevelItemDefinitionUniqueNames } = s .computed(s) @@ -404,6 +533,31 @@ export const DecisionNode = React.memo( return ( <> + { + setIsRefactorModalOpen(false); + dmnEditorStoreApi.setState((state) => { + applyRename({ + definitions: state.dmn.model.definitions, + newName, + shouldRenameReferencedExpressions: true, + }); + }); + }} + onConfirmRenameOnly={() => { + setIsRefactorModalOpen(false); + dmnEditorStoreApi.setState((state) => { + applyRename({ + definitions: state.dmn.model.definitions, + newName, + shouldRenameReferencedExpressions: false, + }); + }); + }} + isRefactorModalOpen={isRefactorModalOpen} + fromName={oldName} + toName={newName} + /> s.computed(s).getDirectlyIncludedExternalModelsByNamespace(externalModelsByNamespace).dmns + ); + const externalDmnModelsByNamespaceMap = useMemo(() => { + const externalModels = new Map>(); + + for (const [key, externalDmn] of externalDmnsByNamespace) { + externalModels.set(key, externalDmn.model); + } + return externalModels; + }, [externalDmnsByNamespace]); + + const [isRefactorModalOpen, setIsRefactorModalOpen] = useState(false); + const [newName, setNewName] = useState(""); + const identifierId = useMemo(() => bkm["@_id"], [bkm]); + const oldName = useMemo(() => bkm["@_label"] ?? bkm["@_name"], [bkm]); + + const applyRename = useCallback( + (args: { + definitions: Normalized; + newName: string; + shouldRenameReferencedExpressions: boolean; + }) => { + renameDrgElement({ + ...args, + index, + externalDmnModelsByNamespaceMap, + }); + }, + [externalDmnModelsByNamespaceMap, index] + ); + const setName = useCallback( - (newName: string) => { + (name: string) => { + if (name === oldName) { + return; + } dmnEditorStoreApi.setState((state) => { - renameDrgElement({ definitions: state.dmn.model.definitions, newName, index }); + if ( + isIdentifierReferencedInSomeExpression({ + identifierUuid: identifierId, + dmnDefinitions: state.dmn.model.definitions, + externalDmnModelsByNamespaceMap, + }) + ) { + setNewName(name); + setIsRefactorModalOpen(true); + } else { + applyRename({ + definitions: state.dmn.model.definitions, + newName: name, + shouldRenameReferencedExpressions: false, + }); + } }); }, - [dmnEditorStoreApi, index] + [oldName, dmnEditorStoreApi, identifierId, externalDmnModelsByNamespaceMap, applyRename] ); const onTypeRefChange = useCallback( @@ -545,6 +751,32 @@ export const BkmNode = React.memo( return ( <> + { + setIsRefactorModalOpen(false); + dmnEditorStoreApi.setState((state) => { + applyRename({ + definitions: state.dmn.model.definitions, + newName, + shouldRenameReferencedExpressions: true, + }); + }); + }} + onConfirmRenameOnly={() => { + setIsRefactorModalOpen(false); + dmnEditorStoreApi.setState((state) => { + applyRename({ + definitions: state.dmn.model.definitions, + newName, + shouldRenameReferencedExpressions: false, + }); + }); + }} + isRefactorModalOpen={isRefactorModalOpen} + fromName={oldName} + toName={newName} + /> + s.computed(s).getDirectlyIncludedExternalModelsByNamespace(externalModelsByNamespace).dmns + ); + const externalDmnModelsByNamespaceMap = useMemo(() => { + const externalModels = new Map>(); + + for (const [key, externalDmn] of externalDmnsByNamespace) { + externalModels.set(key, externalDmn.model); + } + return externalModels; + }, [externalDmnsByNamespace]); + + const [isRefactorModalOpen, setIsRefactorModalOpen] = useState(false); + const [newName, setNewName] = useState(""); + const identifierId = useMemo(() => knowledgeSource["@_id"], [knowledgeSource]); + const oldName = useMemo(() => knowledgeSource["@_label"] ?? knowledgeSource["@_name"], [knowledgeSource]); + + const applyRename = useCallback( + (args: { + definitions: Normalized; + newName: string; + shouldRenameReferencedExpressions: boolean; + }) => { + renameDrgElement({ + ...args, + index, + externalDmnModelsByNamespaceMap, + }); + }, + [externalDmnModelsByNamespaceMap, index] + ); + const setName = useCallback( - (newName: string) => { + (name: string) => { + if (name === oldName) { + return; + } dmnEditorStoreApi.setState((state) => { - renameDrgElement({ definitions: state.dmn.model.definitions, newName, index }); + if ( + isIdentifierReferencedInSomeExpression({ + identifierUuid: identifierId, + dmnDefinitions: state.dmn.model.definitions, + externalDmnModelsByNamespaceMap, + }) + ) { + setNewName(name); + setIsRefactorModalOpen(true); + } else { + applyRename({ + definitions: state.dmn.model.definitions, + newName: name, + shouldRenameReferencedExpressions: false, + }); + } }); }, - [dmnEditorStoreApi, index] + [oldName, dmnEditorStoreApi, identifierId, externalDmnModelsByNamespaceMap, applyRename] ); const getAllFeelVariableUniqueNames = useCallback((s: State) => s.computed(s).getAllFeelVariableUniqueNames(), []); @@ -666,6 +950,31 @@ export const KnowledgeSourceNode = React.memo( return ( <> + { + setIsRefactorModalOpen(false); + dmnEditorStoreApi.setState((state) => { + applyRename({ + definitions: state.dmn.model.definitions, + newName, + shouldRenameReferencedExpressions: true, + }); + }); + }} + onConfirmRenameOnly={() => { + setIsRefactorModalOpen(false); + dmnEditorStoreApi.setState((state) => { + applyRename({ + definitions: state.dmn.model.definitions, + newName, + shouldRenameReferencedExpressions: false, + }); + }); + }} + isRefactorModalOpen={isRefactorModalOpen} + fromName={oldName} + toName={newName} + /> s.computed(s).getDirectlyIncludedExternalModelsByNamespace(externalModelsByNamespace).dmns + ); + const externalDmnModelsByNamespaceMap = useMemo(() => { + const externalModels = new Map>(); + + for (const [key, externalDmn] of externalDmnsByNamespace) { + externalModels.set(key, externalDmn.model); + } + return externalModels; + }, [externalDmnsByNamespace]); + + const [isRefactorModalOpen, setIsRefactorModalOpen] = useState(false); + const [newName, setNewName] = useState(""); + const identifierId = useMemo(() => decisionService["@_id"], [decisionService]); + const oldName = useMemo(() => decisionService["@_label"] ?? decisionService["@_name"], [decisionService]); + + const applyRename = useCallback( + (args: { + definitions: Normalized; + newName: string; + shouldRenameReferencedExpressions: boolean; + }) => { + renameDrgElement({ + ...args, + index, + externalDmnModelsByNamespaceMap, + }); + }, + [externalDmnModelsByNamespaceMap, index] + ); + const setName = useCallback( - (newName: string) => { + (name: string) => { + if (name === oldName) { + return; + } dmnEditorStoreApi.setState((state) => { - renameDrgElement({ definitions: state.dmn.model.definitions, newName, index }); + if ( + isIdentifierReferencedInSomeExpression({ + identifierUuid: identifierId, + dmnDefinitions: state.dmn.model.definitions, + externalDmnModelsByNamespaceMap, + }) + ) { + setNewName(name); + setIsRefactorModalOpen(true); + } else { + applyRename({ + definitions: state.dmn.model.definitions, + newName: name, + shouldRenameReferencedExpressions: false, + }); + } }); }, - [dmnEditorStoreApi, index] + [oldName, dmnEditorStoreApi, identifierId, externalDmnModelsByNamespaceMap, applyRename] ); // Select nodes representing output and encapsulated decisions contained by the Decision Service @@ -966,6 +1326,31 @@ export const DecisionServiceNode = React.memo( return ( <> + { + setIsRefactorModalOpen(false); + dmnEditorStoreApi.setState((state) => { + applyRename({ + definitions: state.dmn.model.definitions, + newName, + shouldRenameReferencedExpressions: true, + }); + }); + }} + onConfirmRenameOnly={() => { + setIsRefactorModalOpen(false); + dmnEditorStoreApi.setState((state) => { + applyRename({ + definitions: state.dmn.model.definitions, + newName, + shouldRenameReferencedExpressions: false, + }); + }); + }} + isRefactorModalOpen={isRefactorModalOpen} + fromName={oldName} + toName={newName} + /> s.computed(s).getDirectlyIncludedExternalModelsByNamespace(externalModelsByNamespace).dmns + ); + + const externalModelsByNamespaceMap = useMemo(() => { + const externalModels = new Map>(); + + for (const [key, externalDmn] of externalDmnsByNamespace) { + externalModels.set(key, externalDmn.model); + } + return externalModels; + }, [externalDmnsByNamespace]); + const rename = useCallback( (newName) => { dmnEditorStoreApi.setState((state) => { @@ -488,10 +502,11 @@ function IncludedModelCard({ newName, allTopLevelDataTypesByFeelName: state.computed(state).getDataTypes(externalModelsByNamespace) .allTopLevelDataTypesByFeelName, + externalModelsByNamespaceMap, }); }); }, - [dmnEditorStoreApi, externalModelsByNamespace, index] + [dmnEditorStoreApi, externalModelsByNamespace, externalModelsByNamespaceMap, index] ); const extension = useMemo(() => { diff --git a/packages/dmn-editor/src/mutations/renameImport.ts b/packages/dmn-editor/src/mutations/renameImport.ts index b10dc6f9cd2..6113599ceb8 100644 --- a/packages/dmn-editor/src/mutations/renameImport.ts +++ b/packages/dmn-editor/src/mutations/renameImport.ts @@ -32,20 +32,30 @@ import { buildFeelQName, parseFeelQName } from "../feel/parseFeelQName"; import { DataTypeIndex } from "../dataTypes/DataTypes"; import { DMN15__tContext } from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types"; import { DMN15_SPEC } from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/Dmn15Spec"; +import { IdentifiersRefactor } from "@kie-tools/dmn-language-service"; +import { DmnLatestModel } from "@kie-tools/dmn-marshaller"; export function renameImport({ definitions, newName, allTopLevelDataTypesByFeelName, index, + externalModelsByNamespaceMap, }: { definitions: Normalized; allTopLevelDataTypesByFeelName: DataTypeIndex; newName: string; index: number; + externalModelsByNamespaceMap: Map>; }) { const trimmedNewName = newName.trim(); + const identifiersRefactor = new IdentifiersRefactor({ + writeableDmnDefinitions: definitions, + _readonly_externalDmnModelsByNamespaceMap: + externalModelsByNamespaceMap ?? new Map>(), + }); + const _import = definitions.import![index]; traverseItemDefinitions(definitions.itemDefinition ?? [], (item) => { @@ -120,7 +130,7 @@ export function renameImport({ // TODO: Tiago --> Update the "document" entry of PMML functions that were pointing to the renamed included PMML model. - // FIXME: Daniel --> Update FEEL expressions that contain references to this import. + identifiersRefactor.renameImport({ oldName: _import["@_name"], newName: trimmedNewName }); _import["@_name"] = trimmedNewName; } diff --git a/packages/dmn-editor/src/mutations/renameItemDefinition.ts b/packages/dmn-editor/src/mutations/renameItemDefinition.ts index e6048862f30..b90f06c098a 100644 --- a/packages/dmn-editor/src/mutations/renameItemDefinition.ts +++ b/packages/dmn-editor/src/mutations/renameItemDefinition.ts @@ -25,17 +25,21 @@ import { } from "../dataTypes/DataTypeSpec"; import { Normalized } from "@kie-tools/dmn-marshaller/dist/normalization/normalize"; import { DataTypeIndex } from "../dataTypes/DataTypes"; +import { DmnLatestModel } from "@kie-tools/dmn-marshaller"; +import { IdentifiersRefactor } from "@kie-tools/dmn-language-service"; export function renameItemDefinition({ definitions, newName, allDataTypesById, itemDefinitionId, + externalModelsByNamespaceMap, }: { definitions: Normalized; newName: string; itemDefinitionId: string; allDataTypesById: DataTypeIndex; + externalModelsByNamespaceMap: Map>; }) { const dataType = allDataTypesById.get(itemDefinitionId); if (!dataType) { @@ -86,7 +90,12 @@ export function renameItemDefinition({ // Not top-level.. meaning that we need to update FEEL expressions referencing it else { - // FIXME: Daniel --> Implement this... + const identifiersRefactor = new IdentifiersRefactor({ + writeableDmnDefinitions: definitions, + _readonly_externalDmnModelsByNamespaceMap: externalModelsByNamespaceMap, + }); + + identifiersRefactor.rename({ identifierUuid: itemDefinitionId, newName: trimmedNewName }); } itemDefinition["@_name"] = trimmedNewName; diff --git a/packages/dmn-editor/src/mutations/renameNode.ts b/packages/dmn-editor/src/mutations/renameNode.ts index 43ccb802c18..335375d52a3 100644 --- a/packages/dmn-editor/src/mutations/renameNode.ts +++ b/packages/dmn-editor/src/mutations/renameNode.ts @@ -24,20 +24,31 @@ import { } from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types"; import { Normalized } from "@kie-tools/dmn-marshaller/dist/normalization/normalize"; import { generateUuid } from "@kie-tools/boxed-expression-component/dist/api"; +import { IdentifiersRefactor } from "@kie-tools/dmn-language-service"; +import { DmnLatestModel } from "@kie-tools/dmn-marshaller"; export function renameDrgElement({ definitions, newName, index, + externalDmnModelsByNamespaceMap, + shouldRenameReferencedExpressions, }: { definitions: Normalized; newName: string; index: number; + externalDmnModelsByNamespaceMap: Map>; + shouldRenameReferencedExpressions: boolean; }) { const trimmedNewName = newName.trim(); const drgElement = definitions.drgElement![index]; + const identifiersRefactor = new IdentifiersRefactor({ + writeableDmnDefinitions: definitions, + _readonly_externalDmnModelsByNamespaceMap: externalDmnModelsByNamespaceMap, + }); + drgElement["@_name"] = trimmedNewName; if (drgElement.__$$element !== "knowledgeSource") { @@ -53,7 +64,9 @@ export function renameDrgElement({ drgElement.encapsulatedLogic["@_label"] = trimmedNewName; } - // FIXME: Daniel --> Here we need to update all FEEL expression that were using this node's name as a variable. + if (shouldRenameReferencedExpressions) { + identifiersRefactor.rename({ identifierUuid: drgElement["@_id"], newName: trimmedNewName }); + } } export function renameGroupNode({ diff --git a/packages/dmn-editor/src/mutations/setDrgElementExpression.ts b/packages/dmn-editor/src/mutations/setDrgElementExpression.ts new file mode 100644 index 00000000000..02f55e8fc25 --- /dev/null +++ b/packages/dmn-editor/src/mutations/setDrgElementExpression.ts @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { BoxedExpression } from "@kie-tools/boxed-expression-component/dist/api"; +import { + DMN15__tDefinitions, + DMN15__tFunctionDefinition, +} from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types"; +import { Normalized } from "@kie-tools/dmn-marshaller/dist/normalization/normalize"; + +export function setDrgElementExpression({ + definitions, + expression, + drgElementIndex, +}: { + definitions: Normalized; + expression: Normalized; + drgElementIndex: number; +}): void { + const drgElement = definitions.drgElement?.[drgElementIndex]; + if (!drgElement) { + throw new Error("DMN MUTATION: Can't update expression for drgElement that doesn't exist."); + } + + if (drgElement?.__$$element === "decision") { + drgElement.expression = expression; + } else if (drgElement?.__$$element === "businessKnowledgeModel") { + if (expression.__$$element !== "functionDefinition") { + throw new Error("DMN MUTATION: Can't have an expression on a BKM that is not a Function."); + } + + if (!expression?.__$$element) { + throw new Error("DMN MUTATION: Can't determine expression type without its __$$element property."); + } + + // We remove the __$$element here, because otherwise the "functionDefinition" element name will be used in the final XML. + const { __$$element, ..._updateExpression } = expression; + drgElement.encapsulatedLogic = _updateExpression as Normalized; + } else { + throw new Error("DMN MUTATION: Can't update expression for drgElement that is not a Decision or a BKM."); + } +} diff --git a/packages/dmn-editor/src/mutations/updateDrgElementType.ts b/packages/dmn-editor/src/mutations/updateDrgElementType.ts new file mode 100644 index 00000000000..44427708b1e --- /dev/null +++ b/packages/dmn-editor/src/mutations/updateDrgElementType.ts @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { BoxedExpression } from "@kie-tools/boxed-expression-component/dist/api"; +import { DMN15__tDefinitions } from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types"; +import { Normalized } from "@kie-tools/dmn-marshaller/dist/normalization/normalize"; + +export function updateDrgElementType({ + definitions, + expression, + drgElementIndex, +}: { + definitions: Normalized; + expression: Normalized; + drgElementIndex: number; +}): void { + const drgElement = definitions.drgElement?.[drgElementIndex]; + if (!drgElement) { + throw new Error("DMN MUTATION: Can't update expression for drgElement that doesn't exist."); + } + + if (drgElement?.__$$element === "decision") { + drgElement.variable!["@_typeRef"] = expression ? expression["@_typeRef"] : drgElement.variable!["@_typeRef"]; + } else if (drgElement?.__$$element === "businessKnowledgeModel") { + if (expression.__$$element !== "functionDefinition") { + throw new Error("DMN MUTATION: Can't have an expression on a BKM that is not a Function."); + } + + if (!expression?.__$$element) { + throw new Error("DMN MUTATION: Can't determine expression type without its __$$element property."); + } + + drgElement.variable!["@_typeRef"] = expression?.["@_typeRef"] ?? drgElement.variable!["@_typeRef"]; + } else { + throw new Error("DMN MUTATION: Can't update expression for drgElement that is not a Decision or a BKM."); + } +} diff --git a/packages/dmn-editor/src/mutations/updateExpression.ts b/packages/dmn-editor/src/mutations/updateExpression.ts index 5087508a22c..e1d4fef55ef 100644 --- a/packages/dmn-editor/src/mutations/updateExpression.ts +++ b/packages/dmn-editor/src/mutations/updateExpression.ts @@ -18,21 +18,23 @@ */ import { BoxedExpression } from "@kie-tools/boxed-expression-component/dist/api"; -import { - DMN15__tDefinitions, - DMN15__tFunctionDefinition, -} from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types"; +import { DMN15__tDefinitions } from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types"; import { Normalized } from "@kie-tools/dmn-marshaller/dist/normalization/normalize"; import { renameDrgElement } from "./renameNode"; +import { DmnLatestModel } from "@kie-tools/dmn-marshaller/dist"; +import { setDrgElementExpression } from "./setDrgElementExpression"; +import { updateDrgElementType } from "./updateDrgElementType"; export function updateExpression({ definitions, expression, drgElementIndex, + externalDmnModelsByNamespaceMap, }: { definitions: Normalized; expression: Normalized; drgElementIndex: number; + externalDmnModelsByNamespaceMap: Map>; }): void { const drgElement = definitions.drgElement?.[drgElementIndex]; if (!drgElement) { @@ -43,25 +45,19 @@ export function updateExpression({ definitions, newName: expression?.["@_label"] ?? drgElement!["@_name"]!, index: drgElementIndex, + externalDmnModelsByNamespaceMap, + shouldRenameReferencedExpressions: false, }); - if (drgElement?.__$$element === "decision") { - drgElement.expression = expression; - drgElement.variable!["@_typeRef"] = expression ? expression["@_typeRef"] : drgElement.variable!["@_typeRef"]; - } else if (drgElement?.__$$element === "businessKnowledgeModel") { - if (expression.__$$element !== "functionDefinition") { - throw new Error("DMN MUTATION: Can't have an expression on a BKM that is not a Function."); - } - - if (!expression?.__$$element) { - throw new Error("DMN MUTATION: Can't determine expression type without its __$$element property."); - } + setDrgElementExpression({ + definitions, + expression, + drgElementIndex, + }); - // We remove the __$$element here, because otherwise the "functionDefinition" element name will be used in the final XML. - const { __$$element, ..._updateExpression } = expression; - drgElement.encapsulatedLogic = _updateExpression as Normalized; - drgElement.variable!["@_typeRef"] = _updateExpression?.["@_typeRef"] ?? drgElement.variable!["@_typeRef"]; - } else { - throw new Error("DMN MUTATION: Can't update expression for drgElement that is not a Decision or a BKM."); - } + updateDrgElementType({ + definitions, + expression, + drgElementIndex, + }); } diff --git a/packages/dmn-editor/src/propertiesPanel/BkmProperties.tsx b/packages/dmn-editor/src/propertiesPanel/BkmProperties.tsx index a46e4522655..07f415580d7 100644 --- a/packages/dmn-editor/src/propertiesPanel/BkmProperties.tsx +++ b/packages/dmn-editor/src/propertiesPanel/BkmProperties.tsx @@ -18,7 +18,10 @@ */ import * as React from "react"; -import { DMN15__tBusinessKnowledgeModel } from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types"; +import { + DMN15__tBusinessKnowledgeModel, + DMN15__tDefinitions, +} from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types"; import { Normalized } from "@kie-tools/dmn-marshaller/dist/normalization/normalize"; import { ClipboardCopy } from "@patternfly/react-core/dist/js/components/ClipboardCopy"; import { FormGroup } from "@patternfly/react-core/dist/js/components/Form"; @@ -30,18 +33,26 @@ import { renameDrgElement } from "../mutations/renameNode"; import { InlineFeelNameInput } from "../feel/InlineFeelNameInput"; import { useDmnEditor } from "../DmnEditorContext"; import { useResolvedTypeRef } from "../dataTypes/useResolvedTypeRef"; -import { useCallback } from "react"; +import { useCallback, useMemo, useState } from "react"; import { generateUuid } from "@kie-tools/boxed-expression-component/dist/api"; import { useSettings } from "../settings/DmnEditorSettingsContext"; +import { DmnLatestModel } from "@kie-tools/dmn-marshaller"; +import { + isIdentifierReferencedInSomeExpression, + RefactorConfirmationDialog, +} from "../refactor/RefactorConfirmationDialog"; +import { OnEditableNodeLabelChange } from "../diagram/nodes/EditableNodeLabel"; export function BkmProperties({ bkm, namespace, index, + externalDmnModelsByNamespaceMap, }: { bkm: Normalized; namespace: string | undefined; index: number; + externalDmnModelsByNamespaceMap: Map>; }) { const { setState } = useDmnEditorStoreApi(); const settings = useSettings(); @@ -53,8 +64,80 @@ export function BkmProperties({ const resolvedTypeRef = useResolvedTypeRef(bkm.variable?.["@_typeRef"], namespace); + const [isRefactorModalOpen, setIsRefactorModalOpen] = useState(false); + const [newName, setNewName] = useState(""); + const identifierId = useMemo(() => bkm["@_id"], [bkm]); + const oldName = useMemo(() => bkm["@_label"] ?? bkm["@_name"], [bkm]); + + const applyRename = useCallback( + (args: { + definitions: Normalized; + newName: string; + shouldRenameReferencedExpressions: boolean; + }) => { + renameDrgElement({ + ...args, + index, + externalDmnModelsByNamespaceMap, + }); + }, + [externalDmnModelsByNamespaceMap, index] + ); + + const setName = useCallback( + (name: string) => { + if (name === oldName) { + return; + } + setState((state) => { + if ( + isIdentifierReferencedInSomeExpression({ + identifierUuid: identifierId, + dmnDefinitions: state.dmn.model.definitions, + externalDmnModelsByNamespaceMap, + }) + ) { + setNewName(name); + setIsRefactorModalOpen(true); + } else { + applyRename({ + definitions: state.dmn.model.definitions, + newName: name, + shouldRenameReferencedExpressions: false, + }); + } + }); + }, + [applyRename, externalDmnModelsByNamespaceMap, oldName, identifierId, setState] + ); + return ( <> + { + setIsRefactorModalOpen(false); + setState((state) => { + applyRename({ + definitions: state.dmn.model.definitions, + newName, + shouldRenameReferencedExpressions: true, + }); + }); + }} + onConfirmRenameOnly={() => { + setIsRefactorModalOpen(false); + setState((state) => { + applyRename({ + definitions: state.dmn.model.definitions, + newName, + shouldRenameReferencedExpressions: false, + }); + }); + }} + isRefactorModalOpen={isRefactorModalOpen} + fromName={oldName} + toName={newName} + /> { - setState((state) => { - renameDrgElement({ - definitions: state.dmn.model.definitions, - index, - newName, - }); - }); - }} + onRenamed={setName} allUniqueNames={useCallback((s) => s.computed(s).getAllFeelVariableUniqueNames(), [])} /> diff --git a/packages/dmn-editor/src/propertiesPanel/BoxedExpressionPropertiesPanelComponents/DecisionTableOutputHeaderCell.tsx b/packages/dmn-editor/src/propertiesPanel/BoxedExpressionPropertiesPanelComponents/DecisionTableOutputHeaderCell.tsx index 46dca185da5..663366e9c08 100644 --- a/packages/dmn-editor/src/propertiesPanel/BoxedExpressionPropertiesPanelComponents/DecisionTableOutputHeaderCell.tsx +++ b/packages/dmn-editor/src/propertiesPanel/BoxedExpressionPropertiesPanelComponents/DecisionTableOutputHeaderCell.tsx @@ -22,7 +22,11 @@ import { useCallback, useMemo, useState } from "react"; import { BoxedExpressionIndex } from "../../boxedExpressions/boxedExpressionIndex"; import { ContentField, DescriptionField, ExpressionLanguageField, NameField, TypeRefField } from "./Fields"; import { FormGroup, FormSection } from "@patternfly/react-core/dist/js/components/Form"; -import { DMN15__tDecision, DMN15__tOutputClause } from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types"; +import { + DMN15__tDecision, + DMN15__tDefinitions, + DMN15__tOutputClause, +} from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types"; import { Normalized } from "@kie-tools/dmn-marshaller/dist/normalization/normalize"; import { buildXmlHref } from "@kie-tools/dmn-marshaller/dist/xml/xmlHrefs"; import { PropertiesPanelHeader } from "../PropertiesPanelHeader"; @@ -35,6 +39,12 @@ import { useDmnEditorStore, useDmnEditorStoreApi } from "../../store/StoreContex import { useExternalModels } from "../../includedModels/DmnEditorDependenciesContext"; import { State } from "../../store/Store"; import { renameDrgElement } from "../../mutations/renameNode"; +import { DmnLatestModel } from "@kie-tools/dmn-marshaller"; +import { OnEditableNodeLabelChange } from "../../diagram/nodes/EditableNodeLabel"; +import { + isIdentifierReferencedInSomeExpression, + RefactorConfirmationDialog, +} from "../../refactor/RefactorConfirmationDialog"; export function DecisionTableOutputHeaderCell(props: { boxedExpressionIndex?: BoxedExpressionIndex; @@ -46,6 +56,18 @@ export function DecisionTableOutputHeaderCell(props: { const { dmnEditorRootElementRef } = useDmnEditor(); const { externalModelsByNamespace } = useExternalModels(); + const externalDmnsByNamespace = useDmnEditorStore( + (s) => s.computed(s).getDirectlyIncludedExternalModelsByNamespace(externalModelsByNamespace).dmns + ); + const externalDmnModelsByNamespaceMap = useMemo(() => { + const externalModels = new Map>(); + + for (const [key, externalDmn] of externalDmnsByNamespace) { + externalModels.set(key, externalDmn.model); + } + return externalModels; + }, [externalDmnsByNamespace]); + const node = useDmnEditorStore((s) => s .computed(s) @@ -77,7 +99,7 @@ export function DecisionTableOutputHeaderCell(props: { [props.boxedExpressionIndex, selectedObjectInfos?.expressionPath] ); - // In case the the output column is merged, the output column should have the same type as the Decision Node + // In case the output column is merged, the output column should have the same type as the Decision Node // It can happen to output column and Decision Node have different types, e.g. broken model. // For this case, the user will be able to fix it. const cellMustHaveSameTypeAsRoot = useMemo( @@ -127,8 +149,81 @@ export function DecisionTableOutputHeaderCell(props: { } }, [selectedObjectInfos?.expressionPath]); + const [isRefactorModalOpen, setIsRefactorModalOpen] = useState(false); + const [newName, setNewName] = useState(""); + + const identifierId = useMemo(() => root?.["@_id"] ?? "", [root]); + const oldName = useMemo(() => root?.["@_label"] ?? "", [root]); + + const applyRename = useCallback( + (args: { + definitions: Normalized; + newName: string; + shouldRenameReferencedExpressions: boolean; + }) => { + renameDrgElement({ + ...args, + index: node?.data.index ?? 0, + externalDmnModelsByNamespaceMap, + }); + }, + [externalDmnModelsByNamespaceMap, node?.data.index] + ); + + const setName = useCallback( + (name: string) => { + if (name === oldName) { + return; + } + dmnEditorStoreApi.setState((state) => { + if ( + isIdentifierReferencedInSomeExpression({ + identifierUuid: identifierId, + dmnDefinitions: state.dmn.model.definitions, + externalDmnModelsByNamespaceMap, + }) + ) { + setNewName(name); + setIsRefactorModalOpen(true); + } else { + applyRename({ + definitions: state.dmn.model.definitions, + newName: name, + shouldRenameReferencedExpressions: false, + }); + } + }); + }, + [oldName, dmnEditorStoreApi, identifierId, externalDmnModelsByNamespaceMap, applyRename] + ); + return ( <> + { + setIsRefactorModalOpen(false); + dmnEditorStoreApi.setState((state) => { + applyRename({ + definitions: state.dmn.model.definitions, + newName, + shouldRenameReferencedExpressions: true, + }); + }); + }} + onConfirmRenameOnly={() => { + setIsRefactorModalOpen(false); + dmnEditorStoreApi.setState((state) => { + applyRename({ + definitions: state.dmn.model.definitions, + newName, + shouldRenameReferencedExpressions: false, + }); + }); + }} + isRefactorModalOpen={isRefactorModalOpen} + fromName={oldName} + toName={newName} + /> {selectedObjectId} @@ -142,15 +237,7 @@ export function DecisionTableOutputHeaderCell(props: { id={root["@_id"]!} name={root?.["@_label"] ?? ""} getAllUniqueNames={getAllUniqueNames} - onChange={(newName) => { - dmnEditorStoreApi.setState((state) => { - renameDrgElement({ - definitions: state.dmn.model.definitions, - index: node?.data.index ?? 0, - newName, - }); - }); - }} + onChange={setName} /> ; namespace: string | undefined; index: number; + externalDmnModelsByNamespaceMap: Map>; }) { const { setState } = useDmnEditorStoreApi(); const settings = useSettings(); @@ -53,8 +61,80 @@ export function DecisionProperties({ const resolvedTypeRef = useResolvedTypeRef(decision.variable?.["@_typeRef"], namespace); + const [isRefactorModalOpen, setIsRefactorModalOpen] = useState(false); + const [newName, setNewName] = useState(""); + const identifierId = useMemo(() => decision["@_id"], [decision]); + const oldName = useMemo(() => decision["@_label"] ?? decision["@_name"], [decision]); + + const applyRename = useCallback( + (args: { + definitions: Normalized; + newName: string; + shouldRenameReferencedExpressions: boolean; + }) => { + renameDrgElement({ + ...args, + index, + externalDmnModelsByNamespaceMap, + }); + }, + [externalDmnModelsByNamespaceMap, index] + ); + + const setName = useCallback( + (name: string) => { + if (name === oldName) { + return; + } + setState((state) => { + if ( + isIdentifierReferencedInSomeExpression({ + identifierUuid: identifierId, + dmnDefinitions: state.dmn.model.definitions, + externalDmnModelsByNamespaceMap, + }) + ) { + setNewName(name); + setIsRefactorModalOpen(true); + } else { + applyRename({ + definitions: state.dmn.model.definitions, + newName: name, + shouldRenameReferencedExpressions: false, + }); + } + }); + }, + [applyRename, externalDmnModelsByNamespaceMap, oldName, identifierId, setState] + ); + return ( <> + { + setIsRefactorModalOpen(false); + setState((state) => { + applyRename({ + definitions: state.dmn.model.definitions, + newName, + shouldRenameReferencedExpressions: true, + }); + }); + }} + onConfirmRenameOnly={() => { + setIsRefactorModalOpen(false); + setState((state) => { + applyRename({ + definitions: state.dmn.model.definitions, + newName, + shouldRenameReferencedExpressions: false, + }); + }); + }} + isRefactorModalOpen={isRefactorModalOpen} + fromName={oldName} + toName={newName} + /> { - setState((state) => { - renameDrgElement({ - definitions: state.dmn.model.definitions, - index, - newName, - }); - }); - }} + onRenamed={setName} allUniqueNames={useCallback((s) => s.computed(s).getAllFeelVariableUniqueNames(), [])} /> diff --git a/packages/dmn-editor/src/propertiesPanel/DecisionServiceProperties.tsx b/packages/dmn-editor/src/propertiesPanel/DecisionServiceProperties.tsx index 19102553b35..6cdc23158b9 100644 --- a/packages/dmn-editor/src/propertiesPanel/DecisionServiceProperties.tsx +++ b/packages/dmn-editor/src/propertiesPanel/DecisionServiceProperties.tsx @@ -33,7 +33,7 @@ import { TextArea } from "@patternfly/react-core/dist/js/components/TextArea"; import { DocumentationLinksFormGroup } from "./DocumentationLinksFormGroup"; import { TypeRefSelector } from "../dataTypes/TypeRefSelector"; import { useDmnEditorStore, useDmnEditorStoreApi } from "../store/StoreContext"; -import { useCallback, useMemo } from "react"; +import { useCallback, useMemo, useState } from "react"; import { DmnObjectListItem } from "../externalNodes/DmnObjectListItem"; import { renameDrgElement } from "../mutations/renameNode"; import { InlineFeelNameInput } from "../feel/InlineFeelNameInput"; @@ -48,6 +48,12 @@ import { generateUuid } from "@kie-tools/boxed-expression-component/dist/api"; import { ExternalDmn } from "../DmnEditor"; import { Unpacked } from "../tsExt/tsExt"; import { useSettings } from "../settings/DmnEditorSettingsContext"; +import { DmnLatestModel } from "@kie-tools/dmn-marshaller"; +import { + isIdentifierReferencedInSomeExpression, + RefactorConfirmationDialog, +} from "../refactor/RefactorConfirmationDialog"; +import { OnEditableNodeLabelChange } from "../diagram/nodes/EditableNodeLabel"; export type AllKnownDrgElementsByHref = Map< string, @@ -59,10 +65,12 @@ export function DecisionServiceProperties({ decisionService, namespace, index, + externalDmnModelsByNamespaceMap, }: { decisionService: Normalized; namespace: string | undefined; index: number; + externalDmnModelsByNamespaceMap: Map>; }) { const { setState } = useDmnEditorStoreApi(); const settings = useSettings(); @@ -112,8 +120,80 @@ export function DecisionServiceProperties({ const resolvedTypeRef = useResolvedTypeRef(decisionService.variable?.["@_typeRef"], namespace); + const [isRefactorModalOpen, setIsRefactorModalOpen] = useState(false); + const [newName, setNewName] = useState(""); + const identifierId = useMemo(() => decisionService["@_id"], [decisionService]); + const oldName = useMemo(() => decisionService["@_label"] ?? decisionService["@_name"], [decisionService]); + + const applyRename = useCallback( + (args: { + definitions: Normalized; + newName: string; + shouldRenameReferencedExpressions: boolean; + }) => { + renameDrgElement({ + ...args, + index, + externalDmnModelsByNamespaceMap, + }); + }, + [externalDmnModelsByNamespaceMap, index] + ); + + const setName = useCallback( + (name: string) => { + if (name === oldName) { + return; + } + setState((state) => { + if ( + isIdentifierReferencedInSomeExpression({ + identifierUuid: identifierId, + dmnDefinitions: state.dmn.model.definitions, + externalDmnModelsByNamespaceMap, + }) + ) { + setNewName(name); + setIsRefactorModalOpen(true); + } else { + applyRename({ + definitions: state.dmn.model.definitions, + newName: name, + shouldRenameReferencedExpressions: false, + }); + } + }); + }, + [applyRename, externalDmnModelsByNamespaceMap, oldName, identifierId, setState] + ); + return ( <> + { + setIsRefactorModalOpen(false); + setState((state) => { + applyRename({ + definitions: state.dmn.model.definitions, + newName, + shouldRenameReferencedExpressions: true, + }); + }); + }} + onConfirmRenameOnly={() => { + setIsRefactorModalOpen(false); + setState((state) => { + applyRename({ + definitions: state.dmn.model.definitions, + newName, + shouldRenameReferencedExpressions: false, + }); + }); + }} + isRefactorModalOpen={isRefactorModalOpen} + fromName={oldName} + toName={newName} + /> { - setState((state) => { - renameDrgElement({ - definitions: state.dmn.model.definitions, - index, - newName, - }); - }); - }} + onRenamed={setName} allUniqueNames={useCallback((s) => s.computed(s).getAllFeelVariableUniqueNames(), [])} /> diff --git a/packages/dmn-editor/src/propertiesPanel/InputDataProperties.tsx b/packages/dmn-editor/src/propertiesPanel/InputDataProperties.tsx index e3baada963f..a19d86ac64b 100644 --- a/packages/dmn-editor/src/propertiesPanel/InputDataProperties.tsx +++ b/packages/dmn-editor/src/propertiesPanel/InputDataProperties.tsx @@ -18,7 +18,7 @@ */ import * as React from "react"; -import { DMN15__tInputData } from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types"; +import { DMN15__tDefinitions, DMN15__tInputData } from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types"; import { Normalized } from "@kie-tools/dmn-marshaller/dist/normalization/normalize"; import { ClipboardCopy } from "@patternfly/react-core/dist/js/components/ClipboardCopy"; import { FormGroup } from "@patternfly/react-core/dist/js/components/Form"; @@ -30,18 +30,26 @@ import { renameDrgElement } from "../mutations/renameNode"; import { InlineFeelNameInput } from "../feel/InlineFeelNameInput"; import { useDmnEditor } from "../DmnEditorContext"; import { useResolvedTypeRef } from "../dataTypes/useResolvedTypeRef"; -import { useCallback } from "react"; +import { useCallback, useMemo, useState } from "react"; import { generateUuid } from "@kie-tools/boxed-expression-component/dist/api"; import { useSettings } from "../settings/DmnEditorSettingsContext"; +import { DmnLatestModel } from "@kie-tools/dmn-marshaller"; +import { OnEditableNodeLabelChange } from "../diagram/nodes/EditableNodeLabel"; +import { + isIdentifierReferencedInSomeExpression, + RefactorConfirmationDialog, +} from "../refactor/RefactorConfirmationDialog"; export function InputDataProperties({ inputData, namespace, index, + externalDmnModelsByNamespaceMap, }: { inputData: Normalized; namespace: string | undefined; index: number; + externalDmnModelsByNamespaceMap: Map>; }) { const { setState } = useDmnEditorStoreApi(); const settings = useSettings(); @@ -53,8 +61,80 @@ export function InputDataProperties({ const resolvedTypeRef = useResolvedTypeRef(inputData.variable?.["@_typeRef"], namespace); + const [isRefactorModalOpen, setIsRefactorModalOpen] = useState(false); + const [newName, setNewName] = useState(""); + const identifierId = useMemo(() => inputData["@_id"], [inputData]); + const oldName = useMemo(() => inputData["@_label"] ?? inputData["@_name"], [inputData]); + + const applyRename = useCallback( + (args: { + definitions: Normalized; + newName: string; + shouldRenameReferencedExpressions: boolean; + }) => { + renameDrgElement({ + ...args, + index, + externalDmnModelsByNamespaceMap, + }); + }, + [externalDmnModelsByNamespaceMap, index] + ); + + const setName = useCallback( + (name: string) => { + if (name === oldName) { + return; + } + setState((state) => { + if ( + isIdentifierReferencedInSomeExpression({ + identifierUuid: identifierId, + dmnDefinitions: state.dmn.model.definitions, + externalDmnModelsByNamespaceMap, + }) + ) { + setNewName(name); + setIsRefactorModalOpen(true); + } else { + applyRename({ + definitions: state.dmn.model.definitions, + newName: name, + shouldRenameReferencedExpressions: false, + }); + } + }); + }, + [applyRename, externalDmnModelsByNamespaceMap, oldName, identifierId, setState] + ); + return ( <> + { + setIsRefactorModalOpen(false); + setState((state) => { + applyRename({ + definitions: state.dmn.model.definitions, + newName, + shouldRenameReferencedExpressions: true, + }); + }); + }} + onConfirmRenameOnly={() => { + setIsRefactorModalOpen(false); + setState((state) => { + applyRename({ + definitions: state.dmn.model.definitions, + newName, + shouldRenameReferencedExpressions: false, + }); + }); + }} + isRefactorModalOpen={isRefactorModalOpen} + fromName={oldName} + toName={newName} + /> { - setState((state) => { - renameDrgElement({ - definitions: state.dmn.model.definitions, - index, - newName, - }); - }); - }} + onRenamed={setName} allUniqueNames={useCallback((s) => s.computed(s).getAllFeelVariableUniqueNames(), [])} /> diff --git a/packages/dmn-editor/src/propertiesPanel/KnowledgeSourceProperties.tsx b/packages/dmn-editor/src/propertiesPanel/KnowledgeSourceProperties.tsx index 3c1586c84e8..b561e069780 100644 --- a/packages/dmn-editor/src/propertiesPanel/KnowledgeSourceProperties.tsx +++ b/packages/dmn-editor/src/propertiesPanel/KnowledgeSourceProperties.tsx @@ -18,7 +18,10 @@ */ import * as React from "react"; -import { DMN15__tKnowledgeSource } from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types"; +import { + DMN15__tDefinitions, + DMN15__tKnowledgeSource, +} from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types"; import { Normalized } from "@kie-tools/dmn-marshaller/dist/normalization/normalize"; import { ClipboardCopy } from "@patternfly/react-core/dist/js/components/ClipboardCopy"; import { FormGroup } from "@patternfly/react-core/dist/js/components/Form"; @@ -28,8 +31,15 @@ import { DocumentationLinksFormGroup } from "./DocumentationLinksFormGroup"; import { useDmnEditorStore, useDmnEditorStoreApi } from "../store/StoreContext"; import { renameDrgElement } from "../mutations/renameNode"; import { InlineFeelNameInput } from "../feel/InlineFeelNameInput"; -import { useCallback } from "react"; +import { useCallback, useMemo, useState } from "react"; import { useSettings } from "../settings/DmnEditorSettingsContext"; +import { useExternalModels } from "../includedModels/DmnEditorDependenciesContext"; +import { DmnLatestModel } from "@kie-tools/dmn-marshaller"; +import { + isIdentifierReferencedInSomeExpression, + RefactorConfirmationDialog, +} from "../refactor/RefactorConfirmationDialog"; +import { OnEditableNodeLabelChange } from "../diagram/nodes/EditableNodeLabel"; export function KnowledgeSourceProperties({ knowledgeSource, @@ -45,9 +55,93 @@ export function KnowledgeSourceProperties({ const thisDmnsNamespace = useDmnEditorStore((s) => s.dmn.model.definitions["@_namespace"]); const isReadOnly = settings.isReadOnly || (!!namespace && namespace !== thisDmnsNamespace); + const { externalModelsByNamespace } = useExternalModels(); + const externalDmnsByNamespace = useDmnEditorStore( + (s) => s.computed(s).getDirectlyIncludedExternalModelsByNamespace(externalModelsByNamespace).dmns + ); + const externalDmnModelsByNamespaceMap = useMemo(() => { + const externalModels = new Map>(); + + for (const [key, externalDmn] of externalDmnsByNamespace) { + externalModels.set(key, externalDmn.model); + } + return externalModels; + }, [externalDmnsByNamespace]); + + const [isRefactorModalOpen, setIsRefactorModalOpen] = useState(false); + const [newName, setNewName] = useState(""); + const identifierId = useMemo(() => knowledgeSource["@_id"], [knowledgeSource]); + const oldName = useMemo(() => knowledgeSource["@_label"] ?? knowledgeSource["@_name"], [knowledgeSource]); + + const applyRename = useCallback( + (args: { + definitions: Normalized; + newName: string; + shouldRenameReferencedExpressions: boolean; + }) => { + renameDrgElement({ + ...args, + index, + externalDmnModelsByNamespaceMap, + }); + }, + [externalDmnModelsByNamespaceMap, index] + ); + + const setName = useCallback( + (name: string) => { + if (name === oldName) { + return; + } + setState((state) => { + if ( + isIdentifierReferencedInSomeExpression({ + identifierUuid: identifierId, + dmnDefinitions: state.dmn.model.definitions, + externalDmnModelsByNamespaceMap, + }) + ) { + setNewName(name); + setIsRefactorModalOpen(true); + } else { + applyRename({ + definitions: state.dmn.model.definitions, + newName: name, + shouldRenameReferencedExpressions: false, + }); + } + }); + }, + [applyRename, externalDmnModelsByNamespaceMap, oldName, identifierId, setState] + ); return ( <> + { + setIsRefactorModalOpen(false); + setState((state) => { + applyRename({ + definitions: state.dmn.model.definitions, + newName, + shouldRenameReferencedExpressions: true, + }); + }); + }} + onConfirmRenameOnly={() => { + setIsRefactorModalOpen(false); + setState((state) => { + applyRename({ + definitions: state.dmn.model.definitions, + newName, + shouldRenameReferencedExpressions: false, + }); + }); + }} + isRefactorModalOpen={isRefactorModalOpen} + fromName={oldName} + toName={newName} + /> { - setState((state) => { - renameDrgElement({ - definitions: state.dmn.model.definitions, - index, - newName, - }); - }); - }} + onRenamed={setName} allUniqueNames={useCallback((s) => s.computed(s).getAllFeelVariableUniqueNames(), [])} /> diff --git a/packages/dmn-editor/src/propertiesPanel/SingleNodeProperties.tsx b/packages/dmn-editor/src/propertiesPanel/SingleNodeProperties.tsx index 2e1060e7273..7128e621655 100644 --- a/packages/dmn-editor/src/propertiesPanel/SingleNodeProperties.tsx +++ b/packages/dmn-editor/src/propertiesPanel/SingleNodeProperties.tsx @@ -51,6 +51,7 @@ import { PropertiesPanelHeader } from "./PropertiesPanelHeader"; import { UnknownProperties } from "./UnknownProperties"; import { useExternalModels } from "../includedModels/DmnEditorDependenciesContext"; import "./SingleNodeProperties.css"; +import { DmnLatestModel } from "@kie-tools/dmn-marshaller"; export function SingleNodeProperties({ nodeId }: { nodeId: string }) { const dmnEditorStoreApi = useDmnEditorStoreApi(); @@ -59,6 +60,18 @@ export function SingleNodeProperties({ nodeId }: { nodeId: string }) { const [isSectionExpanded, setSectionExpanded] = useState(true); const isAlternativeInputDataShape = useDmnEditorStore((s) => s.computed(s).isAlternativeInputDataShape()); const nodeIds = useMemo(() => (node?.id ? [node.id] : []), [node?.id]); + const externalDmnsByNamespace = useDmnEditorStore( + (s) => s.computed(s).getDirectlyIncludedExternalModelsByNamespace(externalModelsByNamespace).dmns + ); + + const externalDmnModelsByNamespaceMap = useMemo(() => { + const externalModels = new Map>(); + + for (const [key, externalDmn] of externalDmnsByNamespace) { + externalModels.set(key, externalDmn.model); + } + return externalModels; + }, [externalDmnsByNamespace]); const Icon = useMemo(() => { if (node?.data?.dmnObject === undefined) { @@ -137,6 +150,7 @@ export function SingleNodeProperties({ nodeId }: { nodeId: string }) { inputData={node.data!.dmnObject as Normalized} namespace={node.data.dmnObjectNamespace} index={node.data.index} + externalDmnModelsByNamespaceMap={externalDmnModelsByNamespaceMap} /> ); case NODE_TYPES.decision: @@ -145,6 +159,7 @@ export function SingleNodeProperties({ nodeId }: { nodeId: string }) { decision={node.data!.dmnObject as Normalized} namespace={node.data.dmnObjectNamespace} index={node.data.index} + externalDmnModelsByNamespaceMap={externalDmnModelsByNamespaceMap} /> ); case NODE_TYPES.bkm: @@ -153,6 +168,7 @@ export function SingleNodeProperties({ nodeId }: { nodeId: string }) { bkm={node.data!.dmnObject as Normalized} namespace={node.data.dmnObjectNamespace} index={node.data.index} + externalDmnModelsByNamespaceMap={externalDmnModelsByNamespaceMap} /> ); case NODE_TYPES.decisionService: @@ -161,6 +177,7 @@ export function SingleNodeProperties({ nodeId }: { nodeId: string }) { decisionService={node.data!.dmnObject as Normalized} namespace={node.data.dmnObjectNamespace} index={node.data.index} + externalDmnModelsByNamespaceMap={externalDmnModelsByNamespaceMap} /> ); case NODE_TYPES.knowledgeSource: diff --git a/packages/dmn-editor/src/refactor/RefactorConfirmationDialog.tsx b/packages/dmn-editor/src/refactor/RefactorConfirmationDialog.tsx new file mode 100644 index 00000000000..0fcd63db594 --- /dev/null +++ b/packages/dmn-editor/src/refactor/RefactorConfirmationDialog.tsx @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Modal, ModalVariant } from "@patternfly/react-core/dist/js/components/Modal"; +import { Button, ButtonVariant } from "@patternfly/react-core/dist/js/components/Button"; +import { Normalized } from "@kie-tools/dmn-marshaller/dist/normalization/normalize"; +import * as React from "react"; +import { DMN15__tDefinitions } from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types"; +import { IdentifiersRefactor } from "../../../dmn-language-service"; +import { DmnLatestModel } from "../../../dmn-marshaller"; + +export function RefactorConfirmationDialog({ + onConfirmExpressionRefactor, + onConfirmRenameOnly, + isRefactorModalOpen, + fromName, + toName, +}: { + onConfirmExpressionRefactor: () => void; + onConfirmRenameOnly: () => void; + isRefactorModalOpen: boolean; + fromName: string | undefined; + toName: string | undefined; +}) { + return ( + + Yes, rename and update the expressions + , + , + ]} + > + The identifier `{fromName ?? ""}` was renamed to `{toName ?? ""}`. +
+
+ This identifier is used in one or more expressions. +
+
+ Do you want also automatically update the expressions to the new name? + + ); +} + +export function isIdentifierReferencedInSomeExpression(args: { + identifierUuid: string; + dmnDefinitions: Normalized; + externalDmnModelsByNamespaceMap: Map>; +}) { + const identifiersRefactor = new IdentifiersRefactor({ + writeableDmnDefinitions: args.dmnDefinitions, + _readonly_externalDmnModelsByNamespaceMap: args.externalDmnModelsByNamespaceMap, + }); + + return Array.from(identifiersRefactor.getExpressionsThatUseTheIdentifier(args.identifierUuid)).length > 0; +} diff --git a/packages/dmn-editor/stories/dev/DevWebApp.stories.tsx b/packages/dmn-editor/stories/dev/DevWebApp.stories.tsx index 3d9922f2118..42d8df1937a 100644 --- a/packages/dmn-editor/stories/dev/DevWebApp.stories.tsx +++ b/packages/dmn-editor/stories/dev/DevWebApp.stories.tsx @@ -36,7 +36,7 @@ import { OnRequestExternalModelByPath, OnRequestExternalModelsAvailableToInclude, OnRequestToJumpToPath, -} from "../../src/DmnEditor"; +} from "@kie-tools/dmn-editor/dist/DmnEditor"; const initialModel = generateEmptyDmn15(); diff --git a/packages/dmn-editor/stories/dev/availableModelsToInclude.ts b/packages/dmn-editor/stories/dev/availableModelsToInclude.ts index bca1dd1eb59..f1b4d3fc4c8 100644 --- a/packages/dmn-editor/stories/dev/availableModelsToInclude.ts +++ b/packages/dmn-editor/stories/dev/availableModelsToInclude.ts @@ -20,8 +20,8 @@ import { getMarshaller } from "@kie-tools/dmn-marshaller"; import { normalize } from "@kie-tools/dmn-marshaller/dist/normalization/normalize"; import { XML2PMML } from "@kie-tools/pmml-editor-marshaller"; -import * as DmnEditor from "../../src/DmnEditor"; -import { getPmmlNamespace } from "../../src/pmml/pmml"; +import * as DmnEditor from "@kie-tools/dmn-editor/dist/DmnEditor"; +import { getPmmlNamespace } from "@kie-tools/dmn-editor/dist/pmml/pmml"; import { sumBkm, sumDiffDs, testTreePmml } from "./externalModels"; export const sumBkmModel = normalize(getMarshaller(sumBkm, { upgradeTo: "latest" }).parser.parse()); diff --git a/packages/dmn-editor/stories/dmnEditorStoriesWrapper.tsx b/packages/dmn-editor/stories/dmnEditorStoriesWrapper.tsx index 25191c97e9e..b70303e5091 100644 --- a/packages/dmn-editor/stories/dmnEditorStoriesWrapper.tsx +++ b/packages/dmn-editor/stories/dmnEditorStoriesWrapper.tsx @@ -20,7 +20,13 @@ import * as React from "react"; import { useCallback, useState, useRef, useMemo, useEffect } from "react"; import { useArgs } from "@storybook/preview-api"; -import { DmnEditor, DmnEditorProps, DmnEditorRef, EvaluationResults, ValidationMessages } from "../src/DmnEditor"; +import { + DmnEditor, + DmnEditorProps, + DmnEditorRef, + EvaluationResults, + ValidationMessages, +} from "@kie-tools/dmn-editor/dist/DmnEditor"; import { DmnLatestModel, getMarshaller } from "@kie-tools/dmn-marshaller"; import { normalize } from "@kie-tools/dmn-marshaller/dist/normalization/normalize"; import { diff } from "deep-object-diff"; diff --git a/packages/dmn-editor/stories/misc/empty/Empty.stories.tsx b/packages/dmn-editor/stories/misc/empty/Empty.stories.tsx index ecbac14555d..60057d614be 100644 --- a/packages/dmn-editor/stories/misc/empty/Empty.stories.tsx +++ b/packages/dmn-editor/stories/misc/empty/Empty.stories.tsx @@ -23,7 +23,7 @@ import { ns as dmn15ns } from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts import { generateUuid } from "@kie-tools/boxed-expression-component/dist/api"; import { DMN15_SPEC } from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/Dmn15Spec"; import { DmnEditorWrapper, StorybookDmnEditorProps } from "../../dmnEditorStoriesWrapper"; -import { DmnEditor, DmnEditorProps } from "../../../src/DmnEditor"; +import { DmnEditor, DmnEditorProps } from "@kie-tools/dmn-editor/dist/DmnEditor"; export const generateEmptyDmn15 = () => ` diff --git a/packages/dmn-feel-antlr4-parser/package.json b/packages/dmn-feel-antlr4-parser/package.json index aaef41cbba3..e023fd2a846 100644 --- a/packages/dmn-feel-antlr4-parser/package.json +++ b/packages/dmn-feel-antlr4-parser/package.json @@ -25,7 +25,8 @@ }, "dependencies": { "@kie-tools/dmn-marshaller": "workspace:*", - "antlr4": "^4.13.0" + "antlr4": "^4.13.0", + "uuid": "^8.3.2" }, "devDependencies": { "@babel/core": "^7.16.0", @@ -33,6 +34,7 @@ "@kie-tools/eslint": "workspace:*", "@kie-tools/root-env": "workspace:*", "@kie-tools/tsconfig": "workspace:*", + "@types/uuid": "^8.3.0", "rimraf": "^3.0.2", "typescript": "^5.5.3" } diff --git a/packages/dmn-feel-antlr4-parser/src/FeelIdentifiers.ts b/packages/dmn-feel-antlr4-parser/src/FeelIdentifiers.ts new file mode 100644 index 00000000000..d24fbf81b24 --- /dev/null +++ b/packages/dmn-feel-antlr4-parser/src/FeelIdentifiers.ts @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { FeelIdentifiersParser } from "./parser/FeelIdentifiersParser"; +import { DmnDefinitions, IdentifiersRepository } from "./parser/IdentifiersRepository"; +import { DmnLatestModel } from "@kie-tools/dmn-marshaller"; +import { ParsedExpression } from "./parser/ParsedExpression"; +import { Expression } from "./parser/Expression"; + +export class FeelIdentifiers { + private readonly parser: FeelIdentifiersParser; + private readonly repository: IdentifiersRepository; + + constructor(args: { + _readonly_dmnDefinitions: DmnDefinitions; + _readonly_externalDefinitions?: Map; + }) { + this.repository = new IdentifiersRepository(args._readonly_dmnDefinitions, args._readonly_externalDefinitions); + this.parser = new FeelIdentifiersParser(this.repository); + } + + public parse(args: { identifierContextUuid: string; expression: string }): ParsedExpression { + return this.parser.parse(args.identifierContextUuid, args.expression); + } + + get expressions(): Map { + return this.repository.expressions; + } +} diff --git a/packages/dmn-feel-antlr4-parser/src/FeelVariables.ts b/packages/dmn-feel-antlr4-parser/src/FeelVariables.ts deleted file mode 100644 index d5036402754..00000000000 --- a/packages/dmn-feel-antlr4-parser/src/FeelVariables.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { FeelVariablesParser } from "./parser/FeelVariablesParser"; -import { DmnDefinitions, VariablesRepository } from "./parser/VariablesRepository"; -import { DmnLatestModel } from "@kie-tools/dmn-marshaller"; - -export class FeelVariables { - private readonly _parser: FeelVariablesParser; - private readonly _repository: VariablesRepository; - - constructor(dmnDefinitions: DmnDefinitions, externalDefinitions: Map) { - this._repository = new VariablesRepository(dmnDefinitions, externalDefinitions); - this._parser = new FeelVariablesParser(this._repository); - } - - get parser(): FeelVariablesParser { - return this._parser; - } - - get repository(): VariablesRepository { - return this._repository; - } -} diff --git a/packages/dmn-feel-antlr4-parser/src/Uuid.ts b/packages/dmn-feel-antlr4-parser/src/Uuid.ts new file mode 100644 index 00000000000..9313517e9c5 --- /dev/null +++ b/packages/dmn-feel-antlr4-parser/src/Uuid.ts @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { v4 as uuid } from "uuid"; + +export {}; + +/** + * Generates a UUID with a format similar to _6EFDBCB4-F4AF-4E9A-9A66-2A9F24185674 + */ +export const generateUuid = () => { + return `_${uuid()}`.toLocaleUpperCase(); +}; diff --git a/packages/dmn-feel-antlr4-parser/src/index.ts b/packages/dmn-feel-antlr4-parser/src/index.ts index cb8a12276bc..f4eeb7b272d 100644 --- a/packages/dmn-feel-antlr4-parser/src/index.ts +++ b/packages/dmn-feel-antlr4-parser/src/index.ts @@ -17,13 +17,14 @@ * under the License. */ -export * from "./FeelVariables"; -export * from "./parser/FeelVariable"; +export * from "./FeelIdentifiers"; +export * from "./parser/FeelIdentifiedSymbol"; export * from "./parser/ReservedWords"; export * from "./parser/FeelSyntacticSymbolNature"; -export * from "./parser/FeelVariablesParser"; -export * from "./parser/VariablesRepository"; +export * from "./parser/FeelIdentifiersParser"; +export * from "./parser/IdentifiersRepository"; export * from "./parser/ParsedExpression"; export * from "./parser/FeelSymbol"; export * from "./parser/BuiltInTypes"; export * from "./parser/DataType"; +export * from "./parser/Expression"; diff --git a/packages/dmn-feel-antlr4-parser/src/parser/BuiltInTypes.ts b/packages/dmn-feel-antlr4-parser/src/parser/BuiltInTypes.ts index d399bb38e52..9f40144f114 100644 --- a/packages/dmn-feel-antlr4-parser/src/parser/BuiltInTypes.ts +++ b/packages/dmn-feel-antlr4-parser/src/parser/BuiltInTypes.ts @@ -18,27 +18,52 @@ */ import { DataType } from "./DataType"; +import { Expression } from "./Expression"; +import { FeelSyntacticSymbolNature } from "./FeelSyntacticSymbolNature"; +import { generateUuid } from "../Uuid"; export class BuiltInTypes { public static readonly Number: DataType = { + uuid: generateUuid(), name: "number", typeRef: "number", properties: new Map([]), + + source: { + expressionsThatUseTheIdentifier: new Map(), + value: "number", + feelSyntacticSymbolNature: FeelSyntacticSymbolNature.GlobalVariable, + }, }; public static readonly Boolean: DataType = { + uuid: generateUuid(), name: "boolean", typeRef: "boolean", properties: new Map([]), + + source: { + expressionsThatUseTheIdentifier: new Map(), + value: "boolean", + feelSyntacticSymbolNature: FeelSyntacticSymbolNature.GlobalVariable, + }, }; public static readonly String: DataType = { + uuid: generateUuid(), name: "string", typeRef: "string", properties: new Map([]), + + source: { + expressionsThatUseTheIdentifier: new Map(), + value: "string", + feelSyntacticSymbolNature: FeelSyntacticSymbolNature.GlobalVariable, + }, }; public static readonly DaysAndTimeDuration: DataType = { + uuid: generateUuid(), name: "days and time duration", typeRef: "days and time duration", properties: new Map([ @@ -48,9 +73,16 @@ export class BuiltInTypes { ["seconds", BuiltInTypes.Number], ["timezone", BuiltInTypes.String], ]), + + source: { + expressionsThatUseTheIdentifier: new Map(), + value: "days and time duration", + feelSyntacticSymbolNature: FeelSyntacticSymbolNature.GlobalVariable, + }, }; public static readonly DateAndTime: DataType = { + uuid: generateUuid(), name: "date and time", typeRef: "date and time", properties: new Map([ @@ -64,18 +96,32 @@ export class BuiltInTypes { ["time offset", BuiltInTypes.DaysAndTimeDuration], ["timezone", BuiltInTypes.String], ]), + + source: { + expressionsThatUseTheIdentifier: new Map(), + value: "date and time", + feelSyntacticSymbolNature: FeelSyntacticSymbolNature.GlobalVariable, + }, }; public static readonly YearsAndMonthsDuration: DataType = { + uuid: generateUuid(), name: "years and months duration", typeRef: "years and months duration", properties: new Map([ ["years", BuiltInTypes.Number], ["months", BuiltInTypes.Number], ]), + + source: { + expressionsThatUseTheIdentifier: new Map(), + value: "years and months duration", + feelSyntacticSymbolNature: FeelSyntacticSymbolNature.GlobalVariable, + }, }; public static readonly Time: DataType = { + uuid: generateUuid(), name: "time", typeRef: "time", properties: new Map([ @@ -85,9 +131,15 @@ export class BuiltInTypes { ["time offset", BuiltInTypes.DaysAndTimeDuration], ["timezone", BuiltInTypes.String], ]), + source: { + expressionsThatUseTheIdentifier: new Map(), + value: "time", + feelSyntacticSymbolNature: FeelSyntacticSymbolNature.GlobalVariable, + }, }; public static readonly Date: DataType = { + uuid: generateUuid(), name: "date", typeRef: "date", properties: new Map([ @@ -96,5 +148,10 @@ export class BuiltInTypes { ["day", BuiltInTypes.Number], ["weekday", BuiltInTypes.Number], ]), + source: { + expressionsThatUseTheIdentifier: new Map(), + value: "date", + feelSyntacticSymbolNature: FeelSyntacticSymbolNature.GlobalVariable, + }, }; } diff --git a/packages/dmn-feel-antlr4-parser/src/parser/DataType.ts b/packages/dmn-feel-antlr4-parser/src/parser/DataType.ts index 1c9f6a0d113..5058b052e5a 100644 --- a/packages/dmn-feel-antlr4-parser/src/parser/DataType.ts +++ b/packages/dmn-feel-antlr4-parser/src/parser/DataType.ts @@ -17,8 +17,12 @@ * under the License. */ +import { Identifier } from "./Identifier"; + export interface DataType { + uuid: string; name: string; properties: Map; typeRef?: string; + source: Identifier; } diff --git a/packages/dmn-feel-antlr4-parser/src/parser/VariableOccurrence.ts b/packages/dmn-feel-antlr4-parser/src/parser/Expression.ts similarity index 52% rename from packages/dmn-feel-antlr4-parser/src/parser/VariableOccurrence.ts rename to packages/dmn-feel-antlr4-parser/src/parser/Expression.ts index 47911bb4a4f..3883f66934c 100644 --- a/packages/dmn-feel-antlr4-parser/src/parser/VariableOccurrence.ts +++ b/packages/dmn-feel-antlr4-parser/src/parser/Expression.ts @@ -17,29 +17,35 @@ * under the License. */ -import { Variable } from "./Variable"; -import { FeelVariable } from "./FeelVariable"; +import { Identifier } from "./Identifier"; +import { FeelIdentifiedSymbol } from "./FeelIdentifiedSymbol"; +import { DmnLiteralExpression } from "./IdentifiersRepository"; export class Expression { private readonly _uuid: string; private _fullExpression: string; - private _variables: Array; + private _identifiersOfTheExpression: Array; + private source: DmnLiteralExpression; - constructor(uuid: string, fullExpression?: string) { + constructor(uuid: string, source: DmnLiteralExpression) { this._uuid = uuid; - this._variables = new Array(); - this._fullExpression = fullExpression ?? ""; + this._identifiersOfTheExpression = new Array(); + this._fullExpression = source.text?.__$$text ?? ""; + this.source = source; } - public renameVariable(renamedVariable: Variable, newName: String) { - // We assume that variables are already ordered by the parser + public applyChangesToExpressionSource() { + this.source.text = { __$$text: this._fullExpression }; + } + public renameIdentifier(identifier: Identifier, newName: String) { + // It is safe to assume that identifiers are already ordered by the parser. let offset = 0; - for (const variable of this._variables) { - variable.startIndex += offset; - if (variable.source != undefined && variable.source === renamedVariable) { - this.replaceAt(variable.startIndex, renamedVariable.value.length, newName); - offset += renamedVariable.value.length - newName.length; + for (const feelIdentifiedSymbol of this._identifiersOfTheExpression) { + feelIdentifiedSymbol.startIndex += offset; + if (feelIdentifiedSymbol.source != undefined && feelIdentifiedSymbol.source === identifier) { + this.replaceAt(feelIdentifiedSymbol.startIndex, identifier.value.length, newName); + offset += newName.length - identifier.value.length; } } } @@ -52,12 +58,12 @@ export class Expression { this.fullExpression = part1 + newPart + part2; } - get variables(): Array { - return this._variables; + get identifiersOfTheExpression(): Array { + return this._identifiersOfTheExpression; } - set variables(value: Array) { - this._variables = value; + set identifiersOfTheExpression(value: Array) { + this._identifiersOfTheExpression = value; } get fullExpression(): string { diff --git a/packages/dmn-feel-antlr4-parser/src/parser/FeelVariable.ts b/packages/dmn-feel-antlr4-parser/src/parser/FeelIdentifiedSymbol.ts similarity index 77% rename from packages/dmn-feel-antlr4-parser/src/parser/FeelVariable.ts rename to packages/dmn-feel-antlr4-parser/src/parser/FeelIdentifiedSymbol.ts index 8470f7ed066..3c2e1f4ef3e 100644 --- a/packages/dmn-feel-antlr4-parser/src/parser/FeelVariable.ts +++ b/packages/dmn-feel-antlr4-parser/src/parser/FeelIdentifiedSymbol.ts @@ -19,15 +19,18 @@ import { FeelSyntacticSymbolNature } from "./FeelSyntacticSymbolNature"; import { FeelSymbol } from "./FeelSymbol"; -import { Variable } from "./Variable"; +import { Identifier } from "./Identifier"; -export class FeelVariable { +/** + * Defines a Feel symbol already identified by the parser, inside an existing expression. + */ +export class FeelIdentifiedSymbol { private readonly _text: string; private _startIndex: number; private readonly _feelSymbolNature: FeelSyntacticSymbolNature; private readonly _length: number; private readonly _scopeSymbols: FeelSymbol[]; - private readonly _source: Variable | undefined; + private readonly _source: Identifier | undefined; private readonly _startLine: number; private readonly _endLine: number; @@ -39,7 +42,7 @@ export class FeelVariable { symbolType: FeelSyntacticSymbolNature, text: string, scopeSymbols?: FeelSymbol[], - source?: Variable + source?: Identifier ) { this._startIndex = startIndex; this._length = length; @@ -51,14 +54,23 @@ export class FeelVariable { this._endLine = endLine; } - get source(): Variable | undefined { + /** + * The source that generated this symbol. + */ + get source(): Identifier | undefined { return this._source; } + /** + * The text (content) of the symbol. + */ get text(): string { return this._text; } + /** + * The start index of the symbol inside the expression. + */ get startIndex(): number { return this._startIndex; } @@ -67,6 +79,9 @@ export class FeelVariable { this._startIndex = value; } + /** + * The start line of the symbol inside the expression. + */ get startLine(): number { return this._startLine; } @@ -74,10 +89,17 @@ export class FeelVariable { get endLine(): number { return this._endLine; } + + /** + * The {@link FeelSyntacticSymbolNature} of the symbol. + */ get feelSymbolNature(): FeelSyntacticSymbolNature { return this._feelSymbolNature; } + /** + * The length of the symbol. + */ get length(): number { return this._length; } diff --git a/packages/dmn-feel-antlr4-parser/src/parser/FeelVariablesParser.ts b/packages/dmn-feel-antlr4-parser/src/parser/FeelIdentifiersParser.ts similarity index 60% rename from packages/dmn-feel-antlr4-parser/src/parser/FeelVariablesParser.ts rename to packages/dmn-feel-antlr4-parser/src/parser/FeelIdentifiersParser.ts index 7fc3ff11997..d295e418ac1 100644 --- a/packages/dmn-feel-antlr4-parser/src/parser/FeelVariablesParser.ts +++ b/packages/dmn-feel-antlr4-parser/src/parser/FeelIdentifiersParser.ts @@ -17,47 +17,34 @@ * under the License. */ -import { VariablesRepository } from "./VariablesRepository"; +import { IdentifiersRepository } from "./IdentifiersRepository"; import { CharStream, CommonTokenStream } from "antlr4"; import FEEL_1_1Parser from "./grammar/generated-parser/FEEL_1_1Parser"; import FEEL_1_1Lexer from "./grammar/generated-parser/FEEL_1_1Lexer"; import { DataType } from "./DataType"; import { Type } from "./grammar/Type"; -import { FeelVariable } from "./FeelVariable"; +import { FeelIdentifiedSymbol } from "./FeelIdentifiedSymbol"; import { MapBackedType } from "./grammar/MapBackedType"; -import { VariableContext } from "./VariableContext"; +import { IdentifierContext } from "./IdentifierContext"; import { ParsedExpression } from "./ParsedExpression"; import { FeelSyntacticSymbolNature } from "./FeelSyntacticSymbolNature"; +import { Expression } from "./Expression"; -export class FeelVariablesParser { - private variablesRepository: VariablesRepository; +export class FeelIdentifiersParser { + private repository: IdentifiersRepository; - constructor(variablesSource: VariablesRepository) { - this.variablesRepository = variablesSource; - this.refreshExpressions(); - } - - public refreshExpressions() { - for (const expression of this.variablesRepository.expressions.values()) { - for (const variable of expression.variables) { - variable.source?.expressions.delete(expression.uuid); - } - const parsedExpression = this.parse(expression.uuid, expression.fullExpression); - expression.variables = parsedExpression.feelVariables; - for (const variable of parsedExpression.feelVariables) { - variable.source?.expressions.set(expression.uuid, expression); - } - } + constructor(identifiersRepository: IdentifiersRepository) { + this.repository = identifiersRepository; } public parse(variableContextUuid: string, expression: string): ParsedExpression { - const variables = new Array(); + const variables = new Array(); const chars = new CharStream(expression); const lexer = new FEEL_1_1Lexer(chars); const feelTokens = new CommonTokenStream(lexer); const parser = new FEEL_1_1Parser(feelTokens); - const variableContext = this.variablesRepository.variables.get(variableContextUuid); + const variableContext = this.repository.identifiers.get(variableContextUuid); if (variableContext) { this.defineVariables(variableContext, parser); } @@ -71,28 +58,28 @@ export class FeelVariablesParser { return { availableSymbols: parser.helper.availableSymbols, - feelVariables: variables, + feelIdentifiedSymbols: variables, }; } - private defineVariables(variableContext: VariableContext, parser: FEEL_1_1Parser) { - this.defineInputVariables(variableContext.inputVariables, parser); + private defineVariables(variableContext: IdentifierContext, parser: FEEL_1_1Parser) { + this.defineInputVariables(variableContext.inputIdentifiers, parser); this.addToParser(parser, variableContext); if (variableContext.parent) { this.defineParentVariable(variableContext.parent, parser); } - for (const inputVariableContext of variableContext.inputVariables) { - const localVariable = this.variablesRepository.variables.get(inputVariableContext); + for (const inputVariableContext of variableContext.inputIdentifiers) { + const localVariable = this.repository.identifiers.get(inputVariableContext); if (localVariable) { this.addToParser(parser, localVariable); } } } - private defineParentVariable(variableNode: VariableContext, parser: FEEL_1_1Parser) { - this.defineInputVariables(variableNode.inputVariables, parser); + private defineParentVariable(variableNode: IdentifierContext, parser: FEEL_1_1Parser) { + this.defineInputVariables(variableNode.inputIdentifiers, parser); this.addToParser(parser, variableNode); if (variableNode.parent) { @@ -102,7 +89,7 @@ export class FeelVariablesParser { private createType(dataType: DataType | string): Type { if (typeof dataType !== "string") { - const type = new MapBackedType(dataType.name, dataType.typeRef ?? dataType.name); + const type = new MapBackedType(dataType.name, dataType.typeRef ?? dataType.name, dataType.source); for (const property of dataType.properties) { const innerType = this.createType(property[1]); @@ -114,31 +101,36 @@ export class FeelVariablesParser { return { name: dataType, typeRef: dataType, + source: { + value: dataType, + feelSyntacticSymbolNature: FeelSyntacticSymbolNature.GlobalVariable, + expressionsThatUseTheIdentifier: new Map(), + }, }; } } private defineInputVariables(inputVariables: Array, parser: FEEL_1_1Parser) { for (const inputVariableId of inputVariables) { - const inputVariable = this.variablesRepository.variables.get(inputVariableId); + const inputVariable = this.repository.identifiers.get(inputVariableId); if (inputVariable) { this.addToParser(parser, inputVariable, true); } } } - private addToParser(parser: FEEL_1_1Parser, context: VariableContext, addInvisibleVariables?: boolean) { + private addToParser(parser: FEEL_1_1Parser, context: IdentifierContext, addInvisibleVariables?: boolean) { if ( - context.variable.value !== "" && + context.identifier.value !== "" && ((!addInvisibleVariables && - context.variable.feelSyntacticSymbolNature != FeelSyntacticSymbolNature.InvisibleVariables) || + context.identifier.feelSyntacticSymbolNature != FeelSyntacticSymbolNature.InvisibleVariables) || addInvisibleVariables) ) { parser.helper.defineVariable( - context.variable.value, - context.variable.typeRef ? this.createType(context.variable.typeRef) : undefined, - context.variable.feelSyntacticSymbolNature, - context.variable, + context.identifier.value, + context.identifier.typeRef ? this.createType(context.identifier.typeRef) : undefined, + context.identifier.feelSyntacticSymbolNature, + context.identifier, context.allowDynamicVariables ); } diff --git a/packages/dmn-feel-antlr4-parser/src/parser/Variable.ts b/packages/dmn-feel-antlr4-parser/src/parser/Identifier.ts similarity index 61% rename from packages/dmn-feel-antlr4-parser/src/parser/Variable.ts rename to packages/dmn-feel-antlr4-parser/src/parser/Identifier.ts index 9ea76356e4c..479c742856b 100644 --- a/packages/dmn-feel-antlr4-parser/src/parser/Variable.ts +++ b/packages/dmn-feel-antlr4-parser/src/parser/Identifier.ts @@ -19,29 +19,39 @@ import { DataType } from "./DataType"; import { FeelSyntacticSymbolNature } from "./FeelSyntacticSymbolNature"; -import { Expression } from "./VariableOccurrence"; +import { Expression } from "./Expression"; /** - * Describe a variable in FEEL. + * Describe an identifier in FEEL. */ -export interface Variable { +export interface Identifier { /** - * The name/value of the variable. + * The name/value of the identifier. */ value: string; /** - * The nature of the variable. + * The nature of the identifier. */ feelSyntacticSymbolNature: FeelSyntacticSymbolNature; /** - * The type of the variable, which can be a custom data type defined by the user, a built-in type or not defined. + * The type of the identifier, which can be a custom data type defined by the user, a built-in type or not defined. */ typeRef?: DataType | string | undefined; /** - * The expressions where this variable is being used. + * The expressions where this identifier is being used. */ - expressions: Map; + expressionsThatUseTheIdentifier: Map; + + /** + * Apply the name/value of the identifier to the source that originates this identifier. + */ + applyValueToSource?(): void; + + /** + * Apply the type of the identifier to the source that originates this identifier. + */ + applyTypeRefToSource?(): void; } diff --git a/packages/dmn-feel-antlr4-parser/src/parser/VariableContext.ts b/packages/dmn-feel-antlr4-parser/src/parser/IdentifierContext.ts similarity index 79% rename from packages/dmn-feel-antlr4-parser/src/parser/VariableContext.ts rename to packages/dmn-feel-antlr4-parser/src/parser/IdentifierContext.ts index bea41c02d29..c828a3b241b 100644 --- a/packages/dmn-feel-antlr4-parser/src/parser/VariableContext.ts +++ b/packages/dmn-feel-antlr4-parser/src/parser/IdentifierContext.ts @@ -17,10 +17,10 @@ * under the License. */ -import { Variable } from "./Variable"; +import { Identifier } from "./Identifier"; /** - * Describe a Variable Context in FEEL. Each defined variable have its own context. + * Describe an Identifier Context in FEEL. Each defined identifier have its own context. * For example, when user creates a new DecisionNode-1, a variable called "DecisionNode-1" is defined * and a new context is created. In this context, we can have: * 1. Inner variables: declared inside the Boxed Expression Editor; @@ -28,7 +28,7 @@ import { Variable } from "./Variable"; * 3. The variable: the variable context may declare a Variable that is valid for this context and for its * children, for example, a row in a Context Expression inside a Decision Node. */ -export interface VariableContext { +export interface IdentifierContext { /** * The unique UUID for the variable context. */ @@ -38,22 +38,22 @@ export interface VariableContext { * A Variable Context can be child of another context (i.e. the first entry inside a Context Expression defined in * Boxed Expression Editor is child of the parent node). */ - parent?: VariableContext; + parent?: IdentifierContext; /** - * The defined variable. + * The identifier (variable) declared by this context. */ - variable: Variable; + identifier: Identifier; /** * Children contexts indexed by its unique uuid. */ - children: Map; + children: Map; /** - * Input nodes that define variables. + * Input identifiers to this context. They are external known identifiers in this context. */ - inputVariables: Array; + inputIdentifiers: Array; /** * Dynamic variables are variables only validated during runtime. diff --git a/packages/dmn-feel-antlr4-parser/src/parser/IdentifiersRepository.ts b/packages/dmn-feel-antlr4-parser/src/parser/IdentifiersRepository.ts new file mode 100644 index 00000000000..62975f44e48 --- /dev/null +++ b/packages/dmn-feel-antlr4-parser/src/parser/IdentifiersRepository.ts @@ -0,0 +1,851 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { DataType } from "./DataType"; +import { FeelSyntacticSymbolNature } from "./FeelSyntacticSymbolNature"; +import { IdentifierContext } from "./IdentifierContext"; +import { + DMN15__tBusinessKnowledgeModel, + DMN15__tConditional, + DMN15__tContext, + DMN15__tContextEntry, + DMN15__tDecision, + DMN15__tDecisionService, + DMN15__tDecisionTable, + DMN15__tDefinitions, + DMN15__tFilter, + DMN15__tFor, + DMN15__tFunctionDefinition, + DMN15__tInformationRequirement, + DMN15__tInputData, + DMN15__tInvocation, + DMN15__tItemDefinition, + DMN15__tKnowledgeRequirement, + DMN15__tList, + DMN15__tLiteralExpression, + DMN15__tQuantified, + DMN15__tRelation, +} from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types"; +import { Expression } from "./Expression"; +import { DmnLatestModel } from "@kie-tools/dmn-marshaller"; +import { BuiltInTypes } from "./BuiltInTypes"; + +export type DmnLiteralExpression = { __$$element: "literalExpression" } & DMN15__tLiteralExpression; +export type DmnInvocation = { __$$element: "invocation" } & DMN15__tInvocation; +export type DmnDecisionTable = { __$$element: "decisionTable" } & DMN15__tDecisionTable; +export type DmnContext = { __$$element: "context" } & DMN15__tContext; +export type DmnFunctionDefinition = { __$$element: "functionDefinition" } & DMN15__tFunctionDefinition; +export type DmnRelation = { __$$element: "relation" } & DMN15__tRelation; +export type DmnList = { __$$element: "list" } & DMN15__tList; +export type DmnConditional = { __$$element: "conditional" } & DMN15__tConditional; +export type DmnFilter = { __$$element: "filter" } & DMN15__tFilter; +export type DmnFor = { __$$element: "for" } & DMN15__tFor; +export type DmnEvery = { __$$element: "every" } & DMN15__tQuantified; +export type DmnSome = { __$$element: "some" } & DMN15__tQuantified; +export type DmnDecisionNode = { __$$element: "decision" } & DMN15__tDecision; + +export type DmnDefinitions = DMN15__tDefinitions; +export type DmnKnowledgeRequirement = DMN15__tKnowledgeRequirement; +export type DmnContextEntry = DMN15__tContextEntry; + +type DmnBusinessKnowledgeModel = DMN15__tBusinessKnowledgeModel; +type DmnItemDefinition = DMN15__tItemDefinition; +type DmnInputData = DMN15__tInputData; +type DmnInformationRequirement = DMN15__tInformationRequirement; +type DmnDecisionService = DMN15__tDecisionService; + +export class IdentifiersRepository { + private readonly _identifiersContextIndexedByUuid: Map; + private readonly _expressionsIndexedByUuid: Map; + private readonly _dataTypes: Map; + private readonly _dataTypeIndexedByUuid: Map; + private readonly _importedIdentifiers: Map>; + private readonly _importedDataTypes: Map>; + private currentIdentifierNamePrefix: string; + private currentUuidPrefix: string; + + constructor( + private dmnDefinitions: DmnDefinitions, + private externalDefinitions?: Map + ) { + this._dataTypes = new Map([ + [BuiltInTypes.Number.name, BuiltInTypes.Number], + [BuiltInTypes.Boolean.name, BuiltInTypes.Boolean], + [BuiltInTypes.String.name, BuiltInTypes.String], + [BuiltInTypes.DaysAndTimeDuration.name, BuiltInTypes.DaysAndTimeDuration], + [BuiltInTypes.DateAndTime.name, BuiltInTypes.DateAndTime], + [BuiltInTypes.YearsAndMonthsDuration.name, BuiltInTypes.YearsAndMonthsDuration], + [BuiltInTypes.Time.name, BuiltInTypes.Time], + [BuiltInTypes.Date.name, BuiltInTypes.Date], + ]); + + this._identifiersContextIndexedByUuid = new Map(); + this._expressionsIndexedByUuid = new Map(); + this._dataTypeIndexedByUuid = new Map(); + this._importedIdentifiers = new Map>(); + this._importedDataTypes = new Map>(); + this.loadImportedIdentifiers(dmnDefinitions, externalDefinitions); + + this.currentIdentifierNamePrefix = ""; + this.currentUuidPrefix = ""; + + this.loadIdentifiers(dmnDefinitions); + } + + get identifiersContextIndexedByUuid(): Map { + return this._identifiersContextIndexedByUuid; + } + + get expressionsIndexedByUuid(): Map { + return this._expressionsIndexedByUuid; + } + + get dataTypeIndexedByUuid(): Map { + return this._dataTypeIndexedByUuid; + } + + get importedDataTypes(): Map> { + return this._importedDataTypes; + } + + get importedIdentifiers(): Map> { + return this._importedIdentifiers; + } + + get identifiers(): Map { + return this._identifiersContextIndexedByUuid; + } + + get dataTypes(): Map { + return this._dataTypes; + } + + get expressions(): Map { + return this._expressionsIndexedByUuid; + } + + public reload() { + this._identifiersContextIndexedByUuid.clear(); + this._expressionsIndexedByUuid.clear(); + this._dataTypeIndexedByUuid.clear(); + this._importedIdentifiers.clear(); + this._importedDataTypes.clear(); + this.loadImportedIdentifiers(this.dmnDefinitions, this.externalDefinitions); + + this.currentIdentifierNamePrefix = ""; + this.currentUuidPrefix = ""; + + this.loadIdentifiers(this.dmnDefinitions); + } + + private createDataTypes(definitions: DmnDefinitions) { + definitions.itemDefinition?.forEach((itemDefinition) => { + const dataType = this.createDataType(itemDefinition); + + this._dataTypeIndexedByUuid.set(dataType.uuid, dataType); + this.addImportedDataType(dataType); + + itemDefinition.itemComponent?.forEach((itemComponent) => { + const innerType = this.createInnerType(itemComponent); + this._dataTypeIndexedByUuid.set(innerType.uuid, innerType); + this.addImportedDataType(innerType); + dataType.properties.set(innerType.name, innerType); + }); + + this.dataTypes.set(dataType.name, dataType); + }); + } + + private addImportedDataType(dataType: DataType) { + if (this.currentIdentifierNamePrefix.length != 0) { + if (!this._importedDataTypes.has(this.currentIdentifierNamePrefix)) { + this._importedDataTypes.set(this.currentIdentifierNamePrefix, []); + } + this._importedDataTypes.get(this.currentIdentifierNamePrefix)?.push(dataType); + } + } + + private loadIdentifiersFromDefinitions(definitions: DmnDefinitions) { + definitions.drgElement?.forEach((drg) => { + switch (drg.__$$element) { + case "decision": + this.loadIdentifiersFromDecision(drg); + break; + + case "inputData": + this.loadIdentifiersFromInputData(drg); + break; + + case "businessKnowledgeModel": + this.loadIdentifiersFromBkm(drg); + break; + + case "decisionService": + this.loadIdentifiersFromDecisionService(drg); + break; + + default: + // Do nothing because it is an element that does not declare variables + break; + } + }); + } + + private loadIdentifiersFromInputData(drg: DmnInputData) { + this.addIdentifier({ + uuid: drg["@_id"] ?? "", + name: drg["@_name"], + kind: FeelSyntacticSymbolNature.GlobalVariable, + parentContext: undefined, + typeRef: drg.variable?.["@_typeRef"], + applyValueToSource: (value) => { + drg["@_name"] = value; + }, + applyTypeRefToSource: (value) => { + if (drg.variable) { + if (typeof value === "string") { + drg.variable["@_typeRef"] = value; + } else { + drg.variable["@_typeRef"] = value?.typeRef; + } + } + }, + }); + } + + private loadIdentifiersFromDecisionService(drg: DmnDecisionService) { + this.addIdentifier({ + uuid: drg["@_id"] ?? "", + name: drg["@_name"], + kind: FeelSyntacticSymbolNature.Invocable, + parentContext: undefined, + typeRef: drg.variable?.["@_typeRef"], + applyValueToSource: (value) => { + drg["@_name"] = value; + }, + applyTypeRefToSource: (value) => { + if (drg.variable) { + if (typeof value === "string") { + drg.variable["@_typeRef"] = value; + } else { + drg.variable["@_typeRef"] = value?.typeRef; + } + } + }, + }); + } + + private loadIdentifiersFromBkm(drg: DmnBusinessKnowledgeModel) { + const parent = this.addIdentifier({ + uuid: drg["@_id"] ?? "", + name: drg["@_name"], + kind: FeelSyntacticSymbolNature.Invocable, + parentContext: undefined, + typeRef: drg.variable?.["@_typeRef"], + applyValueToSource: (value) => { + drg["@_name"] = value; + }, + applyTypeRefToSource: (value) => { + if (drg.variable) { + if (typeof value === "string") { + drg.variable["@_typeRef"] = value; + } else { + drg.variable["@_typeRef"] = value?.typeRef; + } + } + }, + }); + + if (drg.encapsulatedLogic) { + let parentElement = parent; + + if (drg.encapsulatedLogic.formalParameter) { + for (const parameter of drg.encapsulatedLogic.formalParameter) { + parentElement = this.addIdentifier({ + uuid: parameter["@_id"] ?? "", + name: parameter["@_name"] ?? "", + kind: FeelSyntacticSymbolNature.Parameter, + parentContext: parentElement, + applyValueToSource: (value) => { + parameter["@_name"] = value; + }, + }); + } + } + + if (drg.encapsulatedLogic.expression) { + this.addInnerExpression(parentElement, drg.encapsulatedLogic.expression); + } + } + } + + private loadIdentifiersFromDecision(drg: DmnDecisionNode) { + const parent: IdentifierContext = this.addIdentifier({ + uuid: drg["@_id"] ?? "", + name: drg["@_name"], + kind: FeelSyntacticSymbolNature.InvisibleVariables, + parentContext: undefined, + typeRef: drg.variable?.["@_typeRef"], + applyValueToSource: (value) => { + drg["@_name"] = value; + }, + applyTypeRefToSource: (value) => { + if (drg.variable) { + if (typeof value === "string") { + drg.variable["@_typeRef"] = value; + } else { + drg.variable["@_typeRef"] = value?.typeRef; + } + } + }, + }); + + if (drg.informationRequirement) { + for (const requirement of drg.informationRequirement) { + this.addInputVariable(parent, requirement); + } + } + + if (drg.knowledgeRequirement) { + for (const knowledgeRequirement of drg.knowledgeRequirement) { + this.addInputVariableFromKnowledge(parent, knowledgeRequirement); + } + } + + if (drg.expression) { + this.addInnerExpression(parent, drg.expression); + } + } + + private addIdentifier(args: { + uuid: string; + name: string; + kind: FeelSyntacticSymbolNature; + parentContext?: IdentifierContext; + typeRef?: string; + allowDynamicVariables?: boolean; + applyValueToSource?: (value: string) => void; + applyTypeRefToSource?: (value: DataType | string | undefined) => void; + }) { + const variableContext = this.createIdentifierContext( + this.buildIdentifierUuid(args.uuid), + this.buildName(args.name), + args.kind, + args.parentContext, + args.typeRef, + args.allowDynamicVariables, + args.applyValueToSource, + args.applyTypeRefToSource + ); + + if (this.currentIdentifierNamePrefix.length != 0) { + if (!this._importedIdentifiers.has(this.currentIdentifierNamePrefix)) { + this._importedIdentifiers.set(this.currentIdentifierNamePrefix, []); + } + this._importedIdentifiers.get(this.currentIdentifierNamePrefix)?.push(variableContext); + } + + this._identifiersContextIndexedByUuid.set(this.buildIdentifierUuid(args.uuid), variableContext); + + return variableContext; + } + + private createIdentifierContext( + uuid: string, + name: string, + variableType: FeelSyntacticSymbolNature, + parent: IdentifierContext | undefined, + typeRef: string | undefined, + allowDynamicVariables: boolean | undefined, + applyValueToSource?: (value: string) => void, + applyTypeRefToSource?: (value: DataType | string | undefined) => void + ): IdentifierContext { + return { + uuid: uuid, + children: new Map(), + parent: parent, + inputIdentifiers: new Array(), + allowDynamicVariables: allowDynamicVariables, + identifier: { + value: name, + feelSyntacticSymbolNature: variableType, + typeRef: this.getTypeRef(typeRef), + expressionsThatUseTheIdentifier: new Map(), + applyValueToSource() { + if (applyValueToSource) { + applyValueToSource(this.value); + } + }, + applyTypeRefToSource() { + if (applyTypeRefToSource) { + applyTypeRefToSource(this.typeRef); + } + }, + }, + }; + } + + public getTypeRef(typeRef: string | undefined) { + return this.dataTypes.has(typeRef ?? "") ? this.dataTypes.get(typeRef ?? "") : typeRef; + } + + private createDataType(itemDefinition: DmnItemDefinition) { + const name = this.buildName(itemDefinition["@_name"]); + const dataType: DataType = { + uuid: itemDefinition["@_id"] ?? "datatype_uuid", + name: name, + properties: new Map(), + typeRef: itemDefinition["typeRef"]?.__$$text ?? itemDefinition["@_name"], + source: { + value: name, + feelSyntacticSymbolNature: FeelSyntacticSymbolNature.GlobalVariable, + expressionsThatUseTheIdentifier: new Map(), + }, + }; + return dataType; + } + + private createInnerType(itemComponent: DmnItemDefinition) { + const dataType: DataType = { + uuid: itemComponent["@_id"] ?? "item_uuid", + name: itemComponent["@_name"], + properties: this.buildProperties(itemComponent), + typeRef: itemComponent["typeRef"]?.__$$text ?? itemComponent["@_name"], + source: { + value: itemComponent["@_name"], + feelSyntacticSymbolNature: FeelSyntacticSymbolNature.GlobalVariable, + expressionsThatUseTheIdentifier: new Map(), + }, + }; + return dataType; + } + + private buildProperties(itemComponent: DmnItemDefinition): Map { + const properties = new Map(); + + itemComponent.itemComponent?.forEach((def) => { + const property: DataType = { + uuid: def["@_id"] ?? "root_property", + name: def["@_name"], + properties: this.buildProperties(def), + typeRef: def["typeRef"]?.__$$text ?? def["@_name"], + source: { + value: def["@_name"], + feelSyntacticSymbolNature: FeelSyntacticSymbolNature.GlobalVariable, + expressionsThatUseTheIdentifier: new Map(), + }, + }; + + this._dataTypeIndexedByUuid.set(property.uuid, property); + properties.set(property.name, property); + }); + + return properties; + } + + private addLiteralExpression(parent: IdentifierContext, element: DmnLiteralExpression) { + const id = element["@_id"] ?? ""; + const expression = new Expression(id, element); + this._expressionsIndexedByUuid.set(id, expression); + this.addIdentifier({ + uuid: id, + name: "", + kind: FeelSyntacticSymbolNature.LocalVariable, + parentContext: parent, + }); + } + + private addInvocation(parent: IdentifierContext, element: DmnInvocation) { + if (element.binding) { + for (const bindingElement of element.binding) { + if (bindingElement.expression) { + this.addInnerExpression(parent, bindingElement.expression); + } + } + } + } + + private addContext(parent: IdentifierContext, element: DmnContext) { + let parentNode = parent; + if (element.contextEntry) { + for (const innerEntry of element.contextEntry) { + parentNode = this.addContextEntry(parentNode, innerEntry); + } + } + } + + private addContextEntry(parentNode: IdentifierContext, contextEntry: DmnContextEntry) { + const variableNode = this.addIdentifier({ + uuid: contextEntry.variable?.["@_id"] ?? "", + name: contextEntry.variable?.["@_name"] ?? "", + kind: FeelSyntacticSymbolNature.LocalVariable, + parentContext: parentNode, + typeRef: contextEntry.variable?.["@_typeRef"], + applyValueToSource: (value) => { + if (contextEntry.variable) { + contextEntry.variable["@_name"] = value; + } + }, + applyTypeRefToSource: (value) => { + if (contextEntry.variable) { + if (typeof value === "string") { + contextEntry.variable["@_typeRef"] = value; + } else { + contextEntry.variable["@_typeRef"] = value?.typeRef; + } + } + }, + }); + + parentNode.children.set(variableNode.uuid, variableNode); + + if (contextEntry.expression) { + if (contextEntry.expression.__$$element) { + // The parent is always the previous node to prevent recursive calls. + // Consider this example: + // + // [ROOT] Context Expression + // [X] Client DTI | [A] + // [Y] Some Other | [B] + // [Z] And Another | [C] + // + // Inside the B we can not call "Some Other" for instance, but we can call "Client DTI" + // + // So the structure for that case should be: + // [ROOT] + // / \ + // [X] [A] + // / \ + // [Y] [B] + // / \ + // [Z] [C] + // + // So in that case, inside "C" we recognize Y, X and ROOT + // Inside B, X and ROOT + // + // By "ROOT" we understand the root expression which for example + // can be the Decision Node itself and its input nodes. + this.addInnerExpression(parentNode, contextEntry.expression); + } + } + + return variableNode; + } + + private addFunctionDefinition(parent: IdentifierContext, element: DmnFunctionDefinition) { + let parentElement = parent; + + if (element.formalParameter) { + for (const parameter of element.formalParameter) { + parentElement = this.addIdentifier({ + uuid: parameter["@_id"] ?? "", + name: parameter["@_name"] ?? "", + kind: FeelSyntacticSymbolNature.Parameter, + parentContext: parentElement, + applyValueToSource: (value) => { + parameter["@_name"] = value; + }, + }); + } + } + + if (element.expression) { + this.addInnerExpression(parentElement, element.expression); + } + } + + private addRelation(parent: IdentifierContext, element: DmnRelation) { + if (element.row) { + for (const rowElement of element.row) { + if (rowElement.expression) { + for (const expression of rowElement.expression) { + this.addInnerExpression(parent, expression); + } + } + } + } + } + + private addList(parent: IdentifierContext, element: DmnList) { + if (element.expression) { + for (const expression of element.expression) { + if (expression) { + this.addInnerExpression(parent, expression); + } + } + } + } + + private addConditional(parent: IdentifierContext, element: DmnConditional) { + if (element.if?.expression) { + this.addInnerExpression(parent, element.if.expression); + } + if (element.then?.expression) { + this.addInnerExpression(parent, element.then.expression); + } + if (element.else?.expression) { + this.addInnerExpression(parent, element.else.expression); + } + } + + private addIterable(parent: IdentifierContext, expression: DmnSome | DmnEvery) { + const localParent = this.addIteratorVariable(parent, expression); + + if (expression.in.expression) { + this.addInnerExpression(localParent, expression.in.expression); + } + if (expression.satisfies.expression) { + this.addInnerExpression(localParent, expression.satisfies.expression); + } + } + + private addFor(parent: IdentifierContext, expression: DmnFor) { + const localParent = this.addIteratorVariable(parent, expression); + + if (expression.return.expression) { + this.addInnerExpression(localParent, expression.return.expression); + } + if (expression.in.expression) { + this.addInnerExpression(localParent, expression.in.expression); + } + } + + private addFilterVariable(parent: IdentifierContext, expression: DmnFilter) { + let type = undefined; + + // We're assuming that the 'in' expression is with the correct type (a list of @_typeRef). + // If it is not the expression will fail anyway. + if (expression.in.expression) { + type = expression.in.expression["@_typeRef"]; + } + return this.addIdentifier({ + uuid: expression["@_id"] ?? "", + name: "item", + kind: FeelSyntacticSymbolNature.LocalVariable, + parentContext: parent, + typeRef: type, + allowDynamicVariables: true, + }); + } + + private addIteratorVariable(parent: IdentifierContext, expression: DmnFor | DmnEvery | DmnSome) { + let localParent = parent; + if (expression["@_iteratorVariable"]) { + let type = undefined; + + // We're assuming that the 'in' expression is with the correct type (a list of @_typeRef). + // If it is not the expression will fail anyway. + if (expression.in.expression) { + type = expression.in.expression["@_typeRef"]; + } + localParent = this.addIdentifier({ + uuid: expression["@_id"] ?? "", + name: expression["@_iteratorVariable"], + kind: FeelSyntacticSymbolNature.LocalVariable, + parentContext: parent, + typeRef: type, + allowDynamicVariables: true, + applyValueToSource: (value) => { + expression["@_iteratorVariable"] = value; + }, + applyTypeRefToSource: (value) => { + if (expression.in.expression) { + if (typeof value === "string") { + expression.in.expression["@_typeRef"] = value; + } else { + expression.in.expression["@_typeRef"] = value?.typeRef; + } + } + }, + }); + } + return localParent; + } + + private addFilter(parent: IdentifierContext, expression: DmnFilter) { + if (expression.in.expression) { + this.addInnerExpression(parent, expression.in.expression); + } + + const localParent = this.addFilterVariable(parent, expression); + if (expression.match.expression) { + this.addInnerExpression(localParent, expression.match.expression); + } + } + + private addInnerExpression( + parent: IdentifierContext, + expression: + | DmnLiteralExpression + | DmnInvocation + | DmnDecisionTable + | DmnContext + | DmnFunctionDefinition + | DmnRelation + | DmnList + | DmnFor + | DmnFilter + | DmnEvery + | DmnSome + | DmnConditional + ) { + switch (expression.__$$element) { + case "literalExpression": + this.addLiteralExpression(parent, expression); + break; + + case "invocation": + this.addInvocation(parent, expression); + break; + + case "decisionTable": + // It doesn't define variables, but we need it to create its own context to use variables inside of Decision Table. + this.addDecisionTable(parent, expression); + break; + + case "context": + this.addContext(parent, expression); + break; + + case "functionDefinition": + this.addFunctionDefinition(parent, expression); + break; + + case "relation": + this.addRelation(parent, expression); + break; + + case "list": + this.addList(parent, expression); + break; + + case "conditional": + this.addConditional(parent, expression); + break; + + case "every": + case "some": + this.addIterable(parent, expression); + break; + + case "for": + this.addFor(parent, expression); + break; + + case "filter": + this.addFilter(parent, expression); + break; + + default: + // throw new Error("Unknown or not supported type for expression."); + } + } + + private addDecisionTableEntryNode(parent: IdentifierContext, entryId: string) { + const ruleInputElementNode = this.addIdentifier({ + uuid: entryId, + name: "", + kind: FeelSyntacticSymbolNature.LocalVariable, + parentContext: parent, + }); + parent.children.set(ruleInputElementNode.uuid, ruleInputElementNode); + this.addIdentifier({ + uuid: ruleInputElementNode.uuid, + name: "", + kind: FeelSyntacticSymbolNature.LocalVariable, + parentContext: ruleInputElementNode, + }); + } + + private addDecisionTable(parent: IdentifierContext, decisionTable: DmnDecisionTable) { + const variableNode = this.addIdentifier({ + uuid: decisionTable["@_id"] ?? "", + name: "", + kind: FeelSyntacticSymbolNature.LocalVariable, + parentContext: parent, + }); + parent.children.set(variableNode.uuid, variableNode); + if (decisionTable.rule) { + for (const ruleElement of decisionTable.rule) { + ruleElement.inputEntry?.forEach((ruleInputElement) => + this.addDecisionTableEntryNode(parent, ruleInputElement["@_id"] ?? "") + ); + ruleElement.outputEntry?.forEach((ruleOutputElement) => + this.addDecisionTableEntryNode(parent, ruleOutputElement["@_id"] ?? "") + ); + } + } + this.addIdentifier({ + uuid: variableNode.uuid, + name: "", + kind: FeelSyntacticSymbolNature.LocalVariable, + parentContext: parent, + }); + } + + private addInputVariable(parent: IdentifierContext, requirement: DmnInformationRequirement) { + if (requirement.requiredDecision) { + parent.inputIdentifiers.push(requirement.requiredDecision["@_href"]?.replace("#", "")); + } else if (requirement.requiredInput) { + parent.inputIdentifiers.push(requirement.requiredInput["@_href"]?.replace("#", "")); + } + } + + private addInputVariableFromKnowledge(parent: IdentifierContext, knowledgeRequirement: DmnKnowledgeRequirement) { + if (knowledgeRequirement.requiredKnowledge) { + parent.inputIdentifiers.push(knowledgeRequirement.requiredKnowledge["@_href"]?.replace("#", "")); + } + } + + private buildIdentifierUuid(uuid: string) { + if (this.currentUuidPrefix.length != 0) { + return this.currentUuidPrefix + uuid; + } + + return uuid; + } + + private buildName(name: string) { + if (this.currentIdentifierNamePrefix.length != 0) { + return `${this.currentIdentifierNamePrefix}.${name}`; + } + + return name; + } + + private loadIdentifiers(dmnDefinitions: DmnDefinitions) { + this.createDataTypes(dmnDefinitions); + this.loadIdentifiersFromDefinitions(dmnDefinitions); + } + + private loadImportedIdentifiers(dmnDefinitions: DmnDefinitions, externalDefinitions?: Map) { + if (dmnDefinitions.import && externalDefinitions) { + for (const dmnImport of dmnDefinitions.import) { + if (externalDefinitions.has(dmnImport["@_namespace"])) { + this.currentIdentifierNamePrefix = dmnImport["@_name"]; + this.currentUuidPrefix = dmnImport["@_namespace"]; + const externalDef = externalDefinitions.get(dmnImport["@_namespace"]); + if (externalDef) { + this.loadIdentifiers(externalDef.definitions); + } + } + } + } + } +} diff --git a/packages/dmn-feel-antlr4-parser/src/parser/ParsedExpression.ts b/packages/dmn-feel-antlr4-parser/src/parser/ParsedExpression.ts index f4d8d92d559..a01044474b2 100644 --- a/packages/dmn-feel-antlr4-parser/src/parser/ParsedExpression.ts +++ b/packages/dmn-feel-antlr4-parser/src/parser/ParsedExpression.ts @@ -17,10 +17,10 @@ * under the License. */ -import { FeelVariable } from "./FeelVariable"; +import { FeelIdentifiedSymbol } from "./FeelIdentifiedSymbol"; import { FeelSymbol } from "./FeelSymbol"; export interface ParsedExpression { availableSymbols: Array; - feelVariables: Array; + feelIdentifiedSymbols: Array; } diff --git a/packages/dmn-feel-antlr4-parser/src/parser/VariablesRepository.ts b/packages/dmn-feel-antlr4-parser/src/parser/VariablesRepository.ts deleted file mode 100644 index a923a0186f7..00000000000 --- a/packages/dmn-feel-antlr4-parser/src/parser/VariablesRepository.ts +++ /dev/null @@ -1,717 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { DataType } from "./DataType"; -import { FeelSyntacticSymbolNature } from "./FeelSyntacticSymbolNature"; -import { VariableContext } from "./VariableContext"; -import { - DMN15__tBusinessKnowledgeModel, - DMN15__tConditional, - DMN15__tContext, - DMN15__tContextEntry, - DMN15__tDecision, - DMN15__tDecisionService, - DMN15__tDecisionTable, - DMN15__tDefinitions, - DMN15__tFilter, - DMN15__tFor, - DMN15__tFunctionDefinition, - DMN15__tInformationRequirement, - DMN15__tInputData, - DMN15__tInvocation, - DMN15__tItemDefinition, - DMN15__tKnowledgeRequirement, - DMN15__tList, - DMN15__tLiteralExpression, - DMN15__tQuantified, - DMN15__tRelation, -} from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types"; -import { Expression } from "./VariableOccurrence"; -import { DmnLatestModel } from "@kie-tools/dmn-marshaller"; -import { BuiltInTypes } from "./BuiltInTypes"; - -type DmnLiteralExpression = { __$$element: "literalExpression" } & DMN15__tLiteralExpression; -type DmnInvocation = { __$$element: "invocation" } & DMN15__tInvocation; -type DmnDecisionTable = { __$$element: "decisionTable" } & DMN15__tDecisionTable; -type DmnContext = { __$$element: "context" } & DMN15__tContext; -type DmnFunctionDefinition = { __$$element: "functionDefinition" } & DMN15__tFunctionDefinition; -type DmnRelation = { __$$element: "relation" } & DMN15__tRelation; -type DmnList = { __$$element: "list" } & DMN15__tList; -type DmnKnowledgeRequirement = DMN15__tKnowledgeRequirement; -type DmnConditional = { __$$element: "conditional" } & DMN15__tConditional; -type DmnFilter = { __$$element: "filter" } & DMN15__tFilter; -type DmnFor = { __$$element: "for" } & DMN15__tFor; -type DmnEvery = { __$$element: "every" } & DMN15__tQuantified; -type DmnSome = { __$$element: "some" } & DMN15__tQuantified; -type DmnDecisionNode = { __$$element: "decision" } & DMN15__tDecision; -type DmnBusinessKnowledgeModel = DMN15__tBusinessKnowledgeModel; -export type DmnDefinitions = DMN15__tDefinitions; -type DmnItemDefinition = DMN15__tItemDefinition; -type DmnContextEntry = DMN15__tContextEntry; -type DmnInputData = DMN15__tInputData; -type DmnInformationRequirement = DMN15__tInformationRequirement; -type DmnDecisionService = DMN15__tDecisionService; - -export class VariablesRepository { - private readonly variablesIndexedByUuid: Map; - private readonly expressionsIndexedByUuid: Map; - private readonly dataTypes: Map; - private currentVariablePrefix: string; - private currentUuidPrefix: string; - - constructor(dmnDefinitions: DmnDefinitions, externalDefinitions: Map) { - this.dataTypes = new Map([ - [BuiltInTypes.Number.name, BuiltInTypes.Number], - [BuiltInTypes.Boolean.name, BuiltInTypes.Boolean], - [BuiltInTypes.String.name, BuiltInTypes.String], - [BuiltInTypes.DaysAndTimeDuration.name, BuiltInTypes.DaysAndTimeDuration], - [BuiltInTypes.DateAndTime.name, BuiltInTypes.DateAndTime], - [BuiltInTypes.YearsAndMonthsDuration.name, BuiltInTypes.YearsAndMonthsDuration], - [BuiltInTypes.Time.name, BuiltInTypes.Time], - [BuiltInTypes.Date.name, BuiltInTypes.Date], - ]); - - this.variablesIndexedByUuid = new Map(); - this.expressionsIndexedByUuid = new Map(); - this.loadImportedVariables(dmnDefinitions, externalDefinitions); - - this.currentVariablePrefix = ""; - this.currentUuidPrefix = ""; - - this.loadVariables(dmnDefinitions); - } - - get variables(): Map { - return this.variablesIndexedByUuid; - } - - public updateVariableType(variableUuid: string, newType: string) { - const variableContext = this.variablesIndexedByUuid.get(variableUuid); - if (variableContext) { - variableContext.variable.typeRef = this.getTypeRef(newType); - } - } - - get expressions(): Map { - return this.expressionsIndexedByUuid; - } - - public renameVariable(variableUuid: string, newName: string) { - const variableContext = this.variablesIndexedByUuid.get(variableUuid); - if (variableContext) { - for (const expression of variableContext.variable.expressions.values()) { - expression.renameVariable(variableContext.variable, newName); - } - - variableContext.variable.value = newName; - } - } - - public addVariableToContext(variableUuid: string, variableName: string, parentUuid: string, childUuid?: string) { - const parentContext = this.variablesIndexedByUuid.get(parentUuid); - if (parentContext) { - const newVariable = { - value: variableName, - feelSyntacticSymbolNature: FeelSyntacticSymbolNature.GlobalVariable, - typeRef: undefined, - expressions: new Map(), - }; - - const newContext: VariableContext = { - uuid: variableUuid, - parent: parentContext, - variable: newVariable, - children: new Map(), - inputVariables: new Array(), - }; - - this.variablesIndexedByUuid.set(newContext.uuid, newContext); - - parentContext.children.set(variableUuid, newContext); - - if (childUuid) { - const childContext = this.variablesIndexedByUuid.get(childUuid); - if (childContext) { - parentContext.children.delete(childUuid); - childContext.parent = newContext; - } - } - } - } - - public removeVariable(variableUuid: string, removeChildren?: boolean) { - const variable = this.variablesIndexedByUuid.get(variableUuid); - if (variable) { - const newChildParent = variable.parent; - if (!removeChildren) { - if (newChildParent) { - newChildParent.children.delete(variableUuid); - for (const child of variable.children.values()) { - child.parent = newChildParent; - newChildParent.children.set(child.uuid, child); - } - } - } else { - variable.parent?.children.delete(variableUuid); - for (const child of variable.children.keys()) { - this.removeVariable(child, true); - } - } - this.variablesIndexedByUuid.delete(variableUuid); - } - } - - private createDataTypes(definitions: DmnDefinitions) { - definitions.itemDefinition?.forEach((itemDefinition) => { - const dataType = this.createDataType(itemDefinition); - - itemDefinition.itemComponent?.forEach((itemComponent) => { - const innerType = this.createInnerType(itemComponent); - dataType.properties.set(innerType.name, innerType); - }); - - this.dataTypes.set(dataType.name, dataType); - }); - } - - private createVariables(definitions: DmnDefinitions) { - definitions.drgElement?.forEach((drg) => { - switch (drg.__$$element) { - case "decision": - this.createVariablesFromDecision(drg); - break; - - case "inputData": - this.createVariablesFromInputData(drg); - break; - - case "businessKnowledgeModel": - this.createVariablesFromBkm(drg); - break; - - case "decisionService": - this.createVariablesFromDecisionService(drg); - break; - - default: - // Do nothing because it is an element that does not declare variables - break; - } - }); - } - - private createVariablesFromInputData(drg: DmnInputData) { - this.addVariable( - drg["@_id"] ?? "", - drg["@_name"], - FeelSyntacticSymbolNature.GlobalVariable, - undefined, - drg.variable?.["@_typeRef"] - ); - } - - private createVariablesFromDecisionService(drg: DmnDecisionService) { - this.addVariable( - drg["@_id"] ?? "", - drg["@_name"], - FeelSyntacticSymbolNature.Invocable, - undefined, - drg.variable?.["@_typeRef"] - ); - } - - private createVariablesFromBkm(drg: DmnBusinessKnowledgeModel) { - const parent = this.addVariable( - drg["@_id"] ?? "", - drg["@_name"], - FeelSyntacticSymbolNature.Invocable, - undefined, - drg.variable?.["@_typeRef"] - ); - - if (drg.encapsulatedLogic) { - let parentElement = parent; - - if (drg.encapsulatedLogic.formalParameter) { - for (const parameter of drg.encapsulatedLogic.formalParameter) { - parentElement = this.addVariable( - parameter["@_id"] ?? "", - parameter["@_name"] ?? "", - FeelSyntacticSymbolNature.Parameter, - parentElement - ); - } - } - - if (drg.encapsulatedLogic.expression) { - this.addInnerExpression(parentElement, drg.encapsulatedLogic.expression); - } - } - } - - private createVariablesFromDecision(drg: DmnDecisionNode) { - const parent = this.addVariable( - drg["@_id"] ?? "", - drg["@_name"], - FeelSyntacticSymbolNature.InvisibleVariables, - undefined, - drg.variable?.["@_typeRef"] - ); - - if (drg.informationRequirement) { - for (const requirement of drg.informationRequirement) { - this.addInputVariable(parent, requirement); - } - } - - if (drg.knowledgeRequirement) { - for (const knowledgeRequirement of drg.knowledgeRequirement) { - this.addInputVariableFromKnowledge(parent, knowledgeRequirement); - } - } - - if (drg.expression) { - this.addInnerExpression(parent, drg.expression); - } - } - - private addVariable( - uuid: string, - name: string, - variableType: FeelSyntacticSymbolNature, - parent?: VariableContext, - typeRef?: string, - allowDynamicVariables?: boolean - ) { - const node = this.createVariableNode( - this.buildVariableUuid(uuid), - this.buildName(name), - variableType, - parent, - typeRef, - allowDynamicVariables - ); - - this.variablesIndexedByUuid.set(this.buildVariableUuid(uuid), node); - - return node; - } - - private createVariableNode( - uuid: string, - name: string, - variableType: FeelSyntacticSymbolNature, - parent: VariableContext | undefined, - typeRef: string | undefined, - allowDynamicVariables: boolean | undefined - ): VariableContext { - return { - uuid: uuid, - children: new Map(), - parent: parent, - inputVariables: new Array(), - allowDynamicVariables: allowDynamicVariables, - variable: { - value: name, - feelSyntacticSymbolNature: variableType, - typeRef: this.getTypeRef(typeRef), - expressions: new Map(), - }, - }; - } - - private getTypeRef(typeRef: string | undefined) { - return this.dataTypes.has(typeRef ?? "") ? this.dataTypes.get(typeRef ?? "") : typeRef; - } - - private createDataType(itemDefinition: DmnItemDefinition) { - return { - name: this.buildName(itemDefinition["@_name"]), - properties: new Map(), - typeRef: itemDefinition["typeRef"]?.__$$text ?? itemDefinition["@_name"], - }; - } - - private createInnerType(itemComponent: DmnItemDefinition) { - return { - name: itemComponent["@_name"], - properties: this.buildProperties(itemComponent), - typeRef: itemComponent["typeRef"]?.__$$text ?? itemComponent["@_name"], - }; - } - - private buildProperties(itemComponent: DmnItemDefinition): Map { - const properties = new Map(); - - itemComponent.itemComponent?.forEach((def) => { - const rootProperty = { - name: def["@_name"], - properties: this.buildProperties(def), - typeRef: def["typeRef"]?.__$$text ?? def["@_name"], - }; - - properties.set(rootProperty.name, rootProperty); - }); - - return properties; - } - - private addLiteralExpression(parent: VariableContext, element: DmnLiteralExpression) { - const id = element["@_id"] ?? ""; - const expression = new Expression(id, element.text?.__$$text); - this.expressionsIndexedByUuid.set(id, expression); - this.addVariable(id, "", FeelSyntacticSymbolNature.LocalVariable, parent); - } - - private addDecisionTableEntryNode(parent: VariableContext, entryId: string) { - const ruleInputElementNode = this.addVariable(entryId, "", FeelSyntacticSymbolNature.LocalVariable, parent); - parent.children.set(ruleInputElementNode.uuid, ruleInputElementNode); - this.addVariable(ruleInputElementNode.uuid, "", FeelSyntacticSymbolNature.LocalVariable, ruleInputElementNode); - } - - private addDecisionTable(parent: VariableContext, decisionTable: DmnDecisionTable) { - const variableNode = this.addVariable( - decisionTable["@_id"] ?? "", - "", - FeelSyntacticSymbolNature.LocalVariable, - parent - ); - parent.children.set(variableNode.uuid, variableNode); - - if (decisionTable.rule) { - for (const ruleElement of decisionTable.rule) { - ruleElement.inputEntry?.forEach((ruleInputElement) => - this.addDecisionTableEntryNode(parent, ruleInputElement["@_id"] ?? "") - ); - ruleElement.outputEntry?.forEach((ruleOutputElement) => - this.addDecisionTableEntryNode(parent, ruleOutputElement["@_id"] ?? "") - ); - } - } - - this.addVariable(variableNode.uuid, "", FeelSyntacticSymbolNature.LocalVariable, parent); - } - - private addInvocation(parent: VariableContext, element: DmnInvocation) { - if (element.binding) { - for (const bindingElement of element.binding) { - if (bindingElement.expression) { - this.addInnerExpression(parent, bindingElement.expression); - } - } - } - } - - private addContext(parent: VariableContext, element: DmnContext) { - let parentNode = parent; - if (element.contextEntry) { - for (const innerEntry of element.contextEntry) { - parentNode = this.addContextEntry(parentNode, innerEntry); - } - } - } - - private addContextEntry(parentNode: VariableContext, contextEntry: DmnContextEntry) { - const variableNode = this.addVariable( - contextEntry.variable?.["@_id"] ?? "", - contextEntry.variable?.["@_name"] ?? "", - FeelSyntacticSymbolNature.LocalVariable, - parentNode, - contextEntry.variable?.["@_typeRef"] - ); - - parentNode.children.set(variableNode.uuid, variableNode); - - if (contextEntry.expression) { - if (contextEntry.expression.__$$element) { - // The parent is always the previous node to prevent recursive calls. - // Consider this example: - // - // [ROOT] Context Expression - // [X] Client DTI | [A] - // [Y] Some Other | [B] - // [Z] And Another | [C] - // - // Inside the B we can not call "Some Other" for instance, but we can call "Client DTI" - // - // So the structure for that case should be: - // [ROOT] - // / \ - // [X] [A] - // / \ - // [Y] [B] - // / \ - // [Z] [C] - // - // So in that case, inside "C" we recognize Y, X and ROOT - // Inside B, X and ROOT - // - // By "ROOT" we understand the root expression which for example - // can be the Decision Node itself and its input nodes. - this.addInnerExpression(parentNode, contextEntry.expression); - } - } - - return variableNode; - } - - private addFunctionDefinition(parent: VariableContext, element: DmnFunctionDefinition) { - let parentElement = parent; - - if (element.formalParameter) { - for (const parameter of element.formalParameter) { - parentElement = this.addVariable( - parameter["@_id"] ?? "", - parameter["@_name"] ?? "", - FeelSyntacticSymbolNature.Parameter, - parentElement - ); - } - } - - if (element.expression) { - this.addInnerExpression(parentElement, element.expression); - } - } - - private addRelation(parent: VariableContext, element: DmnRelation) { - if (element.row) { - for (const rowElement of element.row) { - if (rowElement.expression) { - for (const expression of rowElement.expression) { - this.addInnerExpression(parent, expression); - } - } - } - } - } - - private addList(parent: VariableContext, element: DmnList) { - if (element.expression) { - for (const expression of element.expression) { - if (expression) { - this.addInnerExpression(parent, expression); - } - } - } - } - - private addConditional(parent: VariableContext, element: DmnConditional) { - if (element.if?.expression) { - this.addInnerExpression(parent, element.if.expression); - } - if (element.then?.expression) { - this.addInnerExpression(parent, element.then.expression); - } - if (element.else?.expression) { - this.addInnerExpression(parent, element.else.expression); - } - } - - private addIterable(parent: VariableContext, expression: DmnSome | DmnEvery) { - const localParent = this.addIteratorVariable(parent, expression); - - if (expression.in.expression) { - this.addInnerExpression(localParent, expression.in.expression); - } - if (expression.satisfies.expression) { - this.addInnerExpression(localParent, expression.satisfies.expression); - } - } - - private addFor(parent: VariableContext, expression: DmnFor) { - const localParent = this.addIteratorVariable(parent, expression); - - if (expression.return.expression) { - this.addInnerExpression(localParent, expression.return.expression); - } - if (expression.in.expression) { - this.addInnerExpression(localParent, expression.in.expression); - } - } - - private addFilterVariable(parent: VariableContext, expression: DmnFilter) { - let type = undefined; - - // We're assuming that the 'in' expression is with the correct type (a list of @_typeRef). - // If it is not the expression will fail anyway. - if (expression.in.expression) { - type = expression.in.expression["@_typeRef"]; - } - return this.addVariable( - expression["@_id"] ?? "", - "item", - FeelSyntacticSymbolNature.LocalVariable, - parent, - type, - true - ); - } - - private addIteratorVariable(parent: VariableContext, expression: DmnFor | DmnEvery | DmnSome) { - let localParent = parent; - if (expression["@_iteratorVariable"]) { - let type = undefined; - - // We're assuming that the 'in' expression is with the correct type (a list of @_typeRef). - // If it is not the expression will fail anyway. - if (expression.in.expression) { - type = expression.in.expression["@_typeRef"]; - } - localParent = this.addVariable( - expression["@_id"] ?? "", - expression["@_iteratorVariable"], - FeelSyntacticSymbolNature.LocalVariable, - parent, - type, - true - ); - } - return localParent; - } - - private addFilter(parent: VariableContext, expression: DmnFilter) { - if (expression.in.expression) { - this.addInnerExpression(parent, expression.in.expression); - } - - const localParent = this.addFilterVariable(parent, expression); - if (expression.match.expression) { - this.addInnerExpression(localParent, expression.match.expression); - } - } - - private addInnerExpression( - parent: VariableContext, - expression: - | DmnLiteralExpression - | DmnInvocation - | DmnDecisionTable - | DmnContext - | DmnFunctionDefinition - | DmnRelation - | DmnList - | DmnFor - | DmnFilter - | DmnEvery - | DmnSome - | DmnConditional - ) { - switch (expression.__$$element) { - case "literalExpression": - this.addLiteralExpression(parent, expression); - break; - - case "invocation": - this.addInvocation(parent, expression); - break; - - case "decisionTable": - // It doesn't define variables but we need it to create its own context to use variables inside of Decision Table. - this.addDecisionTable(parent, expression); - break; - - case "context": - this.addContext(parent, expression); - break; - - case "functionDefinition": - this.addFunctionDefinition(parent, expression); - break; - - case "relation": - this.addRelation(parent, expression); - break; - - case "list": - this.addList(parent, expression); - break; - - case "conditional": - this.addConditional(parent, expression); - break; - - case "every": - case "some": - this.addIterable(parent, expression); - break; - - case "for": - this.addFor(parent, expression); - break; - - case "filter": - this.addFilter(parent, expression); - break; - - default: - // throw new Error("Unknown or not supported type for expression."); - } - } - - private addInputVariable(parent: VariableContext, requirement: DmnInformationRequirement) { - if (requirement.requiredDecision) { - parent.inputVariables.push(requirement.requiredDecision["@_href"]?.replace("#", "")); - } else if (requirement.requiredInput) { - parent.inputVariables.push(requirement.requiredInput["@_href"]?.replace("#", "")); - } - } - - private addInputVariableFromKnowledge(parent: VariableContext, knowledgeRequirement: DmnKnowledgeRequirement) { - if (knowledgeRequirement.requiredKnowledge) { - parent.inputVariables.push(knowledgeRequirement.requiredKnowledge["@_href"]?.replace("#", "")); - } - } - - private buildVariableUuid(uuid: string) { - if (this.currentUuidPrefix.length != 0) { - return this.currentUuidPrefix + uuid; - } - - return uuid; - } - - private buildName(name: string) { - if (this.currentVariablePrefix.length != 0) { - return this.currentVariablePrefix + "." + name; - } - - return name; - } - - private loadVariables(dmnDefinitions: DmnDefinitions) { - this.createDataTypes(dmnDefinitions); - this.createVariables(dmnDefinitions); - } - - private loadImportedVariables(dmnDefinitions: DmnDefinitions, externalDefinitions: Map) { - if (dmnDefinitions.import) { - for (const dmnImport of dmnDefinitions.import) { - if (externalDefinitions.has(dmnImport["@_namespace"])) { - this.currentVariablePrefix = dmnImport["@_name"]; - this.currentUuidPrefix = dmnImport["@_namespace"]; - const externalDef = externalDefinitions.get(dmnImport["@_namespace"]); - if (externalDef) { - this.loadVariables(externalDef.definitions); - } - } - } - } - } -} diff --git a/packages/dmn-feel-antlr4-parser/src/parser/grammar/BaseSymbol.ts b/packages/dmn-feel-antlr4-parser/src/parser/grammar/BaseSymbol.ts index 2e7b39392fd..d87837efad5 100644 --- a/packages/dmn-feel-antlr4-parser/src/parser/grammar/BaseSymbol.ts +++ b/packages/dmn-feel-antlr4-parser/src/parser/grammar/BaseSymbol.ts @@ -21,25 +21,38 @@ import { Symbol } from "./Symbol"; import { Type } from "./Type"; import { Scope } from "./Scope"; -export class BaseSymbol implements Symbol { +/** + * Define a base symbol. + * A "symbol" is anything known by the parser. + */ +export abstract class BaseSymbol implements Symbol { private readonly id?: string; private readonly type?: Type; private readonly scope?: Scope; - constructor(id?: string, type?: Type, scope?: Scope) { + protected constructor(id?: string, type?: Type, scope?: Scope) { this.id = id; this.type = type; this.scope = scope; } + /** + * The ID of the symbol. + */ getId(): string | undefined { return this.id; } + /** + * The scope of the symbol. + */ getScope(): Scope | undefined { return this.scope; } + /** + * The type of the symbol. + */ getType(): Type | undefined { return this.type; } diff --git a/packages/dmn-feel-antlr4-parser/src/parser/grammar/FunctionSymbol.ts b/packages/dmn-feel-antlr4-parser/src/parser/grammar/FunctionSymbol.ts index 2a82e3672f0..758107fb37c 100644 --- a/packages/dmn-feel-antlr4-parser/src/parser/grammar/FunctionSymbol.ts +++ b/packages/dmn-feel-antlr4-parser/src/parser/grammar/FunctionSymbol.ts @@ -20,7 +20,12 @@ import { Scope } from "./Scope"; import { Symbol } from "./Symbol"; import { Type } from "./Type"; +import { Expression } from "../Expression"; +import { FeelSyntacticSymbolNature } from "../FeelSyntacticSymbolNature"; +/** + * Defines a symbol that represents a function or a method. + */ export class FunctionSymbol implements Symbol { private readonly id; @@ -33,9 +38,19 @@ export class FunctionSymbol implements Symbol { } getType(): Type | undefined { - return { name: this.id }; + return { + source: { + value: this.id, + feelSyntacticSymbolNature: FeelSyntacticSymbolNature.Unknown, + expressionsThatUseTheIdentifier: new Map(), + }, + name: this.id, + }; } + /** + * Symbols and Functions does not define scopes, so it returns undefined. + */ getScope(): Scope | undefined { return undefined; } diff --git a/packages/dmn-feel-antlr4-parser/src/parser/grammar/VariableSymbol.ts b/packages/dmn-feel-antlr4-parser/src/parser/grammar/IdentifierSymbol.ts similarity index 64% rename from packages/dmn-feel-antlr4-parser/src/parser/grammar/VariableSymbol.ts rename to packages/dmn-feel-antlr4-parser/src/parser/grammar/IdentifierSymbol.ts index 3073cefa241..7c022269cc7 100644 --- a/packages/dmn-feel-antlr4-parser/src/parser/grammar/VariableSymbol.ts +++ b/packages/dmn-feel-antlr4-parser/src/parser/grammar/IdentifierSymbol.ts @@ -19,37 +19,50 @@ import { BaseSymbol } from "./BaseSymbol"; import { Type } from "./Type"; - import { FeelSyntacticSymbolNature } from "../FeelSyntacticSymbolNature"; -import { Variable } from "../Variable"; -import { Scope } from "./Scope"; +import { Identifier } from "../Identifier"; -export class VariableSymbol extends BaseSymbol { +/** + * Defines a symbol that represents a variable, property or any other known symbol that is not a {@link FunctionSymbol}. + */ +export class IdentifierSymbol extends BaseSymbol { private readonly _symbolType: FeelSyntacticSymbolNature | undefined; - private readonly _variableSource: Variable | undefined; + private readonly _symbolSource: Identifier | undefined; private readonly _allowDynamicVariables: boolean | undefined; constructor( id?: string, type?: Type, variableType?: FeelSyntacticSymbolNature, - variableSource?: Variable, + variableSource?: Identifier, allowDynamicVariables?: boolean ) { super(id, type); this._symbolType = variableType; - this._variableSource = variableSource; + this._symbolSource = variableSource; this._allowDynamicVariables = allowDynamicVariables; } + /** + * The nature of the symbol. See {@link FeelSyntacticSymbolNature}. + */ get symbolType(): FeelSyntacticSymbolNature | undefined { return this._symbolType; } - get variableSource(): Variable | undefined { - return this._variableSource; + /** + * The source that originated this symbol. + */ + get symbolSource(): Identifier | undefined { + return this._symbolSource; } + /** + * If it is a symbol where the context allow dynamic variables. + * Dynamic variables are variables that are validate during runtime and the parser can not determine if they are + * valid or not. + * See {@link https://github.com/apache/incubator-kie-tools/pull/2296} + */ get allowDynamicVariables(): boolean | undefined { return this._allowDynamicVariables; } diff --git a/packages/dmn-feel-antlr4-parser/src/parser/grammar/MapBackedType.ts b/packages/dmn-feel-antlr4-parser/src/parser/grammar/MapBackedType.ts index b170adab465..18b1e637d15 100644 --- a/packages/dmn-feel-antlr4-parser/src/parser/grammar/MapBackedType.ts +++ b/packages/dmn-feel-antlr4-parser/src/parser/grammar/MapBackedType.ts @@ -18,11 +18,20 @@ */ import { Type } from "./Type"; +import { Identifier } from "../Identifier"; export class MapBackedType implements Type { private readonly _typeRef: string; private readonly _name: string; private readonly _properties: Map; + private readonly _source: Identifier; + + constructor(name: string, typeRef: string, source: Identifier) { + this._typeRef = typeRef; + this._name = name; + this._properties = new Map(); + this._source = source; + } get name(): string { return this._name; @@ -36,9 +45,7 @@ export class MapBackedType implements Type { return this._typeRef; } - constructor(name: string, typeRef: string) { - this._typeRef = typeRef; - this._name = name; - this._properties = new Map(); + get source(): Identifier { + return this._source; } } diff --git a/packages/dmn-feel-antlr4-parser/src/parser/grammar/ParserHelper.ts b/packages/dmn-feel-antlr4-parser/src/parser/grammar/ParserHelper.ts index 76c80d65760..09bc25acf03 100644 --- a/packages/dmn-feel-antlr4-parser/src/parser/grammar/ParserHelper.ts +++ b/packages/dmn-feel-antlr4-parser/src/parser/grammar/ParserHelper.ts @@ -21,23 +21,23 @@ import { Parser, ParserRuleContext, Token } from "antlr4"; import { FilterPathExpressionContext, KeyStringContext, NameRefContext } from "./generated-parser/FEEL_1_1Parser"; import { Scope } from "./Scope"; import { Type } from "./Type"; -import { VariableSymbol } from "./VariableSymbol"; +import { IdentifierSymbol } from "./IdentifierSymbol"; import { ScopeImpl } from "./ScopeImpl"; import { NameQueue } from "./NameQueue"; -import { FeelVariable } from "../FeelVariable"; +import { FeelIdentifiedSymbol } from "../FeelIdentifiedSymbol"; import { Scopes } from "./Scopes"; import { ReservedWords } from "../ReservedWords"; import { FeelSyntacticSymbolNature } from "../FeelSyntacticSymbolNature"; import { MapBackedType } from "./MapBackedType"; import { FeelSymbol } from "../FeelSymbol"; -import { Variable } from "../Variable"; +import { Identifier } from "../Identifier"; import { FunctionSymbol } from "./FunctionSymbol"; export class ParserHelper { private dynamicResolution = 0; private currentScope: Scope | undefined; private readonly currentName: NameQueue; - private readonly _variables: Array; + private readonly _variables: Array; private readonly scopes = new Scopes(); private readonly _availableSymbols: Array; @@ -45,7 +45,7 @@ export class ParserHelper { this.currentName = new NameQueue(); this.currentName.push(""); this.currentScope = this.scopes.getGlobalScope(); - this._variables = new Array(); + this._variables = new Array(); this._availableSymbols = new Array(); } @@ -53,7 +53,7 @@ export class ParserHelper { return this._availableSymbols; } - get variables(): Array { + get variables(): Array { return this._variables; } @@ -110,10 +110,10 @@ export class ParserHelper { variable: string | ParserRuleContext, type?: Type, variableType?: FeelSyntacticSymbolNature, - variableSource?: Variable, + variableSource?: Identifier, allowDynamicVariables?: boolean ) { - const variableSymbol = new VariableSymbol( + const variableSymbol = new IdentifierSymbol( variable instanceof ParserRuleContext ? this.getName(variable) : variable, type, variableType, @@ -154,7 +154,9 @@ export class ParserHelper { if (resolved != null && scopeType instanceof MapBackedType) { this.pushScope(scopeType); for (const f of scopeType.properties) { - this.currentScope?.define(new VariableSymbol(f[0], f[1])); + this.currentScope?.define( + new IdentifierSymbol(f[0], f[1], FeelSyntacticSymbolNature.GlobalVariable, f[1].source) + ); } } else { this.pushScope(); @@ -193,15 +195,22 @@ export class ParserHelper { const variableName = name.replaceAll("\r\n", " ").replaceAll("\n", " ").replace(/\s\s+/g, " "); if (this.currentScope?.getChildScopes().has(variableName)) { this.variables.push( - new FeelVariable(start, length, startLine, endLine, FeelSyntacticSymbolNature.GlobalVariable, variableName) + new FeelIdentifiedSymbol( + start, + length, + startLine, + endLine, + FeelSyntacticSymbolNature.GlobalVariable, + variableName + ) ); } else { const symbol = this.currentScope?.resolve(variableName); if (symbol) { - if (symbol instanceof VariableSymbol) { + if (symbol instanceof IdentifierSymbol) { const scopeSymbols = []; - if ((symbol as VariableSymbol).getType() instanceof MapBackedType) { - const map = (symbol as VariableSymbol).getType() as MapBackedType; + if ((symbol as IdentifierSymbol).getType() instanceof MapBackedType) { + const map = (symbol as IdentifierSymbol).getType() as MapBackedType; for (const [key, value] of map.properties) { scopeSymbols.push({ name: key, @@ -215,26 +224,34 @@ export class ParserHelper { } this.variables.push( - new FeelVariable( + new FeelIdentifiedSymbol( start, length, startLine, endLine, symbol.symbolType ?? FeelSyntacticSymbolNature.GlobalVariable, variableName, - scopeSymbols + scopeSymbols, + symbol.symbolSource ) ); } else if (!(symbol instanceof FunctionSymbol)) { // We ignore FunctionSymbols (built-in functions) because they are not variables this.variables.push( - new FeelVariable(start, length, startLine, endLine, FeelSyntacticSymbolNature.GlobalVariable, variableName) + new FeelIdentifiedSymbol( + start, + length, + startLine, + endLine, + FeelSyntacticSymbolNature.GlobalVariable, + variableName + ) ); } } else { if (!ReservedWords.FeelFunctions.has(variableName) && !ReservedWords.FeelKeywords.has(variableName)) { this.variables.push( - new FeelVariable(start, length, startLine, endLine, FeelSyntacticSymbolNature.Unknown, variableName) + new FeelIdentifiedSymbol(start, length, startLine, endLine, FeelSyntacticSymbolNature.Unknown, variableName) ); } } diff --git a/packages/dmn-feel-antlr4-parser/src/parser/grammar/ScopeImpl.ts b/packages/dmn-feel-antlr4-parser/src/parser/grammar/ScopeImpl.ts index 16c86905704..2c86a2d7c29 100644 --- a/packages/dmn-feel-antlr4-parser/src/parser/grammar/ScopeImpl.ts +++ b/packages/dmn-feel-antlr4-parser/src/parser/grammar/ScopeImpl.ts @@ -23,8 +23,9 @@ import { Symbol } from "./Symbol"; import { TokenTree } from "./TokenTree"; import { CharStreams, Token } from "antlr4"; import FEEL_1_1Lexer from "./generated-parser/FEEL_1_1Lexer"; -import { VariableSymbol } from "./VariableSymbol"; +import { IdentifierSymbol } from "./IdentifierSymbol"; import { FeelSyntacticSymbolNature } from "../FeelSyntacticSymbolNature"; +import { Expression } from "../Expression"; export class ScopeImpl implements Scope { private readonly name?: string; @@ -119,10 +120,15 @@ export class ScopeImpl implements Scope { resolve(parameter: string | string[]): Symbol | undefined { if (typeof parameter === "string") { if (this._allowDynamicVariables) { - return new VariableSymbol( + return new IdentifierSymbol( parameter, { name: "name", + source: { + value: "name", + feelSyntacticSymbolNature: FeelSyntacticSymbolNature.DynamicVariable, + expressionsThatUseTheIdentifier: new Map(), + }, }, FeelSyntacticSymbolNature.DynamicVariable ); diff --git a/packages/dmn-feel-antlr4-parser/src/parser/grammar/Type.ts b/packages/dmn-feel-antlr4-parser/src/parser/grammar/Type.ts index 37ccdbc3819..ad10b5d4cb4 100644 --- a/packages/dmn-feel-antlr4-parser/src/parser/grammar/Type.ts +++ b/packages/dmn-feel-antlr4-parser/src/parser/grammar/Type.ts @@ -17,7 +17,10 @@ * under the License. */ +import { Identifier } from "../Identifier"; + export interface Type { name: string; typeRef?: string; + source: Identifier; } diff --git a/packages/dmn-language-service/src/index.ts b/packages/dmn-language-service/src/index.ts index f217cc1e131..21ed58d7985 100644 --- a/packages/dmn-language-service/src/index.ts +++ b/packages/dmn-language-service/src/index.ts @@ -19,3 +19,4 @@ export * from "./DmnDocumentData"; export * from "./DmnLanguageService"; +export * from "./refactor/IdentifiersRefactor"; diff --git a/packages/dmn-language-service/src/refactor/IdentifiersRefactor.ts b/packages/dmn-language-service/src/refactor/IdentifiersRefactor.ts new file mode 100644 index 00000000000..fe3f2cab231 --- /dev/null +++ b/packages/dmn-language-service/src/refactor/IdentifiersRefactor.ts @@ -0,0 +1,160 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { DmnDefinitions, IdentifiersRepository } from "@kie-tools/dmn-feel-antlr4-parser/dist"; +import { DmnLatestModel } from "@kie-tools/dmn-marshaller"; +import { Expression } from "@kie-tools/dmn-feel-antlr4-parser/dist"; +import { FeelIdentifiersParser } from "@kie-tools/dmn-feel-antlr4-parser/dist"; +import { IdentifierContext } from "@kie-tools/dmn-feel-antlr4-parser/dist/parser/IdentifierContext"; +import { Normalized } from "@kie-tools/dmn-marshaller/dist/normalization/normalize"; + +/** + * Defines the IdentifiersRefactor, which can be used to rename identifiers in the DMN files changing all expressions + * that use those identifiers. + */ +export class IdentifiersRefactor { + private readonly repository: IdentifiersRepository; + + constructor(args: { + writeableDmnDefinitions: Normalized; + _readonly_externalDmnModelsByNamespaceMap: Map>; + }) { + this.repository = new IdentifiersRepository( + args.writeableDmnDefinitions, + args._readonly_externalDmnModelsByNamespaceMap + ); + + this.computeIdentifiersLinksToExpressions(); + } + + get expressions(): Map { + return this.repository.expressions; + } + + get identifiers(): Map { + return this.repository.identifiers; + } + + public reload() { + this.repository.reload(); + this.computeIdentifiersLinksToExpressions(); + } + + public changeType(args: { identifierUuid: string; newType: string | undefined }) { + const context = this.repository.identifiersContextIndexedByUuid.get(args.identifierUuid); + if (context) { + context.identifier.typeRef = this.repository.getTypeRef(args.newType); + if (context.identifier.applyTypeRefToSource) { + context.identifier.applyTypeRefToSource(); + } + } + this.applyExpressionsChangesToDefinition(); + } + + /** + * Rename some specific identifier to the new name. + * We need to use the `identifierUuid` because the names are context dependent, so we may have the same identifier + * name in more than one context. + * @param args An object with the `identifierUuid` and the `newName`. + */ + public rename(args: { identifierUuid: string; newName: string }) { + const context = this.repository.identifiersContextIndexedByUuid.get(args.identifierUuid); + if (context) { + for (const expression of context.identifier.expressionsThatUseTheIdentifier.values()) { + expression.renameIdentifier(context.identifier, args.newName); + } + + context.identifier.value = args.newName; + if (context.identifier.applyValueToSource) { + context.identifier.applyValueToSource(); + } + } else { + const dataType = this.repository.dataTypeIndexedByUuid.get(args.identifierUuid); + if (dataType) { + for (const expression of dataType.source.expressionsThatUseTheIdentifier.values()) { + expression.renameIdentifier(dataType.source, args.newName); + } + dataType.source.value = args.newName; + } + } + this.applyExpressionsChangesToDefinition(); + } + + public renameImport(args: { oldName: string; newName: string }) { + const importedIdentifiers = this.repository.importedIdentifiers.get(args.oldName); + if (importedIdentifiers) { + for (const imported of importedIdentifiers) { + const newName = imported.identifier.value.replace(args.oldName + ".", args.newName + "."); + for (const expression of imported.identifier.expressionsThatUseTheIdentifier.values()) { + expression.renameIdentifier(imported.identifier, newName); + } + imported.identifier.value = newName; + } + this.repository.importedIdentifiers.delete(args.oldName); + this.repository.importedIdentifiers.set(args.newName, importedIdentifiers); + } + const importedDataTypes = this.repository.importedDataTypes.get(args.oldName); + if (importedDataTypes) { + for (const imported of importedDataTypes) { + const newDataTypeName = imported.source.value.replace(args.oldName + ".", args.newName + "."); + for (const expression of imported.source.expressionsThatUseTheIdentifier.values()) { + expression.renameIdentifier(imported.source, newDataTypeName); + } + imported.source.value = newDataTypeName; + } + this.repository.importedDataTypes.delete(args.oldName); + this.repository.importedDataTypes.set(args.newName, importedDataTypes); + } + this.applyExpressionsChangesToDefinition(); + } + + public getExpressionsThatUseTheIdentifier(identifierId: string) { + const identifierContext = this.repository.identifiers.get(identifierId); + if (!identifierContext) { + return []; + } + + return identifierContext.identifier.expressionsThatUseTheIdentifier.values(); + } + + private computeIdentifiersLinksToExpressions() { + const parser = new FeelIdentifiersParser(this.repository); + + for (const expression of this.repository.expressions.values()) { + for (const identifier of expression.identifiersOfTheExpression) { + identifier.source?.expressionsThatUseTheIdentifier.delete(expression.uuid); + } + + // The parser is the only one able to parse all the expressions and set in the repository the links between + // the existing expressions and the existing identifiers. Otherwise, we just have a collection of known identifiers + // and known expressions without any link between them. + const parsedExpression = parser.parse(expression.uuid, expression.fullExpression); + expression.identifiersOfTheExpression = parsedExpression.feelIdentifiedSymbols; + for (const feelIdentifiedSymbol of parsedExpression.feelIdentifiedSymbols) { + feelIdentifiedSymbol.source?.expressionsThatUseTheIdentifier.set(expression.uuid, expression); + } + } + } + + private applyExpressionsChangesToDefinition() { + for (const expression of this.expressions.values()) { + expression.applyChangesToExpressionSource(); + } + } +} diff --git a/packages/dmn-language-service/tests/fixtures/refactor/includeMathModel.dmn b/packages/dmn-language-service/tests/fixtures/refactor/includeMathModel.dmn new file mode 100644 index 00000000000..73af2b2f0b5 --- /dev/null +++ b/packages/dmn-language-service/tests/fixtures/refactor/includeMathModel.dmn @@ -0,0 +1,160 @@ + + + + + + + + + + + + + + + + + + + + + + INCLUDED_MATH.Input A + INCLUDED_MATH.Input B + INCLUDED_MATH.Input C + INCLUDED_MATH.Sum Numbers + INCLUDED_MATH_FAKE + + + + + + 123456 + + + + + + + + 907 + + + 190 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/dmn-language-service/tests/fixtures/refactor/math.dmn b/packages/dmn-language-service/tests/fixtures/refactor/math.dmn new file mode 100644 index 00000000000..067668f6e15 --- /dev/null +++ b/packages/dmn-language-service/tests/fixtures/refactor/math.dmn @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Sum Numbers + 10 + Sum Numbers + 20 + + + + + + + + 10 + Sum Numbers + 50+Sum Numbers/10*Sum Numbers-Sum Numbers+1 + + + + + Inner Calc 1+1+Inner Calc 2+2+Sum Numbers/Inner Calc 1 + Inner Calc 2 + Sum Numbers + + + + + + + + + + + + + + Sum Numbers +10 + + + Sum Numbers*2 + + + + + + mySomeVar > 20 + + + + + + + + + + + + + + + + + + + + (Input A + Input B + Input C)/2 + + + (Input A + Input B + Input C)/3 + + + Input A + Input B + Input C + + + + + + myEveryVar > 2 + + + + + + + + + + + + + + + + + + + My Decision Service(10, 20, 30) /3 + My Decision Service(1,1,1) + + + + + + + + 120 + + + 469 + + + 190 + + + 190 + + + 190 + + + 190 + + + 190 + + + 349 + + + 349 + + + 349 + + + 411 + + + 784 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/dmn-language-service/tests/fixtures/refactor/sampleLoan.dmn b/packages/dmn-language-service/tests/fixtures/refactor/sampleLoan.dmn new file mode 100644 index 00000000000..adb76c85f77 --- /dev/null +++ b/packages/dmn-language-service/tests/fixtures/refactor/sampleLoan.dmn @@ -0,0 +1,857 @@ + + + + + + + Product_Type + + + number + + + number + + + number + + + + string + + "M","D","S" + + + + + number + + + Marital_Status + + + string + + "Unemployed","Employed","Self-employed","Student" + + + + boolean + + + + number + + + number + + + number + + + number + + + number + + + + + + Risk_Category + + + number + + + + + string + + + number + + + + string + + "Ineligible","Eligible" + + + + string + + "Decline","Bureau","Through" + + + + string + + "Full","Mini","None" + + + + string + + "Standard Loan","Special Loan" + + + + string + + "High","Medium","Low","Very Low","Decline" + + + + string + + "Poor","Bad","Fair","Good","Excellent" + + + + string + + "Insufficient","Sufficient" + + + + string + + "Sufficient","Insufficient" + + + + string + + "Not Qualified","Qualified" + + + + + number + + [300..850] + + + + + + string + + "Qualified","Not Qualified" + + + + string + + + + + + + + + + + + 0.36 + + + + + + + + + + + + + + + + + + + + + + + + PITI + + + + + (Requested Product.Amount * ((Requested Product.Rate/100)/12)) / (1-(1/(1+(Requested Product.Rate/100)/12) * -Requested Product.Term)) + + + + + + Applicant Data.Monthly.Tax + + + + + + Applicant Data.Monthly.Insurance + + + + + + Applicant Data.Monthly.Income + + + + + + + if Client PITI <= Lender Acceptable PITI() + then "Sufficient" + else "Insufficient" + + + + + + + + + + + + + + (pmt+tax+insurance) / income + + + + + + + + + + + + + + + + + + + + + + + + + DTI + + + + + Applicant Data.Monthly.Repayments + Applicant Data.Monthly.Expenses + + + + + + Applicant Data.Monthly.Income + + + + + + + if Client DTI <= Lender Acceptable DTI() + then "Sufficient" + else "Insufficient" + + + + + + + + + + + + + + Credit Score.FICO + + + + + + + >= 750 + + + "Excellent" + + + + + + + + [700..750) + + + "Good" + + + + + + + + [650..700) + + + "Fair" + + + + + + + + [600..650) + + + "Poor" + + + + + + + + < 600 + + + "Bad" + + + + + + + + + + + + + + + + + + + + + + + Credit Score Rating + + + + + Back End Ratio + + + + + Front End Ratio + + + + + + + + "Poor", "Bad" + + + - + + + - + + + "Not Qualified" + + + "Credit Score too low." + + + + + + + + - + + + "Insufficient" + + + "Sufficient" + + + "Not Qualified" + + + "Debt to income ratio is too high." + + + + + + + + - + + + "Sufficient" + + + "Insufficient" + + + "Not Qualified" + + + "Mortgage payment to income ratio is too high." + + + + + + + + - + + + "Insufficient" + + + "Insufficient" + + + "Not Qualified" + + + "Debt to income ratio is too high AND mortgage payment to income ratio is too high." + + + + + + + + "Fair", "Good", "Excellent" + + + "Sufficient" + + + "Sufficient" + + + "Qualified" + + + "The borrower has been successfully prequalified for the requested loan." + + + + + + + + + + + + + + + + + + + d/i + + + + + + + + + 0.28 + + + + + + + + + 209 + + + 50 + 209 + + + 50 + 100 + 1280 + + + 50 + 100 + 1110 + + + + 1110 + + + 1110 + + + 1110 + + + 1110 + + + 1280 + + + 300 + + + 50 + 300 + + + 50 + 100 + 802 + + + 50 + 100 + 632 + + + + 632 + + + 632 + + + 802 + + + 50 + 224 + 226 + 335 + + + 50 + 233 + 130 + 136 + 135 + 681 + 100 + + + 150 + + + 50 + 150 + + + 228 + + + 50 + 228 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/dmn-language-service/tests/fs/fixtures.ts b/packages/dmn-language-service/tests/fs/fixtures.ts index b2f265c5fb9..016dcc98a7f 100644 --- a/packages/dmn-language-service/tests/fs/fixtures.ts +++ b/packages/dmn-language-service/tests/fs/fixtures.ts @@ -89,6 +89,24 @@ export const threeLevelRecursionC = () => { }); }; +export const sampleLoanDmnModel = () => { + return getModelXmlForTestFixtures({ + normalizedPosixPathRelativeToTheWorkspaceRoot: "fixtures/refactor/sampleLoan.dmn", + }); +}; + +export const mathDmnModel = () => { + return getModelXmlForTestFixtures({ + normalizedPosixPathRelativeToTheWorkspaceRoot: "fixtures/refactor/math.dmn", + }); +}; + +export const includeMathModelDmn = () => { + return getModelXmlForTestFixtures({ + normalizedPosixPathRelativeToTheWorkspaceRoot: "fixtures/refactor/includeMathModel.dmn", + }); +}; + export const decisions = () => { return getModelXmlForTestFixtures({ normalizedPosixPathRelativeToTheWorkspaceRoot: "fixtures/decisions.dmn" }); }; diff --git a/packages/dmn-language-service/tests/refactor/IdentifiersRefactor.test.ts b/packages/dmn-language-service/tests/refactor/IdentifiersRefactor.test.ts new file mode 100644 index 00000000000..39946b867f2 --- /dev/null +++ b/packages/dmn-language-service/tests/refactor/IdentifiersRefactor.test.ts @@ -0,0 +1,223 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { DMN15__tDefinitions } from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types"; +import { DmnLatestModel, getMarshaller } from "@kie-tools/dmn-marshaller"; +import { includeMathModelDmn, mathDmnModel, sampleLoanDmnModel } from "../fs/fixtures"; +import { IdentifiersRefactor } from "@kie-tools/dmn-language-service"; +import { Normalized } from "@kie-tools/dmn-marshaller/dist/normalization/normalize"; + +describe("Refactor renamed identifiers", () => { + test("rename input element - should update referenced expressions", async () => { + const identifiersRefactor = new IdentifiersRefactor({ + writeableDmnDefinitions: getDefinitions(sampleLoanDmnModel()), + _readonly_externalDmnModelsByNamespaceMap: new Map(), + }); + + // Rename "Requested Product" to "Changed Input" + identifiersRefactor.rename({ identifierUuid: "_6E3205AF-7E3D-4ABE-A367-96F3F6E8210E", newName: "Changed Input" }); + + // We reload to re-read the DMN file and make sure that the changes was correctly applied to it + identifiersRefactor.reload(); + + expect( + Array.from(identifiersRefactor.getExpressionsThatUseTheIdentifier("_6E3205AF-7E3D-4ABE-A367-96F3F6E8210E"))[0] + .fullExpression + ).toEqual( + "(Changed Input.Amount * ((Changed Input.Rate/100)/12)) / (1-(1/(1+(Changed Input.Rate/100)/12) * -Changed Input.Term))" + ); + }); + + test("rename bkm element - should update referenced expressions", async () => { + const identifiersRefactor = new IdentifiersRefactor({ + writeableDmnDefinitions: getDefinitions(sampleLoanDmnModel()), + _readonly_externalDmnModelsByNamespaceMap: new Map(), + }); + + // Rename "Lender Acceptable PITI" to "LenderAcceptable_PITI" + identifiersRefactor.rename({ + identifierUuid: "_C98BE939-B9C7-43E0-83E8-EE7A16C5276D", + newName: "LenderAcceptable_PITI", + }); + + // We reload to re-read the DMN file and make sure that the changes was correctly applied to it + identifiersRefactor.reload(); + + expect( + Array.from(identifiersRefactor.getExpressionsThatUseTheIdentifier("_C98BE939-B9C7-43E0-83E8-EE7A16C5276D"))[0] + .fullExpression + ).toEqual("if Client PITI <= LenderAcceptable_PITI()\n" + ' then "Sufficient"\n' + ' else "Insufficient"'); + }); + + test("rename data type properties - should update referenced expressions", async () => { + const identifiersRefactor = new IdentifiersRefactor({ + writeableDmnDefinitions: getDefinitions(sampleLoanDmnModel()), + _readonly_externalDmnModelsByNamespaceMap: new Map(), + }); + + // Rename the "Monthly" data type to "MON" + identifiersRefactor.rename({ + identifierUuid: "_bb9ef72e-2e0d-4175-ba58-d613bda7e9b3", + newName: "MON", + }); + + // id="_4a4d01be-fe97-49a2-8c4c-3a49ff27968d" name="Tax" + // Rename the "Tax" property of "Monthly" data type to "The Tax" + identifiersRefactor.rename({ + identifierUuid: "_4a4d01be-fe97-49a2-8c4c-3a49ff27968d", + newName: "The Tax", + }); + + // Rename the "Rate" property of "Requested Product" data type to "R_A_T_E" + identifiersRefactor.rename({ + identifierUuid: "_ab1647c2-cb63-4808-8d90-36d41591a40c", + newName: "R_A_T_E", + }); + + identifiersRefactor.reload(); + + const expressions = Array.from(identifiersRefactor.expressions.values()).map((e) => e.fullExpression); + + expect(expressions).not.toContain( + "(Requested Product.Amount * ((Requested Product.Rate/100)/12)) / (1-(1/(1+(Requested Product.Rate/100)/12) * -Requested Product.Term))" + ); + expect(expressions).not.toContain("Applicant Data.Monthly.Tax"); + expect(expressions).not.toContain("Applicant Data.Monthly.Insurance"); + + expect(expressions).toContain( + "(Requested Product.Amount * ((Requested Product.R_A_T_E/100)/12)) / (1-(1/(1+(Requested Product.R_A_T_E/100)/12) * -Requested Product.Term))" + ); + expect(expressions).toContain("Applicant Data.MON.The Tax"); + expect(expressions).toContain("Applicant Data.MON.Insurance"); + }); + + test("rename decision element - should update referenced expressions", async () => { + const identifiersRefactor = new IdentifiersRefactor({ + writeableDmnDefinitions: getDefinitions(mathDmnModel()), + _readonly_externalDmnModelsByNamespaceMap: new Map(), + }); + + // Rename "Sum Numbers" to "This Sum 3 Numbers" + identifiersRefactor.rename({ + identifierUuid: "_D1B3D6E9-83C7-4BD0-B8CE-1D2F15E73826", + newName: "This Sum 3 Numbers", + }); + + // We reload to re-read the DMN file and make sure that the changes was correctly applied to it + identifiersRefactor.reload(); + + const expressions = Array.from( + identifiersRefactor.getExpressionsThatUseTheIdentifier("_D1B3D6E9-83C7-4BD0-B8CE-1D2F15E73826") + ).map((c) => c.fullExpression); + + expect(expressions).toContain("This Sum 3 Numbers + 10 + This Sum 3 Numbers + 20"); + expect(expressions).toContain( + "10 + This Sum 3 Numbers + 50+This Sum 3 Numbers/10*This Sum 3 Numbers-This Sum 3 Numbers+1" + ); + expect(expressions).toContain( + "Inner Calc 1+1+Inner Calc 2+2+This Sum 3 Numbers/Inner Calc 1 + Inner Calc 2 + This Sum 3 Numbers" + ); + expect(expressions).toContain("This Sum 3 Numbers +10"); + expect(expressions).toContain("This Sum 3 Numbers*2"); + }); + + test("rename context entry - should update referenced expressions", async () => { + const identifiersRefactor = new IdentifiersRefactor({ + writeableDmnDefinitions: getDefinitions(mathDmnModel()), + _readonly_externalDmnModelsByNamespaceMap: new Map(), + }); + + // Rename "Inner Calc 1" to "Some-Calc-1" + identifiersRefactor.rename({ identifierUuid: "_0A838F50-7852-45B4-A3A8-615A45D24C90", newName: "Some-Calc-1" }); + // Rename "Inner Calc 2" to "a" + identifiersRefactor.rename({ identifierUuid: "_B5B3C83D-6BDB-4924-AF58-D7D85EF70BDF", newName: "a" }); + + // We reload to re-read the DMN file and make sure that the changes was correctly applied to it + identifiersRefactor.reload(); + + expect( + Array.from(identifiersRefactor.getExpressionsThatUseTheIdentifier("_0A838F50-7852-45B4-A3A8-615A45D24C90"))[0] + .fullExpression + ).toEqual("Some-Calc-1+1+a+2+Sum Numbers/Some-Calc-1 + a + Sum Numbers"); + }); + + test("rename 'some' variable in Some expression - should update satisfies", async () => { + const identifiersRefactor = new IdentifiersRefactor({ + writeableDmnDefinitions: getDefinitions(mathDmnModel()), + _readonly_externalDmnModelsByNamespaceMap: new Map(), + }); + + // Rename "mySomeVar" to "my New Var" + identifiersRefactor.rename({ identifierUuid: "_46F5BE0C-ADA7-4FE9-B418-48C7D2A3EBFD", newName: "my New Var" }); + + // We reload to re-read the DMN file and make sure that the changes was correctly applied to it + identifiersRefactor.reload(); + + expect( + Array.from(identifiersRefactor.getExpressionsThatUseTheIdentifier("_46F5BE0C-ADA7-4FE9-B418-48C7D2A3EBFD"))[0] + .fullExpression + ).toEqual("my New Var > 20"); + }); + + test("rename 'every' variable in Every expression - should update satisfies", async () => { + const identifiersRefactor = new IdentifiersRefactor({ + writeableDmnDefinitions: getDefinitions(mathDmnModel()), + _readonly_externalDmnModelsByNamespaceMap: new Map(), + }); + + // Rename "myEveryVar" to "x" + identifiersRefactor.rename({ identifierUuid: "_531BBFBA-6C3B-4A5C-B412-113CF70DF8F4", newName: "x" }); + + // We reload to re-read the DMN file and make sure that the changes was correctly applied to it + identifiersRefactor.reload(); + + expect( + Array.from(identifiersRefactor.getExpressionsThatUseTheIdentifier("_531BBFBA-6C3B-4A5C-B412-113CF70DF8F4"))[0] + .fullExpression + ).toEqual("x > 2"); + }); + + // test("rename decision service element - should update referenced expressions", async () => {}); + test("rename included model - should update referenced expressions", async () => { + const identifiersRefactor = new IdentifiersRefactor({ + writeableDmnDefinitions: getDefinitions(includeMathModelDmn()), + _readonly_externalDmnModelsByNamespaceMap: new Map([ + ["https://kie.org/dmn/_39AA2E1D-15A9-400B-BA55-B663B90AA2DF", getModel(mathDmnModel())], + ]), + }); + + identifiersRefactor.renameImport({ oldName: "INCLUDED_MATH", newName: "math" }); + + // We reload to re-read the DMN file and make sure that the changes was correctly applied to it + identifiersRefactor.reload(); + + // This is the expression in the model that uses the included nodes. + expect(identifiersRefactor.expressions.get("_C8B740AC-F2E3-47F4-A9FE-60B6FF1A713B")?.fullExpression).toEqual( + "math.Input A + math.Input B + math.Input C + math.Sum Numbers + INCLUDED_MATH_FAKE" + ); + }); +}); + +function getDefinitions(content: string): Normalized { + return getMarshaller(content, { upgradeTo: "latest" }).parser.parse().definitions as Normalized; +} + +function getModel(content: string): Normalized { + return getMarshaller(content, { upgradeTo: "latest" }).parser.parse() as Normalized; +} diff --git a/packages/feel-input-component/src/FeelInput.tsx b/packages/feel-input-component/src/FeelInput.tsx index dd5c790812f..ce37e5e2a3a 100644 --- a/packages/feel-input-component/src/FeelInput.tsx +++ b/packages/feel-input-component/src/FeelInput.tsx @@ -29,7 +29,7 @@ import { MONACO_FEEL_THEME, } from "./FeelConfigs"; -import { FeelSyntacticSymbolNature, FeelVariables, ParsedExpression } from "@kie-tools/dmn-feel-antlr4-parser"; +import { FeelSyntacticSymbolNature, FeelIdentifiers, ParsedExpression } from "@kie-tools/dmn-feel-antlr4-parser"; import { SemanticTokensProvider } from "./semanticTokensProvider"; export const EXPRESSION_PROPERTIES_SEPARATOR = "."; @@ -49,7 +49,7 @@ export interface FeelInputProps { onKeyDown?: (event: Monaco.IKeyboardEvent, value: string) => void; onChange?: (event: Monaco.editor.IModelContentChangedEvent, value: string, preview: string) => void; options?: Monaco.editor.IStandaloneEditorConstructionOptions; - feelVariables?: FeelVariables; + feelIdentifiers?: FeelIdentifiers; expressionId?: string; } @@ -87,7 +87,7 @@ export const FeelInput = React.forwardRef( onKeyDown, onChange, options, - feelVariables, + feelIdentifiers, expressionId, }, forwardRef @@ -99,24 +99,24 @@ export const FeelInput = React.forwardRef( const [currentParsedExpression, setCurrentParsedExpression] = useState(); const semanticTokensProvider = useMemo( - () => new SemanticTokensProvider(feelVariables, expressionId, setCurrentParsedExpression), - [expressionId, feelVariables] + () => new SemanticTokensProvider(feelIdentifiers, expressionId, setCurrentParsedExpression), + [expressionId, feelIdentifiers] ); const getLastValidSymbolAtPosition = useCallback((currentParsedExpression: ParsedExpression, position: number) => { let lastValidSymbol; - for (let i = 0; i < currentParsedExpression.feelVariables.length; i++) { - const feelVariable = currentParsedExpression.feelVariables[i]; + for (let i = 0; i < currentParsedExpression.feelIdentifiedSymbols.length; i++) { + const feelVariable = currentParsedExpression.feelIdentifiedSymbols[i]; if (feelVariable.startIndex < position && position <= feelVariable.startIndex + feelVariable.length) { lastValidSymbol = feelVariable; const target = i - 1; if ( - target < currentParsedExpression.feelVariables.length && + target < currentParsedExpression.feelIdentifiedSymbols.length && 0 <= target && lastValidSymbol.feelSymbolNature === FeelSyntacticSymbolNature.Unknown ) { - lastValidSymbol = currentParsedExpression.feelVariables[target]; + lastValidSymbol = currentParsedExpression.feelIdentifiedSymbols[target]; } break; } @@ -125,7 +125,7 @@ export const FeelInput = React.forwardRef( }, []); const getSymbolAtPosition = useCallback((currentParsedExpression: ParsedExpression, position: number) => { - for (const feelVariable of currentParsedExpression.feelVariables) { + for (const feelVariable of currentParsedExpression.feelIdentifiedSymbols) { if (feelVariable.startIndex < position && position <= feelVariable.startIndex + feelVariable.length) { return feelVariable; } @@ -285,7 +285,7 @@ export const FeelInput = React.forwardRef( return () => { disposable.dispose(); }; - }, [enabled, expressionId, feelVariables, semanticTokensProvider]); + }, [enabled, expressionId, feelIdentifiers, semanticTokensProvider]); const config = useMemo(() => { return feelDefaultConfig(options); diff --git a/packages/feel-input-component/src/semanticTokensProvider.ts b/packages/feel-input-component/src/semanticTokensProvider.ts index 9b77c871119..d05b6727e5b 100644 --- a/packages/feel-input-component/src/semanticTokensProvider.ts +++ b/packages/feel-input-component/src/semanticTokensProvider.ts @@ -19,11 +19,11 @@ import * as Monaco from "@kie-tools-core/monaco-editor"; import { Element } from "./themes/Element"; -import { FeelSyntacticSymbolNature, FeelVariables, ParsedExpression } from "@kie-tools/dmn-feel-antlr4-parser"; +import { FeelSyntacticSymbolNature, FeelIdentifiers, ParsedExpression } from "@kie-tools/dmn-feel-antlr4-parser"; export class SemanticTokensProvider implements Monaco.languages.DocumentSemanticTokensProvider { constructor( - private feelVariables: FeelVariables | undefined, + private feelIdentifiers: FeelIdentifiers | undefined, private expressionId: string | undefined, private setCurrentParsedExpression: ( value: ((prevState: ParsedExpression) => ParsedExpression) | ParsedExpression @@ -46,13 +46,16 @@ export class SemanticTokensProvider implements Monaco.languages.DocumentSemantic ): Monaco.languages.ProviderResult { const tokenTypes = new Array(); - if (!this.feelVariables) { + if (!this.feelIdentifiers) { return; } const text = model.getValue().replaceAll("\r\n", "\n"); const contentByLines = model.getLinesContent(); - const parsedExpression = this.feelVariables.parser.parse(this.expressionId ?? "", text); + const parsedExpression = this.feelIdentifiers.parse({ + identifierContextUuid: this.expressionId ?? "", + expression: text, + }); // This is to autocomplete, so we don't need to parse it again. this.setCurrentParsedExpression(parsedExpression); @@ -71,48 +74,50 @@ export class SemanticTokensProvider implements Monaco.languages.DocumentSemantic // // The code bellow does this calculation fixing the startIndex solved by the parser to the // startIndex we need here, relative to the LINE where the variable is, not to the full expression. - for (const variable of parsedExpression.feelVariables) { + for (const feelIdentifiedSymbol of parsedExpression.feelIdentifiedSymbols) { let lineOffset = 0; - for (let i = 0; i < variable.startLine; i++) { + for (let i = 0; i < feelIdentifiedSymbol.startLine; i++) { lineOffset += contentByLines[i].length + 1; // +1 = is the line break } - variable.startIndex -= lineOffset; + feelIdentifiedSymbol.startIndex -= lineOffset; } let startOfPreviousVariable = 0; let previousLine = 0; - for (const variable of parsedExpression.feelVariables) { - if (previousLine != variable.startLine) { + for (const feelIdentifiedSymbol of parsedExpression.feelIdentifiedSymbols) { + if (previousLine != feelIdentifiedSymbol.startLine) { startOfPreviousVariable = 0; } // It is a variable that it is NOT split in multiple-lines - if (variable.startLine === variable.endLine) { + if (feelIdentifiedSymbol.startLine === feelIdentifiedSymbol.endLine) { tokenTypes.push( - variable.startLine - previousLine, // lineIndex = relative to the PREVIOUS line - variable.startIndex - startOfPreviousVariable, // columnIndex = relative to the start of the PREVIOUS token NOT to the start of the line - variable.length, - this.getTokenTypeIndex(variable.feelSymbolNature), + feelIdentifiedSymbol.startLine - previousLine, // lineIndex = relative to the PREVIOUS line + feelIdentifiedSymbol.startIndex - startOfPreviousVariable, // columnIndex = relative to the start of the PREVIOUS token NOT to the start of the line + feelIdentifiedSymbol.length, + this.getTokenTypeIndex(feelIdentifiedSymbol.feelSymbolNature), 0 // token modifier = not used so we keep it 0 ); - previousLine = variable.startLine; - startOfPreviousVariable = variable.startIndex; + previousLine = feelIdentifiedSymbol.startLine; + startOfPreviousVariable = feelIdentifiedSymbol.startIndex; } else { // It is a MULTILINE variable. // We colorize the first line of the variable and then other lines. tokenTypes.push( - variable.startLine - previousLine, - variable.startIndex - startOfPreviousVariable, - contentByLines[variable.startLine - previousLine].length - variable.startIndex, - this.getTokenTypeIndex(variable.feelSymbolNature), + feelIdentifiedSymbol.startLine - previousLine, + feelIdentifiedSymbol.startIndex - startOfPreviousVariable, + contentByLines[feelIdentifiedSymbol.startLine - previousLine].length - feelIdentifiedSymbol.startIndex, + this.getTokenTypeIndex(feelIdentifiedSymbol.feelSymbolNature), 0 ); let remainingChars = - variable.length - 1 - (contentByLines[variable.startLine - previousLine].length - variable.startIndex); // -1 = line break - const remainingLines = variable.endLine - variable.startLine; - let currentLine = variable.startLine + 1; + feelIdentifiedSymbol.length - + 1 - + (contentByLines[feelIdentifiedSymbol.startLine - previousLine].length - feelIdentifiedSymbol.startIndex); // -1 = line break + const remainingLines = feelIdentifiedSymbol.endLine - feelIdentifiedSymbol.startLine; + let currentLine = feelIdentifiedSymbol.startLine + 1; // We colorize the remaining lines here. It can be one of the following cases: // 1. The entire line is part of the variable, colorize the entire line; @@ -124,7 +129,7 @@ export class SemanticTokensProvider implements Monaco.languages.DocumentSemantic toColorize = contentByLines[currentLine].length; } - tokenTypes.push(1, 0, toColorize, this.getTokenTypeIndex(variable.feelSymbolNature), 0); + tokenTypes.push(1, 0, toColorize, this.getTokenTypeIndex(feelIdentifiedSymbol.feelSymbolNature), 0); remainingChars -= toColorize + 1; currentLine++; @@ -135,7 +140,7 @@ export class SemanticTokensProvider implements Monaco.languages.DocumentSemantic // the line. So, here, we're setting it to 0 because the last painted "part of the variable" // was painted at position 0 of the line. startOfPreviousVariable = 0; - previousLine = variable.endLine; + previousLine = feelIdentifiedSymbol.endLine; } } diff --git a/packages/feel-input-component/tests/semanticTokensProvider.test.ts b/packages/feel-input-component/tests/semanticTokensProvider.test.ts index 44d2261587c..dbc3ca059dc 100644 --- a/packages/feel-input-component/tests/semanticTokensProvider.test.ts +++ b/packages/feel-input-component/tests/semanticTokensProvider.test.ts @@ -18,7 +18,7 @@ */ import { SemanticTokensProvider } from "@kie-tools/feel-input-component/dist/semanticTokensProvider"; -import { BuiltInTypes, DmnDefinitions, FeelVariables } from "@kie-tools/dmn-feel-antlr4-parser"; +import { BuiltInTypes, DmnDefinitions, FeelIdentifiers } from "@kie-tools/dmn-feel-antlr4-parser"; import * as Monaco from "@kie-tools-core/monaco-editor"; import { Element } from "@kie-tools/feel-input-component/dist/themes/Element"; @@ -217,7 +217,10 @@ ThatShouldFailWhenBreakLine`, }, }); - const feelVariables = new FeelVariables(dmnDefinitions, new Map()); + const feelVariables = new FeelIdentifiers({ + _readonly_dmnDefinitions: dmnDefinitions, + _readonly_externalDefinitions: new Map(), + }); const semanticTokensProvider = new SemanticTokensProvider(feelVariables, id, () => {}); const semanticMonacoTokens = await semanticTokensProvider.provideDocumentSemanticTokens( @@ -274,7 +277,10 @@ ThatShouldFailWhenBreakLine`, }, }); - const feelVariables = new FeelVariables(model, new Map()); + const feelVariables = new FeelIdentifiers({ + _readonly_dmnDefinitions: model, + _readonly_externalDefinitions: new Map(), + }); const semanticTokensProvider = new SemanticTokensProvider(feelVariables, id, () => {}); const semanticMonacoTokens = await semanticTokensProvider.provideDocumentSemanticTokens( @@ -303,10 +309,10 @@ ThatShouldFailWhenBreakLine`, const id = "_AEC3EEB0-8436-4767-A214-20FF5E5CB7BE"; const modelMock = createModelMockForExpression(expression); - const feelVariables = new FeelVariables( - localModel.definitions, - new Map([[includedModel.definitions["@_namespace"] ?? "", includedModel]]) - ); + const feelVariables = new FeelIdentifiers({ + _readonly_dmnDefinitions: localModel.definitions, + _readonly_externalDefinitions: new Map([[includedModel.definitions["@_namespace"] ?? "", includedModel]]), + }); const semanticTokensProvider = new SemanticTokensProvider(feelVariables, id, () => {}); @@ -339,10 +345,10 @@ ThatShouldFailWhenBreakLine`, const id = "_206131ED-0B81-4013-980A-4BB2539A53D0"; const modelMock = createModelMockForExpression(expression); - const feelVariables = new FeelVariables( - localModel.definitions, - new Map([[includedModel.definitions["@_namespace"] ?? "", includedModel]]) - ); + const feelVariables = new FeelIdentifiers({ + _readonly_dmnDefinitions: localModel.definitions, + _readonly_externalDefinitions: new Map([[includedModel.definitions["@_namespace"] ?? "", includedModel]]), + }); const semanticTokensProvider = new SemanticTokensProvider(feelVariables, id, () => {}); @@ -381,10 +387,10 @@ ThatShouldFailWhenBreakLine`, const id = "_18832484-9481-49BC-BD40-927CB9872C6B"; const modelMock = createModelMockForExpression(expression); - const feelVariables = new FeelVariables( - localModel.definitions, - new Map([[includedModel.definitions["@_namespace"] ?? "", includedModel]]) - ); + const feelVariables = new FeelIdentifiers({ + _readonly_dmnDefinitions: localModel.definitions, + _readonly_externalDefinitions: new Map([[includedModel.definitions["@_namespace"] ?? "", includedModel]]), + }); const semanticTokensProvider = new SemanticTokensProvider(feelVariables, id, () => {}); diff --git a/packages/stunner-editors-dmn-loader/src/index.tsx b/packages/stunner-editors-dmn-loader/src/index.tsx index aed77b103e0..432ea7a455c 100644 --- a/packages/stunner-editors-dmn-loader/src/index.tsx +++ b/packages/stunner-editors-dmn-loader/src/index.tsx @@ -18,6 +18,7 @@ */ import { BoxedExpressionEditor } from "@kie-tools/boxed-expression-component/dist/BoxedExpressionEditor"; +import { OnExpressionChange } from "@kie-tools/boxed-expression-component/dist/BoxedExpressionEditorContext"; import { GWTLayerService, ImportJavaClasses, @@ -35,11 +36,12 @@ import { Normalized, PmmlDocument, } from "@kie-tools/boxed-expression-component/dist/api"; -import { FeelVariables } from "@kie-tools/dmn-feel-antlr4-parser"; + import { GwtExpressionDefinition } from "./types"; import { dmnExpressionToGwtExpression, gwtExpressionToDmnExpression, gwtLogicType } from "./mapping"; import { DmnLatestModel, getMarshaller } from "@kie-tools/dmn-marshaller"; import { updateExpression } from "./tmpDuplicateCode__updateExpression"; +import { FeelIdentifiers } from "@kie-tools/dmn-feel-antlr4-parser"; export interface BoxedExpressionEditorWrapperProps { /** Identifier of the decision node, where the expression will be hold */ @@ -113,21 +115,18 @@ const BoxedExpressionEditorWrapper: React.FunctionComponent>) => { - setExpressionWrapper((prevState) => { - return { - source: "react", - expression: - typeof newExpressionAction === "function" - ? newExpressionAction(prevState.expression as any) - : newExpressionAction, - widthsById: prevState.widthsById, - }; - }); - }, - [] - ); + const setExpressionNotifyingUserAction = useCallback((onExpressionChange) => { + setExpressionWrapper((prevState) => { + return { + source: "react", + expression: + typeof onExpressionChange.setExpressionAction === "function" + ? onExpressionChange.setExpressionAction(prevState.expression as any) + : onExpressionChange.setExpressionAction, + widthsById: prevState.widthsById, + }; + }); + }, []); const setWidthsByIdNotifyingUserAction = useCallback( (newWidthsByIdAction: React.SetStateAction>) => { @@ -193,7 +192,11 @@ const BoxedExpressionEditorWrapper: React.FunctionComponent new FeelVariables(modelWhenRendered.current!.definitions, new Map()); + return () => + new FeelIdentifiers({ + _readonly_dmnDefinitions: modelWhenRendered.current!.definitions, + _readonly_externalDefinitions: new Map(), + }); }, []); // END (feelVariables) @@ -208,7 +211,7 @@ const BoxedExpressionEditorWrapper: React.FunctionComponent Date: Thu, 21 Nov 2024 11:23:13 -0300 Subject: [PATCH 02/12] kie-issues#208: Renaming any "NamedElement" on the DMN Editor should update all references to the old name --- .../dmn-editor/src/dataTypes/DataTypeName.tsx | 93 +++++++++++++++++-- .../src/mutations/renameItemDefinition.ts | 18 ++-- .../src/refactor/IdentifiersRefactor.ts | 4 +- .../refactor/IdentifiersRefactor.test.ts | 2 - 4 files changed, 97 insertions(+), 20 deletions(-) diff --git a/packages/dmn-editor/src/dataTypes/DataTypeName.tsx b/packages/dmn-editor/src/dataTypes/DataTypeName.tsx index 832ca2fe33b..651c8aea668 100644 --- a/packages/dmn-editor/src/dataTypes/DataTypeName.tsx +++ b/packages/dmn-editor/src/dataTypes/DataTypeName.tsx @@ -18,8 +18,11 @@ */ import * as React from "react"; -import { useCallback, useMemo } from "react"; -import { DMN15__tItemDefinition } from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types"; +import { useCallback, useMemo, useState } from "react"; +import { + DMN15__tDefinitions, + DMN15__tItemDefinition, +} from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types"; import { Normalized } from "@kie-tools/dmn-marshaller/dist/normalization/normalize"; import { Flex } from "@patternfly/react-core/dist/js/layouts/Flex"; import { EditableNodeLabel, useEditableNodeLabel } from "../diagram/nodes/EditableNodeLabel"; @@ -33,6 +36,11 @@ import { useExternalModels } from "../includedModels/DmnEditorDependenciesContex import { State } from "../store/Store"; import { DmnBuiltInDataType } from "@kie-tools/boxed-expression-component/dist/api"; import { DmnLatestModel } from "@kie-tools/dmn-marshaller"; +import { + isIdentifierReferencedInSomeExpression, + RefactorConfirmationDialog, +} from "../refactor/RefactorConfirmationDialog"; +import { DataTypeIndex } from "./DataTypes"; export function DataTypeName({ isReadOnly, @@ -75,7 +83,7 @@ export function DataTypeName({ (s) => s.computed(s).getDirectlyIncludedExternalModelsByNamespace(externalModelsByNamespace).dmns ); - const externalModelsByNamespaceMap = useMemo(() => { + const externalDmnModelsByNamespaceMap = useMemo(() => { const externalModels = new Map>(); for (const [key, externalDmn] of externalDmnsByNamespace) { @@ -84,29 +92,94 @@ export function DataTypeName({ return externalModels; }, [externalDmnsByNamespace]); + const _shouldCommitOnBlur = shouldCommitOnBlur ?? true; // Defaults to true + + const [isRefactorModalOpen, setIsRefactorModalOpen] = useState(false); + const [newName, setNewName] = useState(""); + const identifierId = useMemo(() => itemDefinition["@_id"], [itemDefinition]); + const oldName = useMemo(() => itemDefinition["@_name"], [itemDefinition]); + + const applyRename = useCallback( + (args: { + definitions: Normalized; + newName: string; + shouldRenameReferencedExpressions: boolean; + allDataTypesById: DataTypeIndex; + }) => { + renameItemDefinition({ + ...args, + itemDefinitionId: itemDefinition["@_id"]!, + externalDmnModelsByNamespaceMap, + }); + }, + [externalDmnModelsByNamespaceMap, itemDefinition] + ); + const onRenamed = useCallback( (newName) => { - if (isReadOnly) { + if (isReadOnly || newName === oldName) { return; } dmnEditorStoreApi.setState((state) => { - renameItemDefinition({ + if ( + isIdentifierReferencedInSomeExpression({ + identifierUuid: identifierId, + dmnDefinitions: state.dmn.model.definitions, + externalDmnModelsByNamespaceMap, + }) + ) { + setNewName(newName); + setIsRefactorModalOpen(true); + } else { + applyRename({ + definitions: state.dmn.model.definitions, + newName, + shouldRenameReferencedExpressions: false, + allDataTypesById: state.computed(state).getDataTypes(externalModelsByNamespace).allDataTypesById, + }); + } + }); + }, + [ + applyRename, + dmnEditorStoreApi, + externalDmnModelsByNamespaceMap, + externalModelsByNamespace, + identifierId, + isReadOnly, + oldName, + ] + ); + + const confirmRename = useCallback( + (shouldRenameReferencedExpressions: boolean) => { + setIsRefactorModalOpen(false); + dmnEditorStoreApi.setState((state) => { + applyRename({ definitions: state.dmn.model.definitions, newName, - itemDefinitionId: itemDefinition["@_id"]!, + shouldRenameReferencedExpressions, allDataTypesById: state.computed(state).getDataTypes(externalModelsByNamespace).allDataTypesById, - externalModelsByNamespaceMap, }); }); }, - [dmnEditorStoreApi, externalModelsByNamespace, externalModelsByNamespaceMap, isReadOnly, itemDefinition] + [applyRename, dmnEditorStoreApi, externalModelsByNamespace, newName] ); - const _shouldCommitOnBlur = shouldCommitOnBlur ?? true; // Defaults to true - return ( <> + { + confirmRename(true); + }} + onConfirmRenameOnly={() => { + confirmRename(false); + }} + isRefactorModalOpen={isRefactorModalOpen} + fromName={oldName} + toName={newName} + /> {editMode === "hover" && ( ; newName: string; itemDefinitionId: string; allDataTypesById: DataTypeIndex; - externalModelsByNamespaceMap: Map>; + externalDmnModelsByNamespaceMap: Map>; + shouldRenameReferencedExpressions: boolean; }) { const dataType = allDataTypesById.get(itemDefinitionId); if (!dataType) { @@ -90,12 +92,14 @@ export function renameItemDefinition({ // Not top-level.. meaning that we need to update FEEL expressions referencing it else { - const identifiersRefactor = new IdentifiersRefactor({ - writeableDmnDefinitions: definitions, - _readonly_externalDmnModelsByNamespaceMap: externalModelsByNamespaceMap, - }); + if (shouldRenameReferencedExpressions) { + const identifiersRefactor = new IdentifiersRefactor({ + writeableDmnDefinitions: definitions, + _readonly_externalDmnModelsByNamespaceMap: externalDmnModelsByNamespaceMap, + }); - identifiersRefactor.rename({ identifierUuid: itemDefinitionId, newName: trimmedNewName }); + identifiersRefactor.rename({ identifierUuid: itemDefinitionId, newName: trimmedNewName }); + } } itemDefinition["@_name"] = trimmedNewName; diff --git a/packages/dmn-language-service/src/refactor/IdentifiersRefactor.ts b/packages/dmn-language-service/src/refactor/IdentifiersRefactor.ts index fe3f2cab231..173045984e0 100644 --- a/packages/dmn-language-service/src/refactor/IdentifiersRefactor.ts +++ b/packages/dmn-language-service/src/refactor/IdentifiersRefactor.ts @@ -127,7 +127,9 @@ export class IdentifiersRefactor { public getExpressionsThatUseTheIdentifier(identifierId: string) { const identifierContext = this.repository.identifiers.get(identifierId); if (!identifierContext) { - return []; + return ( + this.repository.dataTypeIndexedByUuid.get(identifierId)?.source.expressionsThatUseTheIdentifier.values() ?? [] + ); } return identifierContext.identifier.expressionsThatUseTheIdentifier.values(); diff --git a/packages/dmn-language-service/tests/refactor/IdentifiersRefactor.test.ts b/packages/dmn-language-service/tests/refactor/IdentifiersRefactor.test.ts index 39946b867f2..5dcd227452e 100644 --- a/packages/dmn-language-service/tests/refactor/IdentifiersRefactor.test.ts +++ b/packages/dmn-language-service/tests/refactor/IdentifiersRefactor.test.ts @@ -77,7 +77,6 @@ describe("Refactor renamed identifiers", () => { newName: "MON", }); - // id="_4a4d01be-fe97-49a2-8c4c-3a49ff27968d" name="Tax" // Rename the "Tax" property of "Monthly" data type to "The Tax" identifiersRefactor.rename({ identifierUuid: "_4a4d01be-fe97-49a2-8c4c-3a49ff27968d", @@ -193,7 +192,6 @@ describe("Refactor renamed identifiers", () => { ).toEqual("x > 2"); }); - // test("rename decision service element - should update referenced expressions", async () => {}); test("rename included model - should update referenced expressions", async () => { const identifiersRefactor = new IdentifiersRefactor({ writeableDmnDefinitions: getDefinitions(includeMathModelDmn()), From 2b0a5b34545bffc1b77fc0965af3b41a1e71e9ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Jos=C3=A9=20dos=20Santos?= Date: Mon, 25 Nov 2024 10:23:47 -0300 Subject: [PATCH 03/12] Fix include --- .../dmn-editor/src/refactor/RefactorConfirmationDialog.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/dmn-editor/src/refactor/RefactorConfirmationDialog.tsx b/packages/dmn-editor/src/refactor/RefactorConfirmationDialog.tsx index 0fcd63db594..b769ad56364 100644 --- a/packages/dmn-editor/src/refactor/RefactorConfirmationDialog.tsx +++ b/packages/dmn-editor/src/refactor/RefactorConfirmationDialog.tsx @@ -22,8 +22,8 @@ import { Button, ButtonVariant } from "@patternfly/react-core/dist/js/components import { Normalized } from "@kie-tools/dmn-marshaller/dist/normalization/normalize"; import * as React from "react"; import { DMN15__tDefinitions } from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types"; -import { IdentifiersRefactor } from "../../../dmn-language-service"; -import { DmnLatestModel } from "../../../dmn-marshaller"; +import { IdentifiersRefactor } from "@kie-tools/dmn-language-service"; +import { DmnLatestModel } from "@kie-tools/dmn-marshaller/dist"; export function RefactorConfirmationDialog({ onConfirmExpressionRefactor, From 260d50423e67aa92decb48941138b76781d5fb55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Jos=C3=A9=20dos=20Santos?= Date: Tue, 26 Nov 2024 17:16:47 -0300 Subject: [PATCH 04/12] - Fixes issue with Decision Tables - Fixes includes to point to src - Some refactor to reduce nesting --- .../stories/boxedExpressionStoriesWrapper.tsx | 2 +- .../stories/dev/WebApp.stories.tsx | 9 +--- .../stories/dev/DevWebApp.stories.tsx | 2 +- .../stories/dev/availableModelsToInclude.ts | 4 +- .../stories/dmnEditorStoriesWrapper.tsx | 8 +--- .../LoanPreQualification.stories.tsx | 2 +- .../src/parser/Expression.ts | 6 +-- .../src/parser/IdentifiersRepository.ts | 44 ++++++++----------- 8 files changed, 29 insertions(+), 48 deletions(-) diff --git a/packages/boxed-expression-component/stories/boxedExpressionStoriesWrapper.tsx b/packages/boxed-expression-component/stories/boxedExpressionStoriesWrapper.tsx index d154bc686b5..4a553ce91a1 100644 --- a/packages/boxed-expression-component/stories/boxedExpressionStoriesWrapper.tsx +++ b/packages/boxed-expression-component/stories/boxedExpressionStoriesWrapper.tsx @@ -24,7 +24,7 @@ import { BoxedExpressionEditor, BoxedExpressionEditorProps } from "../src/BoxedE import { BeeGwtService, BoxedExpression, DmnBuiltInDataType, generateUuid, Normalized } from "../src/api"; import { DEFAULT_EXPRESSION_VARIABLE_NAME } from "../src/expressionVariable/ExpressionVariableMenu"; import { getDefaultBoxedExpressionForStories } from "./getDefaultBoxedExpressionForStories"; -import { OnExpressionChange } from "@kie-tools/boxed-expression-component/dist/BoxedExpressionEditorContext"; +import { OnExpressionChange } from "../src/BoxedExpressionEditorContext"; export const pmmlDocuments = [ { diff --git a/packages/boxed-expression-component/stories/dev/WebApp.stories.tsx b/packages/boxed-expression-component/stories/dev/WebApp.stories.tsx index bbadd2aef60..16f684595a0 100644 --- a/packages/boxed-expression-component/stories/dev/WebApp.stories.tsx +++ b/packages/boxed-expression-component/stories/dev/WebApp.stories.tsx @@ -19,12 +19,7 @@ import * as React from "react"; import { useCallback, useEffect, useState } from "react"; -import { - BeeGwtService, - BoxedExpression, - DmnBuiltInDataType, - Normalized, -} from "@kie-tools/boxed-expression-component/dist/api"; +import { BeeGwtService, BoxedExpression, DmnBuiltInDataType, Normalized } from "../../src/api"; import { getDefaultBoxedExpressionForDevWebapp } from "./getDefaultBoxedExpressionForDevWebapp"; import type { Meta, StoryObj } from "@storybook/react"; import { BoxedExpressionEditorStory, BoxedExpressionEditorStoryArgs } from "../boxedExpressionStoriesWrapper"; @@ -39,7 +34,7 @@ import { postBureauAffordabilityExpression, postBureauAffordabilityWidthsById, } from "../useCases/LoanOriginations/RoutingDecisionService/PostBureauAffordability/PostBureauAffordability.stories"; -import { OnExpressionChange } from "@kie-tools/boxed-expression-component/dist/BoxedExpressionEditorContext"; +import { OnExpressionChange } from "../../src/BoxedExpressionEditorContext"; /** * Constants copied from tests to fix debugger diff --git a/packages/dmn-editor/stories/dev/DevWebApp.stories.tsx b/packages/dmn-editor/stories/dev/DevWebApp.stories.tsx index 42d8df1937a..3d9922f2118 100644 --- a/packages/dmn-editor/stories/dev/DevWebApp.stories.tsx +++ b/packages/dmn-editor/stories/dev/DevWebApp.stories.tsx @@ -36,7 +36,7 @@ import { OnRequestExternalModelByPath, OnRequestExternalModelsAvailableToInclude, OnRequestToJumpToPath, -} from "@kie-tools/dmn-editor/dist/DmnEditor"; +} from "../../src/DmnEditor"; const initialModel = generateEmptyDmn15(); diff --git a/packages/dmn-editor/stories/dev/availableModelsToInclude.ts b/packages/dmn-editor/stories/dev/availableModelsToInclude.ts index f1b4d3fc4c8..bca1dd1eb59 100644 --- a/packages/dmn-editor/stories/dev/availableModelsToInclude.ts +++ b/packages/dmn-editor/stories/dev/availableModelsToInclude.ts @@ -20,8 +20,8 @@ import { getMarshaller } from "@kie-tools/dmn-marshaller"; import { normalize } from "@kie-tools/dmn-marshaller/dist/normalization/normalize"; import { XML2PMML } from "@kie-tools/pmml-editor-marshaller"; -import * as DmnEditor from "@kie-tools/dmn-editor/dist/DmnEditor"; -import { getPmmlNamespace } from "@kie-tools/dmn-editor/dist/pmml/pmml"; +import * as DmnEditor from "../../src/DmnEditor"; +import { getPmmlNamespace } from "../../src/pmml/pmml"; import { sumBkm, sumDiffDs, testTreePmml } from "./externalModels"; export const sumBkmModel = normalize(getMarshaller(sumBkm, { upgradeTo: "latest" }).parser.parse()); diff --git a/packages/dmn-editor/stories/dmnEditorStoriesWrapper.tsx b/packages/dmn-editor/stories/dmnEditorStoriesWrapper.tsx index b70303e5091..25191c97e9e 100644 --- a/packages/dmn-editor/stories/dmnEditorStoriesWrapper.tsx +++ b/packages/dmn-editor/stories/dmnEditorStoriesWrapper.tsx @@ -20,13 +20,7 @@ import * as React from "react"; import { useCallback, useState, useRef, useMemo, useEffect } from "react"; import { useArgs } from "@storybook/preview-api"; -import { - DmnEditor, - DmnEditorProps, - DmnEditorRef, - EvaluationResults, - ValidationMessages, -} from "@kie-tools/dmn-editor/dist/DmnEditor"; +import { DmnEditor, DmnEditorProps, DmnEditorRef, EvaluationResults, ValidationMessages } from "../src/DmnEditor"; import { DmnLatestModel, getMarshaller } from "@kie-tools/dmn-marshaller"; import { normalize } from "@kie-tools/dmn-marshaller/dist/normalization/normalize"; import { diff } from "deep-object-diff"; diff --git a/packages/dmn-editor/stories/useCases/loanPreQualification/LoanPreQualification.stories.tsx b/packages/dmn-editor/stories/useCases/loanPreQualification/LoanPreQualification.stories.tsx index 9917fb87362..131be55e62a 100644 --- a/packages/dmn-editor/stories/useCases/loanPreQualification/LoanPreQualification.stories.tsx +++ b/packages/dmn-editor/stories/useCases/loanPreQualification/LoanPreQualification.stories.tsx @@ -21,7 +21,7 @@ import * as React from "react"; import type { Meta, StoryObj } from "@storybook/react"; import { getMarshaller } from "@kie-tools/dmn-marshaller"; import { Empty } from "../../misc/empty/Empty.stories"; -import { DmnEditor, DmnEditorProps } from "@kie-tools/dmn-editor/dist/DmnEditor"; +import { DmnEditor, DmnEditorProps } from "../../../src/DmnEditor"; import { StorybookDmnEditorProps } from "../../dmnEditorStoriesWrapper"; export const loanPreQualificationDmn = ` diff --git a/packages/dmn-feel-antlr4-parser/src/parser/Expression.ts b/packages/dmn-feel-antlr4-parser/src/parser/Expression.ts index 3883f66934c..edce40687b5 100644 --- a/packages/dmn-feel-antlr4-parser/src/parser/Expression.ts +++ b/packages/dmn-feel-antlr4-parser/src/parser/Expression.ts @@ -19,15 +19,15 @@ import { Identifier } from "./Identifier"; import { FeelIdentifiedSymbol } from "./FeelIdentifiedSymbol"; -import { DmnLiteralExpression } from "./IdentifiersRepository"; +import { ExpressionSource } from "./IdentifiersRepository"; export class Expression { private readonly _uuid: string; private _fullExpression: string; private _identifiersOfTheExpression: Array; - private source: DmnLiteralExpression; + private source: ExpressionSource; - constructor(uuid: string, source: DmnLiteralExpression) { + constructor(uuid: string, source: ExpressionSource) { this._uuid = uuid; this._identifiersOfTheExpression = new Array(); this._fullExpression = source.text?.__$$text ?? ""; diff --git a/packages/dmn-feel-antlr4-parser/src/parser/IdentifiersRepository.ts b/packages/dmn-feel-antlr4-parser/src/parser/IdentifiersRepository.ts index 62975f44e48..c37d34186f5 100644 --- a/packages/dmn-feel-antlr4-parser/src/parser/IdentifiersRepository.ts +++ b/packages/dmn-feel-antlr4-parser/src/parser/IdentifiersRepository.ts @@ -46,6 +46,7 @@ import { Expression } from "./Expression"; import { DmnLatestModel } from "@kie-tools/dmn-marshaller"; import { BuiltInTypes } from "./BuiltInTypes"; +export type ExpressionSource = { text?: { __$$text: string }; "@_id"?: string }; export type DmnLiteralExpression = { __$$element: "literalExpression" } & DMN15__tLiteralExpression; export type DmnInvocation = { __$$element: "invocation" } & DMN15__tInvocation; export type DmnDecisionTable = { __$$element: "decisionTable" } & DMN15__tDecisionTable; @@ -459,7 +460,7 @@ export class IdentifiersRepository { return properties; } - private addLiteralExpression(parent: IdentifierContext, element: DmnLiteralExpression) { + private addExpression(parent: IdentifierContext, element: ExpressionSource) { const id = element["@_id"] ?? ""; const expression = new Expression(id, element); this._expressionsIndexedByUuid.set(id, expression); @@ -707,7 +708,7 @@ export class IdentifiersRepository { ) { switch (expression.__$$element) { case "literalExpression": - this.addLiteralExpression(parent, expression); + this.addExpression(parent, expression); break; case "invocation": @@ -757,20 +758,15 @@ export class IdentifiersRepository { } } - private addDecisionTableEntryNode(parent: IdentifierContext, entryId: string) { + private addDecisionTableEntryNode(parent: IdentifierContext, entryNode: ExpressionSource) { const ruleInputElementNode = this.addIdentifier({ - uuid: entryId, + uuid: entryNode["@_id"] ?? "", name: "", kind: FeelSyntacticSymbolNature.LocalVariable, parentContext: parent, }); parent.children.set(ruleInputElementNode.uuid, ruleInputElementNode); - this.addIdentifier({ - uuid: ruleInputElementNode.uuid, - name: "", - kind: FeelSyntacticSymbolNature.LocalVariable, - parentContext: ruleInputElementNode, - }); + this.addExpression(parent, entryNode); } private addDecisionTable(parent: IdentifierContext, decisionTable: DmnDecisionTable) { @@ -783,12 +779,8 @@ export class IdentifiersRepository { parent.children.set(variableNode.uuid, variableNode); if (decisionTable.rule) { for (const ruleElement of decisionTable.rule) { - ruleElement.inputEntry?.forEach((ruleInputElement) => - this.addDecisionTableEntryNode(parent, ruleInputElement["@_id"] ?? "") - ); - ruleElement.outputEntry?.forEach((ruleOutputElement) => - this.addDecisionTableEntryNode(parent, ruleOutputElement["@_id"] ?? "") - ); + ruleElement.inputEntry?.forEach((inputElement) => this.addDecisionTableEntryNode(parent, inputElement)); + ruleElement.outputEntry?.forEach((outputElement) => this.addDecisionTableEntryNode(parent, outputElement)); } } this.addIdentifier({ @@ -835,16 +827,16 @@ export class IdentifiersRepository { } private loadImportedIdentifiers(dmnDefinitions: DmnDefinitions, externalDefinitions?: Map) { - if (dmnDefinitions.import && externalDefinitions) { - for (const dmnImport of dmnDefinitions.import) { - if (externalDefinitions.has(dmnImport["@_namespace"])) { - this.currentIdentifierNamePrefix = dmnImport["@_name"]; - this.currentUuidPrefix = dmnImport["@_namespace"]; - const externalDef = externalDefinitions.get(dmnImport["@_namespace"]); - if (externalDef) { - this.loadIdentifiers(externalDef.definitions); - } - } + if (!(dmnDefinitions.import && externalDefinitions)) { + return; + } + + for (const dmnImport of dmnDefinitions.import.filter((imp) => externalDefinitions.has(imp["@_namespace"]))) { + this.currentIdentifierNamePrefix = dmnImport["@_name"]; + this.currentUuidPrefix = dmnImport["@_namespace"]; + const externalDef = externalDefinitions.get(dmnImport["@_namespace"]); + if (externalDef) { + this.loadIdentifiers(externalDef.definitions); } } } From 329a7889813d79f453da49ff1d8e6b2a3bb30dd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Jos=C3=A9=20dos=20Santos?= Date: Thu, 28 Nov 2024 16:37:13 -0300 Subject: [PATCH 05/12] Adds cancel to Refactor Confirmation Dialog --- .../ExpressionVariableMenu.tsx | 7 ++++++- .../boxedExpressions/BoxedExpressionScreen.tsx | 5 +++++ .../dmn-editor/src/dataTypes/DataTypeName.tsx | 18 ++++++++++++++++-- .../src/diagram/nodes/EditableNodeLabel.tsx | 1 + .../dmn-editor/src/diagram/nodes/Nodes.tsx | 15 +++++++++++++++ .../src/propertiesPanel/BkmProperties.tsx | 9 ++++++++- .../DecisionTableOutputHeaderCell.tsx | 3 +++ .../src/propertiesPanel/DecisionProperties.tsx | 9 ++++++++- .../DecisionServiceProperties.tsx | 9 ++++++++- .../propertiesPanel/InputDataProperties.tsx | 9 ++++++++- .../KnowledgeSourceProperties.tsx | 9 ++++++++- .../refactor/RefactorConfirmationDialog.tsx | 7 +++++-- 12 files changed, 91 insertions(+), 10 deletions(-) diff --git a/packages/boxed-expression-component/src/expressionVariable/ExpressionVariableMenu.tsx b/packages/boxed-expression-component/src/expressionVariable/ExpressionVariableMenu.tsx index 47a0416b26e..6b35602b682 100644 --- a/packages/boxed-expression-component/src/expressionVariable/ExpressionVariableMenu.tsx +++ b/packages/boxed-expression-component/src/expressionVariable/ExpressionVariableMenu.tsx @@ -148,7 +148,12 @@ export function ExpressionVariableMenu({ } saveExpression(); popoverMenuRef?.current?.setIsVisible(false); - }, [saveExpression]); + // We reset the expression name to its default because the name change could be canceled outside. + // If we don't reset it, we will keep it an outdated name. + // If the change is confirmed, then the selectExpressionName will be updated to the new one in the next + // render of this component. + setExpressionName(selectedExpressionName); + }, [saveExpression, selectedExpressionName]); const onCancel = useCallback(() => { cancelEdit.current = true; diff --git a/packages/dmn-editor/src/boxedExpressions/BoxedExpressionScreen.tsx b/packages/dmn-editor/src/boxedExpressions/BoxedExpressionScreen.tsx index 61e4cf79a3f..9d457c189a2 100644 --- a/packages/dmn-editor/src/boxedExpressions/BoxedExpressionScreen.tsx +++ b/packages/dmn-editor/src/boxedExpressions/BoxedExpressionScreen.tsx @@ -505,6 +505,11 @@ export function BoxedExpressionScreen({ container }: { container: React.RefObjec isRefactorModalOpen={isRefactorModalOpen} fromName={variableChangedArgs?.nameChange?.from} toName={variableChangedArgs?.nameChange?.to} + onCancel={() => { + setIsRefactorModalOpen(false); + setVariableChangedArgs(undefined); + setNewExpression(undefined); + }} />
itemDefinition["@_id"], [itemDefinition]); const oldName = useMemo(() => itemDefinition["@_name"], [itemDefinition]); + const currentName = useMemo(() => { + if (editMode === "hover") { + return newName === "" ? feelQNameToDisplay.full : newName; + } else if (editMode === "double-click") { + return newName === "" ? itemDefinition["@_name"] : newName; + } else { + throw new Error(`Unknown edit mode in DataTypeName: ${editMode}`); + } + }, [editMode, feelQNameToDisplay.full, itemDefinition, newName]); + const applyRename = useCallback( (args: { definitions: Normalized; @@ -179,6 +189,10 @@ export function DataTypeName({ isRefactorModalOpen={isRefactorModalOpen} fromName={oldName} toName={newName} + onCancel={() => { + setNewName(""); + setIsRefactorModalOpen(false); + }} /> {editMode === "hover" && ( { + setIsRefactorModalOpen(false); + }} isRefactorModalOpen={isRefactorModalOpen} fromName={oldName} toName={newName} @@ -554,6 +557,9 @@ export const DecisionNode = React.memo( }); }); }} + onCancel={() => { + setIsRefactorModalOpen(false); + }} isRefactorModalOpen={isRefactorModalOpen} fromName={oldName} toName={newName} @@ -772,6 +778,9 @@ export const BkmNode = React.memo( }); }); }} + onCancel={() => { + setIsRefactorModalOpen(false); + }} isRefactorModalOpen={isRefactorModalOpen} fromName={oldName} toName={newName} @@ -971,6 +980,9 @@ export const KnowledgeSourceNode = React.memo( }); }); }} + onCancel={() => { + setIsRefactorModalOpen(false); + }} isRefactorModalOpen={isRefactorModalOpen} fromName={oldName} toName={newName} @@ -1347,6 +1359,9 @@ export const DecisionServiceNode = React.memo( }); }); }} + onCancel={() => { + setIsRefactorModalOpen(false); + }} isRefactorModalOpen={isRefactorModalOpen} fromName={oldName} toName={newName} diff --git a/packages/dmn-editor/src/propertiesPanel/BkmProperties.tsx b/packages/dmn-editor/src/propertiesPanel/BkmProperties.tsx index 07f415580d7..d8b249de903 100644 --- a/packages/dmn-editor/src/propertiesPanel/BkmProperties.tsx +++ b/packages/dmn-editor/src/propertiesPanel/BkmProperties.tsx @@ -68,6 +68,9 @@ export function BkmProperties({ const [newName, setNewName] = useState(""); const identifierId = useMemo(() => bkm["@_id"], [bkm]); const oldName = useMemo(() => bkm["@_label"] ?? bkm["@_name"], [bkm]); + const currentName = useMemo(() => { + return newName === "" ? oldName : newName; + }, [newName, oldName]); const applyRename = useCallback( (args: { @@ -134,6 +137,10 @@ export function BkmProperties({ }); }); }} + onCancel={() => { + setIsRefactorModalOpen(false); + setNewName(""); + }} isRefactorModalOpen={isRefactorModalOpen} fromName={oldName} toName={newName} @@ -143,7 +150,7 @@ export function BkmProperties({ enableAutoFocusing={false} isPlain={false} id={bkm["@_id"]!} - name={bkm["@_name"]} + name={currentName} isReadOnly={isReadOnly} shouldCommitOnBlur={true} className={"pf-c-form-control"} diff --git a/packages/dmn-editor/src/propertiesPanel/BoxedExpressionPropertiesPanelComponents/DecisionTableOutputHeaderCell.tsx b/packages/dmn-editor/src/propertiesPanel/BoxedExpressionPropertiesPanelComponents/DecisionTableOutputHeaderCell.tsx index 663366e9c08..2c20407c2fe 100644 --- a/packages/dmn-editor/src/propertiesPanel/BoxedExpressionPropertiesPanelComponents/DecisionTableOutputHeaderCell.tsx +++ b/packages/dmn-editor/src/propertiesPanel/BoxedExpressionPropertiesPanelComponents/DecisionTableOutputHeaderCell.tsx @@ -220,6 +220,9 @@ export function DecisionTableOutputHeaderCell(props: { }); }); }} + onCancel={() => { + setIsRefactorModalOpen(false); + }} isRefactorModalOpen={isRefactorModalOpen} fromName={oldName} toName={newName} diff --git a/packages/dmn-editor/src/propertiesPanel/DecisionProperties.tsx b/packages/dmn-editor/src/propertiesPanel/DecisionProperties.tsx index d71e41a339f..1f58f810df1 100644 --- a/packages/dmn-editor/src/propertiesPanel/DecisionProperties.tsx +++ b/packages/dmn-editor/src/propertiesPanel/DecisionProperties.tsx @@ -65,6 +65,9 @@ export function DecisionProperties({ const [newName, setNewName] = useState(""); const identifierId = useMemo(() => decision["@_id"], [decision]); const oldName = useMemo(() => decision["@_label"] ?? decision["@_name"], [decision]); + const currentName = useMemo(() => { + return newName === "" ? oldName : newName; + }, [newName, oldName]); const applyRename = useCallback( (args: { @@ -131,6 +134,10 @@ export function DecisionProperties({ }); }); }} + onCancel={() => { + setIsRefactorModalOpen(false); + setNewName(""); + }} isRefactorModalOpen={isRefactorModalOpen} fromName={oldName} toName={newName} @@ -140,7 +147,7 @@ export function DecisionProperties({ enableAutoFocusing={false} isPlain={false} id={decision["@_id"]!} - name={decision["@_name"]} + name={currentName} isReadOnly={isReadOnly} shouldCommitOnBlur={true} className={"pf-c-form-control"} diff --git a/packages/dmn-editor/src/propertiesPanel/DecisionServiceProperties.tsx b/packages/dmn-editor/src/propertiesPanel/DecisionServiceProperties.tsx index 6cdc23158b9..56d14916a56 100644 --- a/packages/dmn-editor/src/propertiesPanel/DecisionServiceProperties.tsx +++ b/packages/dmn-editor/src/propertiesPanel/DecisionServiceProperties.tsx @@ -124,6 +124,9 @@ export function DecisionServiceProperties({ const [newName, setNewName] = useState(""); const identifierId = useMemo(() => decisionService["@_id"], [decisionService]); const oldName = useMemo(() => decisionService["@_label"] ?? decisionService["@_name"], [decisionService]); + const currentName = useMemo(() => { + return newName === "" ? oldName : newName; + }, [newName, oldName]); const applyRename = useCallback( (args: { @@ -190,6 +193,10 @@ export function DecisionServiceProperties({ }); }); }} + onCancel={() => { + setIsRefactorModalOpen(false); + setNewName(""); + }} isRefactorModalOpen={isRefactorModalOpen} fromName={oldName} toName={newName} @@ -199,7 +206,7 @@ export function DecisionServiceProperties({ enableAutoFocusing={false} isPlain={false} id={decisionService["@_id"]!} - name={decisionService["@_name"]} + name={currentName} isReadOnly={isReadOnly} shouldCommitOnBlur={true} className={"pf-c-form-control"} diff --git a/packages/dmn-editor/src/propertiesPanel/InputDataProperties.tsx b/packages/dmn-editor/src/propertiesPanel/InputDataProperties.tsx index a19d86ac64b..b3123ac0321 100644 --- a/packages/dmn-editor/src/propertiesPanel/InputDataProperties.tsx +++ b/packages/dmn-editor/src/propertiesPanel/InputDataProperties.tsx @@ -65,6 +65,9 @@ export function InputDataProperties({ const [newName, setNewName] = useState(""); const identifierId = useMemo(() => inputData["@_id"], [inputData]); const oldName = useMemo(() => inputData["@_label"] ?? inputData["@_name"], [inputData]); + const currentName = useMemo(() => { + return newName === "" ? oldName : newName; + }, [newName, oldName]); const applyRename = useCallback( (args: { @@ -131,6 +134,10 @@ export function InputDataProperties({ }); }); }} + onCancel={() => { + setIsRefactorModalOpen(false); + setNewName(""); + }} isRefactorModalOpen={isRefactorModalOpen} fromName={oldName} toName={newName} @@ -140,7 +147,7 @@ export function InputDataProperties({ enableAutoFocusing={false} isPlain={false} id={inputData["@_id"]!} - name={inputData["@_name"]} + name={currentName} isReadOnly={isReadOnly} shouldCommitOnBlur={true} className={"pf-c-form-control"} diff --git a/packages/dmn-editor/src/propertiesPanel/KnowledgeSourceProperties.tsx b/packages/dmn-editor/src/propertiesPanel/KnowledgeSourceProperties.tsx index b561e069780..da207c7aefd 100644 --- a/packages/dmn-editor/src/propertiesPanel/KnowledgeSourceProperties.tsx +++ b/packages/dmn-editor/src/propertiesPanel/KnowledgeSourceProperties.tsx @@ -72,6 +72,9 @@ export function KnowledgeSourceProperties({ const [newName, setNewName] = useState(""); const identifierId = useMemo(() => knowledgeSource["@_id"], [knowledgeSource]); const oldName = useMemo(() => knowledgeSource["@_label"] ?? knowledgeSource["@_name"], [knowledgeSource]); + const currentName = useMemo(() => { + return newName === "" ? oldName : newName; + }, [newName, oldName]); const applyRename = useCallback( (args: { @@ -138,6 +141,10 @@ export function KnowledgeSourceProperties({ }); }); }} + onCancel={() => { + setIsRefactorModalOpen(false); + setNewName(""); + }} isRefactorModalOpen={isRefactorModalOpen} fromName={oldName} toName={newName} @@ -147,7 +154,7 @@ export function KnowledgeSourceProperties({ enableAutoFocusing={false} isPlain={false} id={knowledgeSource["@_id"]!} - name={knowledgeSource["@_name"]} + name={currentName} isReadOnly={isReadOnly} shouldCommitOnBlur={true} className={"pf-c-form-control"} diff --git a/packages/dmn-editor/src/refactor/RefactorConfirmationDialog.tsx b/packages/dmn-editor/src/refactor/RefactorConfirmationDialog.tsx index b769ad56364..8db1fb61a6a 100644 --- a/packages/dmn-editor/src/refactor/RefactorConfirmationDialog.tsx +++ b/packages/dmn-editor/src/refactor/RefactorConfirmationDialog.tsx @@ -28,12 +28,14 @@ import { DmnLatestModel } from "@kie-tools/dmn-marshaller/dist"; export function RefactorConfirmationDialog({ onConfirmExpressionRefactor, onConfirmRenameOnly, + onCancel, isRefactorModalOpen, fromName, toName, }: { onConfirmExpressionRefactor: () => void; onConfirmRenameOnly: () => void; + onCancel: () => void; isRefactorModalOpen: boolean; fromName: string | undefined; toName: string | undefined; @@ -43,12 +45,13 @@ export function RefactorConfirmationDialog({ aria-labelledby={"identifier-renamed"} variant={ModalVariant.small} isOpen={isRefactorModalOpen} - showClose={false} + showClose={true} + onClose={onCancel} actions={[ , - , ]} From 170fb1561a04b95cec3beeeaafc2e41c7f4d7802 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Jos=C3=A9=20dos=20Santos?= Date: Fri, 29 Nov 2024 11:12:31 -0300 Subject: [PATCH 06/12] Update packages/dmn-editor/src/refactor/RefactorConfirmationDialog.tsx Co-authored-by: Kbowers <92726146+kbowers-ibm@users.noreply.github.com> --- packages/dmn-editor/src/refactor/RefactorConfirmationDialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dmn-editor/src/refactor/RefactorConfirmationDialog.tsx b/packages/dmn-editor/src/refactor/RefactorConfirmationDialog.tsx index 8db1fb61a6a..a397a44c82d 100644 --- a/packages/dmn-editor/src/refactor/RefactorConfirmationDialog.tsx +++ b/packages/dmn-editor/src/refactor/RefactorConfirmationDialog.tsx @@ -62,7 +62,7 @@ export function RefactorConfirmationDialog({ This identifier is used in one or more expressions.

- Do you want also automatically update the expressions to the new name? + Would you like to automatically replace all instances of `{fromName ?? ""}` with `{toName ?? ""}`? ); } From b695986e13b050665227803211e8772c5ab41298 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Jos=C3=A9=20dos=20Santos?= Date: Fri, 29 Nov 2024 15:05:33 -0300 Subject: [PATCH 07/12] Change text --- .../dmn-editor/src/refactor/RefactorConfirmationDialog.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/dmn-editor/src/refactor/RefactorConfirmationDialog.tsx b/packages/dmn-editor/src/refactor/RefactorConfirmationDialog.tsx index a397a44c82d..d45dbb7b422 100644 --- a/packages/dmn-editor/src/refactor/RefactorConfirmationDialog.tsx +++ b/packages/dmn-editor/src/refactor/RefactorConfirmationDialog.tsx @@ -49,7 +49,7 @@ export function RefactorConfirmationDialog({ onClose={onCancel} actions={[ ,