diff --git a/packages/editor/package.json b/packages/editor/package.json index dc0e0af13..35e60ae41 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -28,7 +28,7 @@ "@axonivy/ui-icons": "~14.0.0-next", "@tanstack/react-query": "^5.64", "@tanstack/react-query-devtools": "^5.64", - "@tanstack/react-table": "^8.20", + "@tanstack/react-table": "^9.0.0", "i18next": "^25.0.0 || ^26.0.0", "inversify": "^6.2", "react": "^19.0.0", diff --git a/packages/editor/src/ui-tools/history/History.tsx b/packages/editor/src/ui-tools/history/History.tsx index cd93d32a4..fc0811e93 100644 --- a/packages/editor/src/ui-tools/history/History.tsx +++ b/packages/editor/src/ui-tools/history/History.tsx @@ -82,7 +82,7 @@ export const HistoryContent = ({ actionDispatcher, togglePinned, closeHistory, a updater => { setExpandedByPid(current => ({ ...current, - [pid]: typeof updater === 'function' ? updater(expanded) : updater + [pid]: typeof updater === 'function' ? updater(current[pid] ?? expanded) : updater })); }, [expanded, pid] @@ -92,17 +92,17 @@ export const HistoryContent = ({ actionDispatcher, togglePinned, closeHistory, a async (node: HistoryNode) => { setExpandedByPid(current => ({ ...current, - [pid]: setExpandedNode(current[pid] ?? (data ? lastLeafPathExpandedState(data) : {}), node.id, true) + [pid]: setExpandedNode(current[pid] ?? expanded, node.id, true) })); const result = await loadLazyNodeData(node); if (result === 'error' || result === 'invalid') { setExpandedByPid(current => ({ ...current, - [pid]: setExpandedNode(current[pid] ?? (data ? lastLeafPathExpandedState(data) : {}), node.id, false) + [pid]: setExpandedNode(current[pid] ?? expanded, node.id, false) })); } }, - [data, loadLazyNodeData, pid] + [expanded, loadLazyNodeData, pid] ); const successActions: Array = [ diff --git a/packages/editor/src/ui-tools/history/HistoryTree.tsx b/packages/editor/src/ui-tools/history/HistoryTree.tsx index d44f01d7e..d6f3bf964 100644 --- a/packages/editor/src/ui-tools/history/HistoryTree.tsx +++ b/packages/editor/src/ui-tools/history/HistoryTree.tsx @@ -1,19 +1,22 @@ // @jsxRuntime automatic import { type HistoryNode } from '@axonivy/process-editor-protocol'; -import { BasicTooltip, ExpandableCell, IvyIcon, Table, TableBody, TableCell, TableRow, useTableGlobalFilter } from '@axonivy/ui-components'; -import { IvyIcons } from '@axonivy/ui-icons'; import { - flexRender, - getCoreRowModel, - getExpandedRowModel, - useReactTable, - type ColumnDef, - type ExpandedState, - type OnChangeFn -} from '@tanstack/react-table'; + BasicTooltip, + dataTreeHelper, + ExpandableCell, + IvyIcon, + Table, + TableBody, + TableCell, + TableGlobalFilter, + TableRow, + type DataTableFeatures +} from '@axonivy/ui-components'; +import { IvyIcons } from '@axonivy/ui-icons'; +import { flexRender, useTable, type ExpandedState, type OnChangeFn } from '@tanstack/react-table'; import React, { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { isExpandableDataNode, isHistoryNodeLoaded, type HistoryLazyState } from './history-tree-state'; +import { isExpandableDataNode, isHistoryNodeLoaded, lastLeafPathExpandedState, type HistoryLazyState } from './history-tree-state'; export type HistoryTreeProps = { data: Array; @@ -24,65 +27,70 @@ export type HistoryTreeProps = { onLoadLazyNode: (node: HistoryNode) => void; }; +const { columnHelper, tableOptions } = dataTreeHelper(); +const loadingChildIdPrefix = 'history-loading-'; + export const HistoryTree = ({ data, searchActive, expanded, onExpandedChange, lazyState, onLoadLazyNode }: HistoryTreeProps) => { - const globalFilter = useTableGlobalFilter({ searchAutoFocus: true }); - const columns: ColumnDef[] = useMemo( - () => [ - { - accessorKey: 'description', - cell: cell => { - const node = cell.row.original; - const label = - node.type === 'EXECUTION' - ? new Date(cell.getValue()).toLocaleTimeString(undefined, { - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - fractionalSecondDigits: 3 - }) - : cell.getValue(); + const treeData = useMemo(() => addLoadingChildren(data, lazyState.loadingById), [data, lazyState.loadingById]); + const columns = useMemo( + () => + columnHelper.columns([ + columnHelper.accessor('description', { + cell: cell => { + const node = cell.row.original; + const loadingParentId = getLoadingParentId(node.id); + const label = + node.type === 'EXECUTION' + ? new Date(cell.getValue()).toLocaleTimeString(undefined, { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + fractionalSecondDigits: 3 + }) + : cell.getValue(); - return ( - onLoadLazyNode(node) - } - : undefined - } - > - - {label} - - ); - } - } - ], + return ( + onLoadLazyNode(node) + } + : undefined + } + > + {loadingParentId ? ( + + ) : ( + <> + + {label} + + )} + + ); + } + }) + ]), [lazyState, onLoadLazyNode] ); - const table = useReactTable({ - ...globalFilter.options, - data, + const table = useTable({ + ...tableOptions, + data: treeData, columns, - getCoreRowModel: getCoreRowModel(), - getExpandedRowModel: getExpandedRowModel(), - getSubRows: row => row.children, getRowId: row => row.id, - getRowCanExpand: row => row.original.children.length > 0, + initialState: { expanded: lastLeafPathExpandedState(treeData) }, + autoResetExpanded: false, onExpandedChange, - state: { - expanded, - ...globalFilter.tableState - } + state: { expanded } }); return ( <> - {searchActive && globalFilter.filter} + {table.getRowModel().rows.map(row => ( @@ -98,16 +106,16 @@ export const HistoryTree = ({ data, searchActive, expanded, onExpandedChange, la ); }; -const LazyStatus = ({ lazyState, node }: { lazyState: HistoryLazyState; node: HistoryNode }) => { +const LazyStatus = ({ lazyState, nodeId }: { lazyState: HistoryLazyState; nodeId: string }) => { const { t } = useTranslation(); - if (lazyState.loadingById[node.id]) { + if (lazyState.loadingById[nodeId]) { return ( ); } - const error = lazyState.errorById[node.id]; + const error = lazyState.errorById[nodeId]; if (error) { return ( @@ -117,6 +125,30 @@ const LazyStatus = ({ lazyState, node }: { lazyState: HistoryLazyState; node: Hi } }; +const addLoadingChildren = (nodes: Array, loadingById: Record): Array => + nodes.map(node => { + const children = addLoadingChildren(node.children, loadingById); + if (!loadingById[node.id]) { + return children === node.children ? node : { ...node, children }; + } + + return { + ...node, + children: [ + { + id: `${loadingChildIdPrefix}${node.id}`, + type: 'DATA', + description: 'Loading...', + expandable: false, + children: [] + } + ] + }; + }); + +const getLoadingParentId = (nodeId: string) => + nodeId.startsWith(loadingChildIdPrefix) ? nodeId.slice(loadingChildIdPrefix.length) : undefined; + const historyNodeIcon = (node: HistoryNode) => { switch (node.type) { case 'REQUEST_FINISHED': diff --git a/packages/inscription-view/package.json b/packages/inscription-view/package.json index a41341510..8eef89809 100644 --- a/packages/inscription-view/package.json +++ b/packages/inscription-view/package.json @@ -33,7 +33,7 @@ "@radix-ui/react-tabs": "^1.1", "@tanstack/react-query": "^5.64", "@tanstack/react-query-devtools": "^5.64", - "@tanstack/react-table": "^8.20", + "@tanstack/react-table": "^9.0.0", "downshift": "^9.0", "i18next": "^25.0.0 || ^26.0.0", "react": "^19.0", diff --git a/packages/inscription-view/src/components/browser/BrowserTableRow.tsx b/packages/inscription-view/src/components/browser/BrowserTableRow.tsx index 669351f3e..1831f4da6 100644 --- a/packages/inscription-view/src/components/browser/BrowserTableRow.tsx +++ b/packages/inscription-view/src/components/browser/BrowserTableRow.tsx @@ -1,9 +1,15 @@ -import { SelectRow, TableCell, TableRow, type BrowserNode } from '@axonivy/ui-components'; -import { flexRender, type Row } from '@tanstack/react-table'; +import { type DataTableFeatures, SelectRow, TableCell, TableRow } from '@axonivy/ui-components'; +import { flexRender, type Row, type RowData } from '@tanstack/react-table'; -const BrowserTableRow = ({ row, onDoubleClick }: { row: Row; onDoubleClick: () => void }) => ( +const BrowserTableRow = ({ + row, + onDoubleClick +}: { + row: Row; + onDoubleClick: () => void; +}) => ( <> - {(row.original as BrowserNode).notSelectable ? ( + {row.original.notSelectable ? ( {row.getVisibleCells().map(cell => ( {flexRender(cell.column.columnDef.cell, cell.getContext())} diff --git a/packages/inscription-view/src/components/browser/attribute/AttributeBrowser.tsx b/packages/inscription-view/src/components/browser/attribute/AttributeBrowser.tsx index 7bcac54ab..85c93636c 100644 --- a/packages/inscription-view/src/components/browser/attribute/AttributeBrowser.tsx +++ b/packages/inscription-view/src/components/browser/attribute/AttributeBrowser.tsx @@ -1,8 +1,17 @@ import type { VariableInfo } from '@axonivy/process-editor-inscription-protocol'; -import { ExpandableHeader, TableBody, TableHead, TableHeader, TableRow, useTableKeyHandler } from '@axonivy/ui-components'; +import { + dataTreeHelper, + ExpandableHeader, + TableBody, + TableHead, + TableHeader, + TableRow, + useTableKeyHandler, + type DataTableFeatures +} from '@axonivy/ui-components'; import { IvyIcons } from '@axonivy/ui-icons'; -import type { ColumnDef, ExpandedState, Row, RowSelectionState } from '@tanstack/react-table'; -import { flexRender, getCoreRowModel, getExpandedRowModel, getFilteredRowModel, useReactTable } from '@tanstack/react-table'; +import type { Row } from '@tanstack/react-table'; +import { flexRender, useTable } from '@tanstack/react-table'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useEditorContext } from '../../../context/useEditorContext'; @@ -28,6 +37,8 @@ export const useAttributeBrowser = (onDoubleClick: () => void, location: string) }; }; +const { columnHelper, tableOptions } = dataTreeHelper(); + const AttributeBrowser = ({ value, onChange, @@ -69,73 +80,61 @@ const AttributeBrowser = ({ [varInfo] ); - const columns = useMemo[]>( - () => [ - { - accessorFn: row => row.attribute, - id: 'attribute', - header: header => , - cell: cell => ( - loadChildren(cell.row.original)} - title={cell.row.original.description} - additionalInfo={cell.row.original.simpleType} - icon={IvyIcons.Attribute} - /> - ) - } - ], + const columns = useMemo( + () => + columnHelper.columns([ + { + accessorFn: row => row.attribute, + id: 'attribute', + header: header => , + cell: cell => ( + loadChildren(cell.row.original)} + title={cell.row.original.description} + additionalInfo={cell.row.original.simpleType} + icon={IvyIcons.Attribute} + /> + ) + } + ]), [loadChildren, t] ); - const [expanded, setExpanded] = useState(true); - const [globalFilter, setGlobalFilter] = useState(''); - const [rowSelection, setRowSelection] = useState({}); - - const table = useReactTable({ + const table = useTable({ + ...tableOptions, data: tree, columns: columns, - state: { - expanded, - globalFilter, - rowSelection - }, - filterFromLeafRows: true, enableRowSelection: true, enableMultiRowSelection: false, - enableSubRowSelection: false, - onExpandedChange: setExpanded, - onGlobalFilterChange: setGlobalFilter, - onRowSelectionChange: setRowSelection, - getSubRows: row => row.children, - getCoreRowModel: getCoreRowModel(), - getExpandedRowModel: getExpandedRowModel(), - getFilteredRowModel: getFilteredRowModel() + enableSubRowSelection: false }); const { handleKeyDown } = useTableKeyHandler({ table, data: tree, options: { - lazyLoadChildren: (row: Row) => loadChildren(row.original) + lazyLoadChildren: (row: Row) => loadChildren(row.original) } }); useEffect(() => { - const selectedRow = table.getSelectedRowModel().flatRows[0]; - if (selectedRow === undefined) { - return; - } - // eslint-disable-next-line @eslint-react/set-state-in-effect - setShowHelper(true); - onChange({ value: calcFullPathId(selectedRow) }); - }, [onChange, rowSelection, table]); + const subscribed = table.atoms.rowSelection.subscribe(() => { + const selectedRow = table.getSelectedRowModel().flatRows[0]; + if (selectedRow === undefined) { + return; + } + + setShowHelper(true); + onChange({ value: calcFullPathId(selectedRow) }); + }); + return () => subscribed.unsubscribe(); + }, [onChange, table]); return ( <> - handleKeyDown(e, onDoubleClick)}> + handleKeyDown(e, onDoubleClick)}> {table.getHeaderGroups().map(headerGroup => ( diff --git a/packages/inscription-view/src/components/browser/cms/CmsBrowser.tsx b/packages/inscription-view/src/components/browser/cms/CmsBrowser.tsx index 702499010..ab5973a8d 100644 --- a/packages/inscription-view/src/components/browser/cms/CmsBrowser.tsx +++ b/packages/inscription-view/src/components/browser/cms/CmsBrowser.tsx @@ -1,9 +1,8 @@ import type { ContentObject, ContentObjectType } from '@axonivy/process-editor-inscription-protocol'; -import { Button, Flex, Message, SelectRow, TableBody, TableCell, toast, useTableKeyHandler } from '@axonivy/ui-components'; +import { Button, dataTreeHelper, Flex, Message, SelectRow, TableBody, TableCell, toast, useTableKeyHandler } from '@axonivy/ui-components'; import { IvyIcons } from '@axonivy/ui-icons'; import { useQueryClient } from '@tanstack/react-query'; -import type { ColumnDef, ColumnFiltersState, ExpandedState, RowSelectionState, VisibilityState } from '@tanstack/react-table'; -import { flexRender, getCoreRowModel, getExpandedRowModel, getFilteredRowModel, useReactTable } from '@tanstack/react-table'; +import { flexRender, useTable } from '@tanstack/react-table'; import { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useEditorContext } from '../../../context/useEditorContext'; @@ -62,6 +61,8 @@ interface CmsBrowserProps { typeFilter?: CmsTypeFilter; } +const { columnHelper, tableOptions } = dataTreeHelper(); + const CmsBrowser = ({ value, onChange, noApiCall, typeFilter, onDoubleClick, location, setDisableApply }: CmsBrowserProps) => { const { t } = useTranslation(); const { context } = useEditorContext(); @@ -90,108 +91,74 @@ const CmsBrowser = ({ value, onChange, noApiCall, typeFilter, onDoubleClick, loc } ); - const columns = useMemo[]>( - () => [ - { - accessorFn: row => row.name, - id: 'name', - cell: cell => { - return ( - - ); - } - }, - { - accessorFn: row => row.type, - id: 'type', - cell: cell => {cell.getValue() as string} - }, - { - accessorFn: row => JSON.stringify(row.values), - id: 'values', - cell: cell => {JSON.stringify(cell.getValue())} - } - ], + const columns = useMemo( + () => + columnHelper.columns([ + columnHelper.accessor('name', { + cell: cell => { + return ( + + ); + } + }), + columnHelper.accessor('type', { + cell: cell => {cell.getValue()} + }), + columnHelper.accessor(row => row.values, { + id: 'values', + cell: cell => {JSON.stringify(cell.getValue())} + }) + ]), [] ); - const [expanded, setExpanded] = useState(false as ExpandedState); - const [globalFilter, setGlobalFilter] = useState(''); - const [rowSelection, setRowSelection] = useState({}); - const [columnVisibility, setColumnVisibility] = useState({ type: false, values: false }); - const [columnFilters, setColumnFilters] = useState( - typeFilter === 'NONE' || typeFilter === undefined ? [] : [{ id: 'type', value: typeFilter }] - ); - - const table = useReactTable({ + const table = useTable({ + ...tableOptions, data: tree, columns: columns, + initialState: { + expanded: undefined, + columnFilters: typeFilter === 'NONE' || typeFilter === undefined ? [] : [{ id: 'type', value: typeFilter }] + }, state: { - expanded, - globalFilter, - rowSelection, - columnFilters, - columnVisibility + columnVisibility: { type: false, values: false } }, - filterFromLeafRows: true, enableRowSelection: true, enableMultiRowSelection: false, - enableSubRowSelection: false, - onExpandedChange: setExpanded, - onGlobalFilterChange: setGlobalFilter, - onColumnFiltersChange: setColumnFilters, - onRowSelectionChange: setRowSelection, - getSubRows: row => row.children, - getCoreRowModel: getCoreRowModel(), - getExpandedRowModel: getExpandedRowModel(), - getFilteredRowModel: getFilteredRowModel(), - onColumnVisibilityChange: setColumnVisibility + enableSubRowSelection: false }); const { handleKeyDown } = useTableKeyHandler({ table, data: tree }); useEffect(() => { - const addIvyPathToValue = (value: string, type: ContentObjectType, noApiCall?: boolean) => { - if (noApiCall || value.length === 0) { - return value; + const subscription = table.atoms.rowSelection.subscribe(() => { + const selectedRow = table.getSelectedRowModel().flatRows[0]; + if (selectedRow === undefined) { + setSelectedContentObject({ name: '', children: [], fullPath: '', type: 'STRING', values: {} }); + + setShowHelper(false); + onChange({ value: '' }); + return; } - if (type === 'FOLDER') { - return value; - } - if (type === 'FILE' && location === 'attachments') { - return `ivy.cm.findObject("${value}")`; - } else if (type === 'FILE' && location !== 'message') { - return `ivy.cms.cr("${value}")`; - } - return `ivy.cms.co("${value}")`; - }; - const selectedRow = table.getSelectedRowModel().flatRows[0]; - if (selectedRow === undefined) { - // eslint-disable-next-line @eslint-react/set-state-in-effect - setSelectedContentObject({ name: '', children: [], fullPath: '', type: 'STRING', values: {} }); - // eslint-disable-next-line @eslint-react/set-state-in-effect - setShowHelper(false); - onChange({ value: '' }); - return; - } - // eslint-disable-next-line @eslint-react/set-state-in-effect - setSelectedContentObject(selectedRow.original); - // eslint-disable-next-line @eslint-react/set-state-in-effect - setShowHelper(true); - onChange({ value: addIvyPathToValue(selectedRow.original.fullPath, selectedRow.original.type, noApiCall) }); - }, [onChange, rowSelection, noApiCall, table, location]); + setSelectedContentObject(selectedRow.original); + + setShowHelper(true); + onChange({ value: addIvyPathToValue(selectedRow.original.fullPath, selectedRow.original.type, location, noApiCall) }); + }); + return () => subscription.unsubscribe(); + }, [onChange, noApiCall, table, location]); - const folderSelected = () => table.getSelectedRowModel().flatRows[0]?.original.type === 'FOLDER'; + const folderSelected = table.getSelectedRowModel().flatRows[0]?.original.type === 'FOLDER'; return ( <> @@ -203,37 +170,23 @@ const CmsBrowser = ({ value, onChange, noApiCall, typeFilter, onDoubleClick, loc onClick={() => addNewCmsString.mutate({ context, parentUri: table.getSelectedRowModel().flatRows[0]?.original.fullPath ?? '' })} aria-label={t('browser.cms.add')} title={t('browser.cms.add')} - disabled={ - table.getSelectedRowModel().flatRows.length === 0 || - requiredProject || - table.getSelectedRowModel().flatRows[0]?.original.type !== 'FOLDER' - } + disabled={table.getSelectedRowModel().flatRows.length === 0 || requiredProject || !folderSelected} /> )} - { - setGlobalFilter(newFilterValue); - setExpanded(true); - } - }} - onKeyDown={e => handleKeyDown(e, onDoubleClick)} - > + handleKeyDown(e, onDoubleClick)} onSearchChange={() => table.setExpanded(true)}> {table.getRowModel().rows.map(row => ( (row.original.type === 'FOLDER' ? setDisableApply(true) : setDisableApply(false))} > {row.getVisibleCells().map(cell => ( {flexRender(cell.column.columnDef.cell, cell.getContext())} ))} - ))} @@ -259,3 +212,18 @@ const CmsBrowser = ({ value, onChange, noApiCall, typeFilter, onDoubleClick, loc ); }; + +const addIvyPathToValue = (value: string, type: ContentObjectType, location: string, noApiCall?: boolean) => { + if (noApiCall || value.length === 0) { + return value; + } + if (type === 'FOLDER') { + return value; + } + if (type === 'FILE' && location === 'attachments') { + return `ivy.cm.findObject("${value}")`; + } else if (type === 'FILE' && location !== 'message') { + return `ivy.cms.cr("${value}")`; + } + return `ivy.cms.co("${value}")`; +}; diff --git a/packages/inscription-view/src/components/browser/function/FunctionBrowser.tsx b/packages/inscription-view/src/components/browser/function/FunctionBrowser.tsx index e91ae0085..e55c06b84 100644 --- a/packages/inscription-view/src/components/browser/function/FunctionBrowser.tsx +++ b/packages/inscription-view/src/components/browser/function/FunctionBrowser.tsx @@ -1,15 +1,7 @@ import type { Function } from '@axonivy/process-editor-inscription-protocol'; -import { TableBody, TableFooter, useTableKeyHandler } from '@axonivy/ui-components'; +import { dataTableHelper, TableBody, TableFooter, useTableKeyHandler } from '@axonivy/ui-components'; import { IvyIcons } from '@axonivy/ui-icons'; -import { - getCoreRowModel, - getExpandedRowModel, - getFilteredRowModel, - useReactTable, - type ColumnDef, - type ExpandedState, - type RowSelectionState -} from '@tanstack/react-table'; +import { useTable } from '@tanstack/react-table'; import { useVirtualizer } from '@tanstack/react-virtual'; import DOMPurify from 'dompurify'; import { useEffect, useMemo, useRef, useState } from 'react'; @@ -43,6 +35,8 @@ type FunctionBrowserProps = { onDoubleClick: () => void; }; +const { columnHelper, tableOptions } = dataTableHelper(); + const FunctionBrowser = ({ value, onChange, onDoubleClick }: FunctionBrowserProps) => { const { t } = useTranslation(); const { context } = useEditorContext(); @@ -75,52 +69,44 @@ const FunctionBrowser = ({ value, onChange, onDoubleClick }: FunctionBrowserProp const [selectedFunctionDoc, setSelectedFunctionDoc] = useState(''); const [showHelper, setShowHelper] = useState(false); - const columns = useMemo[]>( - () => [ - { - accessorFn: row => - `${row.name.split('.').pop()}${ - row.isField === false ? `(${row.params.map(param => param.type.split('.').pop()).join(', ')})` : '' - }`, - id: 'name', - cell: cell => { - return ( - - ); - } - } - ], + const columns = useMemo( + () => + columnHelper.columns([ + columnHelper.accessor( + row => + `${row.name.split('.').pop()}${ + row.isField === false ? `(${row.params.map(param => param.type.split('.').pop()).join(', ')})` : '' + }`, + { + id: 'name', + cell: cell => { + return ( + + ); + } + } + ) + ]), [t] ); - const [expanded, setExpanded] = useState({ [0]: true }); - const [globalFilter, setGlobalFilter] = useState(''); - const [rowSelection, setRowSelection] = useState({}); - - const table = useReactTable({ + const table = useTable({ + ...tableOptions, data: sortedTree, columns: columns, - state: { - expanded, - globalFilter, - rowSelection + initialState: { + expanded: { [0]: true } }, filterFromLeafRows: true, enableRowSelection: true, enableMultiRowSelection: false, enableSubRowSelection: false, - onExpandedChange: setExpanded, - onGlobalFilterChange: setGlobalFilter, - onRowSelectionChange: setRowSelection, - getSubRows: row => (row.returnType ? row.returnType.functions : []), - getCoreRowModel: getCoreRowModel(), - getExpandedRowModel: getExpandedRowModel(), - getFilteredRowModel: getFilteredRowModel() + getSubRows: row => (row.returnType ? row.returnType.functions : []) }); const { handleKeyDown } = useTableKeyHandler({ table, data: sortedTree }); const { rows } = table.getRowModel(); @@ -134,48 +120,43 @@ const FunctionBrowser = ({ value, onChange, onDoubleClick }: FunctionBrowserProp }); useEffect(() => { - const selectedRow = table.getSelectedRowModel().flatRows[0]; - if (selectedRow === undefined) { - // eslint-disable-next-line @eslint-react/set-state-in-effect - setShowHelper(false); - onChange({ value: '' }); - return; - } - //setup correct functionName for the accept-method - const parentNames = getParentNames(selectedRow); - const selectedName = parentNames.reverse().join('.'); - onChange({ value: selectedName }); - - //setup Meta-Call for docApi - const parentRow = selectedRow.getParentRow(); - // eslint-disable-next-line @eslint-react/set-state-in-effect - setType( - parentRow - ? parentRow.original.returnType.packageName + '.' + parentRow.original.returnType.simpleName - : selectedRow.original.returnType.packageName + '.' + selectedRow.original.returnType.simpleName - ); - // eslint-disable-next-line @eslint-react/set-state-in-effect - setMethod(selectedRow.original.name); - // eslint-disable-next-line @eslint-react/set-state-in-effect - setParamTypes(selectedRow.original.params.map(param => param.type)); - //setup Helpertext - // eslint-disable-next-line @eslint-react/set-state-in-effect - setSelectedFunctionDoc(doc); - // eslint-disable-next-line @eslint-react/set-state-in-effect - setShowHelper(true); - }, [doc, onChange, rowSelection, table]); + const subscription = table.atoms.rowSelection.subscribe(() => { + const selectedRow = table.getSelectedRowModel().flatRows[0]; + if (selectedRow === undefined) { + setShowHelper(false); + onChange({ value: '' }); + return; + } + //setup correct functionName for the accept-method + const parentNames = getParentNames(selectedRow); + const selectedName = parentNames.reverse().join('.'); + onChange({ value: selectedName }); + + //setup Meta-Call for docApi + const parentRow = selectedRow.getParentRow(); + + setType( + parentRow + ? parentRow.original.returnType.packageName + '.' + parentRow.original.returnType.simpleName + : selectedRow.original.returnType.packageName + '.' + selectedRow.original.returnType.simpleName + ); + setMethod(selectedRow.original.name); + setParamTypes(selectedRow.original.params.map(param => param.type)); + //setup Helpertext + setSelectedFunctionDoc(doc); + setShowHelper(true); + }); + return () => subscription.unsubscribe(); + }, [doc, onChange, table]); return ( <> { - setGlobalFilter(newFilterValue); - setExpanded(newFilterValue.length > 0 ? true : { [0]: true }); - setRowSelection({}); - setRowNumber(100); - } + table={table} + onSearchChange={filter => { + table.setExpanded(filter.length > 0 ? true : { [0]: true }); + table.setRowSelection({}); + setRowNumber(100); }} onKeyDown={e => handleKeyDown(e, onDoubleClick)} ref={parentRef} diff --git a/packages/inscription-view/src/components/browser/function/parent-name.test.ts b/packages/inscription-view/src/components/browser/function/parent-name.test.ts index 5828621f5..9437b9d23 100644 --- a/packages/inscription-view/src/components/browser/function/parent-name.test.ts +++ b/packages/inscription-view/src/components/browser/function/parent-name.test.ts @@ -1,10 +1,11 @@ import type { Function } from '@axonivy/process-editor-inscription-protocol'; +import type { DataTableFeatures } from '@axonivy/ui-components'; import type { Row } from '@tanstack/react-table'; import { expect, test } from 'vitest'; import type { DeepPartial } from '../../../test-utils/type-utils'; import { getParentNames } from './parent-name'; -const mockFunction: DeepPartial> = { +const mockFunction: DeepPartial> = { original: { isField: true, name: 'ivy.cal', @@ -46,5 +47,5 @@ const mockFunction: DeepPartial> = { }; test('getParentNames from mockFunction', () => { - expect(getParentNames(mockFunction as Row)).toEqual(['ivy.cal', 'get(String)', 'getBusinessDuration(DateTime, DateTime)']); + expect(getParentNames(mockFunction as Row)).toEqual(['ivy.cal', 'get(String)', 'getBusinessDuration(DateTime, DateTime)']); }); diff --git a/packages/inscription-view/src/components/browser/function/parent-name.ts b/packages/inscription-view/src/components/browser/function/parent-name.ts index 6aa0129a6..6cfd5e7d2 100644 --- a/packages/inscription-view/src/components/browser/function/parent-name.ts +++ b/packages/inscription-view/src/components/browser/function/parent-name.ts @@ -1,7 +1,8 @@ import type { Function } from '@axonivy/process-editor-inscription-protocol'; +import type { DataTableFeatures } from '@axonivy/ui-components'; import type { Row } from '@tanstack/react-table'; -export const getParentNames = (currentRow: Row, parentNames: string[] = []): string[] => { +export const getParentNames = (currentRow: Row, parentNames: string[] = []): string[] => { parentNames.push( currentRow.original.isField ? currentRow.original.name diff --git a/packages/inscription-view/src/components/browser/role/AddRolePopover.tsx b/packages/inscription-view/src/components/browser/role/AddRolePopover.tsx index e100e182b..64772b161 100644 --- a/packages/inscription-view/src/components/browser/role/AddRolePopover.tsx +++ b/packages/inscription-view/src/components/browser/role/AddRolePopover.tsx @@ -9,7 +9,8 @@ import { PopoverArrow, PopoverContent, PopoverTrigger, - toast + toast, + type DataTableFeatures } from '@axonivy/ui-components'; import { IvyIcons } from '@axonivy/ui-icons'; import { useQueryClient } from '@tanstack/react-query'; @@ -28,7 +29,7 @@ export const AddRolePopover = ({ setAddedRoleName }: { value: string; - table: Table; + table: Table; setAddedRoleName: (value: string) => void; }) => { const { t } = useTranslation(); diff --git a/packages/inscription-view/src/components/browser/role/RoleBrowser.tsx b/packages/inscription-view/src/components/browser/role/RoleBrowser.tsx index b51d5c917..e553f873d 100644 --- a/packages/inscription-view/src/components/browser/role/RoleBrowser.tsx +++ b/packages/inscription-view/src/components/browser/role/RoleBrowser.tsx @@ -1,15 +1,7 @@ import type { RoleMeta } from '@axonivy/process-editor-inscription-protocol'; -import { Flex, TableBody, TableCell, TableRow, useTableKeyHandler } from '@axonivy/ui-components'; +import { dataTreeHelper, Flex, TableBody, TableCell, TableRow, useTableKeyHandler } from '@axonivy/ui-components'; import { IvyIcons } from '@axonivy/ui-icons'; -import { - getCoreRowModel, - getExpandedRowModel, - getFilteredRowModel, - useReactTable, - type ColumnDef, - type ExpandedState, - type RowSelectionState -} from '@tanstack/react-table'; +import { useTable } from '@tanstack/react-table'; import { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useRoles } from '../../parts/common/role/useRoles'; @@ -43,60 +35,57 @@ type RoleBrowserProps = { onDoubleClick: () => void; }; +const { columnHelper, tableOptions } = dataTreeHelper(); + const RoleBrowser = ({ value, showtaskRoles, onChange, onDoubleClick }: RoleBrowserProps) => { const { t } = useTranslation(); const { rolesAsTree: roleItems } = useRoles(showtaskRoles); const [showHelper, setShowHelper] = useState(false); - const columns = useMemo[]>( - () => [ - { - accessorFn: row => row.id, - id: 'name', - cell: cell => { - return ; - } - } - ], + const columns = useMemo( + () => + columnHelper.columns([ + columnHelper.accessor('id', { + id: 'name', + cell: cell => { + return ( + + ); + } + }) + ]), [] ); - const [expanded, setExpanded] = useState({ [0]: true, [1]: true, [2]: true, [3]: true }); - const [globalFilter, setGlobalFilter] = useState(''); - const [rowSelection, setRowSelection] = useState({}); - - const table = useReactTable({ + const table = useTable({ + ...tableOptions, data: roleItems, columns: columns, - state: { expanded, globalFilter, rowSelection }, - filterFromLeafRows: true, + initialState: { + expanded: { [0]: true, [1]: true, [2]: true, [3]: true } + }, enableRowSelection: true, enableMultiRowSelection: false, - enableSubRowSelection: false, - onExpandedChange: setExpanded, - onGlobalFilterChange: setGlobalFilter, - getSubRows: row => row.children, - getExpandedRowModel: getExpandedRowModel(), - onRowSelectionChange: setRowSelection, - getCoreRowModel: getCoreRowModel(), - getFilteredRowModel: getFilteredRowModel() + enableSubRowSelection: false }); const { handleKeyDown } = useTableKeyHandler({ table, data: roleItems }); useEffect(() => { - const selectedRow = table.getSelectedRowModel().flatRows[0]; - if (selectedRow === undefined) { - onChange({ value: '' }); - // eslint-disable-next-line @eslint-react/set-state-in-effect - setShowHelper(false); - return; - } + const subscription = table.atoms.rowSelection.subscribe(() => { + const selectedRow = table.getSelectedRowModel().flatRows[0]; + if (selectedRow === undefined) { + onChange({ value: '' }); + + setShowHelper(false); + return; + } - // eslint-disable-next-line @eslint-react/set-state-in-effect - setShowHelper(true); - onChange({ value: selectedRow.original.id }); - }, [onChange, rowSelection, table]); + setShowHelper(true); + onChange({ value: selectedRow.original.id }); + }); + return () => subscription.unsubscribe(); + }, [onChange, table]); const [addedRole, setAddedRole] = useState(''); @@ -107,8 +96,8 @@ const RoleBrowser = ({ value, showtaskRoles, onChange, onDoubleClick }: RoleBrow const newRow = table.getRowModel().flatRows.find(row => row.original.id === addedRole); if (newRow) { newRow.getParentRow()?.toggleExpanded(true); - // eslint-disable-next-line @eslint-react/set-state-in-effect - setRowSelection({ [newRow.id]: true }); + + table.setRowSelection({ [newRow.id]: true }); // eslint-disable-next-line @eslint-react/set-state-in-effect setAddedRole(''); } @@ -120,13 +109,10 @@ const RoleBrowser = ({ value, showtaskRoles, onChange, onDoubleClick }: RoleBrow { - setGlobalFilter(newFilterValue); - setRowSelection({}); - setExpanded(true); - } + table={table} + onSearchChange={filter => { + table.setExpanded(filter.length > 0 ? true : { [0]: true }); + table.setRowSelection({}); }} onKeyDown={e => handleKeyDown(e, onDoubleClick)} > diff --git a/packages/inscription-view/src/components/browser/role/validate-role.test.ts b/packages/inscription-view/src/components/browser/role/validate-role.test.ts index c931a6d16..226e8f53c 100644 --- a/packages/inscription-view/src/components/browser/role/validate-role.test.ts +++ b/packages/inscription-view/src/components/browser/role/validate-role.test.ts @@ -1,9 +1,10 @@ import type { RoleMeta } from '@axonivy/process-editor-inscription-protocol'; +import type { DataTableFeatures } from '@axonivy/ui-components'; import type { Table } from '@tanstack/react-table'; // replace with the actual path import { describe, expect, test } from 'vitest'; import { isValidRowSelected, newNameExists, newNameIsValid } from './validate-role'; -const testTable = (rows: RoleMeta[], selectedRow?: RoleMeta): Table => { +const testTable = (rows: RoleMeta[], selectedRow?: RoleMeta): Table => { return { getRowModel: () => ({ flatRows: rows.map(row => ({ original: row })) @@ -11,7 +12,7 @@ const testTable = (rows: RoleMeta[], selectedRow?: RoleMeta): Table => getSelectedRowModel: () => ({ flatRows: selectedRow ? [{ original: selectedRow }] : [] }) - } as Table; + } as Table; }; describe('newNameExists', () => { diff --git a/packages/inscription-view/src/components/browser/role/validate-role.ts b/packages/inscription-view/src/components/browser/role/validate-role.ts index 3de5420d9..1deac863f 100644 --- a/packages/inscription-view/src/components/browser/role/validate-role.ts +++ b/packages/inscription-view/src/components/browser/role/validate-role.ts @@ -1,11 +1,12 @@ import type { RoleMeta } from '@axonivy/process-editor-inscription-protocol'; +import type { DataTableFeatures } from '@axonivy/ui-components'; import type { Table } from '@tanstack/react-table'; -export const newNameExists = (table: Table, newRoleName: string): boolean => { +export const newNameExists = (table: Table, newRoleName: string): boolean => { return table.getRowModel().flatRows.some(row => row.original.id === newRoleName); }; -export const newNameIsValid = (table: Table, newRoleName: string): boolean => { +export const newNameIsValid = (table: Table, newRoleName: string): boolean => { if (newRoleName.length === 0 || newRoleName.trim().length === 0) { return false; } @@ -15,7 +16,7 @@ export const newNameIsValid = (table: Table, newRoleName: string): boo return true; }; -export const isValidRowSelected = (table: Table, taskRoles: RoleMeta[]): boolean => { +export const isValidRowSelected = (table: Table, taskRoles: RoleMeta[]): boolean => { const selectedRow = table.getSelectedRowModel().flatRows[0]?.original; if (!selectedRow) return false; diff --git a/packages/inscription-view/src/components/browser/tableCol/TableColBrowser.tsx b/packages/inscription-view/src/components/browser/tableCol/TableColBrowser.tsx index 0ef5fbf28..a3948dbe5 100644 --- a/packages/inscription-view/src/components/browser/tableCol/TableColBrowser.tsx +++ b/packages/inscription-view/src/components/browser/tableCol/TableColBrowser.tsx @@ -1,7 +1,7 @@ import type { DatabaseColumn } from '@axonivy/process-editor-inscription-protocol'; -import { TableBody, TableCell, TableRow, useTableKeyHandler } from '@axonivy/ui-components'; +import { dataTableHelper, TableBody, TableCell, TableRow, useTableKeyHandler } from '@axonivy/ui-components'; import { IvyIcons } from '@axonivy/ui-icons'; -import { getCoreRowModel, getFilteredRowModel, useReactTable, type ColumnDef, type RowSelectionState } from '@tanstack/react-table'; +import { useTable } from '@tanstack/react-table'; import { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useEditorContext } from '../../../context/useEditorContext'; @@ -31,6 +31,8 @@ type TableColumnBrowserProps = { onDoubleClick: () => void; }; +const { columnHelper, tableOptions } = dataTableHelper(); + const TableColumnBrowser = ({ value, onChange, onDoubleClick }: TableColumnBrowserProps) => { const { t } = useTranslation(); const { elementContext: context } = useEditorContext(); @@ -52,69 +54,50 @@ const TableColumnBrowser = ({ value, onChange, onDoubleClick }: TableColumnBrows setData(columns); }, [columnMetas, config.query.sql.select]); - const columns = useMemo[]>( - () => [ - { - accessorFn: row => row.name, - id: 'name', - cell: cell => { - return ( - <> - {cell.getValue() as string} - : {cell.row.original.type} - - ); - } - } - ], + const columns = useMemo( + () => + columnHelper.columns([ + columnHelper.accessor('name', { + cell: cell => { + return ( + <> + {cell.getValue()} + : {cell.row.original.type} + + ); + } + }) + ]), [] ); - const [globalFilter, setGlobalFilter] = useState(''); - const [rowSelection, setRowSelection] = useState({}); - - const table = useReactTable({ + const table = useTable({ + ...tableOptions, data: data, columns: columns, - state: { - globalFilter, - rowSelection - }, - filterFromLeafRows: true, enableRowSelection: true, enableMultiRowSelection: false, - enableSubRowSelection: false, - onGlobalFilterChange: setGlobalFilter, - onRowSelectionChange: setRowSelection, - getCoreRowModel: getCoreRowModel(), - getFilteredRowModel: getFilteredRowModel() + enableSubRowSelection: false }); const { handleKeyDown } = useTableKeyHandler({ table, data }); useEffect(() => { - const selectedRow = table.getSelectedRowModel().flatRows[0]; - if (selectedRow === undefined) { - onChange({ value: '' }); - // eslint-disable-next-line @eslint-react/set-state-in-effect - setShowHelper(false); - return; - } + const subscription = table.atoms.rowSelection.subscribe(() => { + const selectedRow = table.getSelectedRowModel().flatRows[0]; + if (selectedRow === undefined) { + onChange({ value: '' }); + setShowHelper(false); + return; + } - // eslint-disable-next-line @eslint-react/set-state-in-effect - setShowHelper(true); - onChange({ value: selectedRow.original.name, data: selectedRow.original }); - }, [onChange, rowSelection, table]); + setShowHelper(true); + onChange({ value: selectedRow.original.name, data: selectedRow.original }); + }); + return () => subscription.unsubscribe(); + }, [onChange, table]); return ( <> - { - setGlobalFilter(newFilterValue); - } - }} - onKeyDown={e => handleKeyDown(e, onDoubleClick)} - > + handleKeyDown(e, onDoubleClick)}> {table.getRowModel().rows.length > 0 ? ( table.getRowModel().rows.map(row => ) diff --git a/packages/inscription-view/src/components/browser/type/TypeBrowser.tsx b/packages/inscription-view/src/components/browser/type/TypeBrowser.tsx index efc22c774..05a69fcae 100644 --- a/packages/inscription-view/src/components/browser/type/TypeBrowser.tsx +++ b/packages/inscription-view/src/components/browser/type/TypeBrowser.tsx @@ -1,10 +1,10 @@ import type { DataclassType } from '@axonivy/process-editor-inscription-protocol'; -import { TableBody, TableCell, useTableKeyHandler, type BrowserNode } from '@axonivy/ui-components'; +import { dataTreeHelper, TableBody, TableCell, useTableKeyHandler, type BrowserNode, type DataTableFeatures } from '@axonivy/ui-components'; import { IvyIcons } from '@axonivy/ui-icons'; -import type { ColumnDef, ExpandedState, FilterFn, RowSelectionState } from '@tanstack/react-table'; -import { getCoreRowModel, getExpandedRowModel, getFilteredRowModel, useReactTable } from '@tanstack/react-table'; +import type { FilterFn } from '@tanstack/react-table'; +import { useTable } from '@tanstack/react-table'; import DOMPurify from 'dompurify'; -import { useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useEditorContext } from '../../../context/useEditorContext'; import { useMeta } from '../../../context/useMeta'; @@ -46,6 +46,8 @@ interface TypeBrowserProps { location: string; } +const { columnHelper, tableOptions } = dataTreeHelper>(); + const TypeBrowser = ({ value, onChange, onDoubleClick, initSearchFilter, location }: TypeBrowserProps) => { const { t } = useTranslation(); const { context } = useEditorContext(); @@ -69,109 +71,90 @@ const TypeBrowser = ({ value, onChange, onDoubleClick, initSearchFilter, locatio const [type, setType] = useState(''); const { data: doc } = useMeta('meta/scripting/apiDoc', { context, method: '', paramTypes: [], type }, ''); - const columns = useMemo, string>[]>( - () => [ - { - accessorKey: 'value', - cell: cell => ( - - ) - } - ], + const columns = useMemo( + () => + columnHelper.columns([ + columnHelper.accessor('value', { + cell: cell => ( + + ) + }) + ]), [] ); - const [expanded, setExpanded] = useState(true); - const [globalFilter, setGlobalFilter] = useState(initSearchFilter); - const [rowSelection, setRowSelection] = useState({}); - - const regexFilter: FilterFn> = (row, columnId, filterValue) => { + const regexFilter: FilterFn> = (row, _columnId, filterValue) => { const cellValue = row.original.value || ''; const regexPattern = new RegExp(filterValue.replace(/\*/g, '.*'), 'i'); return regexPattern.test(cellValue); }; - const tableDynamic = useReactTable({ + const table = useTable({ + ...tableOptions, data: types, columns: columns, - state: { - expanded, - globalFilter, - rowSelection + initialState: { + globalFilter: initSearchFilter, + expanded: true }, globalFilterFn: regexFilter, - filterFromLeafRows: true, enableRowSelection: true, enableMultiRowSelection: false, enableSubRowSelection: false, - enableFilters: true, - onExpandedChange: setExpanded, - getSubRows: row => row.children, - onGlobalFilterChange: setGlobalFilter, - onRowSelectionChange: setRowSelection, - getCoreRowModel: getCoreRowModel(), - getExpandedRowModel: getExpandedRowModel(), - getFilteredRowModel: getFilteredRowModel() + enableFilters: true }); - const { handleKeyDown } = useTableKeyHandler({ table: tableDynamic, data: types }); + const { handleKeyDown } = useTableKeyHandler({ table, data: types }); - useEffect(() => { - const selectedRow = tableDynamic.getSelectedRowModel().flatRows[0]; - if (selectedRow === undefined) { - onChange({ value: '' }); - // eslint-disable-next-line @eslint-react/set-state-in-effect - setShowHelper(false); - return; - } - - // eslint-disable-next-line @eslint-react/set-state-in-effect - setShowHelper(true); - const isIvyType = ivyTypes.some(javaClass => javaClass.fullQualifiedName === selectedRow.original.data?.fullQualifiedName); - - // eslint-disable-next-line @eslint-react/set-state-in-effect - setType(selectedRow.original.data?.fullQualifiedName ?? ''); - - if (location.includes('code')) { - onChange({ - value: getCursorValue(selectedRow.original, isIvyType, typeAsList, true), - firstLine: isIvyType ? undefined : 'import ' + selectedRow.original.data?.fullQualifiedName + ';\n' - }); - } else { - onChange({ - value: getCursorValue(selectedRow.original, isIvyType, typeAsList, false) - }); - } - }, [ivyTypes, location, onChange, rowSelection, tableDynamic, typeAsList]); - - const [debouncedFilterValue, setDebouncedFilterValue] = useState(''); + const updateChange = useCallback( + (typeAsList: boolean) => { + const selectedRow = table.getSelectedRowModel().flatRows[0]; + if (selectedRow === undefined) { + onChange({ value: '' }); - useEffect(() => { - const timer = setTimeout(() => { - setDebouncedFilterValue(globalFilter); - }, 150); + setShowHelper(false); + return; + } + + setShowHelper(true); + const isIvyType = ivyTypes.some(javaClass => javaClass.fullQualifiedName === selectedRow.original.data?.fullQualifiedName); - return () => { - clearTimeout(timer); - }; - }, [globalFilter]); + setType(selectedRow.original.data?.fullQualifiedName ?? ''); + + if (location.includes('code')) { + onChange({ + value: getCursorValue(selectedRow.original, isIvyType, typeAsList, true), + firstLine: isIvyType ? undefined : 'import ' + selectedRow.original.data?.fullQualifiedName + ';\n' + }); + } else { + onChange({ + value: getCursorValue(selectedRow.original, isIvyType, typeAsList, false) + }); + } + }, + [ivyTypes, location, onChange, table] + ); + + useEffect(() => { + const subscription = table.atoms.rowSelection.subscribe(() => updateChange(typeAsList)); + return () => subscription.unsubscribe(); + }, [table.atoms.rowSelection, updateChange, typeAsList]); useEffect(() => { - if (debouncedFilterValue.length > 0) { - // eslint-disable-next-line @eslint-react/set-state-in-effect - setMainFilter(debouncedFilterValue); - } else { - // eslint-disable-next-line @eslint-react/set-state-in-effect - setMainFilter(''); - } - // eslint-disable-next-line @eslint-react/set-state-in-effect - setExpanded(true); - }, [debouncedFilterValue]); + table.atoms.globalFilter.subscribe(() => { + const timer = setTimeout(() => { + setMainFilter(table.atoms.globalFilter.get()); + table.setExpanded(true); + }, 150); + + return () => clearTimeout(timer); + }); + }, [table]); return ( <> @@ -181,25 +164,15 @@ const TypeBrowser = ({ value, onChange, onDoubleClick, initSearchFilter, locatio value={allTypesSearchActive} onChange={() => { setAllTypesSearchActive(!allTypesSearchActive); - setRowSelection({}); + table.setRowSelection({}); }} /> - { - setGlobalFilter(newFilterValue); - setRowSelection({}); - } - }} - onKeyDown={e => handleKeyDown(e, onDoubleClick)} - > + table.setRowSelection({})} onKeyDown={e => handleKeyDown(e, onDoubleClick)}> - {tableDynamic.getRowModel().rows.length > 0 ? ( + {table.getRowModel().rows.length > 0 ? ( <> - {!isFetching && - tableDynamic.getRowModel().rows.map(row => )} + {!isFetching && table.getRowModel().rows.map(row => )} ) : ( @@ -220,7 +193,14 @@ const TypeBrowser = ({ value, onChange, onDoubleClick, initSearchFilter, locatio )} - setTypeAsList(!typeAsList)} /> + { + setTypeAsList(!typeAsList); + updateChange(!typeAsList); + }} + /> ); }; diff --git a/packages/inscription-view/src/components/parts/common/customfield/CustomFieldTable.tsx b/packages/inscription-view/src/components/parts/common/customfield/CustomFieldTable.tsx index 1ed646f46..bee7055cd 100644 --- a/packages/inscription-view/src/components/parts/common/customfield/CustomFieldTable.tsx +++ b/packages/inscription-view/src/components/parts/common/customfield/CustomFieldTable.tsx @@ -1,8 +1,16 @@ import type { WfCustomField, WorkflowType } from '@axonivy/process-editor-inscription-protocol'; import { CUSTOM_FIELD_TYPE } from '@axonivy/process-editor-inscription-protocol'; -import { ComboCell, SelectCell, SortableHeader, Table, TableBody, TableCell, TableResizableHeader } from '@axonivy/ui-components'; +import { + ComboCell, + dataTableHelper, + SelectCell, + SortableHeader, + Table, + TableBody, + TableCell, + TableResizableHeader +} from '@axonivy/ui-components'; import { IvyIcons } from '@axonivy/ui-icons'; -import type { ColumnDef } from '@tanstack/react-table'; import { flexRender } from '@tanstack/react-table'; import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -23,6 +31,8 @@ type CustomFieldTableProps = { const EMPTY_WFCUSTOMFIELD: WfCustomField = { name: '', type: 'STRING', value: '' } as const; +const { columnHelper } = dataTableHelper(); + const CustomFieldTable = ({ data, onChange, type }: CustomFieldTableProps) => { const { t } = useTranslation(); const items = useMemo(() => Object.entries(CUSTOM_FIELD_TYPE).map(([value, label]) => ({ label, value })), []); @@ -31,29 +41,29 @@ const CustomFieldTable = ({ data, onChange, type }: CustomFieldTableProps) => { const predefinedCustomField: WfCustomField[] = useMeta('meta/workflow/customFields', { context, type: type }, []).data; - const columns = useMemo[]>( - () => [ - { - accessorKey: 'name', - header: ({ column }) => , - cell: cell => ( - !data.find(d => d.name === pcf.name)).map(pcf => ({ value: pcf.name }))} - cell={cell} - /> - ) - }, - { - accessorKey: 'type', - header: ({ column }) => , - cell: cell => - }, - { - accessorKey: 'value', - header: ({ column }) => , - cell: cell => - } - ], + const columns = useMemo( + () => + columnHelper.columns([ + columnHelper.accessor('name', { + header: ({ column }) => , + cell: cell => ( + !data.find(d => d.name === pcf.name)).map(pcf => ({ value: pcf.name }))} + cell={cell} + /> + ) + }), + columnHelper.accessor('type', { + header: ({ column }) => , + cell: cell => + }), + columnHelper.accessor('value', { + header: ({ column }) => , + cell: cell => ( + + ) + }) + ]), [data, items, predefinedCustomField, t] ); @@ -73,7 +83,7 @@ const CustomFieldTable = ({ data, onChange, type }: CustomFieldTableProps) => { }); }; - const { table, setRowSelection, selectedRowActions, showAddButton } = useResizableEditableTable({ + const { table, selectedRowActions, showAddButton } = useResizableEditableTable({ data, columns, onChange, @@ -95,7 +105,7 @@ const CustomFieldTable = ({ data, onChange, type }: CustomFieldTableProps) => { 0} controls={tableActions}>
- setRowSelection({})} /> + table.setRowSelection({})} /> {table.getRowModel().rows.map(row => ( diff --git a/packages/inscription-view/src/components/parts/common/customfield/StartCustomFieldTable.tsx b/packages/inscription-view/src/components/parts/common/customfield/StartCustomFieldTable.tsx index 1254e1bcb..9214c4729 100644 --- a/packages/inscription-view/src/components/parts/common/customfield/StartCustomFieldTable.tsx +++ b/packages/inscription-view/src/components/parts/common/customfield/StartCustomFieldTable.tsx @@ -1,7 +1,6 @@ import type { StartCustomStartField } from '@axonivy/process-editor-inscription-protocol'; -import { ComboCell, SortableHeader, Table, TableBody, TableCell, TableResizableHeader } from '@axonivy/ui-components'; +import { ComboCell, dataTableHelper, SortableHeader, Table, TableBody, TableCell, TableResizableHeader } from '@axonivy/ui-components'; import { IvyIcons } from '@axonivy/ui-icons'; -import type { ColumnDef } from '@tanstack/react-table'; import { flexRender } from '@tanstack/react-table'; import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -21,6 +20,8 @@ type StartCustomFieldTableProps = { const EMPTY_STARTCUSTOMSTARTFIELD: StartCustomStartField = { name: '', value: '' } as const; +const { columnHelper } = dataTableHelper(); + const StartCustomFieldTable = ({ data, onChange }: StartCustomFieldTableProps) => { const { t } = useTranslation(); const { context } = useEditorContext(); @@ -32,23 +33,22 @@ const StartCustomFieldTable = ({ data, onChange }: StartCustomFieldTableProps) = value: pcf.name })); - const columns = useMemo[]>( - () => [ - { - accessorKey: 'name', - header: ({ column }) => , - cell: cell => !data.find(d => d.name === pcf.value))} /> - }, - { - accessorKey: 'value', - header: ({ column }) => , - cell: cell => - } - ], + const columns = useMemo( + () => + columnHelper.columns([ + columnHelper.accessor('name', { + header: ({ column }) => , + cell: cell => !data.find(d => d.name === pcf.value))} /> + }), + columnHelper.accessor('value', { + header: ({ column }) => , + cell: cell => + }) + ]), [data, predefinedCustomField, t] ); - const { table, setRowSelection, selectedRowActions, showAddButton } = useResizableEditableTable({ + const { table, selectedRowActions, showAddButton } = useResizableEditableTable({ data, columns, onChange, @@ -69,7 +69,7 @@ const StartCustomFieldTable = ({ data, onChange }: StartCustomFieldTableProps) = 0}>
- setRowSelection({})} /> + table.setRowSelection({})} /> {table.getRowModel().rows.map(row => ( diff --git a/packages/inscription-view/src/components/parts/common/mapping-tree/MappingPart.tsx b/packages/inscription-view/src/components/parts/common/mapping-tree/MappingPart.tsx index ce13d29f7..ffc60ac72 100644 --- a/packages/inscription-view/src/components/parts/common/mapping-tree/MappingPart.tsx +++ b/packages/inscription-view/src/components/parts/common/mapping-tree/MappingPart.tsx @@ -7,7 +7,7 @@ import type { BrowserType } from '../../../browser/useBrowser'; import Fieldset from '../../../widgets/fieldset/Fieldset'; import { PathCollapsible } from '../path/PathCollapsible'; import MappingTree from './MappingTree'; -import { useTableGlobalFilter, useTableOnlyInscribed } from './useMappingTree'; +import { useMappingTree, useTableGlobalFilter, useTableOnlyInscribed } from './useMappingTree'; export type MappingPartProps = { data: Record; @@ -20,8 +20,9 @@ export type MappingPartProps = { const MappingPart = ({ path, data, defaultData, defaultOpen, ...props }: MappingPartProps & { defaultData: Record }) => { const { t } = useTranslation(); - const globalFilter = useTableGlobalFilter(); - const onlyInscribedFilter = useTableOnlyInscribed(); + const tree = useMappingTree(data, props.variableInfo, props.onChange, props.browsers); + const globalFilter = useTableGlobalFilter(tree); + const onlyInscribedFilter = useTableOnlyInscribed(tree); return ( - + ); }; export const MappingField = ({ path, data, ...props }: MappingPartProps) => { const { t } = useTranslation(); - const globalFilter = useTableGlobalFilter(); - const onlyInscribedFilter = useTableOnlyInscribed(); + const tree = useMappingTree(data, props.variableInfo, props.onChange, props.browsers); + const globalFilter = useTableGlobalFilter(tree); + const onlyInscribedFilter = useTableOnlyInscribed(tree); return (
- +
); diff --git a/packages/inscription-view/src/components/parts/common/mapping-tree/MappingTree.tsx b/packages/inscription-view/src/components/parts/common/mapping-tree/MappingTree.tsx index dedd1e66c..ec5d7bf76 100644 --- a/packages/inscription-view/src/components/parts/common/mapping-tree/MappingTree.tsx +++ b/packages/inscription-view/src/components/parts/common/mapping-tree/MappingTree.tsx @@ -1,131 +1,27 @@ -import { ExpandableHeader, TableBody, TableCell, TableResizableHeader } from '@axonivy/ui-components'; -import type { ColumnDef, ColumnFiltersState, ExpandedState, RowSelectionState } from '@tanstack/react-table'; -import { - flexRender, - getCoreRowModel, - getExpandedRowModel, - getFilteredRowModel, - getSortedRowModel, - useReactTable -} from '@tanstack/react-table'; -import { memo, useCallback, useEffect, useMemo, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { ExpandableCell } from '../../../widgets/table/cell/ExpandableCell'; -import { ScriptCell } from '../../../widgets/table/cell/ScriptCell'; +import { TableBody, TableCell, TableResizableHeader, type DataTableFeatures } from '@axonivy/ui-components'; +import type { ReactTable } from '@tanstack/react-table'; +import { flexRender } from '@tanstack/react-table'; +import { memo } from 'react'; import { SearchTable } from '../../../widgets/table/table/Table'; import { ValidationRow } from '../path/validation/ValidationRow'; import { MappingTreeData } from './mapping-tree-data'; -import type { MappingPartProps } from './MappingPart'; -import type { TableFilter } from './useMappingTree'; -import { calcFullPathId, expandState } from './useMappingTree'; +import { calcFullPathId } from './useMappingTree'; -type MappingTreeProps = MappingPartProps & { - globalFilter: TableFilter; - onlyInscribedFilter: TableFilter; +type MappingTreeProps = { + tree: ReactTable; + globalFilterActive: boolean; }; -const MappingTree = ({ data, variableInfo, onChange, globalFilter, onlyInscribedFilter, browsers }: MappingTreeProps) => { - const { t } = useTranslation(); - const [tree, setTree] = useState([]); - const [updateExpanded, setUpdateExpanded] = useState(true); - const [expanded, setExpanded] = useState({ 0: true }); - const [rowSelection, setRowSelection] = useState({}); - - useEffect(() => { - const treeData = MappingTreeData.of(variableInfo); - Object.entries(data).forEach(mapping => MappingTreeData.update(variableInfo, treeData, mapping[0].split('.'), mapping[1])); - // eslint-disable-next-line @eslint-react/set-state-in-effect - setTree(treeData); - if (updateExpanded) { - // eslint-disable-next-line @eslint-react/set-state-in-effect - setExpanded(expandState(treeData)); - } - }, [data, updateExpanded, variableInfo]); - - const loadChildren = useCallback<(row: MappingTreeData) => void>( - row => setTree(tree => MappingTreeData.loadChildrenFor(variableInfo, row.type, tree)), - [variableInfo, setTree] - ); - - const columns = useMemo[]>( - () => [ - { - accessorFn: row => row.attribute, - id: 'attribute', - header: header => , - cell: cell => ( - loadChildren(cell.row.original)} - isUnknown={cell.row.original.type.length === 0} - title={cell.row.original.description} - /> - ), - size: 100, - minSize: 60 - }, - { - accessorFn: row => row.value, - id: 'value', - header: () => {t('label.expression')}, - cell: cell => , - filterFn: (row, columnId, filterValue) => filterValue || row.original.value.length > 0 - } - ], - [browsers, loadChildren, t] - ); - - const table = useReactTable({ - data: tree, - columns: columns, - state: { - expanded, - rowSelection, - globalFilter: globalFilter.filter, - columnFilters: onlyInscribedFilter.filter - }, - filterFromLeafRows: true, - columnResizeMode: 'onChange', - columnResizeDirection: 'ltr', - enableRowSelection: true, - enableMultiRowSelection: false, - enableSubRowSelection: false, - onRowSelectionChange: setRowSelection, - onExpandedChange: setExpanded, - onGlobalFilterChange: globalFilter.setFilter, - onColumnFiltersChange: onlyInscribedFilter.setFilter, - getSubRows: row => row.children, - getCoreRowModel: getCoreRowModel(), - getSortedRowModel: getSortedRowModel(), - getExpandedRowModel: getExpandedRowModel(), - getFilteredRowModel: getFilteredRowModel(), - meta: { - updateData: (rowId: string, columnId: string, value: string) => { - const rowIndex = rowId.split('.').map(parseFloat); - setUpdateExpanded(false); - onChange(MappingTreeData.to(MappingTreeData.updateDeep(tree, rowIndex, columnId, value))); - } - } - }); - +const MappingTree = ({ tree, globalFilterActive }: MappingTreeProps) => { return ( { - setExpanded(change.length > 0 ? true : { 0: true }); - globalFilter.setFilter(change); - } - } - : undefined - } + table={tree} + searchActive={globalFilterActive} + onSearchChange={filter => tree.setExpanded(filter.length > 0 ? true : { 0: true })} > - setRowSelection({})} /> + tree.setRowSelection({})} /> - {table.getRowModel().rows.map(row => ( + {tree.getRowModel().rows.map(row => ( {row.getVisibleCells().map(cell => ( diff --git a/packages/inscription-view/src/components/parts/common/mapping-tree/useMappingTree.test.ts b/packages/inscription-view/src/components/parts/common/mapping-tree/useMappingTree.test.ts index 51b332f4c..4a1affa1f 100644 --- a/packages/inscription-view/src/components/parts/common/mapping-tree/useMappingTree.test.ts +++ b/packages/inscription-view/src/components/parts/common/mapping-tree/useMappingTree.test.ts @@ -28,8 +28,35 @@ const tree = (mailValue = ''): MappingTreeData[] => [ } ]; +const treeWithDirectIdValue: MappingTreeData[] = [ + { + attribute: 'param', + children: [ + { + attribute: 'category', + children: [ + { attribute: 'id', children: [], value: '', type: 'Long', simpleType: 'Long', isLoaded: true, description: '' }, + { attribute: 'name', children: [], value: '', type: 'String', simpleType: 'String', isLoaded: true, description: '' } + ], + value: '', + type: 'api.v3.client.Category', + simpleType: 'Category', + isLoaded: true, + description: '' + }, + { attribute: 'id', children: [], value: 'CH', type: 'String', simpleType: 'String', isLoaded: true, description: '' } + ], + value: '', + type: 'api.v3.client.Pet', + simpleType: 'Pet', + isLoaded: true, + description: '' + } +]; + test('expandState', () => { expect(expandState([])).toEqual({ '0': true }); expect(expandState(tree())).toEqual({ '0': true }); - expect(expandState(tree('louis'))).toEqual({ '0': true, '0.2': true, '0.2.0': true }); + expect(expandState(tree('louis'))).toEqual({ '0': true, '0.2': true }); + expect(expandState(treeWithDirectIdValue)).toEqual({ '0': true }); }); diff --git a/packages/inscription-view/src/components/parts/common/mapping-tree/useMappingTree.ts b/packages/inscription-view/src/components/parts/common/mapping-tree/useMappingTree.ts deleted file mode 100644 index 5f0b2ea75..000000000 --- a/packages/inscription-view/src/components/parts/common/mapping-tree/useMappingTree.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { IvyIcons } from '@axonivy/ui-icons'; -import type { ColumnFiltersState, ExpandedStateList, Row } from '@tanstack/react-table'; -import type { Dispatch } from 'react'; -import { useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import type { FieldsetControl } from '../../../widgets/fieldset/fieldset-control'; -import { MappingTreeData } from './mapping-tree-data'; - -export type TableFilter = { - active: boolean; - filter: TFilter; - setFilter: Dispatch>; - control: FieldsetControl; -}; - -export const useTableGlobalFilter = (): TableFilter => { - const { t } = useTranslation(); - const [active, setActive] = useState(false); - const [filter, setFilter] = useState(''); - return { - active: active, - filter, - setFilter, - control: { - label: t('common.label.search'), - icon: IvyIcons.Search, - active, - action: () => { - setActive(show => !show); - setFilter(''); - } - } - }; -}; - -export const useTableOnlyInscribed = (): TableFilter => { - const { t } = useTranslation(); - const [active, setActive] = useState(false); - const [filter, setFilter] = useState([]); - return { - active, - filter, - setFilter, - control: { - label: t('label.mapped'), - icon: IvyIcons.Rule, - active, - action: () => { - setActive(show => !show); - setFilter([{ id: 'value', value: active }]); - } - } - }; -}; - -export const calcFullPathId = (row: Row) => { - return [...row.getParentRows().map(parent => parent.original.attribute), row.original.attribute].join('.'); -}; - -export const expandState = (tree: Array, path: string = '', expanded: ExpandedStateList = {}): ExpandedStateList => { - expandStateDeep(tree, path, expanded); - expanded['0'] = true; - return expanded; -}; - -const expandStateDeep = (tree: Array, path: string, expanded: ExpandedStateList): boolean => { - let changed = false; - tree.forEach((node, index) => { - const id = `${path}${index}`; - const childExpanded = expandStateDeep(node.children, `${id}.`, expanded); - if (node.value.length > 0 || childExpanded) { - expanded[id] = true; - changed = true; - } - }); - return changed; -}; diff --git a/packages/inscription-view/src/components/parts/common/mapping-tree/useMappingTree.tsx b/packages/inscription-view/src/components/parts/common/mapping-tree/useMappingTree.tsx new file mode 100644 index 000000000..25e51e54f --- /dev/null +++ b/packages/inscription-view/src/components/parts/common/mapping-tree/useMappingTree.tsx @@ -0,0 +1,169 @@ +import type { VariableInfo } from '@axonivy/process-editor-inscription-protocol'; +import { dataTreeHelper, ExpandableHeader, type DataTableFeatures } from '@axonivy/ui-components'; +import { IvyIcons } from '@axonivy/ui-icons'; +import { useTable, type ExpandedStateList, type ReactTable, type Row } from '@tanstack/react-table'; +import { useCallback, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { deepEqual } from '../../../../utils/equals'; +import type { BrowserType } from '../../../browser/useBrowser'; +import type { FieldsetControl } from '../../../widgets/fieldset/fieldset-control'; +import { ExpandableCell } from '../../../widgets/table/cell/ExpandableCell'; +import { ScriptCell } from '../../../widgets/table/cell/ScriptCell'; +import { MappingTreeData } from './mapping-tree-data'; + +const { columnHelper, tableOptions } = dataTreeHelper(); + +export const useMappingTree = ( + data: Record, + variableInfo: VariableInfo, + onChange: (change: Record) => void, + browsers: BrowserType[] +) => { + const { t } = useTranslation(); + const [treeData, setTreeData] = useState(() => createTreeData(variableInfo, data)); + const [prevData, setPrevData] = useState(data); + const [prevVariableInfo, setPrevVariableInfo] = useState(variableInfo); + const [updateExpanded, setUpdateExpanded] = useState(true); + + const loadChildren = useCallback<(row: MappingTreeData) => void>( + row => setTreeData(tree => MappingTreeData.loadChildrenFor(variableInfo, row.type, tree)), + [variableInfo, setTreeData] + ); + + const columns = useMemo( + () => + columnHelper.columns([ + columnHelper.accessor('attribute', { + header: header => , + cell: cell => ( + loadChildren(cell.row.original)} + isUnknown={cell.row.original.type.length === 0} + title={cell.row.original.description} + /> + ), + size: 100, + minSize: 60 + }), + columnHelper.accessor('value', { + header: () => {t('label.expression')}, + cell: cell => , + filterFn: (row, columnId, filterValue) => filterValue || row.original.value.length > 0 + }) + ]), + [browsers, loadChildren, t] + ); + + const tree = useTable({ + ...tableOptions, + data: treeData, + columns: columns, + filterFromLeafRows: true, + columnResizeMode: 'onChange', + columnResizeDirection: 'ltr', + enableRowSelection: true, + enableMultiRowSelection: false, + enableSubRowSelection: false, + initialState: { + expanded: expandState(treeData), + columnFilters: [{ id: 'value', value: true }] + }, + meta: { + updateData: (rowId: string, columnId: string, value: unknown) => { + if (typeof value !== 'string') { + return; + } + const rowIndex = rowId.split('.').map(parseFloat); + setUpdateExpanded(false); + onChange(MappingTreeData.to(MappingTreeData.updateDeep(treeData, rowIndex, columnId, value))); + } + } + }); + + if (!deepEqual(data, prevData) || !deepEqual(variableInfo, prevVariableInfo)) { + setPrevData(data); + setPrevVariableInfo(variableInfo); + const treeData = createTreeData(variableInfo, data); + setTreeData(treeData); + if (updateExpanded) { + tree.setExpanded(expandState(treeData)); + } + } + + return tree; +}; + +const createTreeData = (variableInfo: VariableInfo, data: Record): MappingTreeData[] => { + const treeData = MappingTreeData.of(variableInfo); + Object.entries(data).forEach(mapping => MappingTreeData.update(variableInfo, treeData, mapping[0].split('.'), mapping[1])); + return treeData; +}; + +export type TableFilter = { + active: boolean; + control: FieldsetControl; +}; + +export const useTableGlobalFilter = (table: ReactTable): TableFilter => { + const { t } = useTranslation(); + const [active, setActive] = useState(false); + return { + active: active, + control: { + label: t('common.label.search'), + icon: IvyIcons.Search, + active, + action: () => { + setActive(show => !show); + table.setGlobalFilter(''); + } + } + }; +}; + +export const useTableOnlyInscribed = (table: ReactTable): TableFilter => { + const { t } = useTranslation(); + const [active, setActive] = useState(false); + return { + active, + control: { + label: t('label.mapped'), + icon: IvyIcons.Rule, + active, + action: () => { + setActive(show => { + const newShow = !show; + table.setColumnFilters([{ id: 'value', value: show }]); + return newShow; + }); + } + } + }; +}; + +export const calcFullPathId = (row: Row) => { + return [...row.getParentRows().map(parent => parent.original.attribute), row.original.attribute].join('.'); +}; + +export const expandState = (tree: Array, path: string = '', expanded: ExpandedStateList = {}): ExpandedStateList => { + expandStateDeep(tree, path, expanded); + expanded['0'] = true; + return expanded; +}; + +const expandStateDeep = (tree: Array, path: string, expanded: ExpandedStateList): boolean => { + let changed = false; + tree.forEach((node, index) => { + const id = `${path}${index}`; + const childExpanded = expandStateDeep(node.children, `${id}.`, expanded); + if (node.value.length > 0 || childExpanded) { + if (node.children.length > 0) { + expanded[id] = true; + } + changed = true; + } + }); + return changed; +}; diff --git a/packages/inscription-view/src/components/parts/common/parameter/ParameterTable.tsx b/packages/inscription-view/src/components/parts/common/parameter/ParameterTable.tsx index f5e4083c1..b8ebf27e6 100644 --- a/packages/inscription-view/src/components/parts/common/parameter/ParameterTable.tsx +++ b/packages/inscription-view/src/components/parts/common/parameter/ParameterTable.tsx @@ -1,6 +1,5 @@ import type { ScriptVariable } from '@axonivy/process-editor-inscription-protocol'; -import { InputCell, SortableHeader, Table, TableBody, TableCell, TableResizableHeader } from '@axonivy/ui-components'; -import type { ColumnDef } from '@tanstack/react-table'; +import { dataTableHelper, InputCell, SortableHeader, Table, TableBody, TableCell, TableResizableHeader } from '@axonivy/ui-components'; import { flexRender } from '@tanstack/react-table'; import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -18,32 +17,33 @@ type ParameterTableProps = { const EMPTY_SCRIPT_VARIABLE: ScriptVariable = { name: '', type: 'String', desc: '' } as const; +const { columnHelper } = dataTableHelper(); + const ParameterTable = ({ data, onChange, hideDesc, label }: ParameterTableProps) => { const { t } = useTranslation(); const columns = useMemo(() => { - const colDef: ColumnDef[] = [ - { - accessorKey: 'name', + const colDef = columnHelper.columns([ + columnHelper.accessor('name', { header: ({ column }) => , cell: cell => - }, - { - accessorKey: 'type', + }), + columnHelper.accessor('type', { header: ({ column }) => , cell: cell => - } - ]; + }) + ]); if (hideDesc === undefined || !hideDesc) { - colDef.push({ - accessorKey: 'desc', - header: ({ column }) => , - cell: cell => - }); + colDef.push( + columnHelper.accessor('desc', { + header: ({ column }) => , + cell: cell => + }) + ); } return colDef; }, [hideDesc, t]); - const { table, setRowSelection, selectedRowActions, showAddButton } = useResizableEditableTable({ + const { table, selectedRowActions, showAddButton } = useResizableEditableTable({ data, columns, onChange, @@ -54,7 +54,7 @@ const ParameterTable = ({ data, onChange, hideDesc, label }: ParameterTableProps
- setRowSelection({})} /> + table.setRowSelection({})} /> {table.getRowModel().rows.map(row => ( diff --git a/packages/inscription-view/src/components/parts/common/path/validation/ValidationRow.test.tsx b/packages/inscription-view/src/components/parts/common/path/validation/ValidationRow.test.tsx index 89b357527..155818eba 100644 --- a/packages/inscription-view/src/components/parts/common/path/validation/ValidationRow.test.tsx +++ b/packages/inscription-view/src/components/parts/common/path/validation/ValidationRow.test.tsx @@ -1,4 +1,5 @@ import type { ValidationResult } from '@axonivy/process-editor-inscription-protocol'; +import type { DataTableFeatures } from '@axonivy/ui-components'; import type { Row } from '@tanstack/react-table'; import { customRender, screen } from 'test-utils'; import { describe, expect, test } from 'vitest'; @@ -11,7 +12,7 @@ describe('ValidationRow', () => {
false, getVisibleCells: () => [{}] } as Row} + row={{ getIsSelected: () => false, getVisibleCells: () => [{}] } as Row>} rowPathSuffix={path} title='this is a title' > diff --git a/packages/inscription-view/src/components/parts/common/path/validation/ValidationRow.tsx b/packages/inscription-view/src/components/parts/common/path/validation/ValidationRow.tsx index 33701c315..decfcc7d3 100644 --- a/packages/inscription-view/src/components/parts/common/path/validation/ValidationRow.tsx +++ b/packages/inscription-view/src/components/parts/common/path/validation/ValidationRow.tsx @@ -1,18 +1,18 @@ -import type { TableRow } from '@axonivy/ui-components'; +import type { DataTableFeatures, TableRow } from '@axonivy/ui-components'; import { MessageRow, ReorderRow, SelectRow } from '@axonivy/ui-components'; -import type { Row } from '@tanstack/react-table'; +import type { Row, RowData } from '@tanstack/react-table'; import type { ComponentPropsWithoutRef } from 'react'; import { mergePaths, usePath } from '../../../../../context/usePath'; import { useValidations } from '../../../../../context/useValidation'; import { toMessageData, type ValidationMessage } from '../../../../widgets/message/Message'; import './ValidationRow.css'; -type ValidationProps = { - row: Row; +type ValidationProps = { + row: Row; rowPathSuffix: string | number; }; -type ValidationRowProps = ComponentPropsWithoutRef & ValidationProps; +type ValidationRowProps = ComponentPropsWithoutRef & ValidationProps; const styleMessageRow = (validation?: ValidationMessage) => { if (validation) { @@ -28,7 +28,7 @@ const useValidationRow = (rowPathSuffix: string | number) => { return validations.find(val => val.path === rowPath); }; -export const ValidationRow = ({ rowPathSuffix, row, ...props }: ValidationRowProps) => { +export const ValidationRow = ({ rowPathSuffix, row, ...props }: ValidationRowProps) => { const validation = useValidationRow(rowPathSuffix); return ( <> @@ -38,9 +38,9 @@ export const ValidationRow = ({ rowPathSuffix, row, ...pro ); }; -type ValidationReorderRowProps = ComponentPropsWithoutRef> & ValidationProps; +type ValidationReorderRowProps = ComponentPropsWithoutRef> & ValidationProps; -export const ValidationSelectableReorderRow = ({ +export const ValidationSelectableReorderRow = ({ rowPathSuffix, row, ...props diff --git a/packages/inscription-view/src/components/parts/common/properties/PropertyTable.tsx b/packages/inscription-view/src/components/parts/common/properties/PropertyTable.tsx index 133a1c515..834ea81e9 100644 --- a/packages/inscription-view/src/components/parts/common/properties/PropertyTable.tsx +++ b/packages/inscription-view/src/components/parts/common/properties/PropertyTable.tsx @@ -1,10 +1,18 @@ import type { ScriptMappings } from '@axonivy/process-editor-inscription-protocol'; import { IVY_SCRIPT_TYPES } from '@axonivy/process-editor-inscription-protocol'; -import { ComboCell, SortableHeader, Table, TableAddRow, TableBody, TableCell, TableResizableHeader } from '@axonivy/ui-components'; +import { + ComboCell, + dataTableHelper, + SortableHeader, + Table, + TableAddRow, + TableBody, + TableCell, + TableResizableHeader +} from '@axonivy/ui-components'; import { IvyIcons } from '@axonivy/ui-icons'; -import type { ColumnDef, RowSelectionState, SortingState } from '@tanstack/react-table'; -import { flexRender, getCoreRowModel, getSortedRowModel, useReactTable } from '@tanstack/react-table'; -import { useMemo, useState } from 'react'; +import { flexRender, useTable } from '@tanstack/react-table'; +import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { deepEqual } from '../../../../utils/equals'; import type { ComboboxItem } from '../../../widgets/combobox/Combobox'; @@ -25,6 +33,8 @@ type PropertyTableProps = { const EMPTY_PROPERTY: Property = { expression: '', name: '' }; +const { columnHelper, tableOptions } = dataTableHelper(); + export const PropertyTable = ({ properties, update, knownProperties, hideProperties, label, defaultOpen }: PropertyTableProps) => { const { t } = useTranslation(); const data = useMemo(() => Property.of(properties), [properties]); @@ -33,32 +43,28 @@ export const PropertyTable = ({ properties, update, knownProperties, hidePropert const knownPropertyItems = knownProperties.map(prop => ({ value: prop })); - const columns = useMemo[]>( - () => [ - { - accessorKey: 'name', - header: ({ column }) => , - cell: cell => - }, - { - accessorKey: 'expression', - header: ({ column }) => , - cell: cell => ( - - ) - } - ], + const columns = useMemo( + () => + columnHelper.columns([ + columnHelper.accessor('name', { + header: ({ column }) => , + cell: cell => + }), + columnHelper.accessor('expression', { + header: ({ column }) => , + cell: cell => ( + + ) + }) + ]), [knownPropertyItems, t] ); - const [sorting, setSorting] = useState([]); - const [rowSelection, setRowSelection] = useState({}); - const showAddButton = () => { return data.filter(obj => deepEqual(obj, EMPTY_PROPERTY)).length === 0; }; @@ -67,61 +73,57 @@ export const PropertyTable = ({ properties, update, knownProperties, hidePropert const newData = [...data]; newData.push(EMPTY_PROPERTY); onChange(newData); - setRowSelection({ [`${newData.length - 1}`]: true }); + table.setRowSelection({ [`${newData.length - 1}`]: true }); }; const removeRow = (index: number) => { const newData = [...data]; newData.splice(index, 1); if (newData.length === 0) { - setRowSelection({}); + table.setRowSelection({}); } else if (index === data.length - 1) { - setRowSelection({ [`${newData.length - 1}`]: true }); + table.setRowSelection({ [`${newData.length - 1}`]: true }); } onChange(newData); }; - const table = useReactTable({ + const table = useTable({ + ...tableOptions, data, columns, - state: { sorting, rowSelection }, columnResizeMode: 'onChange', columnResizeDirection: 'ltr', enableRowSelection: true, enableMultiRowSelection: false, enableSubRowSelection: false, - onRowSelectionChange: setRowSelection, - onSortingChange: setSorting, - getCoreRowModel: getCoreRowModel(), - getSortedRowModel: getSortedRowModel(), meta: { - updateData: (rowId: string, columnId: string, value: string) => { + updateData: (rowId: string, columnId: string, value: unknown) => { + if (typeof value !== 'string') { + return; + } const rowIndex = parseInt(rowId); onChange(Property.update(data, rowIndex, columnId, value)); } } }); - const firstSelectionId = Object.keys(rowSelection)[0]; + const firstSelectedRow = table.getSelectedRowModel().rows[0]; let tableActions: FieldsetControl[] = []; - if (firstSelectionId) { - const firstSelectionRow = table.getRowModel().rowsById[firstSelectionId]; - if (firstSelectionRow) { - tableActions = [ - { - label: t('label.removeRow'), - icon: IvyIcons.Trash, - action: () => removeRow(firstSelectionRow?.index) - } - ]; - } + if (firstSelectedRow) { + tableActions = [ + { + label: t('label.removeRow'), + icon: IvyIcons.Trash, + action: () => removeRow(firstSelectedRow?.index) + } + ]; } return (
- setRowSelection({})} /> + table.setRowSelection({})} /> {table.getRowModel().rows.map(row => { if (hideProperties?.includes(row.original.name)) { diff --git a/packages/inscription-view/src/components/parts/common/table/useResizableEditableTable.tsx b/packages/inscription-view/src/components/parts/common/table/useResizableEditableTable.tsx index 60ef96cd7..928c5688f 100644 --- a/packages/inscription-view/src/components/parts/common/table/useResizableEditableTable.tsx +++ b/packages/inscription-view/src/components/parts/common/table/useResizableEditableTable.tsx @@ -1,22 +1,24 @@ -import { TableAddRow } from '@axonivy/ui-components'; +import { dataTableHelper, TableAddRow, type DataTableFeatures } from '@axonivy/ui-components'; import { IvyIcons } from '@axonivy/ui-icons'; -import type { ColumnDef, Row, RowSelectionState, SortingState } from '@tanstack/react-table'; -import { getCoreRowModel, getSortedRowModel, useReactTable } from '@tanstack/react-table'; +import type { Row, RowData } from '@tanstack/react-table'; +import { useTable } from '@tanstack/react-table'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { deepEqual } from '../../../../utils/equals'; import type { FieldsetControl } from '../../../widgets/fieldset/fieldset-control'; import { focusNewCell } from './cellFocus-utils'; -interface UseResizableEditableTableProps { +type DataTableColumns = Parameters>['columnHelper']['columns']>[0]; + +interface UseResizableEditableTableProps { data: TData[]; - columns: ColumnDef[]; + columns: DataTableColumns; onChange: (change: TData[]) => void; emptyDataObject: TData; specialUpdateData?: (data: Array, rowIndex: number, columnId: string) => void; } -const useResizableEditableTable = ({ +const useResizableEditableTable = ({ data, columns, onChange, @@ -25,15 +27,16 @@ const useResizableEditableTable = ({ }: UseResizableEditableTableProps) => { const { t } = useTranslation(); const [tableData, setTableData] = useState(data); - const [sorting, setSorting] = useState([]); - const [rowSelection, setRowSelection] = useState({}); const updateTableData = (tableData: Array) => { setTableData(tableData); onChange(tableData.filter(obj => !deepEqual(obj, emptyDataObject))); }; - const updateData = (rowId: string, columnId: string, value: string) => { + const updateData = (rowId: string, columnId: string, value: unknown) => { + if (typeof value !== 'string') { + return; + } const rowIndex = parseInt(rowId); const updatedData = tableData.map((row, index) => { if (index === rowIndex && tableData[rowIndex]) { @@ -52,19 +55,17 @@ const useResizableEditableTable = ({ } }; - const table = useReactTable({ + const { tableOptions } = dataTableHelper(); + + const table = useTable({ + ...tableOptions, data: tableData, columns, - state: { sorting, rowSelection }, columnResizeMode: 'onChange', columnResizeDirection: 'ltr', enableRowSelection: true, enableMultiRowSelection: false, enableSubRowSelection: false, - onRowSelectionChange: setRowSelection, - onSortingChange: setSorting, - getCoreRowModel: getCoreRowModel(), - getSortedRowModel: getSortedRowModel(), meta: { updateData } }); @@ -74,7 +75,7 @@ const useResizableEditableTable = ({ const newData = [...tableData]; newData.push(emptyDataObject); updateTableData(newData); - setRowSelection({ [`${newData.length - 1}`]: true }); + table.setRowSelection({ [`${newData.length - 1}`]: true }); focusNewCell(domTable, newData.length, 'input'); }; @@ -89,9 +90,9 @@ const useResizableEditableTable = ({ const newData = [...tableData]; newData.splice(index, 1); if (newData.length === 0) { - setRowSelection({}); + table.setRowSelection({}); } else if (index === tableData.length - 1) { - setRowSelection({ [`${newData.length - 1}`]: true }); + table.setRowSelection({ [`${newData.length - 1}`]: true }); } if (newData.length === 1 && deepEqual(newData[0], emptyDataObject)) { updateTableData([]); @@ -100,7 +101,7 @@ const useResizableEditableTable = ({ } }; - const selectedRowActions = (additionalActionsSupplier?: (row: Row, rowIndex: number) => FieldsetControl[]) => { + const selectedRowActions = (additionalActionsSupplier?: (row: Row, rowIndex: number) => FieldsetControl[]) => { if (table.getSelectedRowModel().rows.length === 0) { return []; } @@ -116,7 +117,7 @@ const useResizableEditableTable = ({ return [{ label: t('label.removeRow'), icon: IvyIcons.Trash, action: () => removeRow(firstSelectedRowIndex) }, ...additionalActions]; }; - return { table, rowSelection, selectedRowActions, setRowSelection, showAddButton }; + return { table, selectedRowActions, showAddButton }; }; export { useResizableEditableTable }; diff --git a/packages/inscription-view/src/components/parts/condition/ConditionTable.tsx b/packages/inscription-view/src/components/parts/condition/ConditionTable.tsx index 48d154ad4..55461fb70 100644 --- a/packages/inscription-view/src/components/parts/condition/ConditionTable.tsx +++ b/packages/inscription-view/src/components/parts/condition/ConditionTable.tsx @@ -1,9 +1,8 @@ import { IVY_SCRIPT_TYPES } from '@axonivy/process-editor-inscription-protocol'; -import { ReorderHandleWrapper, Table, TableBody, TableCell, TableResizableHeader } from '@axonivy/ui-components'; +import { dataTableHelper, ReorderHandleWrapper, Table, TableBody, TableCell, TableResizableHeader } from '@axonivy/ui-components'; import { IvyIcons } from '@axonivy/ui-icons'; -import type { ColumnDef, RowSelectionState, SortingState } from '@tanstack/react-table'; -import { flexRender, getCoreRowModel, getSortedRowModel, useReactTable } from '@tanstack/react-table'; -import { useCallback, useMemo, useState } from 'react'; +import { flexRender, useTable } from '@tanstack/react-table'; +import { useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import type { FieldsetControl } from '../../widgets/fieldset/fieldset-control'; import { ScriptCell } from '../../widgets/table/cell/ScriptCell'; @@ -18,6 +17,8 @@ const ConditionTypeCell = ({ condition }: { condition: Condition }) => { return ⛔ {condition.fid}; }; +const { columnHelper, tableOptions } = dataTableHelper(); + const ConditionTable = ({ data, onChange }: { data: Condition[]; onChange: (change: Condition[]) => void }) => { const { t } = useTranslation(); const updateOrder = useCallback( @@ -30,81 +31,73 @@ const ConditionTable = ({ data, onChange }: { data: Condition[]; onChange: (chan const newData = [...data]; newData.splice(index, 1); if (newData.length === 0) { - setRowSelection({}); + table.setRowSelection({}); } else if (index === data.length - 1) { - setRowSelection({ [`${newData.length - 1}`]: true }); + table.setRowSelection({ [`${newData.length - 1}`]: true }); } onChange(newData); }; - const columns = useMemo[]>( - () => [ - { - accessorKey: 'fid', - header: () => {t('common.label.type')}, - cell: cell => - }, - { - accessorKey: 'expression', - header: () => {t('label.expression')}, - cell: cell => ( - - - - ) - } - ], + const columns = useMemo( + () => + columnHelper.columns([ + columnHelper.accessor('fid', { + header: () => {t('common.label.type')}, + cell: cell => + }), + columnHelper.accessor('expression', { + header: () => {t('label.expression')}, + cell: cell => ( + + + + ) + }) + ]), [t] ); - const [sorting, setSorting] = useState([]); - const [rowSelection, setRowSelection] = useState({}); - - const table = useReactTable({ + const table = useTable({ + ...tableOptions, data, columns, - state: { sorting, rowSelection }, columnResizeMode: 'onChange', columnResizeDirection: 'ltr', enableRowSelection: true, enableMultiRowSelection: false, enableSubRowSelection: false, - onRowSelectionChange: setRowSelection, - onSortingChange: setSorting, - getCoreRowModel: getCoreRowModel(), - getSortedRowModel: getSortedRowModel(), meta: { - updateData: (rowId: string, columnId: string, value: string) => { + updateData: (rowId: string, columnId: string, value: unknown) => { + if (typeof value !== 'string') { + return; + } const rowIndex = parseInt(rowId); onChange(Condition.update(data, rowIndex, columnId, value)); } } }); - const firstSelectionId = Object.keys(rowSelection)[0]; + const firstSelectedRow = table.getSelectedRowModel().rows[0]; let tableActions: FieldsetControl[] = []; - if (firstSelectionId) { - const firstSelectionRow = table.getRowModel().rowsById[firstSelectionId]; - if (firstSelectionRow && !firstSelectionRow?.original.target) { - tableActions = [ - { - label: t('label.removeRow'), - icon: IvyIcons.Trash, - action: () => removeRow(firstSelectionRow.index) - } - ]; - } + if (firstSelectedRow && !firstSelectedRow?.original.target) { + tableActions = [ + { + label: t('label.removeRow'), + icon: IvyIcons.Trash, + action: () => removeRow(firstSelectedRow.index) + } + ]; } return (
- setRowSelection({})} /> + table.setRowSelection({})} /> {table.getRowModel().rows.map(row => ( { type MailAttachment = { attachment: string }; const EMPTY_ATTACHMENT: MailAttachment = { attachment: '' } as const; +const { columnHelper } = dataTableHelper(); + const MailAttachmentTable = () => { const { t } = useTranslation(); const { config, update } = useMailData(); const data = useMemo(() => config.attachments.map(filename => ({ attachment: filename })), [config.attachments]); - const columns = useMemo[]>( - () => [ - { - id: 'attachment', - accessorFn: row => row.attachment, - cell: cell => ( - - ) - } - ], + const columns = useMemo( + () => + columnHelper.columns([ + columnHelper.accessor('attachment', { + cell: cell => ( + + ) + }) + ]), [] ); diff --git a/packages/inscription-view/src/components/parts/name/document/DocumentTable.tsx b/packages/inscription-view/src/components/parts/name/document/DocumentTable.tsx index eca406a83..844f235dd 100644 --- a/packages/inscription-view/src/components/parts/name/document/DocumentTable.tsx +++ b/packages/inscription-view/src/components/parts/name/document/DocumentTable.tsx @@ -1,7 +1,15 @@ import type { Document } from '@axonivy/process-editor-inscription-protocol'; -import { InputCell, SelectRow, SortableHeader, Table, TableBody, TableCell, TableResizableHeader } from '@axonivy/ui-components'; +import { + dataTableHelper, + InputCell, + SelectRow, + SortableHeader, + Table, + TableBody, + TableCell, + TableResizableHeader +} from '@axonivy/ui-components'; import { IvyIcons } from '@axonivy/ui-icons'; -import type { ColumnDef } from '@tanstack/react-table'; import { flexRender } from '@tanstack/react-table'; import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -11,26 +19,26 @@ import { useResizableEditableTable } from '../../common/table/useResizableEditab const EMPTY_DOCUMENT: Document = { name: '', url: '' } as const; +const { columnHelper } = dataTableHelper(); + const DocumentTable = ({ data, onChange }: { data: Document[]; onChange: (change: Document[]) => void }) => { const { t } = useTranslation(); - const columns = useMemo[]>( - () => [ - { - accessorKey: 'name', - header: ({ column }) => , - cell: cell => - }, - { - accessorFn: row => row.url, - id: 'url', - header: ({ column }) => , - cell: cell => - } - ], + const columns = useMemo( + () => + columnHelper.columns([ + columnHelper.accessor('name', { + header: ({ column }) => , + cell: cell => + }), + columnHelper.accessor('url', { + header: ({ column }) => , + cell: cell => + }) + ]), [t] ); - const { table, setRowSelection, selectedRowActions, showAddButton } = useResizableEditableTable({ + const { table, selectedRowActions, showAddButton } = useResizableEditableTable({ data, columns, onChange, @@ -51,7 +59,7 @@ const DocumentTable = ({ data, onChange }: { data: Document[]; onChange: (change 0}>
- setRowSelection({})} /> + table.setRowSelection({})} /> {table.getRowModel().rows.map(row => ( diff --git a/packages/inscription-view/src/components/parts/query/database/TableFields.tsx b/packages/inscription-view/src/components/parts/query/database/TableFields.tsx index e3a93fedd..7767f5521 100644 --- a/packages/inscription-view/src/components/parts/query/database/TableFields.tsx +++ b/packages/inscription-view/src/components/parts/query/database/TableFields.tsx @@ -1,8 +1,7 @@ import type { DatabaseColumn } from '@axonivy/process-editor-inscription-protocol'; -import { SortableHeader, Table, TableBody, TableCell, TableResizableHeader } from '@axonivy/ui-components'; -import type { ColumnDef, RowSelectionState, SortingState } from '@tanstack/react-table'; -import { flexRender, getCoreRowModel, getSortedRowModel, useReactTable } from '@tanstack/react-table'; -import { useMemo, useState } from 'react'; +import { dataTableHelper, SortableHeader, Table, TableBody, TableCell, TableResizableHeader } from '@axonivy/ui-components'; +import { flexRender, useTable } from '@tanstack/react-table'; +import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useEditorContext } from '../../../../context/useEditorContext'; import { useMeta } from '../../../../context/useMeta'; @@ -16,6 +15,8 @@ type Column = DatabaseColumn & { expression: string; }; +const { columnHelper, tableOptions } = dataTableHelper(); + export const TableFields = () => { const { t } = useTranslation(); const { config, updateSql } = useQueryData(); @@ -33,45 +34,40 @@ export const TableFields = () => { return columnData; }, [columnMetas, config.query.sql.fields]); - const columns = useMemo[]>( - () => [ - { - accessorKey: 'name', - header: ({ column }) => , - cell: cell => ( - <> - {cell.getValue()} - : {cell.row.original.type} - - ) - }, - { - accessorKey: 'expression', - header: ({ column }) => , - cell: cell => - } - ], + const columns = useMemo( + () => + columnHelper.columns([ + columnHelper.accessor('name', { + header: ({ column }) => , + cell: cell => ( + <> + {cell.getValue()} + : {cell.row.original.type} + + ) + }), + columnHelper.accessor('expression', { + header: ({ column }) => , + cell: cell => + }) + ]), [t] ); - const [sorting, setSorting] = useState([]); - const [rowSelection, setRowSelection] = useState({}); - - const table = useReactTable({ + const table = useTable({ + ...tableOptions, data, columns, - state: { sorting, rowSelection }, columnResizeMode: 'onChange', columnResizeDirection: 'ltr', enableRowSelection: true, enableMultiRowSelection: false, enableSubRowSelection: false, - onRowSelectionChange: setRowSelection, - onSortingChange: setSorting, - getCoreRowModel: getCoreRowModel(), - getSortedRowModel: getSortedRowModel(), meta: { - updateData: (rowId: string, columnId: string, value: string) => { + updateData: (rowId: string, columnId: string, value: unknown) => { + if (typeof value !== 'string') { + return; + } const rowIndex = parseInt(rowId); const fields: Record = {}; data @@ -99,7 +95,7 @@ export const TableFields = () => { defaultOpen={config.query.sql.fields && Object.keys(config.query.sql.fields).length > 0} >
- setRowSelection({})} /> + table.setRowSelection({})} /> {table.getRowModel().rows.map(row => ( diff --git a/packages/inscription-view/src/components/parts/query/database/TableReadFields.tsx b/packages/inscription-view/src/components/parts/query/database/TableReadFields.tsx index 3a094684e..c44fe0544 100644 --- a/packages/inscription-view/src/components/parts/query/database/TableReadFields.tsx +++ b/packages/inscription-view/src/components/parts/query/database/TableReadFields.tsx @@ -1,8 +1,17 @@ import type { DatabaseColumn } from '@axonivy/process-editor-inscription-protocol'; -import { SortableHeader, Table, TableBody, TableCell, TableResizableHeader, TableRow } from '@axonivy/ui-components'; -import type { ColumnDef, Row, SortingState } from '@tanstack/react-table'; -import { flexRender, getCoreRowModel, getSortedRowModel, useReactTable } from '@tanstack/react-table'; -import { useMemo, useState } from 'react'; +import { + type DataTableFeatures, + dataTableHelper, + SortableHeader, + Table, + TableBody, + TableCell, + TableResizableHeader, + TableRow +} from '@axonivy/ui-components'; +import type { Row } from '@tanstack/react-table'; +import { flexRender, useTable } from '@tanstack/react-table'; +import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useEditorContext } from '../../../../context/useEditorContext'; import { useMeta } from '../../../../context/useMeta'; @@ -14,6 +23,8 @@ type Column = Omit & { selected: boolean; }; +const { columnHelper, tableOptions } = dataTableHelper(); + export const TableReadFields = () => { const { t } = useTranslation(); const { config, updateSql } = useQueryData(); @@ -26,40 +37,35 @@ export const TableReadFields = () => { return columnMetas.map(c => ({ ...c, selected: select.includes(c.name) })); }, [columnMetas, config.query.sql.select]); - const columns = useMemo[]>( - () => [ - { - accessorKey: 'name', - header: ({ column }) => , - cell: cell => ( - <> - {cell.getValue() as string} - : {cell.row.original.type} - - ) - }, - { - accessorKey: 'selected', - header: ({ column }) => , - cell: cell => {(cell.getValue() as boolean) ? '✅' : ''} - } - ], + const columns = useMemo( + () => + columnHelper.columns([ + columnHelper.accessor('name', { + header: ({ column }) => , + cell: cell => ( + <> + {cell.getValue()} + : {cell.row.original.type} + + ) + }), + columnHelper.accessor('selected', { + header: ({ column }) => , + cell: cell => {(cell.getValue() as boolean) ? '✅' : ''} + }) + ]), [t] ); - const [sorting, setSorting] = useState([]); - const table = useReactTable({ + const table = useTable({ + ...tableOptions, data, columns, - state: { sorting }, columnResizeMode: 'onChange', - columnResizeDirection: 'ltr', - onSortingChange: setSorting, - getCoreRowModel: getCoreRowModel(), - getSortedRowModel: getSortedRowModel() + columnResizeDirection: 'ltr' }); - const selectRow = (row: Row) => { + const selectRow = (row: Row) => { const column = row.original.name; const select = data.filter(c => c.selected).map(c => c.name); const index = select.indexOf(column); diff --git a/packages/inscription-view/src/components/parts/query/database/TableSort.tsx b/packages/inscription-view/src/components/parts/query/database/TableSort.tsx index 02fac05f9..44e5ad6e1 100644 --- a/packages/inscription-view/src/components/parts/query/database/TableSort.tsx +++ b/packages/inscription-view/src/components/parts/query/database/TableSort.tsx @@ -1,6 +1,7 @@ import { QUERY_ORDER } from '@axonivy/process-editor-inscription-protocol'; import { arraymove, + dataTableHelper, indexOf, ReorderHandleWrapper, ReorderRow, @@ -12,8 +13,7 @@ import { TableResizableHeader } from '@axonivy/ui-components'; import { IvyIcons } from '@axonivy/ui-icons'; -import type { ColumnDef, RowSelectionState, SortingState } from '@tanstack/react-table'; -import { flexRender, getCoreRowModel, getSortedRowModel, useReactTable } from '@tanstack/react-table'; +import { flexRender, useTable } from '@tanstack/react-table'; import { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useEditorContext } from '../../../../context/useEditorContext'; @@ -55,6 +55,8 @@ const columnsToOrderBy = (data: Column[]) => { return orderBy; }; +const { columnHelper, tableOptions } = dataTableHelper(); + export const TableSort = () => { const { t } = useTranslation(); const { config, updateSql } = useQueryData(); @@ -64,23 +66,22 @@ export const TableSort = () => { const columnItems = useMeta('meta/database/columns', { context, database: config.query.dbName, table: config.query.sql.table }, []).data; const orderItems = useMemo(() => Object.entries(QUERY_ORDER).map(([label, value]) => ({ label, value })), []); - const columns = useMemo[]>( - () => [ - { - accessorKey: 'name', - header: () => {t('label.column')}, - cell: cell => ({ label: c.name, value: c.name }))} /> - }, - { - accessorKey: 'sorting', - header: () => {t('part.db.direction')}, - cell: cell => ( - - - - ) - } - ], + const columns = useMemo( + () => + columnHelper.columns([ + columnHelper.accessor('name', { + header: () => {t('label.column')}, + cell: cell => ({ label: c.name, value: c.name }))} /> + }), + columnHelper.accessor('sorting', { + header: () => {t('part.db.direction')}, + cell: cell => ( + + + + ) + }) + ]), [columnItems, orderItems, t] ); @@ -89,24 +90,20 @@ export const TableSort = () => { updateSql('orderBy', columnsToOrderBy(data)); }; - const [sorting, setSorting] = useState([]); - const [rowSelection, setRowSelection] = useState({}); - - const table = useReactTable({ + const table = useTable({ + ...tableOptions, data, columns, - state: { sorting, rowSelection }, columnResizeMode: 'onChange', columnResizeDirection: 'ltr', enableRowSelection: true, enableMultiRowSelection: false, enableSubRowSelection: false, - onRowSelectionChange: setRowSelection, - onSortingChange: setSorting, - getCoreRowModel: getCoreRowModel(), - getSortedRowModel: getSortedRowModel(), meta: { - updateData: (rowId: string, columnId: string, value: string) => { + updateData: (rowId: string, columnId: string, value: unknown) => { + if (typeof value !== 'string') { + return; + } const rowIndex = parseInt(rowId); const newData = data.map((row, index) => { if (index === rowIndex && data[rowIndex]) { @@ -126,9 +123,9 @@ export const TableSort = () => { const newData = [...data]; newData.splice(index, 1); if (newData.length === 0) { - setRowSelection({}); + table.setRowSelection({}); } else if (index === data.length - 1) { - setRowSelection({ [`${newData.length - 1}`]: true }); + table.setRowSelection({ [`${newData.length - 1}`]: true }); } updateOrderBy(newData); }; @@ -139,7 +136,7 @@ export const TableSort = () => { const newData = [...data]; newData.push(EMPTY_COLUMN); setData(newData); - setRowSelection({ [`${newData.length - 1}`]: true }); + table.setRowSelection({ [`${newData.length - 1}`]: true }); focusNewCell(domTable, newData.length, 'button'); }; @@ -150,26 +147,23 @@ export const TableSort = () => { updateOrderBy(data); }; - const firstSelectionId = Object.keys(rowSelection)[0]; + const firstSelectedRow = table.getSelectedRowModel().rows[0]; let tableActions: FieldsetControl[] = []; - if (firstSelectionId) { - const firstSelectionRow = table.getRowModel().rowsById[firstSelectionId]; - if (firstSelectionRow) { - tableActions = [ - { - label: t('label.removeRow'), - icon: IvyIcons.Trash, - action: () => removeRow(firstSelectionRow?.index) - } - ]; - } + if (firstSelectedRow) { + tableActions = [ + { + label: t('label.removeRow'), + icon: IvyIcons.Trash, + action: () => removeRow(firstSelectedRow?.index) + } + ]; } return ( 0} controls={tableActions}>
- setRowSelection({})} /> + table.setRowSelection({})} /> {table.getRowModel().rows.map(row => ( diff --git a/packages/inscription-view/src/components/parts/rest/rest-request/rest-body/RestForm.tsx b/packages/inscription-view/src/components/parts/rest/rest-request/rest-body/RestForm.tsx index 1f7be7e18..936798291 100644 --- a/packages/inscription-view/src/components/parts/rest/rest-request/rest-body/RestForm.tsx +++ b/packages/inscription-view/src/components/parts/rest/rest-request/rest-body/RestForm.tsx @@ -1,9 +1,17 @@ import { IVY_SCRIPT_TYPES } from '@axonivy/process-editor-inscription-protocol'; -import { InputCell, SortableHeader, Table, TableAddRow, TableBody, TableCell, TableResizableHeader } from '@axonivy/ui-components'; +import { + dataTableHelper, + InputCell, + SortableHeader, + Table, + TableAddRow, + TableBody, + TableCell, + TableResizableHeader +} from '@axonivy/ui-components'; import { IvyIcons } from '@axonivy/ui-icons'; -import type { ColumnDef, RowSelectionState, SortingState } from '@tanstack/react-table'; -import { flexRender, getCoreRowModel, getSortedRowModel, useReactTable } from '@tanstack/react-table'; -import { useMemo, useState } from 'react'; +import { flexRender, useTable } from '@tanstack/react-table'; +import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { PathProvider } from '../../../../../context/usePath'; import { deepEqual } from '../../../../../utils/equals'; @@ -19,6 +27,8 @@ import { restParamBuilder, toRestMap, updateRestParams } from './rest-parameter' const EMPTY_PARAMETER: RestParam = { name: '', expression: '', known: false }; +const { columnHelper, tableOptions } = dataTableHelper(); + export const RestForm = () => { const { t } = useTranslation(); const { config, updateBody } = useRestRequestData(); @@ -32,32 +42,28 @@ export const RestForm = () => { const onChange = (params: RestParam[]) => updateBody('form', toRestMap(params)); - const columns = useMemo[]>( - () => [ - { - accessorKey: 'name', - header: ({ column }) => , - cell: cell => - }, - { - accessorKey: 'expression', - header: ({ column }) => , - cell: cell => ( - - ) - } - ], + const columns = useMemo( + () => + columnHelper.columns([ + columnHelper.accessor('name', { + header: ({ column }) => , + cell: cell => + }), + columnHelper.accessor('expression', { + header: ({ column }) => , + cell: cell => ( + + ) + }) + ]), [t] ); - const [sorting, setSorting] = useState([]); - const [rowSelection, setRowSelection] = useState({}); - const showAddButton = () => { return data.filter(obj => deepEqual(obj, EMPTY_PARAMETER)).length === 0; }; @@ -68,7 +74,7 @@ export const RestForm = () => { const newData = [...data]; newData.push(EMPTY_PARAMETER); onChange(newData); - setRowSelection({ [`${newData.length - 1}`]: true }); + table.setRowSelection({ [`${newData.length - 1}`]: true }); focusNewCell(domTable, newData.length, 'input'); }; @@ -76,47 +82,43 @@ export const RestForm = () => { const newData = [...data]; newData.splice(index, 1); if (newData.length === 0) { - setRowSelection({}); + table.setRowSelection({}); } else if (index === data.length - 1) { - setRowSelection({ [`${newData.length - 1}`]: true }); + table.setRowSelection({ [`${newData.length - 1}`]: true }); } onChange(newData); }; - const table = useReactTable({ + const table = useTable({ + ...tableOptions, data, columns, - state: { sorting, rowSelection }, columnResizeMode: 'onChange', columnResizeDirection: 'ltr', enableRowSelection: true, enableMultiRowSelection: false, enableSubRowSelection: false, - onRowSelectionChange: setRowSelection, - onSortingChange: setSorting, - getCoreRowModel: getCoreRowModel(), - getSortedRowModel: getSortedRowModel(), meta: { - updateData: (rowId: string, columnId: string, value: string) => { + updateData: (rowId: string, columnId: string, value: unknown) => { + if (typeof value !== 'string') { + return; + } const rowIndex = parseInt(rowId); onChange(updateRestParams(data, rowIndex, columnId, value)); } } }); - const firstSelectionId = Object.keys(rowSelection)[0]; + const firstSelectedRow = table.getSelectedRowModel().rows[0]; let tableActions: FieldsetControl[] = []; - if (firstSelectionId) { - const firstSelectionRow = table.getRowModel().rowsById[firstSelectionId]; - if (firstSelectionRow && !firstSelectionRow?.original.known) { - tableActions = [ - { - label: t('label.removeRow'), - icon: IvyIcons.Trash, - action: () => removeRow(firstSelectionRow.index) - } - ]; - } + if (firstSelectedRow && !firstSelectedRow.original.known) { + tableActions = [ + { + label: t('label.removeRow'), + icon: IvyIcons.Trash, + action: () => removeRow(firstSelectedRow.index) + } + ]; } return ( @@ -124,7 +126,7 @@ export const RestForm = () => {
{tableActions.length > 0 &&
}
- setRowSelection({})} /> + table.setRowSelection({})} /> {table.getRowModel().rows.map(row => ( diff --git a/packages/inscription-view/src/components/parts/rest/rest-request/rest-target/RestParameters.tsx b/packages/inscription-view/src/components/parts/rest/rest-request/rest-target/RestParameters.tsx index 5d303ea94..867661309 100644 --- a/packages/inscription-view/src/components/parts/rest/rest-request/rest-target/RestParameters.tsx +++ b/packages/inscription-view/src/components/parts/rest/rest-request/rest-target/RestParameters.tsx @@ -1,5 +1,6 @@ import { IVY_SCRIPT_TYPES, REST_PARAM_KIND } from '@axonivy/process-editor-inscription-protocol'; import { + dataTableHelper, InputCell, SelectCell, SortableHeader, @@ -10,9 +11,8 @@ import { TableResizableHeader } from '@axonivy/ui-components'; import { IvyIcons } from '@axonivy/ui-icons'; -import type { ColumnDef, RowSelectionState, SortingState } from '@tanstack/react-table'; -import { flexRender, getCoreRowModel, getSortedRowModel, useReactTable } from '@tanstack/react-table'; -import { useMemo, useState } from 'react'; +import { flexRender, useTable } from '@tanstack/react-table'; +import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { deepEqual } from '../../../../../utils/equals'; import type { FieldsetControl } from '../../../../widgets/fieldset/fieldset-control'; @@ -28,6 +28,8 @@ import { useFindPathParams } from './usePathParams'; const EMPTY_PARAMETER: Parameter = { kind: 'Query', name: '', expression: '', known: false }; +const { columnHelper, tableOptions } = dataTableHelper(); + export const RestParameters = () => { const { t } = useTranslation(); const { config, updateParameters } = useRestRequestData(); @@ -46,37 +48,32 @@ export const RestParameters = () => { updateParameters({ queryParams: Parameter.to(props, 'Query'), templateParams: Parameter.to(props, 'Path') }); const kindItems = useMemo(() => Object.entries(REST_PARAM_KIND).map(([value, label]) => ({ label, value })), []); - const columns = useMemo[]>( - () => [ - { - accessorKey: 'kind', - header: ({ column }) => , - cell: cell => - }, - { - accessorKey: 'name', - header: ({ column }) => , - cell: cell => - }, - { - accessorKey: 'expression', - header: ({ column }) => , - cell: cell => ( - - ) - } - ], + const columns = useMemo( + () => + columnHelper.columns([ + columnHelper.accessor('kind', { + header: ({ column }) => , + cell: cell => + }), + columnHelper.accessor('name', { + header: ({ column }) => , + cell: cell => + }), + columnHelper.accessor('expression', { + header: ({ column }) => , + cell: cell => ( + + ) + }) + ]), [kindItems, t] ); - const [sorting, setSorting] = useState([]); - const [rowSelection, setRowSelection] = useState({}); - const showAddButton = () => { return data.filter(obj => deepEqual(obj, EMPTY_PARAMETER)).length === 0; }; @@ -87,7 +84,7 @@ export const RestParameters = () => { const newData = [...data]; newData.push(EMPTY_PARAMETER); onChange(newData); - setRowSelection({ [`${newData.length - 1}`]: true }); + table.setRowSelection({ [`${newData.length - 1}`]: true }); focusNewCell(domTable, newData.length, 'button'); }; @@ -95,53 +92,49 @@ export const RestParameters = () => { const newData = [...data]; newData.splice(index, 1); if (newData.length === 0) { - setRowSelection({}); + table.setRowSelection({}); } else if (index === data.length - 1) { - setRowSelection({ [`${newData.length - 1}`]: true }); + table.setRowSelection({ [`${newData.length - 1}`]: true }); } onChange(newData); }; - const table = useReactTable({ + const table = useTable({ + ...tableOptions, data, columns, - state: { sorting, rowSelection }, columnResizeMode: 'onChange', columnResizeDirection: 'ltr', enableRowSelection: true, enableMultiRowSelection: false, enableSubRowSelection: false, - onRowSelectionChange: setRowSelection, - onSortingChange: setSorting, - getCoreRowModel: getCoreRowModel(), - getSortedRowModel: getSortedRowModel(), meta: { - updateData: (rowId: string, columnId: string, value: string) => { + updateData: (rowId: string, columnId: string, value: unknown) => { + if (typeof value !== 'string') { + return; + } const rowIndex = parseInt(rowId); onChange(Parameter.update(data, rowIndex, columnId, value)); } } }); - const firstSelectionId = Object.keys(rowSelection)[0]; + const firstSelectedRow = table.getSelectedRowModel().rows[0]; let tableActions: FieldsetControl[] = []; - if (firstSelectionId) { - const firstSelectionRow = table.getRowModel().rowsById[firstSelectionId]; - if (firstSelectionRow && !firstSelectionRow?.original.known) { - tableActions = [ - { - label: t('label.removeRow'), - icon: IvyIcons.Trash, - action: () => removeRow(firstSelectionRow?.index) - } - ]; - } + if (firstSelectedRow && !firstSelectedRow?.original.known) { + tableActions = [ + { + label: t('label.removeRow'), + icon: IvyIcons.Trash, + action: () => removeRow(firstSelectedRow?.index) + } + ]; } return ( 0} controls={tableActions}>
- setRowSelection({})} /> + table.setRowSelection({})} /> {table.getRowModel().rows.map(row => ( diff --git a/packages/inscription-view/src/components/widgets/table/cell/BrowserInputCell.tsx b/packages/inscription-view/src/components/widgets/table/cell/BrowserInputCell.tsx index e153f4355..c2fcac1e7 100644 --- a/packages/inscription-view/src/components/widgets/table/cell/BrowserInputCell.tsx +++ b/packages/inscription-view/src/components/widgets/table/cell/BrowserInputCell.tsx @@ -1,15 +1,15 @@ -import { Flex, useEditCell } from '@axonivy/ui-components'; -import type { CellContext } from '@tanstack/react-table'; +import { type DataTableFeatures, Flex, useEditCell } from '@axonivy/ui-components'; +import type { CellContext, RowData } from '@tanstack/react-table'; import { usePath } from '../../../../context/usePath'; import Browser from '../../../browser/Browser'; import { useBrowser } from '../../../browser/useBrowser'; import { Input } from '../../input/Input'; -type BrowserInputCellProps = { - cell: CellContext; +type BrowserInputCellProps = { + cell: CellContext; }; -export function BrowserInputCell({ cell }: BrowserInputCellProps) { +export function BrowserInputCell({ cell }: BrowserInputCellProps) { const { value, setValue, updateValue, onBlur, className: editCell } = useEditCell(cell); const browser = useBrowser(); const path = usePath(); diff --git a/packages/inscription-view/src/components/widgets/table/cell/CodeEditorCell.tsx b/packages/inscription-view/src/components/widgets/table/cell/CodeEditorCell.tsx index cb5cfdaa3..b9568d4ca 100644 --- a/packages/inscription-view/src/components/widgets/table/cell/CodeEditorCell.tsx +++ b/packages/inscription-view/src/components/widgets/table/cell/CodeEditorCell.tsx @@ -1,5 +1,5 @@ -import { Button, selectNextPreviousCell } from '@axonivy/ui-components'; -import type { CellContext } from '@tanstack/react-table'; +import { Button, type DataTableFeatures, selectNextPreviousCell } from '@axonivy/ui-components'; +import type { CellContext, RowData } from '@tanstack/react-table'; import { useState } from 'react'; import { usePath } from '../../../../context/usePath'; import { focusAdjacentTabIndexMonaco } from '../../../../utils/focus'; @@ -13,16 +13,16 @@ import { useMonacoEditor } from '../../code-editor/useCodeEditor'; import { Input } from '../../input/Input'; import './CodeEditorCell.css'; -type CodeEditorCellProps = { - cell: CellContext; +type CodeEditorCellProps = { + cell: CellContext; macro: boolean; type?: string; placeholder?: string; browsers: BrowserType[]; }; -export function CodeEditorCell({ cell, macro, type, browsers, placeholder }: CodeEditorCellProps) { - const initialValue = cell.getValue() as string; +export function CodeEditorCell({ cell, macro, type, browsers, placeholder }: CodeEditorCellProps) { + const initialValue = cell.getValue() ?? ''; const [value, setValue] = useState(initialValue); const [prevValue, setPrevValue] = useState(initialValue); if (prevValue !== initialValue) { diff --git a/packages/inscription-view/src/components/widgets/table/cell/ExpandableCell.tsx b/packages/inscription-view/src/components/widgets/table/cell/ExpandableCell.tsx index aca8d01e6..a574f81df 100644 --- a/packages/inscription-view/src/components/widgets/table/cell/ExpandableCell.tsx +++ b/packages/inscription-view/src/components/widgets/table/cell/ExpandableCell.tsx @@ -1,11 +1,11 @@ -import { Button, Flex } from '@axonivy/ui-components'; +import { Button, type DataTableFeatures, Flex } from '@axonivy/ui-components'; import { IvyIcons } from '@axonivy/ui-icons'; -import type { CellContext } from '@tanstack/react-table'; +import type { CellContext, RowData } from '@tanstack/react-table'; import IvyIcon from '../../IvyIcon'; import './ExpandableCell.css'; -type ExpandableCellProps = { - cell: CellContext; +type ExpandableCellProps = { + cell: CellContext; isLoaded?: boolean; loadChildren?: () => void; isUnknown?: boolean; @@ -14,7 +14,7 @@ type ExpandableCellProps = { additionalInfo?: string; }; -export function ExpandableCell({ +export function ExpandableCell({ cell, isLoaded, loadChildren, @@ -64,7 +64,7 @@ export function ExpandableCell({ ) : ( <>{icon && } )} - {cell.getValue() as string} + {cell.getValue()} {additionalInfo && : {additionalInfo}} ); diff --git a/packages/inscription-view/src/components/widgets/table/cell/MacroCell.tsx b/packages/inscription-view/src/components/widgets/table/cell/MacroCell.tsx index 6f0b4887b..8ddadf0b7 100644 --- a/packages/inscription-view/src/components/widgets/table/cell/MacroCell.tsx +++ b/packages/inscription-view/src/components/widgets/table/cell/MacroCell.tsx @@ -1,6 +1,13 @@ -import type { CellContext } from '@tanstack/react-table'; +import type { DataTableFeatures } from '@axonivy/ui-components'; +import type { CellContext, RowData } from '@tanstack/react-table'; import { CodeEditorCell } from './CodeEditorCell'; -export const MacroCell = ({ cell, placeholder }: { cell: CellContext; placeholder?: string }) => ( +export const MacroCell = ({ + cell, + placeholder +}: { + cell: CellContext; + placeholder?: string; +}) => ( ); diff --git a/packages/inscription-view/src/components/widgets/table/cell/ScriptCell.tsx b/packages/inscription-view/src/components/widgets/table/cell/ScriptCell.tsx index 954496bda..4a5d258cf 100644 --- a/packages/inscription-view/src/components/widgets/table/cell/ScriptCell.tsx +++ b/packages/inscription-view/src/components/widgets/table/cell/ScriptCell.tsx @@ -1,16 +1,16 @@ -import { Button, Flex, InputCell } from '@axonivy/ui-components'; +import { Button, type DataTableFeatures, Flex, InputCell } from '@axonivy/ui-components'; import { IvyIcons } from '@axonivy/ui-icons'; -import type { CellContext } from '@tanstack/react-table'; +import type { CellContext, RowData } from '@tanstack/react-table'; import type { BrowserType } from '../../../browser/useBrowser'; import { CodeEditorCell } from './CodeEditorCell'; -export const ScriptCell = ({ +export const ScriptCell = ({ cell, type, browsers, placeholder }: { - cell: CellContext; + cell: CellContext; type: string; browsers: BrowserType[]; placeholder?: string; diff --git a/packages/inscription-view/src/components/widgets/table/table/Table.tsx b/packages/inscription-view/src/components/widgets/table/table/Table.tsx index 358195d75..9d5e69b0b 100644 --- a/packages/inscription-view/src/components/widgets/table/table/Table.tsx +++ b/packages/inscription-view/src/components/widgets/table/table/Table.tsx @@ -1,17 +1,20 @@ -import { Flex, SearchInput, Table } from '@axonivy/ui-components'; +import { Flex, Table, TableGlobalFilter, type DataTableFeatures } from '@axonivy/ui-components'; +import type { ReactTable, RowData } from '@tanstack/react-table'; import { type ComponentProps } from 'react'; import { useTranslation } from 'react-i18next'; import './Table.css'; -type TableProps = ComponentProps & { - search?: { value: string; onChange: (value: string) => void }; +type TableProps = ComponentProps & { + table: ReactTable; + searchActive?: boolean; + onSearchChange?: (filter: string) => void; }; -export const SearchTable = ({ search, ...props }: TableProps) => { +export const SearchTable = ({ table, searchActive, onSearchChange, ...props }: TableProps) => { const { t } = useTranslation(); return ( - {search && } +
); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1e0ebdabe..2254b4641 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,13 +18,13 @@ importers: devDependencies: '@axonivy/eslint-config': specifier: next-14.0.0 - version: 14.0.0-next.1232.745bdb0(@typescript/typescript6@6.0.2)(jiti@2.7.0)(supports-color@7.2.0)(tailwindcss@4.2.2) + version: 14.0.0-next.1236.70f9188(@typescript/typescript6@6.0.2)(jiti@2.7.0)(supports-color@7.2.0)(tailwindcss@4.2.2) '@axonivy/prettier-config': specifier: next-14.0.0 - version: 14.0.0-next.1232.745bdb0 + version: 14.0.0-next.1236.70f9188 '@axonivy/ts-config': specifier: next-14.0.0 - version: 14.0.0-next.1232.745bdb0 + version: 14.0.0-next.1236.70f9188 '@lerna-lite/cli': specifier: ^5.4.2 version: 5.6.0(@lerna-lite/publish@5.6.0(@types/node@24.13.3)(conventional-commits-filter@6.0.1)(supports-color@7.2.0))(@lerna-lite/version@5.6.0(@lerna-lite/publish@5.6.0(@types/node@24.13.3)(conventional-commits-filter@6.0.1)(supports-color@7.2.0))(@types/node@24.13.3)(conventional-commits-filter@6.0.1))(@types/node@24.13.3) @@ -66,7 +66,7 @@ importers: dependencies: '@axonivy/jsonrpc': specifier: next-14.0.0 - version: 14.0.0-next.1232.745bdb0 + version: 14.0.0-next.1236.70f9188 '@axonivy/process-editor-inscription-core': specifier: workspace:~ version: link:../../packages/inscription-core @@ -78,10 +78,10 @@ importers: version: link:../../packages/inscription-view '@axonivy/ui-components': specifier: next-14.0.0 - version: 14.0.0-next.1232.745bdb0(@axonivy/ui-icons@14.0.0-next.1232.745bdb0)(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + version: 14.0.0-next.1236.70f9188(@axonivy/ui-icons@14.0.0-next.1236.70f9188)(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@axonivy/ui-icons': specifier: next-14.0.0 - version: 14.0.0-next.1232.745bdb0 + version: 14.0.0-next.1236.70f9188 '@tanstack/react-query': specifier: ^5.101.4 version: 5.101.4(react@19.2.8) @@ -133,7 +133,7 @@ importers: dependencies: '@axonivy/jsonrpc': specifier: next-14.0.0 - version: 14.0.0-next.1232.745bdb0 + version: 14.0.0-next.1236.70f9188 '@axonivy/process-editor': specifier: workspace:~ version: link:../../packages/editor @@ -151,10 +151,10 @@ importers: version: link:../../packages/protocol '@axonivy/ui-components': specifier: next-14.0.0 - version: 14.0.0-next.1232.745bdb0(@axonivy/ui-icons@14.0.0-next.1232.745bdb0)(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + version: 14.0.0-next.1236.70f9188(@axonivy/ui-icons@14.0.0-next.1236.70f9188)(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@axonivy/ui-icons': specifier: next-14.0.0 - version: 14.0.0-next.1232.745bdb0 + version: 14.0.0-next.1236.70f9188 '@eclipse-glsp/client': specifier: 2.7.0 version: 2.7.0(inversify@6.2.2(reflect-metadata@0.2.2))(reflect-metadata@0.2.2) @@ -200,10 +200,10 @@ importers: version: link:../../packages/protocol '@axonivy/ui-components': specifier: next-14.0.0 - version: 14.0.0-next.1232.745bdb0(@axonivy/ui-icons@14.0.0-next.1232.745bdb0)(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + version: 14.0.0-next.1236.70f9188(@axonivy/ui-icons@14.0.0-next.1236.70f9188)(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@axonivy/ui-icons': specifier: next-14.0.0 - version: 14.0.0-next.1232.745bdb0 + version: 14.0.0-next.1236.70f9188 '@eclipse-glsp/client': specifier: 2.7.0 version: 2.7.0(inversify@6.2.2(reflect-metadata@0.2.2))(reflect-metadata@0.2.2) @@ -245,8 +245,8 @@ importers: specifier: ^5.64 version: 5.101.4(@tanstack/react-query@5.101.4(react@19.2.8))(react@19.2.8) '@tanstack/react-table': - specifier: ^8.20 - version: 8.21.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + specifier: ^9.0.0 + version: 9.1.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) dompurify: specifier: ^3.4.13 version: 3.4.14 @@ -274,10 +274,10 @@ importers: devDependencies: '@axonivy/ui-components': specifier: next-14.0.0 - version: 14.0.0-next.1232.745bdb0(@axonivy/ui-icons@14.0.0-next.1232.745bdb0)(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + version: 14.0.0-next.1236.70f9188(@axonivy/ui-icons@14.0.0-next.1236.70f9188)(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@axonivy/ui-icons': specifier: next-14.0.0 - version: 14.0.0-next.1232.745bdb0 + version: 14.0.0-next.1236.70f9188 '@testing-library/react': specifier: ^16.3.2 version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) @@ -344,10 +344,10 @@ importers: devDependencies: '@axonivy/ui-components': specifier: next-14.0.0 - version: 14.0.0-next.1232.745bdb0(@axonivy/ui-icons@14.0.0-next.1232.745bdb0)(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + version: 14.0.0-next.1236.70f9188(@axonivy/ui-icons@14.0.0-next.1236.70f9188)(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@axonivy/ui-icons': specifier: next-14.0.0 - version: 14.0.0-next.1232.745bdb0 + version: 14.0.0-next.1236.70f9188 '@types/react': specifier: ^19.2.17 version: 19.2.18 @@ -369,7 +369,7 @@ importers: devDependencies: '@axonivy/jsonrpc': specifier: next-14.0.0 - version: 14.0.0-next.1232.745bdb0 + version: 14.0.0-next.1236.70f9188 packages/inscription-protocol: devDependencies: @@ -404,8 +404,8 @@ importers: specifier: ^5.64 version: 5.101.4(@tanstack/react-query@5.101.4(react@19.2.8))(react@19.2.8) '@tanstack/react-table': - specifier: ^8.20 - version: 8.21.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + specifier: ^9.0.0 + version: 9.1.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@tanstack/react-virtual': specifier: ^3.14.8 version: 3.14.10(react-dom@19.2.8(react@19.2.8))(react@19.2.8) @@ -436,13 +436,13 @@ importers: devDependencies: '@axonivy/jsonrpc': specifier: next-14.0.0 - version: 14.0.0-next.1232.745bdb0 + version: 14.0.0-next.1236.70f9188 '@axonivy/ui-components': specifier: next-14.0.0 - version: 14.0.0-next.1232.745bdb0(@axonivy/ui-icons@14.0.0-next.1232.745bdb0)(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + version: 14.0.0-next.1236.70f9188(@axonivy/ui-icons@14.0.0-next.1236.70f9188)(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@axonivy/ui-icons': specifier: next-14.0.0 - version: 14.0.0-next.1232.745bdb0 + version: 14.0.0-next.1236.70f9188 '@testing-library/jest-dom': specifier: ^7.0.0 version: 7.0.1(@testing-library/dom@10.4.1)(vitest@4.1.11(@types/node@24.13.3)(happy-dom@20.11.6)(jsdom@30.0.1)(vite@8.2.2(@types/node@24.13.3)(jiti@2.7.0)(yaml@2.9.0))) @@ -509,26 +509,29 @@ packages: resolution: {integrity: sha512-gdTwo43uXmJm+3qhuE7i13sHepN12Wd+K9kP7psbhwaWvFA8fVZE0J+xZ4pZByBg+mkchmj3h+2zj75bpc+QGw==} engines: {node: ^22.13.0 || >=24.0.0} - '@axonivy/eslint-config@14.0.0-next.1232.745bdb0': - resolution: {integrity: sha512-w70nkgP3IfUDyR864UUs8r6mG3zy2FndPJDsFyaiBYvuKtt2PiJticeVxoe9KFJj5R+SlvcFdsTTaJYZ19KSHg==} + '@axonivy/eslint-config@14.0.0-next.1236.70f9188': + resolution: {integrity: sha512-sn+bC0mJIC7CCllIuGcl8TQD62ZaxOYc55CLjndmrB82Ygp/4YMAjXseNlMJNa+Iyx/8+qp4D0lnfJhKQZGYKA==} '@axonivy/jsonrpc@14.0.0-next.1232.745bdb0': resolution: {integrity: sha512-emGuJduocT3V7WhscXNBYt1LeBkojxJ7V6JAlWUm/FtqMZMgQs0u+hdqQYQ8Gx+xEf/gqgwp+ReU17ocEd2aRA==} - '@axonivy/prettier-config@14.0.0-next.1232.745bdb0': - resolution: {integrity: sha512-wHu4HBEcbZYO250hvJFtBYFqMpDYYKf8Qa8mghZWHNmvyJsI5GWoQulUQKx27xRnxX7y8M9af+pMyl9e5iamnw==} + '@axonivy/jsonrpc@14.0.0-next.1236.70f9188': + resolution: {integrity: sha512-66+HVKndEaYzu+BMDWp0FUwlnUxxQLt/SzLkXFIoNJP9AkFDobz8/s/hxPpYVperqT7dqQBDROvP8zirRoV4bg==} + + '@axonivy/prettier-config@14.0.0-next.1236.70f9188': + resolution: {integrity: sha512-09QkYsDdAILaBB1UmCncB7QQbrdU0FfB3DWFjBMsFgqLJvzIAt/jrV7j+rHbcSKkjHPH6qZVDEiXTQERyX3KFg==} - '@axonivy/ts-config@14.0.0-next.1232.745bdb0': - resolution: {integrity: sha512-d/o+/6zr9l2DAI2f+EGKao/BtEn+EkXHDUCypLKmIq5ufMFOnqWp5mMlozmXLgU01svWZpBXh7VxdxNE69gJWQ==} + '@axonivy/ts-config@14.0.0-next.1236.70f9188': + resolution: {integrity: sha512-eJhWUkm5VeAz2TXC3uIkAxWptOG16f2bH/hP9qEpz8TraKgLmc+cIGna/lEwthG5jc3dqisLFcVecsHRESxM3Q==} - '@axonivy/ui-components@14.0.0-next.1232.745bdb0': - resolution: {integrity: sha512-3+SOMkvFN7QsFU6G0fAD/TwmYz/crjdHWZ24niyU0oyXNKffvcEzfHNkfjH/6rq9jTjmtcBMsQXhh0Az0r8Uww==} + '@axonivy/ui-components@14.0.0-next.1236.70f9188': + resolution: {integrity: sha512-jX9qGr/QBoBvik47VM+Z1mr6a2HoJ4rN6PK1jSHpQCQgG9Fb5KWc50wuPZZs9RlWVW5HFkSqoptJMN7NQ3cUOA==} peerDependencies: '@axonivy/ui-icons': ~14.0.0-next react: ^19.0 - '@axonivy/ui-icons@14.0.0-next.1232.745bdb0': - resolution: {integrity: sha512-+VJOqtZCeqtccvcGTj9j9u4GZlE+/xQJeJgr+HsiGa9vBQseim4EoAOE93IP3d+gaXSCrqJLrc4fmpdg0N4BpQ==} + '@axonivy/ui-icons@14.0.0-next.1236.70f9188': + resolution: {integrity: sha512-xdWnd6hNvJAHhIulm5fwEL0hkdP8SCBrJl0/6TwphRSggi2hlX5YUsmMvkmtKRU+02VyRB5qVwEr4I9TwBa+oA==} '@babel/code-frame@7.29.7': resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} @@ -1920,12 +1923,17 @@ packages: peerDependencies: react: ^18 || ^19 - '@tanstack/react-table@8.21.3': - resolution: {integrity: sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==} - engines: {node: '>=12'} + '@tanstack/react-store@0.11.1': + resolution: {integrity: sha512-HaIGKI3YLmjBYIvy5DFDY23oNaYZIsTZfngey07Uh5iLVJgM3bIGCnZeOFOqzjFld9JHWcaHJnasD/bKoGKwJQ==} peerDependencies: - react: '>=16.8' - react-dom: '>=16.8' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tanstack/react-table@9.1.0': + resolution: {integrity: sha512-KXZDQ+MjmacO6KG0Fo019UMHJV7nXVzf/GeBiwSK7a52RVcycFJWRx88OYtOfGPMDRfAK5vyyguC8B2zvc95HA==} + engines: {node: '>=20'} + peerDependencies: + react: '>=18' '@tanstack/react-virtual@3.14.10': resolution: {integrity: sha512-SRyoUbdFMRHuYXMijV5H4ZarQWpXkj3iANq8OFre+pybeVap8ZJjZ3Nz9bVjx4d8PfobVUQUdKyyyHYk3E+djw==} @@ -1933,9 +1941,12 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - '@tanstack/table-core@8.21.3': - resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} - engines: {node: '>=12'} + '@tanstack/store@0.11.1': + resolution: {integrity: sha512-mzTOBhypOuDJAy/D8n2MfUZ1HFkXnmSETviRyhqEC8LUE7/IZQExOTxMANj3KjTofYTkFNpBY67qaVrT41YccA==} + + '@tanstack/table-core@9.1.0': + resolution: {integrity: sha512-tvnnqdWlvF/l6P65yS4ca4keW2VCC3e+OTT8/pUM7ZF5OQBJ7zTVy67T0lLpWBpkELrZN8sFJxY5DHF05fLUDQ==} + engines: {node: '>=20'} '@tanstack/virtual-core@3.17.8': resolution: {integrity: sha512-BfEvehNpOT75r5Ksc5xW6NZuXujTfb7nlSEyVu4XHG3gdxNg1KqXruWbDewXOUaUYIo4oRbSfkjIajz4MAT8tA==} @@ -3719,8 +3730,8 @@ packages: '@types/react': optional: true - react-resizable-panels@4.12.2: - resolution: {integrity: sha512-NwY5LCo4WrxVvDh0xoMML6EMLPONP/8ckKcIdpnojxexoatZdjLiRqLJQjQK5CPkd4SYiB/2M5BVrjZBQtOO7Q==} + react-resizable-panels@4.12.3: + resolution: {integrity: sha512-GHMJWnDXui/3RX4bT+cgBP+N3N2nkwcoZATr/2xLFpqQQe7TlBrE0cGM0dUa9ceT7A90tUrSRsiqgRG+g0/2AA==} peerDependencies: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 @@ -4399,7 +4410,7 @@ snapshots: is-potential-custom-element-name: 1.0.1 lru-cache: 11.5.2 - '@axonivy/eslint-config@14.0.0-next.1232.745bdb0(@typescript/typescript6@6.0.2)(jiti@2.7.0)(supports-color@7.2.0)(tailwindcss@4.2.2)': + '@axonivy/eslint-config@14.0.0-next.1236.70f9188(@typescript/typescript6@6.0.2)(jiti@2.7.0)(supports-color@7.2.0)(tailwindcss@4.2.2)': dependencies: '@eslint-react/eslint-plugin': 5.18.6(@typescript/typescript6@6.0.2)(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0) '@eslint/compat': 2.1.0(eslint@10.8.1(jiti@2.7.0)(supports-color@7.2.0)) @@ -4426,15 +4437,19 @@ snapshots: dependencies: vscode-jsonrpc: 8.2.1 - '@axonivy/prettier-config@14.0.0-next.1232.745bdb0': + '@axonivy/jsonrpc@14.0.0-next.1236.70f9188': + dependencies: + vscode-jsonrpc: 8.2.1 + + '@axonivy/prettier-config@14.0.0-next.1236.70f9188': dependencies: prettier: 3.9.6 - '@axonivy/ts-config@14.0.0-next.1232.745bdb0': {} + '@axonivy/ts-config@14.0.0-next.1236.70f9188': {} - '@axonivy/ui-components@14.0.0-next.1232.745bdb0(@axonivy/ui-icons@14.0.0-next.1232.745bdb0)(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@axonivy/ui-components@14.0.0-next.1236.70f9188(@axonivy/ui-icons@14.0.0-next.1236.70f9188)(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@axonivy/ui-icons': 14.0.0-next.1232.745bdb0 + '@axonivy/ui-icons': 14.0.0-next.1236.70f9188 '@base-ui/react': 1.7.0(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@radix-ui/react-accordion': 1.2.20(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@radix-ui/react-checkbox': 1.3.11(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) @@ -4452,13 +4467,13 @@ snapshots: '@radix-ui/react-toggle-group': 1.1.19(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@radix-ui/react-tooltip': 1.2.16(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@react-aria/dnd': 3.12.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - '@tanstack/react-table': 8.21.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@tanstack/react-table': 9.1.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) class-variance-authority: 0.7.1 cnfast: 0.1.0 downshift: 9.0.13(react@19.2.8) react: 19.2.8 react-hotkeys-hook: 5.2.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8) - react-resizable-panels: 4.12.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react-resizable-panels: 4.12.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8) sonner: 2.0.8(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) transitivePeerDependencies: - '@date-fns/tz' @@ -4467,7 +4482,7 @@ snapshots: - date-fns - react-dom - '@axonivy/ui-icons@14.0.0-next.1232.745bdb0': {} + '@axonivy/ui-icons@14.0.0-next.1236.70f9188': {} '@babel/code-frame@7.29.7': dependencies: @@ -5922,11 +5937,20 @@ snapshots: '@tanstack/query-core': 5.101.4 react: 19.2.8 - '@tanstack/react-table@8.21.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@tanstack/react-store@0.11.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@tanstack/table-core': 8.21.3 + '@tanstack/store': 0.11.1 react: 19.2.8 react-dom: 19.2.8(react@19.2.8) + use-sync-external-store: 1.6.0(react@19.2.8) + + '@tanstack/react-table@9.1.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@tanstack/react-store': 0.11.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@tanstack/table-core': 9.1.0 + react: 19.2.8 + transitivePeerDependencies: + - react-dom '@tanstack/react-virtual@3.14.10(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: @@ -5934,7 +5958,11 @@ snapshots: react: 19.2.8 react-dom: 19.2.8(react@19.2.8) - '@tanstack/table-core@8.21.3': {} + '@tanstack/store@0.11.1': {} + + '@tanstack/table-core@9.1.0': + dependencies: + '@tanstack/store': 0.11.1 '@tanstack/virtual-core@3.17.8': {} @@ -7753,7 +7781,7 @@ snapshots: optionalDependencies: '@types/react': 19.2.18 - react-resizable-panels@4.12.2(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + react-resizable-panels@4.12.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: react: 19.2.8 react-dom: 19.2.8(react@19.2.8)