/* * 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 * as React from "react"; import { useCallback, useEffect, useMemo, useState } from "react"; import * as ReactTable from "react-table"; import { Action, BeeTableContextMenuAllowedOperationsConditions, BeeTableHeaderVisibility, BeeTableOperation, BeeTableOperationConfig, BeeTableProps, BoxedContext, BoxedExpression, DmnBuiltInDataType, ExpressionChangedArgs, generateUuid, getNextAvailablePrefixedName, Normalized, } from "../../api"; import { useBoxedExpressionEditorI18n } from "../../i18n"; import { useNestedExpressionContainerWithNestedExpressions } from "../../resizing/Hooks"; import { NestedExpressionContainerContext } from "../../resizing/NestedExpressionContainerContext"; import { ResizerStopBehavior, ResizingWidth } from "../../resizing/ResizingWidthsContext"; import { CONTEXT_ENTRY_EXPRESSION_MIN_WIDTH, CONTEXT_ENTRY_VARIABLE_COLUMN_WIDTH_INDEX, CONTEXT_ENTRY_VARIABLE_MIN_WIDTH, CONTEXT_EXPRESSION_EXTRA_WIDTH, } from "../../resizing/WidthConstants"; import { useBeeTableCoordinates, useBeeTableSelectableCellRef } from "../../selection/BeeTableSelectionContext"; import { BeeTable, BeeTableColumnUpdate } from "../../table/BeeTable"; import { useBoxedExpressionEditor, useBoxedExpressionEditorDispatch } from "../../BoxedExpressionEditorContext"; import { DEFAULT_EXPRESSION_VARIABLE_NAME } from "../../expressionVariable/ExpressionVariableMenu"; import { ContextEntryExpressionCell } from "./ContextEntryExpressionCell"; import { ExpressionVariableCell, ExpressionWithVariable } from "../../expressionVariable/ExpressionVariableCell"; import { ContextResultExpressionCell } from "./ContextResultExpressionCell"; import { getExpressionTotalMinWidth } from "../../resizing/WidthMaths"; import { DMN_LATEST__tContextEntry } from "@kie-tools/dmn-marshaller"; import { findAllIdsDeep } from "../../ids/ids"; import "./ContextExpression.css"; export type ROWTYPE = ExpressionWithVariable & { index: number }; export function ContextExpression({ isNested, parentElementId, expression: contextExpression, }: { expression: Normalized; isNested: boolean; parentElementId: string; }) { const { i18n } = useBoxedExpressionEditorI18n(); const { setExpression, setWidthsById } = useBoxedExpressionEditorDispatch(); const { expressionHolderId, widthsById, isReadOnly } = useBoxedExpressionEditor(); const id = contextExpression["@_id"]!; const widths = useMemo(() => widthsById.get(id) ?? [], [id, widthsById]); const getEntryVariableWidth = useCallback( (widths: number[]) => widths?.[CONTEXT_ENTRY_VARIABLE_COLUMN_WIDTH_INDEX] ?? CONTEXT_ENTRY_VARIABLE_MIN_WIDTH, [] ); const entryVariableWidth = useMemo(() => getEntryVariableWidth(widths), [getEntryVariableWidth, widths]); const setEntryVariableWidth = useCallback( (newWidthAction: React.SetStateAction) => { setWidthsById(({ newMap }) => { const prev = newMap.get(id) ?? []; const newWidth = typeof newWidthAction === "function" ? newWidthAction(getEntryVariableWidth(prev)) : newWidthAction; if (newWidth) { const minSize = CONTEXT_ENTRY_VARIABLE_COLUMN_WIDTH_INDEX + 1; const newValues = [...prev]; newValues.push( ...Array(Math.max(0, minSize - newValues.length)).fill(CONTEXT_ENTRY_VARIABLE_MIN_WIDTH) ); newValues.splice(CONTEXT_ENTRY_VARIABLE_COLUMN_WIDTH_INDEX, 1, newWidth); newMap.set(id, newValues); } }); }, [getEntryVariableWidth, id, setWidthsById] ); const [entryVariableResizingWidth, setEntryVariableResizingWidth] = useState({ value: entryVariableWidth, isPivoting: false, }); const onColumnResizingWidthChange1 = useCallback((args: Map) => { const newResizingWidth = args.get(1); if (newResizingWidth) { setEntryVariableResizingWidth(newResizingWidth); } }, []); /// ////////////////////////////////////////////////////// /// ///////////// RESIZING WIDTHS //////////////////////// /// ////////////////////////////////////////////////////// const { nestedExpressionContainerValue, onColumnResizingWidthChange: onColumnResizingWidthChange2 } = useNestedExpressionContainerWithNestedExpressions( useMemo(() => { const nestedExpressions = (contextExpression.contextEntry ?? []).map((e) => e.expression); const maxNestedExpressionTotalMinWidth = Math.max( ...nestedExpressions.map((e) => getExpressionTotalMinWidth(0, e, widthsById)), CONTEXT_ENTRY_EXPRESSION_MIN_WIDTH ); return { nestedExpressions: nestedExpressions, fixedColumnActualWidth: entryVariableWidth, fixedColumnResizingWidth: entryVariableResizingWidth, fixedColumnMinWidth: CONTEXT_ENTRY_VARIABLE_MIN_WIDTH, nestedExpressionMinWidth: maxNestedExpressionTotalMinWidth, extraWidth: CONTEXT_EXPRESSION_EXTRA_WIDTH, expression: contextExpression, flexibleColumnIndex: 2, widthsById: widthsById, }; }, [contextExpression, entryVariableResizingWidth, entryVariableWidth, widthsById]) ); /// ////////////////////////////////////////////////////// const onColumnResizingWidthChange = useCallback( (args: Map) => { onColumnResizingWidthChange2?.(args); onColumnResizingWidthChange1(args); }, [onColumnResizingWidthChange1, onColumnResizingWidthChange2] ); const beeTableColumns = useMemo[]>(() => { return [ { accessor: expressionHolderId as any, // FIXME: https://github.com/apache/incubator-kie-issues/issues/169 label: contextExpression["@_label"] ?? DEFAULT_EXPRESSION_VARIABLE_NAME, isRowIndexColumn: false, dataType: contextExpression["@_typeRef"] ?? DmnBuiltInDataType.Undefined, width: undefined, columns: [ { accessor: "variable", label: i18n.contextExpression.variable, isRowIndexColumn: false, dataType: DmnBuiltInDataType.Undefined, isWidthPinned: true, minWidth: CONTEXT_ENTRY_VARIABLE_MIN_WIDTH, width: entryVariableWidth, setWidth: setEntryVariableWidth, }, { accessor: "expression", label: i18n.contextExpression.expression, dataType: DmnBuiltInDataType.Undefined, isRowIndexColumn: false, minWidth: CONTEXT_ENTRY_EXPRESSION_MIN_WIDTH, width: undefined, }, ], }, ]; }, [contextExpression, entryVariableWidth, expressionHolderId, setEntryVariableWidth, i18n.contextExpression]); const onColumnUpdates = useCallback( ([{ name, typeRef }]: BeeTableColumnUpdate[]) => { 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; }, expressionChangedArgs, }); }, [contextExpression, expressionHolderId, setExpression] ); const headerVisibility = useMemo(() => { return isNested ? BeeTableHeaderVisibility.None : BeeTableHeaderVisibility.SecondToLastLevel; }, [isNested]); const updateVariable = useCallback( (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] ); const cellComponentByColumnAccessor: BeeTableProps["cellComponentByColumnAccessor"] = useMemo(() => { return { variable: (props) => , expression: (props) => , }; }, [updateVariable]); const beeTableOperationConfig = useMemo(() => { return [ { group: i18n.contextEntry, items: [ { name: i18n.rowOperations.reset, type: BeeTableOperation.RowReset }, { name: i18n.rowOperations.insertAbove, type: BeeTableOperation.RowInsertAbove }, { name: i18n.rowOperations.insertBelow, type: BeeTableOperation.RowInsertBelow }, { name: i18n.insert, type: BeeTableOperation.RowInsertN }, { name: i18n.rowOperations.delete, type: BeeTableOperation.RowDelete }, ], }, { group: i18n.terms.selection.toUpperCase(), items: [ { name: i18n.terms.copy, type: BeeTableOperation.SelectionCopy }, { name: i18n.terms.cut, type: BeeTableOperation.SelectionCut }, { name: i18n.terms.paste, type: BeeTableOperation.SelectionPaste }, { name: i18n.terms.reset, type: BeeTableOperation.SelectionReset }, ], }, ]; }, [i18n]); const getRowKey = useCallback((row: ReactTable.Row) => { return row.id; }, []); const beeTableAdditionalRow = useMemo(() => { return [ , , ]; }, [contextExpression, parentElementId]); const getDefaultContextEntry = useCallback( (name?: string): Normalized => { const variableName = name || getNextAvailablePrefixedName( (contextExpression.contextEntry ?? []).map((e) => e.variable?.["@_name"] ?? ""), "ContextEntry" ); return { "@_id": generateUuid(), expression: undefined!, // SPEC DISCREPANCY: Starting without an expression gives users the ability to select the expression type. variable: { "@_id": generateUuid(), "@_name": variableName, "@_typeRef": undefined, description: { __$$text: "" }, }, }; }, [contextExpression] ); const onRowAdded = useCallback( (args: { beforeIndex: number; rowsCount: number }) => { 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); } // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 const ret: Normalized = { ...prev, contextEntry: newContextEntries, }; return ret; }, expressionChangedArgs: { action: Action.RowsAdded, rowIndex: args.beforeIndex, rowsCount: args.rowsCount }, }); }, [getDefaultContextEntry, setExpression] ); const onRowDeleted = useCallback( (args: { rowIndex: number }) => { let oldExpression: Normalized | undefined; setExpression({ setExpressionAction: (prev: Normalized) => { const newContextEntries = [...(prev.contextEntry ?? [])]; 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); } // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 const ret: Normalized = { ...prev, contextEntry: newContextEntries, }; return ret; }, expressionChangedArgs: { action: Action.RowRemoved, rowIndex: args.rowIndex }, }); setWidthsById(({ newMap }) => { for (const id of findAllIdsDeep(oldExpression)) { newMap.delete(id); } }); }, [setExpression, setWidthsById] ); const onRowReset = useCallback( (args: { rowIndex: number }) => { let oldExpression: Normalized | undefined; 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 { 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, }; return ret; }, expressionChangedArgs: { action: Action.RowReset, rowIndex: args.rowIndex }, }); setWidthsById(({ newMap }) => { for (const id of findAllIdsDeep(oldExpression)) { newMap.delete(id); } }); }, [getDefaultContextEntry, setExpression, setWidthsById] ); const allowedOperations = useCallback( (conditions: BeeTableContextMenuAllowedOperationsConditions) => { if (!conditions.selection.selectionStart || !conditions.selection.selectionEnd) { return []; } const columnIndex = conditions.selection.selectionStart.columnIndex; const rowIndex = conditions.selection.selectionStart.rowIndex; const contextEntries = contextExpression.contextEntry ?? []; const { isResultOperation, hasResultEntry } = solveResultAndEntriesIndex({ contextEntries, rowIndex }); const canDeleteEntry = !isResultOperation && (hasResultEntry ? contextEntries.length > 2 : contextEntries.length > 1); return [ BeeTableOperation.SelectionCopy, ...(columnIndex > 1 ? [BeeTableOperation.SelectionCut, BeeTableOperation.SelectionPaste, BeeTableOperation.SelectionReset] : []), ...(conditions.selection.selectionStart.rowIndex >= 0 ? [ BeeTableOperation.RowInsertAbove, ...(!isResultOperation ? [BeeTableOperation.RowInsertBelow] : []), ...(!isResultOperation ? [BeeTableOperation.RowInsertN] : []), ...(canDeleteEntry ? [BeeTableOperation.RowDelete] : []), BeeTableOperation.RowReset, ] : []), ]; }, [contextExpression.contextEntry] ); const beeTableRows = useMemo(() => { return (contextExpression.contextEntry ?? []).flatMap((contextEntry, i) => !contextEntry.variable ? [] : { ...contextEntry, variable: contextEntry.variable, index: i, } ); }, [contextExpression.contextEntry]); return (
isReadOnly={isReadOnly} isEditableHeader={!isReadOnly} resizerStopBehavior={ResizerStopBehavior.SET_WIDTH_WHEN_SMALLER} tableId={id} headerLevelCountForAppendingRowIndexColumn={1} headerVisibility={headerVisibility} cellComponentByColumnAccessor={cellComponentByColumnAccessor} columns={beeTableColumns} rows={beeTableRows} onColumnUpdates={onColumnUpdates} operationConfig={beeTableOperationConfig} allowedOperations={allowedOperations} getRowKey={getRowKey} additionalRow={beeTableAdditionalRow} onRowAdded={onRowAdded} onRowReset={onRowReset} onRowDeleted={onRowDeleted} onColumnResizingWidthChange={onColumnResizingWidthChange} shouldRenderRowIndexColumn={false} shouldShowRowsInlineControls={true} shouldShowColumnsInlineControls={false} />
); } export function solveResultAndEntriesIndex({ contextEntries, rowIndex, }: { contextEntries: DMN_LATEST__tContextEntry[]; rowIndex: number; }) { const resultIndex = contextEntries.findIndex((e) => !e.variable); const hasResultEntry = resultIndex > -1; const isResultOperation = rowIndex === Math.max(1, hasResultEntry ? contextEntries.length - 1 : contextEntries.length); const entryIndex = Math.min( contextEntries.length - 1, resultIndex === -1 ? rowIndex // : resultIndex < rowIndex ? rowIndex + 1 : rowIndex ); return { isResultOperation, hasResultEntry, resultIndex, entryIndex }; } export function ContextResultInfoCell(props: { parentElementId: string }) { const { containerCellCoordinates } = useBeeTableCoordinates(); const value = useMemo(() => { return ``; }, []); const getValue = useCallback(() => { return value; }, [value]); const { isActive } = useBeeTableSelectableCellRef( containerCellCoordinates?.rowIndex ?? 0, containerCellCoordinates?.columnIndex ?? 0, undefined, getValue ); const { beeGwtService } = useBoxedExpressionEditor(); // Selecting the context result cell should be the parent data type useEffect(() => { if (isActive) { beeGwtService?.selectObject(props.parentElementId); } }, [beeGwtService, isActive, props.parentElementId]); return
{value}
; }