export function ContextExpression()

in packages/boxed-expression-component/src/expressions/ContextExpression/ContextExpression.tsx [62:523]


export function ContextExpression({
  isNested,
  parentElementId,
  expression: contextExpression,
}: {
  expression: Normalized<BoxedContext>;
  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<number | undefined>) => {
      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<number>(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<ResizingWidth>({
    value: entryVariableWidth,
    isPivoting: false,
  });

  const onColumnResizingWidthChange1 = useCallback((args: Map<number, ResizingWidth | undefined>) => {
    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<number, ResizingWidth | undefined>) => {
      onColumnResizingWidthChange2?.(args);
      onColumnResizingWidthChange1(args);
    },
    [onColumnResizingWidthChange1, onColumnResizingWidthChange2]
  );

  const beeTableColumns = useMemo<ReactTable.Column<ROWTYPE>[]>(() => {
    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: "variable",
            isRowIndexColumn: false,
            dataType: DmnBuiltInDataType.Undefined,
            isWidthPinned: true,
            minWidth: CONTEXT_ENTRY_VARIABLE_MIN_WIDTH,
            width: entryVariableWidth,
            setWidth: setEntryVariableWidth,
          },
          {
            accessor: "expression",
            label: "expression",
            dataType: DmnBuiltInDataType.Undefined,
            isRowIndexColumn: false,
            minWidth: CONTEXT_ENTRY_EXPRESSION_MIN_WIDTH,
            width: undefined,
          },
        ],
      },
    ];
  }, [contextExpression, entryVariableWidth, expressionHolderId, setEntryVariableWidth]);

  const onColumnUpdates = useCallback(
    ([{ name, typeRef }]: BeeTableColumnUpdate<ROWTYPE>[]) => {
      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<BoxedContext>) => {
          // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241
          const ret: Normalized<BoxedContext> = {
            ...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<BoxedContext>) => {
          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<BoxedContext> = {
            ...prev,
            contextEntry: contextEntries,
          };

          return ret;
        },
        expressionChangedArgs: variableChangedArgs,
      });
    },
    [setExpression]
  );

  const cellComponentByColumnAccessor: BeeTableProps<ROWTYPE>["cellComponentByColumnAccessor"] = useMemo(() => {
    return {
      variable: (props) => <ExpressionVariableCell {...props} onExpressionWithVariableUpdated={updateVariable} />,
      expression: (props) => <ContextEntryExpressionCell {...props} />,
    };
  }, [updateVariable]);

  const beeTableOperationConfig = useMemo<BeeTableOperationConfig>(() => {
    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<ROWTYPE>) => {
    return row.id;
  }, []);

  const beeTableAdditionalRow = useMemo(() => {
    return [
      <ContextResultInfoCell key={"context-result-info"} parentElementId={parentElementId} />,
      <ContextResultExpressionCell
        key={"context-result-expression"}
        contextExpression={contextExpression}
        rowIndex={contextExpression.contextEntry?.length ?? 1}
        columnIndex={2}
      />,
    ];
  }, [contextExpression, parentElementId]);

  const getDefaultContextEntry = useCallback(
    (name?: string): Normalized<DMN15__tContextEntry> => {
      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<BoxedContext>) => {
          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<BoxedContext> = {
            ...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<BoxedExpression> | undefined;

      setExpression({
        setExpressionAction: (prev: Normalized<BoxedContext>) => {
          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 <result> 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<BoxedContext> = {
            ...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<BoxedExpression> | undefined;

      setExpression({
        setExpressionAction: (prev: Normalized<BoxedContext>) => {
          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<BoxedContext> = {
            ...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<ROWTYPE[]>(() => {
    return (contextExpression.contextEntry ?? []).flatMap((contextEntry, i) =>
      !contextEntry.variable
        ? []
        : {
            ...contextEntry,
            variable: contextEntry.variable,
            index: i,
          }
    );
  }, [contextExpression.contextEntry]);

  return (
    <NestedExpressionContainerContext.Provider value={nestedExpressionContainerValue}>
      <div className={`context-expression ${id}`}>
        <BeeTable<ROWTYPE>
          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}
        />
      </div>
    </NestedExpressionContainerContext.Provider>
  );
}