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:
-3
@@ -7,7 +7,6 @@ import { ObjectOptionsDropdownFieldsContent } from '@/object-record/object-optio
|
||||
import { ObjectOptionsDropdownHiddenFieldsContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownHiddenFieldsContent';
|
||||
import { ObjectOptionsDropdownHiddenRecordGroupsContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownHiddenRecordGroupsContent';
|
||||
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 { ObjectOptionsDropdownRecordGroupFieldsContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupFieldsContent';
|
||||
import { ObjectOptionsDropdownRecordGroupsContent } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdownRecordGroupsContent';
|
||||
@@ -26,8 +25,6 @@ export const ObjectOptionsDropdownContent = () => {
|
||||
switch (currentContentId) {
|
||||
case 'layout':
|
||||
return <ObjectOptionsDropdownLayoutContent />;
|
||||
case 'layoutOpenIn':
|
||||
return <ObjectOptionsDropdownLayoutOpenInContent />;
|
||||
case 'fields':
|
||||
return <ObjectOptionsDropdownFieldsContent />;
|
||||
case 'hiddenFields':
|
||||
|
||||
-30
@@ -34,8 +34,6 @@ import {
|
||||
IconCalendarWeek,
|
||||
IconChevronLeft,
|
||||
IconLayoutList,
|
||||
IconLayoutNavbar,
|
||||
IconLayoutSidebarRight,
|
||||
IconTable,
|
||||
} from 'twenty-ui/icon';
|
||||
import { OverflowingTextWithTooltip } from 'twenty-ui/surfaces';
|
||||
@@ -43,7 +41,6 @@ import { MenuItem, MenuItemSelect, MenuItemToggle } from 'twenty-ui/navigation';
|
||||
import {
|
||||
FeatureFlagKey,
|
||||
ViewCalendarLayout,
|
||||
ViewOpenRecordIn,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
export const ObjectOptionsDropdownLayoutContent = () => {
|
||||
@@ -129,7 +126,6 @@ export const ObjectOptionsDropdownLayoutContent = () => {
|
||||
ViewType.TABLE,
|
||||
...(isDefaultView ? [] : [ViewType.KANBAN]),
|
||||
...(!isDefaultView ? [ViewType.CALENDAR] : []),
|
||||
ViewOpenRecordIn.SIDE_PANEL,
|
||||
...(currentView?.type === ViewType.KANBAN ? ['Group'] : []),
|
||||
...(currentView?.type === ViewType.CALENDAR
|
||||
? [
|
||||
@@ -285,32 +281,6 @@ export const ObjectOptionsDropdownLayoutContent = () => {
|
||||
</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 && (
|
||||
<SelectableListItem
|
||||
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 { useUpdateCurrentView } from '@/views/hooks/useUpdateCurrentView';
|
||||
import { type GraphQLView } from '@/views/types/GraphQLView';
|
||||
import { type ViewOpenRecordIn } from '~/generated-metadata/graphql';
|
||||
import { viewPickerInputNameComponentState } from '@/views/view-picker/states/viewPickerInputNameComponentState';
|
||||
import { viewPickerSelectedIconComponentState } from '@/views/view-picker/states/viewPickerSelectedIconComponentState';
|
||||
import { useCallback } from 'react';
|
||||
@@ -17,16 +16,6 @@ export const useUpdateObjectViewOptions = () => {
|
||||
|
||||
const { updateCurrentView } = useUpdateCurrentView();
|
||||
|
||||
const setAndPersistOpenRecordIn = useCallback(
|
||||
(openRecordIn: ViewOpenRecordIn, view: GraphQLView | undefined) => {
|
||||
if (!view) return;
|
||||
updateCurrentView({
|
||||
openRecordIn,
|
||||
});
|
||||
},
|
||||
[updateCurrentView],
|
||||
);
|
||||
|
||||
const setAndPersistViewName = useCallback(
|
||||
(viewName: string, view: GraphQLView | undefined) => {
|
||||
if (!view) return;
|
||||
@@ -50,7 +39,6 @@ export const useUpdateObjectViewOptions = () => {
|
||||
);
|
||||
|
||||
return {
|
||||
setAndPersistOpenRecordIn,
|
||||
setAndPersistViewName,
|
||||
setAndPersistViewIcon,
|
||||
};
|
||||
|
||||
-1
@@ -1,6 +1,5 @@
|
||||
export type ObjectOptionsContentId =
|
||||
| 'layout'
|
||||
| 'layoutOpenIn'
|
||||
| 'fields'
|
||||
| 'hiddenFields'
|
||||
| 'recordGroups'
|
||||
|
||||
Reference in New Issue
Block a user