491 save the page component instance id for side panel navigation (#10700)
Closes https://github.com/twentyhq/core-team-issues/issues/491 This PR: - Duplicates the right drawer pages for the command menu and replace all the states used in these pages by component states (The right drawer pages will be deleted when we deprecate the command menu v1) - Wraps those pages into a component instance provider - We store the component instance id upon navigation to restore the states when we navigate back to a page The only pages which are not updated for now are the pages related to the workflow objects, this will be done in another PR. In another PR, to improve the navigation experience I will replace the icons and titles of the chips by the label identifier and the avatar if the page is a record page. https://github.com/user-attachments/assets/a76d3345-01f3-4db9-8a55-331cca8b87e0 --------- Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
@ -1,424 +0,0 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
|
||||
import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector';
|
||||
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
|
||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
|
||||
import { IconDotsVertical, IconSearch, useIcons } from 'twenty-ui';
|
||||
|
||||
import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId';
|
||||
import { COMMAND_MENU_CONTEXT_CHIP_GROUPS_DROPDOWN_ID } from '@/command-menu/constants/CommandMenuContextChipGroupsDropdownId';
|
||||
import { COMMAND_MENU_PREVIOUS_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuPreviousComponentInstanceId';
|
||||
import { useCopyContextStoreStates } from '@/command-menu/hooks/useCopyContextStoreAndActionMenuStates';
|
||||
import { useResetContextStoreStates } from '@/command-menu/hooks/useResetContextStoreStates';
|
||||
import {
|
||||
CommandMenuNavigationStackItem,
|
||||
commandMenuNavigationStackState,
|
||||
} from '@/command-menu/states/commandMenuNavigationStackState';
|
||||
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
|
||||
import { commandMenuPageInfoState } from '@/command-menu/states/commandMenuPageTitle';
|
||||
import { hasUserSelectedCommandState } from '@/command-menu/states/hasUserSelectedCommandState';
|
||||
import { isCommandMenuClosingState } from '@/command-menu/states/isCommandMenuClosingState';
|
||||
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
|
||||
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
|
||||
import { contextStoreCurrentObjectMetadataItemComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemComponentState';
|
||||
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
|
||||
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
|
||||
import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState';
|
||||
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
|
||||
import { RIGHT_DRAWER_RECORD_INSTANCE_ID } from '@/object-record/record-right-drawer/constants/RightDrawerRecordInstanceId';
|
||||
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
|
||||
import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState';
|
||||
import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2';
|
||||
import { emitRightDrawerCloseEvent } from '@/ui/layout/right-drawer/utils/emitRightDrawerCloseEvent';
|
||||
import { isDragSelectionStartEnabledState } from '@/ui/utilities/drag-select/states/internal/isDragSelectionStartEnabledState';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useCallback } from 'react';
|
||||
import { capitalize, isDefined } from 'twenty-shared';
|
||||
import { isCommandMenuOpenedState } from '../states/isCommandMenuOpenedState';
|
||||
|
||||
export const useCommandMenu = () => {
|
||||
const { resetSelectedItem } = useSelectableList('command-menu-list');
|
||||
const {
|
||||
setHotkeyScopeAndMemorizePreviousScope,
|
||||
goBackToPreviousHotkeyScope,
|
||||
} = usePreviousHotkeyScope();
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
const { copyContextStoreStates } = useCopyContextStoreStates();
|
||||
const { resetContextStoreStates } = useResetContextStoreStates();
|
||||
|
||||
const { closeDropdown } = useDropdownV2();
|
||||
|
||||
const closeCommandMenu = useRecoilCallback(
|
||||
({ set }) =>
|
||||
() => {
|
||||
set(isCommandMenuOpenedState, false);
|
||||
set(isCommandMenuClosingState, true);
|
||||
set(isDragSelectionStartEnabledState, true);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const onCommandMenuCloseAnimationComplete = useRecoilCallback(
|
||||
({ set }) =>
|
||||
() => {
|
||||
closeDropdown(COMMAND_MENU_CONTEXT_CHIP_GROUPS_DROPDOWN_ID);
|
||||
|
||||
resetContextStoreStates(COMMAND_MENU_COMPONENT_INSTANCE_ID);
|
||||
resetContextStoreStates(COMMAND_MENU_PREVIOUS_COMPONENT_INSTANCE_ID);
|
||||
|
||||
set(viewableRecordIdState, null);
|
||||
set(commandMenuPageState, CommandMenuPages.Root);
|
||||
set(commandMenuPageInfoState, {
|
||||
title: undefined,
|
||||
Icon: undefined,
|
||||
});
|
||||
set(isCommandMenuOpenedState, false);
|
||||
set(commandMenuSearchState, '');
|
||||
set(commandMenuNavigationStackState, []);
|
||||
resetSelectedItem();
|
||||
set(hasUserSelectedCommandState, false);
|
||||
goBackToPreviousHotkeyScope();
|
||||
|
||||
emitRightDrawerCloseEvent();
|
||||
set(isCommandMenuClosingState, false);
|
||||
},
|
||||
[
|
||||
closeDropdown,
|
||||
goBackToPreviousHotkeyScope,
|
||||
resetContextStoreStates,
|
||||
resetSelectedItem,
|
||||
],
|
||||
);
|
||||
|
||||
const openCommandMenu = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
() => {
|
||||
const isCommandMenuOpened = snapshot
|
||||
.getLoadable(isCommandMenuOpenedState)
|
||||
.getValue();
|
||||
|
||||
const isCommandMenuClosing = snapshot
|
||||
.getLoadable(isCommandMenuClosingState)
|
||||
.getValue();
|
||||
|
||||
if (isCommandMenuClosing) {
|
||||
onCommandMenuCloseAnimationComplete();
|
||||
}
|
||||
|
||||
setHotkeyScopeAndMemorizePreviousScope(AppHotkeyScope.CommandMenuOpen);
|
||||
|
||||
if (isCommandMenuOpened) {
|
||||
return;
|
||||
}
|
||||
|
||||
copyContextStoreStates({
|
||||
instanceIdToCopyFrom: MAIN_CONTEXT_STORE_INSTANCE_ID,
|
||||
instanceIdToCopyTo: COMMAND_MENU_COMPONENT_INSTANCE_ID,
|
||||
});
|
||||
|
||||
set(isCommandMenuOpenedState, true);
|
||||
set(hasUserSelectedCommandState, false);
|
||||
set(isDragSelectionStartEnabledState, false);
|
||||
},
|
||||
[
|
||||
copyContextStoreStates,
|
||||
onCommandMenuCloseAnimationComplete,
|
||||
setHotkeyScopeAndMemorizePreviousScope,
|
||||
],
|
||||
);
|
||||
|
||||
const navigateCommandMenu = useRecoilCallback(
|
||||
({ snapshot, set }) => {
|
||||
return ({
|
||||
page,
|
||||
pageTitle,
|
||||
pageIcon,
|
||||
resetNavigationStack = false,
|
||||
}: CommandMenuNavigationStackItem & {
|
||||
resetNavigationStack?: boolean;
|
||||
}) => {
|
||||
openCommandMenu();
|
||||
set(commandMenuPageState, page);
|
||||
set(commandMenuPageInfoState, {
|
||||
title: pageTitle,
|
||||
Icon: pageIcon,
|
||||
});
|
||||
|
||||
const isCommandMenuClosing = snapshot
|
||||
.getLoadable(isCommandMenuClosingState)
|
||||
.getValue();
|
||||
|
||||
const currentNavigationStack = isCommandMenuClosing
|
||||
? []
|
||||
: snapshot.getLoadable(commandMenuNavigationStackState).getValue();
|
||||
|
||||
const itemIsAlreadyInStack = currentNavigationStack.some(
|
||||
(item) => item.page === page,
|
||||
);
|
||||
|
||||
if (resetNavigationStack || itemIsAlreadyInStack) {
|
||||
set(commandMenuNavigationStackState, [{ page, pageTitle, pageIcon }]);
|
||||
} else {
|
||||
set(commandMenuNavigationStackState, [
|
||||
...currentNavigationStack,
|
||||
{ page, pageTitle, pageIcon },
|
||||
]);
|
||||
}
|
||||
};
|
||||
},
|
||||
[openCommandMenu],
|
||||
);
|
||||
|
||||
const openRootCommandMenu = useCallback(() => {
|
||||
navigateCommandMenu({
|
||||
page: CommandMenuPages.Root,
|
||||
pageTitle: 'Command Menu',
|
||||
pageIcon: IconDotsVertical,
|
||||
resetNavigationStack: true,
|
||||
});
|
||||
}, [navigateCommandMenu]);
|
||||
|
||||
const toggleCommandMenu = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
async () => {
|
||||
const isCommandMenuOpened = snapshot
|
||||
.getLoadable(isCommandMenuOpenedState)
|
||||
.getValue();
|
||||
|
||||
set(commandMenuSearchState, '');
|
||||
|
||||
if (isCommandMenuOpened) {
|
||||
closeCommandMenu();
|
||||
} else {
|
||||
openRootCommandMenu();
|
||||
}
|
||||
},
|
||||
[closeCommandMenu, openRootCommandMenu],
|
||||
);
|
||||
|
||||
const goBackFromCommandMenu = useRecoilCallback(
|
||||
({ snapshot, set }) => {
|
||||
return () => {
|
||||
const currentNavigationStack = snapshot
|
||||
.getLoadable(commandMenuNavigationStackState)
|
||||
.getValue();
|
||||
|
||||
const newNavigationStack = currentNavigationStack.slice(0, -1);
|
||||
const lastNavigationStackItem = newNavigationStack.at(-1);
|
||||
|
||||
if (!isDefined(lastNavigationStackItem)) {
|
||||
closeCommandMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
set(commandMenuPageState, lastNavigationStackItem.page);
|
||||
|
||||
set(commandMenuPageInfoState, {
|
||||
title: lastNavigationStackItem.pageTitle,
|
||||
Icon: lastNavigationStackItem.pageIcon,
|
||||
});
|
||||
|
||||
set(commandMenuNavigationStackState, newNavigationStack);
|
||||
set(hasUserSelectedCommandState, false);
|
||||
};
|
||||
},
|
||||
[closeCommandMenu],
|
||||
);
|
||||
|
||||
const navigateCommandMenuHistory = useRecoilCallback(({ snapshot, set }) => {
|
||||
return (pageIndex: number) => {
|
||||
const currentNavigationStack = snapshot
|
||||
.getLoadable(commandMenuNavigationStackState)
|
||||
.getValue();
|
||||
|
||||
const newNavigationStack = currentNavigationStack.slice(0, pageIndex + 1);
|
||||
|
||||
set(commandMenuNavigationStackState, newNavigationStack);
|
||||
|
||||
const newNavigationStackItem = newNavigationStack.at(-1);
|
||||
|
||||
if (!isDefined(newNavigationStackItem)) {
|
||||
throw new Error(
|
||||
`No command menu navigation stack item found for index ${pageIndex}`,
|
||||
);
|
||||
}
|
||||
|
||||
set(commandMenuPageState, newNavigationStackItem?.page);
|
||||
set(commandMenuPageInfoState, {
|
||||
title: newNavigationStackItem?.pageTitle,
|
||||
Icon: newNavigationStackItem?.pageIcon,
|
||||
});
|
||||
|
||||
set(hasUserSelectedCommandState, false);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const openRecordInCommandMenu = useRecoilCallback(
|
||||
({ set, snapshot }) => {
|
||||
return ({
|
||||
recordId,
|
||||
objectNameSingular,
|
||||
isNewRecord = false,
|
||||
}: {
|
||||
recordId: string;
|
||||
objectNameSingular: string;
|
||||
isNewRecord?: boolean;
|
||||
}) => {
|
||||
set(viewableRecordNameSingularState, objectNameSingular);
|
||||
set(viewableRecordIdState, recordId);
|
||||
|
||||
const objectMetadataItem = snapshot
|
||||
.getLoadable(
|
||||
objectMetadataItemFamilySelector({
|
||||
objectName: objectNameSingular,
|
||||
objectNameType: 'singular',
|
||||
}),
|
||||
)
|
||||
.getValue();
|
||||
|
||||
if (!objectMetadataItem) {
|
||||
throw new Error(
|
||||
`No object metadata item found for object name ${objectNameSingular}`,
|
||||
);
|
||||
}
|
||||
|
||||
set(
|
||||
contextStoreCurrentObjectMetadataItemComponentState.atomFamily({
|
||||
instanceId: RIGHT_DRAWER_RECORD_INSTANCE_ID,
|
||||
}),
|
||||
objectMetadataItem,
|
||||
);
|
||||
|
||||
set(
|
||||
contextStoreTargetedRecordsRuleComponentState.atomFamily({
|
||||
instanceId: RIGHT_DRAWER_RECORD_INSTANCE_ID,
|
||||
}),
|
||||
{
|
||||
mode: 'selection',
|
||||
selectedRecordIds: [recordId],
|
||||
},
|
||||
);
|
||||
|
||||
set(
|
||||
contextStoreNumberOfSelectedRecordsComponentState.atomFamily({
|
||||
instanceId: RIGHT_DRAWER_RECORD_INSTANCE_ID,
|
||||
}),
|
||||
1,
|
||||
);
|
||||
|
||||
set(
|
||||
contextStoreCurrentViewTypeComponentState.atomFamily({
|
||||
instanceId: RIGHT_DRAWER_RECORD_INSTANCE_ID,
|
||||
}),
|
||||
ContextStoreViewType.ShowPage,
|
||||
);
|
||||
|
||||
set(
|
||||
contextStoreCurrentViewIdComponentState.atomFamily({
|
||||
instanceId: RIGHT_DRAWER_RECORD_INSTANCE_ID,
|
||||
}),
|
||||
snapshot
|
||||
.getLoadable(
|
||||
contextStoreCurrentViewIdComponentState.atomFamily({
|
||||
instanceId: MAIN_CONTEXT_STORE_INSTANCE_ID,
|
||||
}),
|
||||
)
|
||||
.getValue(),
|
||||
);
|
||||
|
||||
const Icon = objectMetadataItem?.icon
|
||||
? getIcon(objectMetadataItem.icon)
|
||||
: getIcon('IconList');
|
||||
|
||||
const capitalizedObjectNameSingular = capitalize(objectNameSingular);
|
||||
|
||||
navigateCommandMenu({
|
||||
page: CommandMenuPages.ViewRecord,
|
||||
pageTitle: isNewRecord
|
||||
? t`New ${capitalizedObjectNameSingular}`
|
||||
: capitalizedObjectNameSingular,
|
||||
pageIcon: Icon,
|
||||
// TODO: remove this once we can store the navigation stack page states
|
||||
resetNavigationStack: true,
|
||||
});
|
||||
};
|
||||
},
|
||||
[getIcon, navigateCommandMenu],
|
||||
);
|
||||
|
||||
const openRecordsSearchPage = () => {
|
||||
navigateCommandMenu({
|
||||
page: CommandMenuPages.SearchRecords,
|
||||
pageTitle: 'Search',
|
||||
pageIcon: IconSearch,
|
||||
});
|
||||
};
|
||||
|
||||
const setGlobalCommandMenuContext = useRecoilCallback(
|
||||
({ set }) => {
|
||||
return () => {
|
||||
copyContextStoreStates({
|
||||
instanceIdToCopyFrom: COMMAND_MENU_COMPONENT_INSTANCE_ID,
|
||||
instanceIdToCopyTo: COMMAND_MENU_PREVIOUS_COMPONENT_INSTANCE_ID,
|
||||
});
|
||||
|
||||
set(
|
||||
contextStoreTargetedRecordsRuleComponentState.atomFamily({
|
||||
instanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID,
|
||||
}),
|
||||
{
|
||||
mode: 'selection',
|
||||
selectedRecordIds: [],
|
||||
},
|
||||
);
|
||||
|
||||
set(
|
||||
contextStoreNumberOfSelectedRecordsComponentState.atomFamily({
|
||||
instanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID,
|
||||
}),
|
||||
0,
|
||||
);
|
||||
|
||||
set(
|
||||
contextStoreFiltersComponentState.atomFamily({
|
||||
instanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
set(
|
||||
contextStoreCurrentViewTypeComponentState.atomFamily({
|
||||
instanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID,
|
||||
}),
|
||||
ContextStoreViewType.Table,
|
||||
);
|
||||
|
||||
set(commandMenuPageInfoState, {
|
||||
title: undefined,
|
||||
Icon: undefined,
|
||||
});
|
||||
|
||||
set(hasUserSelectedCommandState, false);
|
||||
};
|
||||
},
|
||||
[copyContextStoreStates],
|
||||
);
|
||||
|
||||
return {
|
||||
openRootCommandMenu,
|
||||
closeCommandMenu,
|
||||
onCommandMenuCloseAnimationComplete,
|
||||
navigateCommandMenu,
|
||||
navigateCommandMenuHistory,
|
||||
goBackFromCommandMenu,
|
||||
openRecordsSearchPage,
|
||||
openRecordInCommandMenu,
|
||||
toggleCommandMenu,
|
||||
setGlobalCommandMenuContext,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user