diff --git a/packages/boxed-expression-component/src/api/BoxedExpression.ts b/packages/boxed-expression-component/src/api/BoxedExpression.ts index ae5192d5c35..f9e0239d4c6 100644 --- a/packages/boxed-expression-component/src/api/BoxedExpression.ts +++ b/packages/boxed-expression-component/src/api/BoxedExpression.ts @@ -51,6 +51,8 @@ export enum BoxedFunctionKind { Pmml = "PMML", } +export type BoxedIterator = BoxedFor | BoxedEvery | BoxedSome; + export type BoxedExpression = | BoxedLiteral | BoxedRelation diff --git a/packages/boxed-expression-component/src/expressions/ExpressionDefinitionRoot/ExpressionDefinitionLogicTypeSelector.tsx b/packages/boxed-expression-component/src/expressions/ExpressionDefinitionRoot/ExpressionDefinitionLogicTypeSelector.tsx index 36911234637..c997c59b639 100644 --- a/packages/boxed-expression-component/src/expressions/ExpressionDefinitionRoot/ExpressionDefinitionLogicTypeSelector.tsx +++ b/packages/boxed-expression-component/src/expressions/ExpressionDefinitionRoot/ExpressionDefinitionLogicTypeSelector.tsx @@ -29,6 +29,9 @@ import { CutIcon } from "@patternfly/react-icons/dist/js/icons/cut-icon"; import { ListIcon } from "@patternfly/react-icons/dist/js/icons/list-icon"; import { PasteIcon } from "@patternfly/react-icons/dist/js/icons/paste-icon"; import { TableIcon } from "@patternfly/react-icons/dist/js/icons/table-icon"; +import { RebootingIcon } from "@patternfly/react-icons/dist/js/icons/rebooting-icon"; +import { ResourcesAlmostEmptyIcon } from "@patternfly/react-icons/dist/js/icons/resources-almost-empty-icon"; +import { ResourcesFullIcon } from "@patternfly/react-icons/dist/js/icons/resources-full-icon"; import * as React from "react"; import { useCallback, useEffect, useMemo, useState } from "react"; import { BoxedExpression } from "../../api"; @@ -47,13 +50,14 @@ import { LiteralExpression } from "../LiteralExpression/LiteralExpression"; import { RelationExpression } from "../RelationExpression/RelationExpression"; import { BoxedExpressionClipboard, - DMN_BOXED_EXPRESSION_CLIPBOARD_MIME_TYPE, buildClipboardFromExpression, + DMN_BOXED_EXPRESSION_CLIPBOARD_MIME_TYPE, } from "../../clipboard/clipboard"; import { findAllIdsDeep, mutateExpressionRandomizingIds } from "../../ids/ids"; import "./ExpressionDefinitionLogicTypeSelector.css"; import { NavigationKeysUtils } from "../../keysUtils/keyUtils"; import { ConditionalExpression } from "../ConditionalExpression/ConditionalExpression"; +import { IteratorExpressionComponent } from "../IteratorExpression/IteratorExpressionComponent"; export interface ExpressionDefinitionLogicTypeSelectorProps { /** Expression properties */ @@ -95,9 +99,9 @@ export function ExpressionDefinitionLogicTypeSelector({ "invocation", ...(isNested ? (["functionDefinition"] as const) : []), ...(!hideDmn14BoxedExpressions ? (["conditional"] as const) : []), - // "for", - // "every", - // "some", + "for", + "every", + "some", // "filter", ], [hideDmn14BoxedExpressions, isNested] @@ -134,6 +138,9 @@ export function ExpressionDefinitionLogicTypeSelector({ case "for": case "every": case "some": + return ( + + ); case "filter": return <>; default: @@ -235,8 +242,11 @@ export function ExpressionDefinitionLogicTypeSelector({ ); case "for": + return ; case "every": + return ; case "some": + return ; case "filter": return <>; default: @@ -330,6 +340,25 @@ export function ExpressionDefinitionLogicTypeSelector({ return "A boxed list expression in DMN represents a FEEL list of items. You use boxed lists to define lists of relevant items for a particular node in a decision."; case "conditional": return 'A boxed conditional offers a visual representation of an if statement using three rows. The expression in the "if" part MUST resolve to a boolean.'; + case "for": + return ( + "A boxed iterator offers a visual representation of an iterator statement. " + + 'For the for loop, the right part of the "for" displays the iterator variable name. The second row holds an expression representing the collection that will be iterated over. The expression in the "in" row MUST resolve to a collection.' + + " The last row contains the expression that will process each element of the collection." + ); + + case "every": + return ( + "A boxed iterator offers a visual representation of an iterator statement. " + + 'For the "every" loop, the right part of the "every" displays the iterator variable name. The second row holds an expression representing the collection that will be iterated over. The expression in the "in" row MUST resolve to a collection.' + + "The last line is an expression that will be evaluated on each item. The expression defined in the satisfies MUST resolve to a boolean." + ); + case "some": + return ( + "A boxed iterator offers a visual representation of an iterator statement. " + + 'For the "some" loop, the right part of the "some" displays the iterator variable name. The second row holds an expression representing the collection that will be iterated over. The expression in the "in" row MUST resolve to a collection. ' + + "The last line is an expression that will be evaluated on each item. The expression defined in the satisfies MUST resolve to a boolean." + ); default: return ""; } diff --git a/packages/boxed-expression-component/src/expressions/IteratorExpression/IteratorExpressionCell.tsx b/packages/boxed-expression-component/src/expressions/IteratorExpression/IteratorExpressionCell.tsx new file mode 100644 index 00000000000..99c0c7e5ab6 --- /dev/null +++ b/packages/boxed-expression-component/src/expressions/IteratorExpression/IteratorExpressionCell.tsx @@ -0,0 +1,80 @@ +import { BoxedIterator } from "../../api"; + +import { + NestedExpressionDispatchContextProvider, + useBoxedExpressionEditorDispatch, +} from "../../BoxedExpressionEditorContext"; +import * as React from "react"; +import { useCallback, useMemo } from "react"; +import { ExpressionContainer } from "../ExpressionDefinitionRoot/ExpressionContainer"; +import { IteratorClause } from "./IteratorExpressionComponent"; + +export interface IteratorExpressionCellExpressionCellProps { + iteratorClause: IteratorClause; + rowIndex: number; + columnIndex: number; + columnId: string; +} + +export function IteratorExpressionCell({ + rowIndex, + columnIndex, + parentElementId, + iteratorClause, +}: IteratorExpressionCellExpressionCellProps & { parentElementId: string }) { + const { setExpression } = useBoxedExpressionEditorDispatch(); + + const onSetExpression = useCallback( + ({ getNewExpression }) => { + setExpression((prev: BoxedIterator) => { + switch (rowIndex) { + case 1: + return { + ...prev, + in: { + expression: getNewExpression(prev.in.expression), + }, + }; + case 2: + default: + if (prev.__$$element === "for") { + return { + ...prev, + return: { + expression: getNewExpression(prev.return.expression), + }, + }; + } else { + return { + ...prev, + satisfies: { + expression: getNewExpression(prev.satisfies.expression), + }, + }; + } + } + }); + }, + [rowIndex, setExpression] + ); + + const currentExpression = useMemo(() => { + if (typeof iteratorClause.child !== "string") { + return iteratorClause.child?.expression; + } + }, [iteratorClause.child]); + + return ( + + + + ); +} diff --git a/packages/boxed-expression-component/src/expressions/IteratorExpression/IteratorExpressionComponent.tsx b/packages/boxed-expression-component/src/expressions/IteratorExpression/IteratorExpressionComponent.tsx new file mode 100644 index 00000000000..86d118fe2a6 --- /dev/null +++ b/packages/boxed-expression-component/src/expressions/IteratorExpression/IteratorExpressionComponent.tsx @@ -0,0 +1,341 @@ +/* + * 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 { + BeeTableHeaderVisibility, + BeeTableOperation, + BeeTableOperationConfig, + BeeTableProps, + BoxedFor, + BoxedIterator, + DmnBuiltInDataType, +} from "../../api"; +import { BeeTable, BeeTableColumnUpdate, BeeTableRef } from "../../table/BeeTable"; +import { ResizerStopBehavior } from "../../resizing/ResizingWidthsContext"; +import React, { useCallback, useMemo, useRef } from "react"; +import { + DMN15__tChildExpression, + DMN15__tTypedChildExpression, +} from "@kie-tools/dmn-marshaller/dist/schemas/dmn-1_5/ts-gen/types"; +import * as ReactTable from "react-table"; +import { useBoxedExpressionEditorI18n } from "../../i18n"; +import { BeeTableReadOnlyCell } from "../../table/BeeTable/BeeTableReadOnlyCell"; +import { IteratorExpressionCell } from "./IteratorExpressionCell"; + +import { useBoxedExpressionEditor, useBoxedExpressionEditorDispatch } from "../../BoxedExpressionEditorContext"; +import { NestedExpressionContainerContext } from "../../resizing/NestedExpressionContainerContext"; +import { useNestedExpressionContainerWithNestedExpressions } from "../../resizing/Hooks"; +import { + ITERATOR_EXPRESSION_CLAUSE_COLUMN_MIN_WIDTH, + ITERATOR_EXPRESSION_EXTRA_WIDTH, + ITERATOR_EXPRESSION_LABEL_COLUMN_WIDTH, +} from "../../resizing/WidthConstants"; +import { DEFAULT_EXPRESSION_VARIABLE_NAME } from "../../expressionVariable/ExpressionVariableMenu"; +import { InlineEditableTextInput } from "../../table/BeeTable/InlineEditableTextInput"; + +type ROWTYPE = IteratorClause; + +export type IteratorClause = { + child: DMN15__tTypedChildExpression | DMN15__tChildExpression | string | undefined; + label: string; +}; + +export function IteratorExpressionComponent({ + isNested, + parentElementId, + expression: expression, +}: { + expression: BoxedIterator; + isNested: boolean; + parentElementId: string; +}) { + const { i18n } = useBoxedExpressionEditorI18n(); + const { expressionHolderId, widthsById } = useBoxedExpressionEditor(); + const { setExpression } = useBoxedExpressionEditorDispatch(); + + const id = expression["@_id"]!; + + const tableColumns = useMemo[]>(() => { + return [ + { + accessor: expressionHolderId as any, // FIXME: https://github.com/kiegroup/kie-issues/issues/169 + label: expression["@_label"] ?? DEFAULT_EXPRESSION_VARIABLE_NAME, + isRowIndexColumn: false, + dataType: expression["@_typeRef"] ?? DmnBuiltInDataType.Undefined, + width: undefined, + columns: [ + { + accessor: "label", + label: "label", + width: ITERATOR_EXPRESSION_LABEL_COLUMN_WIDTH, + minWidth: ITERATOR_EXPRESSION_LABEL_COLUMN_WIDTH, + isInlineEditable: false, + isRowIndexColumn: false, + isWidthPinned: true, + isWidthConstant: true, + dataType: undefined as any, + }, + { + accessor: "child", + label: "child", + width: undefined, + minWidth: ITERATOR_EXPRESSION_CLAUSE_COLUMN_MIN_WIDTH, + isInlineEditable: false, + isRowIndexColumn: false, + dataType: undefined as any, + }, + ], + }, + ]; + }, [expression, expressionHolderId]); + + const headerVisibility = useMemo(() => { + return isNested ? BeeTableHeaderVisibility.None : BeeTableHeaderVisibility.SecondToLastLevel; + }, [isNested]); + + const beeTableOperationConfig = useMemo(() => { + return [ + { + group: i18n.contextEntry, + items: [{ name: i18n.rowOperations.reset, type: BeeTableOperation.RowReset }], + }, + { + group: i18n.terms.selection.toUpperCase(), + items: [{ name: i18n.terms.copy, type: BeeTableOperation.SelectionCopy }], + }, + ]; + }, [i18n]); + + const getIterableRowLabel = useCallback( + (rowNumber: number) => { + if (rowNumber === 0) { + if (expression.__$$element === "for") { + return "for"; + } else if (expression.__$$element === "some") { + return "some"; + } else if (expression.__$$element === "every") { + return "every"; + } else { + throw new Error("Unknown IteratorExpression element"); + } + } else if (rowNumber === 1) { + return "in"; + } else if (rowNumber === 2) { + if (expression.__$$element === "for") { + return "return"; + } else if (expression.__$$element === "some" || expression.__$$element === "every") { + return "satisfies"; + } else { + throw new Error("Unknown IteratorExpression element"); + } + } else { + throw new Error("IteratorExpression can't have more than 3 rows."); + } + }, + [expression.__$$element] + ); + + const getIterableRowElement = useCallback( + (rowNumber: number) => { + if (rowNumber === 0) { + return expression["@_iteratorVariable"] ?? ""; + } else if (rowNumber === 1) { + return expression.in; + } else { + switch (expression.__$$element) { + case "for": + return expression.return; + case "every": + case "some": + return expression.satisfies; + } + } + }, + [expression] + ); + + const tableRows = useMemo(() => { + return [ + { label: getIterableRowLabel(0), child: getIterableRowElement(0) }, + { label: getIterableRowLabel(1), child: getIterableRowElement(1) }, + { label: getIterableRowLabel(2), child: getIterableRowElement(2) }, + ]; + }, [getIterableRowElement, getIterableRowLabel]); + + const allowedOperations = useCallback(() => { + return [BeeTableOperation.SelectionCopy, BeeTableOperation.RowReset]; + }, []); + + const beeTableRef = useRef(null); + + const cellComponentByColumnAccessor: BeeTableProps["cellComponentByColumnAccessor"] = useMemo(() => { + return { + label: (props) => { + return ; + }, + child: (props) => { + if (props.rowIndex === 0) { + return ( +
+ { + setExpression((prev: BoxedIterator) => { + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: BoxedIterator = { + ...prev, + "@_iteratorVariable": updatedValue, + }; + return ret; + }); + }} + setActiveCellEditing={(value) => { + beeTableRef.current?.setActiveCellEditing(value); + }} + /> +
+ ); + } else if (props.rowIndex === 2 || props.rowIndex === 3) { + return ( + + ); + } else { + throw new Error("IteratorExpression can't have more than 3 rows."); + } + }, + }; + }, [parentElementId, setExpression]); + + const { nestedExpressionContainerValue, onColumnResizingWidthChange } = + useNestedExpressionContainerWithNestedExpressions( + useMemo(() => { + const nestedExpressions = [ + expression.in.expression, + expression.__$$element === "for" ? expression.return.expression : expression.satisfies.expression, + ]; + + return { + nestedExpressions: nestedExpressions, + fixedColumnActualWidth: ITERATOR_EXPRESSION_LABEL_COLUMN_WIDTH, + fixedColumnResizingWidth: { value: ITERATOR_EXPRESSION_LABEL_COLUMN_WIDTH, isPivoting: false }, + fixedColumnMinWidth: ITERATOR_EXPRESSION_LABEL_COLUMN_WIDTH, + nestedExpressionMinWidth: ITERATOR_EXPRESSION_CLAUSE_COLUMN_MIN_WIDTH, + extraWidth: ITERATOR_EXPRESSION_EXTRA_WIDTH, + expression: expression, + flexibleColumnIndex: 2, + widthsById: widthsById, + }; + }, [expression, widthsById]) + ); + + const onColumnUpdates = useCallback( + ([{ name, typeRef }]: BeeTableColumnUpdate[]) => { + setExpression((prev: BoxedIterator) => { + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: BoxedIterator = { + ...prev, + "@_label": name, + "@_typeRef": typeRef, + }; + + return ret; + }); + }, + [setExpression] + ); + const onRowReset = useCallback( + (args: { rowIndex: number }) => { + setExpression((prev: BoxedIterator) => { + if (args.rowIndex === 0) { + // Do not inline this variable for type safety. See https://github.com/microsoft/TypeScript/issues/241 + const ret: BoxedIterator = { + ...prev, + "@_iteratorVariable": undefined, + }; + 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: BoxedIterator = { + ...prev, + in: { expression: undefined! }, // SPEC DISCREPANCY + }; + 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: BoxedFor = { + ...prev, + return: { 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: BoxedIterator = { + ...prev, + satisfies: { expression: undefined! }, // SPEC DISCREPANCY + }; + return iterator; + } else { + throw new Error("Nested expression type not supported in IteratorExpression."); + } + } else { + throw new Error("IteratorExpression shouldn't have more than 3 rows."); + } + }); + }, + [setExpression] + ); + + return ( + +
+ + forwardRef={beeTableRef} + resizerStopBehavior={ResizerStopBehavior.SET_WIDTH_WHEN_SMALLER} + tableId={id} + headerLevelCountForAppendingRowIndexColumn={1} + headerVisibility={headerVisibility} + cellComponentByColumnAccessor={cellComponentByColumnAccessor} + columns={tableColumns} + rows={tableRows} + operationConfig={beeTableOperationConfig} + allowedOperations={allowedOperations} + onColumnUpdates={onColumnUpdates} + onRowReset={onRowReset} + onColumnResizingWidthChange={onColumnResizingWidthChange} + shouldRenderRowIndexColumn={false} + shouldShowRowsInlineControls={false} + shouldShowColumnsInlineControls={false} + /> +
+
+ ); +} diff --git a/packages/boxed-expression-component/src/resizing/BeeTableResizableColumnsContext.tsx b/packages/boxed-expression-component/src/resizing/BeeTableResizableColumnsContext.tsx index 5f9b13d9ae5..c13f5e14dc6 100644 --- a/packages/boxed-expression-component/src/resizing/BeeTableResizableColumnsContext.tsx +++ b/packages/boxed-expression-component/src/resizing/BeeTableResizableColumnsContext.tsx @@ -18,7 +18,6 @@ */ import * as React from "react"; -import * as ReactTable from "react-table"; import { useCallback, useEffect, useImperativeHandle, useMemo, useState } from "react"; import { ResizerStopBehavior, ResizingWidth, useResizerRef, useResizingWidthsDispatch } from "./ResizingWidthsContext"; import { BEE_TABLE_ROW_INDEX_COLUMN_WIDTH } from "./WidthConstants"; @@ -31,7 +30,9 @@ export type BeeTableResizableColumnsContextType = { export interface BeeTableResizableColumnsDispatchContextType { updateColumnResizingWidths(newColumnResizingWidths: Map): void; + registerResizableCellRef(columnIndex: number, ref: BeeTableResizableCellRef): BeeTableResizableCellRef; + deregisterResizableCellRef(columnIndex: number, ref: BeeTableResizableCellRef): void; } @@ -46,69 +47,69 @@ export interface BeeTableResizableCellRef { // PROVIDER -type Props = React.PropsWithChildren<{ +export type BeeTableResizingRef = BeeTableResizableColumnsDispatchContextType; + +export const BeeTableResizableColumnsContextProvider = ({ + children, + onChange, + resizingRef, +}: React.PropsWithChildren<{ onChange?: (args: Map) => void; - columns: ReactTable.Column[]; -}>; + resizingRef: React.RefObject; +}>) => { + const refs = React.useRef>>(new Map()); -type MyRef = BeeTableResizableColumnsDispatchContextType; + const [columnResizingWidths, setColumnResizingWidths] = useState>(new Map()); -export const BeeTableResizableColumnsContextProvider = React.forwardRef( - ({ children, onChange, columns }, forwardRef) => { - const refs = React.useRef>>(new Map()); + const onColumnResizingWidthChange = useCallback((args: Map) => { + setColumnResizingWidths((prev) => { + const n = new Map(prev); + for (const [columnIndex, newResizingWidth] of args.entries()) { + if (newResizingWidth) { + n.set(columnIndex, newResizingWidth); + } + } + return n; + }); + }, []); - const [columnResizingWidths, setColumnResizingWidths] = useState>(new Map()); + const value = useMemo(() => { + return { columnResizingWidths }; + }, [columnResizingWidths]); - const onColumnResizingWidthChange = useCallback((args: Map) => { - setColumnResizingWidths((prev) => { - const n = new Map(prev); - for (const [columnIndex, newResizingWidth] of args.entries()) { - if (newResizingWidth) { - n.set(columnIndex, newResizingWidth); + const dispatch = useMemo(() => { + return { + updateColumnResizingWidths: (newColumnResizingWidths) => { + for (const [columnIndex, newResizingWidth] of newColumnResizingWidths.entries()) { + for (const ref of refs.current.get(columnIndex) ?? []) { + ref.setResizingWidth?.(newResizingWidth); } } - return n; - }); - }, []); - - const value = useMemo(() => { - return { columnResizingWidths }; - }, [columnResizingWidths]); - - const dispatch = useMemo(() => { - return { - updateColumnResizingWidths: (newColumnResizingWidths) => { - for (const [columnIndex, newResizingWidth] of newColumnResizingWidths.entries()) { - for (const ref of refs.current.get(columnIndex) ?? []) { - ref.setResizingWidth?.(newResizingWidth); - } - } - onColumnResizingWidthChange(newColumnResizingWidths); - onChange?.(newColumnResizingWidths); - }, - registerResizableCellRef: (columnIndex, ref) => { - const prev = refs.current?.get(columnIndex) ?? new Set(); - refs.current?.set(columnIndex, new Set([...prev, ref])); - return ref; - }, - deregisterResizableCellRef: (columnIndex, ref) => { - refs.current?.get(columnIndex)?.delete(ref); - }, - }; - }, [onChange, onColumnResizingWidthChange]); - - useImperativeHandle(forwardRef, () => dispatch, [dispatch]); - - return ( - - - <>{children} - - - ); - } -); + onColumnResizingWidthChange(newColumnResizingWidths); + onChange?.(newColumnResizingWidths); + }, + registerResizableCellRef: (columnIndex, ref) => { + const prev = refs.current?.get(columnIndex) ?? new Set(); + refs.current?.set(columnIndex, new Set([...prev, ref])); + return ref; + }, + deregisterResizableCellRef: (columnIndex, ref) => { + refs.current?.get(columnIndex)?.delete(ref); + }, + }; + }, [onChange, onColumnResizingWidthChange]); + + useImperativeHandle(resizingRef, () => dispatch, [dispatch]); + + return ( + + + <>{children} + + + ); +}; export function useBeeTableResizableColumnsDispatch() { return React.useContext(BeeTableResizableColumnsDispatchContext); diff --git a/packages/boxed-expression-component/src/resizing/WidthConstants.ts b/packages/boxed-expression-component/src/resizing/WidthConstants.ts index 0e307d20eef..06d98a78d26 100644 --- a/packages/boxed-expression-component/src/resizing/WidthConstants.ts +++ b/packages/boxed-expression-component/src/resizing/WidthConstants.ts @@ -71,3 +71,7 @@ export const JAVA_FUNCTION_EXPRESSION_EXTRA_WIDTH = BEE_TABLE_ROW_INDEX_COLUMN_W export const CONDITIONAL_EXPRESSION_LABEL_COLUMN_WIDTH = 80; export const CONDITIONAL_EXPRESSION_CLAUSE_COLUMN_MIN_WIDTH = 210; export const CONDITIONAL_EXPRESSION_EXTRA_WIDTH = 2; // 2px for borders of context entry expression // It's a mistery why to this cell is counting the borders. + +export const ITERATOR_EXPRESSION_LABEL_COLUMN_WIDTH = 80; +export const ITERATOR_EXPRESSION_CLAUSE_COLUMN_MIN_WIDTH = 210; +export const ITERATOR_EXPRESSION_EXTRA_WIDTH = 2; // 2px for borders of context entry expression // It's a mistery why to this cell is counting the borders. diff --git a/packages/boxed-expression-component/src/resizing/WidthMaths.ts b/packages/boxed-expression-component/src/resizing/WidthMaths.ts index ba4430b3d34..f18fbe11080 100644 --- a/packages/boxed-expression-component/src/resizing/WidthMaths.ts +++ b/packages/boxed-expression-component/src/resizing/WidthMaths.ts @@ -51,6 +51,9 @@ import { PMML_FUNCTION_EXPRESSION_LABEL_MIN_WIDTH, PMML_FUNCTION_EXPRESSION_VALUES_MIN_WIDTH, RELATION_EXPRESSION_COLUMN_MIN_WIDTH, + ITERATOR_EXPRESSION_LABEL_COLUMN_WIDTH, + ITERATOR_EXPRESSION_CLAUSE_COLUMN_MIN_WIDTH, + ITERATOR_EXPRESSION_EXTRA_WIDTH, } from "./WidthConstants"; export function getExpressionMinWidth(expression?: BoxedExpression): number { @@ -328,6 +331,34 @@ export function getExpressionResizingWidth( ); } + // For + else if (expression.__$$element === "for") { + const nestedExpressions = [expression.in.expression, expression.return.expression]; + return ( + resizingWidth ?? + ITERATOR_EXPRESSION_LABEL_COLUMN_WIDTH + + Math.max( + ITERATOR_EXPRESSION_CLAUSE_COLUMN_MIN_WIDTH, + ...nestedExpressions.map((e) => getExpressionResizingWidth(e, resizingWidths, widthsById)) + ) + + ITERATOR_EXPRESSION_EXTRA_WIDTH + ); + } + + // Every and Some + else if (expression.__$$element === "every" || expression.__$$element === "some") { + const nestedExpressions = [expression.in.expression, expression.satisfies.expression]; + return ( + resizingWidth ?? + ITERATOR_EXPRESSION_LABEL_COLUMN_WIDTH + + Math.max( + ITERATOR_EXPRESSION_CLAUSE_COLUMN_MIN_WIDTH, + ...nestedExpressions.map((e) => getExpressionResizingWidth(e, resizingWidths, widthsById)) + ) + + ITERATOR_EXPRESSION_EXTRA_WIDTH + ); + } + // Others else { throw new Error(`Can't determine resizing width for expression of unknown type '${expression.__$$element}'`); diff --git a/packages/boxed-expression-component/src/table/BeeTable/BeeTable.tsx b/packages/boxed-expression-component/src/table/BeeTable/BeeTable.tsx index b3fe39be607..44dc827e1cd 100644 --- a/packages/boxed-expression-component/src/table/BeeTable/BeeTable.tsx +++ b/packages/boxed-expression-component/src/table/BeeTable/BeeTable.tsx @@ -19,7 +19,7 @@ import _ from "lodash"; import * as React from "react"; -import { useCallback, useMemo, useRef } from "react"; +import { useCallback, useImperativeHandle, useMemo, useRef } from "react"; import * as ReactTable from "react-table"; import { BeeTableHeaderVisibility, BeeTableProps, InsertRowColumnsDirection } from "../../api"; import { useBoxedExpressionEditor } from "../../BoxedExpressionEditorContext"; @@ -29,7 +29,7 @@ import { BEE_TABLE_ROW_INDEX_COLUMN_WIDTH } from "../../resizing/WidthConstants" import { BeeTableBody } from "./BeeTableBody"; import { BeeTableResizableColumnsContextProvider, - BeeTableResizableColumnsDispatchContextType, + BeeTableResizingRef, } from "../../resizing/BeeTableResizableColumnsContext"; import { BeeTableContextMenuHandler } from "./BeeTableContextMenuHandler"; import { BeeTableDefaultCell } from "./BeeTableDefaultCell"; @@ -62,7 +62,12 @@ export function getColumnsAtLastLevel | ReactTa }); } +export type BeeTableSelectionRef = { + setActiveCellEditing: (isEditing: boolean) => void; +}; + export function BeeTableInternal({ + selectionRef, tableId, additionalRow, editColumnLabel, @@ -98,7 +103,9 @@ export function BeeTableInternal({ resizerStopBehavior, lastColumnMinWidth, rowWrapper, -}: BeeTableProps) { +}: BeeTableProps & { + selectionRef?: React.RefObject; +}) { const { resetSelectionAt, erase, copy, cut, paste, adaptSelection, mutateSelection, setCurrentDepth } = useBeeTableSelectionDispatch(); const tableComposableRef = useRef(null); @@ -172,7 +179,7 @@ export function BeeTableInternal({ [hasAdditionalRow] ); - const _setEditing = useCallback( + const _setActiveCellEditing = useCallback( (rowCount: number, columnCount: (rowIndex: number) => number) => (isEditing: boolean) => { mutateSelection({ part: SelectionPart.ActiveCell, @@ -239,7 +246,7 @@ export function BeeTableInternal({ cellProps={cellProps} onCellUpdates={onCellUpdates} isReadOnly={isReadOnly} - setEditing={_setEditing(cellProps.rows.length, () => cellProps.allColumns.length)} + setEditing={_setActiveCellEditing(cellProps.rows.length, () => cellProps.allColumns.length)} navigateHorizontally={_navigateHorizontally(cellProps.rows.length, () => cellProps.allColumns.length)} navigateVertically={_navigateVertically(cellProps.rows.length, () => cellProps.allColumns.length)} /> @@ -247,7 +254,14 @@ export function BeeTableInternal({ } }, }), - [cellComponentByColumnAccessor, onCellUpdates, isReadOnly, _setEditing, _navigateHorizontally, _navigateVertically] + [ + cellComponentByColumnAccessor, + onCellUpdates, + isReadOnly, + _setActiveCellEditing, + _navigateHorizontally, + _navigateVertically, + ] ); const reactTableInstance = ReactTable.useTable( @@ -588,9 +602,17 @@ export function BeeTableInternal({ [adaptSelection, onColumnDeleted] ); - const setEditing = useMemo(() => { - return _setEditing(reactTableInstance.rows.length, () => reactTableInstance.allColumns.length); - }, [_setEditing, reactTableInstance.allColumns.length, reactTableInstance.rows.length]); + const setActiveCellEditing = useMemo(() => { + return _setActiveCellEditing(reactTableInstance.rows.length, () => reactTableInstance.allColumns.length); + }, [_setActiveCellEditing, reactTableInstance.allColumns.length, reactTableInstance.rows.length]); + + useImperativeHandle( + selectionRef, + () => ({ + setActiveCellEditing: (isEditing) => setActiveCellEditing(isEditing), + }), + [setActiveCellEditing] + ); return (
@@ -615,7 +637,7 @@ export function BeeTableInternal({ onHeaderClick={onHeaderClick} onHeaderKeyUp={onHeaderKeyUp} lastColumnMinWidth={lastColumnMinWidth} - setEditing={setEditing} + setActiveCellEditing={setActiveCellEditing} /> rowWrapper={rowWrapper} @@ -649,22 +671,41 @@ export function BeeTableInternal({ ); } -export type BeeTableRef = BeeTableResizableColumnsDispatchContextType; +export type BeeTableRef = BeeTableResizingRef & BeeTableSelectionRef; -export type ForwardRefBeeTableProps = BeeTableProps & { forwardRef?: React.Ref } & { +export type ForwardRefBeeTableProps = BeeTableProps & { + forwardRef?: React.Ref; +} & { onColumnResizingWidthChange?: (args: Map) => void; }; -export const BeeTable = (props: ForwardRefBeeTableProps) => { +export const BeeTable = ({ + forwardRef, + onColumnResizingWidthChange, + ...props +}: ForwardRefBeeTableProps) => { + const beeTableResizingRef = useRef(null); + const beeTableSelectionRef = useRef(null); + + useImperativeHandle( + forwardRef, + () => { + if (!beeTableResizingRef.current || !beeTableSelectionRef.current) { + return null; + } + + return { + ...beeTableSelectionRef.current!, + ...beeTableResizingRef.current!, + }; + }, + [] + ); return ( - + - + diff --git a/packages/boxed-expression-component/src/table/BeeTable/BeeTableHeader.tsx b/packages/boxed-expression-component/src/table/BeeTable/BeeTableHeader.tsx index 0549a1b224a..4c0057aaa7f 100644 --- a/packages/boxed-expression-component/src/table/BeeTable/BeeTableHeader.tsx +++ b/packages/boxed-expression-component/src/table/BeeTable/BeeTableHeader.tsx @@ -81,7 +81,7 @@ export interface BeeTableHeaderProps { resizerStopBehavior: ResizerStopBehavior; lastColumnMinWidth?: number; - setEditing: React.Dispatch>; + setActiveCellEditing: (isEditing: boolean) => void; } export function BeeTableHeader({ @@ -99,7 +99,7 @@ export function BeeTableHeader({ shouldShowRowsInlineControls, resizerStopBehavior, lastColumnMinWidth, - setEditing, + setActiveCellEditing, }: BeeTableHeaderProps) { const getColumnLabel: (groupType: string) => string | undefined = useCallback( (groupType) => { @@ -235,7 +235,7 @@ export function BeeTableHeader({ column.headerCellElement ) : column.isInlineEditable ? ( ({ getColumnLabel, onColumnAdded, lastColumnMinWidth, - setEditing, + setActiveCellEditing, onExpressionHeaderUpdated, ] ); diff --git a/packages/boxed-expression-component/src/table/BeeTable/InlineEditableTextInput.tsx b/packages/boxed-expression-component/src/table/BeeTable/InlineEditableTextInput.tsx index 7df524d9dfc..fe62b621845 100644 --- a/packages/boxed-expression-component/src/table/BeeTable/InlineEditableTextInput.tsx +++ b/packages/boxed-expression-component/src/table/BeeTable/InlineEditableTextInput.tsx @@ -27,7 +27,7 @@ import { useBeeTableSelectableCellRef } from "../../selection/BeeTableSelectionC export interface InlineEditableTextInputProps { value: string; onChange: (updatedValue: string) => void; - setEditing: React.Dispatch>; + setActiveCellEditing: (isEditing: boolean) => void; rowIndex: number; columnIndex: number; } @@ -36,21 +36,26 @@ export const InlineEditableTextInput: React.FunctionComponent { const { i18n } = useBoxedExpressionEditorI18n(); const inputRef = useRef(null); - const { isEditing } = useBeeTableSelectableCellRef(rowIndex, columnIndex, undefined, undefined); + const { isEditing } = useBeeTableSelectableCellRef( + rowIndex, + columnIndex, + onChange, + useCallback(() => value, [value]) + ); const stopEditingPersistingValue = useCallback(() => { const newValue = inputRef.current?.value; if (newValue && newValue !== value) { onChange(newValue); } - setEditing(false); - }, [onChange, setEditing, value]); + setActiveCellEditing(false); + }, [onChange, setActiveCellEditing, value]); const onInputKeyDown = useMemo( () => (e: React.KeyboardEvent) => { @@ -61,17 +66,17 @@ export const InlineEditableTextInput: React.FunctionComponent { - setEditing(true); - }, [setEditing]); + setActiveCellEditing(true); + }, [setActiveCellEditing]); - const getTextStyle = useMemo(() => { + const textStyle = useMemo(() => { if (_.isEmpty(value)) { return { fontStyle: "italic", cursor: "pointer", color: "gray" }; } else { @@ -87,7 +92,11 @@ export const InlineEditableTextInput: React.FunctionComponent +

{value || i18n.enterText}

) : (