From 7d8e4c099f93cae57fedf1e35ffa2422f93a7e66 Mon Sep 17 00:00:00 2001 From: Justin Blumencranz <96924014+j15z@users.noreply.github.com> Date: Mon, 24 Aug 2026 13:35:37 -0700 Subject: [PATCH 1/2] improvement(tables): inline column creation and header-menu editing --- .../column-config-sidebar.tsx | 305 ++++++----------- .../column-config-sidebar/column-types.ts | 8 +- .../new-column-dropdown.tsx | 33 +- .../table-grid/headers/column-header-menu.tsx | 43 ++- .../headers/draft-column-header.tsx | 67 ++++ .../components/table-grid/headers/index.ts | 1 + .../headers/workflow-group-meta-cell.tsx | 85 ++++- .../components/table-grid/table-grid.tsx | 320 +++++++++++++++++- .../table-grid/table-primitives.tsx | 7 + .../components/table-grid/utils.test.ts | 21 ++ .../[tableId]/components/table-grid/utils.ts | 26 +- .../[workspaceId]/tables/[tableId]/table.tsx | 50 ++- apps/sim/hooks/queries/tables.ts | 13 + apps/sim/hooks/queries/utils/table-keys.ts | 2 + .../__tests__/column-type-registry.test.ts | 63 ++++ apps/sim/lib/table/column-types/currency.ts | 1 + apps/sim/lib/table/column-types/select.ts | 2 + apps/sim/lib/table/column-types/types.ts | 15 + apps/sim/lib/table/dates.ts | 23 +- 19 files changed, 849 insertions(+), 236 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/draft-column-header.tsx diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/column-config-sidebar/column-config-sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/column-config-sidebar/column-config-sidebar.tsx index ffcef8ce465..b715a8a5ba6 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/column-config-sidebar/column-config-sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/column-config-sidebar/column-config-sidebar.tsx @@ -1,28 +1,20 @@ 'use client' import { useState } from 'react' -import { Button, ChipCombobox, ChipInput, cn, FieldDivider, Label, Switch, toast } from '@sim/emcn' +import { Button, ChipCombobox, cn, FieldDivider, Label, Switch, toast } from '@sim/emcn' import { X } from '@sim/emcn/icons' import { toError } from '@sim/utils/errors' -import { findValidationIssue, isValidationError } from '@/lib/api/client/errors' +import { useIsMutating } from '@tanstack/react-query' +import { isValidationError } from '@/lib/api/client/errors' import type { ColumnDefinition, SelectOption } from '@/lib/table' -import { - DEFAULT_CURRENCY_CODE, - getCurrencyOptions, - resolveCurrencyCode, -} from '@/lib/table/currency' +import { getCurrencyOptions, resolveCurrencyCode } from '@/lib/table/currency' import { FieldError, RequiredLabel, } from '@/app/workspace/[workspaceId]/tables/[tableId]/components/sidebar-fields' -import { useAddTableColumn, useUpdateColumn } from '@/hooks/queries/tables' +import { useUpdateColumn } from '@/hooks/queries/tables' +import { tableKeys } from '@/hooks/queries/utils/table-keys' import { SelectOptionsEditor } from '../select-field' -import { PLAIN_COLUMN_TYPE_OPTIONS } from './column-types' - -/** Whether a column type carries an option set. */ -function isSelectType(type: ColumnDefinition['type']): boolean { - return type === 'select' -} /** * Picker entries, built once at module load: the option list is derived from the @@ -38,41 +30,55 @@ function optionsEqual(a: SelectOption[], b: SelectOption[]): boolean { } /** - * Discriminates the two flows the column-config sidebar handles. Workflow - * configuration is a separate component (``) so this surface - * never has to branch on `isWorkflow`. + * Discriminates the flows the column-config sidebar handles. Name and type + * never appear here — renaming is inline in the header and type changes go + * through the header menu's "Change type" submenu. Workflow configuration is a + * separate component (``). */ export type ColumnConfig = - | { mode: 'create'; proposedName: string; type: ColumnDefinition['type'] } - | { mode: 'edit'; columnName: string } + /** + * A select column being created: its name has been committed in the grid + * header, but it exists only as a draft there until Save hands the option + * set to the creator, which persists name and options in one create. + * Dismissing discards the draft. + */ + | { mode: 'draft-select' } + /** + * Convert an existing column to select: Save applies the type change + * together with the configured options; dismissing leaves the column's + * current type untouched. + */ + | { mode: 'convert-select'; columnName: string } + /** Edit the configuration of an existing select or currency column. */ + | { mode: 'configure'; columnName: string } interface ColumnConfigSidebarProps { /** When non-null the sidebar is open. */ config: ColumnConfig | null onClose: () => void - /** Existing column record for `mode: 'edit'`; ignored otherwise. */ + /** + * `draft-select` Save: hands the validated option set to the draft's owner, + * which persists the column. Resolves `true` once created (the parent then + * closes the sidebar) or `false` to stay open — the draft's name was refused + * or the create failed, and the owner has already surfaced why. + */ + onDraftSave: (options: SelectOption[], multiple: boolean) => Promise + /** Existing column record for modes that carry a `columnName`; otherwise null. */ existingColumn: ColumnDefinition | null workspaceId: string tableId: string - /** Notify parent of a rename so it can rewrite local `columnOrder` / - * `columnWidths` keys that reference the old name. */ - onColumnRename?: (oldName: string, newName: string) => void } /** - * Right-edge sidebar for plain (non-workflow) column configuration. Handles - * create (with type pre-chosen by the parent's "+ New column" dropdown) and - * edit. No `isWorkflow` branches — workflow-output columns route through - * `` instead. + * Right-edge sidebar for per-type column configuration: a select's option set + * and a currency's display code. Everything else about a column — name, type, + * unique — is edited in the grid header and its menu. * - * Form state seeds from props via lazy `useState` initializers; the parent - * uses `key={config?.columnName ?? 'closed'}` to remount when switching - * columns, eliminating the prop-mirroring `useEffect` the previous combined - * sidebar relied on. + * Form state seeds from props via lazy `useState` initializers; the body is + * keyed on the config identity so opening a different column or mode remounts + * and re-seeds. */ export function ColumnConfigSidebar(props: ColumnConfigSidebarProps) { - // Mount the form body with `key` keyed on the config identity so opening a - // different column / mode remounts and re-seeds state from props. const open = props.config !== null return (