Fix record title cell reopening on every page refresh (#23551)
## What `location.state.isNewRecord` is set when navigating to a freshly created record so the title cell opens for naming. But router state lives in **browser history state, which survives page refreshes** — so every refresh of that record's page re-opens the title cell with an empty draft and a blinking cursor. Strip the flag after its one intended consumption in `PageChangeEffect` (react-router keeps user state under `history.state.usr`). ## Repro (on current main, any view set to open records in record page — or on mobile) 1. Create a record from a table; you land on its record page with the title focused (intended). 2. Name it, click away, then refresh the page. 3. The title cell re-opens, empty, focused — on every refresh, forever. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23551?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: bosiraphael <raphael.bosi@gmail.com>
This commit is contained in:
@@ -19,6 +19,7 @@ import { useResetTableRowSelection } from '@/object-record/record-table/hooks/in
|
||||
import { useActiveRecordTableRow } from '@/object-record/record-table/hooks/useActiveRecordTableRow';
|
||||
import { useFocusedRecordTableRow } from '@/object-record/record-table/hooks/useFocusedRecordTableRow';
|
||||
import { useOpenNewRecordTitleCell } from '@/object-record/record-title-cell/hooks/useOpenNewRecordTitleCell';
|
||||
import { newRecordTitleCellToOpenState } from '@/object-record/record-title-cell/states/newRecordTitleCellToOpenState';
|
||||
import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId';
|
||||
import { PageFocusId } from '@/types/PageFocusId';
|
||||
import { useResetFocusStackToFocusItem } from '@/ui/utilities/focus/hooks/useResetFocusStackToFocusItem';
|
||||
@@ -206,7 +207,6 @@ export const PageChangeEffect = () => {
|
||||
break;
|
||||
}
|
||||
case isMatchingLocation(location, AppPath.RecordShowPage): {
|
||||
const isNewRecord = location.state?.isNewRecord === true;
|
||||
const isSidePanelOpen = store.get(isSidePanelOpenedState.atom);
|
||||
|
||||
if (!isSidePanelOpen) {
|
||||
@@ -225,14 +225,21 @@ export const PageChangeEffect = () => {
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
isNewRecord &&
|
||||
isDefined(location.state?.labelIdentifierFieldName)
|
||||
) {
|
||||
openNewRecordTitleCell({
|
||||
recordId: location.state.objectRecordId,
|
||||
fieldName: location.state.labelIdentifierFieldName,
|
||||
});
|
||||
const newRecordTitleCellToOpen = store.get(
|
||||
newRecordTitleCellToOpenState.atom,
|
||||
);
|
||||
|
||||
if (isDefined(newRecordTitleCellToOpen)) {
|
||||
const objectRecordIdFromPath = matchPath(
|
||||
AppPath.RecordShowPage,
|
||||
location.pathname,
|
||||
)?.params.objectRecordId;
|
||||
|
||||
if (newRecordTitleCellToOpen.recordId === objectRecordIdFromPath) {
|
||||
openNewRecordTitleCell(newRecordTitleCellToOpen);
|
||||
}
|
||||
|
||||
store.set(newRecordTitleCellToOpenState.atom, null);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
+4
-9
@@ -17,15 +17,10 @@ export const ComposeCampaignCommand = () => {
|
||||
|
||||
await createMessageCampaign({ id: campaignId });
|
||||
|
||||
navigateApp(
|
||||
AppPath.RecordShowPage,
|
||||
{
|
||||
objectNameSingular: CoreObjectNameSingular.MessageCampaign,
|
||||
objectRecordId: campaignId,
|
||||
},
|
||||
undefined,
|
||||
{ state: { isNewRecord: true, objectRecordId: campaignId } },
|
||||
);
|
||||
navigateApp(AppPath.RecordShowPage, {
|
||||
objectNameSingular: CoreObjectNameSingular.MessageCampaign,
|
||||
objectRecordId: campaignId,
|
||||
});
|
||||
};
|
||||
|
||||
return <HeadlessEngineCommandWrapperEffect execute={handleExecute} ready />;
|
||||
|
||||
+12
-15
@@ -9,6 +9,7 @@ import { useResolveOpenRecordIn } from '@/object-record/record-index/hooks/useRe
|
||||
import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState';
|
||||
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
|
||||
import { useBuildRecordInputFromFilters } from '@/object-record/record-table/hooks/useBuildRecordInputFromFilters';
|
||||
import { newRecordTitleCellToOpenState } from '@/object-record/record-title-cell/states/newRecordTitleCellToOpenState';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { useOpenRecordInSidePanel } from '@/side-panel/hooks/useOpenRecordInSidePanel';
|
||||
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
|
||||
@@ -101,22 +102,18 @@ export const useCreateNewIndexRecord = ({
|
||||
const labelIdentifierFieldMetadataItem =
|
||||
getLabelIdentifierFieldMetadataItem(objectMetadataItem);
|
||||
|
||||
if (isDefined(labelIdentifierFieldMetadataItem)) {
|
||||
store.set(newRecordTitleCellToOpenState.atom, {
|
||||
recordId,
|
||||
fieldName: labelIdentifierFieldMetadataItem.name,
|
||||
});
|
||||
}
|
||||
|
||||
closeSidePanelMenu();
|
||||
navigate(
|
||||
AppPath.RecordShowPage,
|
||||
{
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
objectRecordId: recordId,
|
||||
},
|
||||
undefined,
|
||||
{
|
||||
state: {
|
||||
isNewRecord: true,
|
||||
objectRecordId: recordId,
|
||||
labelIdentifierFieldName: labelIdentifierFieldMetadataItem?.name,
|
||||
},
|
||||
},
|
||||
);
|
||||
navigate(AppPath.RecordShowPage, {
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
objectRecordId: recordId,
|
||||
});
|
||||
}
|
||||
|
||||
if (isDefined(recordIndexGroupFieldMetadataItem)) {
|
||||
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import { type NewRecordTitleCellToOpen } from '@/object-record/record-title-cell/types/NewRecordTitleCellToOpen';
|
||||
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
|
||||
|
||||
export const newRecordTitleCellToOpenState =
|
||||
createAtomState<NewRecordTitleCellToOpen | null>({
|
||||
key: 'record-title-cell/newRecordTitleCellToOpenState',
|
||||
defaultValue: null,
|
||||
});
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
export type NewRecordTitleCellToOpen = {
|
||||
recordId: string;
|
||||
fieldName: string;
|
||||
};
|
||||
+17
-20
@@ -7,6 +7,7 @@ import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-sto
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
|
||||
import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem';
|
||||
import { newRecordTitleCellToOpenState } from '@/object-record/record-title-cell/states/newRecordTitleCellToOpenState';
|
||||
import { getDefaultRecordPageLayoutId } from '@/page-layout/utils/getDefaultRecordPageLayoutId';
|
||||
import { getTabListInstanceIdFromPageLayoutAndRecord } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutAndRecord';
|
||||
import { SIDE_PANEL_COMPONENT_INSTANCE_ID } from '@/side-panel/constants/SidePanelComponentInstanceId';
|
||||
@@ -133,6 +134,7 @@ describe('useOpenRecordInSidePanel', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockIsMobile = false;
|
||||
jotaiStore.set(newRecordTitleCellToOpenState.atom, null);
|
||||
});
|
||||
|
||||
it('should set the correct states and navigate to the record page', () => {
|
||||
@@ -284,13 +286,12 @@ describe('useOpenRecordInSidePanel', () => {
|
||||
});
|
||||
});
|
||||
|
||||
expect(mockNavigateApp).toHaveBeenCalledWith(
|
||||
AppPath.RecordShowPage,
|
||||
{ objectNameSingular: 'person', objectRecordId: 'record-123' },
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
expect(mockNavigateApp).toHaveBeenCalledWith(AppPath.RecordShowPage, {
|
||||
objectNameSingular: 'person',
|
||||
objectRecordId: 'record-123',
|
||||
});
|
||||
expect(mockNavigateSidePanel).not.toHaveBeenCalled();
|
||||
expect(jotaiStore.get(newRecordTitleCellToOpenState.atom)).toBeNull();
|
||||
});
|
||||
|
||||
it('should forward new record state to the record page on mobile', () => {
|
||||
@@ -305,20 +306,16 @@ describe('useOpenRecordInSidePanel', () => {
|
||||
});
|
||||
});
|
||||
|
||||
expect(mockNavigateApp).toHaveBeenCalledWith(
|
||||
AppPath.RecordShowPage,
|
||||
{ objectNameSingular: 'person', objectRecordId: 'new-record-123' },
|
||||
undefined,
|
||||
{
|
||||
state: {
|
||||
isNewRecord: true,
|
||||
objectRecordId: 'new-record-123',
|
||||
labelIdentifierFieldName: getLabelIdentifierFieldMetadataItem(
|
||||
personMockObjectMetadataItem,
|
||||
)?.name,
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(mockNavigateApp).toHaveBeenCalledWith(AppPath.RecordShowPage, {
|
||||
objectNameSingular: 'person',
|
||||
objectRecordId: 'new-record-123',
|
||||
});
|
||||
expect(jotaiStore.get(newRecordTitleCellToOpenState.atom)).toEqual({
|
||||
recordId: 'new-record-123',
|
||||
fieldName: getLabelIdentifierFieldMetadataItem(
|
||||
personMockObjectMetadataItem,
|
||||
)?.name,
|
||||
});
|
||||
expect(mockOpenNewRecordTitleCell).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import { getIconColorForObjectType } from '@/object-metadata/utils/getIconColorF
|
||||
import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem';
|
||||
import { viewableRecordIdState } from '@/object-record/record-side-panel/states/viewableRecordIdState';
|
||||
import { useOpenNewRecordTitleCell } from '@/object-record/record-title-cell/hooks/useOpenNewRecordTitleCell';
|
||||
import { newRecordTitleCellToOpenState } from '@/object-record/record-title-cell/states/newRecordTitleCellToOpenState';
|
||||
import { setRecordPageActiveTabId } from '@/page-layout/utils/setRecordPageActiveTabId';
|
||||
import {
|
||||
AppPath,
|
||||
@@ -79,22 +80,19 @@ export const useOpenRecordInSidePanel = () => {
|
||||
? getLabelIdentifierFieldMetadataItem(objectMetadataItemForRecordPage)
|
||||
: undefined;
|
||||
|
||||
if (isNewRecord && isDefined(labelIdentifierField)) {
|
||||
store.set(newRecordTitleCellToOpenState.atom, {
|
||||
recordId,
|
||||
fieldName: labelIdentifierField.name,
|
||||
});
|
||||
}
|
||||
|
||||
closeSidePanelMenu();
|
||||
|
||||
navigate(
|
||||
AppPath.RecordShowPage,
|
||||
{ objectNameSingular, objectRecordId: recordId },
|
||||
undefined,
|
||||
isNewRecord
|
||||
? {
|
||||
state: {
|
||||
isNewRecord: true,
|
||||
objectRecordId: recordId,
|
||||
labelIdentifierFieldName: labelIdentifierField?.name,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
navigate(AppPath.RecordShowPage, {
|
||||
objectNameSingular,
|
||||
objectRecordId: recordId,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user