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>
);
}