23f5ba9ebf
## What & why Lets users resize the columns of a Kanban (record board) view. Requested by a user; the design avoids the "ragged board" problem by making the width a **single shared value**. ## Behaviour - A drag handle appears on the right edge of every column header. - Because all columns read **one** width value, dragging any handle resizes **every** column together — they can never end up mismatched. - Width is clamped between **150px** and **400px** (default **200px**). - The width is **persisted per view** and restored on reload. ## Approach **Backend** — a new nullable `View.kanbanColumnWidth` field, threaded through the existing view-level setting pattern (the same one `kanbanAggregateOperation` / `shouldHideEmptyGroups` use), so it gets create/update/manifest/override support for free: - entity column + `ViewOverrides` + `@WasIntroducedInUpgrade` - `CreateViewInput` / `UpdateViewInput` (`Int`, `@Min(150)`/`@Max(400)`) + `ViewDTO` - flat-view editable properties, entity-properties config, compare-type, standard-view + manifest converters - a fast instance command adding the `core.view` column **Frontend** — the value hydrates into a view-scoped atom and drives a single CSS variable set on the board container, which both column headers and bodies read. Live dragging only writes that CSS variable (no per-move React re-render); the final width is committed to the atom and persisted via `updateView` on pointer-up. ## Nullability / defaults `kanbanColumnWidth` is nullable — `null` means "never resized" and the UI falls back to the 200px default, so existing rows need no backfill. ## Validation - `nx typecheck twenty-server` ✅ and `nx typecheck twenty-front` ✅ - `nx lint:diff-with-main twenty-server` ✅; frontend lint fixes applied (split constants to one-per-file, removed `useRef`-for-state in favour of `useState`). - Draft pending a final green CI run (the dev container reclaimed `node_modules` mid-session; re-running locally). ## Test plan - [ ] Drag a kanban column edge → all columns resize together, clamped 150–400px - [ ] Reload → width persists for that view; other views unaffected - [ ] A view that was never resized still renders at 200px https://claude.ai/code/session_016Qe6oDBkhVbrq2QkXBJ5nE --- _Generated by [Claude Code](https://claude.ai/code/session_016Qe6oDBkhVbrq2QkXBJ5nE)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21828?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: Claude <noreply@anthropic.com>
47 lines
1.8 KiB
TypeScript
47 lines
1.8 KiB
TypeScript
import { type GraphQLView } from '@/views/types/GraphQLView';
|
|
import { isDefined } from 'twenty-shared/utils';
|
|
import { type UpdateViewInput } from '~/generated-metadata/graphql';
|
|
|
|
export const convertUpdateViewInputToGql = (
|
|
view: Partial<GraphQLView & { __typename?: string }>,
|
|
): UpdateViewInput => {
|
|
return {
|
|
id: view.id,
|
|
...(view.name && { name: view.name }),
|
|
...(view.icon && { icon: view.icon }),
|
|
...(isDefined(view.position) && { position: view.position }),
|
|
...(isDefined(view.isCompact) && { isCompact: view.isCompact }),
|
|
...(isDefined(view.kanbanAggregateOperation) && {
|
|
kanbanAggregateOperation: view.kanbanAggregateOperation,
|
|
}),
|
|
...(isDefined(view.kanbanAggregateOperationFieldMetadataId) && {
|
|
kanbanAggregateOperationFieldMetadataId:
|
|
view.kanbanAggregateOperationFieldMetadataId,
|
|
}),
|
|
...(isDefined(view.anyFieldFilterValue) && {
|
|
anyFieldFilterValue: view.anyFieldFilterValue,
|
|
}),
|
|
...(isDefined(view.key) && { key: view.key }),
|
|
...(isDefined(view.openRecordIn) && {
|
|
openRecordIn: view.openRecordIn,
|
|
}),
|
|
...(isDefined(view.type) && { type: view.type }),
|
|
...(isDefined(view.calendarLayout) && {
|
|
calendarLayout: view.calendarLayout,
|
|
}),
|
|
...(isDefined(view.calendarFieldMetadataId) && {
|
|
calendarFieldMetadataId: view.calendarFieldMetadataId,
|
|
}),
|
|
...(isDefined(view.visibility) && { visibility: view.visibility }),
|
|
...(isDefined(view.shouldHideEmptyGroups) && {
|
|
shouldHideEmptyGroups: view.shouldHideEmptyGroups,
|
|
}),
|
|
...(view.kanbanColumnWidth !== undefined && {
|
|
kanbanColumnWidth: view.kanbanColumnWidth,
|
|
}),
|
|
...(view.mainGroupByFieldMetadataId !== undefined && {
|
|
mainGroupByFieldMetadataId: view.mainGroupByFieldMetadataId,
|
|
}),
|
|
};
|
|
};
|