feat: drag-and-drop column reordering in the spreadsheet layout - #2
Conversation
Spreadsheet column order was fixed to SPREADSHEET_PROPERTY_LIST. Header cells are now drag sources and drop targets, so columns can be dragged left/right into any order. The order persists per user, per project via a new column_order key on display_filters. That field is an unvalidated JSONField behind a fields = "__all__" serializer and the filter store spreads unknown keys straight through, so this needs no backend change or migration. It is also absent from NON_SERVER_DISPLAY_FILTERS, so a reorder neither refetches nor clears issues. spreadsheetColumnsList already drove both the header row and every body row, so permuting that one array reorders the whole table in lockstep. applyColumnOrder reconciles the stored order against the columns actually available, which is necessary because the stored value is untrusted JSON written by a possibly older build: - columns that are no longer available are dropped (a project with cycles disabled has no "cycle" column) - columns missing from the stored order are inserted at their canonical neighbour rather than appended, so re-enabling modules puts that column back where it was instead of stranding it off-screen right - duplicates and unknown keys are discarded The result is always an exact permutation of the available columns; extras, gaps or repeats would desync header cells from body cells. Sorting is unchanged - it already exists as a per-column dropdown. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
React Doctor found 2 new issues in 2 files · 2 warnings · score 93 / 100 (Great) · 0 fixed · vs 2 warnings
Reviewed by React Doctor for commit |
|
|
||
| // Dropping on the near edge of an adjacent column is a no-op. Return the same | ||
| // reference so callers can skip persisting an identical order. | ||
| return reordered.every((column, index) => column === columns[index]) ? columns : reordered; |
There was a problem hiding this comment.
React Doctor · react-doctor/js-length-check-first (warning)
This is slow because .every() compares two arrays item by item, so check a.length === b.length first to bail out immediately when sizes differ
Fix → Check a.length === b.length && a.every((x, i) => x === b[i]) so arrays of different sizes stop right away
| @@ -20,6 +21,8 @@ import { useBulkOperationStatus } from "@/hooks/use-bulk-operation-status"; | |||
| // local imports | |||
| import type { TRenderQuickActions } from "../list/list-view-types"; | |||
| import { QuickAddIssueRoot, SpreadsheetAddIssueButton } from "../quick-add"; | |||
There was a problem hiding this comment.
React Doctor · react-doctor/no-barrel-import (warning)
This ships extra code to your users & slows page load. Import directly from: "../quick-add/root", "../quick-add/button/spreadsheet".
Fix → Import from the direct path: import { Button } from './components/Button' instead of ./components
What
Spreadsheet columns can now be dragged left/right into any order. Header cells are both drag sources and drop targets, with a vertical accent line showing where the column will land.
The order persists per user, per project.
How it works
spreadsheetColumnsListalready drove both the header row and every body row, so permuting that one array reorders the whole table in lockstep — no per-cell coordination needed.Persistence rides on a new
column_orderkey insidedisplay_filters:display_filtersis an unvalidatedJSONField(db/models/project.py:353) behind afields = "__all__"serializer, and the filter store spreads unknown keys straight through (project/filter.store.ts:230) — no backend change or migration.column_orderis absent fromNON_SERVER_DISPLAY_FILTERS, so a reorder triggers neithergetShouldReFetchIssuesnorgetShouldClearIssues. Dragging a column does not refetch issues.Reconciliation
The stored order is untrusted — plain JSON, possibly written by an older build, against a column list that shifts with project settings.
applyColumnOrdernormalises it:The canonical-neighbour choice is deliberate: appending would make a re-enabled
cyclecolumn leap to the far right even though the user never moved it. The result is always an exact permutation of the available columns — extras, gaps or repeats would desync header cells from body cells.Not included
Sorting is unchanged. It already exists as a per-column dropdown (asc / desc / clear) writing
display_filters.order_by. Multi-column sort would need backend work —apps/api/plane/utils/order_queryset.py:166sanitisesorder_byto a single allowlisted field.Testing
tsc --noEmitclean;oxlintclean on all touched files.applyColumnOrderandreorderColumnexercised against 18 cases — permutation invariant, cycles-disabled dropping, re-enable landing aftermodules, corrupt/duplicate/unknown keys, every reorder direction including the no-op, and input non-mutation. All pass.CustomMenubutton.CustomMenuopens ononClick(packages/ui/src/dropdowns/custom-menu.tsx:254), which does not fire after a drag, so the sort menu and the drag should not conflict — but that is reasoning, not evidence. Sticky first column and horizontal auto-scroll mid-drag are also untested.The helper test cases were run as a throwaway script rather than committed; happy to port them into the repo's test setup if wanted.
🤖 Generated with Claude Code