export function RelationExpression()

in packages/boxed-expression-component/src/expressions/RelationExpression/RelationExpression.tsx [56:598]


export function RelationExpression({
  isNested,
  expression: relationExpression,
}: {
  expression: BoxedRelation;
  isNested: boolean;
  parentElementId: string;
}) {
  const { i18n } = useBoxedExpressionEditorI18n();
  const { widthsById, expressionHolderId, isReadOnly } = useBoxedExpressionEditor();
  const { setExpression, setWidthsById } = useBoxedExpressionEditorDispatch();

  const id = relationExpression["@_id"]!;

  const beeTableOperationConfig = useMemo<BeeTableOperationConfig>(
    () => [
      {
        group: i18n.columns,
        items: [
          { name: i18n.columnOperations.insertLeft, type: BeeTableOperation.ColumnInsertLeft },
          { name: i18n.columnOperations.insertRight, type: BeeTableOperation.ColumnInsertRight },
          { name: i18n.insert, type: BeeTableOperation.ColumnInsertN },
          { name: i18n.columnOperations.delete, type: BeeTableOperation.ColumnDelete },
        ],
      },
      {
        group: i18n.rows,
        items: [
          { 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 },
          { name: i18n.rowOperations.duplicate, type: BeeTableOperation.RowDuplicate },
        ],
      },
      {
        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 widths = useMemo(() => widthsById.get(id) ?? [], [id, widthsById]);

  const getColumnWidth = useCallback((columnIndex: number, widths: number[]) => {
    return widths[columnIndex] ?? RELATION_EXPRESSION_COLUMN_DEFAULT_WIDTH;
  }, []);

  const columns = useMemo(() => {
    return (relationExpression.column ?? []).map((c, index) => ({
      ...c,
      minWidth: RELATION_EXPRESSION_COLUMN_MIN_WIDTH,
      width: getColumnWidth(index + 1, widths),
    }));
  }, [getColumnWidth, relationExpression.column, widths]);

  const rows = useMemo<DMN15__tList[]>(() => {
    return relationExpression.row ?? [];
  }, [relationExpression]);

  const setColumnWidth = useCallback(
    (columnIndex: number) => (newWidthAction: React.SetStateAction<number | undefined>) => {
      setWidthsById(({ newMap }) => {
        const prev = newMap.get(id) ?? [];
        const prevColumnWidth = getColumnWidth(columnIndex, prev);
        const newWidth = typeof newWidthAction === "function" ? newWidthAction(prevColumnWidth) : newWidthAction;

        if (newWidth && prevColumnWidth) {
          const minSize = columnIndex + 1;
          const newValues = [...prev];
          newValues.push(
            ...Array<number>(Math.max(0, minSize - newValues.length)).fill(RELATION_EXPRESSION_COLUMN_MIN_WIDTH)
          );
          newValues.splice(columnIndex, 1, newWidth);
          newMap.set(id, newValues);
        }
      });
    },
    [getColumnWidth, setWidthsById, id]
  );
  /// //////////////////////////////////////////////////////
  /// ///////////// RESIZING WIDTHS ////////////////////////
  /// //////////////////////////////////////////////////////

  const beeTableRef = useRef<BeeTableRef>(null);
  const { onColumnResizingWidthChange, columnResizingWidths, isPivoting } = usePublishedBeeTableResizableColumns(
    relationExpression["@_id"]!,
    columns.length,
    true
  );

  useNestedTableLastColumnMinWidth(columnResizingWidths);

  useApportionedColumnWidthsIfNestedTable(
    beeTableRef,
    isPivoting,
    isNested,
    BEE_TABLE_ROW_INDEX_COLUMN_WIDTH,
    columns,
    columnResizingWidths,
    rows
  );

  /// //////////////////////////////////////////////////////

  const beeTableColumns = useMemo<ReactTable.Column<ROWTYPE>[]>(() => {
    return [
      {
        accessor: expressionHolderId as any, // FIXME: https://github.com/apache/incubator-kie-issues/issues/169
        label: relationExpression["@_label"] ?? DEFAULT_EXPRESSION_VARIABLE_NAME,
        dataType: relationExpression["@_typeRef"] ?? DmnBuiltInDataType.Undefined,
        isRowIndexColumn: false,
        width: undefined,
        columns: columns.map((column, columnIndex) => ({
          accessor: column["@_id"] as any,
          label: column["@_name"],
          dataType: column["@_typeRef"] ?? DmnBuiltInDataType.Undefined,
          isRowIndexColumn: false,
          minWidth: RELATION_EXPRESSION_COLUMN_MIN_WIDTH,
          setWidth: setColumnWidth(columnIndex + 1),
          width: getColumnWidth(columnIndex + 1, widths),
        })),
      },
    ];
  }, [columns, expressionHolderId, getColumnWidth, relationExpression, setColumnWidth, widths]);

  const beeTableRows = useMemo<ROWTYPE[]>(
    () =>
      rows.map((row) => {
        return columns.reduce(
          (tableRow: ROWTYPE, column, columnIndex) => {
            const cellExpression = row.expression?.[columnIndex];
            if (cellExpression?.__$$element === "literalExpression") {
              tableRow[column["@_id"]!] = {
                id: cellExpression["@_id"] ?? generateUuid(),
                content: cellExpression.text?.__$$text ?? "",
              };
            }
            return tableRow;
          },
          { id: row["@_id"] }
        );
      }),
    [rows, columns]
  );

  const onCellUpdates = useCallback(
    (cellUpdates: BeeTableCellUpdate<ROWTYPE>[]) => {
      setExpression({
        setExpressionAction: (prev: Normalized<BoxedRelation>) => {
          let previousExpression: Normalized<BoxedRelation> = { ...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,
            };
          });

          return previousExpression;
        },
        expressionChangedArgs: { action: Action.RelationCellsUpdated },
      });
    },
    [setExpression]
  );

  const getExpressionChangedArgsFromColumnUpdates = useCallback(
    (columnUpdates: BeeTableColumnUpdate<ROWTYPE>[]) => {
      // 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 expressionChangedArgs: ExpressionChangedArgs = { action: Action.ColumnChanged };
        return expressionChangedArgs;
      }
    },
    [expressionHolderId, relationExpression]
  );

  const onColumnUpdates = useCallback(
    (columnUpdates: BeeTableColumnUpdate<ROWTYPE>[]) => {
      const expressionChangedArgs = getExpressionChangedArgsFromColumnUpdates(columnUpdates);
      setExpression({
        setExpressionAction: (prev: Normalized<BoxedRelation>) => {
          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<BoxedRelation> = {
            ...n,
            column: newColumns,
          };

          return ret;
        },
        expressionChangedArgs,
      });
    },
    [getExpressionChangedArgsFromColumnUpdates, setExpression]
  );

  const createCell = useCallback(() => {
    const cell: {
      __$$element: "literalExpression";
      "@_id": string;
      text: { __$$text: string };
    } = {
      __$$element: "literalExpression",
      "@_id": generateUuid(),
      text: {
        __$$text: RELATION_EXPRESSION_DEFAULT_VALUE,
      },
    };
    return cell;
  }, []);

  const onRowAdded = useCallback(
    (args: { beforeIndex: number; rowsCount: number }) => {
      setExpression({
        setExpressionAction: (prev: Normalized<BoxedRelation>) => {
          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);
          }

          // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241
          const ret: Normalized<BoxedRelation> = {
            ...prev,
            row: newRows,
          };

          return ret;
        },
        expressionChangedArgs: { action: Action.RowsAdded, rowsCount: args.rowsCount, rowIndex: args.beforeIndex },
      });
    },
    [createCell, setExpression]
  );

  const onColumnAdded = useCallback(
    (args: { beforeIndex: number; columnsCount: number }) => {
      setExpression({
        setExpressionAction: (prev: Normalized<BoxedRelation>) => {
          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 (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<BoxedRelation> = {
            ...prev,
            column: newColumns,
            row: newRows,
          };

          return ret;
        },
        expressionChangedArgs: {
          action: Action.ColumnAdded,
          columnCount: args.columnsCount,
          columnIndex: args.beforeIndex,
        },
      });

      setWidthsById(({ newMap }) => {
        const prev = newMap.get(id) ?? [];
        const defaultWidth = RELATION_EXPRESSION_COLUMN_DEFAULT_WIDTH;

        const newValues = [...prev];
        for (let i = 0; i < args.columnsCount; i++) {
          newValues.splice(args.beforeIndex + 1, 0, defaultWidth); // + 1 to account for rowIndex column
        }
        newMap.set(id, newValues);
      });
    },
    [createCell, id, setExpression, setWidthsById]
  );

  const onColumnDeleted = useCallback(
    (args: { columnIndex: number }) => {
      setExpression({
        setExpressionAction: (prev: Normalized<BoxedRelation>) => {
          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<BoxedRelation> = {
            ...prev,
            column: newColumns,
            row: newRows,
          };

          return ret;
        },
        expressionChangedArgs: { action: Action.ColumnRemoved, columnIndex: args.columnIndex },
      });

      setWidthsById(({ newMap }) => {
        const prev = newMap.get(id) ?? [];
        const newValues = [...prev];
        newValues.splice(args.columnIndex + 1, 1); // + 1 to account for the rowIndex column
        newMap.set(id, newValues);
      });
    },
    [id, setExpression, setWidthsById]
  );

  const onRowDeleted = useCallback(
    (args: { rowIndex: number }) => {
      setExpression({
        setExpressionAction: (prev: Normalized<BoxedRelation>) => {
          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<BoxedRelation> = {
            ...prev,
            row: newRows,
          };

          return ret;
        },
        expressionChangedArgs: { action: Action.RowRemoved, rowIndex: args.rowIndex },
      });
    },
    [setExpression]
  );

  const onRowDuplicated = useCallback(
    (args: { rowIndex: number }) => {
      setExpression({
        setExpressionAction: (prev: Normalized<BoxedRelation>) => {
          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);

          // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241
          const ret: Normalized<BoxedRelation> = {
            ...prev,
            row: newRows,
          };

          return ret;
        },
        expressionChangedArgs: { action: Action.RowDuplicated, rowIndex: args.rowIndex },
      });
    },
    [setExpression]
  );

  const beeTableHeaderVisibility = useMemo(() => {
    return isNested ? BeeTableHeaderVisibility.LastLevel : BeeTableHeaderVisibility.AllLevels;
  }, [isNested]);

  const allowedOperations = useCallback(
    (conditions: BeeTableContextMenuAllowedOperationsConditions) => {
      if (!conditions.selection.selectionStart || !conditions.selection.selectionEnd) {
        return [];
      }

      const columnIndex = conditions.selection.selectionStart.columnIndex;

      const columnCanBeDeleted = (conditions.columns?.length ?? 0) > 2; // That's a regular column and the rowIndex column

      const columnOperations =
        columnIndex === 0 // This is the rowIndex column
          ? []
          : [
              BeeTableOperation.ColumnInsertLeft,
              BeeTableOperation.ColumnInsertRight,
              BeeTableOperation.ColumnInsertN,
              ...(columnCanBeDeleted ? [BeeTableOperation.ColumnDelete] : []),
            ];

      return [
        ...columnOperations,
        BeeTableOperation.SelectionCopy,
        ...(columnIndex > 0 && conditions.selection.selectionStart.rowIndex >= 0
          ? [BeeTableOperation.SelectionCut, BeeTableOperation.SelectionPaste, BeeTableOperation.SelectionReset]
          : []),
        ...(conditions.selection.selectionStart.rowIndex >= 0
          ? [
              BeeTableOperation.RowInsertAbove,
              BeeTableOperation.RowInsertBelow,
              BeeTableOperation.RowInsertN,
              ...(beeTableRows.length > 1 ? [BeeTableOperation.RowDelete] : []),
              BeeTableOperation.RowReset,
              BeeTableOperation.RowDuplicate,
            ]
          : []),
      ];
    },
    [beeTableRows.length]
  );

  return (
    <div className={`relation-expression`}>
      <BeeTable<ROWTYPE>
        isReadOnly={isReadOnly}
        isEditableHeader={!isReadOnly}
        resizerStopBehavior={
          isPivoting ? ResizerStopBehavior.SET_WIDTH_ALWAYS : ResizerStopBehavior.SET_WIDTH_WHEN_SMALLER
        }
        forwardRef={beeTableRef}
        headerLevelCountForAppendingRowIndexColumn={1}
        editColumnLabel={i18n.editRelation}
        columns={beeTableColumns}
        headerVisibility={beeTableHeaderVisibility}
        rows={beeTableRows}
        onCellUpdates={onCellUpdates}
        onColumnUpdates={onColumnUpdates}
        operationConfig={beeTableOperationConfig}
        allowedOperations={allowedOperations}
        onRowAdded={onRowAdded}
        onRowDuplicated={onRowDuplicated}
        onRowDeleted={onRowDeleted}
        onColumnAdded={onColumnAdded}
        onColumnDeleted={onColumnDeleted}
        onColumnResizingWidthChange={onColumnResizingWidthChange}
        shouldRenderRowIndexColumn={true}
        shouldShowRowsInlineControls={true}
        shouldShowColumnsInlineControls={true}
        // lastColumnMinWidth={lastColumnMinWidth} // FIXME: Check if this is a good strategy or not when doing https://github.com/apache/incubator-kie-issues/issues/181
      />
    </div>
  );
}