diff --git a/app/package.json b/app/package.json index 0a8d9fb4b1..4a9373171e 100644 --- a/app/package.json +++ b/app/package.json @@ -30,6 +30,7 @@ "@epam/uui-docs": "5.7.1", "@epam/uui-editor": "5.7.1", "@epam/uui-timeline": "5.7.1", + "@tanstack/react-query": "^5.17.19", "@udecode/plate-common": "25.0.1", "amplitude-js": "8.9.1", "classnames": "2.2.6", diff --git a/app/src/common/apiReference/TypeRefTable.tsx b/app/src/common/apiReference/TypeRefTable.tsx index 9552dbc2f9..697139d596 100644 --- a/app/src/common/apiReference/TypeRefTable.tsx +++ b/app/src/common/apiReference/TypeRefTable.tsx @@ -68,7 +68,7 @@ export function TypeRefTable(props: TypeRefTableProps) { [items, props.isGrouped], ); - const view = exportPropsDs.getView(tableState, setTableState, { + const view = exportPropsDs.useView(tableState, setTableState, { isFoldedByDefault: () => false, }); diff --git a/app/src/demo/tables/editableTable/ProjectTableDemo.tsx b/app/src/demo/tables/editableTable/ProjectTableDemo.tsx index c2609ce847..8899a6aa39 100644 --- a/app/src/demo/tables/editableTable/ProjectTableDemo.tsx +++ b/app/src/demo/tables/editableTable/ProjectTableDemo.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { DataTable, Panel, Button, FlexCell, FlexRow, FlexSpacer, IconButton, useForm, SearchInput, Tooltip } from '@epam/uui'; -import { AcceptDropParams, DataTableState, DropParams, DropPosition, Metadata, useList } from '@epam/uui-core'; +import { AcceptDropParams, DataTableState, DropParams, DropPosition, Metadata, useArrayDataSource } from '@epam/uui-core'; import { useDataTableFocusManager } from '@epam/uui-components'; import { ReactComponent as undoIcon } from '@epam/assets/icons/content-edit_undo-outline.svg'; @@ -39,7 +39,7 @@ let savedValue: FormState = { items: getDemoTasks() }; export function ProjectTableDemo() { const { - lens, value, save, isChanged, revert, undo, canUndo, redo, canRedo, setValue, + value, save, isChanged, revert, undo, canUndo, redo, canRedo, setValue, lens, } = useForm({ value: savedValue, onSave: async (data) => { @@ -52,7 +52,6 @@ export function ProjectTableDemo() { const [tableState, setTableState] = useState({ sorting: [{ field: 'order' }], visibleCount: 1000 }); const dataTableFocusManager = useDataTableFocusManager({}, []); - // Insert new/exiting top/bottom or above/below relative to other task const insertTask = useCallback((position: DropPosition, relativeTask: Task | null = null, existingTask: Task | null = null) => { let tempRelativeTask = relativeTask; const task: Task = existingTask ? { ...existingTask } : { id: lastId--, name: '' }; @@ -117,15 +116,13 @@ export function ProjectTableDemo() { [], ); - const { rows, listProps } = useList( + const dataSource = useArrayDataSource( { - type: 'array', - listState: tableState, - setListState: setTableState, items: Object.values(value.items), getSearchFields: (item) => [item.name], getId: (i) => i.id, getParentId: (i) => i.parentId, + fixItemBetweenSortings: false, getRowOptions: (task) => ({ ...lens.prop('items').prop(task.id).toProps(), // pass IEditable to each row to allow editing // checkbox: { isVisible: true }, @@ -141,6 +138,8 @@ export function ProjectTableDemo() { [], ); + const view = dataSource.useView(tableState, setTableState); + const columns = useMemo( () => getColumns({ insertTask, deleteTask }), [insertTask, deleteTask], @@ -156,7 +155,7 @@ export function ProjectTableDemo() { const deleteSelectedItem = useCallback(() => { if (selectedItem === undefined) return; - const prevRows = [...rows]; + const prevRows = [...view.getVisibleRows()]; deleteTask(selectedItem); const index = prevRows.findIndex((task) => task.id === selectedItem.id); const newSelectedIndex = index === prevRows.length - 1 @@ -167,7 +166,7 @@ export function ProjectTableDemo() { ...state, selectedId: newSelectedIndex >= 0 ? prevRows[newSelectedIndex].id : undefined, })); - }, [deleteTask, rows, selectedItem, setTableState]); + }, [deleteTask, view, selectedItem, setTableState]); const keydownHandler = useCallback((event: KeyboardEvent) => { if ((event.metaKey || event.ctrlKey) && event.shiftKey && event.code === 'Enter') { @@ -251,7 +250,7 @@ export function ProjectTableDemo() { rows } + getRows={ view.getVisibleRows } columns={ columns } value={ tableState } onValueChange={ setTableState } @@ -259,7 +258,7 @@ export function ProjectTableDemo() { showColumnsConfig allowColumnsResizing allowColumnsReordering - { ...listProps } + { ...view.getListProps() } /> ); diff --git a/app/src/demo/tables/editableTable/helpers.ts b/app/src/demo/tables/editableTable/helpers.ts index b76861d92b..58e1c33a71 100644 --- a/app/src/demo/tables/editableTable/helpers.ts +++ b/app/src/demo/tables/editableTable/helpers.ts @@ -52,12 +52,18 @@ const findAllChildren = (tasks: Task[], parentTask: Task) => { export const deleteTaskWithChildren = (tasks: Record, taskToDelete: Task | null): Record => { const currentTasks = { ...tasks }; - if (!taskToDelete) { + let taskToBeDeleted = taskToDelete; + if (taskToBeDeleted === undefined) { + const rootItems = Object.values(currentTasks).filter((task) => task.parentId === undefined); + taskToBeDeleted = rootItems[rootItems.length - 1]; + } + + if (!taskToBeDeleted) { return currentTasks; } - const childrenIds = findAllChildren(Object.values(currentTasks), taskToDelete); - [taskToDelete.id, ...childrenIds].forEach((id) => { + const childrenIds = findAllChildren(Object.values(currentTasks), taskToBeDeleted); + [taskToBeDeleted.id, ...childrenIds].forEach((id) => { delete currentTasks[id]; }); diff --git a/app/src/demo/tables/filteredTable/FilteredTable.tsx b/app/src/demo/tables/filteredTable/FilteredTable.tsx index 67422c5ea1..5a06a779e7 100644 --- a/app/src/demo/tables/filteredTable/FilteredTable.tsx +++ b/app/src/demo/tables/filteredTable/FilteredTable.tsx @@ -7,8 +7,8 @@ import { } from '@epam/uui'; import { getFilters } from './filters'; import { - useLazyDataSource, useUuiContext, UuiContexts, useTableState, LazyDataSourceApiRequest, ITablePreset, - DataQueryFilter, + useUuiContext, UuiContexts, useTableState, LazyDataSourceApiRequest, ITablePreset, + DataQueryFilter, useLazyDataSource, } from '@epam/uui-core'; import { FilteredTableFooter } from './FilteredTableFooter'; import { Person } from '@epam/uui-docs'; @@ -58,7 +58,7 @@ export function FilteredTable() { return result; }, [svc.api.demo]); - const dataSource = useLazyDataSource( + const dataSource = useLazyDataSource>( { api: api, selectAll: false, diff --git a/app/src/demo/tables/masterDetailedTable/FilterPanel/FilterPanel.tsx b/app/src/demo/tables/masterDetailedTable/FilterPanel/FilterPanel.tsx index 5328c6be72..abb73cd538 100644 --- a/app/src/demo/tables/masterDetailedTable/FilterPanel/FilterPanel.tsx +++ b/app/src/demo/tables/masterDetailedTable/FilterPanel/FilterPanel.tsx @@ -10,13 +10,13 @@ import { FiltersBlock } from './FiltersBlock'; // import { ColumnsBlock } from './ColumnsBlock'; import { GroupingBlock } from './GroupingBlock'; -export interface IFilterPanelProps> extends ITableState { +export interface IFilterPanelProps extends ITableState { columns: DataColumnProps[]; filters: TableFiltersConfig[]; closePanel(): void; } -function FilterPanel(props: IFilterPanelProps) { +function FilterPanel(props: IFilterPanelProps) { return ( <> diff --git a/app/src/demo/tables/masterDetailedTable/FilterPanel/GroupingBlock/GroupingBlock.tsx b/app/src/demo/tables/masterDetailedTable/FilterPanel/GroupingBlock/GroupingBlock.tsx index f10e7630ec..97b3837a36 100644 --- a/app/src/demo/tables/masterDetailedTable/FilterPanel/GroupingBlock/GroupingBlock.tsx +++ b/app/src/demo/tables/masterDetailedTable/FilterPanel/GroupingBlock/GroupingBlock.tsx @@ -1,11 +1,11 @@ -import React from 'react'; +import React, { Dispatch, SetStateAction } from 'react'; import { Accordion, PickerList } from '@epam/uui'; import { DataTableState } from '@epam/uui-core'; import { groupingsDataSource } from '../../groupings'; interface GroupingBlockProps { tableState: DataTableState; - setTableState(newState: DataTableState): void; + setTableState: Dispatch>>; } function GroupingBlock({ tableState, setTableState }: GroupingBlockProps) { @@ -33,4 +33,4 @@ function GroupingBlock({ tableState, set ); } -export default React.memo(GroupingBlock); +export default React.memo(GroupingBlock) as typeof GroupingBlock; diff --git a/app/src/demo/tables/masterDetailedTable/MasterDetailedTable.tsx b/app/src/demo/tables/masterDetailedTable/MasterDetailedTable.tsx index 8df17fa4a5..56e7aedcb2 100644 --- a/app/src/demo/tables/masterDetailedTable/MasterDetailedTable.tsx +++ b/app/src/demo/tables/masterDetailedTable/MasterDetailedTable.tsx @@ -59,6 +59,7 @@ export function MasterDetailedTable() { .addDefaults({ getType: ({ __typename }) => __typename, getGroupBy: () => tableStateApi.tableState.filter?.groupBy, + complexIds: true, backgroundReload: true, fetchStrategy: 'parallel', cascadeSelection: true, diff --git a/app/src/demo/tables/masterDetailedTable/types.ts b/app/src/demo/tables/masterDetailedTable/types.ts index ced48ce973..962259ae3f 100644 --- a/app/src/demo/tables/masterDetailedTable/types.ts +++ b/app/src/demo/tables/masterDetailedTable/types.ts @@ -26,7 +26,7 @@ export type PersonFilters = { PersonEmploymentGroup: DataQueryFilter; }; -export type PersonTableFilter = DataQueryFilter & { groupBy?: GroupByLocation | GroupByEmployment | Array }; +export type PersonTableFilter = DataQueryFilter & { groupBy?: Array }; export interface Grouping { id: string; name: string; diff --git a/app/src/demo/tables/masterDetailedTable/useLazyDataSourceWithGrouping/groupingConfigBuilder.ts b/app/src/demo/tables/masterDetailedTable/useLazyDataSourceWithGrouping/groupingConfigBuilder.ts index 63d87e4463..52cd647313 100644 --- a/app/src/demo/tables/masterDetailedTable/useLazyDataSourceWithGrouping/groupingConfigBuilder.ts +++ b/app/src/demo/tables/masterDetailedTable/useLazyDataSourceWithGrouping/groupingConfigBuilder.ts @@ -121,7 +121,9 @@ export class GroupingConfigBuilder< LazyDataSourceProps[TType], TId[TType], TFilter[TType]>['api'] > ) { - return this.entitiesConfig[this.defaultEntity].api(...apiArgs); + const [request, context] = apiArgs; + const response = await this.entitiesConfig[this.defaultEntity].api(request, context); + return this.getResultsWithMeta(response, context?.parent, []); } private getGroupByPathForParent( diff --git a/app/src/docs/_examples/dataSources/DataSourceViewer.code.example.tsx b/app/src/docs/_examples/dataSources/DataSourceViewer.code.example.tsx index 8a3ba20dcb..4cd7c9104e 100644 --- a/app/src/docs/_examples/dataSources/DataSourceViewer.code.example.tsx +++ b/app/src/docs/_examples/dataSources/DataSourceViewer.code.example.tsx @@ -8,6 +8,7 @@ interface Props extends IEditable { selectAll?: boolean; getName?: (item: TItem) => string; dataSource: IDataSource; + onValueChange: React.Dispatch>>; } export function DataSourceViewer(props: Props) { diff --git a/app/src/docs/_examples/pickerModal/LazyTreePickerModal.example.tsx b/app/src/docs/_examples/pickerModal/LazyTreePickerModal.example.tsx index 051525d5dd..9bcb384848 100644 --- a/app/src/docs/_examples/pickerModal/LazyTreePickerModal.example.tsx +++ b/app/src/docs/_examples/pickerModal/LazyTreePickerModal.example.tsx @@ -28,7 +28,7 @@ export default function LazyTreePickerModal() { const handleModalOpening = useCallback(() => { context.uuiModals .show((props) => ( - initialValue={ value } dataSource={ dataSource } selectionMode="multi" diff --git a/app/src/docs/_examples/statusIndicator/WithTable.example.tsx b/app/src/docs/_examples/statusIndicator/WithTable.example.tsx index c8c9b19f18..45094156c6 100644 --- a/app/src/docs/_examples/statusIndicator/WithTable.example.tsx +++ b/app/src/docs/_examples/statusIndicator/WithTable.example.tsx @@ -55,7 +55,7 @@ const personColumns: DataColumnProps[] = [ export default function WithTableExample() { const { api } = useUuiContext(); - const { tableState, setTableState } = useTableState({ + const { tableState, setTableState } = useTableState({ columns: personColumns, }); diff --git a/app/src/docs/_examples/tables/ArrayTable.example.tsx b/app/src/docs/_examples/tables/ArrayTable.example.tsx index 6982c43132..21473c77e1 100644 --- a/app/src/docs/_examples/tables/ArrayTable.example.tsx +++ b/app/src/docs/_examples/tables/ArrayTable.example.tsx @@ -5,7 +5,7 @@ import { demoData, FeatureClass } from '@epam/uui-docs'; import css from './TablesExamples.module.scss'; export default function ArrayDataTableExample() { - const [value, onValueChange] = useState({}); + const [dataSourceState, setDataSourceState] = useState({}); const dataSource = useArrayDataSource( { @@ -14,7 +14,7 @@ export default function ArrayDataTableExample() { [], ); - const view = dataSource.useView(value, onValueChange, {}); + const view = dataSource.useView(dataSourceState, setDataSourceState, {}); const productColumns: DataColumnProps[] = useMemo( () => [ @@ -47,8 +47,8 @@ export default function ArrayDataTableExample() { diff --git a/app/src/docs/_examples/tables/ColumnsConfig.example.tsx b/app/src/docs/_examples/tables/ColumnsConfig.example.tsx index 5c84978dfb..a9db35044f 100644 --- a/app/src/docs/_examples/tables/ColumnsConfig.example.tsx +++ b/app/src/docs/_examples/tables/ColumnsConfig.example.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useState, useMemo } from 'react'; +import React, { useCallback, useState, useMemo, SetStateAction } from 'react'; import { DataTableState, DataColumnProps, useLazyDataSource, useUuiContext } from '@epam/uui-core'; import { Text, DataTable, Panel, IconButton } from '@epam/uui'; import { City } from '@epam/uui-docs'; @@ -91,17 +91,12 @@ export default function ColumnsConfigurationDataTableExample() { [], ); - const handleTableStateChange = useCallback((newState: DataTableState) => { + const handleTableStateChange = useCallback((newState: SetStateAction) => { + const updatedState = typeof newState === 'function' ? newState(tableState) : newState; // Set columns config to localStorage - svc.uuiUserSettings.set(LOCAL_STORAGE_KEY, newState.columnsConfig || {}); - setTableState(newState); - }, []); - - useEffect(() => { - return () => { - citiesDS.unsubscribeView(handleTableStateChange); - }; - }, []); + svc.uuiUserSettings.set(LOCAL_STORAGE_KEY, updatedState.columnsConfig || {}); + setTableState(updatedState); + }, [svc.uuiUserSettings, tableState]); const view = citiesDS.useView(tableState, handleTableStateChange, { getRowOptions: useCallback(() => ({ checkbox: { isVisible: true } }), []), diff --git a/app/src/docs/_examples/tables/EditableTable.example.tsx b/app/src/docs/_examples/tables/EditableTable.example.tsx index b57b8afd16..5bffc3c5f5 100644 --- a/app/src/docs/_examples/tables/EditableTable.example.tsx +++ b/app/src/docs/_examples/tables/EditableTable.example.tsx @@ -1,51 +1,29 @@ import React, { useCallback, useMemo, useState } from 'react'; -import { DataColumnProps, DataTableRowProps, Metadata, useArrayDataSource } from '@epam/uui-core'; +import { DataColumnProps, DataSourceState, DataTableRowProps, IImmutableMap, ItemsMap, Metadata, PatchOrdering, UuiContexts, useArrayDataSource, useAsyncDataSource, useUuiContext } from '@epam/uui-core'; import { Button, Checkbox, FlexSpacer, DataTable, DataTableCell, DataTableRow, DatePicker, FlexCell, FlexRow, Panel, PickerInput, TextArea, TextInput, useForm, IconButton } from '@epam/uui'; +import { TodoTask } from '@epam/uui-docs'; import { ReactComponent as deleteIcon } from '@epam/assets/icons/common/content-clear-18.svg'; import css from './TablesExamples.module.scss'; - -// Define interface describe data for each row -interface ToDoItem { - id: number; - isDone?: boolean; - name?: string; - priority?: number; - dueDate?: string; - comments?: string; -} +import { TApi } from '../../../data'; +import { useDataTableFocusManager } from '@epam/uui-components'; +import { ReactComponent as undoIcon } from '@epam/assets/icons/content-edit_undo-outline.svg'; +import { ReactComponent as redoIcon } from '@epam/assets/icons/content-edit_redo-outline.svg'; // Define a blank item - for use as a new item, and to simplify mock data definition below -const blankItem: Partial = { +const blankItem: Partial = { isDone: false, name: '', priority: null, comments: '', dueDate: '', + isNew: true, }; -// To store the last item id used -let id = 1; - -// Prepare mock data for the demo. Usually, you'll get initial data from server API call -const demoItems: ToDoItem[] = [ - { - ...blankItem, id: id++, name: 'Complete data sources re-work', comments: 'The plan is to unite all dataSources into a single "useList" hook', - }, { - ...blankItem, id: id++, name: 'Implement editable cells', isDone: true, - }, { - ...blankItem, id: id++, name: 'Find better ways to add/remove rows', dueDate: '01-09-2022', priority: 2, - }, { - ...blankItem, id: id++, name: 'Finalize the "Project" table demo', comments: 'We first need to build the add/remove rows helpers, and rows drag-n-drop', - }, { ...blankItem, id: id++, name: 'Complete cells replication' }, { - ...blankItem, id: id++, name: 'Better rows drag-n-drop support', comments: 'With state-management helpers, and tree/hierarchy support', - }, -]; - // Interface to hold form data. Here we'll only store items, so we might use ToDoItem[] as a state. // However, we'll have an object here to extend the form state if needed later. interface FormState { - items: ToDoItem[]; + items: IImmutableMap; } // Define priorities to use in PickerInput @@ -67,25 +45,67 @@ const metadata: Metadata = { }, }; +let savedItem: FormState = { + items: ItemsMap.blank({ getId: (todo) => todo.id }), + // items: ItemsMap.blank({ }), +}; + +// To store the last item id used +let id = -1; + +const defaultSorting: DataSourceState['sorting'] = [{ field: 'id', direction: 'asc' }]; + export default function EditableTableExample() { + const svc = useUuiContext(); + // Use form to manage state of the editable table const { - lens, save, revert, value, setValue, isChanged, + lens, save, revert, undo, canUndo, redo, canRedo, value, setValue, isChanged, } = useForm({ - value: { items: demoItems }, - onSave: () => Promise.resolve(), + value: savedItem, + onSave: (newValue) => { + savedItem = newValue; + return Promise.resolve({ form: newValue }); + }, getMetadata: () => metadata, }); + const dataTableFocusManager = useDataTableFocusManager({}, []); + + const newItems = useMemo(() => new Map(), []); // Prepare callback to add a new item to the list. const handleNewItem = useCallback(() => { + const newItem = { ...blankItem, id: --id }; + + newItems.set(newItem.id, true); // We can manipulate form state directly with the setValue // - pretty much like we do with the setState of React.useState. - setValue((current) => ({ ...current, items: [...current.items, { ...blankItem, id: id++ }] })); - }, []); + setValue((current) => ({ ...current, items: current.items.set(newItem.id, newItem) })); + dataTableFocusManager?.focusRow(newItem.id); + }, [setValue, newItems, dataTableFocusManager]); + + const handleDeleteItem = useCallback((item: TodoTask) => { + setValue((current) => ({ ...current, items: current.items.set(item.id, { ...item, isDeleted: true }) })); + }, [setValue]); // Use state to hold DataTable state - current sorting, filtering, etc. - const [tableState, setTableState] = useState({}); + const [tableState, setTableState] = useState({ sorting: defaultSorting }); + + const onTableStateChange = useCallback((state: React.SetStateAction) => { + setTableState((currentTableState) => { + let updatedState: DataSourceState; + if (typeof state === 'function') { + updatedState = state(currentTableState); + } else { + updatedState = state; + } + + if (!updatedState.sorting || !updatedState.sorting.length) { + updatedState.sorting = defaultSorting; + } + return updatedState; + }); + }, [setTableState]); // Define DataSource to use in PickerInput in the 'tags' column const pickerDataSource = useArrayDataSource({ items: tags }, []); @@ -113,6 +133,7 @@ export default function EditableTableExample() { ), fix: 'left', width: 300, + isSortable: true, }, { key: 'isDone', @@ -151,49 +172,64 @@ export default function EditableTableExample() { }, { key: 'actions', - render: () => null } color="secondary" />, + render: (item) => ( + handleDeleteItem(item) } + color="secondary" + /> + ), width: 55, alignSelf: 'center', allowResizing: false, }, - ] as DataColumnProps[], + ] as DataColumnProps[], [], ); // Create data-source and view to supply filtered/sorted data to the table in form of DataTableRows. // DataSources describe the way to extract some list/tree-structured data. - // Here we'll use ArrayDataSource - which gets data from an array, which we obtain from our Form - const dataSource = useArrayDataSource( + // Here we'll use ArrayDataSource - which gets data from an array, which we obtain from our Form. + const dataSource = useAsyncDataSource( { - items: value.items, + api: svc.api.demo.todos, }, [], ); // Make an IDataSourceView instance, which takes data from the DataSource, and transforms it into DataTableRows. // It considers current sorting, filtering, scroll position, etc. to get a flat list of currently visible rows. - const view = dataSource.useView(tableState, setTableState, { - getRowOptions: (item: ToDoItem, index: number) => ({ - ...lens.prop('items').index(index).toProps(), + const view = dataSource.useView(tableState, onTableStateChange, { + getRowOptions: (item: TodoTask) => ({ + ...lens.prop('items').key(item.id).default(item).toProps(), }), + patch: value.items, + getNewItemPosition: () => PatchOrdering.TOP, + isDeleted: (item) => item.isDeleted, }); // Render row callback. In simple cases, you don't need, as default implementation would work ok. // Here we override it to change row background for 'isDone' items. const renderRow = useCallback( - (props: DataTableRowProps) => , + (props: DataTableRowProps) => , [], ); - // Render the table, passing the prepared data to it in form of getVisibleRows callback, list props (e.g. items counts) + // Render the table, passing the prepared data to it in form of getRows callback, list props (e.g. items counts) return ( - + {/* Render a panel with Save/Revert buttons to control the form */}