export function ProjectTableDemo()

in app/src/sandbox/editableTable/ProjectTableDemo.tsx [46:305]


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') } />