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 }