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>
132 lines
3.0 KiB
TypeScript
132 lines
3.0 KiB
TypeScript
import { Field, HideField, InputType, Int } from '@nestjs/graphql';
|
|
|
|
import {
|
|
IsBoolean,
|
|
IsEnum,
|
|
IsInt,
|
|
IsNotEmpty,
|
|
IsNumber,
|
|
IsOptional,
|
|
IsString,
|
|
IsUUID,
|
|
Max,
|
|
Min,
|
|
} from 'class-validator';
|
|
import {
|
|
AggregateOperations,
|
|
ViewCalendarLayout,
|
|
ViewOpenRecordIn,
|
|
ViewKey,
|
|
ViewType,
|
|
ViewVisibility,
|
|
} from 'twenty-shared/types';
|
|
|
|
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
|
import { IsValidMetadataName } from 'src/engine/decorators/metadata/is-valid-metadata-name.decorator';
|
|
import { KANBAN_COLUMN_MAX_WIDTH } from 'src/engine/metadata-modules/view/constants/kanban-column-max-width.constant';
|
|
import { KANBAN_COLUMN_MIN_WIDTH } from 'src/engine/metadata-modules/view/constants/kanban-column-min-width.constant';
|
|
|
|
@InputType()
|
|
export class CreateViewInput {
|
|
@IsOptional()
|
|
@IsUUID()
|
|
@Field(() => UUIDScalarType, { nullable: true })
|
|
id?: string;
|
|
|
|
@IsString()
|
|
@IsNotEmpty()
|
|
@IsValidMetadataName()
|
|
@Field({ nullable: false })
|
|
name: string;
|
|
|
|
@IsUUID()
|
|
@Field(() => UUIDScalarType, { nullable: false })
|
|
objectMetadataId: string;
|
|
|
|
@IsEnum(ViewType)
|
|
@Field(() => ViewType, { nullable: true, defaultValue: ViewType.TABLE })
|
|
type?: ViewType;
|
|
|
|
@IsOptional()
|
|
@IsEnum(ViewKey)
|
|
@Field(() => ViewKey, { nullable: true })
|
|
key?: ViewKey;
|
|
|
|
@IsString()
|
|
@Field({ nullable: false })
|
|
icon: string;
|
|
|
|
@IsOptional()
|
|
@IsNumber()
|
|
@Field({ nullable: true, defaultValue: 0 })
|
|
position?: number;
|
|
|
|
@IsOptional()
|
|
@IsBoolean()
|
|
@Field({ nullable: true, defaultValue: false })
|
|
isCompact?: boolean;
|
|
|
|
@IsOptional()
|
|
@IsBoolean()
|
|
@Field({ nullable: true, defaultValue: false })
|
|
shouldHideEmptyGroups?: boolean;
|
|
|
|
@IsOptional()
|
|
@IsInt()
|
|
@Min(KANBAN_COLUMN_MIN_WIDTH)
|
|
@Max(KANBAN_COLUMN_MAX_WIDTH)
|
|
@Field(() => Int, { nullable: true })
|
|
kanbanColumnWidth?: number;
|
|
|
|
@IsOptional()
|
|
@IsEnum(ViewOpenRecordIn)
|
|
@Field(() => ViewOpenRecordIn, {
|
|
nullable: true,
|
|
defaultValue: ViewOpenRecordIn.SIDE_PANEL,
|
|
})
|
|
openRecordIn?: ViewOpenRecordIn;
|
|
|
|
@IsOptional()
|
|
@IsEnum(AggregateOperations)
|
|
@Field(() => AggregateOperations, { nullable: true })
|
|
kanbanAggregateOperation?: AggregateOperations;
|
|
|
|
@IsOptional()
|
|
@IsUUID()
|
|
@Field(() => UUIDScalarType, { nullable: true })
|
|
kanbanAggregateOperationFieldMetadataId?: string;
|
|
|
|
@IsOptional()
|
|
@IsString()
|
|
@Field({ nullable: true })
|
|
anyFieldFilterValue?: string;
|
|
|
|
@IsOptional()
|
|
@IsEnum(ViewCalendarLayout)
|
|
@Field(() => ViewCalendarLayout, { nullable: true })
|
|
calendarLayout?: ViewCalendarLayout;
|
|
|
|
@IsOptional()
|
|
@IsUUID()
|
|
@Field(() => UUIDScalarType, { nullable: true })
|
|
calendarFieldMetadataId?: string;
|
|
|
|
@IsOptional()
|
|
@IsUUID()
|
|
@Field(() => UUIDScalarType, { nullable: true })
|
|
mainGroupByFieldMetadataId?: string;
|
|
|
|
@IsOptional()
|
|
@IsEnum(ViewVisibility)
|
|
@Field(() => ViewVisibility, {
|
|
nullable: true,
|
|
})
|
|
visibility?: ViewVisibility;
|
|
|
|
@HideField()
|
|
universalIdentifier?: string;
|
|
|
|
@HideField()
|
|
applicationId?: string;
|
|
}
|