go.charczuk.com@v0.0.0-20240327042549-bc490516bd1a/projects/nodes/static/_nextjs/src/components/editTableDrawer.tsx (about)

     1  /**
     2   * Copyright (c) 2024 - Present. Will Charczuk. All rights reserved.
     3   * Use of this source code is governed by a MIT license that can be found in the LICENSE file at the root of the repository.
     4   */
     5  import { Button, Classes, ControlGroup, Drawer, EditableText, FormGroup, Label, Spinner, SpinnerSize } from '@blueprintjs/core';
     6  import { EditTable } from './editTable';
     7  import { useCallback, useEffect, useState } from 'react';
     8  import * as api from '../api/nodes'
     9  import { DialogImportCsv } from './dialogImportCsv';
    10  import useStateStore, { StoreState } from '../store/store';
    11  import { shallow } from 'zustand/shallow';
    12  
    13  export interface EditTableDrawerProps {
    14    isOpen: boolean;
    15    onClose: () => void;
    16    node: api.Node;
    17    onLabelChange: (n: api.Node, value: string) => void;
    18    onNodeChange: (n: api.Node) => void;
    19    onError: (e) => void;
    20  }
    21  
    22  const stateSelector = (state: StoreState) => ({
    23    graph: state.graph,
    24  });
    25  
    26  export function EditTableDrawer(props: EditTableDrawerProps) {
    27    const { graph } = useStateStore(stateSelector, shallow);
    28    const [info, setInfo] = useState<api.TableInfo | undefined>();
    29    const [label, setLabel] = useState<string>(props.node.label)
    30    const [dialogImportCsvOpen, setDialogImportCsvOpen] = useState<boolean>(false);
    31  
    32    const suppressError = (e) => { }
    33  
    34    const fetchTableInfo = async () => {
    35      if (graph !== null) {
    36        const info = await api.getNodeValueTableInfo(graph.id, props.node.id)
    37        setInfo(info);
    38      }
    39    }
    40  
    41    useEffect(() => {
    42      if (props.isOpen) {
    43        fetchTableInfo()
    44      }
    45      setLabel(props.node.label)
    46    }, [graph, props.isOpen, props.node]);
    47  
    48    if (graph === null) {
    49      return
    50    }
    51  
    52    const onLabelChangeConfirm = useCallback((label: string) => {
    53      props.onLabelChange(props.node, label)
    54    }, [props.node])
    55  
    56    const onColumnAdded = useCallback(async (col: number, name: string) => {
    57      await api.putNodeValueTable(graph.id, props.node.id, [{ type: 'insert_column', col: col, name: name }]);
    58      props.onNodeChange(props.node);
    59      await fetchTableInfo()
    60    }, [graph, props.node])
    61  
    62    const onColumnRemoved = useCallback(async (col: number) => {
    63      await api.putNodeValueTable(graph.id, props.node.id, [{ type: 'delete_column', col: col }]);
    64      props.onNodeChange(props.node)
    65      await fetchTableInfo()
    66    }, [graph, props.node])
    67  
    68    const onColumnNameChanged = useCallback(async (col: number, name: string) => {
    69      await api.putNodeValueTable(graph.id, props.node.id, [{ type: 'set_column_name', col: col, name: name }]);
    70      props.onNodeChange(props.node)
    71      await fetchTableInfo()
    72    }, [graph, props.node])
    73  
    74    const onCellChanged = useCallback(async (row: number, col: number, value: any) => {
    75      await api.putNodeValueTable(graph.id, props.node.id, [{ type: 'set', row: row, col: col, value: value }]);
    76      props.onNodeChange(props.node)
    77      await fetchTableInfo()
    78    }, [graph, props.node])
    79  
    80    const onColumnReorder = useCallback(async (oldCol: number, newCol: number) => {
    81      await api.putNodeValueTable(graph.id, props.node.id, [{ type: 'reorder_column', old: oldCol, new: newCol }]);
    82      props.onNodeChange(props.node)
    83      await fetchTableInfo()
    84    }, [graph, props.node])
    85  
    86    const onRowRemoved = useCallback(async (row: number) => {
    87      await api.putNodeValueTable(graph.id, props.node.id, [{ type: 'delete_row', row: row }]);
    88      props.onNodeChange(props.node)
    89      await fetchTableInfo()
    90    }, [graph, props.node])
    91  
    92    const onRowReorder = useCallback(async (oldRow: number, newRow: number) => {
    93      await api.putNodeValueTable(graph.id, props.node.id, [{ type: 'reorder_row', old: oldRow, new: newRow }]);
    94      props.onNodeChange(props.node)
    95      await fetchTableInfo()
    96    }, [graph, props.node])
    97  
    98    const getVisibleRange = useCallback(async (range: api.TableRange) => {
    99      return await api.getNodeValueTableRange(graph.id, props.node.id, range);
   100    }, [graph, props.node])
   101  
   102    const submitImportCsvFile = useCallback((loadedFiles?: FileList) => {
   103      if (loadedFiles) {
   104        api.postNodeTableImportCsv(graph.id, props.node.id, loadedFiles).then(() => {
   105          setDialogImportCsvOpen(false);
   106          api.getNodeValueTableInfo(graph.id, props.node.id).then((info) => {
   107            setInfo(info);
   108          }).catch(suppressError);
   109        }).catch(suppressError);
   110      }
   111    }, [graph, props.node])
   112  
   113    const doExportCsv = useCallback(() => {
   114      window.location.href = `/api/v1/graph/${graph.id}/node.value.table/${props.node.id}/csv`
   115    }, [graph, props.node])
   116  
   117    if (!info) {
   118      if (props.isOpen) {
   119        return <Spinner size={SpinnerSize.LARGE} style={{ position: 'relative', left: '50%', top: '50%' }} />
   120      }
   121      return
   122    }
   123  
   124    return (
   125      <Drawer
   126        className='edit-table-drawer'
   127        icon='panel-table'
   128        isOpen={props.isOpen}
   129        onClose={props.onClose}
   130        title="Table Editor"
   131        autoFocus={false}
   132        position='left'
   133        isCloseButtonShown={true}
   134        canEscapeKeyClose={false}
   135        canOutsideClickClose={false}
   136        shouldReturnFocusOnClose={true}
   137        hasBackdrop={false}
   138        usePortal={false}
   139      >
   140        <div className={Classes.DRAWER_BODY}>
   141          <div className={`${Classes.DIALOG_BODY}`}>
   142            <div className="edit-table-drawer-label-row">
   143              <ControlGroup fill={true} vertical={false}>
   144                <EditableText value={label} onChange={(v) => setLabel(v)} confirmOnEnterKey={true} onConfirm={onLabelChangeConfirm} />
   145                <Button icon="cloud-upload" text={"Import CSV"} className={Classes.FIXED} onClick={() => setDialogImportCsvOpen(true)} />
   146                <Button icon="cloud-download" text={"Export CSV"} className={Classes.FIXED} onClick={doExportCsv} />
   147              </ControlGroup>
   148            </div>
   149            <EditTable
   150              info={info}
   151              getVisibleRange={getVisibleRange}
   152              onColumnAdded={onColumnAdded}
   153              onColumnReorder={onColumnReorder}
   154              onColumnNameChanged={onColumnNameChanged}
   155              onColumnRemoved={onColumnRemoved}
   156              onRowRemoved={onRowRemoved}
   157              onRowReorder={onRowReorder}
   158              onCellChanged={onCellChanged}
   159            />
   160          </div>
   161        </div>
   162        <DialogImportCsv isOpen={dialogImportCsvOpen} onClose={() => setDialogImportCsvOpen(false)} onLoad={submitImportCsvFile} />
   163      </Drawer>
   164    )
   165  }