From 1e3a939c56aec0200cc663765599bcbf6e30e446 Mon Sep 17 00:00:00 2001 From: tomtitherington Date: Wed, 8 Nov 2023 21:17:59 +0000 Subject: [PATCH] Add ability to add columns/properties to the table - Refactored the logic within AddPropertyHeader to update the store when the add button is clicked. No more debounce. - Adjustments made the the people store and now using 'immer' to handle mutations to the state in a safe manner. - Moved selectable cells/header components to their own files. --- .dictionary/custom.txt | 1 + frontend/package.json | 1 + .../components/table/cells/SelectableCell.tsx | 19 ++++++ .../table/headers/AddPropertyHeader.tsx | 49 ++++++--------- .../table/headers/SelectableHeader.tsx | 18 ++++++ .../people/components/table/index.tsx | 62 ++++++++----------- .../features/people/stores/usePeopleStore.ts | 32 ++++++---- frontend/yarn.lock | 5 ++ 8 files changed, 111 insertions(+), 76 deletions(-) create mode 100644 frontend/src/features/people/components/table/cells/SelectableCell.tsx create mode 100644 frontend/src/features/people/components/table/headers/SelectableHeader.tsx diff --git a/.dictionary/custom.txt b/.dictionary/custom.txt index dcdf9ae..e961721 100644 --- a/.dictionary/custom.txt +++ b/.dictionary/custom.txt @@ -88,3 +88,4 @@ Customise popperjs tanstack upsert +immer diff --git a/frontend/package.json b/frontend/package.json index f4c3f77..e31a055 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -21,6 +21,7 @@ "babel-jest": "^29.4.3", "formik": "^2.2.9", "history": "^5.3.0", + "immer": "^10.0.3", "lodash": "^4.17.21", "nanoid": "^5.0.2", "openapi-typescript": "^6.1.0", diff --git a/frontend/src/features/people/components/table/cells/SelectableCell.tsx b/frontend/src/features/people/components/table/cells/SelectableCell.tsx new file mode 100644 index 0000000..d0d3de0 --- /dev/null +++ b/frontend/src/features/people/components/table/cells/SelectableCell.tsx @@ -0,0 +1,19 @@ +import { Row } from '@tanstack/react-table'; +import { IndeterminateCheckbox } from 'components'; + +const SelectableHeader = ({ row }: { row: Row }) => { + return ( +
+ +
+ ); +}; + +export default SelectableHeader; diff --git a/frontend/src/features/people/components/table/headers/AddPropertyHeader.tsx b/frontend/src/features/people/components/table/headers/AddPropertyHeader.tsx index 328c66b..e35a4bd 100644 --- a/frontend/src/features/people/components/table/headers/AddPropertyHeader.tsx +++ b/frontend/src/features/people/components/table/headers/AddPropertyHeader.tsx @@ -1,11 +1,11 @@ import { Popover, Transition } from '@headlessui/react'; import { SlimTextInput } from 'components/inputs'; -import { usePeopleStore, PropertyDefinition } from 'features/people/stores/usePeopleStore'; -import { Fragment, useCallback, useEffect, useRef, useState } from 'react'; +import { usePeopleStore } from 'features/people/stores/usePeopleStore'; +import { Fragment, useCallback, useState } from 'react'; import { MdAdd } from 'react-icons/md'; import { usePopper } from 'react-popper'; -import _ from 'lodash'; import { nanoid } from 'nanoid'; +import { MainButton } from 'components/buttons'; const AddPropertyHeader = () => { const [referenceElement, setReferenceElement] = useState(null); @@ -15,30 +15,13 @@ const AddPropertyHeader = () => { }); const [id] = useState(() => nanoid()); // Initialize with a new GUID - const [property, setProperty] = useState>>({}); + const [displayName, setDisplayName] = useState(''); + const [identifier, setIdentifier] = useState(''); - const debounce = useCallback( - // Delay saving state until user activity stops - _.debounce((definition: Partial>) => { - usePeopleStore.getState().upsertDefinition(id, definition); - // API Calls go here - const val = usePeopleStore.getState().propertyDefinitions; - console.log(val); - }, 750), // Delay (ms) - [usePeopleStore.getState().propertyDefinitions], - ); - - const handleChange = useCallback( - (event: React.ChangeEvent) => { - const { name, value } = event.target; - setProperty(prev => { - const updatedProperty = { ...prev, [name]: value }; - debounce(updatedProperty); - return updatedProperty; - }); - }, - [debounce], // Only recreate this function if debounceSave changes - ); + //TODO: Do I need to use useCallback here? + const handleAddColumn = useCallback(() => { + usePeopleStore.getState().upsertDefinition(id, { header: displayName, accessor: identifier }); + }, [usePeopleStore.getState().propertyDefinitions, id, displayName, identifier]); return ( @@ -62,19 +45,27 @@ const AddPropertyHeader = () => { ref={setPopperElement} style={styles.popper} {...attributes.popper} - className="w-64 overflow-hidden p-4 bg-white rounded-md shadow-lg ring-1 ring-black ring-opacity-5"> + className="flex flex-col w-64 overflow-hidden p-4 gap-2 bg-white rounded-md shadow-lg ring-1 ring-black + ring-opacity-5"> handleChange(event)} + onChange={e => setDisplayName(e.target.value)} inputProps={{ name: 'header' }} /> handleChange(event)} + onChange={e => setIdentifier(e.target.value)} inputProps={{ name: 'accessor' }} /> +
+ } + onClick={handleAddColumn} + /> diff --git a/frontend/src/features/people/components/table/headers/SelectableHeader.tsx b/frontend/src/features/people/components/table/headers/SelectableHeader.tsx new file mode 100644 index 0000000..f368b42 --- /dev/null +++ b/frontend/src/features/people/components/table/headers/SelectableHeader.tsx @@ -0,0 +1,18 @@ +import { Table } from '@tanstack/react-table'; +import { IndeterminateCheckbox } from 'components'; + +const SelectableHeader = ({ table }: { table: Table }) => { + return ( +
+ +
+ ); +}; + +export default SelectableHeader; diff --git a/frontend/src/features/people/components/table/index.tsx b/frontend/src/features/people/components/table/index.tsx index d54563e..2ea2aa1 100644 --- a/frontend/src/features/people/components/table/index.tsx +++ b/frontend/src/features/people/components/table/index.tsx @@ -11,58 +11,48 @@ import { MdAdd } from 'react-icons/md'; import EditableCell from './cells/EditableCell'; import { IndeterminateCheckbox } from 'components'; import AddPropertyHeader from './headers/AddPropertyHeader'; +import { usePeopleStore } from 'features/people/stores/usePeopleStore'; +import SelectableHeader from './headers/SelectableHeader'; +import SelectableCell from './cells/SelectableCell'; -const Table = ({ data, columnJson }: { data: any[]; columnJson: any[] }) => { - // Convert the structure to column definitions for TanStack Table - const createColumns = (columnStructure: any[]): ColumnDef[] => { +interface TableProps { + data: any[]; + columnJson: any[]; + className?: string; +} + +const Table = ({ data, columnJson, className }: TableProps) => { + const propertyDefs = usePeopleStore(state => state.propertyDefinitions); + + const columns = useMemo(() => { const columnHelper = createColumnHelper(); - const userColumns = columnStructure.map(column => { - return columnHelper.accessor(column.accessor, { - header: ({ header }) => , + + const cols = Object.entries(propertyDefs).map(([key, value]) => { + return columnHelper.accessor(value.accessor, { + header: ({ header }) => , cell: info => , - size: column.size, + size: 180, }); }); + return [ { id: 'select', - header: ({ table }) => ( -
- -
- ), - cell: ({ row }) => ( -
- -
- ), + header: ({ table }) => , + cell: ({ row }) => , size: 48, maxSize: 48, minSize: 48, }, - ...userColumns, + ...cols, { id: 'add_property', - header: ({table}) => , + header: ({ table }) => , cell: ({ row }) =>
, }, - ]; - }; + ] as ColumnDef[]; + }, [propertyDefs]); - const columns = useMemo(() => createColumns(columnJson), [columnJson]); const [rowSelection, setRowSelection] = useState({}); const tableInstance = useReactTable({ @@ -78,7 +68,7 @@ const Table = ({ data, columnJson }: { data: any[]; columnJson: any[] }) => { }); return ( -
+
{tableInstance.getHeaderGroups().map(headerGroup => (
diff --git a/frontend/src/features/people/stores/usePeopleStore.ts b/frontend/src/features/people/stores/usePeopleStore.ts index b6132bc..4090a84 100644 --- a/frontend/src/features/people/stores/usePeopleStore.ts +++ b/frontend/src/features/people/stores/usePeopleStore.ts @@ -1,14 +1,15 @@ +import { produce } from 'immer'; import { create } from 'zustand'; export interface PropertyDefinition { id: string; - accessor?: string; - header?: string; + accessor: string; + header: string; type?: string; } interface State { - propertyDefinitions: Record; + propertyDefinitions: Record>; } //TODO: Maybe rename to upsertProperty ? @@ -17,12 +18,21 @@ interface Actions { } export const usePeopleStore = create(set => ({ - propertyDefinitions: {}, - upsertDefinition: (id, definition) => - set(state => ({ - propertyDefinitions: { - ...state.propertyDefinitions, - [id]: { ...definition, id }, // Merge with the existing data - }, - })), + propertyDefinitions: { + id_1: { accessor: 'name', header: 'Name' }, + id_2: { accessor: 'age', header: 'Age' }, + }, + upsertDefinition: (id, newDefinition) => + set( + produce(draft => { + if (draft.propertyDefinitions[id]) { + // If the definition exists, merge it + Object.assign(draft.propertyDefinitions[id], newDefinition); + } else { + // If the definition does not exist, create a new one + // (you should validate the full object structure here) + draft.propertyDefinitions[id] = newDefinition as Omit; + } + }), + ), })); diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 8524f54..85777cc 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -3463,6 +3463,11 @@ ignore@^5.2.0: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== +immer@^10.0.3: + version "10.0.3" + resolved "https://registry.yarnpkg.com/immer/-/immer-10.0.3.tgz#a8de42065e964aa3edf6afc282dfc7f7f34ae3c9" + integrity sha512-pwupu3eWfouuaowscykeckFmVTpqbzW+rXFCX8rQLkZzM9ftBmU/++Ra+o+L27mz03zJTlyV4UUr+fdKNffo4A== + import-fresh@^3.0.0, import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"