From 16267e572bf292dd96a1ca52d62b8bea605d7786 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 31 Jul 2025 19:02:30 +0000 Subject: [PATCH 1/2] Refactor grid components, improve performance, and add virtualization Co-authored-by: skylar-anderson --- app/actions.ts | 4 +- app/columns/IssuePRColumnType.tsx | 2 +- app/components/Cell.tsx | 18 +- app/components/ColumnTitle.tsx | 94 +++++---- app/components/GridContext.tsx | 298 ++++++++++++++++------------ app/components/GridTable.tsx | 102 ++++++---- app/components/NewColumnForm.tsx | 208 +++++++++---------- app/components/Row.tsx | 12 +- app/components/VirtualizedTable.tsx | 120 +++++++++++ app/functions/updateIssue.ts | 4 +- 10 files changed, 535 insertions(+), 327 deletions(-) create mode 100644 app/components/VirtualizedTable.tsx diff --git a/app/actions.ts b/app/actions.ts index 878e836..070352e 100644 --- a/app/actions.ts +++ b/app/actions.ts @@ -15,7 +15,7 @@ export type Option = { export type PrimaryDataType = 'issue' | 'commit' | 'pull-request' | 'snippet' | 'item'; type GridCellState = 'empty' | 'generating' | 'done' | 'error'; -export type ColumnType = 'text' | 'select' | 'select-user' | 'file' | 'issue-pr' | 'commit'; +export type ColumnType = 'text' | 'select' | 'select-user' | 'file' | 'boolean' | 'issue-pr' | 'commit'; export type ColumnResponse = { text: string; @@ -24,6 +24,7 @@ export type ColumnResponse = { file: | { file: { path: string; repository: string } } | { files: { path: string; repository: string }[] }; + boolean: { value: boolean }; 'issue-pr': { reference?: { number: number; @@ -50,7 +51,6 @@ export type ColumnResponse = { message?: string; }>; }; - boolean: boolean; }; export type GridCell = { diff --git a/app/columns/IssuePRColumnType.tsx b/app/columns/IssuePRColumnType.tsx index bf612ce..efeb0ce 100644 --- a/app/columns/IssuePRColumnType.tsx +++ b/app/columns/IssuePRColumnType.tsx @@ -110,7 +110,7 @@ export const IssuePRColumnType: BaseColumnType<'issue-pr'> = { parsed = JSON.parse(responseContent); } catch (error) { console.error('Failed to parse response content:', error); - return multiple ? { references: [] } : { reference: null }; + return multiple ? { references: [] } : { reference: undefined }; } return multiple ? { references: parsed.references } : { reference: parsed.reference }; }, diff --git a/app/components/Cell.tsx b/app/components/Cell.tsx index 3a9a3da..ce2f25e 100644 --- a/app/components/Cell.tsx +++ b/app/components/Cell.tsx @@ -14,7 +14,7 @@ type CellProps = { rowIndex?: number; }; -export default function Cell({ sx, cell, onClick, isSelected = false, rowIndex }: CellProps) { +function Cell({ sx, cell, onClick, isSelected = false, rowIndex }: CellProps) { const { deleteRow } = useGridContext(); const isPrimaryCell = rowIndex !== undefined; @@ -31,6 +31,12 @@ export default function Cell({ sx, cell, onClick, isSelected = false, rowIndex } backgroundColor: 'canvas.inset', }; + const handleDeleteRow = React.useCallback(() => { + if (rowIndex !== undefined) { + deleteRow(rowIndex); + } + }, [deleteRow, rowIndex]); + return ( - deleteRow(rowIndex)}> + @@ -88,7 +94,7 @@ export default function Cell({ sx, cell, onClick, isSelected = false, rowIndex } ); } -export function GridCellContent({ cell }: { cell: GridCell }) { +const GridCellContent = React.memo(function GridCellContent({ cell }: { cell: GridCell }) { if (cell.state === 'error') { return cell.errorMessage; } @@ -98,4 +104,8 @@ export function GridCellContent({ cell }: { cell: GridCell }) { const columnType = columnTypes[cell.columnType] as BaseColumnType; return columnType.renderCell(cell); -} +}); + +// Memoize Cell component to prevent unnecessary re-renders +export default React.memo(Cell); +export { GridCellContent }; diff --git a/app/components/ColumnTitle.tsx b/app/components/ColumnTitle.tsx index 33eb8ba..c2051bc 100644 --- a/app/components/ColumnTitle.tsx +++ b/app/components/ColumnTitle.tsx @@ -1,62 +1,74 @@ -import { IconButton, Box, ActionMenu, ActionList } from '@primer/react'; -import { KebabHorizontalIcon, PencilIcon, TrashIcon } from '@primer/octicons-react'; +import React, { useCallback } from 'react'; +import { Box, Text, IconButton, ActionMenu, ActionList } from '@primer/react'; +import { KebabHorizontalIcon, TrashIcon } from '@primer/octicons-react'; import { useGridContext } from './GridContext'; -export default function ColumnTitle({ title, index }: { title: string; index?: number }) { - const { deleteColumnByIndex } = useGridContext(); +type ColumnTitleProps = { + title: string; + index?: number; +}; + +function ColumnTitle({ title, index }: ColumnTitleProps) { + const { deleteColumnByIndex, setGroupBy, setFilterBy } = useGridContext(); + const hasIndex = index !== undefined; + + const handleDeleteColumn = useCallback(() => { + if (index !== undefined) { + deleteColumnByIndex(index); + } + }, [deleteColumnByIndex, index]); + + const handleGroupBy = useCallback(() => { + setGroupBy(title); + }, [setGroupBy, title]); + + const handleClearFilters = useCallback(() => { + setFilterBy(undefined, undefined); + }, [setFilterBy]); + return ( - {title} - - - - - - - - {index !== undefined && ( - deleteColumnByIndex(index)}> + {title} + {hasIndex && ( + + + + + + + Group by {title} + Clear filters + + - Delete + Delete column - )} - alert('Copy link clicked')}> - - - - Edit - - - - + + + + )} ); } + +export default React.memo(ColumnTitle); diff --git a/app/components/GridContext.tsx b/app/components/GridContext.tsx index 6e20b57..9ca5958 100644 --- a/app/components/GridContext.tsx +++ b/app/components/GridContext.tsx @@ -1,4 +1,4 @@ -import React, { createContext, useContext, ReactNode, useState, useCallback } from 'react'; +import React, { createContext, useContext, ReactNode, useState, useCallback, useMemo } from 'react'; import { SuccessfulPrimaryColumnResponse, ErrorResponse, GridState, GridCell } from '../actions'; import type { ColumnResponse, ColumnType, GridCol, Option } from '../actions'; import useLocalStorage from '../utils/local-storage'; @@ -13,23 +13,26 @@ export type Grid = { createdAt: Date; }; -type GridContextType = { +type GridStateContextType = { gridState: GridState | null; + selectedIndex: number | null; + currentGridId: string | null; + isSavingGist: boolean; +}; + +type GridActionsContextType = { setGridState: React.Dispatch>; selectRow: (index: number | null) => void; updateCellState: (columnTitle: string, cellIndex: number, newCellContents: GridCell) => void; addNewColumn: (props: NewColumnProps) => void; inititializeGrid: (s: string) => Promise; - selectedIndex: number | null; deleteColumnByIndex: (index: number) => void; setGroupBy: (columnTitle: string | undefined) => void; setFilterBy: (columnTitle: string | undefined, filterValue: string | undefined) => void; - currentGridId: string | null; setCurrentGridId: (id: string) => void; getAllGrids: () => Grid[]; deleteGrid: (id: string) => void; saveGridAsGist: () => Promise; - isSavingGist: boolean; // Add this new property deleteRow: (index: number) => void; }; @@ -41,29 +44,50 @@ type NewColumnProps = { multiple?: boolean; }; -const GridContext = createContext(undefined); +const GridStateContext = createContext(undefined); +const GridActionsContext = createContext(undefined); -export const useGridContext = () => { - const context = useContext(GridContext); +export const useGridState = () => { + const context = useContext(GridStateContext); + if (context === undefined) { + throw new Error('useGridState must be used within a GridProvider'); + } + return context; +}; + +export const useGridActions = () => { + const context = useContext(GridActionsContext); if (context === undefined) { - throw new Error('useGridContext must be used within a GridProvider'); + throw new Error('useGridActions must be used within a GridProvider'); } return context; }; +// Legacy hook for backward compatibility +export const useGridContext = () => { + const state = useGridState(); + const actions = useGridActions(); + return { ...state, ...actions }; +}; + type ProviderProps = { hydrateCell: (cell: GridCell) => Promise<{ promise: Promise }>; createPrimaryColumn: (s: string) => Promise; children: ReactNode; }; -export const GridProvider = ({ createPrimaryColumn, hydrateCell, children }: ProviderProps) => { +export const GridProvider = React.memo(function GridProvider({ createPrimaryColumn, hydrateCell, children }: ProviderProps) { const [grids, setGrids] = useLocalStorage>('grids', {}); const [currentGridId, setCurrentGridId] = useState(null); + const [selectedIndex, setSelectedIndex] = useState(null); + const [isSavingGist, setIsSavingGist] = useState(false); - const gridState = currentGridId ? grids[currentGridId] : null; + const gridState = useMemo(() => + currentGridId ? grids[currentGridId] : null, + [currentGridId, grids] + ); - const setGridState: React.Dispatch> = (newState) => { + const setGridState: React.Dispatch> = useCallback((newState) => { if (currentGridId) { setGrids((prevGrids) => ({ ...prevGrids, @@ -73,13 +97,13 @@ export const GridProvider = ({ createPrimaryColumn, hydrateCell, children }: Pro : (newState ?? prevGrids[currentGridId]), })); } - }; + }, [currentGridId, setGrids]); const getAllGrids = useCallback(() => { return Object.entries(grids).map(([id, grid]) => ({ id, title: grid.title, - rowCount: grid.primaryColumn.length, + rowCount: grid.primaryColumn.filter(cell => !cell.deleted).length, columnCount: grid.columns.length + 1, createdAt: new Date(), })); @@ -99,7 +123,7 @@ export const GridProvider = ({ createPrimaryColumn, hydrateCell, children }: Pro [setGrids, currentGridId] ); - async function inititializeGrid(title: string): Promise { + const inititializeGrid = useCallback(async (title: string): Promise => { const result = await createPrimaryColumn(title); if (!result.success) { throw new Error(result.message); @@ -112,19 +136,42 @@ export const GridProvider = ({ createPrimaryColumn, hydrateCell, children }: Pro })); setCurrentGridId(newGridId); return newGridId; - } - - const [selectedIndex, setSelectedIndex] = useState(null); + }, [createPrimaryColumn, setGrids]); - const selectRow = (index: number | null) => { + const selectRow = useCallback((index: number | null) => { if (!gridState) { console.warn("Can't select row without grid state!"); return; } setSelectedIndex(index); - }; + }, [gridState]); - function addNewColumn({ title, instructions, type, options, multiple }: NewColumnProps) { + const updateCellState = useCallback((columnTitle: string, cellIndex: number, newCellContents: GridCell) => { + setGridState((prevState) => { + if (prevState === null) { + return null; + } + return { + ...prevState, + columns: prevState.columns.map((column) => { + if (column.title === columnTitle) { + return { + ...column, + cells: column.cells.map((c, i) => { + if (i === cellIndex) { + return newCellContents; + } + return c; + }), + }; + } + return column; + }), + }; + }); + }, [setGridState]); + + const addNewColumn = useCallback(({ title, instructions, type, options, multiple }: NewColumnProps) => { if (!gridState) { alert("Can't add column without grid state!"); return; @@ -169,9 +216,9 @@ export const GridProvider = ({ createPrimaryColumn, hydrateCell, children }: Pro updateCellState(title, cellIndex, hydratedCell); }); }); - } + }, [gridState, setGridState, hydrateCell, updateCellState]); - const deleteColumnByIndex = (index: number) => { + const deleteColumnByIndex = useCallback((index: number) => { setGridState((prevState) => { if (prevState === null) { return null; @@ -181,9 +228,9 @@ export const GridProvider = ({ createPrimaryColumn, hydrateCell, children }: Pro columns: prevState.columns.filter((_, colIndex) => colIndex !== index), }; }); - }; + }, [setGridState]); - const setGroupBy = (columnTitle: string | undefined) => { + const setGroupBy = useCallback((columnTitle: string | undefined) => { setGridState((prevState) => { if (prevState === null) { return null; @@ -193,9 +240,9 @@ export const GridProvider = ({ createPrimaryColumn, hydrateCell, children }: Pro groupBy: columnTitle, }; }); - }; + }, [setGridState]); - const setFilterBy = (columnTitle: string | undefined, filterValue: string | undefined) => { + const setFilterBy = useCallback((columnTitle: string | undefined, filterValue: string | undefined) => { setGridState((prevState) => { if (prevState === null) { return null; @@ -208,67 +255,37 @@ export const GridProvider = ({ createPrimaryColumn, hydrateCell, children }: Pro }, }; }); - }; - - const updateCellState = (columnTitle: string, cellIndex: number, newCellContents: GridCell) => { - setGridState((prevState) => { - if (prevState === null) { - return null; - } - return { - ...prevState, - columns: prevState.columns.map((column) => { - if (column.title === columnTitle) { - return { - ...column, - cells: column.cells.map((c, i) => { - if (i === cellIndex) { - return newCellContents; - } - return c; - }), - }; - } - return column; - }), - }; - }); - }; - - const [isSavingGist, setIsSavingGist] = useState(false); + }, [setGridState]); - const saveGridAsGist = async (): Promise => { - if (!gridState) { - console.warn("Can't save grid without grid state!"); - return null; - } - - setIsSavingGist(true); - try { - const markdownTable = generateMarkdownTable(gridState); - const filename = `${gridState.title}.md`; - const gistUrl = await createGist(filename, markdownTable); - return gistUrl; - } catch (error) { - console.error('Failed to save grid as gist:', error); - return null; - } finally { - setIsSavingGist(false); - } - }; + const deleteRow = useCallback( + (index: number) => { + setGridState((prevState) => { + if (!prevState) return null; - const fileToMd = (file: { path: string; repository: string }) => { - const href = `https://github.com/${file.repository}/blob/${file.path}`; - return `[${file.path}](${href})`; - }; + return { + ...prevState, + primaryColumn: prevState.primaryColumn.map((cell, i) => + i === index ? { ...cell, deleted: true } : cell + ), + }; + }); + }, + [setGridState] + ); - function generateMarkdownTable(gridState: GridState): string { + // Memoize markdown generation to avoid expensive recalculations + const generateMarkdownTable = useCallback((gridState: GridState): string => { const headers = ['Primary Column', ...gridState.columns.map((col) => col.title)]; function escapeMarkdown(text: string): string { return text.replace(/\|/g, '\\|').replace(/\n/g, '
'); } + const fileToMd = (file: { path: string; repository: string }) => { + const href = `https://github.com/${file.repository}/blob/${file.path}`; + return `[${file.path}](${href})`; + }; + function formatCell(response: ColumnResponse[keyof ColumnResponse]): string { if (!response) return ''; if (typeof response === 'string') { @@ -292,62 +309,89 @@ export const GridProvider = ({ createPrimaryColumn, hydrateCell, children }: Pro } } - const rows = gridState.primaryColumn.map((primaryCell, index) => { - return [ - formatCell(primaryCell.response), - ...gridState.columns.map((col) => { - const cell = col.cells[index]; - return formatCell(cell.response as ColumnResponse[keyof ColumnResponse]); - }), - ]; - }); + const rows = gridState.primaryColumn + .filter(cell => !cell.deleted) + .map((primaryCell, index) => { + return [ + formatCell(primaryCell.response), + ...gridState.columns.map((col) => { + const cell = col.cells[index]; + return formatCell(cell.response as ColumnResponse[keyof ColumnResponse]); + }), + ]; + }); const headerRow = `| ${headers.map(escapeMarkdown).join(' | ')} |`; const separatorRow = `| ${headers.map(() => '---').join(' | ')} |`; const dataRows = rows.map((row) => `| ${row.join(' | ')} |`); return [headerRow, separatorRow, ...dataRows].join('\n'); - } + }, []); - const deleteRow = useCallback( - (index: number) => { - setGridState((prevState) => { - if (!prevState) return null; + const saveGridAsGist = useCallback(async (): Promise => { + if (!gridState) { + console.warn("Can't save grid without grid state!"); + return null; + } - return { - ...prevState, - primaryColumn: prevState.primaryColumn.map((cell, i) => - i === index ? { ...cell, deleted: true } : cell - ), - }; - }); - }, - [setGridState] - ); + setIsSavingGist(true); + try { + const markdownTable = generateMarkdownTable(gridState); + const filename = `${gridState.title}.md`; + const gistUrl = await createGist(filename, markdownTable); + return gistUrl; + } catch (error) { + console.error('Failed to save grid as gist:', error); + return null; + } finally { + setIsSavingGist(false); + } + }, [gridState, generateMarkdownTable]); + + // Memoize state context value + const stateValue = useMemo(() => ({ + gridState, + selectedIndex, + currentGridId, + isSavingGist, + }), [gridState, selectedIndex, currentGridId, isSavingGist]); + + // Memoize actions context value + const actionsValue = useMemo(() => ({ + setGridState, + selectRow, + updateCellState, + addNewColumn, + inititializeGrid, + deleteColumnByIndex, + setGroupBy, + setFilterBy, + setCurrentGridId, + getAllGrids, + deleteGrid, + saveGridAsGist, + deleteRow, + }), [ + setGridState, + selectRow, + updateCellState, + addNewColumn, + inititializeGrid, + deleteColumnByIndex, + setGroupBy, + setFilterBy, + setCurrentGridId, + getAllGrids, + deleteGrid, + saveGridAsGist, + deleteRow, + ]); return ( - - {children} - + + + {children} + + ); -}; +}); diff --git a/app/components/GridTable.tsx b/app/components/GridTable.tsx index f42691a..3496a30 100644 --- a/app/components/GridTable.tsx +++ b/app/components/GridTable.tsx @@ -12,9 +12,10 @@ import ColumnTitle from './ColumnTitle'; import { pluralize } from '../utils/pluralize'; import { capitalize } from '../utils/capitalize'; import type { GridState, PrimaryDataType } from '../actions'; -import Row from './Row'; +import React from 'react'; +import VirtualizedTable from './VirtualizedTable'; -function Panel({ children, sx = {} }: { children: React.ReactNode; sx?: any }) { +const Panel = React.memo(function Panel({ children, sx = {} }: { children: React.ReactNode; sx?: any }) { return ( ); -} +}); -function GroupHeader({ groupName, count }: { groupName: string; count: number }) { +const GroupHeader = React.memo(function GroupHeader({ groupName, count }: { groupName: string; count: number }) { return ( {count} ); -} +}); function useColumnDialog() { const [showNewColumnForm, setShowNewColumnForm] = useState(); @@ -75,9 +76,15 @@ function useGroupedRows(gridState: GridState | null) { } const { columns, primaryColumn, groupBy } = gridState; + + // Filter out deleted rows early + const visiblePrimaryRows = primaryColumn + .map((cell, index) => ({ cell, index })) + .filter(({ cell }) => !cell.deleted); + const defaultGroup = { groupName: '', - rows: primaryColumn.map((cell, index) => ({ cell, index })), + rows: visiblePrimaryRows, }; if (!groupBy) { @@ -91,7 +98,7 @@ function useGroupedRows(gridState: GridState | null) { const groups: { [key: string]: { cell: GridCell; index: number }[] } = {}; - primaryColumn.forEach((cell, index) => { + visiblePrimaryRows.forEach(({ cell, index }) => { const groupCell = groupColumn.cells[index]; let groupValues: string[] = []; let optionRes; @@ -126,7 +133,7 @@ function useGroupedRows(gridState: GridState | null) { }, [gridState]); } -function TableHeaderRow({ +const TableHeaderRow = React.memo(function TableHeaderRow({ columns, primaryColumnType, }: { @@ -149,43 +156,60 @@ function TableHeaderRow({ > {columns.map((column: GridCol, index: number) => ( - + ))} ); -} +}); -function TableContent() { +const TableContent = React.memo(function TableContent() { const { gridState, selectRow, selectedIndex } = useGridContext(); + const groupedRows = useGroupedRows(gridState); + if (!gridState) return null; - const { columns, primaryColumn } = gridState; + const { columns } = gridState; - // Filter out deleted rows - const visibleRows = primaryColumn - .map((cell, index) => ({ cell, index })) - .filter(({ cell }) => !cell.deleted); + // If we have groups, render them separately + if (groupedRows.length > 1 || (groupedRows.length === 1 && groupedRows[0].groupName)) { + return ( + + {groupedRows.map((group, groupIndex) => ( + + {group.groupName && ( + + )} + + + ))} + + ); + } + // For simple case without grouping, use virtualization directly + const allRows = groupedRows[0]?.rows || []; + return ( - - {visibleRows.map(({ cell, index }) => ( - - ))} - + ); -} +}); export default function GridTable() { const { showNewColumnForm, setShowNewColumnForm, onDialogClose } = useColumnDialog(); const { gridState, addNewColumn, selectedIndex } = useGridContext(); - const groupedRows = useGroupedRows(gridState); + if (!gridState) { return null; } @@ -205,34 +229,30 @@ export default function GridTable() { !cell.deleted).length} /> - + - {groupedRows.map((group, groupIndex) => ( - - {group.groupName && ( - - )} - - ))} - + + + diff --git a/app/components/NewColumnForm.tsx b/app/components/NewColumnForm.tsx index 697b1e6..26cb28b 100644 --- a/app/components/NewColumnForm.tsx +++ b/app/components/NewColumnForm.tsx @@ -1,135 +1,135 @@ -import React, { useState } from 'react'; -import { Box, Button, TextInput, Textarea, FormControl, Select, Checkbox } from '@primer/react'; -import { columnTypes } from '../columns'; -import type { Option, ColumnType } from '../actions'; - -type Props = { - addNewColumn: ({ - title, - instructions, - type, - options, - multiple, - }: { +import React, { useState, useCallback } from 'react'; +import { + Box, + Button, + FormControl, + Select, + TextInput, + Textarea, + Checkbox, + Text, +} from '@primer/react'; +import type { ColumnType, Option } from '../actions'; + +interface NewColumnFormProps { + addNewColumn: (data: { title: string; instructions: string; type: ColumnType; options: Option[]; - multiple: boolean; + multiple?: boolean; }) => void; - errorMessage?: string; -}; - -export default function NewColumnForm({ addNewColumn, errorMessage }: Props) { - const [title, setTitle] = useState(''); - const [instructions, setInstructions] = useState(''); - const [type, setType] = useState('text'); - const [options, setOptions] = useState([]); - const [multiple, setMultiple] = useState(false); - const [message, setMessage] = useState(errorMessage || ''); - - const selectedColumnType = columnTypes[type]; - - function handleTypeChange(e: React.ChangeEvent) { - const newType = e.target.value as ColumnType; - setType((currentType) => { - if ( - currentType === 'text' && - (newType === 'select' || newType === 'select-user' || newType === 'file') - ) { - setOptions([{ title: '', description: '' }]); - } else if ( - (currentType === 'select' || currentType === 'select-user' || currentType === 'file') && - newType === 'text' - ) { - setOptions([]); - } - return newType; - }); - } +} - function addNewHandler(e: React.FormEvent) { - e.preventDefault(); - setMessage(''); +const NewColumnForm = React.memo(function NewColumnForm({ addNewColumn }: NewColumnFormProps) { + const [title, setTitle] = useState(''); + const [instructions, setInstructions] = useState(''); + const [type, setType] = useState('boolean'); + const [options, setOptions] = useState(''); + const [multiple, setMultiple] = useState(false); - if (title === '') { - setMessage('Enter a title'); - return; - } + const handleSubmit = useCallback((e: React.FormEvent) => { + e.preventDefault(); + + const optionsList: Option[] = options + .split('\n') + .filter((line) => line.trim()) + .map((line) => ({ title: line.trim(), description: '' })); - const filteredOptions = options.filter((option) => option.title !== ''); addNewColumn({ title, instructions, type, - options: filteredOptions, + options: optionsList, multiple, }); + + // Reset form setTitle(''); setInstructions(''); - setType('text'); - setOptions([]); + setType('boolean'); + setOptions(''); setMultiple(false); - } + }, [addNewColumn, title, instructions, type, options, multiple]); + + const handleTitleChange = useCallback((e: React.ChangeEvent) => { + setTitle(e.target.value); + }, []); + + const handleInstructionsChange = useCallback((e: React.ChangeEvent) => { + setInstructions(e.target.value); + }, []); + + const handleTypeChange = useCallback((e: React.ChangeEvent) => { + setType(e.target.value as ColumnType); + }, []); + + const handleOptionsChange = useCallback((e: React.ChangeEvent) => { + setOptions(e.target.value); + }, []); + + const handleMultipleChange = useCallback((e: React.ChangeEvent) => { + setMultiple(e.target.checked); + }, []); + + const showOptions = type === 'select' || type === 'select-user'; return ( - - {message && {message}} - - - Title - setTitle(e.target.value)} block /> + + + Column Title + - - Type + + Instructions +