Commit 8329dac
feat(tables): add select & multi-select column types (#5873)
* feat(tables): add select/multiselect column types (backend)
Adds two enum-style column types where the column declares a fixed set of
options (stable id + name + palette color) and every cell is constrained to
them.
- COLUMN_TYPES gains `select` / `multiselect`; SELECT_COLORS is a fixed,
theme-aware palette mapping 1:1 to Badge color variants (no raw hex)
- ColumnDefinition.options carries the option set. Cells store option *ids*
(a string for select, string[] for multiselect) so renaming or recoloring
an option never rewrites row data
- Row validation enforces membership; coercion tolerantly maps an option
*name* to its id for tool/import writes and drops unmatched entries
- validateColumnDefinition enforces non-empty, unique-id, unique-name and
valid-color option sets, and rejects options on non-select columns
- Contract gains selectOptionSchema plus a cross-field refine requiring
options exactly on select types; routes thread options through type
changes and a new options-only updateColumnOptions path
No migration: column config already lives in the user_table_definitions
schema JSONB blob.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014e4MvFV1szLhNLap1CCNUd
* feat(tables): select/multiselect column UI
Surfaces the new enum column types in the tables grid.
- New `select-field/` module: SelectPill (colored option via the shared Badge
palette), SelectValueEditor (one ChipDropdown-backed picker reused by every
edit surface), SelectOptionsEditor (add/rename/recolor/remove)
- Option colors are picked from inline squircle swatches — no labels, no
nested dropdown. Each swatch's fill is a Badge in that variant, so the
palette stays single-sourced and theme-aware
- Cells render option pills; an empty select cell shows a muted "None" so it
reads as a dropdown. The single-select menu always offers "None" to clear
- Wired into all three edit surfaces (inline cell, expanded popover, row
modal) plus the type picker and column-type icons
- ChipDropdown gains `defaultOpen`/`onOpenChange` so the inline cell editor
can open on mount and commit when the menu closes; open state is now
controlled in both single and multi modes
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014e4MvFV1szLhNLap1CCNUd
* fix(tables): auto-fit select columns on option labels, not ids
Column auto-resize measured `String(val)` for every non-json/date column,
which for a select cell is the opaque option id (and for multiselect the
comma-joined id array). Selecting auto-fit on a select column therefore
sized it to ids the user never sees. Measure the resolved option names
instead, matching what the pills actually render.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014e4MvFV1szLhNLap1CCNUd
* feat(tables): default select options to grey, drop color picker for now
Removes the per-option swatch picker and defaults every option to the
neutral gray pill. The `color` field stays in the data model and the
SELECT_COLORS palette/contract are untouched, so a picker can be re-added
later as a pure UI change with no migration.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014e4MvFV1szLhNLap1CCNUd
* fix(tables): guard select type conversion, escape, and required clear
Addresses review findings on the select/multiselect column types:
- Column type conversion now checks each existing value against the target
option set (resolve by id or name); a `select`/`multiselect` change is
blocked when values don't fit, instead of accepting shapes that later
coercion would strand or silently drop
- Inline select editor discards its draft on Escape (matching the text/date
editors) rather than committing on menu close
- A required single-select no longer offers "None" — clearing to null could
never be committed
Exports resolveSelectOptionId from validation for the conversion gate.
* fix(tables): block emptying required multiselect, skip no-op cell writes
Round 2 review fixes:
- A required multiselect can no longer be emptied — the toggle that would
remove the last option is ignored, since an empty selection can't be
committed (server rejects it). Mirrors the required single-select "None"
guard.
- Inline cell save now compares old vs new value structurally, so a no-op
edit (e.g. opening a multiselect and closing it unchanged, producing a new
array reference) no longer writes a row update or pushes an undo entry.
Uses JSON compare for arrays/objects, matching the existing optionsEqual
convention; primitives keep the === fast path.
* fix(tables): expanded multiselect string value + same-type options drop
Round 3 review fixes:
- Expanded-cell popover normalizes a multiselect's initial value with
toSelectedIds, so a single option-id string (the normal shape right after a
select->multiselect conversion, before the row is rewritten) is no longer
collapsed to an empty array and cleared on save.
- Column PATCH now only routes to updateColumnType on a real type change.
updateColumnType early-returns on an unchanged type, so a payload repeating
the current type together with new options previously applied neither; an
unchanged type with options now routes to the options-only update. Fixed in
both the internal and v1 column routes.
* improvement(tables): inline select edit + idiomatic options editor
- Double-clicking a select/multiselect cell now opens the inline option
dropdown (like date/number) instead of the fixed-height text popover, which
rendered just a small dropdown floating in a large empty container. Removes
the now-unreachable ExpandedSelectEditor from the expanded-cell popover.
- Options editor's add/remove controls match the sibling Filter UI: a ghost,
muted "Add option" button with a small plus, and an X remove icon.
* improvement(tables): inline select cell uses bare DropdownMenu
The inline select editor used a ChipDropdown pill, which reads as a foreign
form control inside a grid cell. It now renders the canonical DropdownMenu
anchored to the cell — an invisible full-cell trigger (no pill, no label
text), options as their colored pills with a check on the selected ones, and
"None" as a proper menu item. The cell keeps showing its pills while the menu
is open instead of going blank.
SelectValueEditor (the ChipDropdown pill) is now used only by the row modal,
where a pill form control is idiomatic. This lets the defaultOpen/onOpenChange
additions to the shared ChipDropdown be reverted — it's back to its original
behavior with no consumers depending on the change.
* improvement(tables): unify select into one type with a multiple flag
- Collapse `select` + `multiselect` into a single `select` column type with a
`multiple` boolean. A cell holds one option id when single, an array when
multiple. Validation/coercion, the contract, routes, and all cell/editor
code now branch on `column.multiple` instead of a separate type.
- The column-config sidebar exposes an "Allow multiple" toggle (create and
edit) and no longer offers "Unique" on select columns.
- Give the closed select cell a dropdown affordance: it renders emcn's
`comboboxVariants` chrome (border + chevron, compacted to the row height),
with the pills inside and the chevron flipping while the menu is open.
- Single↔multiple toggles reconcile lazily on the next row write (single
tolerates an array by taking its first resolved option).
* improvement(tables): revert select cell to chip-only view
Drop the combobox chrome (border + chevron) from the select cell — the plain
option pills read better. Reverts the comboboxVariants barrel export too, since
nothing else uses it.
* fix(tables): block multiple→single select switch when cells have >1 option
Switching a select column from multiple to single would silently keep only the
first option of any multi-valued cell. updateColumnOptions now scans the rows on
that transition and errors ("N row(s) have multiple options selected") instead
of dropping data, mirroring the type-change compatibility guard.
* improvement(tables): rename select "Allow multiple" toggle to "Multiselect"
* fix(tables): drop removed select options from cells instead of stale pills
When an option is deleted, cells that referenced it previously rendered a gray
fallback pill labeled with the raw internal id. They now drop the orphaned id
and fall back to empty ("None"). Editors seed their selection from the still-
valid ids too, so editing/saving such a cell writes the cleaned value.
* improvement(tables): use dashed add-row button for select options
Match the sub-block list editors (filter/sort builders): a full-width,
dashed-border ghost "Add option" button instead of a plain text button.
* perf(tables): don't refetch rows on metadata-only column saves; fix copy log
- Add/update column are metadata-only server-side (row data never changes), so
they now invalidate just the schema, not the rows. Saving a column (e.g.
editing select options) no longer triggers a full rows refetch/flash; cells
re-render from the refetched schema. Delete-column and workflow-group ops keep
the rows invalidation since they do change row data.
- "Failed to copy rows" logged an empty {} for DOMExceptions; log the extracted
message so the real reason (e.g. lost transient activation) is visible.
* fix(tables): migrate legacy multiselect columns; readable contract error
- Tables created before the select/multiselect merge stored `type:
'multiselect'`, which the removed type no longer accepts — every response
carrying such a column failed contract validation and column edits threw.
getTableById now maps `multiselect` → `select` + `multiple: true` on read
(covers reads, column ops via withLockedTable, and their responses); the
migrated shape persists on the table's next schema write.
- requestJson's "Response failed contract validation" now appends a short
"field: reason" summary of the Zod issues instead of a bare message, so the
failing field is visible.
* revert(tables): drop legacy multiselect read-migration
Only local dev tables ever held the removed `multiselect` type (the feature
never shipped), so the read-time backward-compat mapping isn't warranted.
Keeps the readable contract-validation error from the same change.
* improvement(tables): add select options by typing into a trailing row
Replace the "Add option" button with a trailing empty row: the first keystroke
materializes the option and focus jumps into it (cursor at end) so typing flows
straight through. Enter in an option jumps back to the trailing row to add the
next one. Removing a row is unchanged.
* fix(tables): export select columns as option names, not ids
CSV/JSON export serialized the raw stored value for select cells — an option
id (or an array of ids for multi) — so downloads showed opaque `opt_…` ids.
Export now resolves ids to option names: CSV comma-joins multi names; JSON
resolves values before the id→name key translation. Ids with no matching
option (deleted) are dropped.
* fix(tables): resolve select option ids to names across all read/consume surfaces
Stored select cells hold opaque option ids; only the cell renderer and write
coercion handled id<->name. Every other read/serialize/search boundary leaked
the id. Centralize translation in lib/table/select-values and apply it at each
boundary:
- reads return names: sync export (CSV+JSON), function/snapshot mounts,
clipboard copy/cut, workflow-tool + mothership reads, v1 API rows
- filter/search: tolerant name->id on filter operands (row-wire INTERNAL_JWT,
mothership, v1), select-aware SQL predicates (operator whitelist, multiselect
empty), grid Cmd-F matches resolved names, UI filter option picker sends ids
- sort: select columns order alphabetically by option name (id->name CASE)
- multiselect writes split a comma-delimited string; mothership add/update_column
forward options/multiple
* refactor(tables): remove vestigial per-option color from select columns
Colors were never exposed (no picker; every option hardcoded to gray), only
kept in the model for a future picker. Drop the field entirely: SelectOption is
now { id, name }, pills render a fixed neutral badge, and validation/contract no
longer carry a color. Existing stored options keep a harmless color key that is
stripped on the next read/write.
* feat(tables): generate select option ids in the copilot handler from agent-supplied names
The mothership agent authors select options by name only; the copilot tool
handler now mints the stable option id (preserving any id it's re-sent, so
existing cell data survives an options edit) before calling the table service —
the model never authors the cell key. Applied to create (per select column in
the schema), add_column, and update_column.
* refactor(tables): collapse row read boundaries onto one outbound seam
Turning a stored row into its outward form needs two translations — keys
(column id to name) and values (select option id to name) — and every boundary
composed them by hand. That shape leaked twice: table-change triggers and
workflow-column/enrichment inputs both built name-keyed rows without resolving
select values, so workflows saw opt_a1b2 instead of "Open". Neither had any
test coverage; neither is live (select is unreleased on staging).
lib/table/cell-format.ts now fuses both into a single pass:
- namedRowMapper(columns) — keys + values together, so they cannot drift
- fillMissingColumns(named, columns) — widens to match a headers array
- mapInputValues(data, columns, mappings) — for id-keyed input mappings
Migrated the 12 hand-composed pairings and the inlined copy in export-runner,
then deleted rowDataIdToName, resolveRowSelectValues and selectColumnsOf so the
keys-without-values shape is unrepresentable. Also formats dates in the
read-only expanded popover, which rendered raw storage to viewer-role users.
CSV is untouched (export-format.ts has no diff), so export bytes are unchanged.
* fix(tables): stream the persisted cell value, not the raw workflow output
A workflow column writing into a select column persists correctly — updateRow
coerces the option name to its stored id — but it coerces its own merged copy,
so the live SSE snapshot still carried the raw value. The grid resolved that
name as an option id, found nothing, and rendered the cell EMPTY from the
moment the workflow completed until the next refetch: the write looked like it
had failed at exactly the moment the user was watching it land.
Coerce a copy of the event outputs through the same inbound switch the persist
path uses. A copy because the patch object is identity-compared for the
progress writer's retry bookkeeping. This aligns every type, not just select —
a date now streams canonically, and a value the DB would reject streams as null
instead of briefly showing a value that was never stored.
* fix(tables): keep option ids stable when the agent edits a select column
The agent authors select options as bare names, so normalizeSelectOptionsInput
minted a fresh id for every one. On update_column that replaced the whole
option list with new ids — orphaning every cell that referenced the old ones
and silently clearing the column's data on what looks like an additive edit
(adding "Medium" to ["Low", "High"] wiped both existing values).
Match incoming names against the column's current options (case-insensitively)
and reuse those ids; only genuinely new options get a fresh one. Found while
writing the catalog description that promised this behavior.
Also regenerates the tool catalog now that the copilot registry advertises
select columns, options and multiple.
* fix(tables): filter multi-select columns by membership, not equality
A multi-select cell stores an array of option ids, and Postgres containment
does not match a scalar against an array — `{"t":["a"]} @> {"t":"a"}` is
false. So every multi-select filter compiled to a predicate that could never be
true and silently returned nothing.
Split the select operator whitelist by cardinality: single-select keeps
eq/ne/in/nin, multi-select takes contains/ncontains, both keep isEmpty. The
membership clause wraps the operand (`data @> '{"t":["a"]}'`), which still
uses the same GIN index. The equality shorthand (`{ tags: 'opt_a' }`) compiles
to membership too — it bypasses the operator whitelist, so it had to be handled
or it would keep failing silently.
Also resolves $contains/$ncontains operands name → id, so an agent or API
caller filtering by option name reaches the right rows, and narrows the filter
UI's operator list per cardinality.
* chore(tables): drop a stale doc comment and trim two restating ones
The expanded-cell-popover TSDoc claimed workflow and boolean cells are
read-only there, but the editability rule has no workflow check and the inline
comment beside it says the opposite — a comment that contradicts the code is
worse than none. The accurate rule already sits next to what enforces it.
* fix(tables): close review findings on select column conversion and editing
Bugbot round on the select work — eight findings, all real:
- Converting away from select left opaque option ids in every cell. Migrate
ids to option names in the same transaction (multi joins comma-separated),
and run the compatibility check against the name, not the id.
- Multiselect to text silently dropped arrays; text targets now reject
structured values instead of nulling them on the next write.
- Converting to single-select accepted multi-valued cells, which the next
coerce would quietly truncate to the first id. Same guard updateColumnOptions
already had.
- Empty strings had become compatible with every target type, so a text column
with blank cells could convert to number. Narrow that back to select.
- Copilot update_column rejected a multiple-only payload with 'options is
required', contradicting the catalog. Fall back to the column's options.
- Empty multiselect rendered ChipDropdown's 'All' label, reading as if every
option were selected.
- Opening and dismissing an empty multiselect wrote a row update, since null
and [] compared unequal.
- Converting a unique column to select stranded the constraint with no way to
clear it.
Adds the first tests for the conversion rules.
* fix(tables): migrate select cells in both directions and refetch rows after
Follow-up round on the conversion fix — the migration was one-directional.
- Converting TO select left cells holding option names, which every reader
resolves by id, so populated cells rendered as None and matched no filter.
- Toggling single→multi left scalar ids while multi filters compile to array
containment, which a scalar never matches — pre-toggle rows silently dropped
out of their own column's filters.
- useUpdateColumn settled with the schema-only invalidation, whose premise
('stored row data never changes') these migrations broke, so the grid kept
serving pre-migration cells.
Both directions now share set-based helpers keyed off a jsonb map, and the
to-select map keys ids as well as names so re-running is a no-op. The client
refetches rows when the payload carries a type or multiple change.
* fix(tables): align select cell migration with the compatibility check
Two spots where the check and the migration disagreed, each leaving a cell the
check had already accounted for:
- resolveSelectOptionId matches option names case-insensitively, so a cell like
'open' against option 'Open' passed the convert-to-select check, but the
migration map was case-sensitive and left it as the raw name. The map now
keys the folded name too (duplicate names are already rejected
case-insensitively, so it is unambiguous) and lookups try the exact form
first.
- Converting away from select, the check treats an orphaned option id as null
while the migration passed it through, so a column with deleted-option cells
could land an opaque opt_ id in a number/date/boolean cell. Orphans now null,
matching selectValueForConversion; a multi cell drops them and nulls when
nothing survives.
This reverses the pass-through choice from the earlier round — consistency with
the check is what matters, and an orphan is a value the UI never rendered.
* fix(tables): treat an emptied multiselect as empty everywhere
`[]` is the canonical empty multiselect but it is not falsy, so every
emptiness check that only looked for null/undefined/'' read a cleared selection
as filled:
- dependent workflow groups became eligible off an empty cell, and enrichments
with a required input ran on rows they should have skipped
- marking a column required did not count `[]` rows, even though write-path
validation rejects an empty required multiselect
One `isEmptyCellValue` predicate now backs the dep, output-filled and
enrichment checks, and the required-constraint guard counts '[]' too.
Also normalizes '' on conversion to select — compatibility admits it, so it has
to land as null (single) or [] (multi) rather than a bare string the column's
own validation would then reject.
* fix(tables): route an unchanged type with options to the options update
updateColumnType early-returns when the type is unchanged, so an agent that
restated newType: 'select' alongside a new option set had those options
silently dropped while the tool still reported success. The HTTP columns route
already guards this; the copilot path now mirrors it — only a genuine type
change goes to updateColumnType, anything else with options or multiple goes to
updateColumnOptions.
A payload that only restates the current type is a no-op and now reports the
live schema rather than an undefined one.
* fix(tables): restore select options on column-delete undo; map v1 option errors to 400
- The delete-column undo snapshot never captured options or multiple, so
re-creating a select column was rejected outright (it is invalid with no
option set) and the saved cell data — which is option ids — had nothing to
attach to. Undo of a select column deletion simply could not succeed.
- The v1 add-column route mapped only 'already exists' and 'maximum column' to
400, while the internal route also maps invalid-column and option errors. A
bad select option set surfaced as a 500 on the public API.
* fix(tables): clear unique in the service when converting a column to select
The clearing lived only in the column-config sidebar, so a conversion through
the v1 API or the copilot tool left unique: true stranded on a select column —
where the constraint compares the stored option id, capping each option at one
row for the whole table, and the sidebar hides the toggle so it could never be
cleared again.
Moving it into updateColumnType gives all three callers the same behavior; the
sidebar's payload is now belt-and-braces rather than the only enforcement.
* fix(tables): clear removed select options, reject unique selects, resolve pasted names
- updateColumnOptions now drops ids for options that no longer exist, so a
removed option can't leave orphans that block edits on required columns
- validateColumnDefinition and updateColumnConstraints reject unique on select
- cleanCellValue resolves pasted option names to ids client-side so the
optimistic cache holds ids and the pill renders immediately
* fix(tables): refetch rows when a select option is removed
Removing an option rewrites cells server-side, so the schema-only
invalidation left the cache holding orphaned ids — hidden by the grid but
still visible to emptiness checks, filters, and dependent-group eligibility.
* fix(tables): let a multiselect round-trip through text
Converting multiselect to text flattens cells to `Alpha, Beta`, but
converting back read that as one unknown option and rejected the change.
Both the compatibility check and the cell migration now split a comma
string the way the write path already did, through one shared helper.
* fix(tables): scope the empty-selection guard to multiselect; order the option ref map
- cellValuesEqual treated null and [] as equal for every type, so clearing a
stored [] on a json cell (or writing one into a null cell) never saved
- migrateCellsToSelectIds built one flat id/name map, letting an option whose
name equals another option's id overwrite that id and repoint its cells
* fix(tables): prune stale select filters; reject unique+select before converting
An applied filter outlives the schema it was built against, so converting a
column to select (or toggling multiple) could strand an operator the server
rejects, failing every subsequent rows query until the filter was cleared by
hand. Prune those conditions in useTable, above every consumer of the rows
query key, and share one operator whitelist with the filter picker.
A payload setting both newType: "select" and unique: true ran two separate
locked transactions, committing the conversion and then failing the
constraint. Both routes now reject that pair before either runs.
* fix(tables): scope bulk ops to the pruned filter; guard unique+select in copilot
useTable pruned the filter for the rows query but kept it private, so
select-all run/stop/delete still sent the raw one — targeting a predicate the
grid wasn't displaying and that the server rejects. Return it and use it for
every server-bound scope; the filter button and editor keep showing what the
user configured so the stale rule can still be repaired.
The copilot update_column path ran the same two-transaction sequence the HTTP
routes now reject, so a convert-to-select plus unique payload committed the
conversion and then failed. Same guard applied.
Also adds the filter to selectedRunScope's deps — it was read but not listed,
so a filter change while select-all was active carried a stale scope.
* fix(tables): reject the multiple flag on non-select columns
A create/add payload could persist multiple: true on a string column, where
it sits inert until updateColumnType inherits it via `data.multiple ??
column.multiple` — silently turning an intended single-select into a
multiselect and rewriting every cell as an array. Rejected now in both the
service validator and the shared contract refine, which the internal and v1
column routes both consume.
* fix(tables): block required blank conversions; sort multiselect by option name
`required` only rejects null/undefined on a write, so a required string column
legitimately holds ''. Converting it to a required select stored null (or []
for a multi), and every later update of that row then failed its own required
check. Reject a blank source value when the target select is required.
Multiselect sort compared the raw id-array text, since `["opt_b","opt_a"]`
matches no single-id CASE branch. It now resolves elements to names and joins
them in stored order — the same text the grid renders and an export writes.
* fix(tables): validate a conversion against the required flag the request sets
A single PATCH converting an optional column to select while also setting
required: true checked blanks against the column's CURRENT required flag, so
the conversion committed and the constraint write then failed — an error
response with the type change already persisted.
updateColumnType now takes the requested `required` and validates against the
constraint the column ends up with. Empty rows are also counted when the
target is required (they were skipped outright), so the null case fails
up-front too rather than at the constraint write.
* fix(tables): one emptiness predicate, guard required option removal, order aggregates
The conversion pre-flight and the constraint write each had their own idea of
"empty", and rows missing the column key are filtered out of the conversion's
row set entirely — so a combined type + required PATCH passed the pre-flight,
committed the conversion, then failed the constraint. Both now call one
countEmptyCells helper, which is the actual fix: they can no longer disagree.
Removing the last option a required cell holds nulled it, producing exactly the
state updateColumnConstraints refuses to create. Blocked with a count of the
rows that would be stranded.
The multiselect rewrite aggregates carried no ORDER BY while sort and export
preserve stored order via WITH ORDINALITY; they now do too.
* fix(tables): restore in/nin on select filters and assert the whitelists agree
The client operator set was a hand transcription of the server's and dropped
$in/$nin, so the picker never offered them and — worse — pruneFilterForColumns
silently discarded an existing $in filter the server would have accepted.
Both server sets are now exported and a test maps the UI operators onto them
through UI_TO_WIRE_OPERATOR, so the two can't drift again by hand.
* fix(tables): stop Find crashing on a null multiselect cell
jsonb_array_elements_text throws "cannot extract elements from a scalar" on a
JSON null, which is exactly what a multiselect cell holds once it is cleared,
cut, or has its last option removed — so search failed for the whole table.
Gate the array arm on jsonb_typeof, falling back to the single mapping so a
scalar left over from a single->multi toggle stays searchable.
Swept the other array expansions: the rest are inside UPDATEs whose WHERE
already filters to arrays, or are CASE-guarded. This was the only unguarded one.
* fix(tables): clear removed options before the cardinality guard and migration
The multi->single guard counted options the same request was dropping, so
trimming a cell to one option and turning multiselect off in one save was
rejected. Reordering fixes that and a latent corruption: the migration keeps a
multi cell's FIRST element, which could be a removed id sitting ahead of a kept
one, so the surviving option would be discarded and the dead one kept.
Removal now runs first, against the pre-toggle cell shape, then the guard
counts what actually remains, then the shape migration runs.
Find also joined multiselect names unordered while sort and export preserve
stored order, so a search for the displayed label could miss.
* fix(tables): validate option removal against the pending required flag; show the applied filter
The stranded-options guard read the column's current `required`, so a removal
paired with `required: true` cleared cells and then failed the constraint write
— options change committed behind an error — while a removal paired with
`required: false` was blocked despite the column no longer being required. It
now takes the requested value, and also pre-flights already-empty rows when
required is newly imposed.
The filter chip and editor keyed off the raw filter while rows, runs, stops and
deletes used the pruned one, so the UI could claim an active filter the grid
wasn't reflecting. Both now read the applied filter.
* fix(tables): gate the unique guard on the resulting type, not on a type change
Pairing unique: true with an options or multiple update on a column that is
ALREADY select skipped the guard, so updateColumnOptions committed and the
separate constraint write then failed — the options change landing behind a
400. Gating on `updates.type ?? currentColumn.type` covers the conversion and
the options-only case with one condition, on both column routes and the
copilot update_column path.
* fix(tables): stop coercing select option ids in filter values
Option ids are caller-supplied strings, so parseScalar turned an id of "1" or
"true" into a number or boolean; JSONB containment then compared the wrong type
and matched nothing while the picker still showed a valid option.
filterRulesToFilter takes the columns and keeps a select value as text, for $in
lists too. pruneFilterForColumns forwards them as well — its round-trip through
rules would otherwise re-coerce the ids it just preserved.
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>1 parent 892750b commit 8329dac
69 files changed
Lines changed: 4149 additions & 333 deletions
File tree
- apps/sim
- app
- api
- table
- [tableId]
- columns
- export
- v1/tables/[tableId]
- columns
- rows
- [rowId]
- upsert
- workspace/[workspaceId]/tables/[tableId]
- components
- column-config-sidebar
- row-modal
- select-field
- table-filter
- table-grid
- cells
- headers
- hooks
- background
- hooks
- queries
- lib
- api
- client
- contracts
- copilot
- generated
- tools
- handlers
- server/table
- table
- __tests__
- columns
- query-builder
- __tests__
- rows
- stores/table
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
| 18 | + | |
18 | 19 | | |
19 | 20 | | |
| 21 | + | |
20 | 22 | | |
21 | 23 | | |
22 | 24 | | |
| |||
68 | 70 | | |
69 | 71 | | |
70 | 72 | | |
71 | | - | |
| 73 | + | |
| 74 | + | |
72 | 75 | | |
73 | 76 | | |
74 | 77 | | |
| |||
116 | 119 | | |
117 | 120 | | |
118 | 121 | | |
119 | | - | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
120 | 142 | | |
121 | | - | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
122 | 166 | | |
123 | 167 | | |
124 | 168 | | |
| |||
162 | 206 | | |
163 | 207 | | |
164 | 208 | | |
165 | | - | |
| 209 | + | |
| 210 | + | |
166 | 211 | | |
167 | 212 | | |
168 | 213 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
11 | | - | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
12 | 14 | | |
13 | 15 | | |
14 | 16 | | |
| |||
52 | 54 | | |
53 | 55 | | |
54 | 56 | | |
55 | | - | |
| 57 | + | |
56 | 58 | | |
57 | 59 | | |
58 | 60 | | |
| |||
100 | 102 | | |
101 | 103 | | |
102 | 104 | | |
103 | | - | |
| 105 | + | |
104 | 106 | | |
105 | 107 | | |
106 | 108 | | |
107 | 109 | | |
108 | | - | |
109 | | - | |
110 | | - | |
| 110 | + | |
111 | 111 | | |
112 | 112 | | |
113 | 113 | | |
| |||
144 | 144 | | |
145 | 145 | | |
146 | 146 | | |
147 | | - | |
148 | | - | |
149 | | - | |
150 | | - | |
151 | | - | |
152 | | - | |
153 | | - | |
154 | | - | |
155 | | - | |
156 | | - | |
157 | | - | |
158 | | - | |
159 | 147 | | |
160 | 148 | | |
161 | 149 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
| 3 | + | |
3 | 4 | | |
4 | 5 | | |
5 | | - | |
6 | 6 | | |
7 | | - | |
8 | 7 | | |
9 | 8 | | |
10 | | - | |
| 9 | + | |
| 10 | + | |
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
| |||
36 | 36 | | |
37 | 37 | | |
38 | 38 | | |
39 | | - | |
40 | 39 | | |
| 40 | + | |
41 | 41 | | |
42 | | - | |
43 | | - | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
44 | 45 | | |
45 | 46 | | |
46 | 47 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
279 | 279 | | |
280 | 280 | | |
281 | 281 | | |
| 282 | + | |
| 283 | + | |
282 | 284 | | |
283 | 285 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
14 | 14 | | |
15 | 15 | | |
16 | 16 | | |
| 17 | + | |
17 | 18 | | |
18 | 19 | | |
| 20 | + | |
19 | 21 | | |
20 | 22 | | |
21 | 23 | | |
| |||
86 | 88 | | |
87 | 89 | | |
88 | 90 | | |
89 | | - | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
90 | 100 | | |
91 | 101 | | |
92 | 102 | | |
| |||
138 | 148 | | |
139 | 149 | | |
140 | 150 | | |
141 | | - | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
142 | 171 | | |
143 | | - | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
144 | 195 | | |
145 | 196 | | |
146 | 197 | | |
| |||
195 | 246 | | |
196 | 247 | | |
197 | 248 | | |
198 | | - | |
| 249 | + | |
| 250 | + | |
199 | 251 | | |
200 | 252 | | |
201 | 253 | | |
| |||
Lines changed: 7 additions & 11 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
16 | | - | |
17 | | - | |
18 | | - | |
19 | | - | |
20 | | - | |
21 | | - | |
22 | | - | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
23 | 19 | | |
24 | 20 | | |
25 | 21 | | |
| |||
88 | 84 | | |
89 | 85 | | |
90 | 86 | | |
91 | | - | |
| 87 | + | |
92 | 88 | | |
93 | 89 | | |
94 | 90 | | |
95 | 91 | | |
96 | 92 | | |
97 | | - | |
| 93 | + | |
98 | 94 | | |
99 | 95 | | |
100 | 96 | | |
| |||
142 | 138 | | |
143 | 139 | | |
144 | 140 | | |
145 | | - | |
| 141 | + | |
146 | 142 | | |
147 | 143 | | |
148 | 144 | | |
| |||
168 | 164 | | |
169 | 165 | | |
170 | 166 | | |
171 | | - | |
| 167 | + | |
172 | 168 | | |
173 | 169 | | |
174 | 170 | | |
| |||
0 commit comments