fix(command-menu-item): persist overrides after save and add reset-to-default (#21623)
## Context Command menu items are an overridable entity (like page-layout / FIELDS widgets), but the override flow in layout-customization mode was broken: - **Move / pin-unpin / hide-label didn't persist.** `useSaveCommandMenuItemsDraft` fired the `updateCommandMenuItem` mutations (backend persisted correctly) but never wrote the result back into `metadataStoreState`, the source the live menu and edit panel read from. So the UI reverted on exit and changes only showed after a hard reload. - **No true "reset to default".** Existing reset controls only reverted the draft to the last-saved values (which still contained overrides). there was no way to clear overrides back to the original values after a save. Notes - removed the footer "Reset to default" button. This is not clear to me how we want to build, let's re-implement better in the next version - reset are done on click and not delayed on the save. This is similar to other reset to default on page layouts where we actually usually reload the component and this is because the FE has no idea what's original VS override from the response itself <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21623?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. -->
This commit is contained in:
+6
-9
@@ -1,3 +1,4 @@
|
||||
import { useResetCommandMenuItemToDefault } from '@/command-menu-item/edit/hooks/useResetCommandMenuItemToDefault';
|
||||
import { useUpdateCommandMenuItemInDraft } from '@/command-menu-item/edit/hooks/useUpdateCommandMenuItemInDraft';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
@@ -34,14 +35,13 @@ export const CommandMenuItemOptionsDropdown = ({
|
||||
const dropdownId = getCommandMenuItemOptionsDropdownId(itemId);
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
const { updateCommandMenuItemInDraft } = useUpdateCommandMenuItemInDraft();
|
||||
const { resetCommandMenuItemToDefault } = useResetCommandMenuItemToDefault();
|
||||
|
||||
const normalizedServerShortLabel = serverShortLabel ?? null;
|
||||
const normalizedShortLabel = shortLabel ?? null;
|
||||
const hasNoShortLabel = normalizedServerShortLabel === null;
|
||||
const isLabelHidden =
|
||||
normalizedShortLabel === null && isDefined(normalizedServerShortLabel);
|
||||
const hasShortLabelOverride =
|
||||
normalizedShortLabel !== normalizedServerShortLabel;
|
||||
|
||||
const handleToggleHideLabel = (toggled: boolean) => {
|
||||
updateCommandMenuItemInDraft(itemId, {
|
||||
@@ -49,11 +49,9 @@ export const CommandMenuItemOptionsDropdown = ({
|
||||
});
|
||||
};
|
||||
|
||||
const handleResetLabelToDefault = () => {
|
||||
updateCommandMenuItemInDraft(itemId, {
|
||||
shortLabel: normalizedServerShortLabel,
|
||||
});
|
||||
const handleResetToDefault = async () => {
|
||||
closeDropdown(dropdownId);
|
||||
await resetCommandMenuItemToDefault(itemId);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -74,10 +72,9 @@ export const CommandMenuItemOptionsDropdown = ({
|
||||
/>
|
||||
<MenuItem
|
||||
LeftIcon={IconRefresh}
|
||||
onClick={handleResetLabelToDefault}
|
||||
onClick={handleResetToDefault}
|
||||
accent="default"
|
||||
text={t`Reset label to default`}
|
||||
disabled={!hasShortLabelOverride}
|
||||
text={t`Reset to default`}
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownContent>
|
||||
|
||||
-18
@@ -2,7 +2,6 @@ import { CommandMenuItemEditRecordSelectionDropdown } from '@/command-menu-item/
|
||||
import { CommandMenuItemOptionsDropdown } from '@/command-menu-item/edit/components/CommandMenuItemOptionsDropdown';
|
||||
import { useEditableCommandMenuItems } from '@/command-menu-item/edit/hooks/useEditableCommandMenuItems';
|
||||
import { useReorderCommandMenuItemsInDraft } from '@/command-menu-item/edit/hooks/useReorderCommandMenuItemsInDraft';
|
||||
import { useResetCommandMenuItemsDraft } from '@/command-menu-item/edit/hooks/useResetCommandMenuItemsDraft';
|
||||
import { useUpdateCommandMenuItemInDraft } from '@/command-menu-item/edit/hooks/useUpdateCommandMenuItemInDraft';
|
||||
import { useCurrentCommandMenuContextApi } from '@/command-menu-item/hooks/useCurrentCommandMenuContextApi';
|
||||
import { commandMenuItemsSelector } from '@/command-menu-item/states/commandMenuItemsSelector';
|
||||
@@ -14,7 +13,6 @@ import { sidePanelSearchState } from '@/side-panel/states/sidePanelSearchState';
|
||||
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
|
||||
import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableList';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { SidePanelFooter } from '@/ui/layout/side-panel/components/SidePanelFooter';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { type DropResult } from '@hello-pangea/dnd';
|
||||
import { styled } from '@linaria/react';
|
||||
@@ -28,10 +26,8 @@ import {
|
||||
IconDotsVertical,
|
||||
IconPin,
|
||||
IconPinnedOff,
|
||||
IconRefresh,
|
||||
useIcons,
|
||||
} from 'twenty-ui-deprecated/display';
|
||||
import { Button } from 'twenty-ui-deprecated/input';
|
||||
import { MenuItem, MenuItemDraggable } from 'twenty-ui-deprecated/navigation';
|
||||
import { themeCssVariables } from 'twenty-ui-deprecated/theme-constants';
|
||||
import { type CommandMenuItemFieldsFragment } from '~/generated-metadata/graphql';
|
||||
@@ -79,7 +75,6 @@ export const SidePanelCommandMenuItemEditPage = () => {
|
||||
);
|
||||
const { updateCommandMenuItemInDraft } = useUpdateCommandMenuItemInDraft();
|
||||
const { reorderCommandMenuItemInDraft } = useReorderCommandMenuItemsInDraft();
|
||||
const { resetCommandMenuItemsDraft } = useResetCommandMenuItemsDraft();
|
||||
|
||||
const editableCommandMenuItems = useEditableCommandMenuItems();
|
||||
|
||||
@@ -298,19 +293,6 @@ export const SidePanelCommandMenuItemEditPage = () => {
|
||||
</SidePanelGroup>
|
||||
</SidePanelList>
|
||||
</StyledContent>
|
||||
<SidePanelFooter
|
||||
actions={[
|
||||
<Button
|
||||
key="reset"
|
||||
Icon={IconRefresh}
|
||||
title={t`Reset to default`}
|
||||
variant="secondary"
|
||||
accent="default"
|
||||
size="small"
|
||||
onClick={resetCommandMenuItemsDraft}
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
import { CombinedGraphQLErrors } from '@apollo/client/errors';
|
||||
import { useMutation } from '@apollo/client/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { CrudOperationType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { commandMenuItemsDraftState } from '@/command-menu-item/edit/states/commandMenuItemsDraftState';
|
||||
import { RESET_COMMAND_MENU_ITEM } from '@/command-menu-item/graphql/mutations/resetCommandMenuItem';
|
||||
import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler';
|
||||
import { useUpdateMetadataStoreDraft } from '@/metadata-store/hooks/useUpdateMetadataStoreDraft';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import {
|
||||
type ResetCommandMenuItemMutation,
|
||||
type ResetCommandMenuItemMutationVariables,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
export const useResetCommandMenuItemToDefault = () => {
|
||||
const store = useStore();
|
||||
const [resetCommandMenuItem] = useMutation<
|
||||
ResetCommandMenuItemMutation,
|
||||
ResetCommandMenuItemMutationVariables
|
||||
>(RESET_COMMAND_MENU_ITEM);
|
||||
const { updateInDraft, applyChanges } = useUpdateMetadataStoreDraft();
|
||||
const { handleMetadataError } = useMetadataErrorHandler();
|
||||
const { enqueueErrorSnackBar } = useSnackBar();
|
||||
|
||||
const resetCommandMenuItemToDefault = useCallback(
|
||||
async (id: string) => {
|
||||
try {
|
||||
const result = await resetCommandMenuItem({ variables: { id } });
|
||||
|
||||
const resetItem = result.data?.resetCommandMenuItem;
|
||||
|
||||
if (!isDefined(resetItem)) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateInDraft('commandMenuItems', [resetItem]);
|
||||
applyChanges();
|
||||
|
||||
const draft = store.get(commandMenuItemsDraftState.atom);
|
||||
|
||||
if (isDefined(draft)) {
|
||||
store.set(
|
||||
commandMenuItemsDraftState.atom,
|
||||
draft.map((item) =>
|
||||
item.id === id ? { ...item, ...resetItem } : item,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (CombinedGraphQLErrors.is(error)) {
|
||||
handleMetadataError(error, {
|
||||
primaryMetadataName: 'commandMenuItem',
|
||||
operationType: CrudOperationType.UPDATE,
|
||||
});
|
||||
} else {
|
||||
enqueueErrorSnackBar({ message: t`An error occurred.` });
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
resetCommandMenuItem,
|
||||
updateInDraft,
|
||||
applyChanges,
|
||||
store,
|
||||
handleMetadataError,
|
||||
enqueueErrorSnackBar,
|
||||
],
|
||||
);
|
||||
|
||||
return { resetCommandMenuItemToDefault };
|
||||
};
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useStore } from 'jotai';
|
||||
|
||||
import { commandMenuItemsDraftState } from '@/command-menu-item/edit/states/commandMenuItemsDraftState';
|
||||
import { commandMenuItemsSelector } from '@/command-menu-item/states/commandMenuItemsSelector';
|
||||
|
||||
// Resets the draft to the current server state, discarding all user edits.
|
||||
export const useResetCommandMenuItemsDraft = () => {
|
||||
const store = useStore();
|
||||
|
||||
const resetCommandMenuItemsDraft = useCallback(() => {
|
||||
const serverItems = store.get(commandMenuItemsSelector.atom);
|
||||
|
||||
store.set(
|
||||
commandMenuItemsDraftState.atom,
|
||||
serverItems.map((item) => ({ ...item })),
|
||||
);
|
||||
}, [store]);
|
||||
|
||||
return { resetCommandMenuItemsDraft };
|
||||
};
|
||||
+25
-3
@@ -6,13 +6,22 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import { commandMenuItemsDraftState } from '@/command-menu-item/edit/states/commandMenuItemsDraftState';
|
||||
import { UPDATE_COMMAND_MENU_ITEM } from '@/command-menu-item/graphql/mutations/updateCommandMenuItem';
|
||||
import { commandMenuItemsSelector } from '@/command-menu-item/states/commandMenuItemsSelector';
|
||||
import { useUpdateMetadataStoreDraft } from '@/metadata-store/hooks/useUpdateMetadataStoreDraft';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { type UpdateCommandMenuItemInput } from '~/generated-metadata/graphql';
|
||||
import {
|
||||
type UpdateCommandMenuItemInput,
|
||||
type UpdateCommandMenuItemMutation,
|
||||
type UpdateCommandMenuItemMutationVariables,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
export const useSaveCommandMenuItemsDraft = () => {
|
||||
const store = useStore();
|
||||
const [updateCommandMenuItem] = useMutation(UPDATE_COMMAND_MENU_ITEM);
|
||||
const [updateCommandMenuItem] = useMutation<
|
||||
UpdateCommandMenuItemMutation,
|
||||
UpdateCommandMenuItemMutationVariables
|
||||
>(UPDATE_COMMAND_MENU_ITEM);
|
||||
const commandMenuItems = useAtomStateValue(commandMenuItemsSelector);
|
||||
const { updateInDraft, applyChanges } = useUpdateMetadataStoreDraft();
|
||||
|
||||
const saveCommandMenuItemsDraft = useCallback(async () => {
|
||||
const draft = store.get(commandMenuItemsDraftState.atom);
|
||||
@@ -39,6 +48,10 @@ export const useSaveCommandMenuItemsDraft = () => {
|
||||
);
|
||||
});
|
||||
|
||||
if (changedItems.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
changedItems.map((item) => {
|
||||
const input: UpdateCommandMenuItemInput = {
|
||||
@@ -51,7 +64,16 @@ export const useSaveCommandMenuItemsDraft = () => {
|
||||
return updateCommandMenuItem({ variables: { input } });
|
||||
}),
|
||||
);
|
||||
}, [store, commandMenuItems, updateCommandMenuItem]);
|
||||
|
||||
updateInDraft('commandMenuItems', changedItems);
|
||||
applyChanges();
|
||||
}, [
|
||||
store,
|
||||
commandMenuItems,
|
||||
updateCommandMenuItem,
|
||||
updateInDraft,
|
||||
applyChanges,
|
||||
]);
|
||||
|
||||
return { saveCommandMenuItemsDraft };
|
||||
};
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import { COMMAND_MENU_ITEM_FRAGMENT } from '@/command-menu-item/graphql/fragments/commandMenuItemFragment';
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const RESET_COMMAND_MENU_ITEM = gql`
|
||||
${COMMAND_MENU_ITEM_FRAGMENT}
|
||||
mutation ResetCommandMenuItem($id: UUID!) {
|
||||
resetCommandMenuItem(id: $id) {
|
||||
...CommandMenuItemFields
|
||||
}
|
||||
}
|
||||
`;
|
||||
Reference in New Issue
Block a user