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:
+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 { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
@@ -8,6 +10,8 @@ import {
|
||||
} from 'twenty-front-component-renderer';
|
||||
import {
|
||||
AppPath,
|
||||
ObjectOpenRecordIn,
|
||||
OpenRecordIn,
|
||||
SidePanelPages,
|
||||
type EnqueueSnackbarParams,
|
||||
} 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 { contextStoreRecordShowParentViewComponentState } from '@/context-store/states/contextStoreRecordShowParentViewComponentState';
|
||||
import { useRequestApplicationTokenRefresh } from '@/front-components/hooks/useRequestApplicationTokenRefresh';
|
||||
import { canOpenObjectInSidePanel } from '@/object-record/utils/canOpenObjectInSidePanel';
|
||||
import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel';
|
||||
import { useOpenComposeEmailInSidePanel } from '@/side-panel/hooks/useOpenComposeEmailInSidePanel';
|
||||
import { useOpenFrontComponentInSidePanel } from '@/side-panel/hooks/useOpenFrontComponentInSidePanel';
|
||||
@@ -73,6 +76,7 @@ export const useFrontComponentExecutionContext = ({
|
||||
const { openComposeEmailInSidePanel } = useOpenComposeEmailInSidePanel();
|
||||
const { openFrontComponentInSidePanel } = useOpenFrontComponentInSidePanel();
|
||||
const isMobile = useIsMobile();
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
const setSidePanelSearch = useSetAtomState(sidePanelSearchState);
|
||||
const { getIcon } = useIcons();
|
||||
const unmountEngineCommand = useUnmountCommand();
|
||||
@@ -133,7 +137,18 @@ export const useFrontComponentExecutionContext = ({
|
||||
const { recordId, objectNameSingular, tab, resetNavigationStack } =
|
||||
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)) {
|
||||
setRecordPageActiveTabId({
|
||||
recordId,
|
||||
|
||||
Reference in New Issue
Block a user