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>
287 lines
9.1 KiB
TypeScript
287 lines
9.1 KiB
TypeScript
import {
|
|
Check,
|
|
Column,
|
|
CreateDateColumn,
|
|
DeleteDateColumn,
|
|
Entity,
|
|
Index,
|
|
JoinColumn,
|
|
ManyToOne,
|
|
OneToMany,
|
|
PrimaryGeneratedColumn,
|
|
type Relation,
|
|
UpdateDateColumn,
|
|
} from 'typeorm';
|
|
import {
|
|
AggregateOperations,
|
|
type SerializedRelation,
|
|
ViewCalendarLayout,
|
|
ViewKey,
|
|
ViewOpenRecordIn,
|
|
ViewType,
|
|
ViewVisibility,
|
|
} from 'twenty-shared/types';
|
|
|
|
import { WasIntroducedInUpgrade } from 'src/engine/core-modules/upgrade/decorators/was-introduced-in-upgrade.decorator';
|
|
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
|
import { ADD_IS_SYSTEM_SIDE_EFFECT_UPGRADE_COMMAND_NAME } from 'src/database/commands/upgrade-version-command/2-15/is-system-side-effect-upgrade-command-name.constant';
|
|
import { ADD_VIEW_KANBAN_COLUMN_WIDTH_UPGRADE_COMMAND_NAME } from 'src/database/commands/upgrade-version-command/2-15/add-view-kanban-column-width-upgrade-command-name.constant';
|
|
import { ADD_CALENDAR_END_FIELD_METADATA_ID_TO_VIEW_UPGRADE_COMMAND_NAME } from 'src/database/commands/upgrade-version-command/2-22/add-calendar-end-field-metadata-id-to-view-upgrade-command-name.constant';
|
|
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
|
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
|
import { ViewFieldGroupEntity } from 'src/engine/metadata-modules/view-field-group/entities/view-field-group.entity';
|
|
import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity';
|
|
import { ViewFilterGroupEntity } from 'src/engine/metadata-modules/view-filter-group/entities/view-filter-group.entity';
|
|
import { ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity';
|
|
import { ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities/view-group.entity';
|
|
import { ViewSortEntity } from 'src/engine/metadata-modules/view-sort/entities/view-sort.entity';
|
|
import { OverridableEntity } from 'src/engine/workspace-manager/types/overridable-entity';
|
|
|
|
export type ViewOverrides = {
|
|
name?: string;
|
|
type?: ViewType;
|
|
icon?: string;
|
|
position?: number;
|
|
isCompact?: boolean;
|
|
openRecordIn?: ViewOpenRecordIn;
|
|
kanbanAggregateOperation?: AggregateOperations | null;
|
|
kanbanAggregateOperationFieldMetadataId?: SerializedRelation | null;
|
|
anyFieldFilterValue?: string | null;
|
|
calendarLayout?: ViewCalendarLayout | null;
|
|
calendarFieldMetadataId?: SerializedRelation | null;
|
|
calendarEndFieldMetadataId?: SerializedRelation | null;
|
|
visibility?: ViewVisibility;
|
|
mainGroupByFieldMetadataId?: SerializedRelation | null;
|
|
shouldHideEmptyGroups?: boolean;
|
|
kanbanColumnWidth?: number | null;
|
|
};
|
|
|
|
// We could refactor this type to be dynamic to view type
|
|
@Entity({ name: 'view', schema: 'core' })
|
|
@Index('IDX_VIEW_WORKSPACE_ID_OBJECT_METADATA_ID', [
|
|
'workspaceId',
|
|
'objectMetadataId',
|
|
])
|
|
@Index('IDX_VIEW_VISIBILITY', ['visibility'])
|
|
@Index('IDX_VIEW_CALENDAR_FIELD_METADATA', ['calendarFieldMetadataId'])
|
|
@Index('IDX_VIEW_CALENDAR_END_FIELD_METADATA', ['calendarEndFieldMetadataId'])
|
|
@Index('IDX_VIEW_KANBAN_FIELD_METADATA', [
|
|
'kanbanAggregateOperationFieldMetadataId',
|
|
])
|
|
@Index('IDX_VIEW_MAIN_GROUP_BY_FIELD_METADATA', ['mainGroupByFieldMetadataId'])
|
|
@Index('IDX_VIEW_CREATED_BY_USER_WORKSPACE', ['createdByUserWorkspaceId'])
|
|
@Check(
|
|
'CHK_VIEW_CALENDAR_INTEGRITY',
|
|
`("type" NOT IN ('CALENDAR', 'CALENDAR_WIDGET') OR ("calendarLayout" IS NOT NULL AND "calendarFieldMetadataId" IS NOT NULL))`,
|
|
)
|
|
export class ViewEntity
|
|
extends OverridableEntity<ViewOverrides>
|
|
implements Required<ViewEntity>
|
|
{
|
|
@PrimaryGeneratedColumn('uuid')
|
|
id: string;
|
|
|
|
@Column({ nullable: false, type: 'text', default: '' })
|
|
name: string;
|
|
|
|
@Column({ nullable: false, type: 'uuid' })
|
|
objectMetadataId: string;
|
|
|
|
@ManyToOne(() => ObjectMetadataEntity, {
|
|
onDelete: 'CASCADE',
|
|
})
|
|
@JoinColumn({ name: 'objectMetadataId' })
|
|
objectMetadata: Relation<ObjectMetadataEntity>;
|
|
|
|
@Column({
|
|
type: 'enum',
|
|
enum: Object.values(ViewType),
|
|
nullable: false,
|
|
default: ViewType.TABLE,
|
|
})
|
|
type: ViewType;
|
|
|
|
@Column({
|
|
type: 'enum',
|
|
enum: Object.values(ViewKey),
|
|
nullable: true,
|
|
default: null,
|
|
})
|
|
key: ViewKey | null;
|
|
|
|
@Column({ nullable: false, type: 'text' })
|
|
icon: string;
|
|
|
|
@Column({ nullable: false, type: 'double precision', default: 0 })
|
|
position: number;
|
|
|
|
@Column({ nullable: false, default: false, type: 'boolean' })
|
|
isCompact: boolean;
|
|
|
|
@Column({ nullable: false, default: false, type: 'boolean' })
|
|
isCustom: boolean;
|
|
|
|
// Deprecated: superseded by objectMetadata.openRecordIn and the member preference.
|
|
@Column({
|
|
type: 'enum',
|
|
enum: Object.values(ViewOpenRecordIn),
|
|
nullable: false,
|
|
default: ViewOpenRecordIn.SIDE_PANEL,
|
|
})
|
|
openRecordIn: ViewOpenRecordIn;
|
|
|
|
@Column({
|
|
type: 'enum',
|
|
enum: Object.values(AggregateOperations),
|
|
nullable: true,
|
|
default: null,
|
|
})
|
|
kanbanAggregateOperation: AggregateOperations | null;
|
|
|
|
@Column({ nullable: true, type: 'uuid' })
|
|
kanbanAggregateOperationFieldMetadataId: string | null;
|
|
|
|
@ManyToOne(
|
|
() => FieldMetadataEntity,
|
|
(FieldMetadataEntity) => FieldMetadataEntity.kanbanAggregateOperationViews,
|
|
{
|
|
onDelete: 'CASCADE',
|
|
nullable: true,
|
|
},
|
|
)
|
|
@JoinColumn({ name: 'kanbanAggregateOperationFieldMetadataId' })
|
|
kanbanAggregateOperationFieldMetadata: Relation<FieldMetadataEntity> | null;
|
|
|
|
@Column({
|
|
type: 'enum',
|
|
enum: Object.values(ViewCalendarLayout),
|
|
nullable: true,
|
|
default: null,
|
|
})
|
|
calendarLayout: ViewCalendarLayout | null;
|
|
|
|
@Column({ nullable: true, type: 'uuid' })
|
|
calendarFieldMetadataId: string | null;
|
|
|
|
@ManyToOne(
|
|
() => FieldMetadataEntity,
|
|
(fieldMetadata) => fieldMetadata.calendarViews,
|
|
{
|
|
onDelete: 'CASCADE',
|
|
nullable: true,
|
|
},
|
|
)
|
|
@JoinColumn({ name: 'calendarFieldMetadataId' })
|
|
calendarFieldMetadata: Relation<FieldMetadataEntity> | null;
|
|
|
|
@WasIntroducedInUpgrade({
|
|
upgradeCommandName:
|
|
ADD_CALENDAR_END_FIELD_METADATA_ID_TO_VIEW_UPGRADE_COMMAND_NAME,
|
|
})
|
|
@Column({ nullable: true, type: 'uuid' })
|
|
calendarEndFieldMetadataId: string | null;
|
|
|
|
@ManyToOne(
|
|
() => FieldMetadataEntity,
|
|
(fieldMetadata) => fieldMetadata.calendarEndViews,
|
|
{
|
|
onDelete: 'SET NULL',
|
|
nullable: true,
|
|
},
|
|
)
|
|
@JoinColumn({ name: 'calendarEndFieldMetadataId' })
|
|
calendarEndFieldMetadata: Relation<FieldMetadataEntity> | null;
|
|
|
|
@Column({ nullable: true, type: 'uuid' })
|
|
mainGroupByFieldMetadataId: string | null;
|
|
|
|
@ManyToOne(
|
|
() => FieldMetadataEntity,
|
|
(fieldMetadata) => fieldMetadata.mainGroupByFieldMetadataViews,
|
|
{
|
|
onDelete: 'CASCADE',
|
|
nullable: true,
|
|
},
|
|
)
|
|
@JoinColumn({ name: 'mainGroupByFieldMetadataId' })
|
|
mainGroupByFieldMetadata: Relation<FieldMetadataEntity> | null;
|
|
|
|
@Column({ nullable: false, default: false, type: 'boolean' })
|
|
shouldHideEmptyGroups: boolean;
|
|
|
|
@WasIntroducedInUpgrade({
|
|
upgradeCommandName: ADD_VIEW_KANBAN_COLUMN_WIDTH_UPGRADE_COMMAND_NAME,
|
|
})
|
|
@Column({ nullable: true, type: 'int', default: null })
|
|
kanbanColumnWidth: number | null;
|
|
|
|
@CreateDateColumn({ type: 'timestamptz' })
|
|
createdAt: Date;
|
|
|
|
@UpdateDateColumn({ type: 'timestamptz' })
|
|
updatedAt: Date;
|
|
|
|
@DeleteDateColumn({ type: 'timestamptz' })
|
|
deletedAt: Date | null;
|
|
|
|
@Column({ nullable: true, type: 'text', default: null })
|
|
anyFieldFilterValue: string | null;
|
|
|
|
@Column({
|
|
type: 'enum',
|
|
enum: Object.values(ViewVisibility),
|
|
nullable: false,
|
|
default: ViewVisibility.WORKSPACE,
|
|
})
|
|
visibility: ViewVisibility;
|
|
|
|
@WasIntroducedInUpgrade({
|
|
upgradeCommandName: ADD_IS_SYSTEM_SIDE_EFFECT_UPGRADE_COMMAND_NAME,
|
|
})
|
|
@Column({ nullable: false, default: false, type: 'boolean' })
|
|
isSystemSideEffect: boolean;
|
|
|
|
@Column({ nullable: true, type: 'uuid' })
|
|
createdByUserWorkspaceId: string | null;
|
|
|
|
@ManyToOne(() => UserWorkspaceEntity, {
|
|
onDelete: 'SET NULL',
|
|
})
|
|
@JoinColumn({ name: 'createdByUserWorkspaceId' })
|
|
createdBy: Relation<UserWorkspaceEntity>;
|
|
|
|
@OneToMany(() => ViewFieldEntity, (viewField) => viewField.view)
|
|
viewFields: Relation<ViewFieldEntity[]>;
|
|
|
|
@OneToMany(
|
|
() => ViewFieldGroupEntity,
|
|
(viewFieldGroup) => viewFieldGroup.view,
|
|
)
|
|
viewFieldGroups: Relation<ViewFieldGroupEntity[]>;
|
|
|
|
@OneToMany(() => ViewFilterEntity, (viewFilter) => viewFilter.view)
|
|
viewFilters: Relation<ViewFilterEntity[]>;
|
|
|
|
@OneToMany(() => ViewSortEntity, (viewSort) => viewSort.view)
|
|
viewSorts: Relation<ViewSortEntity[]>;
|
|
|
|
@OneToMany(() => ViewGroupEntity, (viewGroup) => viewGroup.view)
|
|
viewGroups: Relation<ViewGroupEntity[]>;
|
|
|
|
@OneToMany(
|
|
() => ViewFilterGroupEntity,
|
|
(viewFilterGroup) => viewFilterGroup.view,
|
|
)
|
|
viewFilterGroups: Relation<ViewFilterGroupEntity[]>;
|
|
}
|
|
|
|
const VIEW_OVERRIDABLE_COLUMNS_UPGRADE_COMMAND_NAME =
|
|
'2.12.0_ViewOverridableEntityFastInstanceCommand_1781114009075';
|
|
|
|
WasIntroducedInUpgrade({
|
|
upgradeCommandName: VIEW_OVERRIDABLE_COLUMNS_UPGRADE_COMMAND_NAME,
|
|
})(ViewEntity.prototype, 'overrides');
|
|
WasIntroducedInUpgrade({
|
|
upgradeCommandName: VIEW_OVERRIDABLE_COLUMNS_UPGRADE_COMMAND_NAME,
|
|
})(ViewEntity.prototype, 'isActive');
|