app/src/sandbox/editableTable/ProjectTableDemo.tsx (303 lines of code) (raw):
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, ItemsMap, Metadata,
PatchOrdering, SortingOption, UuiContexts, getOrderBetween, indexToOrder, useDataRows, useTree, useUuiContext } from '@epam/uui-core';
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';
import { ReactComponent as insertAfter } from '@epam/assets/icons/table-row_plus_after-outline.svg';
import { ReactComponent as insertBefore } from '@epam/assets/icons/table-row_plus_before-outline.svg';
import { ReactComponent as deleteLast } from '@epam/assets/icons/table-row_remove-outline.svg';
import { ReactComponent as add } from '@epam/assets/icons/action-add-outline.svg';
import { Task } from './types';
import { getColumns } from './columns';
import css from './ProjectTableDemo.module.scss';
import { TApi } from '../../data';
import { ProjectTask } from '@epam/uui-docs';
interface FormState {
items: ItemsMap<number, ProjectTask>;
sorting: SortingOption<any>[],
}
const metadata: Metadata<FormState> = {
props: {
items: {
all: {
props: {
name: { isRequired: true },
},
},
},
},
};
let lastId = -1;
const defaultSorting = [{ field: 'order' }];
let savedValue: FormState = {
items: ItemsMap.blank({ getId: (item) => item.id }),
sorting: defaultSorting,
};
export function ProjectTableDemo() {
const svc = useUuiContext<TApi, UuiContexts>();
const [tableState, setState] = useState<DataTableState>({ sorting: defaultSorting });
const {
value, save, isChanged, revert, undo, canUndo, redo, canRedo, setValue, lens,
} = useForm<FormState>({
value: savedValue,
onSave: async (data) => {
// At this point you usually call api.saveSomething(value) to actually send changed data to server
savedValue = data;
},
getMetadata: () => metadata,
});
const setTableState = useCallback<React.Dispatch<React.SetStateAction<DataTableState>>>((newState) => {
setState((st) => {
const updatedTableState = typeof newState === 'function' ? { ...newState(st) } : { ...st, ...newState };
if (st.sorting !== updatedTableState.sorting) {
setValue((currentValue) => {
let newItems = ItemsMap.blank<number, ProjectTask>({ getId: (item) => item.id });
currentValue.items.forEach(({ tempOrder, ...item }) => {
newItems = newItems.set(item.id, item);
});
return { ...currentValue, items: newItems, sorting: updatedTableState.sorting };
});
}
return updatedTableState;
});
}, [setValue]);
const dataTableFocusManager = useDataTableFocusManager<ProjectTask['id']>({}, []);
const searchHandler = useCallback(
(val: string | undefined) => setTableState((currentTableState) => ({
...currentTableState,
search: val,
})),
[],
);
useEffect(() => {
if (tableState.sorting !== value.sorting) {
setState((state) => {
if (state.sorting !== value.sorting) {
return { ...state, sorting: value.sorting };
}
return state;
});
}
}, [setTableState, value.sorting, tableState.sorting]);
const getItemTemporaryOrder = (item: ProjectTask) => item.tempOrder;
const { tree, treeWithoutPatch, ...restProps } = useTree<ProjectTask, number>(
{
type: 'lazy',
api: (rq, ctx) => {
const filter = { parentId: ctx?.parentId };
return svc.api.demo.projectTasks({ ...rq, filter });
},
dataSourceState: tableState,
setDataSourceState: setTableState,
getId: (i) => i.id,
getParentId: (i) => i.parentId,
getChildCount: (task) => task.childCount,
backgroundReload: true,
patch: value.items,
getNewItemPosition: () => PatchOrdering.TOP,
getItemTemporaryOrder,
sortBy: (item, sorting) => {
return item[sorting.field as keyof ProjectTask] ?? '';
},
isDeleted: (item) => item.isDeleted,
},
[],
);
// Insert new/exiting top/bottom or above/below relative to other task
const insertTask = useCallback((
position: DropPosition,
relativeTask: ProjectTask | null = null,
existingTask: ProjectTask | null = null,
) => {
const task: ProjectTask = existingTask ? { ...existingTask } : { id: lastId--, name: '' };
if (position === 'inside') {
task.parentId = relativeTask.id;
position = 'top';
relativeTask = null;
} else if (relativeTask) {
task.parentId = relativeTask.parentId;
}
const originalListIds = treeWithoutPatch.getItems(task.parentId).ids;
const currentListIds = tree.getItems(task.parentId).ids;
const getIndex = (list: number[], id: number) => {
return list.indexOf(id);
};
const getOrderByIndex = (index: number) => {
const id = currentListIds[index];
const item = tree.getById(id) as ProjectTask;
// If item is not in patch, it's order is implied as numberToOrder() of it's original index (before any patch applied)
if (!item.tempOrder) {
const originalIndex = getIndex(originalListIds, id);
return indexToOrder(originalIndex);
} else {
return item.tempOrder;
}
};
let relativeToIndex: number;
if (relativeTask) {
relativeToIndex = getIndex(currentListIds, relativeTask.id);
} else {
relativeToIndex = position === 'top' ? 0 : currentListIds.length - 1;
}
const indexAbove = position === 'top' ? relativeToIndex - 1 : relativeToIndex;
const indexBelow = position === 'bottom' ? relativeToIndex + 1 : relativeToIndex;
const orderAbove = indexAbove >= 0 ? getOrderByIndex(indexAbove) : null;
const orderBelow = indexBelow < currentListIds.length ? getOrderByIndex(indexBelow) : null;
task.tempOrder = getOrderBetween(orderAbove, orderBelow);
setValue((currentValue) => {
return {
...currentValue,
items: currentValue.items.set(task.id, task),
};
});
setTableState((currentTableState) => {
return {
...currentTableState,
folded: (position as DropPosition) === 'inside'
? { ...currentTableState.folded, [`${task.parentId}`]: false }
: currentTableState.folded,
selectedId: task.id,
};
});
dataTableFocusManager?.focusRow(task.id);
}, [setValue, setTableState, dataTableFocusManager, tree, treeWithoutPatch]);
const deleteTask = useCallback((task: Task) => {
setValue((currentValue) => ({
...currentValue, items: currentValue.items.set(task.id, { ...task, isDeleted: true }),
}));
}, [setValue]);
const handleCanAcceptDrop = useCallback((params: AcceptDropParams<ProjectTask & { isTask: boolean }, Task>) => {
if (!params.srcData.isTask || params.srcData.id === params.dstData.id) {
return null;
} else {
return { bottom: true, top: true, inside: true };
}
}, []);
const handleDrop = useCallback(
(params: DropParams<ProjectTask, Task>) => insertTask(params.position, params.dstData, params.srcData),
[insertTask],
);
// console.log(value.items);
const { rows, listProps } = useDataRows({
tree,
...restProps,
getRowOptions: (task) => ({
...lens.prop('items').key(task.id).toProps(), // pass IEditable to each row to allow editing
// checkbox: { isVisible: true },
isSelectable: true,
dnd: {
srcData: { ...task, isTask: true },
dstData: { ...task, isTask: true },
canAcceptDrop: handleCanAcceptDrop,
onDrop: handleDrop,
},
}),
});
const columns = useMemo(
() => getColumns({ insertTask, deleteTask }),
[insertTask, deleteTask],
);
const selectedItem = useMemo(() => {
if (tableState.selectedId !== undefined) {
return tree.getById(tableState.selectedId) as ProjectTask;
}
return undefined;
}, [tableState.selectedId, value.items]);
const deleteSelectedItem = useCallback(() => {
if (selectedItem === undefined) return;
const prevRows = [...rows];
deleteTask(selectedItem);
const index = prevRows.findIndex((task) => task.id === selectedItem.id);
const newSelectedIndex = index === prevRows.length - 1
? (prevRows.length - 2)
: (index + 1);
setTableState((state) => ({
...state,
selectedId: newSelectedIndex >= 0 ? prevRows[newSelectedIndex].id : undefined,
}));
}, [deleteTask, rows, selectedItem, setTableState]);
const keydownHandler = useCallback((event: KeyboardEvent) => {
if ((event.metaKey || event.ctrlKey) && event.shiftKey && event.code === 'Enter') {
event.preventDefault();
insertTask('top', selectedItem);
return;
}
if ((event.metaKey || event.ctrlKey) && event.code === 'Enter') {
event.preventDefault();
insertTask('bottom', selectedItem);
return;
}
if ((event.metaKey || event.ctrlKey) && event.code === 'Backspace') {
event.preventDefault();
deleteSelectedItem();
return;
}
}, [insertTask, selectedItem, deleteSelectedItem]);
useEffect(() => {
document.addEventListener('keydown', keydownHandler);
return () => {
document.removeEventListener('keydown', keydownHandler);
};
}, [keydownHandler]);
const getKeybindingWithControl = (tooltip: string, keybindingWithoutControl: string) => {
const controlKey = navigator.platform.indexOf('Mac') === 0 ? '⌘' : 'Ctrl';
return (
<>
{ tooltip }
{' '}
<br />
{ `(${controlKey} + ${keybindingWithoutControl})` }
</>
);
};
return (
<Panel cx={ css.container }>
<FlexRow spacing="18" padding="24" vPadding="18" borderBottom={ true } background="surface-main">
<FlexCell width="auto">
<Tooltip content={ getKeybindingWithControl('Add new task', 'Enter') } placement="bottom">
<Button size="30" icon={ add } caption="Add Task" onClick={ () => insertTask('bottom') } />
</Tooltip>
</FlexCell>
<FlexCell width="auto">
<Tooltip content={ getKeybindingWithControl('Add new task below', 'Enter') } placement="bottom">
<IconButton size="24" icon={ insertAfter } onClick={ () => insertTask('bottom', selectedItem) } />
</Tooltip>
</FlexCell>
<FlexCell width="auto">
<Tooltip content={ getKeybindingWithControl('Add new task above', 'Shift + Enter') } placement="bottom">
<IconButton size="24" icon={ insertBefore } onClick={ () => insertTask('top', selectedItem) } />
</Tooltip>
</FlexCell>
<FlexCell width="auto">
<Tooltip content={ getKeybindingWithControl('Delete task', 'Backspace') } placement="bottom">
<IconButton size="24" icon={ deleteLast } onClick={ () => deleteSelectedItem() } isDisabled={ selectedItem === undefined } />
</Tooltip>
</FlexCell>
<FlexSpacer />
<FlexCell cx={ css.search } width={ 295 }>
<SearchInput value={ tableState.search } onValueChange={ searchHandler } placeholder="Search" debounceDelay={ 1000 } />
</FlexCell>
<div className={ css.divider } />
<FlexCell width="auto">
<IconButton size="18" icon={ undoIcon } onClick={ undo } isDisabled={ !canUndo } />
</FlexCell>
<FlexCell width="auto">
<IconButton size="18" icon={ redoIcon } onClick={ redo } isDisabled={ !canRedo } />
</FlexCell>
<FlexCell width="auto">
<Button size="30" caption="Cancel" onClick={ revert } isDisabled={ !isChanged } />
</FlexCell>
<FlexCell width="auto">
<Button size="30" color="accent" caption="Save" onClick={ save } isDisabled={ !isChanged } />
</FlexCell>
</FlexRow>
<DataTable
headerTextCase="upper"
rows={ rows }
columns={ columns }
value={ tableState }
onValueChange={ setTableState }
dataTableFocusManager={ dataTableFocusManager }
showColumnsConfig
allowColumnsResizing
allowColumnsReordering
{ ...listProps }
/>
</Panel>
);
}