feat: deprecate WorkspaceFavorite in favor of NavigationMenuItem (#18624)
## Summary - **Removes the entire `modules/favorites/` directory** (~66 files, ~5000 lines deleted) — components, hooks, states, types, utils, tests, and the favorite-folder-picker sub-module - **Eliminates the dual-write pattern** where creating a favorite also created a NavigationMenuItem — all consumers now use `useCreateNavigationMenuItem` directly - **Removes `IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED` feature flag checks** from ~12 files, always taking the NavigationMenuItem code path - **Cleans up backend dual-writes** in `object-metadata.service.ts` and `twenty-standard-application.service.ts` that were creating Favorite records alongside NavigationMenuItems - **Updates prefetch system** to only load NavigationMenuItems (removes favorites prefetch effects and states) - **Cleans up test infrastructure** — updates Storybook decorators, mock data, and graphql mocks to remove favorites references ### What was intentionally kept - **Backend entity definitions** (`FavoriteWorkspaceEntity`, `FavoriteFolderWorkspaceEntity`) — these define the database schema and need a proper database migration to remove - **Cascade deletion listeners** — still needed to clean up existing Favorite data in workspaces that haven't been fully migrated - **v1.18 migration commands** — needed for workspaces upgrading from older versions --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -61,8 +61,8 @@ const jestConfig = {
|
||||
extensionsToTreatAsEsm: ['.ts', '.tsx'],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
statements: 49,
|
||||
lines: 47.6,
|
||||
statements: 48.8,
|
||||
lines: 47.5,
|
||||
functions: 39.5,
|
||||
},
|
||||
},
|
||||
|
||||
+4
-4
@@ -6,11 +6,11 @@ import {
|
||||
PageDecorator,
|
||||
type PageDecoratorArgs,
|
||||
} from '~/testing/decorators/PageDecorator';
|
||||
import { PrefetchLoadingDecorator } from '~/testing/decorators/PrefetchLoadingDecorator';
|
||||
import { LoadingDecorator } from '~/testing/decorators/LoadingDecorator';
|
||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
|
||||
const meta: Meta<PageDecoratorArgs> = {
|
||||
title: 'App/Loading/PrefetchLoading',
|
||||
title: 'App/Loading',
|
||||
component: RecordIndexPage,
|
||||
args: {
|
||||
routePath: '/objects/:objectNamePlural',
|
||||
@@ -20,7 +20,7 @@ const meta: Meta<PageDecoratorArgs> = {
|
||||
},
|
||||
parameters: {
|
||||
msw: graphqlMocks,
|
||||
prefetchLoadingSetDelay: 1000,
|
||||
loadingSetDelay: 1000,
|
||||
},
|
||||
tags: ['no-tests'],
|
||||
};
|
||||
@@ -32,7 +32,7 @@ export type Story = StoryObj<typeof RecordIndexPage>;
|
||||
export const Default: Story = {
|
||||
// oxlint-disable-next-line @typescripttypescript/ban-ts-comment
|
||||
// @ts-ignore
|
||||
decorators: [PrefetchLoadingDecorator, PageDecorator],
|
||||
decorators: [LoadingDecorator, PageDecorator],
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
@@ -18,7 +18,7 @@ import { PromiseRejectionEffect } from '@/error-handler/components/PromiseReject
|
||||
import { HeadlessFrontComponentMountRoot } from '@/front-components/components/HeadlessFrontComponentMountRoot';
|
||||
import { ApolloCoreProvider } from '@/object-metadata/components/ApolloCoreProvider';
|
||||
import { PreComputedChipGeneratorsProvider } from '@/object-metadata/components/PreComputedChipGeneratorsProvider';
|
||||
import { PrefetchDataProvider } from '@/prefetch/components/PrefetchDataProvider';
|
||||
|
||||
import { SSEProvider } from '@/sse-db-event/components/SSEProvider';
|
||||
import { SupportChatEffect } from '@/support/components/SupportChatEffect';
|
||||
import { DialogManager } from '@/ui/feedback/dialog-manager/components/DialogManager';
|
||||
@@ -56,32 +56,30 @@ export const AppRouterProviders = () => {
|
||||
<ApolloCoreProvider>
|
||||
<SSEProvider>
|
||||
<PreComputedChipGeneratorsProvider>
|
||||
<PrefetchDataProvider>
|
||||
<UserThemeProviderEffect />
|
||||
<SnackBarProvider>
|
||||
<ErrorMessageEffect />
|
||||
<AgentChatProvider>
|
||||
<DialogComponentInstanceContext.Provider
|
||||
value={{ instanceId: 'dialog-manager' }}
|
||||
>
|
||||
<DialogManager>
|
||||
<StrictMode>
|
||||
<PromiseRejectionEffect />
|
||||
<GotoHotkeysEffectsProvider />
|
||||
<PageTitle title={pageTitle} />
|
||||
<PageFavicon />
|
||||
<Outlet />
|
||||
<GlobalFilePreviewModal />
|
||||
<CommandMenuConfirmationModalManager />
|
||||
<HeadlessFrontComponentMountRoot />
|
||||
</StrictMode>
|
||||
</DialogManager>
|
||||
</DialogComponentInstanceContext.Provider>
|
||||
</AgentChatProvider>
|
||||
</SnackBarProvider>
|
||||
<MainContextStoreProvider />
|
||||
<SupportChatEffect />
|
||||
</PrefetchDataProvider>
|
||||
<UserThemeProviderEffect />
|
||||
<SnackBarProvider>
|
||||
<ErrorMessageEffect />
|
||||
<AgentChatProvider>
|
||||
<DialogComponentInstanceContext.Provider
|
||||
value={{ instanceId: 'dialog-manager' }}
|
||||
>
|
||||
<DialogManager>
|
||||
<StrictMode>
|
||||
<PromiseRejectionEffect />
|
||||
<GotoHotkeysEffectsProvider />
|
||||
<PageTitle title={pageTitle} />
|
||||
<PageFavicon />
|
||||
<Outlet />
|
||||
<GlobalFilePreviewModal />
|
||||
<CommandMenuConfirmationModalManager />
|
||||
<HeadlessFrontComponentMountRoot />
|
||||
</StrictMode>
|
||||
</DialogManager>
|
||||
</DialogComponentInstanceContext.Provider>
|
||||
</AgentChatProvider>
|
||||
</SnackBarProvider>
|
||||
<MainContextStoreProvider />
|
||||
<SupportChatEffect />
|
||||
<PageChangeEffect />
|
||||
</PreComputedChipGeneratorsProvider>
|
||||
</SSEProvider>
|
||||
|
||||
+10
-29
@@ -7,8 +7,7 @@ import { contextStoreCurrentViewTypeComponentState } from '@/context-store/state
|
||||
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
|
||||
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
||||
import { usePrefetchedNavigationMenuItemsData } from '@/navigation-menu-item/hooks/usePrefetchedNavigationMenuItemsData';
|
||||
import { useNavigationMenuItemsData } from '@/navigation-menu-item/hooks/useNavigationMenuItemsData';
|
||||
import { CoreObjectNameSingular } from 'twenty-shared/types';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
|
||||
@@ -19,11 +18,10 @@ import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback, useContext, useMemo } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||
import type { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||
|
||||
export const useShouldCommandMenuItemBeRegisteredParams = ({
|
||||
objectMetadataItem,
|
||||
@@ -31,11 +29,7 @@ export const useShouldCommandMenuItemBeRegisteredParams = ({
|
||||
objectMetadataItem?: ObjectMetadataItem;
|
||||
}): ShouldBeRegisteredFunctionParams => {
|
||||
const store = useStore();
|
||||
const { sortedFavorites: favorites } = useFavorites();
|
||||
const { navigationMenuItems } = usePrefetchedNavigationMenuItemsData();
|
||||
const isNavigationMenuItemEditingEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED,
|
||||
);
|
||||
const { navigationMenuItems } = useNavigationMenuItemsData();
|
||||
|
||||
const contextStoreTargetedRecordsRule = useAtomComponentStateValue(
|
||||
contextStoreTargetedRecordsRuleComponentState,
|
||||
@@ -47,30 +41,17 @@ export const useShouldCommandMenuItemBeRegisteredParams = ({
|
||||
: undefined;
|
||||
|
||||
const isFavorite = useMemo(() => {
|
||||
if (!isDefined(recordId)) {
|
||||
if (!isDefined(recordId) || !isDefined(objectMetadataItem)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isNavigationMenuItemEditingEnabled && isDefined(objectMetadataItem)) {
|
||||
const foundNavigationMenuItem = navigationMenuItems?.find(
|
||||
(item) =>
|
||||
item.targetRecordId === recordId &&
|
||||
item.targetObjectMetadataId === objectMetadataItem.id,
|
||||
);
|
||||
return !!foundNavigationMenuItem;
|
||||
}
|
||||
|
||||
const foundFavorite = favorites?.find(
|
||||
(favorite) => favorite.recordId === recordId,
|
||||
const foundNavigationMenuItem = navigationMenuItems?.find(
|
||||
(item) =>
|
||||
item.targetRecordId === recordId &&
|
||||
item.targetObjectMetadataId === objectMetadataItem.id,
|
||||
);
|
||||
return !!foundFavorite;
|
||||
}, [
|
||||
recordId,
|
||||
isNavigationMenuItemEditingEnabled,
|
||||
objectMetadataItem,
|
||||
navigationMenuItems,
|
||||
favorites,
|
||||
]);
|
||||
return !!foundNavigationMenuItem;
|
||||
}, [recordId, objectMetadataItem, navigationMenuItems]);
|
||||
|
||||
const selectedRecord =
|
||||
useAtomFamilyStateValue(recordStoreFamilyState, recordId ?? '') ||
|
||||
|
||||
+1
-5
@@ -12,7 +12,6 @@ import {
|
||||
IconSearch,
|
||||
IconSparkles,
|
||||
} from 'twenty-ui/display';
|
||||
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||
|
||||
export const RECORD_AGNOSTIC_COMMAND_MENU_ITEMS_CONFIG: Record<
|
||||
string,
|
||||
@@ -108,10 +107,7 @@ export const RECORD_AGNOSTIC_COMMAND_MENU_ITEMS_CONFIG: Record<
|
||||
Icon: IconLayout,
|
||||
isPinned: false,
|
||||
availableOn: [CommandMenuItemViewType.GLOBAL],
|
||||
shouldBeRegistered: ({ isFeatureFlagEnabled }) =>
|
||||
isFeatureFlagEnabled(
|
||||
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED,
|
||||
),
|
||||
shouldBeRegistered: () => true,
|
||||
component: <EditNavigationSidebarNoSelectionRecordCommand />,
|
||||
},
|
||||
};
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
import { Command } from '@/command-menu-item/display/components/Command';
|
||||
import { useSelectedRecordIdOrThrow } from '@/command-menu-item/record/single-record/hooks/useSelectedRecordIdOrThrow';
|
||||
import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow';
|
||||
import { useCreateFavorite } from '@/favorites/hooks/useCreateFavorite';
|
||||
import { useCreateNavigationMenuItem } from '@/navigation-menu-item/hooks/useCreateNavigationMenuItem';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
@@ -11,7 +11,7 @@ export const AddToFavoritesSingleRecordCommand = () => {
|
||||
|
||||
const recordId = useSelectedRecordIdOrThrow();
|
||||
|
||||
const { createFavorite } = useCreateFavorite();
|
||||
const { createNavigationMenuItem } = useCreateNavigationMenuItem();
|
||||
|
||||
const recordStore = useAtomFamilyStateValue(recordStoreFamilyState, recordId);
|
||||
|
||||
@@ -20,7 +20,7 @@ export const AddToFavoritesSingleRecordCommand = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
createFavorite(recordStore, objectMetadataItem.nameSingular);
|
||||
createNavigationMenuItem(recordStore, objectMetadataItem.nameSingular);
|
||||
};
|
||||
|
||||
return <Command onClick={handleClick} />;
|
||||
|
||||
+2
-15
@@ -1,8 +1,6 @@
|
||||
import { Command } from '@/command-menu-item/display/components/Command';
|
||||
import { useSelectedRecordIdOrThrow } from '@/command-menu-item/record/single-record/hooks/useSelectedRecordIdOrThrow';
|
||||
import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite';
|
||||
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
||||
import { usePrefetchedNavigationMenuItemsData } from '@/navigation-menu-item/hooks/usePrefetchedNavigationMenuItemsData';
|
||||
import { useNavigationMenuItemsData } from '@/navigation-menu-item/hooks/useNavigationMenuItemsData';
|
||||
import { useRemoveNavigationMenuItemByTargetRecordId } from '@/navigation-menu-item/hooks/useRemoveNavigationMenuItemByTargetRecordId';
|
||||
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
|
||||
import { useRemoveSelectedRecordsFromRecordBoard } from '@/object-record/record-board/hooks/useRemoveSelectedRecordsFromRecordBoard';
|
||||
@@ -25,11 +23,8 @@ export const DeleteSingleRecordCommand = () => {
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
});
|
||||
|
||||
const { sortedFavorites: favorites } = useFavorites();
|
||||
const { deleteFavorite } = useDeleteFavorite();
|
||||
|
||||
const { navigationMenuItems, workspaceNavigationMenuItems } =
|
||||
usePrefetchedNavigationMenuItemsData();
|
||||
useNavigationMenuItemsData();
|
||||
const { removeNavigationMenuItemsByTargetRecordIds } =
|
||||
useRemoveNavigationMenuItemByTargetRecordId();
|
||||
|
||||
@@ -38,14 +33,6 @@ export const DeleteSingleRecordCommand = () => {
|
||||
|
||||
resetTableRowSelection();
|
||||
|
||||
const foundFavorite = favorites?.find(
|
||||
(favorite) => favorite.recordId === recordId,
|
||||
);
|
||||
|
||||
if (isDefined(foundFavorite)) {
|
||||
deleteFavorite(foundFavorite.id);
|
||||
}
|
||||
|
||||
const foundNavigationMenuItem = [
|
||||
...navigationMenuItems,
|
||||
...workspaceNavigationMenuItems,
|
||||
|
||||
+3
-12
@@ -1,27 +1,19 @@
|
||||
import { Command } from '@/command-menu-item/display/components/Command';
|
||||
import { useSelectedRecordIdOrThrow } from '@/command-menu-item/record/single-record/hooks/useSelectedRecordIdOrThrow';
|
||||
import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow';
|
||||
import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite';
|
||||
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
||||
import { useDeleteNavigationMenuItem } from '@/navigation-menu-item/hooks/useDeleteNavigationMenuItem';
|
||||
import { usePrefetchedNavigationMenuItemsData } from '@/navigation-menu-item/hooks/usePrefetchedNavigationMenuItemsData';
|
||||
import { useNavigationMenuItemsData } from '@/navigation-menu-item/hooks/useNavigationMenuItemsData';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const RemoveFromFavoritesSingleRecordCommand = () => {
|
||||
const recordId = useSelectedRecordIdOrThrow();
|
||||
const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow();
|
||||
|
||||
const { sortedFavorites: favorites } = useFavorites();
|
||||
const { navigationMenuItems, workspaceNavigationMenuItems } =
|
||||
usePrefetchedNavigationMenuItemsData();
|
||||
useNavigationMenuItemsData();
|
||||
|
||||
const { deleteFavorite } = useDeleteFavorite();
|
||||
const { deleteNavigationMenuItem } = useDeleteNavigationMenuItem();
|
||||
|
||||
const foundFavorite = favorites?.find(
|
||||
(favorite) => favorite.recordId === recordId,
|
||||
);
|
||||
|
||||
const foundNavigationMenuItem = [
|
||||
...navigationMenuItems,
|
||||
...workspaceNavigationMenuItems,
|
||||
@@ -32,12 +24,11 @@ export const RemoveFromFavoritesSingleRecordCommand = () => {
|
||||
);
|
||||
|
||||
const handleClick = () => {
|
||||
if (!isDefined(foundNavigationMenuItem) || !isDefined(foundFavorite)) {
|
||||
if (!isDefined(foundNavigationMenuItem)) {
|
||||
return;
|
||||
}
|
||||
|
||||
deleteNavigationMenuItem(foundNavigationMenuItem.id);
|
||||
deleteFavorite(foundFavorite.id);
|
||||
};
|
||||
|
||||
return <Command onClick={handleClick} />;
|
||||
|
||||
+8
-22
@@ -7,8 +7,7 @@ import { contextStoreIsPageInEditModeComponentState } from '@/context-store/stat
|
||||
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
|
||||
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
||||
import { usePrefetchedNavigationMenuItemsData } from '@/navigation-menu-item/hooks/usePrefetchedNavigationMenuItemsData';
|
||||
import { useNavigationMenuItemsData } from '@/navigation-menu-item/hooks/useNavigationMenuItemsData';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPermissionsForObject';
|
||||
import { hasAnySoftDeleteFilterOnViewComponentSelector } from '@/object-record/record-filter/states/hasAnySoftDeleteFilterOnView';
|
||||
@@ -18,7 +17,6 @@ import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { isNonEmptyArray } from '@sniptt/guards';
|
||||
import { useStore } from 'jotai';
|
||||
import { useContext } from 'react';
|
||||
@@ -27,7 +25,6 @@ import {
|
||||
type CommandMenuContextApi,
|
||||
} from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||
|
||||
export const useCommandMenuContextApi = (): CommandMenuContextApi => {
|
||||
const store = useStore();
|
||||
@@ -42,18 +39,13 @@ export const useCommandMenuContextApi = (): CommandMenuContextApi => {
|
||||
contextStoreTargetedRecordsRuleComponentState,
|
||||
);
|
||||
|
||||
const isNavigationMenuItemEditingEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED,
|
||||
);
|
||||
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(item) => item.id === contextStoreCurrentObjectMetadataItemId,
|
||||
);
|
||||
|
||||
const { sortedFavorites: favorites } = useFavorites();
|
||||
const { navigationMenuItems } = usePrefetchedNavigationMenuItemsData();
|
||||
const { navigationMenuItems } = useNavigationMenuItemsData();
|
||||
|
||||
const recordIds =
|
||||
contextStoreTargetedRecordsRule.mode === 'selection'
|
||||
@@ -61,22 +53,16 @@ export const useCommandMenuContextApi = (): CommandMenuContextApi => {
|
||||
: undefined;
|
||||
|
||||
const favoriteRecordIds = (() => {
|
||||
if (!isNonEmptyArray(recordIds)) {
|
||||
if (!isNonEmptyArray(recordIds) || !isDefined(objectMetadataItem)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (isNavigationMenuItemEditingEnabled && isDefined(objectMetadataItem)) {
|
||||
return recordIds.filter((recordId) =>
|
||||
navigationMenuItems?.some(
|
||||
(item) =>
|
||||
item.targetRecordId === recordId &&
|
||||
item.targetObjectMetadataId === objectMetadataItem.id,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return recordIds.filter((recordId) =>
|
||||
favorites?.some((favorite) => favorite.recordId === recordId),
|
||||
navigationMenuItems?.some(
|
||||
(item) =>
|
||||
item.targetRecordId === recordId &&
|
||||
item.targetObjectMetadataId === objectMetadataItem.id,
|
||||
),
|
||||
);
|
||||
})();
|
||||
|
||||
|
||||
-283
@@ -1,283 +0,0 @@
|
||||
import { FavoriteFolderNavigationDrawerItemDropdown } from '@/favorites/components/FavoriteFolderNavigationDrawerItemDropdown';
|
||||
import { FavoriteIcon } from '@/favorites/components/FavoriteIcon';
|
||||
import { FavoritesDroppable } from '@/favorites/components/FavoritesDroppable';
|
||||
import { FAVORITE_FOLDER_DELETE_MODAL_ID } from '@/favorites/constants/FavoriteFolderDeleteModalId';
|
||||
import { FavoritesDragContext } from '@/favorites/contexts/FavoritesDragContext';
|
||||
import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite';
|
||||
import { useDeleteFavoriteFolder } from '@/favorites/hooks/useDeleteFavoriteFolder';
|
||||
import { useRenameFavoriteFolder } from '@/favorites/hooks/useRenameFavoriteFolder';
|
||||
import { openFavoriteFolderIdsState } from '@/favorites/states/openFavoriteFolderIdsState';
|
||||
import { getFavoriteSecondaryLabel } from '@/favorites/utils/getFavoriteSecondaryLabel';
|
||||
import { isLocationMatchingFavorite } from '@/favorites/utils/isLocationMatchingFavorite';
|
||||
import { type ProcessedFavorite } from '@/favorites/utils/sortFavorites';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
|
||||
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
|
||||
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
||||
import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpenedComponentState';
|
||||
import { NavigationDrawerInput } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerInput';
|
||||
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
|
||||
import { NavigationDrawerItemsCollapsableContainer } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemsCollapsableContainer';
|
||||
import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem';
|
||||
import { currentFavoriteFolderIdState } from '@/ui/navigation/navigation-drawer/states/currentFavoriteFolderIdState';
|
||||
import { getNavigationSubItemLeftAdornment } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemLeftAdornment';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import { Droppable } from '@hello-pangea/dnd';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useContext, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { IconFolder, IconFolderOpen, IconHeartOff } from 'twenty-ui/display';
|
||||
import { LightIconButton } from 'twenty-ui/input';
|
||||
import { AnimatedExpandableContainer } from 'twenty-ui/layout';
|
||||
import { useIsMobile } from 'twenty-ui/utilities';
|
||||
|
||||
type CurrentWorkspaceMemberFavoritesProps = {
|
||||
folder: {
|
||||
folderId: string;
|
||||
folderName: string;
|
||||
favorites: ProcessedFavorite[];
|
||||
};
|
||||
isGroup: boolean;
|
||||
};
|
||||
|
||||
export const CurrentWorkspaceMemberFavorites = ({
|
||||
folder,
|
||||
isGroup,
|
||||
}: CurrentWorkspaceMemberFavoritesProps) => {
|
||||
const { t } = useLingui();
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
|
||||
const currentPath = useLocation().pathname;
|
||||
const currentViewPath = useLocation().pathname + useLocation().search;
|
||||
const { isDragging } = useContext(FavoritesDragContext);
|
||||
const [isFavoriteFolderRenaming, setIsFavoriteFolderRenaming] =
|
||||
useState(false);
|
||||
const [favoriteFolderName, setFavoriteFolderName] = useState(
|
||||
folder.folderName,
|
||||
);
|
||||
const { openModal } = useModal();
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const [openFavoriteFolderIds, setOpenFavoriteFolderIds] = useAtomState(
|
||||
openFavoriteFolderIdsState,
|
||||
);
|
||||
|
||||
const setCurrentFavoriteFolderId = useSetAtomState(
|
||||
currentFavoriteFolderIdState,
|
||||
);
|
||||
|
||||
const isOpen = openFavoriteFolderIds.includes(folder.folderId);
|
||||
|
||||
const handleToggle = () => {
|
||||
if (isMobile) {
|
||||
setCurrentFavoriteFolderId((prev) =>
|
||||
prev === folder.folderId ? null : folder.folderId,
|
||||
);
|
||||
} else {
|
||||
setOpenFavoriteFolderIds((currentOpenFolders) => {
|
||||
if (isOpen) {
|
||||
return currentOpenFolders.filter((id) => id !== folder.folderId);
|
||||
} else {
|
||||
return [...currentOpenFolders, folder.folderId];
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const { renameFavoriteFolder } = useRenameFavoriteFolder();
|
||||
const { deleteFavoriteFolder } = useDeleteFavoriteFolder();
|
||||
|
||||
const dropdownId = `favorite-folder-edit-${folder.folderId}`;
|
||||
|
||||
const isDropdownOpen = useAtomComponentStateValue(
|
||||
isDropdownOpenComponentState,
|
||||
dropdownId,
|
||||
);
|
||||
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
|
||||
const selectedFavoriteIndex = folder.favorites.findIndex((favorite) =>
|
||||
isLocationMatchingFavorite(currentPath, currentViewPath, favorite),
|
||||
);
|
||||
|
||||
const { deleteFavorite } = useDeleteFavorite();
|
||||
|
||||
const favoriteFolderContentLength = folder.favorites.length;
|
||||
|
||||
const handleSubmitRename = async (value: string) => {
|
||||
if (value === '') return;
|
||||
await renameFavoriteFolder(folder.folderId, value);
|
||||
setIsFavoriteFolderRenaming(false);
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleCancelRename = () => {
|
||||
setFavoriteFolderName(folder.folderName);
|
||||
setIsFavoriteFolderRenaming(false);
|
||||
};
|
||||
|
||||
const handleClickOutside = async (
|
||||
event: MouseEvent | TouchEvent,
|
||||
value: string,
|
||||
) => {
|
||||
if (!value) {
|
||||
setIsFavoriteFolderRenaming(false);
|
||||
return;
|
||||
}
|
||||
|
||||
await renameFavoriteFolder(folder.folderId, value);
|
||||
setIsFavoriteFolderRenaming(false);
|
||||
};
|
||||
|
||||
const modalId = `${FAVORITE_FOLDER_DELETE_MODAL_ID}-${folder.folderId}`;
|
||||
|
||||
const handleFavoriteFolderDelete = async () => {
|
||||
if (folder.favorites.length > 0) {
|
||||
openModal(modalId);
|
||||
closeDropdown(dropdownId);
|
||||
} else {
|
||||
await deleteFavoriteFolder(folder.folderId);
|
||||
closeDropdown(dropdownId);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmDelete = async () => {
|
||||
await deleteFavoriteFolder(folder.folderId);
|
||||
};
|
||||
|
||||
const rightOptions = (
|
||||
<FavoriteFolderNavigationDrawerItemDropdown
|
||||
folderId={folder.folderId}
|
||||
onRename={() => setIsFavoriteFolderRenaming(true)}
|
||||
onDelete={handleFavoriteFolderDelete}
|
||||
closeDropdown={() => {
|
||||
closeDropdown(dropdownId);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const isModalOpened = useAtomComponentStateValue(
|
||||
isModalOpenedComponentState,
|
||||
modalId,
|
||||
);
|
||||
|
||||
const favoriteCount = folder.favorites.length;
|
||||
|
||||
return (
|
||||
<>
|
||||
<NavigationDrawerItemsCollapsableContainer
|
||||
key={folder.folderId}
|
||||
isGroup={isGroup}
|
||||
>
|
||||
{isFavoriteFolderRenaming ? (
|
||||
<NavigationDrawerInput
|
||||
Icon={IconFolder}
|
||||
value={favoriteFolderName}
|
||||
onChange={setFavoriteFolderName}
|
||||
onSubmit={handleSubmitRename}
|
||||
onCancel={handleCancelRename}
|
||||
onClickOutside={handleClickOutside}
|
||||
/>
|
||||
) : (
|
||||
<FavoritesDroppable droppableId={`folder-header-${folder.folderId}`}>
|
||||
<NavigationDrawerItem
|
||||
label={folder.folderName}
|
||||
Icon={isOpen ? IconFolderOpen : IconFolder}
|
||||
onClick={handleToggle}
|
||||
rightOptions={rightOptions}
|
||||
className="navigation-drawer-item"
|
||||
isRightOptionsDropdownOpen={isDropdownOpen}
|
||||
triggerEvent="CLICK"
|
||||
preventCollapseOnMobile={isMobile}
|
||||
/>
|
||||
</FavoritesDroppable>
|
||||
)}
|
||||
|
||||
<AnimatedExpandableContainer
|
||||
isExpanded={isOpen}
|
||||
dimension="height"
|
||||
mode="fit-content"
|
||||
containAnimation
|
||||
>
|
||||
<Droppable droppableId={`folder-${folder.folderId}`}>
|
||||
{(provided) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
// oxlint-disable-next-line react/jsx-props-no-spreading
|
||||
{...provided.droppableProps}
|
||||
// TODO: (Drag Drop Bug) Adding bottom margin to ensure drag-to-last-position works. Need to find better solution that doesn't affect spacing.
|
||||
// Issue: Without margin, dragging to last position triggers next folder drop area
|
||||
>
|
||||
{folder.favorites.map((favorite, index) => (
|
||||
<DraggableItem
|
||||
key={favorite.id}
|
||||
draggableId={favorite.id}
|
||||
index={index}
|
||||
isInsideScrollableContainer
|
||||
itemComponent={
|
||||
<NavigationDrawerSubItem
|
||||
secondaryLabel={getFavoriteSecondaryLabel({
|
||||
objectMetadataItems,
|
||||
favoriteObjectNameSingular:
|
||||
favorite.objectNameSingular,
|
||||
})}
|
||||
label={favorite.labelIdentifier}
|
||||
Icon={() => <FavoriteIcon favorite={favorite} />}
|
||||
to={isDragging ? undefined : favorite.link}
|
||||
active={index === selectedFavoriteIndex}
|
||||
subItemState={getNavigationSubItemLeftAdornment({
|
||||
index,
|
||||
arrayLength: favoriteFolderContentLength,
|
||||
selectedIndex: selectedFavoriteIndex,
|
||||
})}
|
||||
rightOptions={
|
||||
<LightIconButton
|
||||
Icon={IconHeartOff}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
deleteFavorite(favorite.id);
|
||||
}}
|
||||
accent="tertiary"
|
||||
/>
|
||||
}
|
||||
isDragging={isDragging}
|
||||
triggerEvent="CLICK"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</AnimatedExpandableContainer>
|
||||
</NavigationDrawerItemsCollapsableContainer>
|
||||
|
||||
{isModalOpened &&
|
||||
createPortal(
|
||||
<ConfirmationModal
|
||||
modalInstanceId={modalId}
|
||||
title={
|
||||
folder.favorites.length > 1
|
||||
? t`Remove ${favoriteCount} favorites?`
|
||||
: t`Remove ${favoriteCount} favorite?`
|
||||
}
|
||||
subtitle={
|
||||
folder.favorites.length > 1
|
||||
? t`This action will delete this favorite folder and all ${favoriteCount} favorites inside. Do you want to continue?`
|
||||
: t`This action will delete this favorite folder and the favorite inside. Do you want to continue?`
|
||||
}
|
||||
onConfirmClick={handleConfirmDelete}
|
||||
confirmButtonText={t`Delete Folder`}
|
||||
/>,
|
||||
document.body,
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
-91
@@ -1,91 +0,0 @@
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { CurrentWorkspaceMemberOrphanFavorites } from '@/favorites/components/CurrentWorkspaceMemberOrphanFavorites';
|
||||
import { FavoritesDragProvider } from '@/favorites/components/FavoritesDragProvider';
|
||||
import { FavoriteFolders } from '@/favorites/components/FavoritesFolders';
|
||||
import { FavoritesSkeletonLoader } from '@/favorites/components/FavoritesSkeletonLoader';
|
||||
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
||||
import { useFavoritesByFolder } from '@/favorites/hooks/useFavoritesByFolder';
|
||||
import { isFavoriteFolderCreatingState } from '@/favorites/states/isFavoriteFolderCreatingState';
|
||||
import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading';
|
||||
import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper';
|
||||
import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection';
|
||||
import { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle';
|
||||
import { useNavigationSection } from '@/ui/navigation/navigation-drawer/hooks/useNavigationSection';
|
||||
import { isNavigationSectionOpenFamilyState } from '@/ui/navigation/navigation-drawer/states/isNavigationSectionOpenFamilyState';
|
||||
import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
|
||||
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconFolderPlus } from 'twenty-ui/display';
|
||||
import { LightIconButton } from 'twenty-ui/input';
|
||||
import { AnimatedExpandableContainer } from 'twenty-ui/layout';
|
||||
|
||||
export const CurrentWorkspaceMemberFavoritesFolders = () => {
|
||||
const currentWorkspaceMember = useAtomStateValue(currentWorkspaceMemberState);
|
||||
const { sortedFavorites: favorites } = useFavorites();
|
||||
const { favoritesByFolder } = useFavoritesByFolder();
|
||||
|
||||
const [isFavoriteFolderCreating, setIsFavoriteFolderCreating] = useAtomState(
|
||||
isFavoriteFolderCreatingState,
|
||||
);
|
||||
|
||||
const loading = useIsPrefetchLoading();
|
||||
|
||||
const { t } = useLingui();
|
||||
|
||||
const { toggleNavigationSection, openNavigationSection } =
|
||||
useNavigationSection('Favorites');
|
||||
const isNavigationSectionOpen = useAtomFamilyStateValue(
|
||||
isNavigationSectionOpenFamilyState,
|
||||
'Favorites',
|
||||
);
|
||||
|
||||
const toggleNewFolder = () => {
|
||||
openNavigationSection();
|
||||
setIsFavoriteFolderCreating((current) => !current);
|
||||
};
|
||||
|
||||
if (loading && isDefined(currentWorkspaceMember)) {
|
||||
return <FavoritesSkeletonLoader />;
|
||||
}
|
||||
|
||||
if (
|
||||
favorites.length === 0 &&
|
||||
!isFavoriteFolderCreating &&
|
||||
favoritesByFolder.length === 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<NavigationDrawerSection>
|
||||
<NavigationDrawerAnimatedCollapseWrapper>
|
||||
<NavigationDrawerSectionTitle
|
||||
label={t`Favorites`}
|
||||
onClick={toggleNavigationSection}
|
||||
rightIcon={
|
||||
<LightIconButton
|
||||
Icon={IconFolderPlus}
|
||||
onClick={toggleNewFolder}
|
||||
accent="tertiary"
|
||||
/>
|
||||
}
|
||||
isOpen={isNavigationSectionOpen}
|
||||
/>
|
||||
</NavigationDrawerAnimatedCollapseWrapper>
|
||||
<AnimatedExpandableContainer
|
||||
isExpanded={isNavigationSectionOpen}
|
||||
dimension="height"
|
||||
mode="fit-content"
|
||||
containAnimation
|
||||
initial={false}
|
||||
>
|
||||
<FavoritesDragProvider>
|
||||
<FavoriteFolders />
|
||||
<CurrentWorkspaceMemberOrphanFavorites />
|
||||
</FavoritesDragProvider>
|
||||
</AnimatedExpandableContainer>
|
||||
</NavigationDrawerSection>
|
||||
);
|
||||
};
|
||||
-85
@@ -1,85 +0,0 @@
|
||||
import { FavoriteIcon } from '@/favorites/components/FavoriteIcon';
|
||||
import { FavoritesDroppable } from '@/favorites/components/FavoritesDroppable';
|
||||
import { FavoritesDragContext } from '@/favorites/contexts/FavoritesDragContext';
|
||||
import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite';
|
||||
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
||||
import { getFavoriteSecondaryLabel } from '@/favorites/utils/getFavoriteSecondaryLabel';
|
||||
import { isLocationMatchingFavorite } from '@/favorites/utils/isLocationMatchingFavorite';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
|
||||
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useContext } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { IconHeartOff } from 'twenty-ui/display';
|
||||
import { LightIconButton } from 'twenty-ui/input';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledEmptyContainer = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledOrphanFavoritesContainer = styled.div`
|
||||
margin-bottom: ${themeCssVariables.betweenSiblingsGap};
|
||||
`;
|
||||
|
||||
export const CurrentWorkspaceMemberOrphanFavorites = () => {
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
|
||||
const { sortedFavorites: favorites } = useFavorites();
|
||||
const { deleteFavorite } = useDeleteFavorite();
|
||||
const currentPath = useLocation().pathname;
|
||||
const currentViewPath = useLocation().pathname + useLocation().search;
|
||||
const { isDragging } = useContext(FavoritesDragContext);
|
||||
|
||||
const orphanFavorites = favorites.filter(
|
||||
(favorite) => !favorite.favoriteFolderId,
|
||||
);
|
||||
|
||||
return (
|
||||
<FavoritesDroppable droppableId="orphan-favorites">
|
||||
{orphanFavorites.length > 0 ? (
|
||||
orphanFavorites.map((favorite, index) => (
|
||||
<DraggableItem
|
||||
key={favorite.id}
|
||||
draggableId={favorite.id}
|
||||
index={index}
|
||||
isInsideScrollableContainer={true}
|
||||
itemComponent={
|
||||
<StyledOrphanFavoritesContainer>
|
||||
<NavigationDrawerItem
|
||||
secondaryLabel={getFavoriteSecondaryLabel({
|
||||
objectMetadataItems,
|
||||
favoriteObjectNameSingular: favorite.objectNameSingular,
|
||||
})}
|
||||
label={favorite.labelIdentifier}
|
||||
Icon={() => <FavoriteIcon favorite={favorite} />}
|
||||
active={isLocationMatchingFavorite(
|
||||
currentPath,
|
||||
currentViewPath,
|
||||
favorite,
|
||||
)}
|
||||
to={isDragging ? undefined : favorite.link}
|
||||
rightOptions={
|
||||
<LightIconButton
|
||||
Icon={IconHeartOff}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
deleteFavorite(favorite.id);
|
||||
}}
|
||||
accent="tertiary"
|
||||
/>
|
||||
}
|
||||
isDragging={isDragging}
|
||||
triggerEvent="CLICK"
|
||||
/>
|
||||
</StyledOrphanFavoritesContainer>
|
||||
}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<StyledEmptyContainer style={{ height: isDragging ? '24px' : '1px' }} />
|
||||
)}
|
||||
</FavoritesDroppable>
|
||||
);
|
||||
};
|
||||
-62
@@ -1,62 +0,0 @@
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { IconDotsVertical, IconPencil, IconTrash } from 'twenty-ui/display';
|
||||
import { LightIconButton } from 'twenty-ui/input';
|
||||
import { MenuItem } from 'twenty-ui/navigation';
|
||||
|
||||
type FavoriteFolderNavigationDrawerItemDropdownProps = {
|
||||
folderId: string;
|
||||
onRename: () => void;
|
||||
onDelete: () => void;
|
||||
closeDropdown: () => void;
|
||||
};
|
||||
|
||||
export const FavoriteFolderNavigationDrawerItemDropdown = ({
|
||||
folderId,
|
||||
onRename,
|
||||
onDelete,
|
||||
closeDropdown,
|
||||
}: FavoriteFolderNavigationDrawerItemDropdownProps) => {
|
||||
const { t } = useLingui();
|
||||
const handleRename = () => {
|
||||
closeDropdown();
|
||||
onRename();
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
closeDropdown();
|
||||
onDelete();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
dropdownId={`favorite-folder-edit-${folderId}`}
|
||||
data-select-disable
|
||||
clickableComponent={
|
||||
<LightIconButton Icon={IconDotsVertical} accent="tertiary" />
|
||||
}
|
||||
dropdownPlacement="bottom-start"
|
||||
dropdownComponents={
|
||||
<DropdownContent widthInPixels={GenericDropdownContentWidth.Narrow}>
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItem
|
||||
LeftIcon={IconPencil}
|
||||
onClick={handleRename}
|
||||
accent="default"
|
||||
text={t`Rename`}
|
||||
/>
|
||||
<MenuItem
|
||||
LeftIcon={IconTrash}
|
||||
onClick={handleDelete}
|
||||
accent="danger"
|
||||
text={t`Delete`}
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownContent>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,28 +0,0 @@
|
||||
import { type ProcessedFavorite } from '@/favorites/utils/sortFavorites';
|
||||
import { useGetStandardObjectIcon } from '@/object-metadata/hooks/useGetStandardObjectIcon';
|
||||
import { useContext } from 'react';
|
||||
import { Avatar, useIcons } from 'twenty-ui/display';
|
||||
import { ThemeContext } from 'twenty-ui/theme-constants';
|
||||
|
||||
export const FavoriteIcon = ({ favorite }: { favorite: ProcessedFavorite }) => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const { getIcon } = useIcons();
|
||||
const { Icon: StandardIcon, IconColor } = useGetStandardObjectIcon(
|
||||
favorite.objectNameSingular || '',
|
||||
);
|
||||
const IconToUse =
|
||||
StandardIcon || (favorite.Icon ? getIcon(favorite.Icon) : undefined);
|
||||
const iconColorToUse = StandardIcon ? IconColor : theme.font.color.secondary;
|
||||
|
||||
return (
|
||||
<Avatar
|
||||
size="md"
|
||||
type={favorite.avatarType}
|
||||
Icon={IconToUse}
|
||||
iconColor={iconColorToUse}
|
||||
avatarUrl={favorite.avatarUrl}
|
||||
placeholder={favorite.labelIdentifier}
|
||||
placeholderColorSeed={favorite.recordId}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,59 +0,0 @@
|
||||
import { currentFavoriteFolderIdState } from '@/ui/navigation/navigation-drawer/states/currentFavoriteFolderIdState';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import { styled } from '@linaria/react';
|
||||
import { IconX } from 'twenty-ui/display';
|
||||
import { useContext } from 'react';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledBackButton = styled.button`
|
||||
align-items: center;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: ${themeCssVariables.font.color.secondary};
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
font-family: ${themeCssVariables.font.family};
|
||||
font-weight: ${themeCssVariables.font.weight.medium};
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
padding-bottom: ${themeCssVariables.spacing[1]};
|
||||
padding-left: ${themeCssVariables.spacing[1]};
|
||||
padding-right: ${themeCssVariables.spacing[0.5]};
|
||||
padding-top: ${themeCssVariables.spacing[1]};
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
background: ${themeCssVariables.background.transparent.light};
|
||||
border-radius: ${themeCssVariables.border.radius.sm};
|
||||
}
|
||||
`;
|
||||
|
||||
type FavoritesBackButtonProps = {
|
||||
folderName: string;
|
||||
};
|
||||
|
||||
export const FavoritesBackButton = ({
|
||||
folderName,
|
||||
}: FavoritesBackButtonProps) => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const setCurrentFavoriteFolderId = useSetAtomState(
|
||||
currentFavoriteFolderIdState,
|
||||
);
|
||||
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setCurrentFavoriteFolderId(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledBackButton onClick={handleClick}>
|
||||
<IconX
|
||||
size={theme.icon.size.md}
|
||||
stroke={theme.icon.stroke.lg}
|
||||
color={theme.font.color.tertiary}
|
||||
/>
|
||||
<span>{folderName}</span>
|
||||
</StyledBackButton>
|
||||
);
|
||||
};
|
||||
@@ -1,37 +0,0 @@
|
||||
import { FavoritesDragContext } from '@/favorites/contexts/FavoritesDragContext';
|
||||
import { useHandleFavoriteDragAndDrop } from '@/favorites/hooks/useHandleFavoriteDragAndDrop';
|
||||
import {
|
||||
DragDropContext,
|
||||
type DragStart,
|
||||
type DropResult,
|
||||
type ResponderProvided,
|
||||
} from '@hello-pangea/dnd';
|
||||
import { type ReactNode, useState } from 'react';
|
||||
|
||||
type FavoritesDragProviderProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export const FavoritesDragProvider = ({
|
||||
children,
|
||||
}: FavoritesDragProviderProps) => {
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const { handleFavoriteDragAndDrop } = useHandleFavoriteDragAndDrop();
|
||||
|
||||
const handleDragStart = (_: DragStart) => {
|
||||
setIsDragging(true);
|
||||
};
|
||||
|
||||
const handleDragEnd = (result: DropResult, provided: ResponderProvided) => {
|
||||
setIsDragging(false);
|
||||
handleFavoriteDragAndDrop(result, provided);
|
||||
};
|
||||
|
||||
return (
|
||||
<FavoritesDragContext.Provider value={{ isDragging }}>
|
||||
<DragDropContext onDragEnd={handleDragEnd} onDragStart={handleDragStart}>
|
||||
{children}
|
||||
</DragDropContext>
|
||||
</FavoritesDragContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -1,72 +0,0 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { Droppable } from '@hello-pangea/dnd';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
type FavoritesDroppableProps = {
|
||||
droppableId: string;
|
||||
children: React.ReactNode;
|
||||
isDragIndicatorVisible?: boolean;
|
||||
showDropLine?: boolean;
|
||||
};
|
||||
|
||||
const StyledDroppableWrapper = styled.div<{
|
||||
isDraggingOver: boolean;
|
||||
isDragIndicatorVisible: boolean;
|
||||
showDropLine: boolean;
|
||||
}>`
|
||||
position: relative;
|
||||
transition: all 150ms ease-in-out;
|
||||
width: 100%;
|
||||
|
||||
${({ isDraggingOver, isDragIndicatorVisible, showDropLine }) =>
|
||||
isDraggingOver && isDragIndicatorVisible
|
||||
? `
|
||||
background-color: ${themeCssVariables.background.transparent.blue};
|
||||
|
||||
${
|
||||
showDropLine
|
||||
? `
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background-color: ${themeCssVariables.color.blue};
|
||||
border-radius: ${themeCssVariables.border.radius.sm} ${themeCssVariables.border.radius.sm} 0 0;
|
||||
}
|
||||
`
|
||||
: ''
|
||||
}
|
||||
`
|
||||
: ''}
|
||||
`;
|
||||
|
||||
export const FavoritesDroppable = ({
|
||||
droppableId,
|
||||
children,
|
||||
isDragIndicatorVisible = true,
|
||||
showDropLine = true,
|
||||
}: FavoritesDroppableProps) => {
|
||||
return (
|
||||
<Droppable droppableId={droppableId}>
|
||||
{(provided, snapshot) => (
|
||||
<StyledDroppableWrapper
|
||||
isDraggingOver={snapshot.isDraggingOver}
|
||||
isDragIndicatorVisible={isDragIndicatorVisible}
|
||||
showDropLine={showDropLine}
|
||||
>
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
// oxlint-disable-next-line react/jsx-props-no-spreading
|
||||
{...provided.droppableProps}
|
||||
>
|
||||
{children}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
</StyledDroppableWrapper>
|
||||
)}
|
||||
</Droppable>
|
||||
);
|
||||
};
|
||||
@@ -1,77 +0,0 @@
|
||||
import { FavoriteIcon } from '@/favorites/components/FavoriteIcon';
|
||||
import { FavoritesBackButton } from '@/favorites/components/FavoritesBackButton';
|
||||
import { FavoritesDragProvider } from '@/favorites/components/FavoritesDragProvider';
|
||||
import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite';
|
||||
import { getFavoriteSecondaryLabel } from '@/favorites/utils/getFavoriteSecondaryLabel';
|
||||
import { type ProcessedFavorite } from '@/favorites/utils/sortFavorites';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
|
||||
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { Droppable } from '@hello-pangea/dnd';
|
||||
import { IconHeartOff } from 'twenty-ui/display';
|
||||
import { LightIconButton } from 'twenty-ui/input';
|
||||
|
||||
type FavoritesFolderContentProps = {
|
||||
folderId: string;
|
||||
folderName: string;
|
||||
favorites: ProcessedFavorite[];
|
||||
};
|
||||
|
||||
export const FavoritesFolderContent = ({
|
||||
folderName,
|
||||
folderId,
|
||||
favorites,
|
||||
}: FavoritesFolderContentProps) => {
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
|
||||
const { deleteFavorite } = useDeleteFavorite();
|
||||
|
||||
return (
|
||||
<>
|
||||
<FavoritesBackButton folderName={folderName} />
|
||||
<FavoritesDragProvider>
|
||||
<Droppable droppableId={`folder-${folderId}`}>
|
||||
{(provided) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
// oxlint-disable-next-line react/jsx-props-no-spreading
|
||||
{...provided.droppableProps}
|
||||
>
|
||||
{favorites.map((favorite, index) => (
|
||||
<DraggableItem
|
||||
key={favorite.id}
|
||||
draggableId={favorite.id}
|
||||
index={index}
|
||||
isInsideScrollableContainer
|
||||
itemComponent={
|
||||
<NavigationDrawerItem
|
||||
secondaryLabel={getFavoriteSecondaryLabel({
|
||||
objectMetadataItems,
|
||||
favoriteObjectNameSingular: favorite.objectNameSingular,
|
||||
})}
|
||||
label={favorite.labelIdentifier}
|
||||
Icon={() => <FavoriteIcon favorite={favorite} />}
|
||||
rightOptions={
|
||||
<LightIconButton
|
||||
Icon={IconHeartOff}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
deleteFavorite(favorite.id);
|
||||
}}
|
||||
accent="tertiary"
|
||||
/>
|
||||
}
|
||||
triggerEvent="CLICK"
|
||||
to={favorite.link}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</FavoritesDragProvider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,76 +0,0 @@
|
||||
import { CurrentWorkspaceMemberFavorites } from '@/favorites/components/CurrentWorkspaceMemberFavorites';
|
||||
import { useCreateFavoriteFolder } from '@/favorites/hooks/useCreateFavoriteFolder';
|
||||
import { useFavoritesByFolder } from '@/favorites/hooks/useFavoritesByFolder';
|
||||
import { isFavoriteFolderCreatingState } from '@/favorites/states/isFavoriteFolderCreatingState';
|
||||
import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper';
|
||||
import { NavigationDrawerInput } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerInput';
|
||||
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
||||
import { useState } from 'react';
|
||||
import { IconFolder } from 'twenty-ui/display';
|
||||
|
||||
export const FavoriteFolders = () => {
|
||||
const [newFolderName, setNewFolderName] = useState('');
|
||||
|
||||
const { favoritesByFolder } = useFavoritesByFolder();
|
||||
const { createNewFavoriteFolder } = useCreateFavoriteFolder();
|
||||
|
||||
const [isFavoriteFolderCreating, setIsFavoriteFolderCreating] = useAtomState(
|
||||
isFavoriteFolderCreatingState,
|
||||
);
|
||||
|
||||
const handleFavoriteFolderNameChange = (value: string) => {
|
||||
setNewFolderName(value);
|
||||
};
|
||||
|
||||
const handleSubmitFavoriteFolderCreation = async (value: string) => {
|
||||
if (value === '') return;
|
||||
|
||||
setIsFavoriteFolderCreating(false);
|
||||
setNewFolderName('');
|
||||
await createNewFavoriteFolder(value);
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleClickOutside = async (
|
||||
_event: MouseEvent | TouchEvent,
|
||||
value: string,
|
||||
) => {
|
||||
if (!value) {
|
||||
setIsFavoriteFolderCreating(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsFavoriteFolderCreating(false);
|
||||
setNewFolderName('');
|
||||
await createNewFavoriteFolder(value);
|
||||
};
|
||||
|
||||
const handleCancelFavoriteFolderCreation = () => {
|
||||
setNewFolderName('');
|
||||
setIsFavoriteFolderCreating(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{isFavoriteFolderCreating && (
|
||||
<NavigationDrawerAnimatedCollapseWrapper>
|
||||
<NavigationDrawerInput
|
||||
Icon={IconFolder}
|
||||
value={newFolderName}
|
||||
onChange={handleFavoriteFolderNameChange}
|
||||
onSubmit={handleSubmitFavoriteFolderCreation}
|
||||
onCancel={handleCancelFavoriteFolderCreation}
|
||||
onClickOutside={handleClickOutside}
|
||||
/>
|
||||
</NavigationDrawerAnimatedCollapseWrapper>
|
||||
)}
|
||||
{favoritesByFolder.map((folder) => (
|
||||
<CurrentWorkspaceMemberFavorites
|
||||
key={folder.folderId}
|
||||
folder={folder}
|
||||
isGroup={favoritesByFolder.length > 1}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,47 +0,0 @@
|
||||
import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useContext } from 'react';
|
||||
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledSkeletonContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
height: 71px;
|
||||
padding-left: ${themeCssVariables.spacing[1]};
|
||||
`;
|
||||
|
||||
const StyledSkeletonColumn = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${themeCssVariables.spacing[1]};
|
||||
`;
|
||||
|
||||
export const FavoritesSkeletonLoader = () => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
return (
|
||||
<SkeletonTheme
|
||||
baseColor={theme.background.tertiary}
|
||||
highlightColor={theme.background.transparent.lighter}
|
||||
borderRadius={4}
|
||||
>
|
||||
<StyledSkeletonContainer>
|
||||
<Skeleton
|
||||
width={56}
|
||||
height={SKELETON_LOADER_HEIGHT_SIZES.standard.xs}
|
||||
/>
|
||||
<StyledSkeletonColumn>
|
||||
<Skeleton
|
||||
width={196}
|
||||
height={SKELETON_LOADER_HEIGHT_SIZES.standard.s}
|
||||
/>
|
||||
<Skeleton
|
||||
width={196}
|
||||
height={SKELETON_LOADER_HEIGHT_SIZES.standard.s}
|
||||
/>
|
||||
</StyledSkeletonColumn>
|
||||
</StyledSkeletonContainer>
|
||||
</SkeletonTheme>
|
||||
);
|
||||
};
|
||||
@@ -1,28 +0,0 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { IconHeart, IconHeartOff } from 'twenty-ui/display';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
|
||||
type PageFavoriteButtonProps = {
|
||||
isFavorite: boolean;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
export const PageFavoriteButton = ({
|
||||
isFavorite,
|
||||
onClick,
|
||||
}: PageFavoriteButtonProps) => {
|
||||
const title = isFavorite ? t`Remove from favorites` : t`Add to favorites`;
|
||||
|
||||
return (
|
||||
<Button
|
||||
Icon={isFavorite ? IconHeartOff : IconHeart}
|
||||
dataTestId="favorite-button"
|
||||
size="small"
|
||||
variant="secondary"
|
||||
accent="default"
|
||||
title={title}
|
||||
onClick={onClick}
|
||||
ariaLabel={title}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,24 +0,0 @@
|
||||
import { useWorkspaceFavorites } from '@/favorites/hooks/useWorkspaceFavorites';
|
||||
import { NavigationDrawerSectionForObjectMetadataItems } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItems';
|
||||
import { NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader';
|
||||
import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
|
||||
export const WorkspaceFavorites = () => {
|
||||
const { workspaceFavoritesObjectMetadataItems } = useWorkspaceFavorites();
|
||||
|
||||
const loading = useIsPrefetchLoading();
|
||||
const { t } = useLingui();
|
||||
|
||||
if (loading) {
|
||||
return <NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<NavigationDrawerSectionForObjectMetadataItems
|
||||
sectionTitle={t`Workspace`}
|
||||
objectMetadataItems={workspaceFavoritesObjectMetadataItems}
|
||||
isRemote={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
import { FOLDER_DROPPABLE_IDS } from '@/ui/layout/draggable-list/utils/folderDroppableIds';
|
||||
|
||||
export const FAVORITE_DROPPABLE_IDS = {
|
||||
ORPHAN_FAVORITES: 'orphan-favorites',
|
||||
FOLDER_PREFIX: FOLDER_DROPPABLE_IDS.FOLDER_PREFIX,
|
||||
FOLDER_HEADER_PREFIX: FOLDER_DROPPABLE_IDS.FOLDER_HEADER_PREFIX,
|
||||
} as const;
|
||||
@@ -1 +0,0 @@
|
||||
export const FAVORITE_FOLDER_DELETE_MODAL_ID = 'favorite-folder-delete-modal';
|
||||
@@ -1,9 +0,0 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
type FavoritesDragContextType = {
|
||||
isDragging: boolean;
|
||||
};
|
||||
|
||||
export const FavoritesDragContext = createContext<FavoritesDragContextType>({
|
||||
isDragging: false,
|
||||
});
|
||||
-111
@@ -1,111 +0,0 @@
|
||||
import { FavoriteFolderPickerFooter } from '@/favorites/favorite-folder-picker/components/FavoriteFolderPickerFooter';
|
||||
import { FavoriteFolderPickerList } from '@/favorites/favorite-folder-picker/components/FavoriteFolderPickerList';
|
||||
import { FavoriteFolderPickerSearchInput } from '@/favorites/favorite-folder-picker/components/FavoriteFolderPickerSearchInput';
|
||||
import { useFavoriteFolderPicker } from '@/favorites/favorite-folder-picker/hooks/useFavoriteFolderPicker';
|
||||
import { FavoriteFolderPickerInstanceContext } from '@/favorites/favorite-folder-picker/states/context/FavoriteFolderPickerInstanceContext';
|
||||
import { favoriteFolderSearchFilterComponentState } from '@/favorites/favorite-folder-picker/states/favoriteFoldersSearchFilterComponentState';
|
||||
import { isFavoriteFolderCreatingState } from '@/favorites/states/isFavoriteFolderCreatingState';
|
||||
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
|
||||
import { Key } from 'ts-key-enum';
|
||||
|
||||
type FavoriteFolderPickerProps = {
|
||||
onSubmit?: () => void;
|
||||
record?: ObjectRecord;
|
||||
objectNameSingular: string;
|
||||
dropdownId: string;
|
||||
};
|
||||
|
||||
const NO_FOLDER_ID = 'no-folder';
|
||||
|
||||
export const FavoriteFolderPicker = ({
|
||||
onSubmit,
|
||||
record,
|
||||
objectNameSingular,
|
||||
dropdownId,
|
||||
}: FavoriteFolderPickerProps) => {
|
||||
const [isFavoriteFolderCreating, setIsFavoriteFolderCreating] = useAtomState(
|
||||
isFavoriteFolderCreatingState,
|
||||
);
|
||||
|
||||
const instanceId = useAvailableComponentInstanceIdOrThrow(
|
||||
FavoriteFolderPickerInstanceContext,
|
||||
);
|
||||
|
||||
const { favoriteFolders, toggleFolderSelection } = useFavoriteFolderPicker({
|
||||
record,
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const [favoriteFolderSearchFilter] = useAtomComponentState(
|
||||
favoriteFolderSearchFilterComponentState,
|
||||
);
|
||||
|
||||
const filteredFolders = favoriteFolders.filter((folder) =>
|
||||
folder.name
|
||||
.toLowerCase()
|
||||
.includes(favoriteFolderSearchFilter.toLowerCase()),
|
||||
);
|
||||
|
||||
const showNoFolderOption =
|
||||
!favoriteFolderSearchFilter ||
|
||||
'no folder'.includes(favoriteFolderSearchFilter.toLowerCase());
|
||||
|
||||
useHotkeysOnFocusedElement({
|
||||
keys: [Key.Escape],
|
||||
callback: () => {
|
||||
if (isFavoriteFolderCreating) {
|
||||
setIsFavoriteFolderCreating(false);
|
||||
return;
|
||||
}
|
||||
onSubmit?.();
|
||||
},
|
||||
focusId: dropdownId,
|
||||
dependencies: [onSubmit, isFavoriteFolderCreating],
|
||||
});
|
||||
|
||||
useHotkeysOnFocusedElement({
|
||||
keys: [Key.Enter],
|
||||
callback: () => {
|
||||
if (filteredFolders.length === 1 && !showNoFolderOption) {
|
||||
toggleFolderSelection(filteredFolders[0].id);
|
||||
onSubmit?.();
|
||||
return;
|
||||
}
|
||||
|
||||
if (showNoFolderOption && filteredFolders.length === 0) {
|
||||
toggleFolderSelection(NO_FOLDER_ID);
|
||||
onSubmit?.();
|
||||
return;
|
||||
}
|
||||
},
|
||||
focusId: instanceId,
|
||||
dependencies: [
|
||||
filteredFolders,
|
||||
showNoFolderOption,
|
||||
toggleFolderSelection,
|
||||
onSubmit,
|
||||
],
|
||||
});
|
||||
|
||||
return (
|
||||
<DropdownContent>
|
||||
<FavoriteFolderPickerSearchInput />
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer hasMaxHeight>
|
||||
<FavoriteFolderPickerList
|
||||
folders={favoriteFolders}
|
||||
toggleFolderSelection={toggleFolderSelection}
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
<DropdownMenuSeparator />
|
||||
<FavoriteFolderPickerFooter dropdownId={dropdownId} />
|
||||
</DropdownContent>
|
||||
);
|
||||
};
|
||||
-82
@@ -1,82 +0,0 @@
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
import { favoriteFolderIdsPickerComponentState } from '@/favorites/favorite-folder-picker/states/favoriteFolderIdPickerComponentState';
|
||||
import { favoriteFolderPickerCheckedComponentState } from '@/favorites/favorite-folder-picker/states/favoriteFolderPickerCheckedComponentState';
|
||||
import { favoriteFolderPickerComponentFamilyState } from '@/favorites/favorite-folder-picker/states/favoriteFolderPickerComponentFamilyState';
|
||||
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
||||
import { usePrefetchedFavoritesFoldersData } from '@/favorites/hooks/usePrefetchedFavoritesFoldersData';
|
||||
import { type FavoriteFolder } from '@/favorites/types/FavoriteFolder';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { useAtomComponentFamilyStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentFamilyStateCallbackState';
|
||||
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
|
||||
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useStore } from 'jotai';
|
||||
|
||||
type FavoriteFolderPickerEffectProps = {
|
||||
record?: ObjectRecord;
|
||||
};
|
||||
|
||||
export const FavoriteFolderPickerEffect = ({
|
||||
record,
|
||||
}: FavoriteFolderPickerEffectProps) => {
|
||||
const store = useStore();
|
||||
const [favoriteFolderIdsPicker, setFavoriteFolderIdsPicker] =
|
||||
useAtomComponentState(favoriteFolderIdsPickerComponentState);
|
||||
|
||||
const favoriteFolderPickerFamily = useAtomComponentFamilyStateCallbackState(
|
||||
favoriteFolderPickerComponentFamilyState,
|
||||
);
|
||||
|
||||
const { favoriteFolders } = usePrefetchedFavoritesFoldersData();
|
||||
|
||||
const { sortedFavorites: favorites } = useFavorites();
|
||||
const setFavoriteFolderPickerChecked = useSetAtomComponentState(
|
||||
favoriteFolderPickerCheckedComponentState,
|
||||
);
|
||||
|
||||
const updateFolders = useCallback(
|
||||
(folders: FavoriteFolder[]) => {
|
||||
folders.forEach((folder) => {
|
||||
const atom = favoriteFolderPickerFamily(folder.id);
|
||||
const currentFolder = store.get(atom);
|
||||
|
||||
if (!isDeeplyEqual(folder, currentFolder)) {
|
||||
store.set(atom, folder);
|
||||
}
|
||||
});
|
||||
},
|
||||
[favoriteFolderPickerFamily, store],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isDefined(favoriteFolders)) {
|
||||
updateFolders(favoriteFolders);
|
||||
|
||||
const folderIds = favoriteFolders.map((folder) => folder.id);
|
||||
if (!isDeeplyEqual(folderIds, favoriteFolderIdsPicker)) {
|
||||
setFavoriteFolderIdsPicker(folderIds);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
favoriteFolders,
|
||||
favoriteFolderIdsPicker,
|
||||
setFavoriteFolderIdsPicker,
|
||||
updateFolders,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const targetId = record?.id;
|
||||
const checkedFolderIds = favorites
|
||||
.filter(
|
||||
(favorite) =>
|
||||
favorite.recordId === targetId &&
|
||||
isDefined(favorite.forWorkspaceMemberId),
|
||||
)
|
||||
.map((favorite) => favorite.favoriteFolderId ?? 'no-folder');
|
||||
setFavoriteFolderPickerChecked(checkedFolderIds);
|
||||
}, [favorites, setFavoriteFolderPickerChecked, record?.id]);
|
||||
|
||||
return null;
|
||||
};
|
||||
-44
@@ -1,44 +0,0 @@
|
||||
import { isFavoriteFolderCreatingState } from '@/favorites/states/isFavoriteFolderCreatingState';
|
||||
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import { useNavigationSection } from '@/ui/navigation/navigation-drawer/hooks/useNavigationSection';
|
||||
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useContext } from 'react';
|
||||
import { IconPlus } from 'twenty-ui/display';
|
||||
import { MenuItem } from 'twenty-ui/navigation';
|
||||
import { ThemeContext } from 'twenty-ui/theme-constants';
|
||||
export const FavoriteFolderPickerFooter = ({
|
||||
dropdownId,
|
||||
}: {
|
||||
dropdownId: string;
|
||||
}) => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const { t } = useLingui();
|
||||
const [, setIsFavoriteFolderCreating] = useAtomState(
|
||||
isFavoriteFolderCreatingState,
|
||||
);
|
||||
const setIsNavigationDrawerExpanded = useSetAtomState(
|
||||
isNavigationDrawerExpandedState,
|
||||
);
|
||||
const { openNavigationSection } = useNavigationSection('Favorites');
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
|
||||
return (
|
||||
<DropdownMenuItemsContainer scrollable={false}>
|
||||
<MenuItem
|
||||
className="add-folder"
|
||||
onClick={() => {
|
||||
setIsNavigationDrawerExpanded(true);
|
||||
openNavigationSection();
|
||||
setIsFavoriteFolderCreating(true);
|
||||
closeDropdown(dropdownId);
|
||||
}}
|
||||
text={t`Add folder`}
|
||||
LeftIcon={() => <IconPlus size={theme.icon.size.md} />}
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
);
|
||||
};
|
||||
-79
@@ -1,79 +0,0 @@
|
||||
import { favoriteFolderPickerCheckedComponentState } from '@/favorites/favorite-folder-picker/states/favoriteFolderPickerCheckedComponentState';
|
||||
import { favoriteFolderSearchFilterComponentState } from '@/favorites/favorite-folder-picker/states/favoriteFoldersSearchFilterComponentState';
|
||||
import { type FavoriteFolder } from '@/favorites/types/FavoriteFolder';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { MenuItem, MenuItemMultiSelect } from 'twenty-ui/navigation';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledItemsContainer = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledSeparatorContainer = styled.div`
|
||||
margin-bottom: ${themeCssVariables.spacing[1]};
|
||||
margin-top: ${themeCssVariables.spacing[1]};
|
||||
`;
|
||||
|
||||
type FavoriteFolderPickerListProps = {
|
||||
folders: FavoriteFolder[];
|
||||
toggleFolderSelection: (folderId: string) => void;
|
||||
};
|
||||
|
||||
export const NO_FOLDER_ID = 'no-folder';
|
||||
|
||||
export const FavoriteFolderPickerList = ({
|
||||
folders,
|
||||
toggleFolderSelection,
|
||||
}: FavoriteFolderPickerListProps) => {
|
||||
const { t } = useLingui();
|
||||
const [favoriteFolderSearchFilter] = useAtomComponentState(
|
||||
favoriteFolderSearchFilterComponentState,
|
||||
);
|
||||
|
||||
const [favoriteFolderPickerChecked] = useAtomComponentState(
|
||||
favoriteFolderPickerCheckedComponentState,
|
||||
);
|
||||
|
||||
const filteredFolders = folders.filter((folder) =>
|
||||
folder.name
|
||||
.toLowerCase()
|
||||
.includes(favoriteFolderSearchFilter.toLowerCase()),
|
||||
);
|
||||
|
||||
const showNoFolderOption =
|
||||
!favoriteFolderSearchFilter ||
|
||||
'no folder'.includes(favoriteFolderSearchFilter.toLowerCase());
|
||||
|
||||
return (
|
||||
<StyledItemsContainer>
|
||||
{showNoFolderOption && (
|
||||
<MenuItemMultiSelect
|
||||
key={`menu-${NO_FOLDER_ID}`}
|
||||
onSelectChange={() => toggleFolderSelection(NO_FOLDER_ID)}
|
||||
selected={favoriteFolderPickerChecked.includes(NO_FOLDER_ID)}
|
||||
text={t`No folder`}
|
||||
className="no-folder-menu-item-multi-select"
|
||||
/>
|
||||
)}
|
||||
{showNoFolderOption && filteredFolders.length > 0 && (
|
||||
<StyledSeparatorContainer>
|
||||
<DropdownMenuSeparator />
|
||||
</StyledSeparatorContainer>
|
||||
)}
|
||||
{filteredFolders.length > 0
|
||||
? filteredFolders.map((folder) => (
|
||||
<MenuItemMultiSelect
|
||||
key={`menu-${folder.id}`}
|
||||
onSelectChange={() => toggleFolderSelection(folder.id)}
|
||||
selected={favoriteFolderPickerChecked.includes(folder.id)}
|
||||
text={folder.name}
|
||||
className="folder-menu-item-multi-select"
|
||||
/>
|
||||
))
|
||||
: !showNoFolderOption && <MenuItem text={t`No folders found`} />}
|
||||
</StyledItemsContainer>
|
||||
);
|
||||
};
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
import { favoriteFolderSearchFilterComponentState } from '@/favorites/favorite-folder-picker/states/favoriteFoldersSearchFilterComponentState';
|
||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
|
||||
import { useCallback } from 'react';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
|
||||
export const FavoriteFolderPickerSearchInput = () => {
|
||||
const [favoriteFolderSearchFilter, setFavoriteFolderSearchFilter] =
|
||||
useAtomComponentState(favoriteFolderSearchFilterComponentState);
|
||||
|
||||
const debouncedSetSearchFilter = useDebouncedCallback(
|
||||
setFavoriteFolderSearchFilter,
|
||||
100,
|
||||
{ leading: true },
|
||||
);
|
||||
|
||||
const handleFilterChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
debouncedSetSearchFilter(event.currentTarget.value);
|
||||
},
|
||||
[debouncedSetSearchFilter],
|
||||
);
|
||||
|
||||
return (
|
||||
<DropdownMenuSearchInput
|
||||
value={favoriteFolderSearchFilter}
|
||||
onChange={handleFilterChange}
|
||||
autoFocus
|
||||
/>
|
||||
);
|
||||
};
|
||||
-2
@@ -1,2 +0,0 @@
|
||||
export const FAVORITE_FOLDER_PICKER_DROPDOWN_ID =
|
||||
'favorite-folder-picker-dropdown';
|
||||
-108
@@ -1,108 +0,0 @@
|
||||
import { favoriteFolderPickerCheckedComponentState } from '@/favorites/favorite-folder-picker/states/favoriteFolderPickerCheckedComponentState';
|
||||
import { favoriteFoldersComponentSelector } from '@/favorites/favorite-folder-picker/states/selectors/favoriteFoldersComponentSelector';
|
||||
import { useCreateFavorite } from '@/favorites/hooks/useCreateFavorite';
|
||||
import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite';
|
||||
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
||||
|
||||
import { type FavoriteFolder } from '@/favorites/types/FavoriteFolder';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type useFavoriteFolderPickerProps = {
|
||||
record?: ObjectRecord;
|
||||
objectNameSingular: string;
|
||||
};
|
||||
|
||||
type useFavoriteFolderPickerReturnType = {
|
||||
favoriteFolders: FavoriteFolder[];
|
||||
toggleFolderSelection: (folderId: string) => Promise<void>;
|
||||
};
|
||||
|
||||
export const useFavoriteFolderPicker = ({
|
||||
record,
|
||||
objectNameSingular,
|
||||
}: useFavoriteFolderPickerProps): useFavoriteFolderPickerReturnType => {
|
||||
const favoriteFoldersMultiSelectChecked = useAtomComponentStateCallbackState(
|
||||
favoriteFolderPickerCheckedComponentState,
|
||||
);
|
||||
|
||||
const { sortedFavorites: favorites } = useFavorites();
|
||||
const { createFavorite } = useCreateFavorite();
|
||||
const { deleteFavorite } = useDeleteFavorite();
|
||||
|
||||
const favoriteFolders = useAtomComponentSelectorValue(
|
||||
favoriteFoldersComponentSelector,
|
||||
);
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const toggleFolderSelection = useCallback(
|
||||
async (folderId: string) => {
|
||||
const targetId = record?.id;
|
||||
const targetObject = record;
|
||||
|
||||
if (!isDefined(targetObject) || !isDefined(targetId)) {
|
||||
throw new Error(
|
||||
`Cannot toggle folder selection: record ${
|
||||
!isDefined(targetObject) ? 'object' : 'id'
|
||||
} is not defined`,
|
||||
);
|
||||
}
|
||||
|
||||
const deleteFavoriteForRecord = async (isUnorganized: boolean) => {
|
||||
const favoriteToDelete = favorites.find(
|
||||
(favorite) =>
|
||||
favorite.recordId === targetId &&
|
||||
(isUnorganized
|
||||
? !favorite.favoriteFolderId
|
||||
: favorite.favoriteFolderId === folderId),
|
||||
);
|
||||
|
||||
if (!isDefined(favoriteToDelete)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await deleteFavorite(favoriteToDelete.id);
|
||||
};
|
||||
|
||||
const checkedIds = store.get(favoriteFoldersMultiSelectChecked);
|
||||
|
||||
const isAlreadyChecked = checkedIds.includes(folderId);
|
||||
|
||||
if (isAlreadyChecked) {
|
||||
await deleteFavoriteForRecord(folderId === 'no-folder');
|
||||
|
||||
const newCheckedIds = checkedIds.filter((id) => id !== folderId);
|
||||
store.set(favoriteFoldersMultiSelectChecked, newCheckedIds);
|
||||
return;
|
||||
}
|
||||
|
||||
const folderIdToUse = folderId === 'no-folder' ? undefined : folderId;
|
||||
|
||||
if (isDefined(record)) {
|
||||
await createFavorite(record, objectNameSingular, folderIdToUse);
|
||||
}
|
||||
|
||||
const newCheckedIds = [...checkedIds, folderId];
|
||||
store.set(favoriteFoldersMultiSelectChecked, newCheckedIds);
|
||||
},
|
||||
[
|
||||
favoriteFoldersMultiSelectChecked,
|
||||
createFavorite,
|
||||
deleteFavorite,
|
||||
favorites,
|
||||
record,
|
||||
objectNameSingular,
|
||||
store,
|
||||
],
|
||||
);
|
||||
|
||||
return {
|
||||
favoriteFolders,
|
||||
toggleFolderSelection,
|
||||
};
|
||||
};
|
||||
-4
@@ -1,4 +0,0 @@
|
||||
import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext';
|
||||
|
||||
export const FavoriteFolderPickerInstanceContext =
|
||||
createComponentInstanceContext();
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
import { FavoriteFolderPickerInstanceContext } from '@/favorites/favorite-folder-picker/states/context/FavoriteFolderPickerInstanceContext';
|
||||
import { createAtomComponentState } from '@/ui/utilities/state/jotai/utils/createAtomComponentState';
|
||||
|
||||
export const favoriteFolderIdsPickerComponentState = createAtomComponentState<
|
||||
string[]
|
||||
>({
|
||||
key: 'favoriteFolderIdsPickerComponentState',
|
||||
defaultValue: [],
|
||||
componentInstanceContext: FavoriteFolderPickerInstanceContext,
|
||||
});
|
||||
-9
@@ -1,9 +0,0 @@
|
||||
import { FavoriteFolderPickerInstanceContext } from '@/favorites/favorite-folder-picker/states/context/FavoriteFolderPickerInstanceContext';
|
||||
import { createAtomComponentState } from '@/ui/utilities/state/jotai/utils/createAtomComponentState';
|
||||
|
||||
export const favoriteFolderPickerCheckedComponentState =
|
||||
createAtomComponentState<string[]>({
|
||||
key: 'favoriteFolderPickerCheckedComponentState',
|
||||
defaultValue: [],
|
||||
componentInstanceContext: FavoriteFolderPickerInstanceContext,
|
||||
});
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
import { FavoriteFolderPickerInstanceContext } from '@/favorites/favorite-folder-picker/states/context/FavoriteFolderPickerInstanceContext';
|
||||
import { type FavoriteFolder } from '@/favorites/types/FavoriteFolder';
|
||||
import { createAtomComponentFamilyState } from '@/ui/utilities/state/jotai/utils/createAtomComponentFamilyState';
|
||||
|
||||
export const favoriteFolderPickerComponentFamilyState =
|
||||
createAtomComponentFamilyState<FavoriteFolder | undefined, string>({
|
||||
key: 'favoriteFolderPickerComponentFamilyState',
|
||||
defaultValue: undefined,
|
||||
componentInstanceContext: FavoriteFolderPickerInstanceContext,
|
||||
});
|
||||
-9
@@ -1,9 +0,0 @@
|
||||
import { FavoriteFolderPickerInstanceContext } from '@/favorites/favorite-folder-picker/states/context/FavoriteFolderPickerInstanceContext';
|
||||
import { createAtomComponentState } from '@/ui/utilities/state/jotai/utils/createAtomComponentState';
|
||||
|
||||
export const favoriteFolderSearchFilterComponentState =
|
||||
createAtomComponentState<string>({
|
||||
key: 'favoriteFolderSearchFilterComponentState',
|
||||
defaultValue: '',
|
||||
componentInstanceContext: FavoriteFolderPickerInstanceContext,
|
||||
});
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
import { FavoriteFolderPickerInstanceContext } from '@/favorites/favorite-folder-picker/states/context/FavoriteFolderPickerInstanceContext';
|
||||
import { favoriteFolderIdsPickerComponentState } from '@/favorites/favorite-folder-picker/states/favoriteFolderIdPickerComponentState';
|
||||
import { favoriteFolderPickerComponentFamilyState } from '@/favorites/favorite-folder-picker/states/favoriteFolderPickerComponentFamilyState';
|
||||
import { type FavoriteFolder } from '@/favorites/types/FavoriteFolder';
|
||||
import { createAtomComponentSelector } from '@/ui/utilities/state/jotai/utils/createAtomComponentSelector';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const favoriteFoldersComponentSelector = createAtomComponentSelector<
|
||||
FavoriteFolder[]
|
||||
>({
|
||||
key: 'favoriteFoldersComponentSelector',
|
||||
componentInstanceContext: FavoriteFolderPickerInstanceContext,
|
||||
get:
|
||||
(componentStateKey) =>
|
||||
({ get }) => {
|
||||
const folderIds = get(
|
||||
favoriteFolderIdsPickerComponentState,
|
||||
componentStateKey,
|
||||
) as string[];
|
||||
|
||||
return folderIds
|
||||
.map((folderId: string) =>
|
||||
get(favoriteFolderPickerComponentFamilyState, {
|
||||
instanceId: componentStateKey.instanceId,
|
||||
familyKey: folderId,
|
||||
}),
|
||||
)
|
||||
.filter((folder): folder is FavoriteFolder => isDefined(folder));
|
||||
},
|
||||
});
|
||||
@@ -1,290 +0,0 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
import { Favorite } from '@/favorites/types/Favorite';
|
||||
import { generateCreateOneRecordMutation } from '@/object-metadata/utils/generateCreateOneRecordMutation';
|
||||
import { generateUpdateOneRecordMutation } from '@/object-metadata/utils/generateUpdateOneRecordMutation';
|
||||
import { ColorScheme } from '@/workspace-member/types/WorkspaceMember';
|
||||
import { AvatarType } from 'twenty-ui/display';
|
||||
import { CreateNavigationMenuItemDocument } from '~/generated-metadata/graphql';
|
||||
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
|
||||
import { getMockObjectMetadataItemOrThrow } from '~/testing/utils/getMockObjectMetadataItemOrThrow';
|
||||
|
||||
export const mockId = '8f3b2121-f194-4ba4-9fbf-2d5a37126806';
|
||||
export const favoriteId = 'f088c8c9-05d2-4276-b065-b863cc7d0b33';
|
||||
const favoriteTargetObjectId = 'f2d8b9e9-7932-4065-bc09-baf12388b75d';
|
||||
export const favoriteTargetObjectRecord = {
|
||||
id: favoriteTargetObjectId,
|
||||
__typename: 'Person',
|
||||
};
|
||||
|
||||
export const initialFavorites: Favorite[] = [
|
||||
{
|
||||
__typename: 'Favorite',
|
||||
id: '1',
|
||||
position: 0,
|
||||
key: mockId,
|
||||
labelIdentifier: 'favoriteLabel',
|
||||
avatarUrl: 'example.com',
|
||||
avatarType: 'squared' as AvatarType,
|
||||
link: 'example.com',
|
||||
recordId: '1',
|
||||
person: { id: '1', name: 'John Doe' },
|
||||
company: { id: '2', name: 'ABC Corp' },
|
||||
forWorkspaceMemberId: '1',
|
||||
favoriteFolderId: '1',
|
||||
},
|
||||
{
|
||||
__typename: 'Favorite',
|
||||
id: '2',
|
||||
position: 1,
|
||||
key: mockId,
|
||||
labelIdentifier: 'favoriteLabel',
|
||||
avatarUrl: 'example.com',
|
||||
avatarType: 'squared' as AvatarType,
|
||||
link: 'example.com',
|
||||
recordId: '1',
|
||||
person: { id: '3', name: 'Jane Doe' },
|
||||
company: { id: '4', name: 'Company Test' },
|
||||
forWorkspaceMemberId: '1',
|
||||
favoriteFolderId: '1',
|
||||
},
|
||||
{
|
||||
__typename: 'Favorite',
|
||||
id: '3',
|
||||
position: 2,
|
||||
key: mockId,
|
||||
labelIdentifier: 'favoriteLabel',
|
||||
avatarUrl: 'example.com',
|
||||
company: { id: '5', name: 'Company Test 2' },
|
||||
avatarType: 'squared' as AvatarType,
|
||||
link: 'example.com',
|
||||
recordId: '1',
|
||||
forWorkspaceMemberId: '1',
|
||||
favoriteFolderId: '1',
|
||||
},
|
||||
];
|
||||
|
||||
export const sortedFavorites = [
|
||||
{
|
||||
__typename: 'Favorite',
|
||||
avatarType: 'squared',
|
||||
avatarUrl: undefined,
|
||||
favoriteFolderId: '1',
|
||||
forWorkspaceMemberId: '1',
|
||||
id: '1',
|
||||
labelIdentifier: 'ABC Corp',
|
||||
link: '/object/company/2',
|
||||
objectNameSingular: 'company',
|
||||
position: 0,
|
||||
recordId: '2',
|
||||
},
|
||||
{
|
||||
__typename: 'Favorite',
|
||||
avatarType: 'squared',
|
||||
avatarUrl: undefined,
|
||||
favoriteFolderId: '1',
|
||||
forWorkspaceMemberId: '1',
|
||||
id: '2',
|
||||
labelIdentifier: 'Company Test',
|
||||
link: '/object/company/4',
|
||||
objectNameSingular: 'company',
|
||||
position: 1,
|
||||
recordId: '4',
|
||||
},
|
||||
{
|
||||
__typename: 'Favorite',
|
||||
avatarType: 'squared',
|
||||
avatarUrl: undefined,
|
||||
favoriteFolderId: '1',
|
||||
forWorkspaceMemberId: '1',
|
||||
id: '3',
|
||||
labelIdentifier: 'Company Test 2',
|
||||
link: '/object/company/5',
|
||||
objectNameSingular: 'company',
|
||||
position: 2,
|
||||
recordId: '5',
|
||||
},
|
||||
];
|
||||
|
||||
const favoriteObjectMetadataItem = getMockObjectMetadataItemOrThrow('favorite');
|
||||
const personObjectMetadataItem = getMockObjectMetadataItemOrThrow('person');
|
||||
|
||||
const UPDATE_ONE_FAVORITE_MUTATION = generateUpdateOneRecordMutation({
|
||||
objectMetadataItem: favoriteObjectMetadataItem,
|
||||
objectMetadataItems: generatedMockObjectMetadataItems,
|
||||
computeReferences: false,
|
||||
objectPermissionsByObjectMetadataId: {},
|
||||
});
|
||||
|
||||
const CREATE_ONE_FAVORITE_MUTATION = generateCreateOneRecordMutation({
|
||||
objectMetadataItem: favoriteObjectMetadataItem,
|
||||
objectMetadataItems: generatedMockObjectMetadataItems,
|
||||
objectPermissionsByObjectMetadataId: {},
|
||||
});
|
||||
|
||||
export const mocks = [
|
||||
{
|
||||
request: {
|
||||
query: CREATE_ONE_FAVORITE_MUTATION,
|
||||
variables: {
|
||||
input: {
|
||||
personId: favoriteTargetObjectId,
|
||||
position: 1,
|
||||
forWorkspaceMemberId: '1',
|
||||
favoriteFolderId: undefined,
|
||||
id: mockId,
|
||||
},
|
||||
},
|
||||
},
|
||||
result: jest.fn(() => ({
|
||||
data: {
|
||||
createFavorite: {
|
||||
__typename: 'Favorite',
|
||||
id: favoriteId,
|
||||
position: 1,
|
||||
},
|
||||
},
|
||||
})),
|
||||
},
|
||||
{
|
||||
request: {
|
||||
query: gql`
|
||||
mutation DeleteOneFavorite($idToDelete: UUID!) {
|
||||
deleteFavorite(id: $idToDelete) {
|
||||
__typename
|
||||
deletedAt
|
||||
id
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: { idToDelete: favoriteId },
|
||||
},
|
||||
result: jest.fn(() => ({
|
||||
data: {
|
||||
deleteFavorite: {
|
||||
__typename: 'Favorite',
|
||||
id: favoriteId,
|
||||
deletedAt: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
})),
|
||||
},
|
||||
{
|
||||
request: {
|
||||
query: UPDATE_ONE_FAVORITE_MUTATION,
|
||||
variables: {
|
||||
idToUpdate: '1',
|
||||
input: {
|
||||
position: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
result: jest.fn(() => ({
|
||||
data: {
|
||||
updateFavorite: {
|
||||
__typename: 'Favorite',
|
||||
id: favoriteId,
|
||||
position: 3,
|
||||
},
|
||||
},
|
||||
})),
|
||||
},
|
||||
|
||||
// Mock for folder move
|
||||
{
|
||||
request: {
|
||||
query: UPDATE_ONE_FAVORITE_MUTATION,
|
||||
variables: {
|
||||
idToUpdate: '1',
|
||||
input: {
|
||||
position: 1,
|
||||
favoriteFolderId: '2',
|
||||
},
|
||||
},
|
||||
},
|
||||
result: jest.fn(() => ({
|
||||
data: {
|
||||
updateFavorite: {
|
||||
__typename: 'Favorite',
|
||||
id: favoriteId,
|
||||
position: 1,
|
||||
favoriteFolderId: '2',
|
||||
},
|
||||
},
|
||||
})),
|
||||
},
|
||||
|
||||
// Mock for orphan favorites
|
||||
{
|
||||
request: {
|
||||
query: UPDATE_ONE_FAVORITE_MUTATION,
|
||||
variables: {
|
||||
idToUpdate: '1',
|
||||
input: {
|
||||
position: 1,
|
||||
favoriteFolderId: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
result: jest.fn(() => ({
|
||||
data: {
|
||||
updateFavorite: {
|
||||
__typename: 'Favorite',
|
||||
id: favoriteId,
|
||||
position: 1,
|
||||
favoriteFolderId: null,
|
||||
},
|
||||
},
|
||||
})),
|
||||
},
|
||||
|
||||
{
|
||||
request: {
|
||||
query: CreateNavigationMenuItemDocument,
|
||||
variables: {
|
||||
input: {
|
||||
targetRecordId: favoriteTargetObjectId,
|
||||
targetObjectMetadataId: personObjectMetadataItem.id,
|
||||
userWorkspaceId: '1',
|
||||
folderId: undefined,
|
||||
position: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
result: jest.fn(() => ({
|
||||
data: {
|
||||
createNavigationMenuItem: {
|
||||
__typename: 'NavigationMenuItem',
|
||||
id: mockId,
|
||||
userWorkspaceId: '1',
|
||||
targetRecordId: favoriteTargetObjectId,
|
||||
targetObjectMetadataId: personObjectMetadataItem.id,
|
||||
viewId: null,
|
||||
folderId: null,
|
||||
name: null,
|
||||
link: null,
|
||||
icon: null,
|
||||
position: 1,
|
||||
applicationId: null,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
})),
|
||||
},
|
||||
];
|
||||
|
||||
export const mockWorkspaceMember = {
|
||||
id: '1',
|
||||
name: {
|
||||
firstName: 'First',
|
||||
lastName: 'Last',
|
||||
},
|
||||
avatarUrl: '',
|
||||
locale: 'en-US',
|
||||
colorScheme: 'Dark' as ColorScheme,
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
userId: '1',
|
||||
userEmail: 'userEmail',
|
||||
};
|
||||
-71
@@ -1,71 +0,0 @@
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { useCreateFavorite } from '@/favorites/hooks/useCreateFavorite';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { CoreObjectNameSingular } from 'twenty-shared/types';
|
||||
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
|
||||
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
|
||||
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
|
||||
import {
|
||||
favoriteTargetObjectRecord,
|
||||
initialFavorites,
|
||||
mockId,
|
||||
mockWorkspaceMember,
|
||||
mocks,
|
||||
} from '@/favorites/hooks/__mocks__/useFavorites';
|
||||
|
||||
jest.mock('uuid', () => ({
|
||||
v4: () => mockId,
|
||||
}));
|
||||
|
||||
jest.mock('@/object-record/hooks/useFindManyRecords', () => ({
|
||||
useFindManyRecords: () => ({ records: initialFavorites }),
|
||||
}));
|
||||
|
||||
jest.mock(
|
||||
'@/navigation-menu-item/hooks/usePrefetchedNavigationMenuItemsData',
|
||||
() => ({
|
||||
usePrefetchedNavigationMenuItemsData: () => ({
|
||||
navigationMenuItems: [],
|
||||
workspaceNavigationMenuItems: [],
|
||||
currentWorkspaceMemberId: '1',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
jest.mock('@/favorites/hooks/usePrefetchedFavoritesFoldersData', () => ({
|
||||
usePrefetchedFavoritesFoldersData: () => ({
|
||||
favoriteFolders: [],
|
||||
}),
|
||||
}));
|
||||
|
||||
const Wrapper = getJestMetadataAndApolloMocksWrapper({
|
||||
apolloMocks: mocks,
|
||||
});
|
||||
|
||||
describe('useCreateFavorite', () => {
|
||||
beforeEach(() => {
|
||||
jotaiStore.set(
|
||||
objectMetadataItemsState.atom,
|
||||
generatedMockObjectMetadataItems,
|
||||
);
|
||||
});
|
||||
|
||||
it('should create favorite successfully', async () => {
|
||||
jotaiStore.set(currentWorkspaceMemberState.atom, mockWorkspaceMember);
|
||||
|
||||
const { result } = renderHook(() => useCreateFavorite(), {
|
||||
wrapper: Wrapper,
|
||||
});
|
||||
|
||||
result.current.createFavorite(
|
||||
favoriteTargetObjectRecord,
|
||||
CoreObjectNameSingular.Person,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mocks[0].result).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
-45
@@ -1,45 +0,0 @@
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
|
||||
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
|
||||
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
|
||||
import {
|
||||
favoriteId,
|
||||
initialFavorites,
|
||||
mockWorkspaceMember,
|
||||
mocks,
|
||||
} from '@/favorites/hooks/__mocks__/useFavorites';
|
||||
|
||||
jest.mock('@/object-record/hooks/useFindManyRecords', () => ({
|
||||
useFindManyRecords: () => ({ records: initialFavorites }),
|
||||
}));
|
||||
|
||||
const Wrapper = getJestMetadataAndApolloMocksWrapper({
|
||||
apolloMocks: mocks,
|
||||
});
|
||||
|
||||
describe('useDeleteFavorite', () => {
|
||||
beforeEach(() => {
|
||||
jotaiStore.set(
|
||||
objectMetadataItemsState.atom,
|
||||
generatedMockObjectMetadataItems,
|
||||
);
|
||||
});
|
||||
|
||||
it('should delete favorite successfully', async () => {
|
||||
jotaiStore.set(currentWorkspaceMemberState.atom, mockWorkspaceMember);
|
||||
|
||||
const { result } = renderHook(() => useDeleteFavorite(), {
|
||||
wrapper: Wrapper,
|
||||
});
|
||||
|
||||
result.current.deleteFavorite(favoriteId);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mocks[1].result).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,39 +0,0 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
||||
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { prefetchFavoritesState } from '@/prefetch/states/prefetchFavoritesState';
|
||||
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
|
||||
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
|
||||
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
|
||||
import {
|
||||
initialFavorites,
|
||||
mockWorkspaceMember,
|
||||
sortedFavorites,
|
||||
} from '@/favorites/hooks/__mocks__/useFavorites';
|
||||
|
||||
const Wrapper = getJestMetadataAndApolloMocksWrapper({
|
||||
apolloMocks: [],
|
||||
});
|
||||
|
||||
describe('useFavorites', () => {
|
||||
beforeEach(() => {
|
||||
jotaiStore.set(
|
||||
objectMetadataItemsState.atom,
|
||||
generatedMockObjectMetadataItems,
|
||||
);
|
||||
jotaiStore.set(prefetchFavoritesState.atom, initialFavorites);
|
||||
});
|
||||
|
||||
it('should fetch and sort favorites successfully', () => {
|
||||
jotaiStore.set(currentWorkspaceMemberState.atom, mockWorkspaceMember);
|
||||
|
||||
const { result } = renderHook(() => useFavorites(), {
|
||||
wrapper: Wrapper,
|
||||
});
|
||||
|
||||
expect(result.current.sortedFavorites).toEqual(sortedFavorites);
|
||||
});
|
||||
});
|
||||
-172
@@ -1,172 +0,0 @@
|
||||
import { type DropResult, type ResponderProvided } from '@hello-pangea/dnd';
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { act } from 'react';
|
||||
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { FAVORITE_DROPPABLE_IDS } from '@/favorites/constants/FavoriteDroppableIds';
|
||||
import { useHandleFavoriteDragAndDrop } from '@/favorites/hooks/useHandleFavoriteDragAndDrop';
|
||||
import { type Favorite } from '@/favorites/types/Favorite';
|
||||
import { createFolderDroppableId } from '@/favorites/utils/createFolderDroppableId';
|
||||
import { createFolderHeaderDroppableId } from '@/favorites/utils/createFolderHeaderDroppableId';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { prefetchFavoritesState } from '@/prefetch/states/prefetchFavoritesState';
|
||||
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
|
||||
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
|
||||
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
|
||||
import {
|
||||
initialFavorites,
|
||||
mockWorkspaceMember,
|
||||
mocks,
|
||||
} from '@/favorites/hooks/__mocks__/useFavorites';
|
||||
|
||||
jest.mock('@/object-record/hooks/useFindManyRecords', () => ({
|
||||
useFindManyRecords: () => ({ records: initialFavorites }),
|
||||
}));
|
||||
|
||||
const Wrapper = getJestMetadataAndApolloMocksWrapper({
|
||||
apolloMocks: mocks,
|
||||
});
|
||||
|
||||
describe('useHandleFavoriteDragAndDrop', () => {
|
||||
const mockResponderProvided: ResponderProvided = {
|
||||
announce: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jotaiStore.set(
|
||||
objectMetadataItemsState.atom,
|
||||
generatedMockObjectMetadataItems,
|
||||
);
|
||||
jotaiStore.set(prefetchFavoritesState.atom, initialFavorites as Favorite[]);
|
||||
});
|
||||
|
||||
const setupHook = () => {
|
||||
jotaiStore.set(currentWorkspaceMemberState.atom, mockWorkspaceMember);
|
||||
|
||||
return renderHook(() => useHandleFavoriteDragAndDrop(), {
|
||||
wrapper: Wrapper,
|
||||
});
|
||||
};
|
||||
|
||||
it('should not update when destination is null', () => {
|
||||
const { result } = setupHook();
|
||||
|
||||
act(() => {
|
||||
result.current.handleFavoriteDragAndDrop(
|
||||
{
|
||||
source: { index: 0, droppableId: createFolderDroppableId('1') },
|
||||
destination: null,
|
||||
draggableId: '1',
|
||||
} as DropResult,
|
||||
mockResponderProvided,
|
||||
);
|
||||
});
|
||||
|
||||
expect(mocks[2].result).not.toHaveBeenCalled();
|
||||
expect(mocks[3].result).not.toHaveBeenCalled();
|
||||
expect(mocks[4].result).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not update when destination is same as source', () => {
|
||||
const { result } = setupHook();
|
||||
const folderOneId = createFolderDroppableId('1');
|
||||
|
||||
act(() => {
|
||||
result.current.handleFavoriteDragAndDrop(
|
||||
{
|
||||
source: { index: 0, droppableId: folderOneId },
|
||||
destination: { index: 0, droppableId: folderOneId },
|
||||
draggableId: '1',
|
||||
} as DropResult,
|
||||
mockResponderProvided,
|
||||
);
|
||||
});
|
||||
|
||||
expect(mocks[2].result).not.toHaveBeenCalled();
|
||||
expect(mocks[3].result).not.toHaveBeenCalled();
|
||||
expect(mocks[4].result).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should reorder within same folder', async () => {
|
||||
const { result } = setupHook();
|
||||
const folderOneId = createFolderDroppableId('1');
|
||||
|
||||
act(() => {
|
||||
result.current.handleFavoriteDragAndDrop(
|
||||
{
|
||||
source: { index: 0, droppableId: folderOneId },
|
||||
destination: { index: 2, droppableId: folderOneId },
|
||||
draggableId: '1',
|
||||
} as DropResult,
|
||||
mockResponderProvided,
|
||||
);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mocks[2].result).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should move to another folder', async () => {
|
||||
const { result } = setupHook();
|
||||
|
||||
act(() => {
|
||||
result.current.handleFavoriteDragAndDrop(
|
||||
{
|
||||
source: { index: 0, droppableId: createFolderDroppableId('1') },
|
||||
destination: { index: 0, droppableId: createFolderDroppableId('2') },
|
||||
draggableId: '1',
|
||||
} as DropResult,
|
||||
mockResponderProvided,
|
||||
);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mocks[3].result).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should move to orphan favorites', async () => {
|
||||
const { result } = setupHook();
|
||||
|
||||
act(() => {
|
||||
result.current.handleFavoriteDragAndDrop(
|
||||
{
|
||||
source: { index: 0, droppableId: createFolderDroppableId('1') },
|
||||
destination: {
|
||||
index: 0,
|
||||
droppableId: FAVORITE_DROPPABLE_IDS.ORPHAN_FAVORITES,
|
||||
},
|
||||
draggableId: '1',
|
||||
} as DropResult,
|
||||
mockResponderProvided,
|
||||
);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mocks[4].result).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle dropping into folder header', async () => {
|
||||
const { result } = setupHook();
|
||||
|
||||
act(() => {
|
||||
result.current.handleFavoriteDragAndDrop(
|
||||
{
|
||||
source: { index: 0, droppableId: createFolderDroppableId('1') },
|
||||
destination: {
|
||||
index: 0,
|
||||
droppableId: createFolderHeaderDroppableId('2'),
|
||||
},
|
||||
draggableId: '1',
|
||||
} as DropResult,
|
||||
mockResponderProvided,
|
||||
);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mocks[3].result).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,124 +0,0 @@
|
||||
import { usePrefetchedNavigationMenuItemsData } from '@/navigation-menu-item/hooks/usePrefetchedNavigationMenuItemsData';
|
||||
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/utils/isNavigationMenuItemFolder';
|
||||
import { CoreObjectNameSingular } from 'twenty-shared/types';
|
||||
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useMutation } from '@apollo/client/react';
|
||||
import { CreateNavigationMenuItemDocument } from '~/generated-metadata/graphql';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { usePrefetchedFavoritesData } from './usePrefetchedFavoritesData';
|
||||
import { usePrefetchedFavoritesFoldersData } from './usePrefetchedFavoritesFoldersData';
|
||||
|
||||
export const useCreateFavorite = () => {
|
||||
const { favorites, currentWorkspaceMemberId } = usePrefetchedFavoritesData();
|
||||
const { favoriteFolders } = usePrefetchedFavoritesFoldersData();
|
||||
const { navigationMenuItems } = usePrefetchedNavigationMenuItemsData();
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
|
||||
|
||||
const { createOneRecord: createOneFavorite } = useCreateOneRecord({
|
||||
objectNameSingular: CoreObjectNameSingular.Favorite,
|
||||
});
|
||||
|
||||
const [createNavigationMenuItemMutation] = useMutation(
|
||||
CreateNavigationMenuItemDocument,
|
||||
{
|
||||
refetchQueries: ['FindManyNavigationMenuItems'],
|
||||
},
|
||||
);
|
||||
|
||||
const createFavorite = async (
|
||||
targetRecord: ObjectRecord,
|
||||
targetObjectNameSingular: string,
|
||||
favoriteFolderId?: string,
|
||||
) => {
|
||||
const relevantFavorites = favoriteFolderId
|
||||
? favorites.filter((fav) => fav.favoriteFolderId === favoriteFolderId)
|
||||
: favorites.filter(
|
||||
(fav) =>
|
||||
!isDefined(fav.favoriteFolderId) &&
|
||||
isDefined(fav.forWorkspaceMemberId),
|
||||
);
|
||||
|
||||
const maxPosition = Math.max(
|
||||
...relevantFavorites.map((fav) => fav.position),
|
||||
0,
|
||||
);
|
||||
|
||||
await createOneFavorite({
|
||||
[`${targetObjectNameSingular}Id`]: targetRecord.id,
|
||||
position: maxPosition + 1,
|
||||
forWorkspaceMemberId: currentWorkspaceMemberId,
|
||||
favoriteFolderId,
|
||||
});
|
||||
|
||||
let navigationMenuItemFolderId: string | undefined;
|
||||
|
||||
if (isDefined(favoriteFolderId)) {
|
||||
const favoriteFolder = favoriteFolders.find(
|
||||
(folder) => folder.id === favoriteFolderId,
|
||||
);
|
||||
|
||||
if (isDefined(favoriteFolder)) {
|
||||
const matchingNavigationMenuItemFolder = navigationMenuItems.find(
|
||||
(item) =>
|
||||
isNavigationMenuItemFolder(item) &&
|
||||
item.name === favoriteFolder.name &&
|
||||
item.userWorkspaceId === currentWorkspaceMemberId,
|
||||
);
|
||||
|
||||
navigationMenuItemFolderId = matchingNavigationMenuItemFolder?.id;
|
||||
}
|
||||
}
|
||||
|
||||
const relevantNavigationMenuItems = navigationMenuItemFolderId
|
||||
? navigationMenuItems.filter(
|
||||
(item) => item.folderId === navigationMenuItemFolderId,
|
||||
)
|
||||
: navigationMenuItems.filter(
|
||||
(item) =>
|
||||
!item.folderId && item.userWorkspaceId === currentWorkspaceMemberId,
|
||||
);
|
||||
|
||||
const maxNavigationMenuItemPosition = Math.max(
|
||||
...relevantNavigationMenuItems.map((item) => item.position),
|
||||
0,
|
||||
);
|
||||
|
||||
const isView = targetObjectNameSingular === 'view';
|
||||
|
||||
if (isView) {
|
||||
await createNavigationMenuItemMutation({
|
||||
variables: {
|
||||
input: {
|
||||
viewId: targetRecord.id,
|
||||
userWorkspaceId: currentWorkspaceMemberId,
|
||||
folderId: navigationMenuItemFolderId,
|
||||
position: maxNavigationMenuItemPosition + 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
} else {
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(item) => item.nameSingular === targetObjectNameSingular,
|
||||
);
|
||||
|
||||
if (isDefined(objectMetadataItem)) {
|
||||
await createNavigationMenuItemMutation({
|
||||
variables: {
|
||||
input: {
|
||||
targetRecordId: targetRecord.id,
|
||||
targetObjectMetadataId: objectMetadataItem.id,
|
||||
userWorkspaceId: currentWorkspaceMemberId,
|
||||
folderId: navigationMenuItemFolderId,
|
||||
position: maxNavigationMenuItemPosition + 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return { createFavorite };
|
||||
};
|
||||
@@ -1,71 +0,0 @@
|
||||
import { usePrefetchedNavigationMenuItemsData } from '@/navigation-menu-item/hooks/usePrefetchedNavigationMenuItemsData';
|
||||
import { CoreObjectNameSingular } from 'twenty-shared/types';
|
||||
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useMutation } from '@apollo/client/react';
|
||||
import { CreateNavigationMenuItemDocument } from '~/generated-metadata/graphql';
|
||||
import { usePrefetchedFavoritesData } from './usePrefetchedFavoritesData';
|
||||
import { usePrefetchedFavoritesFoldersData } from './usePrefetchedFavoritesFoldersData';
|
||||
|
||||
export const useCreateFavoriteFolder = () => {
|
||||
const { createOneRecord: createFavoriteFolder } = useCreateOneRecord({
|
||||
objectNameSingular: CoreObjectNameSingular.FavoriteFolder,
|
||||
});
|
||||
|
||||
const { currentWorkspaceMemberId } = usePrefetchedFavoritesData();
|
||||
const { favoriteFolders } = usePrefetchedFavoritesFoldersData();
|
||||
const { navigationMenuItems } = usePrefetchedNavigationMenuItemsData();
|
||||
|
||||
const [createNavigationMenuItemMutation] = useMutation(
|
||||
CreateNavigationMenuItemDocument,
|
||||
{
|
||||
refetchQueries: ['FindManyNavigationMenuItems'],
|
||||
},
|
||||
);
|
||||
|
||||
const createNewFavoriteFolder = async (name: string): Promise<void> => {
|
||||
if (!name || !currentWorkspaceMemberId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const maxPosition = Math.max(
|
||||
...favoriteFolders.map((folder) => folder.position),
|
||||
0,
|
||||
);
|
||||
|
||||
await createFavoriteFolder({
|
||||
name,
|
||||
position: maxPosition + 1,
|
||||
});
|
||||
|
||||
const folderNavigationMenuItems = navigationMenuItems.filter(
|
||||
(item) =>
|
||||
isDefined(item.name) &&
|
||||
!item.folderId &&
|
||||
!item.targetRecordId &&
|
||||
!item.targetObjectMetadataId &&
|
||||
!item.viewId &&
|
||||
item.userWorkspaceId === currentWorkspaceMemberId,
|
||||
);
|
||||
|
||||
const maxNavigationMenuItemPosition = Math.max(
|
||||
...folderNavigationMenuItems.map((item) => item.position),
|
||||
0,
|
||||
);
|
||||
|
||||
await createNavigationMenuItemMutation({
|
||||
variables: {
|
||||
input: {
|
||||
name,
|
||||
targetRecordId: null,
|
||||
targetObjectMetadataId: null,
|
||||
userWorkspaceId: currentWorkspaceMemberId,
|
||||
folderId: null,
|
||||
position: maxNavigationMenuItemPosition + 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return { createNewFavoriteFolder };
|
||||
};
|
||||
@@ -1,14 +0,0 @@
|
||||
import { CoreObjectNameSingular } from 'twenty-shared/types';
|
||||
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
|
||||
|
||||
export const useDeleteFavorite = () => {
|
||||
const { deleteOneRecord } = useDeleteOneRecord({
|
||||
objectNameSingular: CoreObjectNameSingular.Favorite,
|
||||
});
|
||||
|
||||
const deleteFavorite = (favoriteId: string) => {
|
||||
deleteOneRecord(favoriteId);
|
||||
};
|
||||
|
||||
return { deleteFavorite };
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
import { CoreObjectNameSingular } from 'twenty-shared/types';
|
||||
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
|
||||
|
||||
export const useDeleteFavoriteFolder = () => {
|
||||
const { deleteOneRecord } = useDeleteOneRecord({
|
||||
objectNameSingular: CoreObjectNameSingular.FavoriteFolder,
|
||||
});
|
||||
|
||||
const deleteFavoriteFolder = async (folderId: string): Promise<void> => {
|
||||
await deleteOneRecord(folderId);
|
||||
};
|
||||
|
||||
return {
|
||||
deleteFavoriteFolder,
|
||||
};
|
||||
};
|
||||
@@ -1,60 +0,0 @@
|
||||
import { favoriteViewsWithMinimalDataSelector } from '@/favorites/states/selectors/favoriteViewsWithMinimalDataSelector';
|
||||
import { sortFavorites } from '@/favorites/utils/sortFavorites';
|
||||
import { useGetObjectRecordIdentifierByNameSingular } from '@/object-metadata/hooks/useGetObjectRecordIdentifierByNameSingular';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { CoreObjectNameSingular } from 'twenty-shared/types';
|
||||
import { useMemo } from 'react';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { usePrefetchedFavoritesData } from './usePrefetchedFavoritesData';
|
||||
import { allowRequestsToTwentyIconsState } from '@/client-config/states/allowRequestsToTwentyIcons';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
|
||||
export const useFavorites = () => {
|
||||
const { favorites } = usePrefetchedFavoritesData();
|
||||
const favoriteViewsWithMinimalData = useAtomStateValue(
|
||||
favoriteViewsWithMinimalDataSelector,
|
||||
);
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
|
||||
const { objectMetadataItem: favoriteObjectMetadataItem } =
|
||||
useObjectMetadataItem({
|
||||
objectNameSingular: CoreObjectNameSingular.Favorite,
|
||||
});
|
||||
const allowRequestsToTwentyIcons = useAtomStateValue(
|
||||
allowRequestsToTwentyIconsState,
|
||||
);
|
||||
const getObjectRecordIdentifierByNameSingular =
|
||||
useGetObjectRecordIdentifierByNameSingular(allowRequestsToTwentyIcons);
|
||||
|
||||
const favoriteRelationFieldMetadataItems = useMemo(
|
||||
() =>
|
||||
favoriteObjectMetadataItem.readableFields.filter(
|
||||
(fieldMetadataItem) =>
|
||||
fieldMetadataItem.type === FieldMetadataType.RELATION &&
|
||||
fieldMetadataItem.name !== 'forWorkspaceMember' &&
|
||||
fieldMetadataItem.name !== 'favoriteFolder',
|
||||
),
|
||||
[favoriteObjectMetadataItem.readableFields],
|
||||
);
|
||||
|
||||
const sortedFavorites = useMemo(
|
||||
() =>
|
||||
sortFavorites(
|
||||
favorites,
|
||||
favoriteRelationFieldMetadataItems,
|
||||
getObjectRecordIdentifierByNameSingular,
|
||||
true,
|
||||
favoriteViewsWithMinimalData,
|
||||
objectMetadataItems,
|
||||
),
|
||||
[
|
||||
favorites,
|
||||
favoriteRelationFieldMetadataItems,
|
||||
getObjectRecordIdentifierByNameSingular,
|
||||
favoriteViewsWithMinimalData,
|
||||
objectMetadataItems,
|
||||
],
|
||||
);
|
||||
|
||||
return { sortedFavorites };
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
import { sortFavorites } from '@/favorites/utils/sortFavorites';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { coreViewsState } from '@/views/states/coreViewState';
|
||||
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
|
||||
import { useFavoritesMetadata } from './useFavoritesMetadata';
|
||||
import { usePrefetchedFavoritesData } from './usePrefetchedFavoritesData';
|
||||
import { usePrefetchedFavoritesFoldersData } from './usePrefetchedFavoritesFoldersData';
|
||||
|
||||
export const useFavoritesByFolder = () => {
|
||||
const { favorites } = usePrefetchedFavoritesData();
|
||||
const { favoriteFolders } = usePrefetchedFavoritesFoldersData();
|
||||
const {
|
||||
objectMetadataItems,
|
||||
getObjectRecordIdentifierByNameSingular,
|
||||
favoriteRelationFields,
|
||||
} = useFavoritesMetadata();
|
||||
|
||||
const coreViews = useAtomStateValue(coreViewsState);
|
||||
|
||||
const views = coreViews.map(convertCoreViewToView);
|
||||
|
||||
const favoritesByFolder = favoriteFolders.map((folder) => ({
|
||||
folderId: folder.id,
|
||||
folderName: folder.name,
|
||||
favorites: sortFavorites(
|
||||
favorites.filter((favorite) => favorite.favoriteFolderId === folder.id),
|
||||
favoriteRelationFields,
|
||||
getObjectRecordIdentifierByNameSingular,
|
||||
true,
|
||||
views,
|
||||
objectMetadataItems,
|
||||
),
|
||||
}));
|
||||
|
||||
return { favoritesByFolder };
|
||||
};
|
||||
@@ -1,34 +0,0 @@
|
||||
import { useGetObjectRecordIdentifierByNameSingular } from '@/object-metadata/hooks/useGetObjectRecordIdentifierByNameSingular';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { CoreObjectNameSingular } from 'twenty-shared/types';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { allowRequestsToTwentyIconsState } from '@/client-config/states/allowRequestsToTwentyIcons';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
|
||||
export const useFavoritesMetadata = () => {
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
|
||||
const allowRequestsToTwentyIcons = useAtomStateValue(
|
||||
allowRequestsToTwentyIconsState,
|
||||
);
|
||||
const getObjectRecordIdentifierByNameSingular =
|
||||
useGetObjectRecordIdentifierByNameSingular(allowRequestsToTwentyIcons);
|
||||
|
||||
const { objectMetadataItem: favoriteObjectMetadataItem } =
|
||||
useObjectMetadataItem({
|
||||
objectNameSingular: CoreObjectNameSingular.Favorite,
|
||||
});
|
||||
|
||||
const favoriteRelationFields = favoriteObjectMetadataItem.fields.filter(
|
||||
(fieldMetadataItem) =>
|
||||
fieldMetadataItem.type === FieldMetadataType.RELATION &&
|
||||
fieldMetadataItem.name !== 'forWorkspaceMember' &&
|
||||
fieldMetadataItem.name !== 'favoriteFolder',
|
||||
);
|
||||
|
||||
return {
|
||||
objectMetadataItems,
|
||||
getObjectRecordIdentifierByNameSingular,
|
||||
favoriteRelationFields,
|
||||
};
|
||||
};
|
||||
@@ -1,133 +0,0 @@
|
||||
import { FAVORITE_DROPPABLE_IDS } from '@/favorites/constants/FavoriteDroppableIds';
|
||||
import { useSortedFavorites } from '@/favorites/hooks/useSortedFavorites';
|
||||
import { openFavoriteFolderIdsState } from '@/favorites/states/openFavoriteFolderIdsState';
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
import { calculateNewPosition } from '@/ui/layout/draggable-list/utils/calculateNewPosition';
|
||||
import { validateAndExtractFolderId } from '@/ui/layout/draggable-list/utils/validateAndExtractFolderId';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import { type OnDragEndResponder } from '@hello-pangea/dnd';
|
||||
import { CoreObjectNameSingular } from 'twenty-shared/types';
|
||||
import { usePrefetchedFavoritesData } from './usePrefetchedFavoritesData';
|
||||
|
||||
export const useHandleFavoriteDragAndDrop = () => {
|
||||
const { favorites } = usePrefetchedFavoritesData();
|
||||
const { favoritesSorted } = useSortedFavorites();
|
||||
const { updateOneRecord } = useUpdateOneRecord();
|
||||
const setOpenFavoriteFolderIds = useSetAtomState(openFavoriteFolderIdsState);
|
||||
|
||||
const openDestinationFolder = (folderId: string | null) => {
|
||||
if (!folderId) return;
|
||||
|
||||
setOpenFavoriteFolderIds((current) => {
|
||||
if (!current.includes(folderId)) {
|
||||
return [...current, folderId];
|
||||
}
|
||||
return current;
|
||||
});
|
||||
};
|
||||
|
||||
const handleFavoriteDragAndDrop: OnDragEndResponder = (result) => {
|
||||
const { destination, source, draggableId } = result;
|
||||
|
||||
if (!destination) return;
|
||||
|
||||
if (
|
||||
destination.droppableId === source.droppableId &&
|
||||
destination.index === source.index
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const draggedFavorite = favorites.find((f) => f.id === draggableId);
|
||||
if (!draggedFavorite) return;
|
||||
|
||||
const destinationFolderId = validateAndExtractFolderId({
|
||||
droppableId: destination.droppableId,
|
||||
orphanDroppableId: FAVORITE_DROPPABLE_IDS.ORPHAN_FAVORITES,
|
||||
});
|
||||
const sourceFolderId = validateAndExtractFolderId({
|
||||
droppableId: source.droppableId,
|
||||
orphanDroppableId: FAVORITE_DROPPABLE_IDS.ORPHAN_FAVORITES,
|
||||
});
|
||||
|
||||
if (
|
||||
destination.droppableId.startsWith(
|
||||
FAVORITE_DROPPABLE_IDS.FOLDER_HEADER_PREFIX,
|
||||
)
|
||||
) {
|
||||
if (destinationFolderId === null)
|
||||
throw new Error('Invalid folder header ID');
|
||||
|
||||
const folderFavorites = favoritesSorted.filter(
|
||||
(favorite) => favorite.favoriteFolderId === destinationFolderId,
|
||||
);
|
||||
|
||||
const newPosition =
|
||||
folderFavorites.length === 0
|
||||
? 1
|
||||
: folderFavorites[folderFavorites.length - 1].position + 1;
|
||||
|
||||
updateOneRecord({
|
||||
objectNameSingular: CoreObjectNameSingular.Favorite,
|
||||
idToUpdate: draggableId,
|
||||
updateOneRecordInput: {
|
||||
favoriteFolderId: destinationFolderId,
|
||||
position: newPosition,
|
||||
},
|
||||
});
|
||||
|
||||
openDestinationFolder(destinationFolderId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (destination.droppableId !== source.droppableId) {
|
||||
const destinationFavorites = favoritesSorted.filter(
|
||||
(favorite) => favorite.favoriteFolderId === destinationFolderId,
|
||||
);
|
||||
|
||||
let newPosition;
|
||||
if (destinationFavorites.length === 0) {
|
||||
newPosition = 1;
|
||||
} else if (destination.index === 0) {
|
||||
newPosition = destinationFavorites[0].position - 1;
|
||||
} else if (destination.index >= destinationFavorites.length) {
|
||||
newPosition =
|
||||
destinationFavorites[destinationFavorites.length - 1].position + 1;
|
||||
} else {
|
||||
newPosition = calculateNewPosition({
|
||||
destinationIndex: destination.index,
|
||||
sourceIndex: -1,
|
||||
items: destinationFavorites,
|
||||
});
|
||||
}
|
||||
|
||||
updateOneRecord({
|
||||
objectNameSingular: CoreObjectNameSingular.Favorite,
|
||||
idToUpdate: draggableId,
|
||||
updateOneRecordInput: {
|
||||
favoriteFolderId: destinationFolderId,
|
||||
position: newPosition,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const favoritesInSameList = favoritesSorted
|
||||
.filter((favorite) => favorite.favoriteFolderId === sourceFolderId)
|
||||
.filter((favorite) => favorite.id !== draggableId);
|
||||
|
||||
const newPosition = calculateNewPosition({
|
||||
destinationIndex: destination.index,
|
||||
sourceIndex: source.index,
|
||||
items: favoritesInSameList,
|
||||
});
|
||||
|
||||
updateOneRecord({
|
||||
objectNameSingular: CoreObjectNameSingular.Favorite,
|
||||
idToUpdate: draggableId,
|
||||
updateOneRecordInput: { position: newPosition },
|
||||
});
|
||||
};
|
||||
|
||||
return { handleFavoriteDragAndDrop };
|
||||
};
|
||||
@@ -1,30 +0,0 @@
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { type Favorite } from '@/favorites/types/Favorite';
|
||||
import { prefetchFavoritesState } from '@/prefetch/states/prefetchFavoritesState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
|
||||
type PrefetchedFavoritesData = {
|
||||
favorites: Favorite[];
|
||||
workspaceFavorites: Favorite[];
|
||||
currentWorkspaceMemberId: string | undefined;
|
||||
};
|
||||
|
||||
export const usePrefetchedFavoritesData = (): PrefetchedFavoritesData => {
|
||||
const currentWorkspaceMember = useAtomStateValue(currentWorkspaceMemberState);
|
||||
const currentWorkspaceMemberId = currentWorkspaceMember?.id;
|
||||
const prefetchFavorites = useAtomStateValue(prefetchFavoritesState);
|
||||
|
||||
const favorites = prefetchFavorites.filter(
|
||||
(favorite) => favorite.forWorkspaceMemberId === currentWorkspaceMemberId,
|
||||
);
|
||||
|
||||
const workspaceFavorites = prefetchFavorites.filter(
|
||||
(favorite) => favorite.forWorkspaceMemberId === null,
|
||||
);
|
||||
|
||||
return {
|
||||
favorites,
|
||||
workspaceFavorites,
|
||||
currentWorkspaceMemberId,
|
||||
};
|
||||
};
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { type FavoriteFolder } from '@/favorites/types/FavoriteFolder';
|
||||
import { prefetchFavoriteFoldersState } from '@/prefetch/states/prefetchFavoriteFoldersState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
|
||||
type PrefetchedFavoritesFoldersData = {
|
||||
favoriteFolders: FavoriteFolder[];
|
||||
currentWorkspaceMemberId: string | undefined;
|
||||
};
|
||||
|
||||
export const usePrefetchedFavoritesFoldersData =
|
||||
(): PrefetchedFavoritesFoldersData => {
|
||||
const currentWorkspaceMember = useAtomStateValue(
|
||||
currentWorkspaceMemberState,
|
||||
);
|
||||
const currentWorkspaceMemberId = currentWorkspaceMember?.id;
|
||||
|
||||
const prefetchFavoriteFolders = useAtomStateValue(
|
||||
prefetchFavoriteFoldersState,
|
||||
);
|
||||
|
||||
return {
|
||||
favoriteFolders: prefetchFavoriteFolders,
|
||||
currentWorkspaceMemberId,
|
||||
};
|
||||
};
|
||||
@@ -1,27 +0,0 @@
|
||||
import { CoreObjectNameSingular } from 'twenty-shared/types';
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
|
||||
export const useRenameFavoriteFolder = () => {
|
||||
const { updateOneRecord } = useUpdateOneRecord();
|
||||
|
||||
const renameFavoriteFolder = async (
|
||||
folderId: string,
|
||||
newName: string,
|
||||
): Promise<void> => {
|
||||
if (!newName) {
|
||||
return;
|
||||
}
|
||||
|
||||
await updateOneRecord({
|
||||
objectNameSingular: CoreObjectNameSingular.FavoriteFolder,
|
||||
idToUpdate: folderId,
|
||||
updateOneRecordInput: {
|
||||
name: newName,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
renameFavoriteFolder,
|
||||
};
|
||||
};
|
||||
@@ -1,59 +0,0 @@
|
||||
import { sortFavorites } from '@/favorites/utils/sortFavorites';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { coreViewsState } from '@/views/states/coreViewState';
|
||||
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
|
||||
import { useMemo } from 'react';
|
||||
import { useFavoritesMetadata } from './useFavoritesMetadata';
|
||||
import { usePrefetchedFavoritesData } from './usePrefetchedFavoritesData';
|
||||
|
||||
export const useSortedFavorites = () => {
|
||||
const { favorites, workspaceFavorites } = usePrefetchedFavoritesData();
|
||||
const {
|
||||
objectMetadataItems,
|
||||
getObjectRecordIdentifierByNameSingular,
|
||||
favoriteRelationFields,
|
||||
} = useFavoritesMetadata();
|
||||
|
||||
const coreViews = useAtomStateValue(coreViewsState).map(
|
||||
convertCoreViewToView,
|
||||
);
|
||||
|
||||
const favoritesSorted = useMemo(() => {
|
||||
return sortFavorites(
|
||||
favorites,
|
||||
favoriteRelationFields,
|
||||
getObjectRecordIdentifierByNameSingular,
|
||||
true,
|
||||
coreViews,
|
||||
objectMetadataItems,
|
||||
);
|
||||
}, [
|
||||
coreViews,
|
||||
favoriteRelationFields,
|
||||
favorites,
|
||||
getObjectRecordIdentifierByNameSingular,
|
||||
objectMetadataItems,
|
||||
]);
|
||||
|
||||
const workspaceFavoritesSorted = useMemo(() => {
|
||||
return sortFavorites(
|
||||
workspaceFavorites.filter((favorite) => favorite.viewId),
|
||||
favoriteRelationFields,
|
||||
getObjectRecordIdentifierByNameSingular,
|
||||
false,
|
||||
coreViews,
|
||||
objectMetadataItems,
|
||||
);
|
||||
}, [
|
||||
workspaceFavorites,
|
||||
favoriteRelationFields,
|
||||
getObjectRecordIdentifierByNameSingular,
|
||||
coreViews,
|
||||
objectMetadataItems,
|
||||
]);
|
||||
|
||||
return {
|
||||
favoritesSorted,
|
||||
workspaceFavoritesSorted,
|
||||
};
|
||||
};
|
||||
@@ -1,86 +0,0 @@
|
||||
import { allowRequestsToTwentyIconsState } from '@/client-config/states/allowRequestsToTwentyIcons';
|
||||
import { sortFavorites } from '@/favorites/utils/sortFavorites';
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { useGetObjectRecordIdentifierByNameSingular } from '@/object-metadata/hooks/useGetObjectRecordIdentifierByNameSingular';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { CoreObjectNameSingular } from 'twenty-shared/types';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { coreViewsState } from '@/views/states/coreViewState';
|
||||
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
|
||||
import { useMemo } from 'react';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { usePrefetchedFavoritesData } from './usePrefetchedFavoritesData';
|
||||
|
||||
export const useWorkspaceFavorites = () => {
|
||||
const { workspaceFavorites } = usePrefetchedFavoritesData();
|
||||
const coreViews = useAtomStateValue(coreViewsState);
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
|
||||
const { objectMetadataItem: favoriteObjectMetadataItem } =
|
||||
useObjectMetadataItem({
|
||||
objectNameSingular: CoreObjectNameSingular.Favorite,
|
||||
});
|
||||
const allowRequestsToTwentyIcons = useAtomStateValue(
|
||||
allowRequestsToTwentyIconsState,
|
||||
);
|
||||
const getObjectRecordIdentifierByNameSingular =
|
||||
useGetObjectRecordIdentifierByNameSingular(allowRequestsToTwentyIcons);
|
||||
|
||||
const favoriteRelationFieldMetadataItems = useMemo(
|
||||
() =>
|
||||
favoriteObjectMetadataItem.fields.filter(
|
||||
(fieldMetadataItem) =>
|
||||
fieldMetadataItem.type === FieldMetadataType.RELATION &&
|
||||
fieldMetadataItem.name !== 'forWorkspaceMember' &&
|
||||
fieldMetadataItem.name !== 'favoriteFolder',
|
||||
),
|
||||
[favoriteObjectMetadataItem.fields],
|
||||
);
|
||||
|
||||
const views = coreViews.map(convertCoreViewToView);
|
||||
|
||||
const sortedWorkspaceFavorites = useMemo(
|
||||
() =>
|
||||
sortFavorites(
|
||||
workspaceFavorites.filter((favorite) => favorite.viewId),
|
||||
favoriteRelationFieldMetadataItems,
|
||||
getObjectRecordIdentifierByNameSingular,
|
||||
false,
|
||||
views,
|
||||
objectMetadataItems,
|
||||
),
|
||||
[
|
||||
workspaceFavorites,
|
||||
favoriteRelationFieldMetadataItems,
|
||||
getObjectRecordIdentifierByNameSingular,
|
||||
views,
|
||||
objectMetadataItems,
|
||||
],
|
||||
);
|
||||
|
||||
const workspaceFavoriteIds = new Set(
|
||||
sortedWorkspaceFavorites.map((favorite) => favorite.recordId),
|
||||
);
|
||||
|
||||
const favoriteViewObjectMetadataIds = new Set(
|
||||
views.reduce<string[]>((acc, view) => {
|
||||
if (workspaceFavoriteIds.has(view.id)) {
|
||||
acc.push(view.objectMetadataId);
|
||||
}
|
||||
return acc;
|
||||
}, []),
|
||||
);
|
||||
|
||||
const { activeNonSystemObjectMetadataItems } =
|
||||
useFilteredObjectMetadataItems();
|
||||
|
||||
const activeNonSystemObjectMetadataItemsInWorkspaceFavorites =
|
||||
activeNonSystemObjectMetadataItems.filter((item) =>
|
||||
favoriteViewObjectMetadataIds.has(item.id),
|
||||
);
|
||||
|
||||
return {
|
||||
workspaceFavoritesObjectMetadataItems:
|
||||
activeNonSystemObjectMetadataItemsInWorkspaceFavorites,
|
||||
};
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
|
||||
|
||||
export const isFavoriteFolderCreatingState = createAtomState<boolean>({
|
||||
key: 'isFavoriteFolderCreatingState',
|
||||
defaultValue: false,
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
|
||||
|
||||
export const openFavoriteFolderIdsState = createAtomState<string[]>({
|
||||
key: 'openFavoriteFolderIdsState',
|
||||
defaultValue: [],
|
||||
});
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
import { coreViewsState } from '@/views/states/coreViewState';
|
||||
import { type View } from '@/views/types/View';
|
||||
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
|
||||
import { createAtomSelector } from '@/ui/utilities/state/jotai/utils/createAtomSelector';
|
||||
|
||||
export const favoriteViewsWithMinimalDataSelector = createAtomSelector<
|
||||
Pick<View, 'id' | 'name' | 'objectMetadataId' | 'icon'>[]
|
||||
>({
|
||||
key: 'favoriteViewsWithMinimalDataSelector',
|
||||
get: ({ get }) => {
|
||||
const coreViews = get(coreViewsState);
|
||||
const views = coreViews.map(convertCoreViewToView);
|
||||
return views.map((view) => ({
|
||||
id: view.id,
|
||||
name: view.name,
|
||||
objectMetadataId: view.objectMetadataId,
|
||||
icon: view.icon,
|
||||
}));
|
||||
},
|
||||
});
|
||||
@@ -1,14 +0,0 @@
|
||||
import { type AvatarType } from 'twenty-ui/display';
|
||||
export type Favorite = {
|
||||
id: string;
|
||||
position: number;
|
||||
[key: string]: any;
|
||||
labelIdentifier: string;
|
||||
avatarUrl: string;
|
||||
avatarType: AvatarType;
|
||||
link: string;
|
||||
recordId: string;
|
||||
forWorkspaceMemberId: string;
|
||||
favoriteFolderId?: string;
|
||||
__typename: 'Favorite';
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
import { type FAVORITE_DROPPABLE_IDS } from '@/favorites/constants/FavoriteDroppableIds';
|
||||
|
||||
export type FavoriteDroppableId =
|
||||
| typeof FAVORITE_DROPPABLE_IDS.ORPHAN_FAVORITES
|
||||
| `${typeof FAVORITE_DROPPABLE_IDS.FOLDER_PREFIX}${string}`
|
||||
| `${typeof FAVORITE_DROPPABLE_IDS.FOLDER_HEADER_PREFIX}${string}`;
|
||||
@@ -1,9 +0,0 @@
|
||||
export type FavoriteFolder = {
|
||||
id: string;
|
||||
name: string;
|
||||
position: number;
|
||||
createdAt: string;
|
||||
updatedAt?: string;
|
||||
deletedAt?: string | null;
|
||||
__typename: 'FavoriteFolder';
|
||||
};
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
import { FAVORITE_DROPPABLE_IDS } from '@/favorites/constants/FavoriteDroppableIds';
|
||||
import { createFolderDroppableId } from '@/favorites/utils/createFolderDroppableId';
|
||||
|
||||
describe('createFolderDroppableId', () => {
|
||||
it('should create a valid folder droppable id', () => {
|
||||
const folderId = '123-456';
|
||||
const result = createFolderDroppableId(folderId);
|
||||
|
||||
expect(result).toBe(`${FAVORITE_DROPPABLE_IDS.FOLDER_PREFIX}${folderId}`);
|
||||
});
|
||||
|
||||
it('should work with empty string', () => {
|
||||
const result = createFolderDroppableId('');
|
||||
|
||||
expect(result).toBe(FAVORITE_DROPPABLE_IDS.FOLDER_PREFIX);
|
||||
});
|
||||
});
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
import { FAVORITE_DROPPABLE_IDS } from '@/favorites/constants/FavoriteDroppableIds';
|
||||
import { createFolderHeaderDroppableId } from '@/favorites/utils/createFolderHeaderDroppableId';
|
||||
|
||||
describe('createFolderHeaderDroppableId', () => {
|
||||
it('should create a valid folder header droppable id', () => {
|
||||
const folderId = '123-456';
|
||||
const result = createFolderHeaderDroppableId(folderId);
|
||||
|
||||
expect(result).toBe(
|
||||
`${FAVORITE_DROPPABLE_IDS.FOLDER_HEADER_PREFIX}${folderId}`,
|
||||
);
|
||||
});
|
||||
|
||||
it('should work with empty string', () => {
|
||||
const result = createFolderHeaderDroppableId('');
|
||||
|
||||
expect(result).toBe(FAVORITE_DROPPABLE_IDS.FOLDER_HEADER_PREFIX);
|
||||
});
|
||||
});
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
|
||||
import { getFavoriteSecondaryLabel } from '@/favorites/utils/getFavoriteSecondaryLabel';
|
||||
|
||||
describe('getFavoriteSecondaryLabel', () => {
|
||||
it('should return "View" for view object', () => {
|
||||
const result = getFavoriteSecondaryLabel({
|
||||
objectMetadataItems: generatedMockObjectMetadataItems,
|
||||
favoriteObjectNameSingular: 'view',
|
||||
});
|
||||
|
||||
expect(result).toBe('View');
|
||||
});
|
||||
|
||||
it('should return labelSingular for matching object metadata item', () => {
|
||||
const result = getFavoriteSecondaryLabel({
|
||||
objectMetadataItems: generatedMockObjectMetadataItems,
|
||||
favoriteObjectNameSingular: 'person',
|
||||
});
|
||||
|
||||
expect(result).toBe('Person');
|
||||
});
|
||||
|
||||
it('should return undefined when object metadata item is not found', () => {
|
||||
const result = getFavoriteSecondaryLabel({
|
||||
objectMetadataItems: generatedMockObjectMetadataItems,
|
||||
favoriteObjectNameSingular: 'nonexistent',
|
||||
});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
-37
@@ -1,37 +0,0 @@
|
||||
import { getObjectMetadataNamePluralFromViewId } from '@/favorites/utils/getObjectMetadataNamePluralFromViewId';
|
||||
import { type View } from '@/views/types/View';
|
||||
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
|
||||
|
||||
describe('getObjectMetadataNamePluralFromViewId', () => {
|
||||
it('should return namePlural and view for matching objectMetadataId', () => {
|
||||
const view: Pick<View, 'id' | 'name' | 'objectMetadataId'> = {
|
||||
id: 'view-id',
|
||||
name: 'All People',
|
||||
objectMetadataId:
|
||||
generatedMockObjectMetadataItems[0]?.id ?? 'metadata-id',
|
||||
};
|
||||
|
||||
const result = getObjectMetadataNamePluralFromViewId(
|
||||
view,
|
||||
generatedMockObjectMetadataItems,
|
||||
);
|
||||
|
||||
expect(result.namePlural).toBeDefined();
|
||||
expect(result.view).toEqual(view);
|
||||
});
|
||||
|
||||
it('should throw error when objectMetadataItem is not found', () => {
|
||||
const view: Pick<View, 'id' | 'name' | 'objectMetadataId'> = {
|
||||
id: 'view-id',
|
||||
name: 'All People',
|
||||
objectMetadataId: 'non-existent-id',
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
getObjectMetadataNamePluralFromViewId(
|
||||
view,
|
||||
generatedMockObjectMetadataItems,
|
||||
);
|
||||
}).toThrow('Object metadata item not found for id non-existent-id');
|
||||
});
|
||||
});
|
||||
-55
@@ -1,55 +0,0 @@
|
||||
import { isLocationMatchingFavorite } from '@/favorites/utils/isLocationMatchingFavorite';
|
||||
|
||||
describe('isLocationMatchingFavorite', () => {
|
||||
it('should return true if favorite link matches current path', () => {
|
||||
const currentPath = '/app/objects/people';
|
||||
const currentViewPath = '/app/objects/people?viewId=123';
|
||||
const favorite = {
|
||||
objectNameSingular: 'object',
|
||||
link: '/app/objects/people',
|
||||
};
|
||||
|
||||
expect(
|
||||
isLocationMatchingFavorite(currentPath, currentViewPath, favorite),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true if favorite link matches current view path', () => {
|
||||
const currentPath = '/app/object/company/12';
|
||||
const currentViewPath = '/app/object/company/12?viewId=123';
|
||||
const favorite = {
|
||||
objectNameSingular: 'company',
|
||||
link: '/app/object/company/12',
|
||||
};
|
||||
|
||||
expect(
|
||||
isLocationMatchingFavorite(currentPath, currentViewPath, favorite),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if favorite link does not match current path', () => {
|
||||
const currentPath = '/app/objects/people';
|
||||
const currentViewPath = '/app/objects/people?viewId=123';
|
||||
const favorite = {
|
||||
objectNameSingular: 'object',
|
||||
link: '/app/objects/company',
|
||||
};
|
||||
|
||||
expect(
|
||||
isLocationMatchingFavorite(currentPath, currentViewPath, favorite),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false if favorite link does not match current view path', () => {
|
||||
const currentPath = '/app/objects/companies';
|
||||
const currentViewPath = '/app/objects/companies?viewId=123';
|
||||
const favorite = {
|
||||
objectNameSingular: 'view',
|
||||
link: '/app/objects/companies?viewId=246',
|
||||
};
|
||||
|
||||
expect(
|
||||
isLocationMatchingFavorite(currentPath, currentViewPath, favorite),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,233 +0,0 @@
|
||||
import { type Favorite } from '@/favorites/types/Favorite';
|
||||
import { sortFavorites } from '@/favorites/utils/sortFavorites';
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { type ObjectRecordIdentifier } from '@/object-record/types/ObjectRecordIdentifier';
|
||||
import { type View } from '@/views/types/View';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
jest.mock('@/favorites/utils/getObjectMetadataNamePluralFromViewId', () => ({
|
||||
getObjectMetadataNamePluralFromViewId: jest.fn(
|
||||
(
|
||||
view: Pick<View, 'id' | 'name' | 'objectMetadataId'>,
|
||||
items: ObjectMetadataItem[],
|
||||
) => {
|
||||
const item = items.find((item) => item.id === view.objectMetadataId);
|
||||
return { namePlural: item?.namePlural ?? 'items' };
|
||||
},
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('twenty-shared/utils', () => {
|
||||
const actual = jest.requireActual('twenty-shared/utils');
|
||||
return {
|
||||
...actual,
|
||||
getAppPath: jest.fn((path, params, query) => {
|
||||
const basePath = `/app/objects/${params.objectNamePlural}`;
|
||||
const viewId = query?.viewId;
|
||||
if (viewId !== undefined && viewId !== null) {
|
||||
return `${basePath}?viewId=${viewId}`;
|
||||
}
|
||||
return basePath;
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
describe('sortFavorites', () => {
|
||||
const mockView: Pick<View, 'id' | 'name' | 'objectMetadataId' | 'icon'> = {
|
||||
id: 'view-id',
|
||||
name: 'All People',
|
||||
objectMetadataId: 'metadata-id',
|
||||
icon: 'IconUser',
|
||||
};
|
||||
|
||||
const mockObjectMetadataItem: ObjectMetadataItem = {
|
||||
id: 'metadata-id',
|
||||
nameSingular: 'person',
|
||||
namePlural: 'people',
|
||||
} as ObjectMetadataItem;
|
||||
|
||||
const mockObjectRecord: ObjectRecord = {
|
||||
__typename: 'ObjectRecord',
|
||||
id: 'record-id',
|
||||
name: 'John Doe',
|
||||
} as ObjectRecord;
|
||||
|
||||
const mockObjectRecordIdentifier: ObjectRecordIdentifier = {
|
||||
id: 'record-id',
|
||||
name: 'John Doe',
|
||||
avatarUrl: 'https://example.com/avatar.jpg',
|
||||
avatarType: 'rounded',
|
||||
linkToShowPage: '/app/objects/people/record-id',
|
||||
};
|
||||
|
||||
const mockRelationField: FieldMetadataItem = {
|
||||
name: 'person',
|
||||
type: FieldMetadataType.RELATION,
|
||||
relation: {
|
||||
targetObjectMetadata: {
|
||||
nameSingular: 'person',
|
||||
},
|
||||
},
|
||||
} as FieldMetadataItem;
|
||||
|
||||
const getObjectRecordIdentifierByNameSingular = jest.fn(
|
||||
(
|
||||
record: ObjectRecord,
|
||||
objectNameSingular: string,
|
||||
): ObjectRecordIdentifier => {
|
||||
if (objectNameSingular === 'person') {
|
||||
return mockObjectRecordIdentifier;
|
||||
}
|
||||
return {
|
||||
id: record.id,
|
||||
name: 'Unknown',
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should process favorite with viewId', () => {
|
||||
const favorite = {
|
||||
id: 'favorite-id',
|
||||
viewId: 'view-id',
|
||||
position: 1,
|
||||
} as unknown as Favorite;
|
||||
|
||||
const result = sortFavorites(
|
||||
[favorite],
|
||||
[],
|
||||
getObjectRecordIdentifierByNameSingular,
|
||||
true,
|
||||
[mockView],
|
||||
[mockObjectMetadataItem],
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toMatchObject({
|
||||
id: 'favorite-id',
|
||||
objectNameSingular: 'view',
|
||||
Icon: 'IconUser',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle favorite with viewId when view is not found', () => {
|
||||
const favorite = {
|
||||
id: 'favorite-id',
|
||||
viewId: 'non-existent-view-id',
|
||||
position: 1,
|
||||
} as unknown as Favorite;
|
||||
|
||||
const result = sortFavorites(
|
||||
[favorite],
|
||||
[],
|
||||
getObjectRecordIdentifierByNameSingular,
|
||||
true,
|
||||
[],
|
||||
[mockObjectMetadataItem],
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].objectNameSingular).toBe('view');
|
||||
});
|
||||
|
||||
it('should process favorite with relation field', () => {
|
||||
const favorite = {
|
||||
id: 'favorite-id',
|
||||
person: mockObjectRecord,
|
||||
position: 2,
|
||||
} as unknown as Favorite;
|
||||
|
||||
const result = sortFavorites(
|
||||
[favorite],
|
||||
[mockRelationField],
|
||||
getObjectRecordIdentifierByNameSingular,
|
||||
true,
|
||||
[],
|
||||
[],
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toMatchObject({
|
||||
id: 'favorite-id',
|
||||
objectNameSingular: 'person',
|
||||
labelIdentifier: 'John Doe',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return empty link when hasLinkToShowPage is false', () => {
|
||||
const favorite = {
|
||||
id: 'favorite-id',
|
||||
person: mockObjectRecord,
|
||||
position: 2,
|
||||
} as unknown as Favorite;
|
||||
|
||||
const result = sortFavorites(
|
||||
[favorite],
|
||||
[mockRelationField],
|
||||
getObjectRecordIdentifierByNameSingular,
|
||||
false,
|
||||
[],
|
||||
[],
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].link).toBe('');
|
||||
});
|
||||
|
||||
it('should sort favorites by position', () => {
|
||||
const favorites = [
|
||||
{
|
||||
id: 'favorite-3',
|
||||
viewId: 'view-id',
|
||||
position: 3,
|
||||
},
|
||||
{
|
||||
id: 'favorite-1',
|
||||
viewId: 'view-id',
|
||||
position: 1,
|
||||
},
|
||||
{
|
||||
id: 'favorite-2',
|
||||
viewId: 'view-id',
|
||||
position: 2,
|
||||
},
|
||||
] as unknown as Favorite[];
|
||||
|
||||
const result = sortFavorites(
|
||||
favorites,
|
||||
[],
|
||||
getObjectRecordIdentifierByNameSingular,
|
||||
true,
|
||||
[mockView],
|
||||
[mockObjectMetadataItem],
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result[0].id).toBe('favorite-1');
|
||||
expect(result[1].id).toBe('favorite-2');
|
||||
expect(result[2].id).toBe('favorite-3');
|
||||
});
|
||||
|
||||
it('should filter out favorites with no viewId and no relation fields', () => {
|
||||
const favorite = {
|
||||
id: 'favorite-id',
|
||||
position: 1,
|
||||
} as unknown as Favorite;
|
||||
|
||||
const result = sortFavorites(
|
||||
[favorite],
|
||||
[],
|
||||
getObjectRecordIdentifierByNameSingular,
|
||||
true,
|
||||
[],
|
||||
[],
|
||||
);
|
||||
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
-63
@@ -1,63 +0,0 @@
|
||||
import { FAVORITE_DROPPABLE_IDS } from '@/favorites/constants/FavoriteDroppableIds';
|
||||
import { validateAndExtractFolderId } from '@/ui/layout/draggable-list/utils/validateAndExtractFolderId';
|
||||
|
||||
describe('validateAndExtractFolderId', () => {
|
||||
it('should return null for orphan favorites', () => {
|
||||
const result = validateAndExtractFolderId({
|
||||
droppableId: FAVORITE_DROPPABLE_IDS.ORPHAN_FAVORITES,
|
||||
orphanDroppableId: FAVORITE_DROPPABLE_IDS.ORPHAN_FAVORITES,
|
||||
});
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should extract folder id from folder droppable id', () => {
|
||||
const folderId = '123-456';
|
||||
const droppableId = `${FAVORITE_DROPPABLE_IDS.FOLDER_PREFIX}${folderId}`;
|
||||
|
||||
const result = validateAndExtractFolderId({
|
||||
droppableId,
|
||||
orphanDroppableId: FAVORITE_DROPPABLE_IDS.ORPHAN_FAVORITES,
|
||||
});
|
||||
expect(result).toBe(folderId);
|
||||
});
|
||||
|
||||
it('should extract folder id from folder header droppable id', () => {
|
||||
const folderId = '123-456';
|
||||
const droppableId = `${FAVORITE_DROPPABLE_IDS.FOLDER_HEADER_PREFIX}${folderId}`;
|
||||
|
||||
const result = validateAndExtractFolderId({
|
||||
droppableId,
|
||||
orphanDroppableId: FAVORITE_DROPPABLE_IDS.ORPHAN_FAVORITES,
|
||||
});
|
||||
expect(result).toBe(folderId);
|
||||
});
|
||||
|
||||
it('should throw error for invalid droppable id format', () => {
|
||||
expect(() => {
|
||||
validateAndExtractFolderId({
|
||||
droppableId: 'invalid-id',
|
||||
orphanDroppableId: FAVORITE_DROPPABLE_IDS.ORPHAN_FAVORITES,
|
||||
});
|
||||
}).toThrow('Invalid droppable ID format: invalid-id');
|
||||
});
|
||||
|
||||
it('should throw error for empty folder id in folder format', () => {
|
||||
expect(() => {
|
||||
validateAndExtractFolderId({
|
||||
droppableId: FAVORITE_DROPPABLE_IDS.FOLDER_PREFIX,
|
||||
orphanDroppableId: FAVORITE_DROPPABLE_IDS.ORPHAN_FAVORITES,
|
||||
});
|
||||
}).toThrow(`Invalid folder ID: ${FAVORITE_DROPPABLE_IDS.FOLDER_PREFIX}`);
|
||||
});
|
||||
|
||||
it('should throw error for empty folder id in folder header format', () => {
|
||||
expect(() => {
|
||||
validateAndExtractFolderId({
|
||||
droppableId: FAVORITE_DROPPABLE_IDS.FOLDER_HEADER_PREFIX,
|
||||
orphanDroppableId: FAVORITE_DROPPABLE_IDS.ORPHAN_FAVORITES,
|
||||
});
|
||||
}).toThrow(
|
||||
`Invalid folder header ID: ${FAVORITE_DROPPABLE_IDS.FOLDER_HEADER_PREFIX}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
import { FAVORITE_DROPPABLE_IDS } from '@/favorites/constants/FavoriteDroppableIds';
|
||||
import { type FavoriteDroppableId } from '@/favorites/types/FavoriteDroppableId';
|
||||
|
||||
export const createFolderDroppableId = (
|
||||
folderId: string,
|
||||
): FavoriteDroppableId => `${FAVORITE_DROPPABLE_IDS.FOLDER_PREFIX}${folderId}`;
|
||||
@@ -1,7 +0,0 @@
|
||||
import { FAVORITE_DROPPABLE_IDS } from '@/favorites/constants/FavoriteDroppableIds';
|
||||
import { type FavoriteDroppableId } from '@/favorites/types/FavoriteDroppableId';
|
||||
|
||||
export const createFolderHeaderDroppableId = (
|
||||
folderId: string,
|
||||
): FavoriteDroppableId =>
|
||||
`${FAVORITE_DROPPABLE_IDS.FOLDER_HEADER_PREFIX}${folderId}`;
|
||||
@@ -1,23 +0,0 @@
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
|
||||
type GetFavoriteSecondaryLabelProps = {
|
||||
objectMetadataItems: Pick<
|
||||
ObjectMetadataItem,
|
||||
'nameSingular' | 'labelSingular'
|
||||
>[];
|
||||
favoriteObjectNameSingular: string;
|
||||
};
|
||||
|
||||
export const getFavoriteSecondaryLabel = ({
|
||||
objectMetadataItems,
|
||||
favoriteObjectNameSingular,
|
||||
}: GetFavoriteSecondaryLabelProps) => {
|
||||
if (favoriteObjectNameSingular === 'view') {
|
||||
return 'View';
|
||||
}
|
||||
|
||||
return objectMetadataItems.find(
|
||||
(objectMetadataItem) =>
|
||||
objectMetadataItem.nameSingular === favoriteObjectNameSingular,
|
||||
)?.labelSingular;
|
||||
};
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { type View } from '@/views/types/View';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type ReturnType = {
|
||||
namePlural: string;
|
||||
view: Pick<View, 'id' | 'name' | 'objectMetadataId'>;
|
||||
};
|
||||
|
||||
export const getObjectMetadataNamePluralFromViewId = (
|
||||
view: Pick<View, 'id' | 'name' | 'objectMetadataId'>,
|
||||
objectMetadataItems: ObjectMetadataItem[],
|
||||
): ReturnType => {
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(objectMetadataItem) => objectMetadataItem.id === view.objectMetadataId,
|
||||
);
|
||||
|
||||
if (!isDefined(objectMetadataItem)) {
|
||||
throw new Error(
|
||||
`Object metadata item not found for id ${view.objectMetadataId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const { namePlural } = objectMetadataItem;
|
||||
|
||||
return {
|
||||
namePlural,
|
||||
view,
|
||||
};
|
||||
};
|
||||
@@ -1,12 +0,0 @@
|
||||
import { type ProcessedFavorite } from '@/favorites/utils/sortFavorites';
|
||||
|
||||
// Todo: we could only path the fullPath here (which is currentViewPath) and then split it in the function
|
||||
export const isLocationMatchingFavorite = (
|
||||
currentPath: string,
|
||||
currentViewPath: string,
|
||||
favorite: Pick<ProcessedFavorite, 'objectNameSingular' | 'link'>,
|
||||
) => {
|
||||
return favorite.objectNameSingular === 'view'
|
||||
? favorite.link === currentViewPath
|
||||
: favorite.link === currentPath;
|
||||
};
|
||||
@@ -1,98 +0,0 @@
|
||||
import { type Favorite } from '@/favorites/types/Favorite';
|
||||
import { getObjectMetadataNamePluralFromViewId } from '@/favorites/utils/getObjectMetadataNamePluralFromViewId';
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { type ObjectRecordIdentifier } from '@/object-record/types/ObjectRecordIdentifier';
|
||||
import { type View } from '@/views/types/View';
|
||||
import { AppPath } from 'twenty-shared/types';
|
||||
import { getAppPath, isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export type ProcessedFavorite = Favorite & {
|
||||
Icon?: string;
|
||||
objectNameSingular: string;
|
||||
};
|
||||
|
||||
export const sortFavorites = (
|
||||
favorites: Favorite[],
|
||||
favoriteRelationFieldMetadataItems: FieldMetadataItem[],
|
||||
getObjectRecordIdentifierByNameSingular: (
|
||||
record: ObjectRecord,
|
||||
objectNameSingular: string,
|
||||
) => ObjectRecordIdentifier,
|
||||
hasLinkToShowPage: boolean,
|
||||
views: Pick<View, 'id' | 'name' | 'objectMetadataId' | 'icon'>[],
|
||||
objectMetadataItems: ObjectMetadataItem[],
|
||||
): ProcessedFavorite[] => {
|
||||
return favorites
|
||||
.map((favorite) => {
|
||||
if (isDefined(favorite.viewId)) {
|
||||
const view = views.find((view) => view.id === favorite.viewId);
|
||||
|
||||
if (!isDefined(view)) {
|
||||
return {
|
||||
...favorite,
|
||||
objectNameSingular: 'view',
|
||||
};
|
||||
}
|
||||
|
||||
const { namePlural } = getObjectMetadataNamePluralFromViewId(
|
||||
view,
|
||||
objectMetadataItems,
|
||||
);
|
||||
|
||||
return {
|
||||
__typename: 'Favorite',
|
||||
id: favorite.id,
|
||||
recordId: view?.id,
|
||||
position: favorite.position,
|
||||
avatarType: 'icon',
|
||||
avatarUrl: '',
|
||||
labelIdentifier: view?.name,
|
||||
link: getAppPath(
|
||||
AppPath.RecordIndexPage,
|
||||
{ objectNamePlural: namePlural },
|
||||
favorite.viewId ? { viewId: favorite.viewId } : undefined,
|
||||
),
|
||||
forWorkspaceMemberId: favorite.forWorkspaceMemberId,
|
||||
favoriteFolderId: favorite.favoriteFolderId,
|
||||
objectNameSingular: 'view',
|
||||
Icon: view?.icon,
|
||||
} as ProcessedFavorite;
|
||||
}
|
||||
|
||||
for (const relationField of favoriteRelationFieldMetadataItems) {
|
||||
if (isDefined(favorite[relationField.name])) {
|
||||
const relationObject = favorite[relationField.name];
|
||||
|
||||
const objectNameSingular =
|
||||
relationField.relation?.targetObjectMetadata.nameSingular ?? '';
|
||||
|
||||
const objectRecordIdentifier =
|
||||
getObjectRecordIdentifierByNameSingular(
|
||||
relationObject,
|
||||
objectNameSingular,
|
||||
);
|
||||
|
||||
return {
|
||||
__typename: 'Favorite',
|
||||
id: favorite.id,
|
||||
recordId: objectRecordIdentifier.id,
|
||||
position: favorite.position,
|
||||
avatarType: objectRecordIdentifier.avatarType,
|
||||
avatarUrl: objectRecordIdentifier.avatarUrl,
|
||||
labelIdentifier: objectRecordIdentifier.name,
|
||||
link: hasLinkToShowPage
|
||||
? objectRecordIdentifier.linkToShowPage
|
||||
: '',
|
||||
forWorkspaceMemberId: favorite.forWorkspaceMemberId,
|
||||
favoriteFolderId: favorite.favoriteFolderId,
|
||||
objectNameSingular: objectNameSingular,
|
||||
} as ProcessedFavorite;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(isDefined)
|
||||
.sort((a, b) => a.position - b.position);
|
||||
};
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
import { useListenToMetadataOperationBrowserEvent } from '@/browser-event/hooks/useListenToMetadataOperationBrowserEvent';
|
||||
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
|
||||
import { prefetchNavigationMenuItemsState } from '@/prefetch/states/prefetchNavigationMenuItemsState';
|
||||
import { navigationMenuItemsState } from '@/navigation-menu-item/states/navigationMenuItemsState';
|
||||
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
|
||||
import { useStore } from 'jotai';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
@@ -39,7 +39,7 @@ export const NavigationMenuItemSSEEffect = () => {
|
||||
}
|
||||
|
||||
const existingNavigationMenuItems = store.get(
|
||||
prefetchNavigationMenuItemsState.atom,
|
||||
navigationMenuItemsState.atom,
|
||||
);
|
||||
|
||||
if (
|
||||
@@ -49,7 +49,7 @@ export const NavigationMenuItemSSEEffect = () => {
|
||||
)
|
||||
) {
|
||||
store.set(
|
||||
prefetchNavigationMenuItemsState.atom,
|
||||
navigationMenuItemsState.atom,
|
||||
result.data.navigationMenuItems,
|
||||
);
|
||||
|
||||
|
||||
+3
-3
@@ -2,7 +2,7 @@ import { useListenToMetadataOperationBrowserEvent } from '@/browser-event/hooks/
|
||||
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
|
||||
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { prefetchNavigationMenuItemsState } from '@/prefetch/states/prefetchNavigationMenuItemsState';
|
||||
import { navigationMenuItemsState } from '@/navigation-menu-item/states/navigationMenuItemsState';
|
||||
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
|
||||
import { useStore } from 'jotai';
|
||||
import { useApolloClient } from '@apollo/client/react';
|
||||
@@ -44,7 +44,7 @@ export const ObjectMetadataItemSSEEffect = () => {
|
||||
});
|
||||
|
||||
const existingNavigationMenuItems = store.get(
|
||||
prefetchNavigationMenuItemsState.atom,
|
||||
navigationMenuItemsState.atom,
|
||||
);
|
||||
|
||||
if (
|
||||
@@ -54,7 +54,7 @@ export const ObjectMetadataItemSSEEffect = () => {
|
||||
)
|
||||
) {
|
||||
store.set(
|
||||
prefetchNavigationMenuItemsState.atom,
|
||||
navigationMenuItemsState.atom,
|
||||
navigationMenuItemsResult.data?.navigationMenuItems ?? [],
|
||||
);
|
||||
}
|
||||
|
||||
-12
@@ -1,27 +1,21 @@
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconFolderPlus } from 'twenty-ui/display';
|
||||
import { LightIconButton } from 'twenty-ui/input';
|
||||
import { AnimatedExpandableContainer } from 'twenty-ui/layout';
|
||||
import { CurrentWorkspaceMemberOrphanNavigationMenuItems } from '@/navigation-menu-item/components/CurrentWorkspaceMemberOrphanNavigationMenuItems';
|
||||
import { NavigationMenuItemFolders } from '@/navigation-menu-item/components/NavigationMenuItemFolders';
|
||||
import { NavigationMenuItemSkeletonLoader } from '@/navigation-menu-item/components/NavigationMenuItemSkeletonLoader';
|
||||
import { useNavigationMenuItemsByFolder } from '@/navigation-menu-item/hooks/useNavigationMenuItemsByFolder';
|
||||
import { useSortedNavigationMenuItems } from '@/navigation-menu-item/hooks/useSortedNavigationMenuItems';
|
||||
import { isNavigationMenuItemFolderCreatingState } from '@/navigation-menu-item/states/isNavigationMenuItemFolderCreatingState';
|
||||
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
||||
import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading';
|
||||
import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper';
|
||||
import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection';
|
||||
import { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle';
|
||||
import { useNavigationSection } from '@/ui/navigation/navigation-drawer/hooks/useNavigationSection';
|
||||
import { isNavigationSectionOpenFamilyState } from '@/ui/navigation/navigation-drawer/states/isNavigationSectionOpenFamilyState';
|
||||
import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
|
||||
export const CurrentWorkspaceMemberNavigationMenuItemFolders = () => {
|
||||
const currentWorkspaceMember = useAtomStateValue(currentWorkspaceMemberState);
|
||||
const { navigationMenuItemsSorted } = useSortedNavigationMenuItems();
|
||||
const { userNavigationMenuItemsByFolder } = useNavigationMenuItemsByFolder();
|
||||
|
||||
@@ -30,8 +24,6 @@ export const CurrentWorkspaceMemberNavigationMenuItemFolders = () => {
|
||||
setIsNavigationMenuItemFolderCreating,
|
||||
] = useAtomState(isNavigationMenuItemFolderCreatingState);
|
||||
|
||||
const loading = useIsPrefetchLoading();
|
||||
|
||||
const { t } = useLingui();
|
||||
|
||||
const { toggleNavigationSection, openNavigationSection } =
|
||||
@@ -46,10 +38,6 @@ export const CurrentWorkspaceMemberNavigationMenuItemFolders = () => {
|
||||
setIsNavigationMenuItemFolderCreating((current) => !current);
|
||||
};
|
||||
|
||||
if (loading && isDefined(currentWorkspaceMember)) {
|
||||
return <NavigationMenuItemSkeletonLoader />;
|
||||
}
|
||||
|
||||
if (
|
||||
navigationMenuItemsSorted.length === 0 &&
|
||||
!isNavigationMenuItemFolderCreating &&
|
||||
|
||||
+3
-16
@@ -1,32 +1,19 @@
|
||||
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||
|
||||
import { CurrentWorkspaceMemberFavoritesFolders } from '@/favorites/components/CurrentWorkspaceMemberFavoritesFolders';
|
||||
import { CurrentWorkspaceMemberNavigationMenuItemFolders } from '@/navigation-menu-item/components/CurrentWorkspaceMemberNavigationMenuItemFolders';
|
||||
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
|
||||
import { FavoritesDragDropProviderContent } from '@/navigation/components/FavoritesDragDropProviderContent';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { AnimatedEaseInOut } from 'twenty-ui/utilities';
|
||||
|
||||
export const CurrentWorkspaceMemberNavigationMenuItemFoldersDispatcher = () => {
|
||||
const isNavigationMenuItemEditingEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED,
|
||||
);
|
||||
const isNavigationMenuInEditMode = useAtomStateValue(
|
||||
isNavigationMenuInEditModeState,
|
||||
);
|
||||
|
||||
return (
|
||||
<AnimatedEaseInOut isOpen={!isNavigationMenuInEditMode} initial>
|
||||
{isNavigationMenuItemEditingEnabled ? (
|
||||
<FavoritesDragDropProviderContent>
|
||||
<CurrentWorkspaceMemberNavigationMenuItemFolders />
|
||||
</FavoritesDragDropProviderContent>
|
||||
) : (
|
||||
<FavoritesDragDropProviderContent>
|
||||
<CurrentWorkspaceMemberFavoritesFolders />
|
||||
</FavoritesDragDropProviderContent>
|
||||
)}
|
||||
<FavoritesDragDropProviderContent>
|
||||
<CurrentWorkspaceMemberNavigationMenuItemFolders />
|
||||
</FavoritesDragDropProviderContent>
|
||||
</AnimatedEaseInOut>
|
||||
);
|
||||
};
|
||||
|
||||
+1
-7
@@ -9,7 +9,6 @@ import { sidePanelPageState } from '@/side-panel/states/sidePanelPageState';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
@@ -17,7 +16,6 @@ import { useContext, useState } from 'react';
|
||||
import { SidePanelPages } from 'twenty-shared/types';
|
||||
import { IconCheck, useIcons } from 'twenty-ui/display';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: center;
|
||||
@@ -71,12 +69,8 @@ export const NavigationMenuEditModeBar = () => {
|
||||
const isNavigationMenuInEditMode = useAtomStateValue(
|
||||
isNavigationMenuInEditModeState,
|
||||
);
|
||||
const isNavigationMenuItemEditingEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED,
|
||||
);
|
||||
|
||||
const showNavigationMenuEditModeBar =
|
||||
isNavigationMenuItemEditingEnabled && isNavigationMenuInEditMode;
|
||||
const showNavigationMenuEditModeBar = isNavigationMenuInEditMode;
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!isDirty) return;
|
||||
|
||||
+1
-21
@@ -1,30 +1,20 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||
|
||||
import { FavoritesFolderContent } from '@/favorites/components/FavoritesFolderContent';
|
||||
import { type ProcessedFavorite } from '@/favorites/utils/sortFavorites';
|
||||
import { NavigationMenuItemFolderContent } from '@/navigation-menu-item/components/NavigationMenuItemFolderContent';
|
||||
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
|
||||
type NavigationMenuItemFolderContentDispatcherEffectProps = {
|
||||
folderName: string;
|
||||
folderId: string;
|
||||
favorites?: ProcessedFavorite[];
|
||||
navigationMenuItems?: ProcessedNavigationMenuItem[];
|
||||
};
|
||||
|
||||
export const NavigationMenuItemFolderContentDispatcherEffect = ({
|
||||
folderName,
|
||||
folderId,
|
||||
favorites,
|
||||
navigationMenuItems,
|
||||
}: NavigationMenuItemFolderContentDispatcherEffectProps) => {
|
||||
const isNavigationMenuItemEditingEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED,
|
||||
);
|
||||
|
||||
if (isNavigationMenuItemEditingEnabled && isDefined(navigationMenuItems)) {
|
||||
if (isDefined(navigationMenuItems)) {
|
||||
return (
|
||||
<NavigationMenuItemFolderContent
|
||||
folderId={folderId}
|
||||
@@ -34,15 +24,5 @@ export const NavigationMenuItemFolderContentDispatcherEffect = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (isDefined(favorites)) {
|
||||
return (
|
||||
<FavoritesFolderContent
|
||||
folderName={folderName}
|
||||
folderId={folderId}
|
||||
favorites={favorites}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
+1
-9
@@ -2,7 +2,6 @@ import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Avatar, IconLink, IconWorld, useIcons } from 'twenty-ui/display';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||
|
||||
import { LinkIconWithLinkOverlay } from '@/navigation-menu-item/components/LinkIconWithLinkOverlay';
|
||||
import { StyledNavigationMenuItemIconContainer } from '@/navigation-menu-item/components/NavigationMenuItemIconContainer';
|
||||
@@ -16,7 +15,6 @@ import { useGetStandardObjectIcon } from '@/object-metadata/hooks/useGetStandard
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { ViewKey } from '@/views/types/ViewKey';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
|
||||
export const NavigationMenuItemIcon = ({
|
||||
navigationMenuItem,
|
||||
@@ -24,9 +22,6 @@ export const NavigationMenuItemIcon = ({
|
||||
navigationMenuItem: ProcessedNavigationMenuItem;
|
||||
}) => {
|
||||
const { getIcon } = useIcons();
|
||||
const isNavigationMenuItemEditingEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED,
|
||||
);
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
|
||||
const { Icon: StandardIcon, IconColor } = useGetStandardObjectIcon(
|
||||
navigationMenuItem.objectNameSingular ?? '',
|
||||
@@ -80,10 +75,7 @@ export const NavigationMenuItemIcon = ({
|
||||
(navigationMenuItem.Icon ? getIcon(navigationMenuItem.Icon) : undefined);
|
||||
const effectiveColor =
|
||||
getEffectiveNavigationMenuItemColor(navigationMenuItem);
|
||||
const useStyledIcon =
|
||||
isNavigationMenuItemEditingEnabled &&
|
||||
!isRecord &&
|
||||
isNonEmptyString(effectiveColor);
|
||||
const useStyledIcon = !isRecord && isNonEmptyString(effectiveColor);
|
||||
const iconStyle = useStyledIcon
|
||||
? getNavigationMenuItemIconStyleFromColor(effectiveColor)
|
||||
: null;
|
||||
|
||||
+19
-39
@@ -11,7 +11,6 @@ import {
|
||||
} from 'twenty-ui/display';
|
||||
import { LightIconButton } from 'twenty-ui/input';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||
|
||||
import { FOLDER_ICON_DEFAULT } from '@/navigation-menu-item/constants/FolderIconDefault';
|
||||
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
|
||||
@@ -27,16 +26,13 @@ import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/sta
|
||||
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
|
||||
import { filterWorkspaceNavigationMenuItems } from '@/navigation-menu-item/utils/filterWorkspaceNavigationMenuItems';
|
||||
import { preloadWorkspaceDndKit } from '@/navigation/preloadWorkspaceDndKit';
|
||||
import { NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader';
|
||||
import { NavigationDrawerSectionForWorkspaceItems } from '@/object-metadata/components/NavigationDrawerSectionForWorkspaceItems';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading';
|
||||
import { prefetchNavigationMenuItemsState } from '@/prefetch/states/prefetchNavigationMenuItemsState';
|
||||
import { navigationMenuItemsState } from '@/navigation-menu-item/states/navigationMenuItemsState';
|
||||
import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel';
|
||||
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { useStore } from 'jotai';
|
||||
import { SidePanelPages } from 'twenty-shared/types';
|
||||
|
||||
@@ -51,18 +47,13 @@ export const WorkspaceNavigationMenuItems = () => {
|
||||
const { workspaceNavigationMenuItemsSorted } = useSortedNavigationMenuItems();
|
||||
const store = useStore();
|
||||
const enterEditMode = () => {
|
||||
const prefetchNavigationMenuItems = store.get(
|
||||
prefetchNavigationMenuItemsState.atom,
|
||||
);
|
||||
const currentNavigationMenuItems = store.get(navigationMenuItemsState.atom);
|
||||
const workspaceNavigationMenuItems = filterWorkspaceNavigationMenuItems(
|
||||
prefetchNavigationMenuItems,
|
||||
currentNavigationMenuItems,
|
||||
);
|
||||
store.set(navigationMenuItemsDraftState.atom, workspaceNavigationMenuItems);
|
||||
store.set(isNavigationMenuInEditModeState.atom, true);
|
||||
};
|
||||
const isNavigationMenuItemEditingEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED,
|
||||
);
|
||||
const isNavigationMenuInEditMode = useAtomStateValue(
|
||||
isNavigationMenuInEditModeState,
|
||||
);
|
||||
@@ -79,7 +70,6 @@ export const WorkspaceNavigationMenuItems = () => {
|
||||
useOpenNavigationMenuItemInSidePanel();
|
||||
const { getIcon } = useIcons();
|
||||
|
||||
const loading = useIsPrefetchLoading();
|
||||
const { t } = useLingui();
|
||||
|
||||
const handleEditClick = (event: React.MouseEvent) => {
|
||||
@@ -171,46 +161,36 @@ export const WorkspaceNavigationMenuItems = () => {
|
||||
});
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<NavigationDrawerSectionForWorkspaceItems
|
||||
sectionTitle={t`Workspace`}
|
||||
items={items}
|
||||
rightIcon={
|
||||
isNavigationMenuItemEditingEnabled ? (
|
||||
<StyledRightIconsContainer>
|
||||
{isNavigationMenuInEditMode ? (
|
||||
<StyledRightIconsContainer>
|
||||
{isNavigationMenuInEditMode ? (
|
||||
<LightIconButton
|
||||
Icon={IconPlus}
|
||||
accent="tertiary"
|
||||
size="small"
|
||||
onClick={handleAddMenuItem}
|
||||
/>
|
||||
) : (
|
||||
<div onMouseEnter={preloadWorkspaceDndKit}>
|
||||
<LightIconButton
|
||||
Icon={IconPlus}
|
||||
Icon={IconTool}
|
||||
accent="tertiary"
|
||||
size="small"
|
||||
onClick={handleAddMenuItem}
|
||||
onClick={handleEditClick}
|
||||
/>
|
||||
) : (
|
||||
<div onMouseEnter={preloadWorkspaceDndKit}>
|
||||
<LightIconButton
|
||||
Icon={IconTool}
|
||||
accent="tertiary"
|
||||
size="small"
|
||||
onClick={handleEditClick}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</StyledRightIconsContainer>
|
||||
) : undefined
|
||||
</div>
|
||||
)}
|
||||
</StyledRightIconsContainer>
|
||||
}
|
||||
selectedNavigationMenuItemId={selectedNavigationMenuItemInEditMode}
|
||||
onNavigationMenuItemClick={
|
||||
isNavigationMenuInEditMode ? handleNavigationMenuItemClick : undefined
|
||||
}
|
||||
onActiveObjectMetadataItemClick={
|
||||
isNavigationMenuItemEditingEnabled
|
||||
? handleActiveObjectMetadataItemClick
|
||||
: undefined
|
||||
}
|
||||
onActiveObjectMetadataItemClick={handleActiveObjectMetadataItemClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
+1
-13
@@ -1,17 +1,5 @@
|
||||
import { FeatureFlagKey } from '~/generated-metadata/graphql';
|
||||
|
||||
import { WorkspaceFavorites } from '@/favorites/components/WorkspaceFavorites';
|
||||
import { WorkspaceNavigationMenuItems } from '@/navigation-menu-item/components/WorkspaceNavigationMenuItems';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
|
||||
export const WorkspaceNavigationMenuItemsDispatcher = () => {
|
||||
const isNavigationMenuItemEditingEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_NAVIGATION_MENU_ITEM_EDITING_ENABLED,
|
||||
);
|
||||
|
||||
if (isNavigationMenuItemEditingEnabled) {
|
||||
return <WorkspaceNavigationMenuItems />;
|
||||
}
|
||||
|
||||
return <WorkspaceFavorites />;
|
||||
return <WorkspaceNavigationMenuItems />;
|
||||
};
|
||||
|
||||
+2
-2
@@ -2,14 +2,14 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import { useMutation } from '@apollo/client/react';
|
||||
import { CreateNavigationMenuItemDocument } from '~/generated-metadata/graphql';
|
||||
|
||||
import { usePrefetchedNavigationMenuItemsData } from '@/navigation-menu-item/hooks/usePrefetchedNavigationMenuItemsData';
|
||||
import { useNavigationMenuItemsData } from '@/navigation-menu-item/hooks/useNavigationMenuItemsData';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
|
||||
export const useCreateNavigationMenuItem = () => {
|
||||
const { navigationMenuItems, currentWorkspaceMemberId } =
|
||||
usePrefetchedNavigationMenuItemsData();
|
||||
useNavigationMenuItemsData();
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
|
||||
|
||||
const [createNavigationMenuItemMutation] = useMutation(
|
||||
|
||||
+2
-2
@@ -2,11 +2,11 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import { useMutation } from '@apollo/client/react';
|
||||
import { CreateNavigationMenuItemDocument } from '~/generated-metadata/graphql';
|
||||
|
||||
import { usePrefetchedNavigationMenuItemsData } from '@/navigation-menu-item/hooks/usePrefetchedNavigationMenuItemsData';
|
||||
import { useNavigationMenuItemsData } from '@/navigation-menu-item/hooks/useNavigationMenuItemsData';
|
||||
|
||||
export const useCreateNavigationMenuItemFolder = () => {
|
||||
const { navigationMenuItems, currentWorkspaceMemberId } =
|
||||
usePrefetchedNavigationMenuItemsData();
|
||||
useNavigationMenuItemsData();
|
||||
|
||||
const [createNavigationMenuItemMutation] = useMutation(
|
||||
CreateNavigationMenuItemDocument,
|
||||
|
||||
+2
-2
@@ -10,10 +10,10 @@ import { calculateNewPosition } from '@/ui/layout/draggable-list/utils/calculate
|
||||
import { FOLDER_DROPPABLE_IDS } from '@/ui/layout/draggable-list/utils/folderDroppableIds';
|
||||
import { validateAndExtractFolderId } from '@/ui/layout/draggable-list/utils/validateAndExtractFolderId';
|
||||
|
||||
import { usePrefetchedNavigationMenuItemsData } from './usePrefetchedNavigationMenuItemsData';
|
||||
import { useNavigationMenuItemsData } from './useNavigationMenuItemsData';
|
||||
|
||||
export const useHandleNavigationMenuItemDragAndDrop = () => {
|
||||
const { navigationMenuItems } = usePrefetchedNavigationMenuItemsData();
|
||||
const { navigationMenuItems } = useNavigationMenuItemsData();
|
||||
const { navigationMenuItemsSorted } = useSortedNavigationMenuItems();
|
||||
const { updateNavigationMenuItem } = useUpdateNavigationMenuItem();
|
||||
const setOpenNavigationMenuItemFolderIds = useSetAtomState(
|
||||
|
||||
+2
-3
@@ -15,12 +15,11 @@ import {
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { usePrefetchedNavigationMenuItemsData } from './usePrefetchedNavigationMenuItemsData';
|
||||
import { useNavigationMenuItemsData } from './useNavigationMenuItemsData';
|
||||
|
||||
export const useHandleWorkspaceNavigationMenuItemDragAndDrop = () => {
|
||||
const store = useStore();
|
||||
const { workspaceNavigationMenuItems } =
|
||||
usePrefetchedNavigationMenuItemsData();
|
||||
const { workspaceNavigationMenuItems } = useNavigationMenuItemsData();
|
||||
const setNavigationMenuItemsDraft = useSetAtomState(
|
||||
navigationMenuItemsDraftState,
|
||||
);
|
||||
|
||||
+2
-2
@@ -10,7 +10,7 @@ import { coreViewsState } from '@/views/states/coreViewState';
|
||||
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
|
||||
import { usePrefetchedNavigationMenuItemsData } from './usePrefetchedNavigationMenuItemsData';
|
||||
import { useNavigationMenuItemsData } from './useNavigationMenuItemsData';
|
||||
|
||||
type NavigationMenuItemFolder = {
|
||||
id: string;
|
||||
@@ -30,7 +30,7 @@ export const useNavigationMenuItemsByFolder = () => {
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
|
||||
|
||||
const { navigationMenuItems, workspaceNavigationMenuItems } =
|
||||
usePrefetchedNavigationMenuItemsData();
|
||||
useNavigationMenuItemsData();
|
||||
|
||||
const views = coreViews.map(convertCoreViewToView);
|
||||
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
|
||||
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/navigationMenuItemsDraftState';
|
||||
import { filterWorkspaceNavigationMenuItems } from '@/navigation-menu-item/utils/filterWorkspaceNavigationMenuItems';
|
||||
import { navigationMenuItemsState } from '@/navigation-menu-item/states/navigationMenuItemsState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
type NavigationMenuItemsData = {
|
||||
navigationMenuItems: NavigationMenuItem[];
|
||||
workspaceNavigationMenuItems: NavigationMenuItem[];
|
||||
currentWorkspaceMemberId: string | undefined;
|
||||
};
|
||||
|
||||
export const useNavigationMenuItemsData = (): NavigationMenuItemsData => {
|
||||
const currentWorkspaceMember = useAtomStateValue(currentWorkspaceMemberState);
|
||||
const currentWorkspaceMemberId = currentWorkspaceMember?.id;
|
||||
const navigationMenuItems = useAtomStateValue(navigationMenuItemsState);
|
||||
const isNavigationMenuInEditMode = useAtomStateValue(
|
||||
isNavigationMenuInEditModeState,
|
||||
);
|
||||
const navigationMenuItemsDraft = useAtomStateValue(
|
||||
navigationMenuItemsDraftState,
|
||||
);
|
||||
|
||||
const userNavigationMenuItems = navigationMenuItems.filter((item) =>
|
||||
isDefined(item.userWorkspaceId),
|
||||
);
|
||||
|
||||
const workspaceNavigationMenuItemsFromState =
|
||||
filterWorkspaceNavigationMenuItems(navigationMenuItems);
|
||||
|
||||
const workspaceNavigationMenuItems =
|
||||
isNavigationMenuInEditMode && isDefined(navigationMenuItemsDraft)
|
||||
? navigationMenuItemsDraft
|
||||
: workspaceNavigationMenuItemsFromState;
|
||||
|
||||
return {
|
||||
navigationMenuItems: userNavigationMenuItems,
|
||||
workspaceNavigationMenuItems,
|
||||
currentWorkspaceMemberId,
|
||||
};
|
||||
};
|
||||
+6
-8
@@ -4,34 +4,32 @@ import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
|
||||
import { filterWorkspaceNavigationMenuItems } from '@/navigation-menu-item/utils/filterWorkspaceNavigationMenuItems';
|
||||
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/navigationMenuItemsDraftState';
|
||||
import { prefetchNavigationMenuItemsState } from '@/prefetch/states/prefetchNavigationMenuItemsState';
|
||||
import { navigationMenuItemsState } from '@/navigation-menu-item/states/navigationMenuItemsState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
|
||||
export const useNavigationMenuItemsDraftState = () => {
|
||||
const isNavigationMenuInEditMode = useAtomStateValue(
|
||||
isNavigationMenuInEditModeState,
|
||||
);
|
||||
const prefetchNavigationMenuItems = useAtomStateValue(
|
||||
prefetchNavigationMenuItemsState,
|
||||
);
|
||||
const navigationMenuItems = useAtomStateValue(navigationMenuItemsState);
|
||||
const navigationMenuItemsDraft = useAtomStateValue(
|
||||
navigationMenuItemsDraftState,
|
||||
);
|
||||
|
||||
const workspaceNavigationMenuItemsFromPrefetch =
|
||||
filterWorkspaceNavigationMenuItems(prefetchNavigationMenuItems);
|
||||
const workspaceNavigationMenuItemsFromState =
|
||||
filterWorkspaceNavigationMenuItems(navigationMenuItems);
|
||||
|
||||
const workspaceNavigationMenuItems =
|
||||
isNavigationMenuInEditMode && isDefined(navigationMenuItemsDraft)
|
||||
? navigationMenuItemsDraft
|
||||
: workspaceNavigationMenuItemsFromPrefetch;
|
||||
: workspaceNavigationMenuItemsFromState;
|
||||
|
||||
const isDirty =
|
||||
isNavigationMenuInEditMode &&
|
||||
isDefined(navigationMenuItemsDraft) &&
|
||||
!isDeeplyEqual(
|
||||
navigationMenuItemsDraft,
|
||||
workspaceNavigationMenuItemsFromPrefetch,
|
||||
workspaceNavigationMenuItemsFromState,
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
-50
@@ -1,50 +0,0 @@
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
|
||||
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/navigationMenuItemsDraftState';
|
||||
import { filterWorkspaceNavigationMenuItems } from '@/navigation-menu-item/utils/filterWorkspaceNavigationMenuItems';
|
||||
import { prefetchNavigationMenuItemsState } from '@/prefetch/states/prefetchNavigationMenuItemsState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
type PrefetchedNavigationMenuItemsData = {
|
||||
navigationMenuItems: NavigationMenuItem[];
|
||||
workspaceNavigationMenuItems: NavigationMenuItem[];
|
||||
currentWorkspaceMemberId: string | undefined;
|
||||
};
|
||||
|
||||
export const usePrefetchedNavigationMenuItemsData =
|
||||
(): PrefetchedNavigationMenuItemsData => {
|
||||
const currentWorkspaceMember = useAtomStateValue(
|
||||
currentWorkspaceMemberState,
|
||||
);
|
||||
const currentWorkspaceMemberId = currentWorkspaceMember?.id;
|
||||
const prefetchNavigationMenuItems = useAtomStateValue(
|
||||
prefetchNavigationMenuItemsState,
|
||||
);
|
||||
const isNavigationMenuInEditMode = useAtomStateValue(
|
||||
isNavigationMenuInEditModeState,
|
||||
);
|
||||
const navigationMenuItemsDraft = useAtomStateValue(
|
||||
navigationMenuItemsDraftState,
|
||||
);
|
||||
|
||||
const navigationMenuItems = prefetchNavigationMenuItems.filter((item) =>
|
||||
isDefined(item.userWorkspaceId),
|
||||
);
|
||||
|
||||
const workspaceNavigationMenuItemsFromPrefetch =
|
||||
filterWorkspaceNavigationMenuItems(prefetchNavigationMenuItems);
|
||||
|
||||
const workspaceNavigationMenuItems =
|
||||
isNavigationMenuInEditMode && isDefined(navigationMenuItemsDraft)
|
||||
? navigationMenuItemsDraft
|
||||
: workspaceNavigationMenuItemsFromPrefetch;
|
||||
|
||||
return {
|
||||
navigationMenuItems,
|
||||
workspaceNavigationMenuItems,
|
||||
currentWorkspaceMemberId,
|
||||
};
|
||||
};
|
||||
+5
-7
@@ -2,7 +2,7 @@ import { useCallback } from 'react';
|
||||
|
||||
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
|
||||
import { FindManyNavigationMenuItemsDocument } from '~/generated-metadata/graphql';
|
||||
import { prefetchNavigationMenuItemsState } from '@/prefetch/states/prefetchNavigationMenuItemsState';
|
||||
import { navigationMenuItemsState } from '@/navigation-menu-item/states/navigationMenuItemsState';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useStore } from 'jotai';
|
||||
@@ -12,15 +12,13 @@ export const useRemoveNavigationMenuItemByTargetRecordId = () => {
|
||||
const apolloCoreClient = useApolloCoreClient();
|
||||
const cache = apolloCoreClient.cache;
|
||||
|
||||
const setPrefetchNavigationMenuItems = useSetAtomState(
|
||||
prefetchNavigationMenuItemsState,
|
||||
);
|
||||
const setNavigationMenuItems = useSetAtomState(navigationMenuItemsState);
|
||||
|
||||
const removeNavigationMenuItemsByTargetRecordIds = useCallback(
|
||||
(targetRecordIds: string[]) => {
|
||||
const targetRecordIdsSet = new Set(targetRecordIds);
|
||||
const currentNavigationMenuItems = store.get(
|
||||
prefetchNavigationMenuItemsState.atom,
|
||||
navigationMenuItemsState.atom,
|
||||
);
|
||||
|
||||
const updatedNavigationMenuItems = currentNavigationMenuItems.filter(
|
||||
@@ -29,7 +27,7 @@ export const useRemoveNavigationMenuItemByTargetRecordId = () => {
|
||||
!targetRecordIdsSet.has(item.targetRecordId),
|
||||
);
|
||||
|
||||
setPrefetchNavigationMenuItems(updatedNavigationMenuItems);
|
||||
setNavigationMenuItems(updatedNavigationMenuItems);
|
||||
|
||||
cache.updateQuery(
|
||||
{ query: FindManyNavigationMenuItemsDocument },
|
||||
@@ -45,7 +43,7 @@ export const useRemoveNavigationMenuItemByTargetRecordId = () => {
|
||||
},
|
||||
);
|
||||
},
|
||||
[cache, setPrefetchNavigationMenuItems, store],
|
||||
[cache, setNavigationMenuItems, store],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
+11
-16
@@ -12,7 +12,7 @@ import { filterWorkspaceNavigationMenuItems } from '@/navigation-menu-item/utils
|
||||
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/utils/isNavigationMenuItemFolder';
|
||||
import { isNavigationMenuItemLink } from '@/navigation-menu-item/utils/isNavigationMenuItemLink';
|
||||
import { orderFoldersForCreation } from '@/navigation-menu-item/utils/orderFoldersForCreation';
|
||||
import { prefetchNavigationMenuItemsState } from '@/prefetch/states/prefetchNavigationMenuItemsState';
|
||||
import { navigationMenuItemsState } from '@/navigation-menu-item/states/navigationMenuItemsState';
|
||||
import { useStore } from 'jotai';
|
||||
|
||||
export const useSaveNavigationMenuItemsDraft = () => {
|
||||
@@ -29,12 +29,12 @@ export const useSaveNavigationMenuItemsDraft = () => {
|
||||
|
||||
const saveDraft = useCallback(async () => {
|
||||
const draft = store.get(navigationMenuItemsDraftState.atom);
|
||||
const prefetch = store.get(prefetchNavigationMenuItemsState.atom);
|
||||
const currentItems = store.get(navigationMenuItemsState.atom);
|
||||
|
||||
if (!draft) return;
|
||||
|
||||
const workspacePrefetch = filterWorkspaceNavigationMenuItems(prefetch);
|
||||
const topLevelWorkspace = workspacePrefetch.filter(
|
||||
const workspaceItems = filterWorkspaceNavigationMenuItems(currentItems);
|
||||
const topLevelWorkspace = workspaceItems.filter(
|
||||
(item) => !isDefined(item.folderId),
|
||||
);
|
||||
const draftIds = new Set(draft.map((i) => i.id));
|
||||
@@ -47,7 +47,7 @@ export const useSaveNavigationMenuItemsDraft = () => {
|
||||
.filter(isNavigationMenuItemFolder)
|
||||
.map((item) => item.id),
|
||||
);
|
||||
const folderChildrenToDelete = prefetch.filter(
|
||||
const folderChildrenToDelete = currentItems.filter(
|
||||
(item) =>
|
||||
isDefined(item.folderId) && folderIdsToDelete.has(item.folderId),
|
||||
);
|
||||
@@ -59,13 +59,11 @@ export const useSaveNavigationMenuItemsDraft = () => {
|
||||
await deleteNavigationMenuItem(item.id);
|
||||
}
|
||||
|
||||
const prefetchIds = new Set(workspacePrefetch.map((i) => i.id));
|
||||
const workspacePrefetchById = new Map(
|
||||
workspacePrefetch.map((i) => [i.id, i]),
|
||||
);
|
||||
const idsToCreate = draft.filter((item) => !prefetchIds.has(item.id));
|
||||
const currentIds = new Set(workspaceItems.map((i) => i.id));
|
||||
const workspaceItemsById = new Map(workspaceItems.map((i) => [i.id, i]));
|
||||
const idsToCreate = draft.filter((item) => !currentIds.has(item.id));
|
||||
const idsToRecreate = draft.filter((item) => {
|
||||
const original = workspacePrefetchById.get(item.id);
|
||||
const original = workspaceItemsById.get(item.id);
|
||||
if (!original) return false;
|
||||
return (
|
||||
original.viewId !== item.viewId ||
|
||||
@@ -88,10 +86,7 @@ export const useSaveNavigationMenuItemsDraft = () => {
|
||||
const resolveFolderId = (draftFolderId: string): string =>
|
||||
createdFolderIdByDraftId.get(draftFolderId) ?? draftFolderId;
|
||||
|
||||
const orderedFolders = orderFoldersForCreation(
|
||||
foldersToCreate,
|
||||
prefetchIds,
|
||||
);
|
||||
const orderedFolders = orderFoldersForCreation(foldersToCreate, currentIds);
|
||||
for (const draftItem of orderedFolders) {
|
||||
const input = buildCreateNavigationMenuItemInput(
|
||||
draftItem,
|
||||
@@ -120,7 +115,7 @@ export const useSaveNavigationMenuItemsDraft = () => {
|
||||
for (const draftItem of draft) {
|
||||
if (idsToRecreateSet.has(draftItem.id)) continue;
|
||||
|
||||
const original = workspacePrefetchById.get(draftItem.id);
|
||||
const original = workspaceItemsById.get(draftItem.id);
|
||||
if (!original) continue;
|
||||
|
||||
const positionChanged = original.position !== draftItem.position;
|
||||
|
||||
+2
-2
@@ -11,11 +11,11 @@ import { coreViewsState } from '@/views/states/coreViewState';
|
||||
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
|
||||
import { usePrefetchedNavigationMenuItemsData } from './usePrefetchedNavigationMenuItemsData';
|
||||
import { useNavigationMenuItemsData } from './useNavigationMenuItemsData';
|
||||
|
||||
export const useSortedNavigationMenuItems = () => {
|
||||
const { navigationMenuItems, workspaceNavigationMenuItems } =
|
||||
usePrefetchedNavigationMenuItemsData();
|
||||
useNavigationMenuItemsData();
|
||||
const coreViews = useAtomStateValue(coreViewsState).map(
|
||||
convertCoreViewToView,
|
||||
);
|
||||
|
||||
+2
-2
@@ -8,13 +8,13 @@ import { coreViewsState } from '@/views/states/coreViewState';
|
||||
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { usePrefetchedNavigationMenuItemsData } from './usePrefetchedNavigationMenuItemsData';
|
||||
import { useNavigationMenuItemsData } from './useNavigationMenuItemsData';
|
||||
|
||||
export const useWorkspaceNavigationMenuItems = (): {
|
||||
workspaceNavigationMenuItemsObjectMetadataItems: ObjectMetadataItem[];
|
||||
} => {
|
||||
const { workspaceNavigationMenuItems: rawWorkspaceNavigationMenuItems } =
|
||||
usePrefetchedNavigationMenuItemsData();
|
||||
useNavigationMenuItemsData();
|
||||
const coreViews = useAtomStateValue(coreViewsState);
|
||||
|
||||
const views = coreViews.map(convertCoreViewToView);
|
||||
|
||||
+2
-3
@@ -15,7 +15,7 @@ import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { useNavigationMenuItemsByFolder } from './useNavigationMenuItemsByFolder';
|
||||
import { usePrefetchedNavigationMenuItemsData } from './usePrefetchedNavigationMenuItemsData';
|
||||
import { useNavigationMenuItemsData } from './useNavigationMenuItemsData';
|
||||
import { useSortedNavigationMenuItems } from './useSortedNavigationMenuItems';
|
||||
|
||||
export type FlatWorkspaceItem =
|
||||
@@ -31,8 +31,7 @@ export type NavigationMenuItemClickParams = {
|
||||
};
|
||||
|
||||
export const useWorkspaceSectionItems = (): FlatWorkspaceItem[] => {
|
||||
const { workspaceNavigationMenuItems } =
|
||||
usePrefetchedNavigationMenuItemsData();
|
||||
const { workspaceNavigationMenuItems } = useNavigationMenuItemsData();
|
||||
const { workspaceNavigationMenuItemsSorted } = useSortedNavigationMenuItems();
|
||||
const { workspaceNavigationMenuItemsByFolder } =
|
||||
useNavigationMenuItemsByFolder();
|
||||
|
||||
+2
-4
@@ -1,9 +1,7 @@
|
||||
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
export const prefetchNavigationMenuItemsState = createAtomState<
|
||||
NavigationMenuItem[]
|
||||
>({
|
||||
key: 'prefetchNavigationMenuItemsState',
|
||||
export const navigationMenuItemsState = createAtomState<NavigationMenuItem[]>({
|
||||
key: 'navigationMenuItemsState',
|
||||
defaultValue: [],
|
||||
});
|
||||
-12
@@ -5,18 +5,6 @@ import { type View } from '@/views/types/View';
|
||||
import { ViewKey } from '@/views/types/ViewKey';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
jest.mock('@/favorites/utils/getObjectMetadataNamePluralFromViewId', () => ({
|
||||
getObjectMetadataNamePluralFromViewId: jest.fn(
|
||||
(
|
||||
view: Pick<View, 'id' | 'name' | 'objectMetadataId'>,
|
||||
items: ObjectMetadataItem[],
|
||||
) => {
|
||||
const item = items.find((item) => item.id === view.objectMetadataId);
|
||||
return { namePlural: item?.namePlural ?? 'items' };
|
||||
},
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('twenty-shared/utils', () => {
|
||||
const actual = jest.requireActual('twenty-shared/utils');
|
||||
return {
|
||||
|
||||
+19
-29
@@ -7,8 +7,6 @@ import {
|
||||
} from '@hello-pangea/dnd';
|
||||
import { useState, type ReactNode } from 'react';
|
||||
|
||||
import { FavoritesDragContext } from '@/favorites/contexts/FavoritesDragContext';
|
||||
import { useHandleFavoriteDragAndDrop } from '@/favorites/hooks/useHandleFavoriteDragAndDrop';
|
||||
import { ADD_TO_NAV_SOURCE_DROPPABLE_ID } from '@/navigation-menu-item/constants/AddToNavSourceDroppableId';
|
||||
import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/constants/NavigationMenuItemDroppableIds';
|
||||
import { NavigationDragSourceContext } from '@/navigation-menu-item/contexts/NavigationDragSourceContext';
|
||||
@@ -50,11 +48,10 @@ export const FavoritesDragDropProviderContent = ({
|
||||
const store = useStore();
|
||||
const { workspaceNavigationMenuItems } = useNavigationMenuItemsDraftState();
|
||||
const { handleAddToNavigationDrop } = useHandleAddToNavigationDrop();
|
||||
const { handleFavoriteDragAndDrop } = useHandleFavoriteDragAndDrop();
|
||||
const { handleNavigationMenuItemDragAndDrop } =
|
||||
useHandleNavigationMenuItemDragAndDrop();
|
||||
|
||||
const isFavoritesDroppableId = (droppableId: string) =>
|
||||
const isNavigationMenuItemDroppableId = (droppableId: string) =>
|
||||
droppableId ===
|
||||
NavigationMenuItemDroppableIds.ORPHAN_NAVIGATION_MENU_ITEMS ||
|
||||
droppableId.startsWith('folder-');
|
||||
@@ -109,7 +106,7 @@ export const FavoritesDragDropProviderContent = ({
|
||||
return;
|
||||
}
|
||||
|
||||
if (isFavoritesDroppableId(source.droppableId)) {
|
||||
if (isNavigationMenuItemDroppableId(source.droppableId)) {
|
||||
if (isDefined(destination)) {
|
||||
const dropTargetId =
|
||||
getFavoritesDropTargetIdFromDestination(destination);
|
||||
@@ -141,36 +138,29 @@ export const FavoritesDragDropProviderContent = ({
|
||||
return;
|
||||
}
|
||||
|
||||
if (isFavoritesDroppableId(result.source.droppableId)) {
|
||||
handleNavigationMenuItemDragAndDrop(result, provided);
|
||||
return;
|
||||
}
|
||||
|
||||
handleFavoriteDragAndDrop(result, provided);
|
||||
handleNavigationMenuItemDragAndDrop(result, provided);
|
||||
};
|
||||
|
||||
return (
|
||||
<NavigationDragSourceContext.Provider value={{ sourceDroppableId }}>
|
||||
<NavigationMenuItemDragContext.Provider value={{ isDragging }}>
|
||||
<FavoritesDragContext.Provider value={{ isDragging }}>
|
||||
<NavigationDropTargetContext.Provider
|
||||
value={{
|
||||
activeDropTargetId,
|
||||
setActiveDropTargetId,
|
||||
forbiddenDropTargetId,
|
||||
setForbiddenDropTargetId,
|
||||
addToNavigationFallbackDestination,
|
||||
}}
|
||||
<NavigationDropTargetContext.Provider
|
||||
value={{
|
||||
activeDropTargetId,
|
||||
setActiveDropTargetId,
|
||||
forbiddenDropTargetId,
|
||||
setForbiddenDropTargetId,
|
||||
addToNavigationFallbackDestination,
|
||||
}}
|
||||
>
|
||||
<DragDropContext
|
||||
onDragStart={handleDragStart}
|
||||
onDragUpdate={handleDragUpdate}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<DragDropContext
|
||||
onDragStart={handleDragStart}
|
||||
onDragUpdate={handleDragUpdate}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
{children}
|
||||
</DragDropContext>
|
||||
</NavigationDropTargetContext.Provider>
|
||||
</FavoritesDragContext.Provider>
|
||||
{children}
|
||||
</DragDropContext>
|
||||
</NavigationDropTargetContext.Provider>
|
||||
</NavigationMenuItemDragContext.Provider>
|
||||
</NavigationDragSourceContext.Provider>
|
||||
);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user