f663cd3c68
Replaces the per-view "Open in" setting with a two-level model, following up on #23422 / #23424 and superseding the closed #23446 and #23457: - `objectMetadata.openRecordIn`: `SIDE_PANEL` | `RECORD_PAGE` | `USER_CHOICE` (default `USER_CHOICE`) - `workspaceMember.openRecordIn`: `SIDE_PANEL` | `RECORD_PAGE` (default `SIDE_PANEL`), editable in Settings > Experience The rule: records open where the member prefers, unless the object pins them, and never in a panel there is no room for (mobile always resolves to the record page). ## Why Having the setting on views, objects and members at once was heavy, and view-level resolution was fragile: a chip rendered outside a view (notes, front components, kanban cards pointing at another object) had no view to read from, which is the class of bug behind #23422. Resolution is now context-free: it needs only the object, the current member and the viewport, so chips behave identically everywhere by construction. ## Changes **Object level** - New `openRecordIn` enum column on `objectMetadata`, editable through `updateOneObject` and surfaced in Settings > Data model > Object > Layout ("Open records in": Member preference / Side Panel / Record Page) - Standard definitions pin `workflow`, `workflowVersion`, `dashboard` and `messageCampaign` to the record page (matching the previously hardcoded list) and `calendarEvent` to the side panel (it has no curated record page); everything else, including `workflowRun`, follows the member preference - Apps can set it in `defineObject()` via the object manifest **Member level** - New `openRecordIn` standard field on `workspaceMember`, persisted through the existing settings path (same as `colorScheme`) and exposed in Settings > Experience **View level (deprecated)** - `view.openRecordIn` is no longer read or written by the frontend; the "Open in" entry is gone from the view options dropdown - The column, DTO field and inputs are kept for one release for API compatibility: the output field carries a `deprecationReason`, the inputs keep accepting the value with a `Deprecated:` description (NestJS silently drops input fields that have a `deprecationReason`, which would have been a breaking change) **Upgrade (2.27)** - Fast instance command adds the `objectMetadata.openRecordIn` column defaulting to `USER_CHOICE` - Workspace command adds the `workspaceMember.openRecordIn` field - Workspace command seeds the object column from the standard definitions (any non-`USER_CHOICE` value), then lifts deliberate per-view record page choices onto objects the definitions don't pin **Debt removed** - `canOpenObjectInSidePanel` hardcoded object list and its test - `ObjectOptionsDropdownLayoutOpenInContent` and the `layoutOpenIn` dropdown wiring - `DefaultViewOpenRecordIn` - Context-store/view-based resolution in `useResolveOpenRecordIn` (now reads object metadata + member + viewport) - Front components no longer guess from the current view: an explicit side-panel call honours a pinned object and the viewport, nothing else ## Verification - Ran the three upgrade commands against a live database: column created, the pinned standard objects seeded per workspace (record page pins plus calendarEvent to side panel), member field backfilled to `SIDE_PANEL`; seed rerun is a no-op - Seed command verified on a simulated pre-upgrade workspace (index view set to record page on company): pins the standard objects plus company, idempotent on rerun - Both packages typecheck and lint clean; affected unit suites and the application sync, view creation and metadata cache integration specs pass --------- Co-authored-by: Thomas des Francs <tdesfrancs@gmail.com>
122 lines
3.1 KiB
TypeScript
122 lines
3.1 KiB
TypeScript
import { Field, InputType, Int } from '@nestjs/graphql';
|
|
|
|
import {
|
|
IsBoolean,
|
|
IsEnum,
|
|
IsInt,
|
|
IsNotEmpty,
|
|
IsOptional,
|
|
IsString,
|
|
IsUUID,
|
|
Max,
|
|
Min,
|
|
} from 'class-validator';
|
|
import {
|
|
AggregateOperations,
|
|
ViewCalendarLayout,
|
|
ViewOpenRecordIn,
|
|
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';
|
|
import { VIEW_OPEN_RECORD_IN_DEPRECATION } from 'src/engine/metadata-modules/view/constants/view-open-record-in-deprecation.constant';
|
|
|
|
// TODO: this should be refactored like for view-field.input.ts
|
|
// This is a temporary fix as we were extending the CreateViewInput class which was adding default values for the non filled fields
|
|
@InputType()
|
|
export class UpdateViewInput {
|
|
@IsUUID()
|
|
@Field(() => UUIDScalarType, { nullable: true })
|
|
id: string;
|
|
|
|
@IsOptional()
|
|
@IsNotEmpty()
|
|
@IsValidMetadataName()
|
|
@Field({ nullable: true })
|
|
name?: string;
|
|
|
|
@IsOptional()
|
|
@IsEnum(ViewType)
|
|
@Field(() => ViewType, { nullable: true })
|
|
type?: ViewType;
|
|
|
|
@IsOptional()
|
|
@IsString()
|
|
@Field({ nullable: true })
|
|
icon?: string;
|
|
|
|
@IsOptional()
|
|
@IsBoolean()
|
|
@Field({ nullable: true })
|
|
position?: number;
|
|
|
|
@IsOptional()
|
|
@IsBoolean()
|
|
@Field({ nullable: true })
|
|
isCompact?: boolean;
|
|
|
|
@IsOptional()
|
|
@IsEnum(ViewOpenRecordIn)
|
|
@Field(() => ViewOpenRecordIn, {
|
|
nullable: true,
|
|
description: `Deprecated: ${VIEW_OPEN_RECORD_IN_DEPRECATION}`,
|
|
})
|
|
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 | null;
|
|
|
|
@IsOptional()
|
|
@IsUUID()
|
|
@Field(() => UUIDScalarType, { nullable: true })
|
|
calendarEndFieldMetadataId?: string | null;
|
|
|
|
@IsOptional()
|
|
@IsEnum(ViewVisibility)
|
|
@Field(() => ViewVisibility, { nullable: true })
|
|
visibility?: ViewVisibility;
|
|
|
|
@IsOptional()
|
|
@IsUUID()
|
|
@Field(() => UUIDScalarType, { nullable: true })
|
|
mainGroupByFieldMetadataId?: string | null;
|
|
|
|
@IsOptional()
|
|
@IsBoolean()
|
|
@Field({ nullable: true })
|
|
shouldHideEmptyGroups?: boolean;
|
|
|
|
@IsOptional()
|
|
@IsInt()
|
|
@Min(KANBAN_COLUMN_MIN_WIDTH)
|
|
@Max(KANBAN_COLUMN_MAX_WIDTH)
|
|
@Field(() => Int, { nullable: true })
|
|
kanbanColumnWidth?: number | null;
|
|
}
|