Move open-record-in to object metadata and member preference (#23614)
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>
This commit is contained in:
@@ -343,6 +343,7 @@ type Object {
|
|||||||
isUICreatable: Boolean!
|
isUICreatable: Boolean!
|
||||||
isUIReadOnly: Boolean! @deprecated(reason: "Use isUIEditable")
|
isUIReadOnly: Boolean! @deprecated(reason: "Use isUIEditable")
|
||||||
isSearchable: Boolean!
|
isSearchable: Boolean!
|
||||||
|
openRecordIn: ObjectOpenRecordIn!
|
||||||
applicationId: UUID!
|
applicationId: UUID!
|
||||||
createdAt: DateTime!
|
createdAt: DateTime!
|
||||||
updatedAt: DateTime!
|
updatedAt: DateTime!
|
||||||
@@ -369,6 +370,12 @@ type Object {
|
|||||||
): ObjectIndexMetadatasConnection!
|
): ObjectIndexMetadatasConnection!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ObjectOpenRecordIn {
|
||||||
|
SIDE_PANEL
|
||||||
|
RECORD_PAGE
|
||||||
|
USER_CHOICE
|
||||||
|
}
|
||||||
|
|
||||||
input CursorPaging {
|
input CursorPaging {
|
||||||
"""Paginate before opaque cursor"""
|
"""Paginate before opaque cursor"""
|
||||||
before: ConnectionCursor
|
before: ConnectionCursor
|
||||||
@@ -436,6 +443,7 @@ type WorkspaceMember {
|
|||||||
name: FullName!
|
name: FullName!
|
||||||
userEmail: String!
|
userEmail: String!
|
||||||
colorScheme: String!
|
colorScheme: String!
|
||||||
|
openRecordIn: OpenRecordIn!
|
||||||
avatarUrl: String
|
avatarUrl: String
|
||||||
locale: String
|
locale: String
|
||||||
calendarStartDay: Int
|
calendarStartDay: Int
|
||||||
@@ -447,6 +455,11 @@ type WorkspaceMember {
|
|||||||
numberFormat: WorkspaceMemberNumberFormatEnum
|
numberFormat: WorkspaceMemberNumberFormatEnum
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum OpenRecordIn {
|
||||||
|
SIDE_PANEL
|
||||||
|
RECORD_PAGE
|
||||||
|
}
|
||||||
|
|
||||||
"""Date format as Month first, Day first, Year first or system as default"""
|
"""Date format as Month first, Day first, Year first or system as default"""
|
||||||
enum WorkspaceMemberDateFormatEnum {
|
enum WorkspaceMemberDateFormatEnum {
|
||||||
SYSTEM
|
SYSTEM
|
||||||
@@ -793,7 +806,7 @@ type View {
|
|||||||
position: Float!
|
position: Float!
|
||||||
isCompact: Boolean!
|
isCompact: Boolean!
|
||||||
isCustom: Boolean!
|
isCustom: Boolean!
|
||||||
openRecordIn: ViewOpenRecordIn!
|
openRecordIn: ViewOpenRecordIn! @deprecated(reason: "Superseded by objectMetadata.openRecordIn and the workspace member preference; kept one release for API compatibility, no longer read by the frontend.")
|
||||||
kanbanAggregateOperation: AggregateOperations
|
kanbanAggregateOperation: AggregateOperations
|
||||||
kanbanAggregateOperationFieldMetadataId: UUID
|
kanbanAggregateOperationFieldMetadataId: UUID
|
||||||
mainGroupByFieldMetadataId: UUID
|
mainGroupByFieldMetadataId: UUID
|
||||||
@@ -3749,6 +3762,10 @@ input CreateViewInput {
|
|||||||
isCompact: Boolean = false
|
isCompact: Boolean = false
|
||||||
shouldHideEmptyGroups: Boolean = false
|
shouldHideEmptyGroups: Boolean = false
|
||||||
kanbanColumnWidth: Int
|
kanbanColumnWidth: Int
|
||||||
|
|
||||||
|
"""
|
||||||
|
Deprecated: Superseded by objectMetadata.openRecordIn and the workspace member preference; kept one release for API compatibility, no longer read by the frontend.
|
||||||
|
"""
|
||||||
openRecordIn: ViewOpenRecordIn = SIDE_PANEL
|
openRecordIn: ViewOpenRecordIn = SIDE_PANEL
|
||||||
kanbanAggregateOperation: AggregateOperations
|
kanbanAggregateOperation: AggregateOperations
|
||||||
kanbanAggregateOperationFieldMetadataId: UUID
|
kanbanAggregateOperationFieldMetadataId: UUID
|
||||||
@@ -3767,6 +3784,10 @@ input UpdateViewInput {
|
|||||||
icon: String
|
icon: String
|
||||||
position: Float
|
position: Float
|
||||||
isCompact: Boolean
|
isCompact: Boolean
|
||||||
|
|
||||||
|
"""
|
||||||
|
Deprecated: Superseded by objectMetadata.openRecordIn and the workspace member preference; kept one release for API compatibility, no longer read by the frontend.
|
||||||
|
"""
|
||||||
openRecordIn: ViewOpenRecordIn
|
openRecordIn: ViewOpenRecordIn
|
||||||
kanbanAggregateOperation: AggregateOperations
|
kanbanAggregateOperation: AggregateOperations
|
||||||
kanbanAggregateOperationFieldMetadataId: UUID
|
kanbanAggregateOperationFieldMetadataId: UUID
|
||||||
@@ -3809,6 +3830,10 @@ input UpsertViewWidgetViewSettingsInput {
|
|||||||
type: ViewType
|
type: ViewType
|
||||||
mainGroupByFieldMetadataId: UUID
|
mainGroupByFieldMetadataId: UUID
|
||||||
shouldHideEmptyGroups: Boolean
|
shouldHideEmptyGroups: Boolean
|
||||||
|
|
||||||
|
"""
|
||||||
|
Deprecated: Superseded by objectMetadata.openRecordIn and the workspace member preference; kept one release for API compatibility, no longer read by the frontend.
|
||||||
|
"""
|
||||||
openRecordIn: ViewOpenRecordIn
|
openRecordIn: ViewOpenRecordIn
|
||||||
kanbanAggregateOperation: AggregateOperations
|
kanbanAggregateOperation: AggregateOperations
|
||||||
kanbanAggregateOperationFieldMetadataId: UUID
|
kanbanAggregateOperationFieldMetadataId: UUID
|
||||||
@@ -4182,6 +4207,7 @@ input UpdateObjectPayload {
|
|||||||
imageIdentifierFieldMetadataId: UUID
|
imageIdentifierFieldMetadataId: UUID
|
||||||
isLabelSyncedWithName: Boolean
|
isLabelSyncedWithName: Boolean
|
||||||
isSearchable: Boolean
|
isSearchable: Boolean
|
||||||
|
openRecordIn: ObjectOpenRecordIn
|
||||||
}
|
}
|
||||||
|
|
||||||
input CreateOneIndexInput {
|
input CreateOneIndexInput {
|
||||||
|
|||||||
@@ -245,6 +245,7 @@ export interface Object {
|
|||||||
/** @deprecated Use isUIEditable */
|
/** @deprecated Use isUIEditable */
|
||||||
isUIReadOnly: Scalars['Boolean']
|
isUIReadOnly: Scalars['Boolean']
|
||||||
isSearchable: Scalars['Boolean']
|
isSearchable: Scalars['Boolean']
|
||||||
|
openRecordIn: ObjectOpenRecordIn
|
||||||
applicationId: Scalars['UUID']
|
applicationId: Scalars['UUID']
|
||||||
createdAt: Scalars['DateTime']
|
createdAt: Scalars['DateTime']
|
||||||
updatedAt: Scalars['DateTime']
|
updatedAt: Scalars['DateTime']
|
||||||
@@ -260,6 +261,8 @@ export interface Object {
|
|||||||
__typename: 'Object'
|
__typename: 'Object'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ObjectOpenRecordIn = 'SIDE_PANEL' | 'RECORD_PAGE' | 'USER_CHOICE'
|
||||||
|
|
||||||
export interface FullName {
|
export interface FullName {
|
||||||
firstName: Scalars['String']
|
firstName: Scalars['String']
|
||||||
lastName: Scalars['String']
|
lastName: Scalars['String']
|
||||||
@@ -271,6 +274,7 @@ export interface WorkspaceMember {
|
|||||||
name: FullName
|
name: FullName
|
||||||
userEmail: Scalars['String']
|
userEmail: Scalars['String']
|
||||||
colorScheme: Scalars['String']
|
colorScheme: Scalars['String']
|
||||||
|
openRecordIn: OpenRecordIn
|
||||||
avatarUrl?: Scalars['String']
|
avatarUrl?: Scalars['String']
|
||||||
locale?: Scalars['String']
|
locale?: Scalars['String']
|
||||||
calendarStartDay?: Scalars['Int']
|
calendarStartDay?: Scalars['Int']
|
||||||
@@ -283,6 +287,8 @@ export interface WorkspaceMember {
|
|||||||
__typename: 'WorkspaceMember'
|
__typename: 'WorkspaceMember'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type OpenRecordIn = 'SIDE_PANEL' | 'RECORD_PAGE'
|
||||||
|
|
||||||
|
|
||||||
/** Date format as Month first, Day first, Year first or system as default */
|
/** Date format as Month first, Day first, Year first or system as default */
|
||||||
export type WorkspaceMemberDateFormatEnum = 'SYSTEM' | 'MONTH_FIRST' | 'DAY_FIRST' | 'YEAR_FIRST'
|
export type WorkspaceMemberDateFormatEnum = 'SYSTEM' | 'MONTH_FIRST' | 'DAY_FIRST' | 'YEAR_FIRST'
|
||||||
@@ -551,6 +557,7 @@ export interface View {
|
|||||||
position: Scalars['Float']
|
position: Scalars['Float']
|
||||||
isCompact: Scalars['Boolean']
|
isCompact: Scalars['Boolean']
|
||||||
isCustom: Scalars['Boolean']
|
isCustom: Scalars['Boolean']
|
||||||
|
/** @deprecated Superseded by objectMetadata.openRecordIn and the workspace member preference; kept one release for API compatibility, no longer read by the frontend. */
|
||||||
openRecordIn: ViewOpenRecordIn
|
openRecordIn: ViewOpenRecordIn
|
||||||
kanbanAggregateOperation?: AggregateOperations
|
kanbanAggregateOperation?: AggregateOperations
|
||||||
kanbanAggregateOperationFieldMetadataId?: Scalars['UUID']
|
kanbanAggregateOperationFieldMetadataId?: Scalars['UUID']
|
||||||
@@ -3394,6 +3401,7 @@ export interface ObjectGenqlSelection{
|
|||||||
/** @deprecated Use isUIEditable */
|
/** @deprecated Use isUIEditable */
|
||||||
isUIReadOnly?: boolean | number
|
isUIReadOnly?: boolean | number
|
||||||
isSearchable?: boolean | number
|
isSearchable?: boolean | number
|
||||||
|
openRecordIn?: boolean | number
|
||||||
applicationId?: boolean | number
|
applicationId?: boolean | number
|
||||||
createdAt?: boolean | number
|
createdAt?: boolean | number
|
||||||
updatedAt?: boolean | number
|
updatedAt?: boolean | number
|
||||||
@@ -3448,6 +3456,7 @@ export interface WorkspaceMemberGenqlSelection{
|
|||||||
name?: FullNameGenqlSelection
|
name?: FullNameGenqlSelection
|
||||||
userEmail?: boolean | number
|
userEmail?: boolean | number
|
||||||
colorScheme?: boolean | number
|
colorScheme?: boolean | number
|
||||||
|
openRecordIn?: boolean | number
|
||||||
avatarUrl?: boolean | number
|
avatarUrl?: boolean | number
|
||||||
locale?: boolean | number
|
locale?: boolean | number
|
||||||
calendarStartDay?: boolean | number
|
calendarStartDay?: boolean | number
|
||||||
@@ -3719,6 +3728,7 @@ export interface ViewGenqlSelection{
|
|||||||
position?: boolean | number
|
position?: boolean | number
|
||||||
isCompact?: boolean | number
|
isCompact?: boolean | number
|
||||||
isCustom?: boolean | number
|
isCustom?: boolean | number
|
||||||
|
/** @deprecated Superseded by objectMetadata.openRecordIn and the workspace member preference; kept one release for API compatibility, no longer read by the frontend. */
|
||||||
openRecordIn?: boolean | number
|
openRecordIn?: boolean | number
|
||||||
kanbanAggregateOperation?: boolean | number
|
kanbanAggregateOperation?: boolean | number
|
||||||
kanbanAggregateOperationFieldMetadataId?: boolean | number
|
kanbanAggregateOperationFieldMetadataId?: boolean | number
|
||||||
@@ -6498,9 +6508,13 @@ export interface DestroyViewFilterInput {
|
|||||||
/** The id of the view filter to destroy. */
|
/** The id of the view filter to destroy. */
|
||||||
id: Scalars['UUID']}
|
id: Scalars['UUID']}
|
||||||
|
|
||||||
export interface CreateViewInput {id?: (Scalars['UUID'] | null),name: Scalars['String'],objectMetadataId: Scalars['UUID'],type?: (ViewType | null),key?: (ViewKey | null),icon: Scalars['String'],position?: (Scalars['Float'] | null),isCompact?: (Scalars['Boolean'] | null),shouldHideEmptyGroups?: (Scalars['Boolean'] | null),kanbanColumnWidth?: (Scalars['Int'] | null),openRecordIn?: (ViewOpenRecordIn | null),kanbanAggregateOperation?: (AggregateOperations | null),kanbanAggregateOperationFieldMetadataId?: (Scalars['UUID'] | null),anyFieldFilterValue?: (Scalars['String'] | null),calendarLayout?: (ViewCalendarLayout | null),calendarFieldMetadataId?: (Scalars['UUID'] | null),calendarEndFieldMetadataId?: (Scalars['UUID'] | null),mainGroupByFieldMetadataId?: (Scalars['UUID'] | null),visibility?: (ViewVisibility | null)}
|
export interface CreateViewInput {id?: (Scalars['UUID'] | null),name: Scalars['String'],objectMetadataId: Scalars['UUID'],type?: (ViewType | null),key?: (ViewKey | null),icon: Scalars['String'],position?: (Scalars['Float'] | null),isCompact?: (Scalars['Boolean'] | null),shouldHideEmptyGroups?: (Scalars['Boolean'] | null),kanbanColumnWidth?: (Scalars['Int'] | null),
|
||||||
|
/** Deprecated: Superseded by objectMetadata.openRecordIn and the workspace member preference; kept one release for API compatibility, no longer read by the frontend. */
|
||||||
|
openRecordIn?: (ViewOpenRecordIn | null),kanbanAggregateOperation?: (AggregateOperations | null),kanbanAggregateOperationFieldMetadataId?: (Scalars['UUID'] | null),anyFieldFilterValue?: (Scalars['String'] | null),calendarLayout?: (ViewCalendarLayout | null),calendarFieldMetadataId?: (Scalars['UUID'] | null),calendarEndFieldMetadataId?: (Scalars['UUID'] | null),mainGroupByFieldMetadataId?: (Scalars['UUID'] | null),visibility?: (ViewVisibility | null)}
|
||||||
|
|
||||||
export interface UpdateViewInput {id?: (Scalars['UUID'] | null),name?: (Scalars['String'] | null),type?: (ViewType | null),icon?: (Scalars['String'] | null),position?: (Scalars['Float'] | null),isCompact?: (Scalars['Boolean'] | null),openRecordIn?: (ViewOpenRecordIn | null),kanbanAggregateOperation?: (AggregateOperations | null),kanbanAggregateOperationFieldMetadataId?: (Scalars['UUID'] | null),anyFieldFilterValue?: (Scalars['String'] | null),calendarLayout?: (ViewCalendarLayout | null),calendarFieldMetadataId?: (Scalars['UUID'] | null),calendarEndFieldMetadataId?: (Scalars['UUID'] | null),visibility?: (ViewVisibility | null),mainGroupByFieldMetadataId?: (Scalars['UUID'] | null),shouldHideEmptyGroups?: (Scalars['Boolean'] | null),kanbanColumnWidth?: (Scalars['Int'] | null)}
|
export interface UpdateViewInput {id?: (Scalars['UUID'] | null),name?: (Scalars['String'] | null),type?: (ViewType | null),icon?: (Scalars['String'] | null),position?: (Scalars['Float'] | null),isCompact?: (Scalars['Boolean'] | null),
|
||||||
|
/** Deprecated: Superseded by objectMetadata.openRecordIn and the workspace member preference; kept one release for API compatibility, no longer read by the frontend. */
|
||||||
|
openRecordIn?: (ViewOpenRecordIn | null),kanbanAggregateOperation?: (AggregateOperations | null),kanbanAggregateOperationFieldMetadataId?: (Scalars['UUID'] | null),anyFieldFilterValue?: (Scalars['String'] | null),calendarLayout?: (ViewCalendarLayout | null),calendarFieldMetadataId?: (Scalars['UUID'] | null),calendarEndFieldMetadataId?: (Scalars['UUID'] | null),visibility?: (ViewVisibility | null),mainGroupByFieldMetadataId?: (Scalars['UUID'] | null),shouldHideEmptyGroups?: (Scalars['Boolean'] | null),kanbanColumnWidth?: (Scalars['Int'] | null)}
|
||||||
|
|
||||||
export interface UpsertViewWidgetInput {
|
export interface UpsertViewWidgetInput {
|
||||||
/** The id of the view widget (page layout widget). */
|
/** The id of the view widget (page layout widget). */
|
||||||
@@ -6518,7 +6532,9 @@ viewSorts?: (UpsertViewWidgetViewSortInput[] | null)}
|
|||||||
|
|
||||||
export interface UpsertViewWidgetViewSettingsInput {
|
export interface UpsertViewWidgetViewSettingsInput {
|
||||||
/** The layout type of the widget view. Only widget view types (TABLE_WIDGET, KANBAN_WIDGET, CALENDAR_WIDGET) are allowed. */
|
/** The layout type of the widget view. Only widget view types (TABLE_WIDGET, KANBAN_WIDGET, CALENDAR_WIDGET) are allowed. */
|
||||||
type?: (ViewType | null),mainGroupByFieldMetadataId?: (Scalars['UUID'] | null),shouldHideEmptyGroups?: (Scalars['Boolean'] | null),openRecordIn?: (ViewOpenRecordIn | null),kanbanAggregateOperation?: (AggregateOperations | null),kanbanAggregateOperationFieldMetadataId?: (Scalars['UUID'] | null),kanbanColumnWidth?: (Scalars['Int'] | null),calendarLayout?: (ViewCalendarLayout | null),calendarFieldMetadataId?: (Scalars['UUID'] | null),calendarEndFieldMetadataId?: (Scalars['UUID'] | null)}
|
type?: (ViewType | null),mainGroupByFieldMetadataId?: (Scalars['UUID'] | null),shouldHideEmptyGroups?: (Scalars['Boolean'] | null),
|
||||||
|
/** Deprecated: Superseded by objectMetadata.openRecordIn and the workspace member preference; kept one release for API compatibility, no longer read by the frontend. */
|
||||||
|
openRecordIn?: (ViewOpenRecordIn | null),kanbanAggregateOperation?: (AggregateOperations | null),kanbanAggregateOperationFieldMetadataId?: (Scalars['UUID'] | null),kanbanColumnWidth?: (Scalars['Int'] | null),calendarLayout?: (ViewCalendarLayout | null),calendarFieldMetadataId?: (Scalars['UUID'] | null),calendarEndFieldMetadataId?: (Scalars['UUID'] | null)}
|
||||||
|
|
||||||
export interface UpsertViewWidgetViewFieldInput {
|
export interface UpsertViewWidgetViewFieldInput {
|
||||||
/** The id of an existing view field to update. */
|
/** The id of an existing view field to update. */
|
||||||
@@ -6652,7 +6668,7 @@ export interface UpdateOneObjectInput {update: UpdateObjectPayload,
|
|||||||
/** The id of the object to update */
|
/** The id of the object to update */
|
||||||
id: Scalars['UUID']}
|
id: Scalars['UUID']}
|
||||||
|
|
||||||
export interface UpdateObjectPayload {labelSingular?: (Scalars['String'] | null),labelPlural?: (Scalars['String'] | null),nameSingular?: (Scalars['String'] | null),namePlural?: (Scalars['String'] | null),description?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),shortcut?: (Scalars['String'] | null),color?: (Scalars['String'] | null),isActive?: (Scalars['Boolean'] | null),labelIdentifierFieldMetadataId?: (Scalars['UUID'] | null),imageIdentifierFieldMetadataId?: (Scalars['UUID'] | null),isLabelSyncedWithName?: (Scalars['Boolean'] | null),isSearchable?: (Scalars['Boolean'] | null)}
|
export interface UpdateObjectPayload {labelSingular?: (Scalars['String'] | null),labelPlural?: (Scalars['String'] | null),nameSingular?: (Scalars['String'] | null),namePlural?: (Scalars['String'] | null),description?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),shortcut?: (Scalars['String'] | null),color?: (Scalars['String'] | null),isActive?: (Scalars['Boolean'] | null),labelIdentifierFieldMetadataId?: (Scalars['UUID'] | null),imageIdentifierFieldMetadataId?: (Scalars['UUID'] | null),isLabelSyncedWithName?: (Scalars['Boolean'] | null),isSearchable?: (Scalars['Boolean'] | null),openRecordIn?: (ObjectOpenRecordIn | null)}
|
||||||
|
|
||||||
export interface CreateOneIndexInput {
|
export interface CreateOneIndexInput {
|
||||||
/** The custom index to create */
|
/** The custom index to create */
|
||||||
@@ -9120,6 +9136,17 @@ export const enumIndexType = {
|
|||||||
GIN: 'GIN' as const
|
GIN: 'GIN' as const
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const enumObjectOpenRecordIn = {
|
||||||
|
SIDE_PANEL: 'SIDE_PANEL' as const,
|
||||||
|
RECORD_PAGE: 'RECORD_PAGE' as const,
|
||||||
|
USER_CHOICE: 'USER_CHOICE' as const
|
||||||
|
}
|
||||||
|
|
||||||
|
export const enumOpenRecordIn = {
|
||||||
|
SIDE_PANEL: 'SIDE_PANEL' as const,
|
||||||
|
RECORD_PAGE: 'RECORD_PAGE' as const
|
||||||
|
}
|
||||||
|
|
||||||
export const enumWorkspaceMemberDateFormatEnum = {
|
export const enumWorkspaceMemberDateFormatEnum = {
|
||||||
SYSTEM: 'SYSTEM' as const,
|
SYSTEM: 'SYSTEM' as const,
|
||||||
MONTH_FIRST: 'MONTH_FIRST' as const,
|
MONTH_FIRST: 'MONTH_FIRST' as const,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -79,6 +79,7 @@ export default defineObject({
|
|||||||
- The `universalIdentifier` must be unique and stable across deployments.
|
- The `universalIdentifier` must be unique and stable across deployments.
|
||||||
- Each field requires a `name`, `type`, `label`, and its own stable `universalIdentifier`.
|
- Each field requires a `name`, `type`, `label`, and its own stable `universalIdentifier`.
|
||||||
- The `fields` array is optional — you can define objects without custom fields.
|
- The `fields` array is optional — you can define objects without custom fields.
|
||||||
|
- `openRecordIn` sets where records of this object open when clicked: `ObjectOpenRecordIn.USER_CHOICE` (the default, following each workspace member's own preference from Settings → Experience), `ObjectOpenRecordIn.SIDE_PANEL`, or `ObjectOpenRecordIn.RECORD_PAGE`. Pin it to `RECORD_PAGE` for records that need a full page to be usable, the way workflows and dashboards do, or to `SIDE_PANEL` for records that only make sense as a quick panel, the way calendar events do.
|
||||||
- Inline fields defined here do **not** need an `objectUniversalIdentifier` — it's inherited from the parent object. Use [`defineField()`](/developers/extend/apps/data/extending-objects) to add fields to objects you don't own.
|
- Inline fields defined here do **not** need an `objectUniversalIdentifier` — it's inherited from the parent object. Use [`defineField()`](/developers/extend/apps/data/extending-objects) to add fields to objects you don't own.
|
||||||
- You can scaffold new objects with `yarn twenty dev:add object`, which guides you through naming, fields, and relationships. See [Architecture → Scaffolding entities](/developers/extend/apps/getting-started/scaffolding).
|
- You can scaffold new objects with `yarn twenty dev:add object`, which guides you through naming, fields, and relationships. See [Architecture → Scaffolding entities](/developers/extend/apps/getting-started/scaffolding).
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export default defineView({
|
|||||||
|----------|--------|-------------|
|
|----------|--------|-------------|
|
||||||
| `type` | `ViewType.TABLE` (default), `ViewType.KANBAN`, `ViewType.CALENDAR` | How records are laid out. (`FIELDS_WIDGET`, `TABLE_WIDGET`, `KANBAN_WIDGET`, and `CALENDAR_WIDGET` also exist but are used internally by page-layout widgets.) |
|
| `type` | `ViewType.TABLE` (default), `ViewType.KANBAN`, `ViewType.CALENDAR` | How records are laid out. (`FIELDS_WIDGET`, `TABLE_WIDGET`, `KANBAN_WIDGET`, and `CALENDAR_WIDGET` also exist but are used internally by page-layout widgets.) |
|
||||||
| `visibility` | `ViewVisibility.WORKSPACE` (default), `ViewVisibility.UNLISTED` | Whether the view is listed for the whole workspace or hidden from pickers. |
|
| `visibility` | `ViewVisibility.WORKSPACE` (default), `ViewVisibility.UNLISTED` | Whether the view is listed for the whole workspace or hidden from pickers. |
|
||||||
| `openRecordIn` | `ViewOpenRecordIn.SIDE_PANEL` (default), `ViewOpenRecordIn.RECORD_PAGE` | Where clicking a record opens it. |
|
| `openRecordIn` | deprecated | No longer read: where records open is now a property of the [object](/developers/extend/apps/data/objects) (`openRecordIn` on `defineObject()`), falling back to each member's own preference. |
|
||||||
| `sorts` | `{ fieldMetadataUniversalIdentifier, direction: ViewSortDirection.ASC \| DESC }[]` | Default sort order. |
|
| `sorts` | `{ fieldMetadataUniversalIdentifier, direction: ViewSortDirection.ASC \| DESC }[]` | Default sort order. |
|
||||||
| `isCompact` | `boolean` | Compact row display. |
|
| `isCompact` | `boolean` | Compact row display. |
|
||||||
| `mainGroupByFieldMetadataUniversalIdentifier` + `shouldHideEmptyGroups` | — | Group records (e.g. kanban columns) by a field. |
|
| `mainGroupByFieldMetadataUniversalIdentifier` + `shouldHideEmptyGroups` | — | Group records (e.g. kanban columns) by a field. |
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
+2
-2
@@ -23,7 +23,7 @@ import { t } from '@lingui/core/macro';
|
|||||||
import { useStore } from 'jotai';
|
import { useStore } from 'jotai';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import { IconBrowserMaximize } from 'twenty-ui/icon';
|
import { IconAddressBook } from 'twenty-ui/icon';
|
||||||
import { Button } from 'twenty-ui/input';
|
import { Button } from 'twenty-ui/input';
|
||||||
import { getOsControlSymbol } from 'twenty-ui/utilities';
|
import { getOsControlSymbol } from 'twenty-ui/utilities';
|
||||||
import { useNavigateApp } from '~/hooks/useNavigateApp';
|
import { useNavigateApp } from '~/hooks/useNavigateApp';
|
||||||
@@ -143,7 +143,7 @@ export const RecordShowSidePanelOpenRecordButton = ({
|
|||||||
variant="primary"
|
variant="primary"
|
||||||
accent="blue"
|
accent="blue"
|
||||||
size="small"
|
size="small"
|
||||||
Icon={IconBrowserMaximize}
|
Icon={IconAddressBook}
|
||||||
hotkeys={[getOsControlSymbol(), '⏎']}
|
hotkeys={[getOsControlSymbol(), '⏎']}
|
||||||
onClick={handleOpenRecord}
|
onClick={handleOpenRecord}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+10
-1
@@ -9,6 +9,15 @@ import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainCo
|
|||||||
import { contextStoreRecordShowParentViewComponentState } from '@/context-store/states/contextStoreRecordShowParentViewComponentState';
|
import { contextStoreRecordShowParentViewComponentState } from '@/context-store/states/contextStoreRecordShowParentViewComponentState';
|
||||||
import { useFrontComponentExecutionContext } from '@/front-components/hooks/useFrontComponentExecutionContext';
|
import { useFrontComponentExecutionContext } from '@/front-components/hooks/useFrontComponentExecutionContext';
|
||||||
|
|
||||||
|
jest.mock('@/object-metadata/hooks/useObjectMetadataItems', () => ({
|
||||||
|
useObjectMetadataItems: () => ({
|
||||||
|
objectMetadataItems: [
|
||||||
|
{ nameSingular: 'workflow', openRecordIn: 'RECORD_PAGE' },
|
||||||
|
{ nameSingular: 'lead', openRecordIn: 'USER_CHOICE' },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
const mockNavigateApp = jest.fn();
|
const mockNavigateApp = jest.fn();
|
||||||
const mockRequestAccessTokenRefresh = jest.fn();
|
const mockRequestAccessTokenRefresh = jest.fn();
|
||||||
const mockOpenConfirmationModal = jest.fn();
|
const mockOpenConfirmationModal = jest.fn();
|
||||||
@@ -486,7 +495,7 @@ describe('useFrontComponentExecutionContext', () => {
|
|||||||
expect(mockOpenRecordInSidePanel).not.toHaveBeenCalled();
|
expect(mockOpenRecordInSidePanel).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fall back to full-page navigation when the object cannot open in the side panel', async () => {
|
it('should fall back to full-page navigation when the object is pinned to the record page', async () => {
|
||||||
const { result } = renderUseFrontComponentExecutionContext({
|
const { result } = renderUseFrontComponentExecutionContext({
|
||||||
frontComponentId: FRONT_COMPONENT_ID,
|
frontComponentId: FRONT_COMPONENT_ID,
|
||||||
});
|
});
|
||||||
|
|||||||
+17
-2
@@ -1,3 +1,5 @@
|
|||||||
|
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||||
|
import { resolveOpenRecordIn } from '@/object-record/record-index/utils/resolveOpenRecordIn';
|
||||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||||
import { isNonEmptyString } from '@sniptt/guards';
|
import { isNonEmptyString } from '@sniptt/guards';
|
||||||
import { useLingui } from '@lingui/react/macro';
|
import { useLingui } from '@lingui/react/macro';
|
||||||
@@ -8,6 +10,8 @@ import {
|
|||||||
} from 'twenty-front-component-renderer';
|
} from 'twenty-front-component-renderer';
|
||||||
import {
|
import {
|
||||||
AppPath,
|
AppPath,
|
||||||
|
ObjectOpenRecordIn,
|
||||||
|
OpenRecordIn,
|
||||||
SidePanelPages,
|
SidePanelPages,
|
||||||
type EnqueueSnackbarParams,
|
type EnqueueSnackbarParams,
|
||||||
} from 'twenty-shared/types';
|
} from 'twenty-shared/types';
|
||||||
@@ -21,7 +25,6 @@ import { commandMenuItemProgressFamilyState } from '@/command-menu-item/states/c
|
|||||||
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
|
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
|
||||||
import { contextStoreRecordShowParentViewComponentState } from '@/context-store/states/contextStoreRecordShowParentViewComponentState';
|
import { contextStoreRecordShowParentViewComponentState } from '@/context-store/states/contextStoreRecordShowParentViewComponentState';
|
||||||
import { useRequestApplicationTokenRefresh } from '@/front-components/hooks/useRequestApplicationTokenRefresh';
|
import { useRequestApplicationTokenRefresh } from '@/front-components/hooks/useRequestApplicationTokenRefresh';
|
||||||
import { canOpenObjectInSidePanel } from '@/object-record/utils/canOpenObjectInSidePanel';
|
|
||||||
import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel';
|
import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel';
|
||||||
import { useOpenComposeEmailInSidePanel } from '@/side-panel/hooks/useOpenComposeEmailInSidePanel';
|
import { useOpenComposeEmailInSidePanel } from '@/side-panel/hooks/useOpenComposeEmailInSidePanel';
|
||||||
import { useOpenFrontComponentInSidePanel } from '@/side-panel/hooks/useOpenFrontComponentInSidePanel';
|
import { useOpenFrontComponentInSidePanel } from '@/side-panel/hooks/useOpenFrontComponentInSidePanel';
|
||||||
@@ -73,6 +76,7 @@ export const useFrontComponentExecutionContext = ({
|
|||||||
const { openComposeEmailInSidePanel } = useOpenComposeEmailInSidePanel();
|
const { openComposeEmailInSidePanel } = useOpenComposeEmailInSidePanel();
|
||||||
const { openFrontComponentInSidePanel } = useOpenFrontComponentInSidePanel();
|
const { openFrontComponentInSidePanel } = useOpenFrontComponentInSidePanel();
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
const { objectMetadataItems } = useObjectMetadataItems();
|
||||||
const setSidePanelSearch = useSetAtomState(sidePanelSearchState);
|
const setSidePanelSearch = useSetAtomState(sidePanelSearchState);
|
||||||
const { getIcon } = useIcons();
|
const { getIcon } = useIcons();
|
||||||
const unmountEngineCommand = useUnmountCommand();
|
const unmountEngineCommand = useUnmountCommand();
|
||||||
@@ -133,7 +137,18 @@ export const useFrontComponentExecutionContext = ({
|
|||||||
const { recordId, objectNameSingular, tab, resetNavigationStack } =
|
const { recordId, objectNameSingular, tab, resetNavigationStack } =
|
||||||
params;
|
params;
|
||||||
|
|
||||||
if (isMobile || !canOpenObjectInSidePanel(objectNameSingular)) {
|
const objectMetadataItem = objectMetadataItems.find(
|
||||||
|
(item) => item.nameSingular === objectNameSingular,
|
||||||
|
);
|
||||||
|
|
||||||
|
const resolvedOpenRecordIn = resolveOpenRecordIn({
|
||||||
|
objectOpenRecordIn:
|
||||||
|
objectMetadataItem?.openRecordIn ?? ObjectOpenRecordIn.USER_CHOICE,
|
||||||
|
openRecordInPreference: OpenRecordIn.SIDE_PANEL,
|
||||||
|
canDisplaySidePanel: !isMobile,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (resolvedOpenRecordIn === OpenRecordIn.RECORD_PAGE) {
|
||||||
if (isDefined(tab)) {
|
if (isDefined(tab)) {
|
||||||
setRecordPageActiveTabId({
|
setRecordPageActiveTabId({
|
||||||
recordId,
|
recordId,
|
||||||
|
|||||||
+2
@@ -1,3 +1,4 @@
|
|||||||
|
import { toOpenRecordInPreference } from '@/workspace-member/utils/toOpenRecordInPreference';
|
||||||
import { useHasAccessTokenPair } from '@/auth/hooks/useHasAccessTokenPair';
|
import { useHasAccessTokenPair } from '@/auth/hooks/useHasAccessTokenPair';
|
||||||
import { availableWorkspacesState } from '@/auth/states/availableWorkspacesState';
|
import { availableWorkspacesState } from '@/auth/states/availableWorkspacesState';
|
||||||
import { currentUserState } from '@/auth/states/currentUserState';
|
import { currentUserState } from '@/auth/states/currentUserState';
|
||||||
@@ -129,6 +130,7 @@ export const UserMetadataProviderInitialEffect = () => {
|
|||||||
return {
|
return {
|
||||||
...workspaceMember,
|
...workspaceMember,
|
||||||
colorScheme: (workspaceMember.colorScheme as ColorScheme) ?? 'System',
|
colorScheme: (workspaceMember.colorScheme as ColorScheme) ?? 'System',
|
||||||
|
openRecordIn: toOpenRecordInPreference(workspaceMember.openRecordIn),
|
||||||
locale:
|
locale:
|
||||||
(workspaceMember.locale as keyof typeof APP_LOCALES) ?? SOURCE_LOCALE,
|
(workspaceMember.locale as keyof typeof APP_LOCALES) ?? SOURCE_LOCALE,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export const OBJECT_METADATA_FRAGMENT = gql`
|
|||||||
shortcut
|
shortcut
|
||||||
isLabelSyncedWithName
|
isLabelSyncedWithName
|
||||||
isSearchable
|
isSearchable
|
||||||
|
openRecordIn
|
||||||
duplicateCriteria
|
duplicateCriteria
|
||||||
searchFieldMetadataList {
|
searchFieldMetadataList {
|
||||||
id
|
id
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export const CREATE_ONE_OBJECT_METADATA_ITEM = gql`
|
|||||||
isUIEditable
|
isUIEditable
|
||||||
isUICreatable
|
isUICreatable
|
||||||
isSearchable
|
isSearchable
|
||||||
|
openRecordIn
|
||||||
shortcut
|
shortcut
|
||||||
duplicateCriteria
|
duplicateCriteria
|
||||||
createdAt
|
createdAt
|
||||||
@@ -205,6 +206,7 @@ export const UPDATE_ONE_OBJECT_METADATA_ITEM = gql`
|
|||||||
color
|
color
|
||||||
isActive
|
isActive
|
||||||
isSearchable
|
isSearchable
|
||||||
|
openRecordIn
|
||||||
createdAt
|
createdAt
|
||||||
updatedAt
|
updatedAt
|
||||||
labelIdentifierFieldMetadataId
|
labelIdentifierFieldMetadataId
|
||||||
@@ -228,6 +230,7 @@ export const DELETE_ONE_OBJECT_METADATA_ITEM = gql`
|
|||||||
color
|
color
|
||||||
isActive
|
isActive
|
||||||
isSearchable
|
isSearchable
|
||||||
|
openRecordIn
|
||||||
createdAt
|
createdAt
|
||||||
updatedAt
|
updatedAt
|
||||||
labelIdentifierFieldMetadataId
|
labelIdentifierFieldMetadataId
|
||||||
|
|||||||
+3
@@ -1,4 +1,5 @@
|
|||||||
import { gql } from '@apollo/client';
|
import { gql } from '@apollo/client';
|
||||||
|
import { ObjectOpenRecordIn } from 'twenty-shared/types';
|
||||||
|
|
||||||
export const query = gql`
|
export const query = gql`
|
||||||
mutation DeleteOneObjectMetadataItem($idToDelete: UUID!) {
|
mutation DeleteOneObjectMetadataItem($idToDelete: UUID!) {
|
||||||
@@ -13,6 +14,7 @@ export const query = gql`
|
|||||||
color
|
color
|
||||||
isActive
|
isActive
|
||||||
isSearchable
|
isSearchable
|
||||||
|
openRecordIn
|
||||||
createdAt
|
createdAt
|
||||||
updatedAt
|
updatedAt
|
||||||
labelIdentifierFieldMetadataId
|
labelIdentifierFieldMetadataId
|
||||||
@@ -36,6 +38,7 @@ export const responseData = {
|
|||||||
color: null,
|
color: null,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
isSearchable: false,
|
isSearchable: false,
|
||||||
|
openRecordIn: ObjectOpenRecordIn.USER_CHOICE,
|
||||||
createdAt: '',
|
createdAt: '',
|
||||||
updatedAt: '',
|
updatedAt: '',
|
||||||
labelIdentifierFieldMetadataId: '20202020-72ba-4e11-a36d-e17b544541e1',
|
labelIdentifierFieldMetadataId: '20202020-72ba-4e11-a36d-e17b544541e1',
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { useOpenRecordInSidePanel } from '@/side-panel/hooks/useOpenRecordInSidePanel';
|
import { useOpenRecordInSidePanel } from '@/side-panel/hooks/useOpenRecordInSidePanel';
|
||||||
import { CoreObjectNameSingular } from 'twenty-shared/types';
|
|
||||||
import { getLinkToShowPage } from '@/object-metadata/utils/getLinkToShowPage';
|
import { getLinkToShowPage } from '@/object-metadata/utils/getLinkToShowPage';
|
||||||
import { useRecordChipData } from '@/object-record/hooks/useRecordChipData';
|
import { useRecordChipData } from '@/object-record/hooks/useRecordChipData';
|
||||||
import { useResolveOpenRecordIn } from '@/object-record/record-index/hooks/useResolveOpenRecordIn';
|
import { useResolveOpenRecordIn } from '@/object-record/record-index/hooks/useResolveOpenRecordIn';
|
||||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
import { ViewOpenRecordIn } from '~/generated-metadata/graphql';
|
import { CoreObjectNameSingular, OpenRecordIn } from 'twenty-shared/types';
|
||||||
import { getAbsoluteImageUrl } from '~/utils/image/getAbsoluteImageUrl';
|
import { getAbsoluteImageUrl } from '~/utils/image/getAbsoluteImageUrl';
|
||||||
import { t } from '@lingui/core/macro';
|
import { t } from '@lingui/core/macro';
|
||||||
import { type MouseEvent } from 'react';
|
import { type MouseEvent } from 'react';
|
||||||
@@ -60,7 +59,7 @@ export const RecordChip = ({
|
|||||||
|
|
||||||
const handleCustomClick = isDefined(onClick)
|
const handleCustomClick = isDefined(onClick)
|
||||||
? onClick
|
? onClick
|
||||||
: openRecordIn === ViewOpenRecordIn.SIDE_PANEL
|
: openRecordIn === OpenRecordIn.SIDE_PANEL
|
||||||
? (_event: MouseEvent<HTMLElement>) => {
|
? (_event: MouseEvent<HTMLElement>) => {
|
||||||
openRecordInSidePanel({
|
openRecordInSidePanel({
|
||||||
recordId: record.id,
|
recordId: record.id,
|
||||||
|
|||||||
+2
@@ -1,3 +1,4 @@
|
|||||||
|
import { ObjectOpenRecordIn } from 'twenty-shared/types';
|
||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
||||||
@@ -56,6 +57,7 @@ const mockObjectMetadataItem: EnrichedObjectMetadataItem = {
|
|||||||
isSystem: false,
|
isSystem: false,
|
||||||
isUIEditable: true,
|
isUIEditable: true,
|
||||||
isUICreatable: true,
|
isUICreatable: true,
|
||||||
|
openRecordIn: ObjectOpenRecordIn.USER_CHOICE,
|
||||||
};
|
};
|
||||||
|
|
||||||
const Wrapper = getJestMetadataAndApolloMocksWrapper({
|
const Wrapper = getJestMetadataAndApolloMocksWrapper({
|
||||||
|
|||||||
-3
@@ -7,7 +7,6 @@ import { ObjectOptionsDropdownFieldsContent } from '@/object-record/object-optio
|
|||||||
import { ObjectOptionsDropdownHiddenFieldsContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownHiddenFieldsContent';
|
import { ObjectOptionsDropdownHiddenFieldsContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownHiddenFieldsContent';
|
||||||
import { ObjectOptionsDropdownHiddenRecordGroupsContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownHiddenRecordGroupsContent';
|
import { ObjectOptionsDropdownHiddenRecordGroupsContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownHiddenRecordGroupsContent';
|
||||||
import { ObjectOptionsDropdownLayoutContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownLayoutContent';
|
import { ObjectOptionsDropdownLayoutContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownLayoutContent';
|
||||||
import { ObjectOptionsDropdownLayoutOpenInContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownLayoutOpenInContent';
|
|
||||||
import { ObjectOptionsDropdownMenuContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownMenuContent';
|
import { ObjectOptionsDropdownMenuContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownMenuContent';
|
||||||
import { ObjectOptionsDropdownRecordGroupFieldsContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupFieldsContent';
|
import { ObjectOptionsDropdownRecordGroupFieldsContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupFieldsContent';
|
||||||
import { ObjectOptionsDropdownRecordGroupsContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupsContent';
|
import { ObjectOptionsDropdownRecordGroupsContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupsContent';
|
||||||
@@ -26,8 +25,6 @@ export const ObjectOptionsDropdownContent = () => {
|
|||||||
switch (currentContentId) {
|
switch (currentContentId) {
|
||||||
case 'layout':
|
case 'layout':
|
||||||
return <ObjectOptionsDropdownLayoutContent />;
|
return <ObjectOptionsDropdownLayoutContent />;
|
||||||
case 'layoutOpenIn':
|
|
||||||
return <ObjectOptionsDropdownLayoutOpenInContent />;
|
|
||||||
case 'fields':
|
case 'fields':
|
||||||
return <ObjectOptionsDropdownFieldsContent />;
|
return <ObjectOptionsDropdownFieldsContent />;
|
||||||
case 'hiddenFields':
|
case 'hiddenFields':
|
||||||
|
|||||||
-30
@@ -34,8 +34,6 @@ import {
|
|||||||
IconCalendarWeek,
|
IconCalendarWeek,
|
||||||
IconChevronLeft,
|
IconChevronLeft,
|
||||||
IconLayoutList,
|
IconLayoutList,
|
||||||
IconLayoutNavbar,
|
|
||||||
IconLayoutSidebarRight,
|
|
||||||
IconTable,
|
IconTable,
|
||||||
} from 'twenty-ui/icon';
|
} from 'twenty-ui/icon';
|
||||||
import { OverflowingTextWithTooltip } from 'twenty-ui/surfaces';
|
import { OverflowingTextWithTooltip } from 'twenty-ui/surfaces';
|
||||||
@@ -43,7 +41,6 @@ import { MenuItem, MenuItemSelect, MenuItemToggle } from 'twenty-ui/navigation';
|
|||||||
import {
|
import {
|
||||||
FeatureFlagKey,
|
FeatureFlagKey,
|
||||||
ViewCalendarLayout,
|
ViewCalendarLayout,
|
||||||
ViewOpenRecordIn,
|
|
||||||
} from '~/generated-metadata/graphql';
|
} from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
export const ObjectOptionsDropdownLayoutContent = () => {
|
export const ObjectOptionsDropdownLayoutContent = () => {
|
||||||
@@ -129,7 +126,6 @@ export const ObjectOptionsDropdownLayoutContent = () => {
|
|||||||
ViewType.TABLE,
|
ViewType.TABLE,
|
||||||
...(isDefaultView ? [] : [ViewType.KANBAN]),
|
...(isDefaultView ? [] : [ViewType.KANBAN]),
|
||||||
...(!isDefaultView ? [ViewType.CALENDAR] : []),
|
...(!isDefaultView ? [ViewType.CALENDAR] : []),
|
||||||
ViewOpenRecordIn.SIDE_PANEL,
|
|
||||||
...(currentView?.type === ViewType.KANBAN ? ['Group'] : []),
|
...(currentView?.type === ViewType.KANBAN ? ['Group'] : []),
|
||||||
...(currentView?.type === ViewType.CALENDAR
|
...(currentView?.type === ViewType.CALENDAR
|
||||||
? [
|
? [
|
||||||
@@ -285,32 +281,6 @@ export const ObjectOptionsDropdownLayoutContent = () => {
|
|||||||
</SelectableListItem>
|
</SelectableListItem>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<SelectableListItem
|
|
||||||
itemId={ViewOpenRecordIn.SIDE_PANEL}
|
|
||||||
onEnter={() => {
|
|
||||||
onContentChange('layoutOpenIn');
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MenuItem
|
|
||||||
focused={selectedItemId === ViewOpenRecordIn.SIDE_PANEL}
|
|
||||||
LeftIcon={
|
|
||||||
currentView?.openRecordIn === ViewOpenRecordIn.SIDE_PANEL
|
|
||||||
? IconLayoutSidebarRight
|
|
||||||
: IconLayoutNavbar
|
|
||||||
}
|
|
||||||
text={t`Open in`}
|
|
||||||
onClick={() => {
|
|
||||||
onContentChange('layoutOpenIn');
|
|
||||||
}}
|
|
||||||
contextualText={
|
|
||||||
currentView?.openRecordIn === ViewOpenRecordIn.SIDE_PANEL
|
|
||||||
? t`Side Panel`
|
|
||||||
: t`Record Page`
|
|
||||||
}
|
|
||||||
contextualTextPosition="right"
|
|
||||||
hasSubMenu
|
|
||||||
/>
|
|
||||||
</SelectableListItem>
|
|
||||||
{currentView?.type === ViewType.KANBAN && (
|
{currentView?.type === ViewType.KANBAN && (
|
||||||
<SelectableListItem
|
<SelectableListItem
|
||||||
itemId="Group"
|
itemId="Group"
|
||||||
|
|||||||
-121
@@ -1,121 +0,0 @@
|
|||||||
import { OBJECT_OPTIONS_DROPDOWN_ID } from '@/object-record/object-options-dropdown/constants/ObjectOptionsDropdownId';
|
|
||||||
import { useObjectOptionsDropdown } from '@/object-record/object-options-dropdown/hooks/useObjectOptionsDropdown';
|
|
||||||
import { useUpdateObjectViewOptions } from '@/object-record/object-options-dropdown/hooks/useUpdateObjectViewOptions';
|
|
||||||
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
|
|
||||||
import { canOpenObjectInSidePanel } from '@/object-record/utils/canOpenObjectInSidePanel';
|
|
||||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
|
||||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
|
|
||||||
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
|
|
||||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
|
||||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
|
||||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
|
||||||
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
|
|
||||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
|
||||||
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
|
|
||||||
import { ViewOpenRecordIn } from '~/generated-metadata/graphql';
|
|
||||||
import { t } from '@lingui/core/macro';
|
|
||||||
import {
|
|
||||||
IconChevronLeft,
|
|
||||||
IconLayoutNavbar,
|
|
||||||
IconLayoutSidebarRight,
|
|
||||||
} from 'twenty-ui/icon';
|
|
||||||
import { MenuItemSelect } from 'twenty-ui/navigation';
|
|
||||||
|
|
||||||
export const ObjectOptionsDropdownLayoutOpenInContent = () => {
|
|
||||||
const { onContentChange } = useObjectOptionsDropdown();
|
|
||||||
const { currentView } = useGetCurrentViewOnly();
|
|
||||||
const { setAndPersistOpenRecordIn } = useUpdateObjectViewOptions();
|
|
||||||
const { objectMetadataItem } = useRecordIndexContextOrThrow();
|
|
||||||
const canOpenInSidePanel = canOpenObjectInSidePanel(
|
|
||||||
objectMetadataItem.nameSingular,
|
|
||||||
);
|
|
||||||
|
|
||||||
const selectedItemId = useAtomComponentStateValue(
|
|
||||||
selectedItemIdComponentState,
|
|
||||||
OBJECT_OPTIONS_DROPDOWN_ID,
|
|
||||||
);
|
|
||||||
|
|
||||||
const selectableItemIdArray = [
|
|
||||||
ViewOpenRecordIn.SIDE_PANEL,
|
|
||||||
ViewOpenRecordIn.RECORD_PAGE,
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DropdownContent>
|
|
||||||
<DropdownMenuHeader
|
|
||||||
StartComponent={
|
|
||||||
<DropdownMenuHeaderLeftComponent
|
|
||||||
onClick={() => onContentChange('layout')}
|
|
||||||
Icon={IconChevronLeft}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t`Open in`}
|
|
||||||
</DropdownMenuHeader>
|
|
||||||
<DropdownMenuItemsContainer>
|
|
||||||
<SelectableList
|
|
||||||
selectableListInstanceId={OBJECT_OPTIONS_DROPDOWN_ID}
|
|
||||||
focusId={OBJECT_OPTIONS_DROPDOWN_ID}
|
|
||||||
selectableItemIdArray={selectableItemIdArray}
|
|
||||||
>
|
|
||||||
<SelectableListItem
|
|
||||||
itemId={ViewOpenRecordIn.SIDE_PANEL}
|
|
||||||
onEnter={() => {
|
|
||||||
if (!canOpenInSidePanel) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setAndPersistOpenRecordIn(
|
|
||||||
ViewOpenRecordIn.SIDE_PANEL,
|
|
||||||
currentView,
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MenuItemSelect
|
|
||||||
LeftIcon={IconLayoutSidebarRight}
|
|
||||||
text={t`Side Panel`}
|
|
||||||
selected={
|
|
||||||
currentView?.openRecordIn === ViewOpenRecordIn.SIDE_PANEL
|
|
||||||
}
|
|
||||||
focused={selectedItemId === ViewOpenRecordIn.SIDE_PANEL}
|
|
||||||
onClick={() => {
|
|
||||||
if (!canOpenInSidePanel) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setAndPersistOpenRecordIn(
|
|
||||||
ViewOpenRecordIn.SIDE_PANEL,
|
|
||||||
currentView,
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
disabled={!canOpenInSidePanel}
|
|
||||||
/>
|
|
||||||
</SelectableListItem>
|
|
||||||
<SelectableListItem
|
|
||||||
itemId={ViewOpenRecordIn.RECORD_PAGE}
|
|
||||||
onEnter={() =>
|
|
||||||
setAndPersistOpenRecordIn(
|
|
||||||
ViewOpenRecordIn.RECORD_PAGE,
|
|
||||||
currentView,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<MenuItemSelect
|
|
||||||
LeftIcon={IconLayoutNavbar}
|
|
||||||
text={t`Record Page`}
|
|
||||||
selected={
|
|
||||||
currentView?.openRecordIn === ViewOpenRecordIn.RECORD_PAGE
|
|
||||||
}
|
|
||||||
onClick={() =>
|
|
||||||
setAndPersistOpenRecordIn(
|
|
||||||
ViewOpenRecordIn.RECORD_PAGE,
|
|
||||||
currentView,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
focused={selectedItemId === ViewOpenRecordIn.RECORD_PAGE}
|
|
||||||
/>
|
|
||||||
</SelectableListItem>
|
|
||||||
</SelectableList>
|
|
||||||
</DropdownMenuItemsContainer>
|
|
||||||
</DropdownContent>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
-12
@@ -1,7 +1,6 @@
|
|||||||
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
|
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
|
||||||
import { useUpdateCurrentView } from '@/views/hooks/useUpdateCurrentView';
|
import { useUpdateCurrentView } from '@/views/hooks/useUpdateCurrentView';
|
||||||
import { type GraphQLView } from '@/views/types/GraphQLView';
|
import { type GraphQLView } from '@/views/types/GraphQLView';
|
||||||
import { type ViewOpenRecordIn } from '~/generated-metadata/graphql';
|
|
||||||
import { viewPickerInputNameComponentState } from '@/views/view-picker/states/viewPickerInputNameComponentState';
|
import { viewPickerInputNameComponentState } from '@/views/view-picker/states/viewPickerInputNameComponentState';
|
||||||
import { viewPickerSelectedIconComponentState } from '@/views/view-picker/states/viewPickerSelectedIconComponentState';
|
import { viewPickerSelectedIconComponentState } from '@/views/view-picker/states/viewPickerSelectedIconComponentState';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
@@ -17,16 +16,6 @@ export const useUpdateObjectViewOptions = () => {
|
|||||||
|
|
||||||
const { updateCurrentView } = useUpdateCurrentView();
|
const { updateCurrentView } = useUpdateCurrentView();
|
||||||
|
|
||||||
const setAndPersistOpenRecordIn = useCallback(
|
|
||||||
(openRecordIn: ViewOpenRecordIn, view: GraphQLView | undefined) => {
|
|
||||||
if (!view) return;
|
|
||||||
updateCurrentView({
|
|
||||||
openRecordIn,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[updateCurrentView],
|
|
||||||
);
|
|
||||||
|
|
||||||
const setAndPersistViewName = useCallback(
|
const setAndPersistViewName = useCallback(
|
||||||
(viewName: string, view: GraphQLView | undefined) => {
|
(viewName: string, view: GraphQLView | undefined) => {
|
||||||
if (!view) return;
|
if (!view) return;
|
||||||
@@ -50,7 +39,6 @@ export const useUpdateObjectViewOptions = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
setAndPersistOpenRecordIn,
|
|
||||||
setAndPersistViewName,
|
setAndPersistViewName,
|
||||||
setAndPersistViewIcon,
|
setAndPersistViewIcon,
|
||||||
};
|
};
|
||||||
|
|||||||
-1
@@ -1,6 +1,5 @@
|
|||||||
export type ObjectOptionsContentId =
|
export type ObjectOptionsContentId =
|
||||||
| 'layout'
|
| 'layout'
|
||||||
| 'layoutOpenIn'
|
|
||||||
| 'fields'
|
| 'fields'
|
||||||
| 'hiddenFields'
|
| 'hiddenFields'
|
||||||
| 'recordGroups'
|
| 'recordGroups'
|
||||||
|
|||||||
+7
-1
@@ -1,6 +1,9 @@
|
|||||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
||||||
import { type RecordGqlOperationOrderBy } from 'twenty-shared/types';
|
import {
|
||||||
|
type RecordGqlOperationOrderBy,
|
||||||
|
ObjectOpenRecordIn,
|
||||||
|
} from 'twenty-shared/types';
|
||||||
import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
|
import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy';
|
||||||
import { type RecordSort } from '@/object-record/record-sort/types/RecordSort';
|
import { type RecordSort } from '@/object-record/record-sort/types/RecordSort';
|
||||||
import { type EachTestingContext } from 'twenty-shared/testing';
|
import { type EachTestingContext } from 'twenty-shared/testing';
|
||||||
@@ -40,6 +43,7 @@ const objectMetadataItemWithPositionField: EnrichedObjectMetadataItem = {
|
|||||||
isSystem: false,
|
isSystem: false,
|
||||||
isUIEditable: true,
|
isUIEditable: true,
|
||||||
isUICreatable: true,
|
isUICreatable: true,
|
||||||
|
openRecordIn: ObjectOpenRecordIn.USER_CHOICE,
|
||||||
isRemote: false,
|
isRemote: false,
|
||||||
isSearchable: false,
|
isSearchable: false,
|
||||||
labelPlural: 'object1s',
|
labelPlural: 'object1s',
|
||||||
@@ -203,6 +207,7 @@ describe('turnSortsIntoOrderBy', () => {
|
|||||||
isSystem: false,
|
isSystem: false,
|
||||||
isUIEditable: true,
|
isUIEditable: true,
|
||||||
isUICreatable: true,
|
isUICreatable: true,
|
||||||
|
openRecordIn: ObjectOpenRecordIn.USER_CHOICE,
|
||||||
isRemote: false,
|
isRemote: false,
|
||||||
isSearchable: false,
|
isSearchable: false,
|
||||||
labelPlural: 'Companies',
|
labelPlural: 'Companies',
|
||||||
@@ -254,6 +259,7 @@ describe('turnSortsIntoOrderBy', () => {
|
|||||||
isSystem: false,
|
isSystem: false,
|
||||||
isUIEditable: true,
|
isUIEditable: true,
|
||||||
isUICreatable: true,
|
isUICreatable: true,
|
||||||
|
openRecordIn: ObjectOpenRecordIn.USER_CHOICE,
|
||||||
isRemote: false,
|
isRemote: false,
|
||||||
isSearchable: false,
|
isSearchable: false,
|
||||||
labelPlural: 'People',
|
labelPlural: 'People',
|
||||||
|
|||||||
+2
-2
@@ -16,7 +16,7 @@ import { useAtomComponentFamilyState } from '@/ui/utilities/state/jotai/hooks/us
|
|||||||
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
|
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
|
||||||
import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
|
import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
|
||||||
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
|
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
|
||||||
import { ViewOpenRecordIn } from '~/generated-metadata/graphql';
|
import { OpenRecordIn } from 'twenty-shared/types';
|
||||||
import { styled } from '@linaria/react';
|
import { styled } from '@linaria/react';
|
||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||||
@@ -76,7 +76,7 @@ export const RecordBoardCardHeader = () => {
|
|||||||
const recordStore = useAtomFamilyStateValue(recordStoreFamilyState, recordId);
|
const recordStore = useAtomFamilyStateValue(recordStoreFamilyState, recordId);
|
||||||
|
|
||||||
const triggerEvent =
|
const triggerEvent =
|
||||||
openRecordIn === ViewOpenRecordIn.SIDE_PANEL || isTouchDevice
|
openRecordIn === OpenRecordIn.SIDE_PANEL || isTouchDevice
|
||||||
? 'CLICK'
|
? 'CLICK'
|
||||||
: 'MOUSE_DOWN';
|
: 'MOUSE_DOWN';
|
||||||
|
|
||||||
|
|||||||
+2
@@ -1,3 +1,4 @@
|
|||||||
|
import { ObjectOpenRecordIn } from 'twenty-shared/types';
|
||||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
||||||
import { buildRecordGqlFieldsAggregateForView } from '@/object-record/record-board/record-board-column/utils/buildRecordGqlFieldsAggregateForView';
|
import { buildRecordGqlFieldsAggregateForView } from '@/object-record/record-board/record-board-column/utils/buildRecordGqlFieldsAggregateForView';
|
||||||
@@ -40,6 +41,7 @@ describe('buildRecordGqlFieldsAggregateForView', () => {
|
|||||||
isSystem: false,
|
isSystem: false,
|
||||||
isUIEditable: true,
|
isUIEditable: true,
|
||||||
isUICreatable: true,
|
isUICreatable: true,
|
||||||
|
openRecordIn: ObjectOpenRecordIn.USER_CHOICE,
|
||||||
isRemote: false,
|
isRemote: false,
|
||||||
isSearchable: false,
|
isSearchable: false,
|
||||||
labelIdentifierFieldMetadataId: '06b33746-5293-4d07-9f7f-ebf5ad396064',
|
labelIdentifierFieldMetadataId: '06b33746-5293-4d07-9f7f-ebf5ad396064',
|
||||||
|
|||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
import { OpenRecordIn } from 'twenty-shared/types';
|
||||||
|
|
||||||
|
export const DEFAULT_OPEN_RECORD_IN_PREFERENCE = OpenRecordIn.SIDE_PANEL;
|
||||||
-5
@@ -1,5 +0,0 @@
|
|||||||
import { ViewOpenRecordIn } from '~/generated-metadata/graphql';
|
|
||||||
|
|
||||||
// Used where no view is in scope, so there is no setting to honour: a record
|
|
||||||
// chip in the command menu or in a mention has no list behind it.
|
|
||||||
export const DEFAULT_VIEW_OPEN_RECORD_IN = ViewOpenRecordIn.SIDE_PANEL;
|
|
||||||
+48
-47
@@ -1,11 +1,11 @@
|
|||||||
import { renderHook } from '@testing-library/react';
|
import { renderHook } from '@testing-library/react';
|
||||||
import { Provider as JotaiProvider } from 'jotai';
|
import { Provider as JotaiProvider } from 'jotai';
|
||||||
|
|
||||||
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
|
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||||
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
|
|
||||||
import { useResolveOpenRecordIn } from '@/object-record/record-index/hooks/useResolveOpenRecordIn';
|
import { useResolveOpenRecordIn } from '@/object-record/record-index/hooks/useResolveOpenRecordIn';
|
||||||
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
|
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
|
||||||
import { ViewOpenRecordIn } from '~/generated-metadata/graphql';
|
import { act } from 'react';
|
||||||
|
import { ObjectOpenRecordIn, OpenRecordIn } from 'twenty-shared/types';
|
||||||
|
|
||||||
jest.mock('react-responsive', () => ({
|
jest.mock('react-responsive', () => ({
|
||||||
useMediaQuery: jest.fn().mockReturnValue(false),
|
useMediaQuery: jest.fn().mockReturnValue(false),
|
||||||
@@ -22,65 +22,66 @@ const mockUseAtomFamilySelectorValue = jest.requireMock(
|
|||||||
'@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue',
|
'@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue',
|
||||||
).useAtomFamilySelectorValue as jest.Mock;
|
).useAtomFamilySelectorValue as jest.Mock;
|
||||||
|
|
||||||
// Stands in for the views store: only the view the hook actually asks for
|
const setObjectOpenRecordIn = (
|
||||||
// comes back, so a hook reading the wrong view id resolves to nothing.
|
openRecordIn: ObjectOpenRecordIn | undefined,
|
||||||
mockUseAtomFamilySelectorValue.mockImplementation(
|
) => {
|
||||||
(_selector: unknown, { viewId }: { viewId: string }) =>
|
mockUseAtomFamilySelectorValue.mockImplementation(
|
||||||
viewId === 'test-view-id'
|
(_selector: unknown, { objectName }: { objectName: string }) =>
|
||||||
? { id: viewId, openRecordIn: ViewOpenRecordIn.RECORD_PAGE }
|
objectName === 'company' && openRecordIn !== undefined
|
||||||
: undefined,
|
? { id: 'company-id', nameSingular: 'company', openRecordIn }
|
||||||
|
: undefined,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||||
|
<JotaiProvider store={jotaiStore}>{children}</JotaiProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
const WrapperWithoutContextStore = ({
|
const setMemberPreference = (openRecordIn: OpenRecordIn | undefined) => {
|
||||||
children,
|
act(() => {
|
||||||
}: {
|
jotaiStore.set(
|
||||||
children: React.ReactNode;
|
currentWorkspaceMemberState.atom,
|
||||||
}) => <JotaiProvider store={jotaiStore}>{children}</JotaiProvider>;
|
openRecordIn === undefined
|
||||||
|
? null
|
||||||
const WrapperWithContextStore = ({
|
: ({ id: 'member-id', openRecordIn } as never),
|
||||||
children,
|
);
|
||||||
}: {
|
});
|
||||||
children: React.ReactNode;
|
};
|
||||||
}) => (
|
|
||||||
<JotaiProvider store={jotaiStore}>
|
|
||||||
<ContextStoreComponentInstanceContext.Provider
|
|
||||||
value={{ instanceId: 'test-context-store' }}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</ContextStoreComponentInstanceContext.Provider>
|
|
||||||
</JotaiProvider>
|
|
||||||
);
|
|
||||||
|
|
||||||
describe('useResolveOpenRecordIn', () => {
|
describe('useResolveOpenRecordIn', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jotaiStore.set(
|
setMemberPreference(undefined);
|
||||||
contextStoreCurrentViewIdComponentState.atomFamily({
|
|
||||||
instanceId: 'test-context-store',
|
|
||||||
}),
|
|
||||||
undefined,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('falls back to the default where no context store is mounted', () => {
|
it('follows the member preference when the object leaves the choice open', () => {
|
||||||
|
setObjectOpenRecordIn(ObjectOpenRecordIn.USER_CHOICE);
|
||||||
|
setMemberPreference(OpenRecordIn.RECORD_PAGE);
|
||||||
|
|
||||||
const { result } = renderHook(() => useResolveOpenRecordIn('company'), {
|
const { result } = renderHook(() => useResolveOpenRecordIn('company'), {
|
||||||
wrapper: WrapperWithoutContextStore,
|
wrapper: Wrapper,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.current).toBe(ViewOpenRecordIn.SIDE_PANEL);
|
expect(result.current).toBe(OpenRecordIn.RECORD_PAGE);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('follows the current view of the surrounding context store', () => {
|
it('lets the object pin its records over the member preference', () => {
|
||||||
jotaiStore.set(
|
setObjectOpenRecordIn(ObjectOpenRecordIn.RECORD_PAGE);
|
||||||
contextStoreCurrentViewIdComponentState.atomFamily({
|
setMemberPreference(OpenRecordIn.SIDE_PANEL);
|
||||||
instanceId: 'test-context-store',
|
|
||||||
}),
|
|
||||||
'test-view-id',
|
|
||||||
);
|
|
||||||
|
|
||||||
const { result } = renderHook(() => useResolveOpenRecordIn('company'), {
|
const { result } = renderHook(() => useResolveOpenRecordIn('company'), {
|
||||||
wrapper: WrapperWithContextStore,
|
wrapper: Wrapper,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.current).toBe(ViewOpenRecordIn.RECORD_PAGE);
|
expect(result.current).toBe(OpenRecordIn.RECORD_PAGE);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to the side panel default with no metadata and no member', () => {
|
||||||
|
setObjectOpenRecordIn(undefined);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useResolveOpenRecordIn('company'), {
|
||||||
|
wrapper: Wrapper,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current).toBe(OpenRecordIn.SIDE_PANEL);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+2
-3
@@ -9,10 +9,9 @@ import { useRecordIndexContextOrThrow } from '@/object-record/record-index/conte
|
|||||||
import { useResolveOpenRecordIn } from '@/object-record/record-index/hooks/useResolveOpenRecordIn';
|
import { useResolveOpenRecordIn } from '@/object-record/record-index/hooks/useResolveOpenRecordIn';
|
||||||
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
|
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
|
||||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||||
import { ViewOpenRecordIn } from '~/generated-metadata/graphql';
|
|
||||||
import { useStore } from 'jotai';
|
import { useStore } from 'jotai';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { AppPath, SidePanelPages } from 'twenty-shared/types';
|
import { AppPath, OpenRecordIn, SidePanelPages } from 'twenty-shared/types';
|
||||||
import { useNavigateApp } from '~/hooks/useNavigateApp';
|
import { useNavigateApp } from '~/hooks/useNavigateApp';
|
||||||
|
|
||||||
export const useOpenRecordFromIndexView = () => {
|
export const useOpenRecordFromIndexView = () => {
|
||||||
@@ -65,7 +64,7 @@ export const useOpenRecordFromIndexView = () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (openRecordIn === ViewOpenRecordIn.SIDE_PANEL) {
|
if (openRecordIn === OpenRecordIn.SIDE_PANEL) {
|
||||||
openRecordInSidePanel({
|
openRecordInSidePanel({
|
||||||
recordId,
|
recordId,
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
|
|||||||
+15
-22
@@ -1,36 +1,29 @@
|
|||||||
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
|
import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector';
|
||||||
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
|
|
||||||
import { DEFAULT_VIEW_OPEN_RECORD_IN } from '@/object-record/record-index/constants/DefaultViewOpenRecordIn';
|
|
||||||
import { resolveOpenRecordIn } from '@/object-record/record-index/utils/resolveOpenRecordIn';
|
import { resolveOpenRecordIn } from '@/object-record/record-index/utils/resolveOpenRecordIn';
|
||||||
import { useAvailableComponentInstanceId } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceId';
|
|
||||||
import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue';
|
import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue';
|
||||||
import { viewFromViewIdFamilySelector } from '@/views/states/selectors/viewFromViewIdFamilySelector';
|
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||||
import { useAtomValue } from 'jotai';
|
import { openRecordInPreferenceState } from '@/workspace-member/states/openRecordInPreferenceState';
|
||||||
|
import { ObjectOpenRecordIn } from 'twenty-shared/types';
|
||||||
import { useIsMobile } from 'twenty-ui/utilities';
|
import { useIsMobile } from 'twenty-ui/utilities';
|
||||||
|
|
||||||
export const useResolveOpenRecordIn = (objectNameSingular: string) => {
|
export const useResolveOpenRecordIn = (objectNameSingular: string) => {
|
||||||
// Record chips also render where no context store is mounted at all, such as
|
// Non-throwing on purpose: a chip must not crash while metadata is loading.
|
||||||
// a mention inside a note, and those have no view to take a setting from.
|
const objectMetadataItem = useAtomFamilySelectorValue(
|
||||||
const contextStoreInstanceId = useAvailableComponentInstanceId(
|
objectMetadataItemFamilySelector,
|
||||||
ContextStoreComponentInstanceContext,
|
{
|
||||||
|
objectName: objectNameSingular,
|
||||||
|
objectNameType: 'singular',
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const contextStoreCurrentViewId = useAtomValue(
|
const openRecordInPreference = useAtomStateValue(openRecordInPreferenceState);
|
||||||
contextStoreCurrentViewIdComponentState.atomFamily({
|
|
||||||
instanceId: contextStoreInstanceId ?? '',
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const currentView = useAtomFamilySelectorValue(viewFromViewIdFamilySelector, {
|
|
||||||
viewId: contextStoreCurrentViewId ?? '',
|
|
||||||
});
|
|
||||||
|
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
return resolveOpenRecordIn({
|
return resolveOpenRecordIn({
|
||||||
openRecordInViewSetting:
|
objectOpenRecordIn:
|
||||||
currentView?.openRecordIn ?? DEFAULT_VIEW_OPEN_RECORD_IN,
|
objectMetadataItem?.openRecordIn ?? ObjectOpenRecordIn.USER_CHOICE,
|
||||||
objectNameSingular,
|
openRecordInPreference,
|
||||||
canDisplaySidePanel: !isMobile,
|
canDisplaySidePanel: !isMobile,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
+50
-35
@@ -1,44 +1,59 @@
|
|||||||
import { resolveOpenRecordIn } from '@/object-record/record-index/utils/resolveOpenRecordIn';
|
import { resolveOpenRecordIn } from '@/object-record/record-index/utils/resolveOpenRecordIn';
|
||||||
import { ViewOpenRecordIn } from '~/generated-metadata/graphql';
|
import { ObjectOpenRecordIn, OpenRecordIn } from 'twenty-shared/types';
|
||||||
|
|
||||||
|
const resolve = (
|
||||||
|
overrides: Partial<Parameters<typeof resolveOpenRecordIn>[0]>,
|
||||||
|
) =>
|
||||||
|
resolveOpenRecordIn({
|
||||||
|
objectOpenRecordIn: ObjectOpenRecordIn.USER_CHOICE,
|
||||||
|
openRecordInPreference: OpenRecordIn.SIDE_PANEL,
|
||||||
|
canDisplaySidePanel: true,
|
||||||
|
...overrides,
|
||||||
|
});
|
||||||
|
|
||||||
describe('resolveOpenRecordIn', () => {
|
describe('resolveOpenRecordIn', () => {
|
||||||
it('opens in the side panel when the view asks for it and it can be displayed', () => {
|
describe('when the object leaves the choice to the member', () => {
|
||||||
expect(
|
it('follows a side panel preference', () => {
|
||||||
resolveOpenRecordIn({
|
expect(resolve({ openRecordInPreference: OpenRecordIn.SIDE_PANEL })).toBe(
|
||||||
openRecordInViewSetting: ViewOpenRecordIn.SIDE_PANEL,
|
OpenRecordIn.SIDE_PANEL,
|
||||||
objectNameSingular: 'company',
|
);
|
||||||
canDisplaySidePanel: true,
|
});
|
||||||
}),
|
|
||||||
).toBe(ViewOpenRecordIn.SIDE_PANEL);
|
it('follows a record page preference', () => {
|
||||||
|
expect(
|
||||||
|
resolve({ openRecordInPreference: OpenRecordIn.RECORD_PAGE }),
|
||||||
|
).toBe(OpenRecordIn.RECORD_PAGE);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('falls back to the record page when there is no room for a side panel', () => {
|
describe('when the object pins a destination', () => {
|
||||||
expect(
|
it('ignores the member preference for a pinned record page', () => {
|
||||||
resolveOpenRecordIn({
|
expect(
|
||||||
openRecordInViewSetting: ViewOpenRecordIn.SIDE_PANEL,
|
resolve({
|
||||||
objectNameSingular: 'company',
|
objectOpenRecordIn: ObjectOpenRecordIn.RECORD_PAGE,
|
||||||
canDisplaySidePanel: false,
|
openRecordInPreference: OpenRecordIn.SIDE_PANEL,
|
||||||
}),
|
}),
|
||||||
).toBe(ViewOpenRecordIn.RECORD_PAGE);
|
).toBe(OpenRecordIn.RECORD_PAGE);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores the member preference for a pinned side panel', () => {
|
||||||
|
expect(
|
||||||
|
resolve({
|
||||||
|
objectOpenRecordIn: ObjectOpenRecordIn.SIDE_PANEL,
|
||||||
|
openRecordInPreference: OpenRecordIn.RECORD_PAGE,
|
||||||
|
}),
|
||||||
|
).toBe(OpenRecordIn.SIDE_PANEL);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('falls back to the record page for objects without a side panel', () => {
|
describe('when there is no room for a panel', () => {
|
||||||
expect(
|
it.each([ObjectOpenRecordIn.SIDE_PANEL, ObjectOpenRecordIn.USER_CHOICE])(
|
||||||
resolveOpenRecordIn({
|
'falls back to the record page (%s)',
|
||||||
openRecordInViewSetting: ViewOpenRecordIn.SIDE_PANEL,
|
(objectOpenRecordIn) => {
|
||||||
objectNameSingular: 'workflow',
|
expect(
|
||||||
canDisplaySidePanel: true,
|
resolve({ objectOpenRecordIn, canDisplaySidePanel: false }),
|
||||||
}),
|
).toBe(OpenRecordIn.RECORD_PAGE);
|
||||||
).toBe(ViewOpenRecordIn.RECORD_PAGE);
|
},
|
||||||
});
|
);
|
||||||
|
|
||||||
it('keeps the record page when the view asks for it', () => {
|
|
||||||
expect(
|
|
||||||
resolveOpenRecordIn({
|
|
||||||
openRecordInViewSetting: ViewOpenRecordIn.RECORD_PAGE,
|
|
||||||
objectNameSingular: 'company',
|
|
||||||
canDisplaySidePanel: true,
|
|
||||||
}),
|
|
||||||
).toBe(ViewOpenRecordIn.RECORD_PAGE);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+18
-15
@@ -1,22 +1,25 @@
|
|||||||
import { canOpenObjectInSidePanel } from '@/object-record/utils/canOpenObjectInSidePanel';
|
import { ObjectOpenRecordIn, OpenRecordIn } from 'twenty-shared/types';
|
||||||
import { ViewOpenRecordIn } from '~/generated-metadata/graphql';
|
|
||||||
|
|
||||||
type ResolveOpenRecordInArgs = {
|
type ResolveOpenRecordInArgs = {
|
||||||
openRecordInViewSetting: ViewOpenRecordIn;
|
objectOpenRecordIn: ObjectOpenRecordIn;
|
||||||
objectNameSingular: string;
|
openRecordInPreference: OpenRecordIn;
|
||||||
canDisplaySidePanel: boolean;
|
canDisplaySidePanel: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
// The view setting is an intent, not a decision: the side panel is only a real
|
|
||||||
// destination when there is room to display it next to the record list, and
|
|
||||||
// when the object has a side panel to display at all.
|
|
||||||
export const resolveOpenRecordIn = ({
|
export const resolveOpenRecordIn = ({
|
||||||
openRecordInViewSetting,
|
objectOpenRecordIn,
|
||||||
objectNameSingular,
|
openRecordInPreference,
|
||||||
canDisplaySidePanel,
|
canDisplaySidePanel,
|
||||||
}: ResolveOpenRecordInArgs): ViewOpenRecordIn =>
|
}: ResolveOpenRecordInArgs): OpenRecordIn => {
|
||||||
openRecordInViewSetting === ViewOpenRecordIn.SIDE_PANEL &&
|
const requestedOpenRecordIn =
|
||||||
canDisplaySidePanel &&
|
objectOpenRecordIn === ObjectOpenRecordIn.USER_CHOICE
|
||||||
canOpenObjectInSidePanel(objectNameSingular)
|
? openRecordInPreference
|
||||||
? ViewOpenRecordIn.SIDE_PANEL
|
: objectOpenRecordIn === ObjectOpenRecordIn.SIDE_PANEL
|
||||||
: ViewOpenRecordIn.RECORD_PAGE;
|
? OpenRecordIn.SIDE_PANEL
|
||||||
|
: OpenRecordIn.RECORD_PAGE;
|
||||||
|
|
||||||
|
return requestedOpenRecordIn === OpenRecordIn.SIDE_PANEL &&
|
||||||
|
canDisplaySidePanel
|
||||||
|
? OpenRecordIn.SIDE_PANEL
|
||||||
|
: OpenRecordIn.RECORD_PAGE;
|
||||||
|
};
|
||||||
|
|||||||
+2
-2
@@ -15,7 +15,7 @@ import { RECORD_TABLE_COLUMN_MIN_WIDTH } from '@/object-record/record-table/cons
|
|||||||
import { RecordTableUpdateContext } from '@/object-record/record-table/contexts/RecordTableUpdateContext';
|
import { RecordTableUpdateContext } from '@/object-record/record-table/contexts/RecordTableUpdateContext';
|
||||||
import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
|
import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
|
||||||
import { useIsTouchDevice } from 'twenty-ui/utilities';
|
import { useIsTouchDevice } from 'twenty-ui/utilities';
|
||||||
import { ViewOpenRecordIn } from '~/generated-metadata/graphql';
|
import { OpenRecordIn } from 'twenty-shared/types';
|
||||||
|
|
||||||
type RecordTableContextProviderProps = {
|
type RecordTableContextProviderProps = {
|
||||||
viewBarId: string;
|
viewBarId: string;
|
||||||
@@ -66,7 +66,7 @@ export const RecordTableContextProvider = ({
|
|||||||
// Navigating on mouse down only buys a frame on a real pointer: a tap
|
// Navigating on mouse down only buys a frame on a real pointer: a tap
|
||||||
// synthesises its mouse events after the finger is already gone.
|
// synthesises its mouse events after the finger is already gone.
|
||||||
const triggerEvent =
|
const triggerEvent =
|
||||||
openRecordIn === ViewOpenRecordIn.SIDE_PANEL || isTouchDevice
|
openRecordIn === OpenRecordIn.SIDE_PANEL || isTouchDevice
|
||||||
? 'CLICK'
|
? 'CLICK'
|
||||||
: 'MOUSE_DOWN';
|
: 'MOUSE_DOWN';
|
||||||
|
|
||||||
|
|||||||
+2
-3
@@ -18,10 +18,9 @@ import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/
|
|||||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||||
import { useStore } from 'jotai';
|
import { useStore } from 'jotai';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { AppPath } from 'twenty-shared/types';
|
import { AppPath, OpenRecordIn } from 'twenty-shared/types';
|
||||||
import { findByProperty, isDefined } from 'twenty-shared/utils';
|
import { findByProperty, isDefined } from 'twenty-shared/utils';
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
import { ViewOpenRecordIn } from '~/generated-metadata/graphql';
|
|
||||||
import { useNavigateApp } from '~/hooks/useNavigateApp';
|
import { useNavigateApp } from '~/hooks/useNavigateApp';
|
||||||
|
|
||||||
type UseCreateNewIndexRecordProps = {
|
type UseCreateNewIndexRecordProps = {
|
||||||
@@ -92,7 +91,7 @@ export const useCreateNewIndexRecord = ({
|
|||||||
...mergedRecordInput,
|
...mergedRecordInput,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (openRecordIn === ViewOpenRecordIn.SIDE_PANEL) {
|
if (openRecordIn === OpenRecordIn.SIDE_PANEL) {
|
||||||
openRecordInSidePanel({
|
openRecordInSidePanel({
|
||||||
recordId,
|
recordId,
|
||||||
objectNameSingular: objectMetadataItem.nameSingular,
|
objectNameSingular: objectMetadataItem.nameSingular,
|
||||||
|
|||||||
+2
-2
@@ -31,7 +31,7 @@ import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentTyp
|
|||||||
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
|
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
|
||||||
import { useStore } from 'jotai';
|
import { useStore } from 'jotai';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { ViewOpenRecordIn } from '~/generated-metadata/graphql';
|
import { OpenRecordIn } from 'twenty-shared/types';
|
||||||
|
|
||||||
export type OpenTableCellArgs = {
|
export type OpenTableCellArgs = {
|
||||||
initialValue?: string;
|
initialValue?: string;
|
||||||
@@ -122,7 +122,7 @@ export const useOpenRecordTableCell = (recordTableId: string) => {
|
|||||||
if ((isFirstColumnCell && !isEmpty) || isNavigating) {
|
if ((isFirstColumnCell && !isEmpty) || isNavigating) {
|
||||||
leaveTableFocus();
|
leaveTableFocus();
|
||||||
|
|
||||||
if (openRecordIn === ViewOpenRecordIn.SIDE_PANEL) {
|
if (openRecordIn === OpenRecordIn.SIDE_PANEL) {
|
||||||
activateRecordTableRow(cellPosition.row);
|
activateRecordTableRow(cellPosition.row);
|
||||||
unfocusRecordTableRow();
|
unfocusRecordTableRow();
|
||||||
}
|
}
|
||||||
|
|||||||
-16
@@ -1,16 +0,0 @@
|
|||||||
import { canOpenObjectInSidePanel } from '@/object-record/utils/canOpenObjectInSidePanel';
|
|
||||||
|
|
||||||
describe('canOpenObjectInSidePanel', () => {
|
|
||||||
it('should return false for workflow objects', () => {
|
|
||||||
expect(canOpenObjectInSidePanel('workflow')).toBe(false);
|
|
||||||
expect(canOpenObjectInSidePanel('workflowVersion')).toBe(false);
|
|
||||||
expect(canOpenObjectInSidePanel('dashboard')).toBe(false);
|
|
||||||
expect(canOpenObjectInSidePanel('messageCampaign')).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return true for other objects', () => {
|
|
||||||
expect(canOpenObjectInSidePanel('person')).toBe(true);
|
|
||||||
expect(canOpenObjectInSidePanel('company')).toBe(true);
|
|
||||||
expect(canOpenObjectInSidePanel('task')).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
+3
@@ -1,3 +1,4 @@
|
|||||||
|
import { ObjectOpenRecordIn } from 'twenty-shared/types';
|
||||||
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
||||||
import { generateAggregateQuery } from '@/object-record/utils/generateAggregateQuery';
|
import { generateAggregateQuery } from '@/object-record/utils/generateAggregateQuery';
|
||||||
|
|
||||||
@@ -25,6 +26,7 @@ describe('generateAggregateQuery', () => {
|
|||||||
isSystem: false,
|
isSystem: false,
|
||||||
isUIEditable: true,
|
isUIEditable: true,
|
||||||
isUICreatable: true,
|
isUICreatable: true,
|
||||||
|
openRecordIn: ObjectOpenRecordIn.USER_CHOICE,
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockRecordGqlFields = {
|
const mockRecordGqlFields = {
|
||||||
@@ -69,6 +71,7 @@ describe('generateAggregateQuery', () => {
|
|||||||
isSystem: false,
|
isSystem: false,
|
||||||
isUIEditable: true,
|
isUIEditable: true,
|
||||||
isUICreatable: true,
|
isUICreatable: true,
|
||||||
|
openRecordIn: ObjectOpenRecordIn.USER_CHOICE,
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockRecordGqlFields = {
|
const mockRecordGqlFields = {
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
export const canOpenObjectInSidePanel = (objectNameSingular: string) =>
|
|
||||||
!(
|
|
||||||
objectNameSingular === 'workflow' ||
|
|
||||||
objectNameSingular === 'workflowVersion' ||
|
|
||||||
objectNameSingular === 'dashboard' ||
|
|
||||||
objectNameSingular === 'messageCampaign'
|
|
||||||
);
|
|
||||||
+3
-3
@@ -1,7 +1,7 @@
|
|||||||
import { styled } from '@linaria/react';
|
import { styled } from '@linaria/react';
|
||||||
|
|
||||||
import { SettingsAccountsRadioSettingsCard } from '@/settings/accounts/components/SettingsAccountsRadioSettingsCard';
|
|
||||||
import { SettingsAccountsVisibilityIcon } from '@/settings/accounts/components/SettingsAccountsVisibilityIcon';
|
import { SettingsAccountsVisibilityIcon } from '@/settings/accounts/components/SettingsAccountsVisibilityIcon';
|
||||||
|
import { SettingsRadioSettingsCard } from '@/settings/components/SettingsRadioSettingsCard';
|
||||||
import { msg } from '@lingui/core/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { CalendarChannelVisibility } from '~/generated/graphql';
|
import { CalendarChannelVisibility } from '~/generated/graphql';
|
||||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||||
@@ -13,7 +13,7 @@ type SettingsAccountsEventVisibilitySettingsCardProps = {
|
|||||||
|
|
||||||
const StyledCardMediaContainer = styled.div`
|
const StyledCardMediaContainer = styled.div`
|
||||||
> * {
|
> * {
|
||||||
height: ${themeCssVariables.spacing[6]};
|
height: ${themeCssVariables.spacing[8]};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ export const SettingsAccountsEventVisibilitySettingsCard = ({
|
|||||||
onChange,
|
onChange,
|
||||||
value = CalendarChannelVisibility.SHARE_EVERYTHING,
|
value = CalendarChannelVisibility.SHARE_EVERYTHING,
|
||||||
}: SettingsAccountsEventVisibilitySettingsCardProps) => (
|
}: SettingsAccountsEventVisibilitySettingsCardProps) => (
|
||||||
<SettingsAccountsRadioSettingsCard
|
<SettingsRadioSettingsCard
|
||||||
name="event-visibility"
|
name="event-visibility"
|
||||||
options={eventSettingsVisibilityOptions}
|
options={eventSettingsVisibilityOptions}
|
||||||
value={value}
|
value={value}
|
||||||
|
|||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
import { SettingsAccountsMessageAutoCreationIcon } from '@/settings/accounts/components/SettingsAccountsMessageAutoCreationIcon';
|
import { SettingsAccountsMessageAutoCreationIcon } from '@/settings/accounts/components/SettingsAccountsMessageAutoCreationIcon';
|
||||||
import { SettingsAccountsRadioSettingsCard } from '@/settings/accounts/components/SettingsAccountsRadioSettingsCard';
|
import { SettingsRadioSettingsCard } from '@/settings/components/SettingsRadioSettingsCard';
|
||||||
import { msg } from '@lingui/core/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { MessageChannelContactAutoCreationPolicy } from 'twenty-shared/types';
|
import { MessageChannelContactAutoCreationPolicy } from 'twenty-shared/types';
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ export const SettingsAccountsMessageAutoCreationCard = ({
|
|||||||
onChange,
|
onChange,
|
||||||
value = MessageChannelContactAutoCreationPolicy.SENT_AND_RECEIVED,
|
value = MessageChannelContactAutoCreationPolicy.SENT_AND_RECEIVED,
|
||||||
}: SettingsAccountsMessageAutoCreationCardProps) => (
|
}: SettingsAccountsMessageAutoCreationCardProps) => (
|
||||||
<SettingsAccountsRadioSettingsCard
|
<SettingsRadioSettingsCard
|
||||||
name="message-auto-creation"
|
name="message-auto-creation"
|
||||||
options={autoCreationOptions}
|
options={autoCreationOptions}
|
||||||
value={value}
|
value={value}
|
||||||
|
|||||||
+30
-12
@@ -1,6 +1,7 @@
|
|||||||
import { styled } from '@linaria/react';
|
import { styled } from '@linaria/react';
|
||||||
|
import { IconArrowDown, IconArrowUp } from 'twenty-ui/icon';
|
||||||
|
|
||||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
import { themeCssVariables, useTheme } from 'twenty-ui/theme-constants';
|
||||||
|
|
||||||
type SettingsAccountsMessageAutoCreationIconProps = {
|
type SettingsAccountsMessageAutoCreationIconProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -12,32 +13,49 @@ const StyledIconContainer = styled.div`
|
|||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
border: 2px solid ${themeCssVariables.border.color.medium};
|
border: 2px solid ${themeCssVariables.border.color.medium};
|
||||||
border-radius: ${themeCssVariables.border.radius.sm};
|
border-radius: ${themeCssVariables.border.radius.sm};
|
||||||
|
box-sizing: border-box;
|
||||||
color: ${themeCssVariables.font.color.light};
|
color: ${themeCssVariables.font.color.light};
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: ${themeCssVariables.spacing['0.5']};
|
gap: ${themeCssVariables.spacing['0.5']};
|
||||||
height: ${themeCssVariables.spacing[8]};
|
height: 40px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: ${themeCssVariables.spacing['0.5']};
|
padding: ${themeCssVariables.spacing['0.5']};
|
||||||
width: ${themeCssVariables.spacing[6]};
|
width: 32px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledDirectionSkeleton = styled.div<{ isActive?: boolean }>`
|
const StyledDirectionSkeleton = styled.div<{ isActive?: boolean }>`
|
||||||
|
align-items: center;
|
||||||
background-color: ${({ isActive }) =>
|
background-color: ${({ isActive }) =>
|
||||||
isActive
|
isActive
|
||||||
? themeCssVariables.accent.accent4060
|
? themeCssVariables.accent.accent7
|
||||||
: themeCssVariables.background.quaternary};
|
: themeCssVariables.border.color.medium};
|
||||||
border-radius: 1px;
|
border-radius: 1px;
|
||||||
height: 24px;
|
color: ${themeCssVariables.font.color.inverted};
|
||||||
|
display: flex;
|
||||||
|
flex: 1 0 0;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const SettingsAccountsMessageAutoCreationIcon = ({
|
export const SettingsAccountsMessageAutoCreationIcon = ({
|
||||||
className,
|
className,
|
||||||
isSentActive,
|
isSentActive,
|
||||||
isReceivedActive,
|
isReceivedActive,
|
||||||
}: SettingsAccountsMessageAutoCreationIconProps) => (
|
}: SettingsAccountsMessageAutoCreationIconProps) => {
|
||||||
<StyledIconContainer className={className}>
|
const theme = useTheme();
|
||||||
<StyledDirectionSkeleton isActive={isSentActive} />
|
|
||||||
<StyledDirectionSkeleton isActive={isReceivedActive} />
|
return (
|
||||||
</StyledIconContainer>
|
<StyledIconContainer className={className}>
|
||||||
);
|
<StyledDirectionSkeleton isActive={isSentActive}>
|
||||||
|
<IconArrowUp size={theme.icon.size.sm} stroke={theme.icon.stroke.md} />
|
||||||
|
</StyledDirectionSkeleton>
|
||||||
|
<StyledDirectionSkeleton isActive={isReceivedActive}>
|
||||||
|
<IconArrowDown
|
||||||
|
size={theme.icon.size.sm}
|
||||||
|
stroke={theme.icon.stroke.md}
|
||||||
|
/>
|
||||||
|
</StyledDirectionSkeleton>
|
||||||
|
</StyledIconContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
import { SettingsAccountsMessageFoldersCard } from '@/settings/accounts/components/message-folders/SettingsAccountsMessageFoldersCard';
|
import { SettingsAccountsMessageFoldersCard } from '@/settings/accounts/components/message-folders/SettingsAccountsMessageFoldersCard';
|
||||||
import { SettingsAccountsMessageFolderIcon } from '@/settings/accounts/components/SettingsAccountsMessageFolderIcon';
|
import { SettingsAccountsMessageFolderIcon } from '@/settings/accounts/components/SettingsAccountsMessageFolderIcon';
|
||||||
import { SettingsAccountsRadioSettingsCard } from '@/settings/accounts/components/SettingsAccountsRadioSettingsCard';
|
import { SettingsRadioSettingsCard } from '@/settings/components/SettingsRadioSettingsCard';
|
||||||
import { msg } from '@lingui/core/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { MessageFolderImportPolicy } from 'twenty-shared/types';
|
import { MessageFolderImportPolicy } from 'twenty-shared/types';
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ export const SettingsAccountsMessageFolderCard = ({
|
|||||||
onChange,
|
onChange,
|
||||||
value = MessageFolderImportPolicy.SELECTED_FOLDERS,
|
value = MessageFolderImportPolicy.SELECTED_FOLDERS,
|
||||||
}: SettingsAccountsMessageFolderCardProps) => (
|
}: SettingsAccountsMessageFolderCardProps) => (
|
||||||
<SettingsAccountsRadioSettingsCard
|
<SettingsRadioSettingsCard
|
||||||
name="message-folder-import-policy"
|
name="message-folder-import-policy"
|
||||||
options={INBOX_SETTINGS_VISIBILITY_OPTIONS}
|
options={INBOX_SETTINGS_VISIBILITY_OPTIONS}
|
||||||
value={value}
|
value={value}
|
||||||
|
|||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
import { SettingsAccountsRadioSettingsCard } from '@/settings/accounts/components/SettingsAccountsRadioSettingsCard';
|
|
||||||
import { SettingsAccountsVisibilityIcon } from '@/settings/accounts/components/SettingsAccountsVisibilityIcon';
|
import { SettingsAccountsVisibilityIcon } from '@/settings/accounts/components/SettingsAccountsVisibilityIcon';
|
||||||
|
import { SettingsRadioSettingsCard } from '@/settings/components/SettingsRadioSettingsCard';
|
||||||
import { msg } from '@lingui/core/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { MessageChannelVisibility } from '~/generated/graphql';
|
import { MessageChannelVisibility } from '~/generated/graphql';
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ export const SettingsAccountsMessageVisibilityCard = ({
|
|||||||
onChange,
|
onChange,
|
||||||
value = MessageChannelVisibility.SHARE_EVERYTHING,
|
value = MessageChannelVisibility.SHARE_EVERYTHING,
|
||||||
}: SettingsAccountsMessageVisibilityCardProps) => (
|
}: SettingsAccountsMessageVisibilityCardProps) => (
|
||||||
<SettingsAccountsRadioSettingsCard
|
<SettingsRadioSettingsCard
|
||||||
name="message-visibility"
|
name="message-visibility"
|
||||||
options={inboxSettingsVisibilityOptions}
|
options={inboxSettingsVisibilityOptions}
|
||||||
value={value}
|
value={value}
|
||||||
|
|||||||
-104
@@ -1,104 +0,0 @@
|
|||||||
import { styled } from '@linaria/react';
|
|
||||||
import { type MessageDescriptor } from '@lingui/core';
|
|
||||||
import { Trans } from '@lingui/react';
|
|
||||||
import { type ReactNode } from 'react';
|
|
||||||
import { Radio } from 'twenty-ui/input';
|
|
||||||
import { Card, CardContent } from 'twenty-ui/surfaces';
|
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
|
||||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
|
||||||
|
|
||||||
type SettingsAccountsRadioSettingsCardProps<Option extends { value: string }> =
|
|
||||||
{
|
|
||||||
onChange: (nextValue: Option['value']) => void;
|
|
||||||
options: Option[];
|
|
||||||
value: Option['value'];
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const StyledCardContentContainer = styled.div`
|
|
||||||
> * {
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: ${themeCssVariables.background.transparent.lighter};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledOptionHeader = styled.div`
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
gap: ${themeCssVariables.spacing[4]};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledTitle = styled.div`
|
|
||||||
color: ${themeCssVariables.font.color.primary};
|
|
||||||
font-weight: ${themeCssVariables.font.weight.medium};
|
|
||||||
margin-bottom: ${themeCssVariables.spacing[2]};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledDescription = styled.div`
|
|
||||||
color: ${themeCssVariables.font.color.tertiary};
|
|
||||||
font-size: ${themeCssVariables.font.size.sm};
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledRadioContainer = styled.span`
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
margin-left: auto;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledExpandedContent = styled.div`
|
|
||||||
margin-top: ${themeCssVariables.spacing[4]};
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const SettingsAccountsRadioSettingsCard = <
|
|
||||||
Option extends {
|
|
||||||
cardMedia: ReactNode;
|
|
||||||
description: MessageDescriptor;
|
|
||||||
title: MessageDescriptor;
|
|
||||||
value: string;
|
|
||||||
cardContentExpanded?: ReactNode;
|
|
||||||
},
|
|
||||||
>({
|
|
||||||
onChange,
|
|
||||||
options,
|
|
||||||
value,
|
|
||||||
name,
|
|
||||||
}: SettingsAccountsRadioSettingsCardProps<Option>) => (
|
|
||||||
<Card rounded>
|
|
||||||
{options.map((option, index) => (
|
|
||||||
<StyledCardContentContainer key={option.value}>
|
|
||||||
<CardContent
|
|
||||||
divider={index < options.length - 1}
|
|
||||||
onClick={() => onChange(option.value)}
|
|
||||||
>
|
|
||||||
<StyledOptionHeader>
|
|
||||||
{option.cardMedia}
|
|
||||||
<div>
|
|
||||||
<StyledTitle>
|
|
||||||
<Trans id={option.title.id} />
|
|
||||||
</StyledTitle>
|
|
||||||
<StyledDescription>
|
|
||||||
<Trans id={option.description.id} />
|
|
||||||
</StyledDescription>
|
|
||||||
</div>
|
|
||||||
<StyledRadioContainer>
|
|
||||||
<Radio
|
|
||||||
name={name}
|
|
||||||
value={option.value}
|
|
||||||
onCheckedChange={() => onChange(option.value)}
|
|
||||||
checked={value === option.value}
|
|
||||||
/>
|
|
||||||
</StyledRadioContainer>
|
|
||||||
</StyledOptionHeader>
|
|
||||||
{isDefined(option.cardContentExpanded) && value === option.value && (
|
|
||||||
<StyledExpandedContent>
|
|
||||||
{option.cardContentExpanded}
|
|
||||||
</StyledExpandedContent>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
|
||||||
</StyledCardContentContainer>
|
|
||||||
))}
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
+6
-5
@@ -15,20 +15,21 @@ const StyledCardMedia = styled.div`
|
|||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
border: 2px solid ${themeCssVariables.border.color.medium};
|
border: 2px solid ${themeCssVariables.border.color.medium};
|
||||||
border-radius: ${themeCssVariables.border.radius.sm};
|
border-radius: ${themeCssVariables.border.radius.sm};
|
||||||
|
box-sizing: border-box;
|
||||||
color: ${themeCssVariables.font.color.light};
|
color: ${themeCssVariables.font.color.light};
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: ${themeCssVariables.spacing['0.5']};
|
gap: ${themeCssVariables.spacing['0.5']};
|
||||||
height: ${themeCssVariables.spacing[8]};
|
height: 40px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: ${themeCssVariables.spacing['0.5']};
|
padding: ${themeCssVariables.spacing['0.5']};
|
||||||
width: ${themeCssVariables.spacing[6]};
|
width: 32px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledSubjectSkeleton = styled.div<{ isActive?: boolean }>`
|
const StyledSubjectSkeleton = styled.div<{ isActive?: boolean }>`
|
||||||
background-color: ${({ isActive }) =>
|
background-color: ${({ isActive }) =>
|
||||||
isActive
|
isActive
|
||||||
? themeCssVariables.accent.accent4060
|
? themeCssVariables.accent.accent7
|
||||||
: themeCssVariables.background.quaternary};
|
: themeCssVariables.background.quaternary};
|
||||||
border-radius: 1px;
|
border-radius: 1px;
|
||||||
height: 3px;
|
height: 3px;
|
||||||
@@ -37,7 +38,7 @@ const StyledSubjectSkeleton = styled.div<{ isActive?: boolean }>`
|
|||||||
const StyledMetadataSkeleton = styled.div<{ isActive?: boolean }>`
|
const StyledMetadataSkeleton = styled.div<{ isActive?: boolean }>`
|
||||||
background-color: ${({ isActive }) =>
|
background-color: ${({ isActive }) =>
|
||||||
isActive
|
isActive
|
||||||
? themeCssVariables.accent.accent4060
|
? themeCssVariables.accent.accent7
|
||||||
: themeCssVariables.background.quaternary};
|
: themeCssVariables.background.quaternary};
|
||||||
border-radius: 1px;
|
border-radius: 1px;
|
||||||
height: 3px;
|
height: 3px;
|
||||||
@@ -47,7 +48,7 @@ const StyledMetadataSkeleton = styled.div<{ isActive?: boolean }>`
|
|||||||
const StyledBodySkeleton = styled.div<{ isActive?: boolean }>`
|
const StyledBodySkeleton = styled.div<{ isActive?: boolean }>`
|
||||||
background-color: ${({ isActive }) =>
|
background-color: ${({ isActive }) =>
|
||||||
isActive
|
isActive
|
||||||
? themeCssVariables.accent.accent4060
|
? themeCssVariables.accent.accent7
|
||||||
: themeCssVariables.background.quaternary};
|
: themeCssVariables.background.quaternary};
|
||||||
border-radius: ${themeCssVariables.border.radius.xs};
|
border-radius: ${themeCssVariables.border.radius.xs};
|
||||||
flex: 1 0 auto;
|
flex: 1 0 auto;
|
||||||
|
|||||||
+4
-3
@@ -20,11 +20,12 @@ export const StyledSettingsCardIcon = styled.div`
|
|||||||
background-color: ${themeCssVariables.background.primary};
|
background-color: ${themeCssVariables.background.primary};
|
||||||
border: 2px solid ${themeCssVariables.border.color.light};
|
border: 2px solid ${themeCssVariables.border.color.light};
|
||||||
border-radius: ${themeCssVariables.border.radius.sm};
|
border-radius: ${themeCssVariables.border.radius.sm};
|
||||||
|
box-sizing: border-box;
|
||||||
display: flex;
|
display: flex;
|
||||||
height: ${themeCssVariables.spacing[7]};
|
height: ${themeCssVariables.spacing[8]};
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
min-width: ${themeCssVariables.icon.size.md};
|
min-width: ${themeCssVariables.spacing[8]};
|
||||||
width: ${themeCssVariables.spacing[7]};
|
width: ${themeCssVariables.spacing[8]};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const StyledSettingsCardTitle = styled.div`
|
export const StyledSettingsCardTitle = styled.div`
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
import { styled } from '@linaria/react';
|
||||||
|
import { type MessageDescriptor } from '@lingui/core';
|
||||||
|
import { useLingui } from '@lingui/react/macro';
|
||||||
|
import { type KeyboardEvent, type ReactNode } from 'react';
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
import { Radio } from 'twenty-ui/input';
|
||||||
|
import { Card, CardContent } from 'twenty-ui/surfaces';
|
||||||
|
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||||
|
|
||||||
|
type SettingsRadioSettingsCardProps<Option extends { value: string }> = {
|
||||||
|
name: string;
|
||||||
|
onChange: (nextValue: Option['value']) => void;
|
||||||
|
options: Option[];
|
||||||
|
value: Option['value'];
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledCardContentContainer = styled.div`
|
||||||
|
> * {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: ${themeCssVariables.background.transparent.lighter};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledOptionHeader = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
gap: ${themeCssVariables.spacing[4]};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledTextContainer = styled.div`
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledTitle = styled.div`
|
||||||
|
color: ${themeCssVariables.font.color.primary};
|
||||||
|
font-weight: ${themeCssVariables.font.weight.medium};
|
||||||
|
margin-bottom: ${themeCssVariables.spacing[2]};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledDescription = styled.div`
|
||||||
|
color: ${themeCssVariables.font.color.tertiary};
|
||||||
|
font-size: ${themeCssVariables.font.size.sm};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledRadioContainer = styled.span`
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
margin-left: auto;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledExpandedContent = styled.div`
|
||||||
|
margin-top: ${themeCssVariables.spacing[4]};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const SettingsRadioSettingsCard = <
|
||||||
|
Option extends {
|
||||||
|
cardMedia: ReactNode;
|
||||||
|
description: MessageDescriptor;
|
||||||
|
title: MessageDescriptor;
|
||||||
|
value: string;
|
||||||
|
cardContentExpanded?: ReactNode;
|
||||||
|
},
|
||||||
|
>({
|
||||||
|
name,
|
||||||
|
onChange,
|
||||||
|
options,
|
||||||
|
value,
|
||||||
|
}: SettingsRadioSettingsCardProps<Option>) => {
|
||||||
|
const { i18n } = useLingui();
|
||||||
|
|
||||||
|
const handleKeyDown = (
|
||||||
|
event: KeyboardEvent<HTMLDivElement>,
|
||||||
|
optionValue: Option['value'],
|
||||||
|
) => {
|
||||||
|
if (event.key !== 'Enter' && event.key !== ' ') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
onChange(optionValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card fullWidth rounded role="radiogroup">
|
||||||
|
{options.map((option, index) => {
|
||||||
|
const isSelected = value === option.value;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledCardContentContainer key={option.value}>
|
||||||
|
<CardContent
|
||||||
|
aria-checked={isSelected}
|
||||||
|
divider={index < options.length - 1}
|
||||||
|
onClick={() => onChange(option.value)}
|
||||||
|
onKeyDown={(event) => handleKeyDown(event, option.value)}
|
||||||
|
role="radio"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<StyledOptionHeader>
|
||||||
|
{option.cardMedia}
|
||||||
|
<StyledTextContainer>
|
||||||
|
<StyledTitle>{i18n._(option.title)}</StyledTitle>
|
||||||
|
<StyledDescription>
|
||||||
|
{i18n._(option.description)}
|
||||||
|
</StyledDescription>
|
||||||
|
</StyledTextContainer>
|
||||||
|
<StyledRadioContainer
|
||||||
|
onClick={(event) => event.stopPropagation()}
|
||||||
|
>
|
||||||
|
<Radio
|
||||||
|
checked={isSelected}
|
||||||
|
name={name}
|
||||||
|
onCheckedChange={() => onChange(option.value)}
|
||||||
|
value={option.value}
|
||||||
|
/>
|
||||||
|
</StyledRadioContainer>
|
||||||
|
</StyledOptionHeader>
|
||||||
|
{isDefined(option.cardContentExpanded) && isSelected && (
|
||||||
|
<StyledExpandedContent>
|
||||||
|
{option.cardContentExpanded}
|
||||||
|
</StyledExpandedContent>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</StyledCardContentContainer>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
+13
-4
@@ -4,7 +4,7 @@ import { styled } from '@linaria/react';
|
|||||||
import { useLingui } from '@lingui/react/macro';
|
import { useLingui } from '@lingui/react/macro';
|
||||||
import { AppPath } from 'twenty-shared/types';
|
import { AppPath } from 'twenty-shared/types';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
import { IconLayoutDashboard, IconReload } from 'twenty-ui/icon';
|
import { IconAddressBook, IconReload } from 'twenty-ui/icon';
|
||||||
import { Button } from 'twenty-ui/input';
|
import { Button } from 'twenty-ui/input';
|
||||||
import { Section } from 'twenty-ui/layout';
|
import { Section } from 'twenty-ui/layout';
|
||||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||||
@@ -16,6 +16,7 @@ import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
|||||||
import { useResetPageLayoutToDefault } from '@/page-layout/hooks/useResetPageLayoutToDefault';
|
import { useResetPageLayoutToDefault } from '@/page-layout/hooks/useResetPageLayoutToDefault';
|
||||||
import { recordPageLayoutByObjectMetadataIdFamilySelector } from '@/page-layout/states/selectors/recordPageLayoutByObjectMetadataIdFamilySelector';
|
import { recordPageLayoutByObjectMetadataIdFamilySelector } from '@/page-layout/states/selectors/recordPageLayoutByObjectMetadataIdFamilySelector';
|
||||||
import { SettingsCard } from '@/settings/components/SettingsCard';
|
import { SettingsCard } from '@/settings/components/SettingsCard';
|
||||||
|
import { ObjectOpenRecordInPicker } from '@/settings/data-model/object-details/components/tabs/ObjectOpenRecordInPicker';
|
||||||
import { useHasPermissionFlag } from '@/settings/roles/hooks/useHasPermissionFlag';
|
import { useHasPermissionFlag } from '@/settings/roles/hooks/useHasPermissionFlag';
|
||||||
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
|
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
|
||||||
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
||||||
@@ -91,16 +92,24 @@ export const ObjectLayout = ({ objectMetadataItem }: ObjectLayoutProps) => {
|
|||||||
<StyledContentContainer>
|
<StyledContentContainer>
|
||||||
<Section>
|
<Section>
|
||||||
<H2Title
|
<H2Title
|
||||||
title={t`Customize`}
|
title={t`Record page`}
|
||||||
description={t`Customize the layout for this role`}
|
description={t`Customize the workspace record page`}
|
||||||
/>
|
/>
|
||||||
<SettingsCard
|
<SettingsCard
|
||||||
title={t`Customize record page`}
|
title={t`Customize record page`}
|
||||||
Icon={<IconLayoutDashboard size={theme.icon.size.md} />}
|
description={t`Customize how your record page looks.`}
|
||||||
|
Icon={<IconAddressBook size={theme.icon.size.md} />}
|
||||||
onClick={handleCustomizeRecordPage}
|
onClick={handleCustomizeRecordPage}
|
||||||
disabled={!hasLayoutsPermission || !isDefined(firstRecord)}
|
disabled={!hasLayoutsPermission || !isDefined(firstRecord)}
|
||||||
/>
|
/>
|
||||||
</Section>
|
</Section>
|
||||||
|
<Section>
|
||||||
|
<H2Title
|
||||||
|
title={t`Navigation`}
|
||||||
|
description={t`Where records of this object open`}
|
||||||
|
/>
|
||||||
|
<ObjectOpenRecordInPicker objectMetadataItem={objectMetadataItem} />
|
||||||
|
</Section>
|
||||||
<Section>
|
<Section>
|
||||||
<H2Title
|
<H2Title
|
||||||
title={t`Reset`}
|
title={t`Reset`}
|
||||||
|
|||||||
+53
@@ -0,0 +1,53 @@
|
|||||||
|
import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem';
|
||||||
|
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
||||||
|
import { SettingsRadioSettingsCard } from '@/settings/components/SettingsRadioSettingsCard';
|
||||||
|
import { OpenRecordInCardMedia } from '@/settings/experience/components/OpenRecordInCardMedia';
|
||||||
|
import { msg } from '@lingui/core/macro';
|
||||||
|
import { ObjectOpenRecordIn } from 'twenty-shared/types';
|
||||||
|
|
||||||
|
type ObjectOpenRecordInPickerProps = {
|
||||||
|
objectMetadataItem: EnrichedObjectMetadataItem;
|
||||||
|
};
|
||||||
|
|
||||||
|
const objectOpenRecordInOptions = [
|
||||||
|
{
|
||||||
|
value: ObjectOpenRecordIn.USER_CHOICE,
|
||||||
|
title: msg`Member preference`,
|
||||||
|
description: msg`Let each member decide for themselves`,
|
||||||
|
cardMedia: <OpenRecordInCardMedia type="member-preference" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: ObjectOpenRecordIn.SIDE_PANEL,
|
||||||
|
title: msg`Side panel`,
|
||||||
|
description: msg`Open records alongside the current page`,
|
||||||
|
cardMedia: <OpenRecordInCardMedia type="side-panel" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: ObjectOpenRecordIn.RECORD_PAGE,
|
||||||
|
title: msg`Full page`,
|
||||||
|
description: msg`Open records on a dedicated page`,
|
||||||
|
cardMedia: <OpenRecordInCardMedia type="full-page" />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const ObjectOpenRecordInPicker = ({
|
||||||
|
objectMetadataItem,
|
||||||
|
}: ObjectOpenRecordInPickerProps) => {
|
||||||
|
const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem();
|
||||||
|
|
||||||
|
const handleChange = (openRecordIn: ObjectOpenRecordIn) => {
|
||||||
|
void updateOneObjectMetadataItem({
|
||||||
|
idToUpdate: objectMetadataItem.id,
|
||||||
|
updatePayload: { openRecordIn },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsRadioSettingsCard
|
||||||
|
name="object-open-record-in"
|
||||||
|
onChange={handleChange}
|
||||||
|
options={objectOpenRecordInOptions}
|
||||||
|
value={objectMetadataItem.openRecordIn}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
+119
@@ -0,0 +1,119 @@
|
|||||||
|
import { styled } from '@linaria/react';
|
||||||
|
import { IconArrowsDiagonal, IconUserCircle } from 'twenty-ui/icon';
|
||||||
|
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||||
|
|
||||||
|
type OpenRecordInCardMediaProps = {
|
||||||
|
type: 'member-preference' | 'side-panel' | 'full-page';
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledPreviewFrame = styled.div`
|
||||||
|
background-color: ${themeCssVariables.border.color.medium};
|
||||||
|
border-radius: ${themeCssVariables.border.radius.sm};
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
height: 40px;
|
||||||
|
padding: 2px;
|
||||||
|
width: 32px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledPreviewCanvas = styled.div`
|
||||||
|
background-color: ${themeCssVariables.background.secondary};
|
||||||
|
border-radius: 2px;
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
gap: 2px;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 2px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledMemberPreferenceCanvas = styled(StyledPreviewCanvas)`
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledMemberPreferenceIcon = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
background-color: ${themeCssVariables.background.secondary};
|
||||||
|
border-radius: ${themeCssVariables.border.radius.rounded};
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: ${themeCssVariables.accent.accent7};
|
||||||
|
corner-shape: round;
|
||||||
|
display: flex;
|
||||||
|
height: 16px;
|
||||||
|
justify-content: center;
|
||||||
|
left: 50%;
|
||||||
|
padding: 1px;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 16px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledSidePanelPreview = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
gap: 2px;
|
||||||
|
min-height: 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledSidePanelContent = styled.div`
|
||||||
|
background-color: ${themeCssVariables.border.color.medium};
|
||||||
|
border-radius: 1px;
|
||||||
|
flex: 1;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledSidePanel = styled.div`
|
||||||
|
background-color: ${themeCssVariables.accent.accent7};
|
||||||
|
border-radius: 1px;
|
||||||
|
width: 6px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledFullPage = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
background-color: ${themeCssVariables.accent.accent7};
|
||||||
|
border-radius: 1px;
|
||||||
|
color: ${themeCssVariables.font.color.inverted};
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const SidePanelPreview = () => (
|
||||||
|
<StyledSidePanelPreview>
|
||||||
|
<StyledSidePanelContent />
|
||||||
|
<StyledSidePanel />
|
||||||
|
</StyledSidePanelPreview>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const OpenRecordInCardMedia = ({ type }: OpenRecordInCardMediaProps) => {
|
||||||
|
if (type === 'member-preference') {
|
||||||
|
return (
|
||||||
|
<StyledPreviewFrame>
|
||||||
|
<StyledMemberPreferenceCanvas>
|
||||||
|
<SidePanelPreview />
|
||||||
|
<StyledFullPage />
|
||||||
|
<StyledMemberPreferenceIcon>
|
||||||
|
<IconUserCircle size={14} />
|
||||||
|
</StyledMemberPreferenceIcon>
|
||||||
|
</StyledMemberPreferenceCanvas>
|
||||||
|
</StyledPreviewFrame>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledPreviewFrame>
|
||||||
|
<StyledPreviewCanvas>
|
||||||
|
{type === 'side-panel' ? (
|
||||||
|
<SidePanelPreview />
|
||||||
|
) : (
|
||||||
|
<StyledFullPage>
|
||||||
|
<IconArrowsDiagonal size={14} />
|
||||||
|
</StyledFullPage>
|
||||||
|
)}
|
||||||
|
</StyledPreviewCanvas>
|
||||||
|
</StyledPreviewFrame>
|
||||||
|
);
|
||||||
|
};
|
||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
import { SettingsRadioSettingsCard } from '@/settings/components/SettingsRadioSettingsCard';
|
||||||
|
import { OpenRecordInCardMedia } from '@/settings/experience/components/OpenRecordInCardMedia';
|
||||||
|
import { useOpenRecordInPreference } from '@/settings/experience/hooks/useOpenRecordInPreference';
|
||||||
|
import { msg } from '@lingui/core/macro';
|
||||||
|
import { OpenRecordIn } from 'twenty-shared/types';
|
||||||
|
|
||||||
|
const openRecordInPreferenceOptions = [
|
||||||
|
{
|
||||||
|
value: OpenRecordIn.SIDE_PANEL,
|
||||||
|
title: msg`Side panel`,
|
||||||
|
description: msg`Open records alongside the current page`,
|
||||||
|
cardMedia: <OpenRecordInCardMedia type="side-panel" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: OpenRecordIn.RECORD_PAGE,
|
||||||
|
title: msg`Full page`,
|
||||||
|
description: msg`Open records on a dedicated page`,
|
||||||
|
cardMedia: <OpenRecordInCardMedia type="full-page" />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const OpenRecordInPreferencePicker = () => {
|
||||||
|
const { openRecordInPreference, setOpenRecordInPreference } =
|
||||||
|
useOpenRecordInPreference();
|
||||||
|
|
||||||
|
const handleChange = (value: OpenRecordIn) => {
|
||||||
|
void setOpenRecordInPreference(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsRadioSettingsCard
|
||||||
|
name="open-record-in-preference"
|
||||||
|
onChange={handleChange}
|
||||||
|
options={openRecordInPreferenceOptions}
|
||||||
|
value={openRecordInPreference}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||||
|
import { DEFAULT_OPEN_RECORD_IN_PREFERENCE } from '@/object-record/record-index/constants/DefaultOpenRecordInPreference';
|
||||||
|
import { useUpdateWorkspaceMemberSettings } from '@/settings/profile/hooks/useUpdateWorkspaceMemberSettings';
|
||||||
|
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
import { type OpenRecordIn } from 'twenty-shared/types';
|
||||||
|
|
||||||
|
export const useOpenRecordInPreference = () => {
|
||||||
|
const currentWorkspaceMember = useAtomStateValue(currentWorkspaceMemberState);
|
||||||
|
|
||||||
|
const { updateWorkspaceMemberSettings } = useUpdateWorkspaceMemberSettings();
|
||||||
|
|
||||||
|
const openRecordInPreference =
|
||||||
|
currentWorkspaceMember?.openRecordIn ?? DEFAULT_OPEN_RECORD_IN_PREFERENCE;
|
||||||
|
|
||||||
|
const setOpenRecordInPreference = useCallback(
|
||||||
|
async (value: OpenRecordIn) => {
|
||||||
|
if (!currentWorkspaceMember) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateWorkspaceMemberSettings({
|
||||||
|
workspaceMemberId: currentWorkspaceMember.id,
|
||||||
|
update: {
|
||||||
|
openRecordIn: value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[currentWorkspaceMember, updateWorkspaceMemberSettings],
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
openRecordInPreference,
|
||||||
|
setOpenRecordInPreference,
|
||||||
|
};
|
||||||
|
};
|
||||||
+7
@@ -2,6 +2,8 @@ import { isNull, isNumber, isString } from '@sniptt/guards';
|
|||||||
|
|
||||||
import { type CurrentWorkspaceMember } from '@/auth/states/currentWorkspaceMemberState';
|
import { type CurrentWorkspaceMember } from '@/auth/states/currentWorkspaceMemberState';
|
||||||
import { type ColorScheme } from '@/workspace-member/types/WorkspaceMember';
|
import { type ColorScheme } from '@/workspace-member/types/WorkspaceMember';
|
||||||
|
import { isOpenRecordIn } from '@/workspace-member/utils/toOpenRecordInPreference';
|
||||||
|
import { type OpenRecordIn } from 'twenty-shared/types';
|
||||||
import { isDefined, isPlainObject } from 'twenty-shared/utils';
|
import { isDefined, isPlainObject } from 'twenty-shared/utils';
|
||||||
import {
|
import {
|
||||||
WorkspaceMemberDateFormatEnum,
|
WorkspaceMemberDateFormatEnum,
|
||||||
@@ -18,6 +20,7 @@ export type WorkspaceMemberSettingsUpdateInput = {
|
|||||||
name?: WorkspaceMemberNameUpdate;
|
name?: WorkspaceMemberNameUpdate;
|
||||||
jobTitle?: string | null;
|
jobTitle?: string | null;
|
||||||
colorScheme?: string;
|
colorScheme?: string;
|
||||||
|
openRecordIn?: OpenRecordIn;
|
||||||
avatarUrl?: string | null;
|
avatarUrl?: string | null;
|
||||||
locale?: string;
|
locale?: string;
|
||||||
calendarStartDay?: number;
|
calendarStartDay?: number;
|
||||||
@@ -111,6 +114,10 @@ export const mergeWorkspaceMemberSettingsIntoCurrent = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ('openRecordIn' in payload && isOpenRecordIn(payload.openRecordIn)) {
|
||||||
|
next = { ...next, openRecordIn: payload.openRecordIn };
|
||||||
|
}
|
||||||
|
|
||||||
if ('avatarUrl' in payload) {
|
if ('avatarUrl' in payload) {
|
||||||
const value = payload.avatarUrl;
|
const value = payload.avatarUrl;
|
||||||
if (value === '' || isNull(value)) {
|
if (value === '' || isNull(value)) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { type Role, type WorkspaceMember } from '~/generated-metadata/graphql';
|
|||||||
export type PartialWorkspaceMember = Omit<
|
export type PartialWorkspaceMember = Omit<
|
||||||
WorkspaceMember,
|
WorkspaceMember,
|
||||||
| 'colorScheme'
|
| 'colorScheme'
|
||||||
|
| 'openRecordIn'
|
||||||
| 'locale'
|
| 'locale'
|
||||||
| 'timeZone'
|
| 'timeZone'
|
||||||
| 'dateFormat'
|
| 'dateFormat'
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { useCallback } from 'react';
|
|||||||
import { SOURCE_LOCALE, type APP_LOCALES } from 'twenty-shared/translations';
|
import { SOURCE_LOCALE, type APP_LOCALES } from 'twenty-shared/translations';
|
||||||
import { type ObjectPermissions } from 'twenty-shared/types';
|
import { type ObjectPermissions } from 'twenty-shared/types';
|
||||||
import { isDefined } from 'twenty-shared/utils';
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
import { toOpenRecordInPreference } from '@/workspace-member/utils/toOpenRecordInPreference';
|
||||||
import { type ColorScheme } from 'twenty-ui/input';
|
import { type ColorScheme } from 'twenty-ui/input';
|
||||||
import { useApolloClient } from '@apollo/client/react';
|
import { useApolloClient } from '@apollo/client/react';
|
||||||
import { GetCurrentUserDocument } from '~/generated-metadata/graphql';
|
import { GetCurrentUserDocument } from '~/generated-metadata/graphql';
|
||||||
@@ -88,6 +89,9 @@ export const useLoadCurrentUser = () => {
|
|||||||
workspaceMember = {
|
workspaceMember = {
|
||||||
...user.workspaceMember,
|
...user.workspaceMember,
|
||||||
colorScheme: user.workspaceMember?.colorScheme as ColorScheme,
|
colorScheme: user.workspaceMember?.colorScheme as ColorScheme,
|
||||||
|
openRecordIn: toOpenRecordInPreference(
|
||||||
|
user.workspaceMember?.openRecordIn,
|
||||||
|
),
|
||||||
locale: user.workspaceMember?.locale ?? SOURCE_LOCALE,
|
locale: user.workspaceMember?.locale ?? SOURCE_LOCALE,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+2
@@ -1,3 +1,4 @@
|
|||||||
|
import { ObjectOpenRecordIn } from 'twenty-shared/types';
|
||||||
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
||||||
import { WorkflowFieldsMultiSelect } from '@/workflow/components/WorkflowEditUpdateEventFieldsMultiSelect';
|
import { WorkflowFieldsMultiSelect } from '@/workflow/components/WorkflowEditUpdateEventFieldsMultiSelect';
|
||||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||||
@@ -72,6 +73,7 @@ const mockObjectMetadataItem: EnrichedObjectMetadataItem = {
|
|||||||
isSystem: false,
|
isSystem: false,
|
||||||
isUIEditable: true,
|
isUIEditable: true,
|
||||||
isUICreatable: true,
|
isUICreatable: true,
|
||||||
|
openRecordIn: ObjectOpenRecordIn.USER_CHOICE,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
createdAt: '',
|
createdAt: '',
|
||||||
updatedAt: '',
|
updatedAt: '',
|
||||||
|
|||||||
+1
@@ -8,6 +8,7 @@ export const WORKSPACE_MEMBER_QUERY_FRAGMENT = gql`
|
|||||||
lastName
|
lastName
|
||||||
}
|
}
|
||||||
colorScheme
|
colorScheme
|
||||||
|
openRecordIn
|
||||||
avatarUrl
|
avatarUrl
|
||||||
locale
|
locale
|
||||||
userEmail
|
userEmail
|
||||||
|
|||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||||
|
import { DEFAULT_OPEN_RECORD_IN_PREFERENCE } from '@/object-record/record-index/constants/DefaultOpenRecordInPreference';
|
||||||
|
import { createAtomSelector } from '@/ui/utilities/state/jotai/utils/createAtomSelector';
|
||||||
|
import { type OpenRecordIn } from 'twenty-shared/types';
|
||||||
|
|
||||||
|
// Narrowed so chips don't re-render on unrelated member changes.
|
||||||
|
export const openRecordInPreferenceState = createAtomSelector<OpenRecordIn>({
|
||||||
|
key: 'openRecordInPreferenceState',
|
||||||
|
get: ({ get }) =>
|
||||||
|
get(currentWorkspaceMemberState)?.openRecordIn ??
|
||||||
|
DEFAULT_OPEN_RECORD_IN_PREFERENCE,
|
||||||
|
});
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { type OpenRecordIn } from 'twenty-shared/types';
|
||||||
import {
|
import {
|
||||||
type WorkspaceMemberDateFormatEnum,
|
type WorkspaceMemberDateFormatEnum,
|
||||||
type WorkspaceMemberNumberFormatEnum,
|
type WorkspaceMemberNumberFormatEnum,
|
||||||
@@ -17,6 +18,7 @@ export type WorkspaceMember = {
|
|||||||
avatarUrl?: string | null;
|
avatarUrl?: string | null;
|
||||||
locale: string | null;
|
locale: string | null;
|
||||||
colorScheme: ColorScheme;
|
colorScheme: ColorScheme;
|
||||||
|
openRecordIn?: OpenRecordIn;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
userEmail: string;
|
userEmail: string;
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { DEFAULT_OPEN_RECORD_IN_PREFERENCE } from '@/object-record/record-index/constants/DefaultOpenRecordInPreference';
|
||||||
|
import { OpenRecordIn } from 'twenty-shared/types';
|
||||||
|
|
||||||
|
export const isOpenRecordIn = (value: unknown): value is OpenRecordIn =>
|
||||||
|
value === OpenRecordIn.SIDE_PANEL || value === OpenRecordIn.RECORD_PAGE;
|
||||||
|
|
||||||
|
export const toOpenRecordInPreference = (
|
||||||
|
openRecordIn: string | null | undefined,
|
||||||
|
): OpenRecordIn =>
|
||||||
|
isOpenRecordIn(openRecordIn)
|
||||||
|
? openRecordIn
|
||||||
|
: DEFAULT_OPEN_RECORD_IN_PREFERENCE;
|
||||||
+2
@@ -1,3 +1,4 @@
|
|||||||
|
import { ObjectOpenRecordIn } from 'twenty-shared/types';
|
||||||
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
|
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
|
||||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
||||||
@@ -244,6 +245,7 @@ const buildObjectMetadataItemsFromMarketplaceApp = (
|
|||||||
isSearchable: false,
|
isSearchable: false,
|
||||||
isUIEditable: true,
|
isUIEditable: true,
|
||||||
isUICreatable: true,
|
isUICreatable: true,
|
||||||
|
openRecordIn: ObjectOpenRecordIn.USER_CHOICE,
|
||||||
isLabelSyncedWithName: false,
|
isLabelSyncedWithName: false,
|
||||||
labelIdentifierFieldMetadataId: '',
|
labelIdentifierFieldMetadataId: '',
|
||||||
fields,
|
fields,
|
||||||
|
|||||||
+9
@@ -1,5 +1,6 @@
|
|||||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||||
import { FormatPreferencesSettings } from '@/settings/experience/components/FormatPreferencesSettings';
|
import { FormatPreferencesSettings } from '@/settings/experience/components/FormatPreferencesSettings';
|
||||||
|
import { OpenRecordInPreferencePicker } from '@/settings/experience/components/OpenRecordInPreferencePicker';
|
||||||
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
|
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
|
||||||
import { useColorScheme } from '@/ui/theme/hooks/useColorScheme';
|
import { useColorScheme } from '@/ui/theme/hooks/useColorScheme';
|
||||||
import { Trans, useLingui } from '@lingui/react/macro';
|
import { Trans, useLingui } from '@lingui/react/macro';
|
||||||
@@ -37,6 +38,14 @@ export const SettingsExperience = () => {
|
|||||||
/>
|
/>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
|
<Section>
|
||||||
|
<H2Title
|
||||||
|
title={t`Navigation`}
|
||||||
|
description={t`Choose where records open by default. Some objects may use a workspace setting`}
|
||||||
|
/>
|
||||||
|
<OpenRecordInPreferencePicker />
|
||||||
|
</Section>
|
||||||
|
|
||||||
<Section>
|
<Section>
|
||||||
<H2Title
|
<H2Title
|
||||||
title={t`Language`}
|
title={t`Language`}
|
||||||
|
|||||||
+1
@@ -10,6 +10,7 @@ exports[`stub-twenty-sdk-define plugin > matches the recorded export partition 1
|
|||||||
"HTTPMethod",
|
"HTTPMethod",
|
||||||
"NavigationMenuItemType",
|
"NavigationMenuItemType",
|
||||||
"NumberDataType",
|
"NumberDataType",
|
||||||
|
"ObjectOpenRecordIn",
|
||||||
"ObjectRecordGroupByDateGranularity",
|
"ObjectRecordGroupByDateGranularity",
|
||||||
"OnDeleteAction",
|
"OnDeleteAction",
|
||||||
"PageLayoutTabLayoutMode",
|
"PageLayoutTabLayoutMode",
|
||||||
|
|||||||
@@ -189,6 +189,7 @@ export {
|
|||||||
HTTPMethod,
|
HTTPMethod,
|
||||||
NavigationMenuItemType,
|
NavigationMenuItemType,
|
||||||
NumberDataType,
|
NumberDataType,
|
||||||
|
ObjectOpenRecordIn,
|
||||||
ObjectRecordGroupByDateGranularity,
|
ObjectRecordGroupByDateGranularity,
|
||||||
PageLayoutTabLayoutMode,
|
PageLayoutTabLayoutMode,
|
||||||
PageLayoutType,
|
PageLayoutType,
|
||||||
|
|||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
import { type QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
import { RegisteredInstanceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-instance-command.decorator';
|
||||||
|
import { type FastInstanceCommand } from 'src/engine/core-modules/upgrade/interfaces/fast-instance-command.interface';
|
||||||
|
|
||||||
|
@RegisteredInstanceCommand('2.27.0', 1785504900000)
|
||||||
|
export class AddOpenRecordInToObjectMetadataFastInstanceCommand
|
||||||
|
implements FastInstanceCommand
|
||||||
|
{
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TYPE "core"."objectMetadata_openrecordin_enum" AS ENUM('SIDE_PANEL', 'RECORD_PAGE', 'USER_CHOICE')`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "core"."objectMetadata" ADD "openRecordIn" "core"."objectMetadata_openrecordin_enum" NOT NULL DEFAULT 'USER_CHOICE'`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
'ALTER TABLE "core"."objectMetadata" DROP COLUMN "openRecordIn"',
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
'DROP TYPE "core"."objectMetadata_openrecordin_enum"',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+10
-2
@@ -1,18 +1,26 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { WorkspaceIteratorModule } from 'src/database/commands/command-runners/workspace-iterator.module';
|
import { WorkspaceIteratorModule } from 'src/database/commands/command-runners/workspace-iterator.module';
|
||||||
|
import { AddWorkspaceMemberOpenRecordInCommand } from 'src/database/commands/upgrade-version-command/2-27/2-27-workspace-command-1785505000000-add-workspace-member-open-record-in.command';
|
||||||
|
import { SeedObjectOpenRecordInCommand } from 'src/database/commands/upgrade-version-command/2-27/2-27-workspace-command-1785505100000-seed-object-open-record-in.command';
|
||||||
import { BackfillMissingStandardSkillsCommand } from 'src/database/commands/upgrade-version-command/2-27/2-27-workspace-command-1785499350000-backfill-standard-skills.command';
|
import { BackfillMissingStandardSkillsCommand } from 'src/database/commands/upgrade-version-command/2-27/2-27-workspace-command-1785499350000-backfill-standard-skills.command';
|
||||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||||
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
|
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
|
||||||
|
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/workspace-migration-runner.module';
|
||||||
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
|
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ApplicationModule,
|
ApplicationModule,
|
||||||
WorkspaceCacheModule,
|
WorkspaceCacheModule,
|
||||||
WorkspaceIteratorModule,
|
|
||||||
WorkspaceMigrationModule,
|
WorkspaceMigrationModule,
|
||||||
|
WorkspaceMigrationRunnerModule,
|
||||||
|
WorkspaceIteratorModule,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
AddWorkspaceMemberOpenRecordInCommand,
|
||||||
|
SeedObjectOpenRecordInCommand,
|
||||||
|
BackfillMissingStandardSkillsCommand,
|
||||||
],
|
],
|
||||||
providers: [BackfillMissingStandardSkillsCommand],
|
|
||||||
})
|
})
|
||||||
export class V2_27_UpgradeVersionCommandModule {}
|
export class V2_27_UpgradeVersionCommandModule {}
|
||||||
|
|||||||
+145
@@ -0,0 +1,145 @@
|
|||||||
|
import { Command } from 'nest-commander';
|
||||||
|
|
||||||
|
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
|
||||||
|
import { ProvisionedWorkspaceCommandRunner } from 'src/database/commands/command-runners/provisioned-workspace.command-runner';
|
||||||
|
import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service';
|
||||||
|
import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner';
|
||||||
|
import { getStandardFlatEntitiesToCreateOrThrow } from 'src/database/commands/upgrade-version-command/2-10/utils/get-standard-flat-entities-to-create-or-throw.util';
|
||||||
|
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||||
|
import { RegisteredWorkspaceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator';
|
||||||
|
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||||
|
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||||
|
import { computeTwentyStandardApplicationAllFlatEntityMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/twenty-standard-application-all-flat-entity-maps.constant';
|
||||||
|
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
|
||||||
|
|
||||||
|
const WORKSPACE_MEMBER_OPEN_RECORD_IN_FIELD_UNIVERSAL_IDENTIFIERS = [
|
||||||
|
STANDARD_OBJECTS.workspaceMember.fields.openRecordIn.universalIdentifier,
|
||||||
|
];
|
||||||
|
|
||||||
|
@RegisteredWorkspaceCommand('2.27.0', 1785505000000)
|
||||||
|
@Command({
|
||||||
|
name: 'upgrade:2-27:add-workspace-member-open-record-in',
|
||||||
|
description:
|
||||||
|
'Create the workspace member openRecordIn preference field in existing workspaces',
|
||||||
|
})
|
||||||
|
export class AddWorkspaceMemberOpenRecordInCommand extends ProvisionedWorkspaceCommandRunner {
|
||||||
|
constructor(
|
||||||
|
protected readonly workspaceIteratorService: WorkspaceIteratorService,
|
||||||
|
private readonly applicationService: ApplicationService,
|
||||||
|
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||||
|
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
|
||||||
|
) {
|
||||||
|
super(workspaceIteratorService);
|
||||||
|
}
|
||||||
|
|
||||||
|
override async runOnWorkspace({
|
||||||
|
workspaceId,
|
||||||
|
options,
|
||||||
|
}: RunOnWorkspaceArgs): Promise<void> {
|
||||||
|
const isDryRun = options.dryRun ?? false;
|
||||||
|
|
||||||
|
const { flatObjectMetadataMaps, flatFieldMetadataMaps } =
|
||||||
|
await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||||
|
'flatObjectMetadataMaps',
|
||||||
|
'flatFieldMetadataMaps',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const existingWorkspaceMemberObjectMetadata =
|
||||||
|
flatObjectMetadataMaps.byUniversalIdentifier[
|
||||||
|
STANDARD_OBJECTS.workspaceMember.universalIdentifier
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!isDefined(existingWorkspaceMemberObjectMetadata)) {
|
||||||
|
this.logger.log(
|
||||||
|
`workspaceMember object metadata does not exist for workspace ${workspaceId}, skipping`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cheap idempotency check before building the whole standard application.
|
||||||
|
if (
|
||||||
|
WORKSPACE_MEMBER_OPEN_RECORD_IN_FIELD_UNIVERSAL_IDENTIFIERS.every(
|
||||||
|
(universalIdentifier) =>
|
||||||
|
isDefined(
|
||||||
|
flatFieldMetadataMaps.byUniversalIdentifier[universalIdentifier],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
this.logger.log(
|
||||||
|
`workspaceMember openRecordIn already exists for workspace ${workspaceId}, skipping`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { twentyStandardFlatApplication } =
|
||||||
|
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||||
|
{ workspaceId },
|
||||||
|
);
|
||||||
|
|
||||||
|
const { allFlatEntityMaps: standardAllFlatEntityMaps } =
|
||||||
|
computeTwentyStandardApplicationAllFlatEntityMaps({
|
||||||
|
now: new Date().toISOString(),
|
||||||
|
workspaceId,
|
||||||
|
twentyStandardApplicationId: twentyStandardFlatApplication.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const fieldsToCreate =
|
||||||
|
getStandardFlatEntitiesToCreateOrThrow<FlatFieldMetadata>({
|
||||||
|
standardFlatEntityMaps: standardAllFlatEntityMaps.flatFieldMetadataMaps,
|
||||||
|
existingFlatEntityMaps: flatFieldMetadataMaps,
|
||||||
|
universalIdentifiers:
|
||||||
|
WORKSPACE_MEMBER_OPEN_RECORD_IN_FIELD_UNIVERSAL_IDENTIFIERS,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (fieldsToCreate.length === 0) {
|
||||||
|
this.logger.log(
|
||||||
|
`workspaceMember openRecordIn already exists for workspace ${workspaceId}, skipping`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log(
|
||||||
|
`${isDryRun ? '[DRY RUN] ' : ''}Creating the workspaceMember openRecordIn field for workspace ${workspaceId}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isDryRun) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result =
|
||||||
|
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunLegacyWorkspaceMigration(
|
||||||
|
{
|
||||||
|
isSystemBuild: true,
|
||||||
|
applicationUniversalIdentifier:
|
||||||
|
twentyStandardFlatApplication.universalIdentifier,
|
||||||
|
workspaceId,
|
||||||
|
allFlatEntityOperationByMetadataName: {
|
||||||
|
fieldMetadata: {
|
||||||
|
flatEntityToCreate: fieldsToCreate,
|
||||||
|
flatEntityToDelete: [],
|
||||||
|
flatEntityToUpdate: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.status === 'fail') {
|
||||||
|
this.logger.error(
|
||||||
|
`Failed to create the workspaceMember openRecordIn field:\n${JSON.stringify(result, null, 2)}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`Failed to create the workspaceMember openRecordIn field for workspace ${workspaceId}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log(
|
||||||
|
`Created the workspaceMember openRecordIn field for workspace ${workspaceId}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+195
@@ -0,0 +1,195 @@
|
|||||||
|
import { Command } from 'nest-commander';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ObjectOpenRecordIn,
|
||||||
|
ViewKey,
|
||||||
|
ViewOpenRecordIn,
|
||||||
|
} from 'twenty-shared/types';
|
||||||
|
import { isDefined } from 'twenty-shared/utils';
|
||||||
|
|
||||||
|
import { ProvisionedWorkspaceCommandRunner } from 'src/database/commands/command-runners/provisioned-workspace.command-runner';
|
||||||
|
import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service';
|
||||||
|
import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner';
|
||||||
|
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||||
|
import { RegisteredWorkspaceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator';
|
||||||
|
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||||
|
import { computeTwentyStandardApplicationAllFlatEntityMaps } from 'src/engine/workspace-manager/twenty-standard-application/utils/twenty-standard-application-all-flat-entity-maps.constant';
|
||||||
|
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
|
||||||
|
|
||||||
|
@RegisteredWorkspaceCommand('2.27.0', 1785505100000)
|
||||||
|
@Command({
|
||||||
|
name: 'upgrade:2-27:seed-object-open-record-in',
|
||||||
|
description:
|
||||||
|
'Seed objectMetadata.openRecordIn from the standard definitions and from deliberate per-view record page choices',
|
||||||
|
})
|
||||||
|
export class SeedObjectOpenRecordInCommand extends ProvisionedWorkspaceCommandRunner {
|
||||||
|
// Workspace-invariant, so the standard application is only built once per run.
|
||||||
|
private standardOpenRecordInByUniversalIdentifier?: Record<
|
||||||
|
string,
|
||||||
|
ObjectOpenRecordIn
|
||||||
|
>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected readonly workspaceIteratorService: WorkspaceIteratorService,
|
||||||
|
private readonly applicationService: ApplicationService,
|
||||||
|
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||||
|
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
|
||||||
|
) {
|
||||||
|
super(workspaceIteratorService);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getStandardOpenRecordInByUniversalIdentifier(
|
||||||
|
workspaceId: string,
|
||||||
|
): Promise<Record<string, ObjectOpenRecordIn>> {
|
||||||
|
if (isDefined(this.standardOpenRecordInByUniversalIdentifier)) {
|
||||||
|
return this.standardOpenRecordInByUniversalIdentifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { twentyStandardFlatApplication } =
|
||||||
|
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||||
|
{ workspaceId },
|
||||||
|
);
|
||||||
|
|
||||||
|
const { allFlatEntityMaps: standardAllFlatEntityMaps } =
|
||||||
|
computeTwentyStandardApplicationAllFlatEntityMaps({
|
||||||
|
now: new Date().toISOString(),
|
||||||
|
workspaceId,
|
||||||
|
twentyStandardApplicationId: twentyStandardFlatApplication.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.standardOpenRecordInByUniversalIdentifier = Object.fromEntries(
|
||||||
|
Object.values(
|
||||||
|
standardAllFlatEntityMaps.flatObjectMetadataMaps.byUniversalIdentifier,
|
||||||
|
)
|
||||||
|
.filter(isDefined)
|
||||||
|
.filter(
|
||||||
|
(standardObjectMetadata) =>
|
||||||
|
standardObjectMetadata.openRecordIn !==
|
||||||
|
ObjectOpenRecordIn.USER_CHOICE,
|
||||||
|
)
|
||||||
|
.map((standardObjectMetadata) => [
|
||||||
|
standardObjectMetadata.universalIdentifier,
|
||||||
|
standardObjectMetadata.openRecordIn,
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.standardOpenRecordInByUniversalIdentifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
override async runOnWorkspace({
|
||||||
|
workspaceId,
|
||||||
|
options,
|
||||||
|
}: RunOnWorkspaceArgs): Promise<void> {
|
||||||
|
const isDryRun = options.dryRun ?? false;
|
||||||
|
|
||||||
|
const { flatObjectMetadataMaps, flatViewMaps } =
|
||||||
|
await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||||
|
'flatObjectMetadataMaps',
|
||||||
|
'flatViewMaps',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const targetOpenRecordInByUniversalIdentifier: Record<
|
||||||
|
string,
|
||||||
|
ObjectOpenRecordIn
|
||||||
|
> = {
|
||||||
|
...(await this.getStandardOpenRecordInByUniversalIdentifier(workspaceId)),
|
||||||
|
};
|
||||||
|
|
||||||
|
// A deliberate per-view record page choice is lifted to the object, unless
|
||||||
|
// the standard definitions already pin that object.
|
||||||
|
for (const flatView of Object.values(flatViewMaps.byUniversalIdentifier)) {
|
||||||
|
if (
|
||||||
|
isDefined(flatView) &&
|
||||||
|
flatView.key === ViewKey.INDEX &&
|
||||||
|
flatView.openRecordIn === ViewOpenRecordIn.RECORD_PAGE &&
|
||||||
|
!isDefined(
|
||||||
|
targetOpenRecordInByUniversalIdentifier[
|
||||||
|
flatView.objectMetadataUniversalIdentifier
|
||||||
|
],
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
targetOpenRecordInByUniversalIdentifier[
|
||||||
|
flatView.objectMetadataUniversalIdentifier
|
||||||
|
] = ObjectOpenRecordIn.RECORD_PAGE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
|
const objectMetadatasToUpdate = Object.values(
|
||||||
|
flatObjectMetadataMaps.byUniversalIdentifier,
|
||||||
|
)
|
||||||
|
.filter(isDefined)
|
||||||
|
.flatMap((flatObjectMetadata) => {
|
||||||
|
const targetOpenRecordIn =
|
||||||
|
targetOpenRecordInByUniversalIdentifier[
|
||||||
|
flatObjectMetadata.universalIdentifier
|
||||||
|
];
|
||||||
|
|
||||||
|
if (
|
||||||
|
!isDefined(targetOpenRecordIn) ||
|
||||||
|
flatObjectMetadata.openRecordIn === targetOpenRecordIn
|
||||||
|
) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
...flatObjectMetadata,
|
||||||
|
openRecordIn: targetOpenRecordIn,
|
||||||
|
updatedAt: now,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
if (objectMetadatasToUpdate.length === 0) {
|
||||||
|
this.logger.log(
|
||||||
|
`Object openRecordIn already seeded for workspace ${workspaceId}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log(
|
||||||
|
`${isDryRun ? '[DRY RUN] ' : ''}Workspace ${workspaceId}: seeding openRecordIn on ${objectMetadatasToUpdate.length} object(s)`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isDryRun) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { twentyStandardFlatApplication } =
|
||||||
|
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||||
|
{ workspaceId },
|
||||||
|
);
|
||||||
|
|
||||||
|
const result =
|
||||||
|
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunLegacyWorkspaceMigration(
|
||||||
|
{
|
||||||
|
isSystemBuild: true,
|
||||||
|
applicationUniversalIdentifier:
|
||||||
|
twentyStandardFlatApplication.universalIdentifier,
|
||||||
|
workspaceId,
|
||||||
|
allFlatEntityOperationByMetadataName: {
|
||||||
|
objectMetadata: {
|
||||||
|
flatEntityToCreate: [],
|
||||||
|
flatEntityToDelete: [],
|
||||||
|
flatEntityToUpdate: objectMetadatasToUpdate,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.status === 'fail') {
|
||||||
|
this.logger.error(
|
||||||
|
`Failed to seed object openRecordIn:\n${JSON.stringify(result, null, 2)}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`Failed to seed object openRecordIn for workspace ${workspaceId}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log(`Seeded object openRecordIn for workspace ${workspaceId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
export const ADD_OBJECT_METADATA_OPEN_RECORD_IN_UPGRADE_COMMAND_NAME =
|
||||||
|
'2.27.0_AddOpenRecordInToObjectMetadataFastInstanceCommand_1785504900000';
|
||||||
+2
@@ -131,6 +131,7 @@ import { AddPageLayoutCascadeDeleteIndexesFastInstanceCommand } from './2-25/2-2
|
|||||||
import { AddChannelWebhookSubscriptionExternalIdIndexesFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-25/2-25-instance-command-fast-1785173910915-add-channel-webhook-subscription-external-id-indexes';
|
import { AddChannelWebhookSubscriptionExternalIdIndexesFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-25/2-25-instance-command-fast-1785173910915-add-channel-webhook-subscription-external-id-indexes';
|
||||||
import { AddIsHiddenToAgentMessageFastInstanceCommand } from './2-25/2-25-instance-command-fast-1785230296000-add-is-hidden-to-agent-message';
|
import { AddIsHiddenToAgentMessageFastInstanceCommand } from './2-25/2-25-instance-command-fast-1785230296000-add-is-hidden-to-agent-message';
|
||||||
import { AddConnectedAccountHandleProviderIndexFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-26/2-26-instance-command-fast-1785420705255-add-connected-account-handle-provider-index';
|
import { AddConnectedAccountHandleProviderIndexFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-26/2-26-instance-command-fast-1785420705255-add-connected-account-handle-provider-index';
|
||||||
|
import { AddOpenRecordInToObjectMetadataFastInstanceCommand } from 'src/database/commands/upgrade-version-command/2-27/2-27-instance-command-fast-1785504900000-add-open-record-in-to-object-metadata';
|
||||||
|
|
||||||
export const INSTANCE_COMMANDS = [
|
export const INSTANCE_COMMANDS = [
|
||||||
AddViewFieldGroupIdIndexOnViewFieldFastInstanceCommand,
|
AddViewFieldGroupIdIndexOnViewFieldFastInstanceCommand,
|
||||||
@@ -264,4 +265,5 @@ export const INSTANCE_COMMANDS = [
|
|||||||
AddChannelWebhookSubscriptionExternalIdIndexesFastInstanceCommand,
|
AddChannelWebhookSubscriptionExternalIdIndexesFastInstanceCommand,
|
||||||
AddIsHiddenToAgentMessageFastInstanceCommand,
|
AddIsHiddenToAgentMessageFastInstanceCommand,
|
||||||
AddConnectedAccountHandleProviderIndexFastInstanceCommand,
|
AddConnectedAccountHandleProviderIndexFastInstanceCommand,
|
||||||
|
AddOpenRecordInToObjectMetadataFastInstanceCommand,
|
||||||
];
|
];
|
||||||
|
|||||||
+2
-1
@@ -1,4 +1,4 @@
|
|||||||
import { FieldMetadataType } from 'twenty-shared/types';
|
import { FieldMetadataType, ObjectOpenRecordIn } from 'twenty-shared/types';
|
||||||
|
|
||||||
import { type WorkspaceEntityDuplicateCriteria } from 'src/engine/api/graphql/workspace-query-builder/types/workspace-entity-duplicate-criteria.type';
|
import { type WorkspaceEntityDuplicateCriteria } from 'src/engine/api/graphql/workspace-query-builder/types/workspace-entity-duplicate-criteria.type';
|
||||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||||
@@ -166,6 +166,7 @@ export const mockPersonFlatObjectMetadata = (
|
|||||||
overrides: null,
|
overrides: null,
|
||||||
isUIEditable: true,
|
isUIEditable: true,
|
||||||
isUICreatable: true,
|
isUICreatable: true,
|
||||||
|
openRecordIn: ObjectOpenRecordIn.USER_CHOICE,
|
||||||
applicationUniversalIdentifier: 'test-application-id',
|
applicationUniversalIdentifier: 'test-application-id',
|
||||||
fieldUniversalIdentifiers: mockFieldMetadatas.map(
|
fieldUniversalIdentifiers: mockFieldMetadatas.map(
|
||||||
(field) => field.universalIdentifier,
|
(field) => field.universalIdentifier,
|
||||||
|
|||||||
+2
@@ -1,4 +1,5 @@
|
|||||||
import { type ObjectManifest } from 'twenty-shared/application';
|
import { type ObjectManifest } from 'twenty-shared/application';
|
||||||
|
import { ObjectOpenRecordIn } from 'twenty-shared/types';
|
||||||
|
|
||||||
import { type UniversalFlatObjectMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-object-metadata.type';
|
import { type UniversalFlatObjectMetadata } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-object-metadata.type';
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ export const fromObjectManifestToUniversalFlatObjectMetadata = ({
|
|||||||
labelSingular: objectManifest.labelSingular,
|
labelSingular: objectManifest.labelSingular,
|
||||||
labelPlural: objectManifest.labelPlural,
|
labelPlural: objectManifest.labelPlural,
|
||||||
color: null,
|
color: null,
|
||||||
|
openRecordIn: objectManifest.openRecordIn ?? ObjectOpenRecordIn.USER_CHOICE,
|
||||||
description: objectManifest.description ?? null,
|
description: objectManifest.description ?? null,
|
||||||
icon: objectManifest.icon ?? null,
|
icon: objectManifest.icon ?? null,
|
||||||
overrides: null,
|
overrides: null,
|
||||||
|
|||||||
+6
-1
@@ -1,4 +1,8 @@
|
|||||||
import { FieldMetadataType, RelationType } from 'twenty-shared/types';
|
import {
|
||||||
|
FieldMetadataType,
|
||||||
|
ObjectOpenRecordIn,
|
||||||
|
RelationType,
|
||||||
|
} from 'twenty-shared/types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
computeUpdatedFieldsFromDiff,
|
computeUpdatedFieldsFromDiff,
|
||||||
@@ -39,6 +43,7 @@ const mockObjectMetadata: FlatObjectMetadata = {
|
|||||||
overrides: null,
|
overrides: null,
|
||||||
isUIEditable: true,
|
isUIEditable: true,
|
||||||
isUICreatable: true,
|
isUICreatable: true,
|
||||||
|
openRecordIn: ObjectOpenRecordIn.USER_CHOICE,
|
||||||
labelIdentifierFieldMetadataId: null,
|
labelIdentifierFieldMetadataId: null,
|
||||||
imageIdentifierFieldMetadataId: null,
|
imageIdentifierFieldMetadataId: null,
|
||||||
duplicateCriteria: null,
|
duplicateCriteria: null,
|
||||||
|
|||||||
+2
@@ -1,3 +1,4 @@
|
|||||||
|
import { OpenRecordIn } from 'twenty-shared/types';
|
||||||
import { Test, type TestingModule } from '@nestjs/testing';
|
import { Test, type TestingModule } from '@nestjs/testing';
|
||||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||||
|
|
||||||
@@ -302,6 +303,7 @@ describe('UserWorkspaceService', () => {
|
|||||||
lastName: user.lastName,
|
lastName: user.lastName,
|
||||||
},
|
},
|
||||||
colorScheme: 'System',
|
colorScheme: 'System',
|
||||||
|
openRecordIn: OpenRecordIn.SIDE_PANEL,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
userEmail: user.email,
|
userEmail: user.email,
|
||||||
locale: 'en',
|
locale: 'en',
|
||||||
|
|||||||
+2
-1
@@ -2,7 +2,7 @@ import { Logger } from '@nestjs/common';
|
|||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import { type APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations';
|
import { type APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations';
|
||||||
import { FileFolder } from 'twenty-shared/types';
|
import { FileFolder, OpenRecordIn } from 'twenty-shared/types';
|
||||||
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
|
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
|
||||||
import { IsNull, Not, type QueryRunner, type Repository } from 'typeorm';
|
import { IsNull, Not, type QueryRunner, type Repository } from 'typeorm';
|
||||||
|
|
||||||
@@ -188,6 +188,7 @@ export class UserWorkspaceService {
|
|||||||
lastName: user.lastName,
|
lastName: user.lastName,
|
||||||
},
|
},
|
||||||
colorScheme: 'System',
|
colorScheme: 'System',
|
||||||
|
openRecordIn: OpenRecordIn.SIDE_PANEL,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
userEmail: user.email,
|
userEmail: user.email,
|
||||||
avatarUrl: userWorkspace.defaultAvatarUrl ?? null,
|
avatarUrl: userWorkspace.defaultAvatarUrl ?? null,
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { Field, Int, ObjectType } from '@nestjs/graphql';
|
import { Field, Int, ObjectType, registerEnumType } from '@nestjs/graphql';
|
||||||
|
|
||||||
|
import { OpenRecordIn } from 'twenty-shared/types';
|
||||||
import { Max, Min } from 'class-validator';
|
import { Max, Min } from 'class-validator';
|
||||||
|
|
||||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||||
@@ -9,6 +11,8 @@ import {
|
|||||||
WorkspaceMemberTimeFormatEnum,
|
WorkspaceMemberTimeFormatEnum,
|
||||||
} from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
} from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||||
|
|
||||||
|
registerEnumType(OpenRecordIn, { name: 'OpenRecordIn' });
|
||||||
|
|
||||||
@ObjectType('FullName')
|
@ObjectType('FullName')
|
||||||
export class FullNameDTO {
|
export class FullNameDTO {
|
||||||
@Field({ nullable: false })
|
@Field({ nullable: false })
|
||||||
@@ -32,6 +36,9 @@ export class WorkspaceMemberDTO {
|
|||||||
@Field({ nullable: false })
|
@Field({ nullable: false })
|
||||||
colorScheme: string;
|
colorScheme: string;
|
||||||
|
|
||||||
|
@Field(() => OpenRecordIn, { nullable: false })
|
||||||
|
openRecordIn: OpenRecordIn;
|
||||||
|
|
||||||
@Field({ nullable: true })
|
@Field({ nullable: true })
|
||||||
avatarUrl: string;
|
avatarUrl: string;
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -16,7 +16,7 @@ import {
|
|||||||
type WorkspaceMemberTimeFormatEnum,
|
type WorkspaceMemberTimeFormatEnum,
|
||||||
type WorkspaceMemberWorkspaceEntity,
|
type WorkspaceMemberWorkspaceEntity,
|
||||||
} from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
} from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||||
import { FileFolder } from 'twenty-shared/types';
|
import { FileFolder, type OpenRecordIn } from 'twenty-shared/types';
|
||||||
|
|
||||||
export type ToWorkspaceMemberDtoArgs = {
|
export type ToWorkspaceMemberDtoArgs = {
|
||||||
workspaceMemberEntity: WorkspaceMemberWorkspaceEntity;
|
workspaceMemberEntity: WorkspaceMemberWorkspaceEntity;
|
||||||
@@ -69,6 +69,7 @@ export class WorkspaceMemberTranspiler {
|
|||||||
name,
|
name,
|
||||||
userEmail,
|
userEmail,
|
||||||
colorScheme,
|
colorScheme,
|
||||||
|
openRecordIn,
|
||||||
locale,
|
locale,
|
||||||
timeFormat,
|
timeFormat,
|
||||||
timeZone,
|
timeZone,
|
||||||
@@ -98,6 +99,7 @@ export class WorkspaceMemberTranspiler {
|
|||||||
avatarUrl,
|
avatarUrl,
|
||||||
userWorkspaceId: userWorkspace.id,
|
userWorkspaceId: userWorkspace.id,
|
||||||
colorScheme,
|
colorScheme,
|
||||||
|
openRecordIn: openRecordIn as OpenRecordIn,
|
||||||
dateFormat: dateFormat as WorkspaceMemberDateFormatEnum,
|
dateFormat: dateFormat as WorkspaceMemberDateFormatEnum,
|
||||||
locale,
|
locale,
|
||||||
timeFormat: timeFormat as WorkspaceMemberTimeFormatEnum,
|
timeFormat: timeFormat as WorkspaceMemberTimeFormatEnum,
|
||||||
|
|||||||
+1
@@ -166,6 +166,7 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should ma
|
|||||||
},
|
},
|
||||||
"objectMetadata": {
|
"objectMetadata": {
|
||||||
"propertiesToCompare": [
|
"propertiesToCompare": [
|
||||||
|
"openRecordIn",
|
||||||
"color",
|
"color",
|
||||||
"description",
|
"description",
|
||||||
"icon",
|
"icon",
|
||||||
|
|||||||
+1
@@ -28,6 +28,7 @@ exports[`registry-derived override property maps derives the overridable propert
|
|||||||
"logicFunction": [],
|
"logicFunction": [],
|
||||||
"navigationMenuItem": [],
|
"navigationMenuItem": [],
|
||||||
"objectMetadata": [
|
"objectMetadata": [
|
||||||
|
"openRecordIn",
|
||||||
"color",
|
"color",
|
||||||
"description",
|
"description",
|
||||||
"icon",
|
"icon",
|
||||||
|
|||||||
+6
@@ -170,6 +170,12 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
|
|||||||
toStringify: false,
|
toStringify: false,
|
||||||
universalProperty: undefined,
|
universalProperty: undefined,
|
||||||
},
|
},
|
||||||
|
openRecordIn: {
|
||||||
|
toCompare: true,
|
||||||
|
toStringify: false,
|
||||||
|
universalProperty: undefined,
|
||||||
|
isOverridable: true,
|
||||||
|
},
|
||||||
color: {
|
color: {
|
||||||
toCompare: true,
|
toCompare: true,
|
||||||
toStringify: false,
|
toStringify: false,
|
||||||
|
|||||||
+1
@@ -41,6 +41,7 @@ type Assertions = [
|
|||||||
keyof FlatEntityUpdate<'objectMetadata'>,
|
keyof FlatEntityUpdate<'objectMetadata'>,
|
||||||
| 'icon'
|
| 'icon'
|
||||||
| 'color'
|
| 'color'
|
||||||
|
| 'openRecordIn'
|
||||||
| 'description'
|
| 'description'
|
||||||
| 'isActive'
|
| 'isActive'
|
||||||
| 'overrides'
|
| 'overrides'
|
||||||
|
|||||||
+2
@@ -1,3 +1,4 @@
|
|||||||
|
import { ObjectOpenRecordIn } from 'twenty-shared/types';
|
||||||
import { faker } from '@faker-js/faker';
|
import { faker } from '@faker-js/faker';
|
||||||
import { TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER } from 'twenty-shared/application';
|
import { TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER } from 'twenty-shared/application';
|
||||||
|
|
||||||
@@ -37,6 +38,7 @@ export const getFlatObjectMetadataMock = (
|
|||||||
isSystem: false,
|
isSystem: false,
|
||||||
isUIEditable: true,
|
isUIEditable: true,
|
||||||
isUICreatable: true,
|
isUICreatable: true,
|
||||||
|
openRecordIn: ObjectOpenRecordIn.USER_CHOICE,
|
||||||
labelIdentifierFieldMetadataId,
|
labelIdentifierFieldMetadataId,
|
||||||
labelPlural: 'default flat object metadata label plural',
|
labelPlural: 'default flat object metadata label plural',
|
||||||
labelSingular: 'default flat object metadata label singular',
|
labelSingular: 'default flat object metadata label singular',
|
||||||
|
|||||||
+2
@@ -3,6 +3,7 @@ import { type MetadataEntityPropertyName } from 'src/engine/metadata-modules/fla
|
|||||||
export const FLAT_OBJECT_METADATA_EDITABLE_PROPERTIES = {
|
export const FLAT_OBJECT_METADATA_EDITABLE_PROPERTIES = {
|
||||||
custom: [
|
custom: [
|
||||||
'color',
|
'color',
|
||||||
|
'openRecordIn',
|
||||||
'description',
|
'description',
|
||||||
'icon',
|
'icon',
|
||||||
'isActive',
|
'isActive',
|
||||||
@@ -17,6 +18,7 @@ export const FLAT_OBJECT_METADATA_EDITABLE_PROPERTIES = {
|
|||||||
],
|
],
|
||||||
standard: [
|
standard: [
|
||||||
'color',
|
'color',
|
||||||
|
'openRecordIn',
|
||||||
'description',
|
'description',
|
||||||
'icon',
|
'icon',
|
||||||
'isActive',
|
'isActive',
|
||||||
|
|||||||
+2
@@ -1,4 +1,5 @@
|
|||||||
import { getFieldUniversalIdentifier } from 'twenty-shared/application';
|
import { getFieldUniversalIdentifier } from 'twenty-shared/application';
|
||||||
|
import { ObjectOpenRecordIn } from 'twenty-shared/types';
|
||||||
import {
|
import {
|
||||||
capitalize,
|
capitalize,
|
||||||
isDefined,
|
isDefined,
|
||||||
@@ -60,6 +61,7 @@ export const fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCre
|
|||||||
updatedAt: createdAt,
|
updatedAt: createdAt,
|
||||||
duplicateCriteria: null,
|
duplicateCriteria: null,
|
||||||
color: createObjectInput.color ?? null,
|
color: createObjectInput.color ?? null,
|
||||||
|
openRecordIn: ObjectOpenRecordIn.USER_CHOICE,
|
||||||
description: createObjectInput.description ?? null,
|
description: createObjectInput.description ?? null,
|
||||||
icon: createObjectInput.icon ?? null,
|
icon: createObjectInput.icon ?? null,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
|
|||||||
+2
@@ -19,6 +19,7 @@ export const fromFlatObjectMetadataToObjectMetadataDto = (
|
|||||||
isLabelSyncedWithName,
|
isLabelSyncedWithName,
|
||||||
isRemote,
|
isRemote,
|
||||||
isSearchable,
|
isSearchable,
|
||||||
|
openRecordIn,
|
||||||
isSystem,
|
isSystem,
|
||||||
isUIEditable,
|
isUIEditable,
|
||||||
isUICreatable,
|
isUICreatable,
|
||||||
@@ -39,6 +40,7 @@ export const fromFlatObjectMetadataToObjectMetadataDto = (
|
|||||||
isLabelSyncedWithName,
|
isLabelSyncedWithName,
|
||||||
isRemote,
|
isRemote,
|
||||||
isSearchable,
|
isSearchable,
|
||||||
|
openRecordIn,
|
||||||
isSystem,
|
isSystem,
|
||||||
isUIEditable,
|
isUIEditable,
|
||||||
isUICreatable,
|
isUICreatable,
|
||||||
|
|||||||
+13
-1
@@ -1,4 +1,11 @@
|
|||||||
import { Field, HideField, ObjectType } from '@nestjs/graphql';
|
import {
|
||||||
|
Field,
|
||||||
|
HideField,
|
||||||
|
ObjectType,
|
||||||
|
registerEnumType,
|
||||||
|
} from '@nestjs/graphql';
|
||||||
|
|
||||||
|
import { ObjectOpenRecordIn } from 'twenty-shared/types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Authorize,
|
Authorize,
|
||||||
@@ -14,6 +21,8 @@ import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dto
|
|||||||
import { IndexMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-metadata.dto';
|
import { IndexMetadataDTO } from 'src/engine/metadata-modules/index-metadata/dtos/index-metadata.dto';
|
||||||
import { type ObjectMetadataOverrides } from 'src/engine/metadata-modules/object-metadata/types/object-metadata-overrides.type';
|
import { type ObjectMetadataOverrides } from 'src/engine/metadata-modules/object-metadata/types/object-metadata-overrides.type';
|
||||||
|
|
||||||
|
registerEnumType(ObjectOpenRecordIn, { name: 'ObjectOpenRecordIn' });
|
||||||
|
|
||||||
@ObjectType('Object')
|
@ObjectType('Object')
|
||||||
@Authorize({
|
@Authorize({
|
||||||
// oxlint-disable-next-line typescript/no-explicit-any
|
// oxlint-disable-next-line typescript/no-explicit-any
|
||||||
@@ -87,6 +96,9 @@ export class ObjectMetadataDTO {
|
|||||||
@FilterableField()
|
@FilterableField()
|
||||||
isSearchable: boolean;
|
isSearchable: boolean;
|
||||||
|
|
||||||
|
@Field(() => ObjectOpenRecordIn)
|
||||||
|
openRecordIn: ObjectOpenRecordIn;
|
||||||
|
|
||||||
@HideField()
|
@HideField()
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
|
|
||||||
|
|||||||
+7
@@ -1,8 +1,10 @@
|
|||||||
import { Field, InputType } from '@nestjs/graphql';
|
import { Field, InputType } from '@nestjs/graphql';
|
||||||
|
|
||||||
import { Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
|
import { ObjectOpenRecordIn } from 'twenty-shared/types';
|
||||||
import {
|
import {
|
||||||
IsBoolean,
|
IsBoolean,
|
||||||
|
IsEnum,
|
||||||
IsNotEmpty,
|
IsNotEmpty,
|
||||||
IsOptional,
|
IsOptional,
|
||||||
IsString,
|
IsString,
|
||||||
@@ -81,6 +83,11 @@ export class UpdateObjectPayload {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@Field({ nullable: true })
|
@Field({ nullable: true })
|
||||||
isSearchable?: boolean;
|
isSearchable?: boolean;
|
||||||
|
|
||||||
|
@IsEnum(ObjectOpenRecordIn)
|
||||||
|
@IsOptional()
|
||||||
|
@Field(() => ObjectOpenRecordIn, { nullable: true })
|
||||||
|
openRecordIn?: ObjectOpenRecordIn;
|
||||||
}
|
}
|
||||||
|
|
||||||
@InputType()
|
@InputType()
|
||||||
|
|||||||
+13
@@ -9,7 +9,10 @@ import {
|
|||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
|
|
||||||
|
import { ObjectOpenRecordIn } from 'twenty-shared/types';
|
||||||
|
|
||||||
import { ADD_METADATA_OVERRIDES_COLUMN_UPGRADE_COMMAND_NAME } from 'src/database/commands/upgrade-version-command/2-19/add-metadata-overrides-column-upgrade-command-name.constant';
|
import { ADD_METADATA_OVERRIDES_COLUMN_UPGRADE_COMMAND_NAME } from 'src/database/commands/upgrade-version-command/2-19/add-metadata-overrides-column-upgrade-command-name.constant';
|
||||||
|
import { ADD_OBJECT_METADATA_OPEN_RECORD_IN_UPGRADE_COMMAND_NAME } from 'src/database/commands/upgrade-version-command/2-27/add-object-metadata-open-record-in-upgrade-command-name.constant';
|
||||||
import { DROP_METADATA_STANDARD_OVERRIDES_COLUMN_UPGRADE_COMMAND_NAME } from 'src/database/commands/upgrade-version-command/2-20/drop-metadata-standard-overrides-column-upgrade-command-name.constant';
|
import { DROP_METADATA_STANDARD_OVERRIDES_COLUMN_UPGRADE_COMMAND_NAME } from 'src/database/commands/upgrade-version-command/2-20/drop-metadata-standard-overrides-column-upgrade-command-name.constant';
|
||||||
import { type WorkspaceEntityDuplicateCriteria } from 'src/engine/api/graphql/workspace-query-builder/types/workspace-entity-duplicate-criteria.type';
|
import { type WorkspaceEntityDuplicateCriteria } from 'src/engine/api/graphql/workspace-query-builder/types/workspace-entity-duplicate-criteria.type';
|
||||||
import { WasIntroducedInUpgrade } from 'src/engine/core-modules/upgrade/decorators/was-introduced-in-upgrade.decorator';
|
import { WasIntroducedInUpgrade } from 'src/engine/core-modules/upgrade/decorators/was-introduced-in-upgrade.decorator';
|
||||||
@@ -66,6 +69,16 @@ export class ObjectMetadataEntity
|
|||||||
@Column({ nullable: true, type: 'text' })
|
@Column({ nullable: true, type: 'text' })
|
||||||
color: string | null;
|
color: string | null;
|
||||||
|
|
||||||
|
@WasIntroducedInUpgrade({
|
||||||
|
upgradeCommandName: ADD_OBJECT_METADATA_OPEN_RECORD_IN_UPGRADE_COMMAND_NAME,
|
||||||
|
})
|
||||||
|
@Column({
|
||||||
|
type: 'enum',
|
||||||
|
enum: Object.values(ObjectOpenRecordIn),
|
||||||
|
default: ObjectOpenRecordIn.USER_CHOICE,
|
||||||
|
})
|
||||||
|
openRecordIn: ObjectOpenRecordIn;
|
||||||
|
|
||||||
@WasIntroducedInUpgrade({
|
@WasIntroducedInUpgrade({
|
||||||
upgradeCommandName: ADD_METADATA_OVERRIDES_COLUMN_UPGRADE_COMMAND_NAME,
|
upgradeCommandName: ADD_METADATA_OVERRIDES_COLUMN_UPGRADE_COMMAND_NAME,
|
||||||
})
|
})
|
||||||
|
|||||||
+1
@@ -23,6 +23,7 @@ export const fromObjectMetadataEntityToObjectMetadataDto = (
|
|||||||
isUICreatable: entity.isUICreatable,
|
isUICreatable: entity.isUICreatable,
|
||||||
isUIReadOnly: !entity.isUIEditable,
|
isUIReadOnly: !entity.isUIEditable,
|
||||||
isSearchable: entity.isSearchable,
|
isSearchable: entity.isSearchable,
|
||||||
|
openRecordIn: entity.openRecordIn,
|
||||||
isLabelSyncedWithName: entity.isLabelSyncedWithName,
|
isLabelSyncedWithName: entity.isLabelSyncedWithName,
|
||||||
workspaceId: entity.workspaceId,
|
workspaceId: entity.workspaceId,
|
||||||
labelIdentifierFieldMetadataId:
|
labelIdentifierFieldMetadataId:
|
||||||
|
|||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
export const VIEW_OPEN_RECORD_IN_DEPRECATION =
|
||||||
|
'Superseded by objectMetadata.openRecordIn and the workspace member preference; kept one release for API compatibility, no longer read by the frontend.';
|
||||||
+2
@@ -25,6 +25,7 @@ import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/
|
|||||||
import { IsValidMetadataName } from 'src/engine/decorators/metadata/is-valid-metadata-name.decorator';
|
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_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 { 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';
|
||||||
|
|
||||||
@InputType()
|
@InputType()
|
||||||
export class CreateViewInput {
|
export class CreateViewInput {
|
||||||
@@ -82,6 +83,7 @@ export class CreateViewInput {
|
|||||||
@IsEnum(ViewOpenRecordIn)
|
@IsEnum(ViewOpenRecordIn)
|
||||||
@Field(() => ViewOpenRecordIn, {
|
@Field(() => ViewOpenRecordIn, {
|
||||||
nullable: true,
|
nullable: true,
|
||||||
|
description: `Deprecated: ${VIEW_OPEN_RECORD_IN_DEPRECATION}`,
|
||||||
defaultValue: ViewOpenRecordIn.SIDE_PANEL,
|
defaultValue: ViewOpenRecordIn.SIDE_PANEL,
|
||||||
})
|
})
|
||||||
openRecordIn?: ViewOpenRecordIn;
|
openRecordIn?: ViewOpenRecordIn;
|
||||||
|
|||||||
+2
@@ -23,6 +23,7 @@ import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/
|
|||||||
import { IsValidMetadataName } from 'src/engine/decorators/metadata/is-valid-metadata-name.decorator';
|
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_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 { 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
|
// 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
|
// This is a temporary fix as we were extending the CreateViewInput class which was adding default values for the non filled fields
|
||||||
@@ -62,6 +63,7 @@ export class UpdateViewInput {
|
|||||||
@IsEnum(ViewOpenRecordIn)
|
@IsEnum(ViewOpenRecordIn)
|
||||||
@Field(() => ViewOpenRecordIn, {
|
@Field(() => ViewOpenRecordIn, {
|
||||||
nullable: true,
|
nullable: true,
|
||||||
|
description: `Deprecated: ${VIEW_OPEN_RECORD_IN_DEPRECATION}`,
|
||||||
})
|
})
|
||||||
openRecordIn?: ViewOpenRecordIn;
|
openRecordIn?: ViewOpenRecordIn;
|
||||||
|
|
||||||
|
|||||||
+5
-1
@@ -19,6 +19,7 @@ import {
|
|||||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||||
import { KANBAN_COLUMN_MAX_WIDTH } from 'src/engine/metadata-modules/view/constants/kanban-column-max-width.constant';
|
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 { 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';
|
||||||
|
|
||||||
@InputType()
|
@InputType()
|
||||||
export class UpsertViewWidgetViewSettingsInput {
|
export class UpsertViewWidgetViewSettingsInput {
|
||||||
@@ -43,7 +44,10 @@ export class UpsertViewWidgetViewSettingsInput {
|
|||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsEnum(ViewOpenRecordIn)
|
@IsEnum(ViewOpenRecordIn)
|
||||||
@Field(() => ViewOpenRecordIn, { nullable: true })
|
@Field(() => ViewOpenRecordIn, {
|
||||||
|
nullable: true,
|
||||||
|
description: `Deprecated: ${VIEW_OPEN_RECORD_IN_DEPRECATION}`,
|
||||||
|
})
|
||||||
openRecordIn?: ViewOpenRecordIn;
|
openRecordIn?: ViewOpenRecordIn;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { ViewFilterGroupDTO } from 'src/engine/metadata-modules/view-filter-grou
|
|||||||
import { ViewFilterDTO } from 'src/engine/metadata-modules/view-filter/dtos/view-filter.dto';
|
import { ViewFilterDTO } from 'src/engine/metadata-modules/view-filter/dtos/view-filter.dto';
|
||||||
import { ViewGroupDTO } from 'src/engine/metadata-modules/view-group/dtos/view-group.dto';
|
import { ViewGroupDTO } from 'src/engine/metadata-modules/view-group/dtos/view-group.dto';
|
||||||
import { ViewSortDTO } from 'src/engine/metadata-modules/view-sort/dtos/view-sort.dto';
|
import { ViewSortDTO } from 'src/engine/metadata-modules/view-sort/dtos/view-sort.dto';
|
||||||
|
import { VIEW_OPEN_RECORD_IN_DEPRECATION } from 'src/engine/metadata-modules/view/constants/view-open-record-in-deprecation.constant';
|
||||||
|
|
||||||
registerEnumType(ViewOpenRecordIn, { name: 'ViewOpenRecordIn' });
|
registerEnumType(ViewOpenRecordIn, { name: 'ViewOpenRecordIn' });
|
||||||
registerEnumType(ViewType, { name: 'ViewType' });
|
registerEnumType(ViewType, { name: 'ViewType' });
|
||||||
@@ -61,6 +62,7 @@ export class ViewDTO {
|
|||||||
@Field(() => ViewOpenRecordIn, {
|
@Field(() => ViewOpenRecordIn, {
|
||||||
nullable: false,
|
nullable: false,
|
||||||
defaultValue: ViewOpenRecordIn.SIDE_PANEL,
|
defaultValue: ViewOpenRecordIn.SIDE_PANEL,
|
||||||
|
deprecationReason: VIEW_OPEN_RECORD_IN_DEPRECATION,
|
||||||
})
|
})
|
||||||
openRecordIn: ViewOpenRecordIn;
|
openRecordIn: ViewOpenRecordIn;
|
||||||
|
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ export class ViewEntity
|
|||||||
@Column({ nullable: false, default: false, type: 'boolean' })
|
@Column({ nullable: false, default: false, type: 'boolean' })
|
||||||
isCustom: boolean;
|
isCustom: boolean;
|
||||||
|
|
||||||
|
// Deprecated: superseded by objectMetadata.openRecordIn and the member preference.
|
||||||
@Column({
|
@Column({
|
||||||
type: 'enum',
|
type: 'enum',
|
||||||
enum: Object.values(ViewOpenRecordIn),
|
enum: Object.values(ViewOpenRecordIn),
|
||||||
|
|||||||
+2
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
type FieldMetadataType,
|
type FieldMetadataType,
|
||||||
type ObjectsPermissions,
|
type ObjectsPermissions,
|
||||||
|
ObjectOpenRecordIn,
|
||||||
} from 'twenty-shared/types';
|
} from 'twenty-shared/types';
|
||||||
import { EntityManager } from 'typeorm';
|
import { EntityManager } from 'typeorm';
|
||||||
import { EntityPersistExecutor } from 'typeorm/persistence/EntityPersistExecutor';
|
import { EntityPersistExecutor } from 'typeorm/persistence/EntityPersistExecutor';
|
||||||
@@ -124,6 +125,7 @@ describe('WorkspaceEntityManager', () => {
|
|||||||
isLabelSyncedWithName: false,
|
isLabelSyncedWithName: false,
|
||||||
isUIEditable: true,
|
isUIEditable: true,
|
||||||
isUICreatable: true,
|
isUICreatable: true,
|
||||||
|
openRecordIn: ObjectOpenRecordIn.USER_CHOICE,
|
||||||
duplicateCriteria: null,
|
duplicateCriteria: null,
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
|
|||||||
+2
-1
@@ -1,4 +1,4 @@
|
|||||||
import { FieldMetadataType } from 'twenty-shared/types';
|
import { FieldMetadataType, ObjectOpenRecordIn } from 'twenty-shared/types';
|
||||||
|
|
||||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||||
@@ -39,6 +39,7 @@ describe('getColumnNameToFieldMetadataIdMap', () => {
|
|||||||
overrides: null,
|
overrides: null,
|
||||||
isUIEditable: true,
|
isUIEditable: true,
|
||||||
isUICreatable: true,
|
isUICreatable: true,
|
||||||
|
openRecordIn: ObjectOpenRecordIn.USER_CHOICE,
|
||||||
labelIdentifierFieldMetadataId: null,
|
labelIdentifierFieldMetadataId: null,
|
||||||
imageIdentifierFieldMetadataId: null,
|
imageIdentifierFieldMetadataId: null,
|
||||||
duplicateCriteria: null,
|
duplicateCriteria: null,
|
||||||
|
|||||||
+2
-1
@@ -1,4 +1,4 @@
|
|||||||
import { FieldMetadataType } from 'twenty-shared/types';
|
import { FieldMetadataType, ObjectOpenRecordIn } from 'twenty-shared/types';
|
||||||
|
|
||||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||||
@@ -39,6 +39,7 @@ describe('getFieldMetadataIdToColumnNamesMap', () => {
|
|||||||
overrides: null,
|
overrides: null,
|
||||||
isUIEditable: true,
|
isUIEditable: true,
|
||||||
isUICreatable: true,
|
isUICreatable: true,
|
||||||
|
openRecordIn: ObjectOpenRecordIn.USER_CHOICE,
|
||||||
labelIdentifierFieldMetadataId: null,
|
labelIdentifierFieldMetadataId: null,
|
||||||
imageIdentifierFieldMetadataId: null,
|
imageIdentifierFieldMetadataId: null,
|
||||||
duplicateCriteria: null,
|
duplicateCriteria: null,
|
||||||
|
|||||||
+6
-1
@@ -1,4 +1,8 @@
|
|||||||
import { FieldMetadataType, type ObjectRecord } from 'twenty-shared/types';
|
import {
|
||||||
|
FieldMetadataType,
|
||||||
|
ObjectOpenRecordIn,
|
||||||
|
type ObjectRecord,
|
||||||
|
} from 'twenty-shared/types';
|
||||||
|
|
||||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||||
@@ -39,6 +43,7 @@ describe('isRecordMatchingRLSRowLevelPermissionPredicate', () => {
|
|||||||
overrides: null,
|
overrides: null,
|
||||||
isUIEditable: true,
|
isUIEditable: true,
|
||||||
isUICreatable: true,
|
isUICreatable: true,
|
||||||
|
openRecordIn: ObjectOpenRecordIn.USER_CHOICE,
|
||||||
labelIdentifierFieldMetadataId: null,
|
labelIdentifierFieldMetadataId: null,
|
||||||
imageIdentifierFieldMetadataId: null,
|
imageIdentifierFieldMetadataId: null,
|
||||||
duplicateCriteria: null,
|
duplicateCriteria: null,
|
||||||
|
|||||||
+27
-24
@@ -2956,22 +2956,22 @@ exports[`getStandardObjectMetadataRelatedEntityIds should return standard object
|
|||||||
"workspaceMember": {
|
"workspaceMember": {
|
||||||
"fields": {
|
"fields": {
|
||||||
"accountOwnerForCompanies": {
|
"accountOwnerForCompanies": {
|
||||||
"id": "00000000-0000-0000-0000-000000000898",
|
"id": "00000000-0000-0000-0000-000000000899",
|
||||||
},
|
},
|
||||||
"assignedTasks": {
|
"assignedTasks": {
|
||||||
"id": "00000000-0000-0000-0000-000000000896",
|
"id": "00000000-0000-0000-0000-000000000897",
|
||||||
},
|
},
|
||||||
"avatarUrl": {
|
"avatarUrl": {
|
||||||
"id": "00000000-0000-0000-0000-000000000892",
|
"id": "00000000-0000-0000-0000-000000000893",
|
||||||
},
|
},
|
||||||
"blocklist": {
|
"blocklist": {
|
||||||
"id": "00000000-0000-0000-0000-000000000900",
|
|
||||||
},
|
|
||||||
"calendarEventParticipants": {
|
|
||||||
"id": "00000000-0000-0000-0000-000000000901",
|
"id": "00000000-0000-0000-0000-000000000901",
|
||||||
},
|
},
|
||||||
|
"calendarEventParticipants": {
|
||||||
|
"id": "00000000-0000-0000-0000-000000000902",
|
||||||
|
},
|
||||||
"calendarStartDay": {
|
"calendarStartDay": {
|
||||||
"id": "00000000-0000-0000-0000-000000000906",
|
"id": "00000000-0000-0000-0000-000000000907",
|
||||||
},
|
},
|
||||||
"colorScheme": {
|
"colorScheme": {
|
||||||
"id": "00000000-0000-0000-0000-000000000890",
|
"id": "00000000-0000-0000-0000-000000000890",
|
||||||
@@ -2983,7 +2983,7 @@ exports[`getStandardObjectMetadataRelatedEntityIds should return standard object
|
|||||||
"id": "00000000-0000-0000-0000-000000000885",
|
"id": "00000000-0000-0000-0000-000000000885",
|
||||||
},
|
},
|
||||||
"dateFormat": {
|
"dateFormat": {
|
||||||
"id": "00000000-0000-0000-0000-000000000904",
|
"id": "00000000-0000-0000-0000-000000000905",
|
||||||
},
|
},
|
||||||
"deletedAt": {
|
"deletedAt": {
|
||||||
"id": "00000000-0000-0000-0000-000000000884",
|
"id": "00000000-0000-0000-0000-000000000884",
|
||||||
@@ -2992,22 +2992,25 @@ exports[`getStandardObjectMetadataRelatedEntityIds should return standard object
|
|||||||
"id": "00000000-0000-0000-0000-000000000881",
|
"id": "00000000-0000-0000-0000-000000000881",
|
||||||
},
|
},
|
||||||
"jobTitle": {
|
"jobTitle": {
|
||||||
"id": "00000000-0000-0000-0000-000000000894",
|
"id": "00000000-0000-0000-0000-000000000895",
|
||||||
},
|
},
|
||||||
"locale": {
|
"locale": {
|
||||||
"id": "00000000-0000-0000-0000-000000000891",
|
"id": "00000000-0000-0000-0000-000000000892",
|
||||||
},
|
},
|
||||||
"messageParticipants": {
|
"messageParticipants": {
|
||||||
"id": "00000000-0000-0000-0000-000000000899",
|
"id": "00000000-0000-0000-0000-000000000900",
|
||||||
},
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"id": "00000000-0000-0000-0000-000000000889",
|
"id": "00000000-0000-0000-0000-000000000889",
|
||||||
},
|
},
|
||||||
"numberFormat": {
|
"numberFormat": {
|
||||||
"id": "00000000-0000-0000-0000-000000000907",
|
"id": "00000000-0000-0000-0000-000000000908",
|
||||||
|
},
|
||||||
|
"openRecordIn": {
|
||||||
|
"id": "00000000-0000-0000-0000-000000000891",
|
||||||
},
|
},
|
||||||
"ownedOpportunities": {
|
"ownedOpportunities": {
|
||||||
"id": "00000000-0000-0000-0000-000000000897",
|
"id": "00000000-0000-0000-0000-000000000898",
|
||||||
},
|
},
|
||||||
"position": {
|
"position": {
|
||||||
"id": "00000000-0000-0000-0000-000000000887",
|
"id": "00000000-0000-0000-0000-000000000887",
|
||||||
@@ -3016,13 +3019,13 @@ exports[`getStandardObjectMetadataRelatedEntityIds should return standard object
|
|||||||
"id": "00000000-0000-0000-0000-000000000888",
|
"id": "00000000-0000-0000-0000-000000000888",
|
||||||
},
|
},
|
||||||
"timeFormat": {
|
"timeFormat": {
|
||||||
"id": "00000000-0000-0000-0000-000000000905",
|
"id": "00000000-0000-0000-0000-000000000906",
|
||||||
},
|
},
|
||||||
"timeZone": {
|
"timeZone": {
|
||||||
"id": "00000000-0000-0000-0000-000000000903",
|
"id": "00000000-0000-0000-0000-000000000904",
|
||||||
},
|
},
|
||||||
"timelineActivities": {
|
"timelineActivities": {
|
||||||
"id": "00000000-0000-0000-0000-000000000902",
|
"id": "00000000-0000-0000-0000-000000000903",
|
||||||
},
|
},
|
||||||
"updatedAt": {
|
"updatedAt": {
|
||||||
"id": "00000000-0000-0000-0000-000000000883",
|
"id": "00000000-0000-0000-0000-000000000883",
|
||||||
@@ -3031,29 +3034,29 @@ exports[`getStandardObjectMetadataRelatedEntityIds should return standard object
|
|||||||
"id": "00000000-0000-0000-0000-000000000886",
|
"id": "00000000-0000-0000-0000-000000000886",
|
||||||
},
|
},
|
||||||
"userEmail": {
|
"userEmail": {
|
||||||
"id": "00000000-0000-0000-0000-000000000893",
|
"id": "00000000-0000-0000-0000-000000000894",
|
||||||
},
|
},
|
||||||
"userId": {
|
"userId": {
|
||||||
"id": "00000000-0000-0000-0000-000000000895",
|
"id": "00000000-0000-0000-0000-000000000896",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"id": "00000000-0000-0000-0000-000000000913",
|
"id": "00000000-0000-0000-0000-000000000914",
|
||||||
"views": {
|
"views": {
|
||||||
"allWorkspaceMembers": {
|
"allWorkspaceMembers": {
|
||||||
"id": "00000000-0000-0000-0000-000000000912",
|
"id": "00000000-0000-0000-0000-000000000913",
|
||||||
"viewFieldGroups": {},
|
"viewFieldGroups": {},
|
||||||
"viewFields": {
|
"viewFields": {
|
||||||
"assignedTasks": {
|
"assignedTasks": {
|
||||||
"id": "00000000-0000-0000-0000-000000000911",
|
"id": "00000000-0000-0000-0000-000000000912",
|
||||||
},
|
},
|
||||||
"createdAt": {
|
"createdAt": {
|
||||||
"id": "00000000-0000-0000-0000-000000000909",
|
"id": "00000000-0000-0000-0000-000000000910",
|
||||||
},
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"id": "00000000-0000-0000-0000-000000000908",
|
"id": "00000000-0000-0000-0000-000000000909",
|
||||||
},
|
},
|
||||||
"ownedOpportunities": {
|
"ownedOpportunities": {
|
||||||
"id": "00000000-0000-0000-0000-000000000910",
|
"id": "00000000-0000-0000-0000-000000000911",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"viewGroups": {},
|
"viewGroups": {},
|
||||||
|
|||||||
+22
@@ -5,6 +5,7 @@ import {
|
|||||||
FieldMetadataType,
|
FieldMetadataType,
|
||||||
NumberDataType,
|
NumberDataType,
|
||||||
RelationType,
|
RelationType,
|
||||||
|
OpenRecordIn,
|
||||||
} from 'twenty-shared/types';
|
} from 'twenty-shared/types';
|
||||||
|
|
||||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||||
@@ -163,6 +164,27 @@ export const buildWorkspaceMemberStandardFlatFieldMetadatas = ({
|
|||||||
twentyStandardApplicationId,
|
twentyStandardApplicationId,
|
||||||
now,
|
now,
|
||||||
}),
|
}),
|
||||||
|
openRecordIn: createStandardFieldFlatMetadata({
|
||||||
|
objectName,
|
||||||
|
workspaceId,
|
||||||
|
context: {
|
||||||
|
fieldName: 'openRecordIn',
|
||||||
|
type: FieldMetadataType.TEXT,
|
||||||
|
label: i18nLabel(msg`Open Records In`),
|
||||||
|
description: i18nLabel(
|
||||||
|
msg`Where records open for objects that follow the member's preference`,
|
||||||
|
),
|
||||||
|
icon: 'IconLayoutSidebarRight',
|
||||||
|
isSystem: true,
|
||||||
|
isNullable: false,
|
||||||
|
isUIEditable: false,
|
||||||
|
defaultValue: `'${OpenRecordIn.SIDE_PANEL}'`,
|
||||||
|
},
|
||||||
|
standardObjectMetadataRelatedEntityIds,
|
||||||
|
dependencyFlatEntityMaps,
|
||||||
|
twentyStandardApplicationId,
|
||||||
|
now,
|
||||||
|
}),
|
||||||
locale: createStandardFieldFlatMetadata({
|
locale: createStandardFieldFlatMetadata({
|
||||||
objectName,
|
objectName,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
|
|||||||
+6
@@ -1,5 +1,6 @@
|
|||||||
import { msg } from '@lingui/core/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
|
import { STANDARD_OBJECTS } from 'twenty-shared/metadata';
|
||||||
|
import { ObjectOpenRecordIn } from 'twenty-shared/types';
|
||||||
|
|
||||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||||
import { type AllStandardObjectName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-object-name.type';
|
import { type AllStandardObjectName } from 'src/engine/workspace-manager/twenty-standard-application/types/all-standard-object-name.type';
|
||||||
@@ -144,6 +145,7 @@ export const STANDARD_FLAT_OBJECT_METADATA_BUILDERS_BY_OBJECT_NAME = {
|
|||||||
context: {
|
context: {
|
||||||
universalIdentifier: STANDARD_OBJECTS.calendarEvent.universalIdentifier,
|
universalIdentifier: STANDARD_OBJECTS.calendarEvent.universalIdentifier,
|
||||||
nameSingular: 'calendarEvent',
|
nameSingular: 'calendarEvent',
|
||||||
|
openRecordIn: ObjectOpenRecordIn.SIDE_PANEL,
|
||||||
namePlural: 'calendarEvents',
|
namePlural: 'calendarEvents',
|
||||||
labelSingular: i18nLabel(msg`Calendar event`),
|
labelSingular: i18nLabel(msg`Calendar event`),
|
||||||
labelPlural: i18nLabel(msg`Calendar events`),
|
labelPlural: i18nLabel(msg`Calendar events`),
|
||||||
@@ -232,6 +234,7 @@ export const STANDARD_FLAT_OBJECT_METADATA_BUILDERS_BY_OBJECT_NAME = {
|
|||||||
context: {
|
context: {
|
||||||
universalIdentifier: STANDARD_OBJECTS.dashboard.universalIdentifier,
|
universalIdentifier: STANDARD_OBJECTS.dashboard.universalIdentifier,
|
||||||
nameSingular: 'dashboard',
|
nameSingular: 'dashboard',
|
||||||
|
openRecordIn: ObjectOpenRecordIn.RECORD_PAGE,
|
||||||
namePlural: 'dashboards',
|
namePlural: 'dashboards',
|
||||||
labelSingular: i18nLabel(msg`Dashboard`),
|
labelSingular: i18nLabel(msg`Dashboard`),
|
||||||
labelPlural: i18nLabel(msg`Dashboards`),
|
labelPlural: i18nLabel(msg`Dashboards`),
|
||||||
@@ -263,6 +266,7 @@ export const STANDARD_FLAT_OBJECT_METADATA_BUILDERS_BY_OBJECT_NAME = {
|
|||||||
universalIdentifier:
|
universalIdentifier:
|
||||||
STANDARD_OBJECTS.messageCampaign.universalIdentifier,
|
STANDARD_OBJECTS.messageCampaign.universalIdentifier,
|
||||||
nameSingular: 'messageCampaign',
|
nameSingular: 'messageCampaign',
|
||||||
|
openRecordIn: ObjectOpenRecordIn.RECORD_PAGE,
|
||||||
namePlural: 'messageCampaigns',
|
namePlural: 'messageCampaigns',
|
||||||
labelSingular: i18nLabel(msg`Campaign`),
|
labelSingular: i18nLabel(msg`Campaign`),
|
||||||
labelPlural: i18nLabel(msg`Campaigns`),
|
labelPlural: i18nLabel(msg`Campaigns`),
|
||||||
@@ -713,6 +717,7 @@ export const STANDARD_FLAT_OBJECT_METADATA_BUILDERS_BY_OBJECT_NAME = {
|
|||||||
context: {
|
context: {
|
||||||
universalIdentifier: STANDARD_OBJECTS.workflow.universalIdentifier,
|
universalIdentifier: STANDARD_OBJECTS.workflow.universalIdentifier,
|
||||||
nameSingular: 'workflow',
|
nameSingular: 'workflow',
|
||||||
|
openRecordIn: ObjectOpenRecordIn.RECORD_PAGE,
|
||||||
namePlural: 'workflows',
|
namePlural: 'workflows',
|
||||||
labelSingular: i18nLabel(msg`Workflow`),
|
labelSingular: i18nLabel(msg`Workflow`),
|
||||||
labelPlural: i18nLabel(msg`Workflows`),
|
labelPlural: i18nLabel(msg`Workflows`),
|
||||||
@@ -803,6 +808,7 @@ export const STANDARD_FLAT_OBJECT_METADATA_BUILDERS_BY_OBJECT_NAME = {
|
|||||||
universalIdentifier:
|
universalIdentifier:
|
||||||
STANDARD_OBJECTS.workflowVersion.universalIdentifier,
|
STANDARD_OBJECTS.workflowVersion.universalIdentifier,
|
||||||
nameSingular: 'workflowVersion',
|
nameSingular: 'workflowVersion',
|
||||||
|
openRecordIn: ObjectOpenRecordIn.RECORD_PAGE,
|
||||||
namePlural: 'workflowVersions',
|
namePlural: 'workflowVersions',
|
||||||
labelSingular: i18nLabel(msg`Workflow Version`),
|
labelSingular: i18nLabel(msg`Workflow Version`),
|
||||||
labelPlural: i18nLabel(msg`Workflow Versions`),
|
labelPlural: i18nLabel(msg`Workflow Versions`),
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user