Always show Favorites section and add favorites via the side panel (#21087)
## What & why The left-sidebar **Favorites** section was hidden whenever the user had no favorites, so it was effectively undiscoverable — and personal favorites could only be created via the record-level "Add to favorites" action or drag-drop. This PR: - **Always shows the Favorites section**, with an empty-state **"Add a favorite"** call-to-action. - Adds a **"+" on the Favorites header that opens the same "New menu item" side panel** the Workspace section already uses, so users can add personal **Objects, Views, Records, Links and Folders** directly from the sidebar. ## How Favorites and workspace navigation are the same `NavigationMenuItem` entity (a personal favorite simply has `userWorkspaceId` set). Rather than build a separate favorites-only flow, the shared add/edit side-panel subsystem is made **section-aware** (`NavigationMenuItemSection = 'workspace' | 'favorite'`): - a new `navigationMenuItemEditSectionState` atom records which section the panel is operating on; - a new `useNavigationMenuItemEditController` forks persistence — the **workspace** section stages changes in the draft (saved on layout-customization exit), while the **favorite** section creates/updates/deletes personal items **immediately** with `userWorkspaceId = current member`. This mirrors the existing `useHandleNavigationMenuItemDragAndDrop` fork. The existing add/edit hooks, pickers and title editors were rerouted through the controller and a section-aware items hook, so they work for both sections with no behavior change to the workspace flow. **Backend: no changes** — `canUserCreateNavigationMenuItem` already authorizes personal navigation menu items of every type for any authenticated user. ## Decisions & tradeoffs - **Folder button → unified "+":** the folder-only header button is replaced by the single "+" (Folder is one of the panel's options), matching the Workspace section. This removed the inline folder-create code path. - **Click-to-add only in v1:** dragging items from the panel directly into Favorites is deferred — those drag handles are disabled in the favorite section (the drag path is hardwired to workspace layout mode), with a defense-in-depth no-op in the drop handler. - **Persist-on-commit:** favorite title/URL edits hit the network once on blur/enter, never per keystroke. - **Personal color edits** change only the favorite's own color, never the shared object metadata (that remains a workspace-customization behavior). - The change touches ~37 files because it generalizes the shared subsystem rather than duplicating it; net diff is slightly negative (+605 / −636). ## Testing - `npx nx typecheck twenty-front` — passes - `npx nx lint twenty-front` (oxlint + oxfmt) — passes - `navigation-menu-item` unit tests — pass (incl. an updated `computeInsertIndexAndPosition` test covering personal items) - Manual end-to-end walkthrough still recommended before merge.
This commit is contained in:
+2
@@ -9,6 +9,7 @@ import { commandMenuItemsDraftState } from '@/command-menu-item/edit/states/comm
|
||||
import { commandMenuItemsSelector } from '@/command-menu-item/states/commandMenuItemsSelector';
|
||||
import { activeCustomizationPageLayoutIdsState } from '@/layout-customization/states/activeCustomizationPageLayoutIdsState';
|
||||
import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
|
||||
import { navigationMenuItemEditSectionState } from '@/navigation-menu-item/common/states/navigationMenuItemEditSectionState';
|
||||
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/states/navigationMenuItemsDraftState';
|
||||
import { navigationMenuItemsSelector } from '@/navigation-menu-item/common/states/navigationMenuItemsSelector';
|
||||
import { filterWorkspaceNavigationMenuItems } from '@/navigation-menu-item/common/utils/filterWorkspaceNavigationMenuItems';
|
||||
@@ -60,6 +61,7 @@ export const useEnterLayoutCustomizationMode = () => {
|
||||
prefetchNavigationMenuItems,
|
||||
);
|
||||
store.set(navigationMenuItemsDraftState.atom, workspaceNavigationMenuItems);
|
||||
store.set(navigationMenuItemEditSectionState.atom, 'workspace');
|
||||
|
||||
const persistedCommandMenuItems = store.get(commandMenuItemsSelector.atom);
|
||||
store.set(commandMenuItemsDraftState.atom, persistedCommandMenuItems);
|
||||
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
|
||||
|
||||
export const isNavigationMenuItemFolderCreatingState = createAtomState<boolean>(
|
||||
{
|
||||
key: 'isNavigationMenuItemFolderCreatingState',
|
||||
defaultValue: false,
|
||||
},
|
||||
);
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import type { NavigationMenuItemSection } from '@/navigation-menu-item/common/types/NavigationMenuItemSection';
|
||||
|
||||
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
|
||||
|
||||
export const navigationMenuItemEditSectionState =
|
||||
createAtomState<NavigationMenuItemSection>({
|
||||
key: 'navigationMenuItemEditSectionState',
|
||||
defaultValue: 'workspace',
|
||||
});
|
||||
+13
-5
@@ -18,7 +18,7 @@ describe('computeInsertIndexAndPosition', () => {
|
||||
expect(between.position).toBe(15);
|
||||
});
|
||||
|
||||
it('should only consider items in target folder and exclude userWorkspaceId', () => {
|
||||
it('should only consider items in the target folder', () => {
|
||||
const draft: NavigationMenuItem[] = [
|
||||
{ id: '1', folderId: 'folder-a', position: 10 } as NavigationMenuItem,
|
||||
{ id: '2', folderId: 'folder-b', position: 20 } as NavigationMenuItem,
|
||||
@@ -27,9 +27,16 @@ describe('computeInsertIndexAndPosition', () => {
|
||||
const result = computeInsertIndexAndPosition(draft, 'folder-b', 1);
|
||||
expect(result.flatIndex).toBe(2);
|
||||
expect(result.position).toBe(25);
|
||||
});
|
||||
|
||||
const withWorkspace: NavigationMenuItem[] = [
|
||||
{ id: '1', folderId: null, position: 10 } as NavigationMenuItem,
|
||||
it('should consider personal items, as callers pass a section-scoped list', () => {
|
||||
const personalItems: NavigationMenuItem[] = [
|
||||
{
|
||||
id: '1',
|
||||
folderId: null,
|
||||
position: 10,
|
||||
userWorkspaceId: 'ws-1',
|
||||
} as NavigationMenuItem,
|
||||
{
|
||||
id: '2',
|
||||
folderId: null,
|
||||
@@ -37,7 +44,8 @@ describe('computeInsertIndexAndPosition', () => {
|
||||
userWorkspaceId: 'ws-1',
|
||||
} as NavigationMenuItem,
|
||||
];
|
||||
const excluded = computeInsertIndexAndPosition(withWorkspace, null, 1);
|
||||
expect(excluded.position).toBe(10.5);
|
||||
const inserted = computeInsertIndexAndPosition(personalItems, null, 1);
|
||||
expect(inserted.flatIndex).toBe(1);
|
||||
expect(inserted.position).toBe(15);
|
||||
});
|
||||
});
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
// Normalizes a link rename/url edit: a blank name falls back to "Link", and a
|
||||
// blank url is ignored so the existing link is kept.
|
||||
export const buildNavigationMenuLinkUpdate = (updates: {
|
||||
name?: string;
|
||||
link?: string;
|
||||
}): Partial<NavigationMenuItem> => {
|
||||
const normalized: Partial<NavigationMenuItem> = {};
|
||||
|
||||
if (updates.name !== undefined) {
|
||||
normalized.name = updates.name.trim() || 'Link';
|
||||
}
|
||||
|
||||
if (updates.link !== undefined && updates.link.trim() !== '') {
|
||||
normalized.link = updates.link.trim();
|
||||
}
|
||||
|
||||
return normalized;
|
||||
};
|
||||
+6
-10
@@ -2,24 +2,20 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import type { NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
export const computeInsertIndexAndPosition = (
|
||||
currentDraft: NavigationMenuItem[],
|
||||
items: NavigationMenuItem[],
|
||||
targetFolderId: string | null,
|
||||
targetIndex: number,
|
||||
) => {
|
||||
const itemsInFolder = currentDraft
|
||||
.filter(
|
||||
(item) =>
|
||||
(item.folderId ?? null) === targetFolderId &&
|
||||
!isDefined(item.userWorkspaceId),
|
||||
)
|
||||
const itemsInFolder = items
|
||||
.filter((item) => (item.folderId ?? null) === targetFolderId)
|
||||
.sort((a, b) => a.position - b.position);
|
||||
const insertRef = itemsInFolder[targetIndex];
|
||||
const lastInFolder = itemsInFolder[itemsInFolder.length - 1];
|
||||
const flatIndex = isDefined(insertRef)
|
||||
? currentDraft.indexOf(insertRef)
|
||||
? items.indexOf(insertRef)
|
||||
: isDefined(lastInFolder)
|
||||
? currentDraft.indexOf(lastInFolder) + 1
|
||||
: currentDraft.length;
|
||||
? items.indexOf(lastInFolder) + 1
|
||||
: items.length;
|
||||
const prevPosition = itemsInFolder[targetIndex - 1]?.position ?? 0;
|
||||
const nextPosition = itemsInFolder[targetIndex]?.position ?? prevPosition + 1;
|
||||
const position = (prevPosition + nextPosition) / 2;
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/common/utils/isNavigationMenuItemFolder';
|
||||
|
||||
type FolderWithChildren = {
|
||||
id: string;
|
||||
navigationMenuItems: NavigationMenuItem[];
|
||||
};
|
||||
|
||||
// Expand a display-ordered top-level list (orphans + folders) into a flat list
|
||||
// where each folder is immediately followed by its children.
|
||||
export const flattenNavigationMenuItemsWithFolderChildren = (
|
||||
topLevelItems: NavigationMenuItem[],
|
||||
foldersWithChildren: FolderWithChildren[],
|
||||
): NavigationMenuItem[] => {
|
||||
const childrenByFolderId = new Map(
|
||||
foldersWithChildren.map((folder) => [
|
||||
folder.id,
|
||||
folder.navigationMenuItems,
|
||||
]),
|
||||
);
|
||||
|
||||
return topLevelItems.flatMap((item) =>
|
||||
isNavigationMenuItemFolder(item)
|
||||
? [item, ...(childrenByFolderId.get(item.id) ?? [])]
|
||||
: [item],
|
||||
);
|
||||
};
|
||||
+120
-107
@@ -1,45 +1,37 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { isDefined, normalizeUrl } from 'twenty-shared/utils';
|
||||
import { IconFolder, IconLink, useIcons } from 'twenty-ui/display';
|
||||
|
||||
import { useEnterLayoutCustomizationMode } from '@/layout-customization/hooks/useEnterLayoutCustomizationMode';
|
||||
import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
|
||||
import { ADD_TO_NAV_SOURCE_DROPPABLE_ID } from '@/navigation-menu-item/common/constants/AddToNavSourceDroppableId';
|
||||
import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER } from '@/navigation-menu-item/common/constants/NavigationMenuItemDefaultColorFolder';
|
||||
import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_LINK } from '@/navigation-menu-item/common/constants/NavigationMenuItemDefaultColorLink';
|
||||
import { addToNavPayloadRegistryState } from '@/navigation-menu-item/common/states/addToNavPayloadRegistryState';
|
||||
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/states/navigationMenuItemsDraftState';
|
||||
import { navigationMenuItemEditSectionState } from '@/navigation-menu-item/common/states/navigationMenuItemEditSectionState';
|
||||
import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/common/states/openNavigationMenuItemFolderIdsState';
|
||||
import { canNavigationMenuItemBeDroppedIn } from '@/navigation-menu-item/common/utils/canNavigationMenuItemBeDroppedIn';
|
||||
import { getObjectMetadataIdsInDraft } from '@/navigation-menu-item/common/utils/getObjectMetadataIdsInDraft';
|
||||
import { validateAndExtractWorkspaceFolderId } from '@/navigation-menu-item/common/utils/validateAndExtractWorkspaceFolderId';
|
||||
import { useAddFolderToNavigationMenuDraft } from '@/navigation-menu-item/edit/folder/hooks/useAddFolderToNavigationMenuDraft';
|
||||
import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemsDraftState';
|
||||
import {
|
||||
type NewNavigationMenuItemInput,
|
||||
useNavigationMenuItemEditController,
|
||||
} from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemEditController';
|
||||
import { useOpenNavigationMenuItemInSidePanel } from '@/navigation-menu-item/edit/hooks/useOpenNavigationMenuItemInSidePanel';
|
||||
import { useAddLinkToNavigationMenuDraft } from '@/navigation-menu-item/edit/link/hooks/useAddLinkToNavigationMenuDraft';
|
||||
import { useAddObjectToNavigationMenuDraft } from '@/navigation-menu-item/edit/object/hooks/useAddObjectToNavigationMenuDraft';
|
||||
import { useAddRecordToNavigationMenuDraft } from '@/navigation-menu-item/edit/record/hooks/useAddRecordToNavigationMenuDraft';
|
||||
import { useAddViewToNavigationMenuDraft } from '@/navigation-menu-item/edit/view/hooks/useAddViewToNavigationMenuDraft';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { getObjectColorWithFallback } from '@/object-metadata/utils/getObjectColorWithFallback';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
|
||||
|
||||
import { useStore } from 'jotai';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
|
||||
import type { NavigationMenuItemDropResult } from '@/navigation-menu-item/common/types/navigationMenuItemDropResult';
|
||||
|
||||
export const useHandleAddToNavigationDrop = () => {
|
||||
const store = useStore();
|
||||
const { addObjectToDraft } = useAddObjectToNavigationMenuDraft();
|
||||
const { addViewToDraft } = useAddViewToNavigationMenuDraft();
|
||||
const { addRecordToDraft } = useAddRecordToNavigationMenuDraft();
|
||||
const { addFolderToDraft } = useAddFolderToNavigationMenuDraft();
|
||||
const { addLinkToDraft } = useAddLinkToNavigationMenuDraft();
|
||||
const { workspaceNavigationMenuItems } = useNavigationMenuItemsDraftState();
|
||||
const navigationMenuItemsDraft = useAtomStateValue(
|
||||
navigationMenuItemsDraftState,
|
||||
);
|
||||
const { currentItems, createItem } = useNavigationMenuItemEditController();
|
||||
const { openNavigationMenuItemInSidePanel } =
|
||||
useOpenNavigationMenuItemInSidePanel();
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
@@ -52,6 +44,12 @@ export const useHandleAddToNavigationDrop = () => {
|
||||
|
||||
const handleAddToNavigationDrop = useCallback(
|
||||
(result: NavigationMenuItemDropResult) => {
|
||||
// Drag-to-add always targets the workspace sidebar; favorites are added
|
||||
// by click only.
|
||||
if (store.get(navigationMenuItemEditSectionState.atom) === 'favorite') {
|
||||
return;
|
||||
}
|
||||
|
||||
const { source, destination, draggableId } = result;
|
||||
if (
|
||||
source.droppableId !== ADD_TO_NAV_SOURCE_DROPPABLE_ID ||
|
||||
@@ -70,9 +68,6 @@ export const useHandleAddToNavigationDrop = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentDraft = isDefined(navigationMenuItemsDraft)
|
||||
? navigationMenuItemsDraft
|
||||
: workspaceNavigationMenuItems;
|
||||
const folderId = validateAndExtractWorkspaceFolderId(
|
||||
destination.droppableId,
|
||||
);
|
||||
@@ -88,74 +83,93 @@ export const useHandleAddToNavigationDrop = () => {
|
||||
);
|
||||
}
|
||||
|
||||
const openEditForNewNavItem = (
|
||||
newItemId: string,
|
||||
options: Omit<
|
||||
// Enter customization mode first: it (re)seeds the workspace draft from
|
||||
// the live items, so creating beforehand would have the new item
|
||||
// overwritten. It also bails without enabling the mode when a dashboard
|
||||
// is mid-edit, so skip the create unless the mode is actually on —
|
||||
// otherwise the item lands in a draft that is neither shown nor saved.
|
||||
const addToWorkspaceAndOpenEdit = (
|
||||
input: NewNavigationMenuItemInput,
|
||||
position: { targetFolderId: string | null; targetIndex: number },
|
||||
openOptions: Omit<
|
||||
Parameters<typeof openNavigationMenuItemInSidePanel>[0],
|
||||
'itemId'
|
||||
>,
|
||||
) => {
|
||||
enterLayoutCustomizationMode();
|
||||
openNavigationMenuItemInSidePanel({ ...options, itemId: newItemId });
|
||||
if (!store.get(isLayoutCustomizationModeEnabledState.atom)) {
|
||||
return;
|
||||
}
|
||||
const newItemId = createItem(input, position);
|
||||
openNavigationMenuItemInSidePanel({
|
||||
...openOptions,
|
||||
itemId: newItemId,
|
||||
});
|
||||
};
|
||||
|
||||
switch (payload.type) {
|
||||
case NavigationMenuItemType.FOLDER: {
|
||||
const newFolderId = addFolderToDraft(
|
||||
payload.name,
|
||||
currentDraft,
|
||||
null,
|
||||
index,
|
||||
addToWorkspaceAndOpenEdit(
|
||||
{
|
||||
type: NavigationMenuItemType.FOLDER,
|
||||
name: payload.name,
|
||||
color: DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER,
|
||||
},
|
||||
{ targetFolderId: null, targetIndex: index },
|
||||
{
|
||||
pageTitle: t`Edit folder`,
|
||||
pageIcon: IconFolder,
|
||||
focusTitleInput: true,
|
||||
},
|
||||
);
|
||||
openEditForNewNavItem(newFolderId, {
|
||||
pageTitle: t`Edit folder`,
|
||||
pageIcon: IconFolder,
|
||||
focusTitleInput: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
case NavigationMenuItemType.LINK: {
|
||||
const newLinkId = addLinkToDraft(
|
||||
payload.name || t`Link label`,
|
||||
payload.link,
|
||||
currentDraft,
|
||||
folderId,
|
||||
index,
|
||||
addToWorkspaceAndOpenEdit(
|
||||
{
|
||||
type: NavigationMenuItemType.LINK,
|
||||
name: payload.name || t`Link label`,
|
||||
link: normalizeUrl(payload.link),
|
||||
color: DEFAULT_NAVIGATION_MENU_ITEM_COLOR_LINK,
|
||||
},
|
||||
{ targetFolderId: folderId, targetIndex: index },
|
||||
{
|
||||
pageTitle: t`Edit link`,
|
||||
pageIcon: IconLink,
|
||||
focusTitleInput: true,
|
||||
},
|
||||
);
|
||||
openEditForNewNavItem(newLinkId, {
|
||||
pageTitle: t`Edit link`,
|
||||
pageIcon: IconLink,
|
||||
focusTitleInput: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
case NavigationMenuItemType.OBJECT: {
|
||||
const objectMetadataIdsInWorkspace =
|
||||
getObjectMetadataIdsInDraft(currentDraft);
|
||||
|
||||
if (objectMetadataIdsInWorkspace.has(payload.objectMetadataId)) {
|
||||
if (
|
||||
getObjectMetadataIdsInDraft(currentItems).has(
|
||||
payload.objectMetadataId,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(item) => item.id === payload.objectMetadataId,
|
||||
);
|
||||
const newItemId = addObjectToDraft({
|
||||
objectMetadataId: payload.objectMetadataId,
|
||||
currentDraft,
|
||||
targetFolderId: folderId,
|
||||
targetIndex: index,
|
||||
color:
|
||||
payload.iconColor ??
|
||||
(objectMetadataItem
|
||||
? getObjectColorWithFallback(objectMetadataItem)
|
||||
: undefined),
|
||||
});
|
||||
openEditForNewNavItem(newItemId, {
|
||||
pageTitle: objectMetadataItem?.labelPlural ?? payload.label,
|
||||
pageIcon: objectMetadataItem
|
||||
? getIcon(objectMetadataItem.icon)
|
||||
: IconFolder,
|
||||
});
|
||||
addToWorkspaceAndOpenEdit(
|
||||
{
|
||||
type: NavigationMenuItemType.OBJECT,
|
||||
targetObjectMetadataId: payload.objectMetadataId,
|
||||
color:
|
||||
payload.iconColor ??
|
||||
(objectMetadataItem
|
||||
? getObjectColorWithFallback(objectMetadataItem)
|
||||
: undefined),
|
||||
},
|
||||
{ targetFolderId: folderId, targetIndex: index },
|
||||
{
|
||||
pageTitle: objectMetadataItem?.labelPlural ?? payload.label,
|
||||
pageIcon: objectMetadataItem
|
||||
? getIcon(objectMetadataItem.icon)
|
||||
: IconFolder,
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
case NavigationMenuItemType.VIEW: {
|
||||
@@ -165,62 +179,61 @@ export const useHandleAddToNavigationDrop = () => {
|
||||
(item) => item.id === view.objectMetadataId,
|
||||
)
|
||||
: undefined;
|
||||
const newItemId = addViewToDraft(
|
||||
payload.viewId,
|
||||
currentDraft,
|
||||
folderId,
|
||||
index,
|
||||
viewObjectMetadataItem
|
||||
? getObjectColorWithFallback(viewObjectMetadataItem)
|
||||
: undefined,
|
||||
addToWorkspaceAndOpenEdit(
|
||||
{
|
||||
type: NavigationMenuItemType.VIEW,
|
||||
viewId: payload.viewId,
|
||||
color: viewObjectMetadataItem
|
||||
? getObjectColorWithFallback(viewObjectMetadataItem)
|
||||
: undefined,
|
||||
},
|
||||
{ targetFolderId: folderId, targetIndex: index },
|
||||
{
|
||||
pageTitle: view?.name ?? payload.label,
|
||||
pageIcon: view ? getIcon(view.icon) : IconFolder,
|
||||
},
|
||||
);
|
||||
openEditForNewNavItem(newItemId, {
|
||||
pageTitle: view?.name ?? payload.label,
|
||||
pageIcon: view ? getIcon(view.icon) : IconFolder,
|
||||
});
|
||||
return;
|
||||
}
|
||||
case NavigationMenuItemType.RECORD: {
|
||||
const newItemId = addRecordToDraft(
|
||||
{
|
||||
recordId: payload.recordId,
|
||||
objectMetadataId: payload.objectMetadataId,
|
||||
objectNameSingular: payload.objectNameSingular,
|
||||
label: payload.label,
|
||||
imageUrl: payload.imageUrl,
|
||||
},
|
||||
currentDraft,
|
||||
folderId,
|
||||
index,
|
||||
);
|
||||
if (!isDefined(newItemId)) return;
|
||||
if (!isDefined(payload.objectMetadataId)) {
|
||||
return;
|
||||
}
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(item) => item.id === payload.objectMetadataId,
|
||||
);
|
||||
openEditForNewNavItem(newItemId, {
|
||||
pageTitle: payload.label,
|
||||
pageIcon: objectMetadataItem
|
||||
? getIcon(objectMetadataItem.icon)
|
||||
: IconFolder,
|
||||
});
|
||||
addToWorkspaceAndOpenEdit(
|
||||
{
|
||||
type: NavigationMenuItemType.RECORD,
|
||||
targetObjectMetadataId: payload.objectMetadataId,
|
||||
targetRecordId: payload.recordId,
|
||||
targetRecordIdentifier: {
|
||||
id: payload.recordId,
|
||||
labelIdentifier: payload.label,
|
||||
imageIdentifier: payload.imageUrl ?? null,
|
||||
},
|
||||
},
|
||||
{ targetFolderId: folderId, targetIndex: index },
|
||||
{
|
||||
pageTitle: payload.label,
|
||||
pageIcon: objectMetadataItem
|
||||
? getIcon(objectMetadataItem.icon)
|
||||
: IconFolder,
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
addFolderToDraft,
|
||||
addLinkToDraft,
|
||||
addObjectToDraft,
|
||||
addRecordToDraft,
|
||||
addViewToDraft,
|
||||
createItem,
|
||||
currentItems,
|
||||
views,
|
||||
getIcon,
|
||||
navigationMenuItemsDraft,
|
||||
objectMetadataItems,
|
||||
openNavigationMenuItemInSidePanel,
|
||||
setOpenNavigationMenuItemFolderIds,
|
||||
enterLayoutCustomizationMode,
|
||||
workspaceNavigationMenuItems,
|
||||
store,
|
||||
],
|
||||
);
|
||||
|
||||
-45
@@ -1,45 +0,0 @@
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { useCreateManyNavigationMenuItems } from '@/navigation-menu-item/common/hooks/useCreateManyNavigationMenuItems';
|
||||
import { useNavigationMenuItemsData } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemsData';
|
||||
|
||||
export const useCreateNavigationMenuItemFolder = () => {
|
||||
const { navigationMenuItems, currentWorkspaceMemberId } =
|
||||
useNavigationMenuItemsData();
|
||||
|
||||
const { createManyNavigationMenuItems } = useCreateManyNavigationMenuItems();
|
||||
|
||||
const createNewNavigationMenuItemFolder = async (
|
||||
name: string,
|
||||
): Promise<void> => {
|
||||
if (!name || !currentWorkspaceMemberId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const topLevelItems = navigationMenuItems.filter(
|
||||
(item) =>
|
||||
!isDefined(item.folderId) &&
|
||||
item.userWorkspaceId === currentWorkspaceMemberId,
|
||||
);
|
||||
|
||||
const minPosition =
|
||||
topLevelItems.length > 0
|
||||
? Math.min(...topLevelItems.map((item) => item.position))
|
||||
: 1;
|
||||
|
||||
await createManyNavigationMenuItems([
|
||||
{
|
||||
type: NavigationMenuItemType.FOLDER,
|
||||
name,
|
||||
targetRecordId: null,
|
||||
targetObjectMetadataId: null,
|
||||
userWorkspaceId: currentWorkspaceMemberId,
|
||||
folderId: null,
|
||||
position: minPosition - 1,
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
return { createNewNavigationMenuItemFolder };
|
||||
};
|
||||
+4
-12
@@ -1,7 +1,7 @@
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
|
||||
import { flattenNavigationMenuItemsWithFolderChildren } from '@/navigation-menu-item/common/utils/flattenNavigationMenuItemsWithFolderChildren';
|
||||
import { getWorkspaceSidebarOrphanItemsInDisplayOrder } from '@/navigation-menu-item/display/utils/getWorkspaceSidebarOrphanItemsInDisplayOrder';
|
||||
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
|
||||
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
||||
@@ -30,13 +30,6 @@ export const useNavigationMenuItemSectionItems = (): NavigationMenuItem[] => {
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
|
||||
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
|
||||
|
||||
const folderChildrenById = new Map(
|
||||
workspaceNavigationMenuItemsByFolder.map((folder) => [
|
||||
folder.id,
|
||||
folder.navigationMenuItems,
|
||||
]),
|
||||
);
|
||||
|
||||
const flatItems = getWorkspaceSidebarOrphanItemsInDisplayOrder({
|
||||
workspaceNavigationMenuItems,
|
||||
workspaceNavigationMenuItemsSorted,
|
||||
@@ -46,9 +39,8 @@ export const useNavigationMenuItemSectionItems = (): NavigationMenuItem[] => {
|
||||
includeInaccessibleObjectBackedItems: isLayoutCustomizationModeEnabled,
|
||||
});
|
||||
|
||||
return flatItems.flatMap((item) =>
|
||||
item.type === NavigationMenuItemType.FOLDER
|
||||
? [item, ...(folderChildrenById.get(item.id) ?? [])]
|
||||
: [item],
|
||||
return flattenNavigationMenuItemsWithFolderChildren(
|
||||
flatItems,
|
||||
workspaceNavigationMenuItemsByFolder,
|
||||
);
|
||||
};
|
||||
|
||||
+35
-73
@@ -1,7 +1,8 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useCallback, useContext, useMemo, useState } from 'react';
|
||||
import { IconFolder, IconFolderPlus, IconHeartOff } from 'twenty-ui/display';
|
||||
import { useCallback, useContext, useMemo } from 'react';
|
||||
import { SidePanelPages } from 'twenty-shared/types';
|
||||
import { IconHeart, IconHeartOff, IconPlus } from 'twenty-ui/display';
|
||||
import { LightIconButton } from 'twenty-ui/input';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
@@ -10,23 +11,23 @@ import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/common/co
|
||||
import { NavigationSections } from '@/navigation-menu-item/common/constants/NavigationSections.constants';
|
||||
import { NavigationMenuItemDragContext } from '@/navigation-menu-item/common/contexts/NavigationMenuItemDragContext';
|
||||
import { useDeleteManyNavigationMenuItems } from '@/navigation-menu-item/common/hooks/useDeleteManyNavigationMenuItems';
|
||||
import { isNavigationMenuItemFolderCreatingState } from '@/navigation-menu-item/common/states/isNavigationMenuItemFolderCreatingState';
|
||||
import { navigationMenuItemEditSectionState } from '@/navigation-menu-item/common/states/navigationMenuItemEditSectionState';
|
||||
import { pendingInsertionNavigationMenuItemState } from '@/navigation-menu-item/common/states/pendingInsertionNavigationMenuItemState';
|
||||
import { selectedNavigationMenuItemIdInEditModeState } from '@/navigation-menu-item/common/states/selectedNavigationMenuItemIdInEditModeState';
|
||||
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/common/utils/isNavigationMenuItemFolder';
|
||||
import { NavigationMenuItemDisplay } from '@/navigation-menu-item/display/components/NavigationMenuItemDisplay';
|
||||
import { NavigationMenuItemDroppableSlot } from '@/navigation-menu-item/display/dnd/components/NavigationMenuItemDroppableSlot';
|
||||
import { NavigationMenuItemSortableItem } from '@/navigation-menu-item/display/dnd/components/NavigationMenuItemSortableItem';
|
||||
import { useIsDropDisabledForSection } from '@/navigation-menu-item/display/dnd/hooks/useIsDropDisabledForSection';
|
||||
import { useCreateNavigationMenuItemFolder } from '@/navigation-menu-item/display/folder/hooks/useCreateNavigationMenuItemFolder';
|
||||
import { useNavigationMenuItemsByFolder } from '@/navigation-menu-item/display/folder/hooks/useNavigationMenuItemsByFolder';
|
||||
import { useSortedNavigationMenuItems } from '@/navigation-menu-item/display/hooks/useSortedNavigationMenuItems';
|
||||
import { NavigationMenuItemOrphanDropTarget } from '@/navigation-menu-item/display/sections/components/NavigationMenuItemOrphanDropTarget';
|
||||
import { NavigationMenuItemSection } from '@/navigation-menu-item/display/sections/components/NavigationMenuItemSection';
|
||||
import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper';
|
||||
import { NavigationDrawerInput } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerInput';
|
||||
import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel';
|
||||
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 { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
|
||||
const StyledList = styled.div`
|
||||
display: flex;
|
||||
@@ -40,10 +41,6 @@ const StyledListItemRow = styled.div`
|
||||
gap: 0;
|
||||
`;
|
||||
|
||||
const StyledEmptyContainer = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const ORPHAN_DROPPABLE_ID =
|
||||
NavigationMenuItemDroppableIds.FAVORITE_ORPHAN_NAVIGATION_MENU_ITEMS;
|
||||
|
||||
@@ -54,14 +51,16 @@ export const FavoritesSection = () => {
|
||||
const { isDragging } = useContext(NavigationMenuItemDragContext);
|
||||
const favoritesDropDisabled = useIsDropDisabledForSection(false);
|
||||
|
||||
const [newFolderName, setNewFolderName] = useState('');
|
||||
const { createNewNavigationMenuItemFolder } =
|
||||
useCreateNavigationMenuItemFolder();
|
||||
|
||||
const [
|
||||
isNavigationMenuItemFolderCreating,
|
||||
setIsNavigationMenuItemFolderCreating,
|
||||
] = useAtomState(isNavigationMenuItemFolderCreatingState);
|
||||
const { navigateSidePanel } = useNavigateSidePanel();
|
||||
const setNavigationMenuItemEditSection = useSetAtomState(
|
||||
navigationMenuItemEditSectionState,
|
||||
);
|
||||
const setPendingInsertionNavigationMenuItem = useSetAtomState(
|
||||
pendingInsertionNavigationMenuItemState,
|
||||
);
|
||||
const setSelectedNavigationMenuItemIdInEditMode = useSetAtomState(
|
||||
selectedNavigationMenuItemIdInEditModeState,
|
||||
);
|
||||
|
||||
const { t } = useLingui();
|
||||
|
||||
@@ -90,35 +89,20 @@ export const FavoritesSection = () => {
|
||||
[topLevelItems],
|
||||
);
|
||||
|
||||
const toggleNewFolder = () => {
|
||||
const handleAddFavorite = (event?: React.MouseEvent) => {
|
||||
event?.stopPropagation();
|
||||
// Expansion is gated on items existing, so this stays collapsed if the user
|
||||
// cancels and reveals the favorite as soon as it is added.
|
||||
openNavigationSection();
|
||||
setIsNavigationMenuItemFolderCreating((current) => !current);
|
||||
};
|
||||
|
||||
const handleSubmitFolderCreation = async (value: string) => {
|
||||
if (value === '') return;
|
||||
setIsNavigationMenuItemFolderCreating(false);
|
||||
setNewFolderName('');
|
||||
await createNewNavigationMenuItemFolder(value);
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleClickOutside = async (
|
||||
_event: MouseEvent | TouchEvent,
|
||||
value: string,
|
||||
) => {
|
||||
if (!value) {
|
||||
setIsNavigationMenuItemFolderCreating(false);
|
||||
return;
|
||||
}
|
||||
setIsNavigationMenuItemFolderCreating(false);
|
||||
setNewFolderName('');
|
||||
await createNewNavigationMenuItemFolder(value);
|
||||
};
|
||||
|
||||
const handleCancelFolderCreation = () => {
|
||||
setNewFolderName('');
|
||||
setIsNavigationMenuItemFolderCreating(false);
|
||||
setNavigationMenuItemEditSection('favorite');
|
||||
setPendingInsertionNavigationMenuItem(null);
|
||||
setSelectedNavigationMenuItemIdInEditMode(null);
|
||||
navigateSidePanel({
|
||||
page: SidePanelPages.NavigationMenuAddItem,
|
||||
pageTitle: t`New favorite`,
|
||||
pageIcon: IconHeart,
|
||||
resetNavigationStack: true,
|
||||
});
|
||||
};
|
||||
|
||||
const makeRightOptions = useCallback(
|
||||
@@ -135,40 +119,20 @@ export const FavoritesSection = () => {
|
||||
[deleteManyNavigationMenuItems],
|
||||
);
|
||||
|
||||
if (
|
||||
topLevelItems.length === 0 &&
|
||||
!isNavigationMenuItemFolderCreating &&
|
||||
userNavigationMenuItemsByFolder.length === 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<NavigationMenuItemSection
|
||||
title={t`Favorites`}
|
||||
isOpen={isNavigationSectionOpen}
|
||||
isOpen={topLevelItems.length > 0 && isNavigationSectionOpen}
|
||||
onToggle={toggleNavigationSection}
|
||||
rightIcon={
|
||||
<LightIconButton
|
||||
Icon={IconFolderPlus}
|
||||
onClick={toggleNewFolder}
|
||||
Icon={IconPlus}
|
||||
onClick={handleAddFavorite}
|
||||
accent="tertiary"
|
||||
/>
|
||||
}
|
||||
>
|
||||
{isNavigationMenuItemFolderCreating && (
|
||||
<NavigationDrawerAnimatedCollapseWrapper>
|
||||
<NavigationDrawerInput
|
||||
Icon={IconFolder}
|
||||
value={newFolderName}
|
||||
onChange={setNewFolderName}
|
||||
onSubmit={handleSubmitFolderCreation}
|
||||
onCancel={handleCancelFolderCreation}
|
||||
onClickOutside={handleClickOutside}
|
||||
/>
|
||||
</NavigationDrawerAnimatedCollapseWrapper>
|
||||
)}
|
||||
{topLevelItems.length > 0 ? (
|
||||
{topLevelItems.length > 0 && (
|
||||
<StyledList>
|
||||
{topLevelItems.map((item, index) => (
|
||||
<StyledListItemRow key={item.id}>
|
||||
@@ -227,8 +191,6 @@ export const FavoritesSection = () => {
|
||||
/>
|
||||
</NavigationMenuItemDroppableSlot>
|
||||
</StyledList>
|
||||
) : (
|
||||
<StyledEmptyContainer style={{ height: isDragging ? '24px' : '1px' }} />
|
||||
)}
|
||||
</NavigationMenuItemSection>
|
||||
);
|
||||
|
||||
-14
@@ -1,28 +1,14 @@
|
||||
import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
|
||||
import { NavigationSections } from '@/navigation-menu-item/common/constants/NavigationSections.constants';
|
||||
import { isNavigationMenuItemFolderCreatingState } from '@/navigation-menu-item/common/states/isNavigationMenuItemFolderCreatingState';
|
||||
import { NavigationMenuItemDndKitProvider } from '@/navigation-menu-item/display/dnd/providers/NavigationMenuItemDndKitProvider';
|
||||
import { useSortedNavigationMenuItems } from '@/navigation-menu-item/display/hooks/useSortedNavigationMenuItems';
|
||||
import { FavoritesSection } from '@/navigation-menu-item/display/sections/favorites/components/FavoritesSection';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { AnimatedEaseInOut } from 'twenty-ui/utilities';
|
||||
|
||||
export const FavoritesSectionDispatcher = () => {
|
||||
const { navigationMenuItemsSorted } = useSortedNavigationMenuItems();
|
||||
const isLayoutCustomizationModeEnabled = useAtomStateValue(
|
||||
isLayoutCustomizationModeEnabledState,
|
||||
);
|
||||
const isNavigationMenuItemFolderCreating = useAtomStateValue(
|
||||
isNavigationMenuItemFolderCreatingState,
|
||||
);
|
||||
|
||||
const hasFavoritesSectionContent =
|
||||
isNavigationMenuItemFolderCreating ||
|
||||
navigationMenuItemsSorted.some((item) => !item.folderId);
|
||||
|
||||
if (!hasFavoritesSectionContent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<AnimatedEaseInOut isOpen={!isLayoutCustomizationModeEnabled} initial>
|
||||
|
||||
-65
@@ -1,65 +0,0 @@
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
import type { NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER } from '@/navigation-menu-item/common/constants/NavigationMenuItemDefaultColorFolder';
|
||||
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/states/navigationMenuItemsDraftState';
|
||||
import { computeInsertIndexAndPosition } from '@/navigation-menu-item/common/utils/computeInsertIndexAndPosition';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
|
||||
export const useAddFolderToNavigationMenuDraft = () => {
|
||||
const setNavigationMenuItemsDraft = useSetAtomState(
|
||||
navigationMenuItemsDraftState,
|
||||
);
|
||||
|
||||
const addFolderToDraft = (
|
||||
name: string,
|
||||
currentDraft: NavigationMenuItem[],
|
||||
targetFolderId?: string | null,
|
||||
targetIndex?: number,
|
||||
): string => {
|
||||
const folderId = targetFolderId ?? null;
|
||||
|
||||
const itemsInFolder = currentDraft.filter(
|
||||
(item) =>
|
||||
(item.folderId ?? null) === folderId &&
|
||||
!isDefined(item.userWorkspaceId),
|
||||
);
|
||||
const index = targetIndex ?? itemsInFolder.length;
|
||||
|
||||
const { flatIndex, position } = computeInsertIndexAndPosition(
|
||||
currentDraft,
|
||||
folderId,
|
||||
index,
|
||||
);
|
||||
|
||||
const newItemId = v4();
|
||||
const newItem: NavigationMenuItem = {
|
||||
__typename: 'NavigationMenuItem',
|
||||
id: newItemId,
|
||||
type: NavigationMenuItemType.FOLDER,
|
||||
viewId: undefined,
|
||||
targetObjectMetadataId: undefined,
|
||||
targetRecordId: undefined,
|
||||
folderId: folderId ?? undefined,
|
||||
position,
|
||||
userWorkspaceId: undefined,
|
||||
name: name.trim(),
|
||||
color: DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER,
|
||||
applicationId: undefined,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const newDraft = [
|
||||
...currentDraft.slice(0, flatIndex),
|
||||
newItem,
|
||||
...currentDraft.slice(flatIndex),
|
||||
];
|
||||
setNavigationMenuItemsDraft(newDraft);
|
||||
return newItemId;
|
||||
};
|
||||
|
||||
return { addFolderToDraft };
|
||||
};
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/states/navigationMenuItemsDraftState';
|
||||
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/common/utils/isNavigationMenuItemFolder';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
|
||||
export type UpdateFolderInDraftUpdates = {
|
||||
name?: string;
|
||||
icon?: string;
|
||||
};
|
||||
|
||||
export const useUpdateFolderInDraft = () => {
|
||||
const setNavigationMenuItemsDraft = useSetAtomState(
|
||||
navigationMenuItemsDraftState,
|
||||
);
|
||||
|
||||
const updateFolderInDraft = (
|
||||
folderId: string,
|
||||
updates: UpdateFolderInDraftUpdates,
|
||||
) => {
|
||||
setNavigationMenuItemsDraft((draft) => {
|
||||
if (!draft) return draft;
|
||||
|
||||
return draft.map((item) => {
|
||||
if (!isNavigationMenuItemFolder(item) || item.id !== folderId) {
|
||||
return item;
|
||||
}
|
||||
return { ...item, ...updates };
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return { updateFolderInDraft };
|
||||
};
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemsDraftState';
|
||||
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/states/navigationMenuItemsDraftState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
|
||||
export const useDraftNavigationMenuItems = () => {
|
||||
const { workspaceNavigationMenuItems } = useNavigationMenuItemsDraftState();
|
||||
const navigationMenuItemsDraft = useAtomStateValue(
|
||||
navigationMenuItemsDraftState,
|
||||
);
|
||||
|
||||
const currentDraft = navigationMenuItemsDraft ?? workspaceNavigationMenuItems;
|
||||
|
||||
return { currentDraft };
|
||||
};
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
import { useDraftNavigationMenuItems } from '@/navigation-menu-item/edit/hooks/useDraftNavigationMenuItems';
|
||||
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/common/utils/isNavigationMenuItemFolder';
|
||||
|
||||
export const useDraftNavigationMenuItemsAllFolders = () => {
|
||||
const { currentDraft } = useDraftNavigationMenuItems();
|
||||
|
||||
const allFolders =
|
||||
currentDraft?.filter(isNavigationMenuItemFolder).map((item) => ({
|
||||
id: item.id,
|
||||
name: item.name ?? 'Folder',
|
||||
folderId: item.folderId,
|
||||
icon: item.icon,
|
||||
color: item.color,
|
||||
})) ?? [];
|
||||
|
||||
return { allFolders };
|
||||
};
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
import { useNavigationMenuItemsByFolder } from '@/navigation-menu-item/display/folder/hooks/useNavigationMenuItemsByFolder';
|
||||
|
||||
export const useDraftNavigationMenuItemsWorkspaceFolders = () => {
|
||||
const { workspaceNavigationMenuItemsByFolder } =
|
||||
useNavigationMenuItemsByFolder();
|
||||
|
||||
const workspaceFolders = workspaceNavigationMenuItemsByFolder.map(
|
||||
(folder) => ({
|
||||
id: folder.id,
|
||||
name: folder.folderName,
|
||||
icon: folder.icon,
|
||||
color: folder.color,
|
||||
}),
|
||||
);
|
||||
|
||||
return { workspaceFolders };
|
||||
};
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { type NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
import { useCreateManyNavigationMenuItems } from '@/navigation-menu-item/common/hooks/useCreateManyNavigationMenuItems';
|
||||
import { useDeleteManyNavigationMenuItems } from '@/navigation-menu-item/common/hooks/useDeleteManyNavigationMenuItems';
|
||||
import { useUpdateManyNavigationMenuItems } from '@/navigation-menu-item/common/hooks/useUpdateManyNavigationMenuItems';
|
||||
import { navigationMenuItemEditSectionState } from '@/navigation-menu-item/common/states/navigationMenuItemEditSectionState';
|
||||
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/states/navigationMenuItemsDraftState';
|
||||
import { buildCreateNavigationMenuItemInput } from '@/navigation-menu-item/common/utils/buildCreateNavigationMenuItemInput';
|
||||
import { computeInsertIndexAndPosition } from '@/navigation-menu-item/common/utils/computeInsertIndexAndPosition';
|
||||
import { useNavigationMenuItemsData } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemsData';
|
||||
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';
|
||||
|
||||
export type NewNavigationMenuItemInput = {
|
||||
type: NavigationMenuItemType;
|
||||
targetObjectMetadataId?: string | null;
|
||||
viewId?: string | null;
|
||||
targetRecordId?: string | null;
|
||||
targetRecordIdentifier?: NavigationMenuItem['targetRecordIdentifier'];
|
||||
name?: string | null;
|
||||
link?: string | null;
|
||||
color?: string | null;
|
||||
};
|
||||
|
||||
type CreateItemOptions = {
|
||||
targetFolderId?: string | null;
|
||||
targetIndex?: number;
|
||||
};
|
||||
|
||||
// The single create/update/delete API for the add/edit side panel. It forks on
|
||||
// the active section (mirroring useHandleNavigationMenuItemDragAndDrop): the
|
||||
// workspace section stages changes in the draft (saved on layout exit), while
|
||||
// the favorite section persists personal items immediately and optimistically.
|
||||
// On the immediate path the mutation hooks roll back on failure, so errors are
|
||||
// surfaced here and the rejection is swallowed — callers fire-and-forget.
|
||||
export const useNavigationMenuItemEditController = () => {
|
||||
const { t } = useLingui();
|
||||
const { enqueueErrorSnackBar } = useSnackBar();
|
||||
const navigationMenuItemEditSection = useAtomStateValue(
|
||||
navigationMenuItemEditSectionState,
|
||||
);
|
||||
const {
|
||||
navigationMenuItems,
|
||||
workspaceNavigationMenuItems,
|
||||
currentWorkspaceMemberId,
|
||||
} = useNavigationMenuItemsData();
|
||||
const setNavigationMenuItemsDraft = useSetAtomState(
|
||||
navigationMenuItemsDraftState,
|
||||
);
|
||||
const { createManyNavigationMenuItems } = useCreateManyNavigationMenuItems();
|
||||
const { updateManyNavigationMenuItems } = useUpdateManyNavigationMenuItems();
|
||||
const { deleteManyNavigationMenuItems } = useDeleteManyNavigationMenuItems();
|
||||
|
||||
const isDraftMode = navigationMenuItemEditSection === 'workspace';
|
||||
const currentItems = isDraftMode
|
||||
? workspaceNavigationMenuItems
|
||||
: navigationMenuItems;
|
||||
const targetUserWorkspaceId = isDraftMode
|
||||
? undefined
|
||||
: currentWorkspaceMemberId;
|
||||
|
||||
const createItem = (
|
||||
input: NewNavigationMenuItemInput,
|
||||
{ targetFolderId, targetIndex }: CreateItemOptions = {},
|
||||
): string => {
|
||||
const folderId = targetFolderId ?? null;
|
||||
const itemsInFolder = currentItems.filter(
|
||||
(item) => (item.folderId ?? null) === folderId,
|
||||
);
|
||||
const index = targetIndex ?? itemsInFolder.length;
|
||||
const { flatIndex, position } = computeInsertIndexAndPosition(
|
||||
currentItems,
|
||||
folderId,
|
||||
index,
|
||||
);
|
||||
|
||||
const id = v4();
|
||||
const newItem: NavigationMenuItem = {
|
||||
__typename: 'NavigationMenuItem',
|
||||
id,
|
||||
type: input.type,
|
||||
position,
|
||||
userWorkspaceId: targetUserWorkspaceId,
|
||||
folderId: folderId ?? undefined,
|
||||
targetObjectMetadataId: input.targetObjectMetadataId ?? undefined,
|
||||
targetRecordId: input.targetRecordId ?? undefined,
|
||||
targetRecordIdentifier: input.targetRecordIdentifier,
|
||||
viewId: input.viewId ?? undefined,
|
||||
name: input.name ?? undefined,
|
||||
link: input.link ?? undefined,
|
||||
color: input.color ?? undefined,
|
||||
applicationId: undefined,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
if (isDraftMode) {
|
||||
setNavigationMenuItemsDraft((draft) => {
|
||||
const current = draft ?? currentItems;
|
||||
return [
|
||||
...current.slice(0, flatIndex),
|
||||
newItem,
|
||||
...current.slice(flatIndex),
|
||||
];
|
||||
});
|
||||
return id;
|
||||
}
|
||||
|
||||
if (isDefined(targetUserWorkspaceId)) {
|
||||
void createManyNavigationMenuItems([
|
||||
{
|
||||
...buildCreateNavigationMenuItemInput(newItem, (value) => value),
|
||||
userWorkspaceId: targetUserWorkspaceId,
|
||||
},
|
||||
]).catch(() =>
|
||||
enqueueErrorSnackBar({ message: t`Couldn't add to favorites` }),
|
||||
);
|
||||
} else {
|
||||
enqueueErrorSnackBar({ message: t`Couldn't add to favorites` });
|
||||
}
|
||||
|
||||
return id;
|
||||
};
|
||||
|
||||
const updateItem = async (
|
||||
id: string,
|
||||
update: Partial<NavigationMenuItem>,
|
||||
): Promise<void> => {
|
||||
if (isDraftMode) {
|
||||
setNavigationMenuItemsDraft((draft) =>
|
||||
isDefined(draft)
|
||||
? draft.map((item) =>
|
||||
item.id === id ? { ...item, ...update } : item,
|
||||
)
|
||||
: draft,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await updateManyNavigationMenuItems([{ id, update }]);
|
||||
} catch {
|
||||
enqueueErrorSnackBar({ message: t`Couldn't update favorite` });
|
||||
}
|
||||
};
|
||||
|
||||
const deleteItems = async (ids: string[]): Promise<void> => {
|
||||
if (isDraftMode) {
|
||||
setNavigationMenuItemsDraft((draft) =>
|
||||
isDefined(draft)
|
||||
? draft.filter((item) => !ids.includes(item.id))
|
||||
: draft,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await deleteManyNavigationMenuItems(ids);
|
||||
} catch {
|
||||
enqueueErrorSnackBar({ message: t`Couldn't remove favorite` });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
currentItems,
|
||||
isDraftMode,
|
||||
targetUserWorkspaceId,
|
||||
createItem,
|
||||
updateItem,
|
||||
deleteItems,
|
||||
};
|
||||
};
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
import { navigationMenuItemEditSectionState } from '@/navigation-menu-item/common/states/navigationMenuItemEditSectionState';
|
||||
import { flattenNavigationMenuItemsWithFolderChildren } from '@/navigation-menu-item/common/utils/flattenNavigationMenuItemsWithFolderChildren';
|
||||
import { useNavigationMenuItemsByFolder } from '@/navigation-menu-item/display/folder/hooks/useNavigationMenuItemsByFolder';
|
||||
import { useNavigationMenuItemSectionItems } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemSectionItems';
|
||||
import { useSortedNavigationMenuItems } from '@/navigation-menu-item/display/hooks/useSortedNavigationMenuItems';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
|
||||
// Flat ordered item list (orphans + folders followed by their children) for the
|
||||
// section the add/edit panel is operating on. The edit page resolves the
|
||||
// selected item and computes move/organize bounds against this list, so it must
|
||||
// return personal items in the favorite section and workspace items otherwise.
|
||||
export const useNavigationMenuItemEditSectionItems =
|
||||
(): NavigationMenuItem[] => {
|
||||
const navigationMenuItemEditSection = useAtomStateValue(
|
||||
navigationMenuItemEditSectionState,
|
||||
);
|
||||
const workspaceSectionItems = useNavigationMenuItemSectionItems();
|
||||
const { navigationMenuItemsSorted } = useSortedNavigationMenuItems();
|
||||
const { userNavigationMenuItemsByFolder } =
|
||||
useNavigationMenuItemsByFolder();
|
||||
|
||||
if (navigationMenuItemEditSection === 'workspace') {
|
||||
return workspaceSectionItems;
|
||||
}
|
||||
|
||||
const topLevelItems = navigationMenuItemsSorted.filter(
|
||||
(item) => !item.folderId,
|
||||
);
|
||||
|
||||
return flattenNavigationMenuItemsWithFolderChildren(
|
||||
topLevelItems,
|
||||
userNavigationMenuItemsByFolder,
|
||||
);
|
||||
};
|
||||
+108
-156
@@ -1,184 +1,136 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/states/navigationMenuItemsDraftState';
|
||||
import { getPositionBetween } from '@/navigation-menu-item/common/utils/getPositionBetween';
|
||||
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/common/utils/isNavigationMenuItemFolder';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
|
||||
import { useNavigationMenuItemSectionItems } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemSectionItems';
|
||||
import { useNavigationMenuItemEditController } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemEditController';
|
||||
import { useNavigationMenuItemEditSectionItems } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemEditSectionItems';
|
||||
|
||||
export const useNavigationMenuItemMoveRemove = () => {
|
||||
const setNavigationMenuItemsDraft = useSetAtomState(
|
||||
navigationMenuItemsDraftState,
|
||||
);
|
||||
const items = useNavigationMenuItemSectionItems();
|
||||
const { currentItems, updateItem, deleteItems } =
|
||||
useNavigationMenuItemEditController();
|
||||
const items = useNavigationMenuItemEditSectionItems();
|
||||
const visibleItemIds = new Set(items.map((item) => item.id));
|
||||
|
||||
const moveUp = (navigationMenuItemId: string) => {
|
||||
setNavigationMenuItemsDraft((draft) => {
|
||||
if (!draft) {
|
||||
return draft;
|
||||
}
|
||||
const getSortedSiblings = (navigationMenuItemId: string) => {
|
||||
const currentItem = currentItems.find(
|
||||
(item) => item.id === navigationMenuItemId,
|
||||
);
|
||||
if (!isDefined(currentItem)) {
|
||||
return null;
|
||||
}
|
||||
const folderId = currentItem.folderId ?? null;
|
||||
return currentItems
|
||||
.filter(
|
||||
(item) =>
|
||||
(item.folderId ?? null) === folderId && visibleItemIds.has(item.id),
|
||||
)
|
||||
.sort((a, b) => a.position - b.position);
|
||||
};
|
||||
|
||||
const currentItem = draft.find(
|
||||
(item) => item.id === navigationMenuItemId,
|
||||
);
|
||||
if (!currentItem) {
|
||||
return draft;
|
||||
}
|
||||
|
||||
const folderId = currentItem.folderId ?? null;
|
||||
const siblings = draft
|
||||
.filter(
|
||||
(item) =>
|
||||
(item.folderId ?? null) === folderId && visibleItemIds.has(item.id),
|
||||
)
|
||||
.sort((a, b) => a.position - b.position);
|
||||
|
||||
const currentIndex = siblings.findIndex(
|
||||
(item) => item.id === navigationMenuItemId,
|
||||
);
|
||||
if (currentIndex <= 0) {
|
||||
return draft;
|
||||
}
|
||||
|
||||
const prev = siblings[currentIndex - 1];
|
||||
const prevPrev = siblings[currentIndex - 2];
|
||||
const newPosition = getPositionBetween(prevPrev?.position, prev.position);
|
||||
|
||||
return draft.map((item) =>
|
||||
item.id === navigationMenuItemId
|
||||
? { ...item, position: newPosition }
|
||||
: item,
|
||||
);
|
||||
const moveUp = async (navigationMenuItemId: string) => {
|
||||
const siblings = getSortedSiblings(navigationMenuItemId);
|
||||
if (!isDefined(siblings)) {
|
||||
return;
|
||||
}
|
||||
const currentIndex = siblings.findIndex(
|
||||
(item) => item.id === navigationMenuItemId,
|
||||
);
|
||||
if (currentIndex <= 0) {
|
||||
return;
|
||||
}
|
||||
const prev = siblings[currentIndex - 1];
|
||||
const prevPrev = siblings[currentIndex - 2];
|
||||
await updateItem(navigationMenuItemId, {
|
||||
position: getPositionBetween(prevPrev?.position, prev.position),
|
||||
});
|
||||
};
|
||||
|
||||
const moveDown = (navigationMenuItemId: string) => {
|
||||
setNavigationMenuItemsDraft((draft) => {
|
||||
if (!draft) {
|
||||
return draft;
|
||||
}
|
||||
|
||||
const currentItem = draft.find(
|
||||
(item) => item.id === navigationMenuItemId,
|
||||
);
|
||||
if (!currentItem) {
|
||||
return draft;
|
||||
}
|
||||
|
||||
const folderId = currentItem.folderId ?? null;
|
||||
const siblings = draft
|
||||
.filter(
|
||||
(item) =>
|
||||
(item.folderId ?? null) === folderId && visibleItemIds.has(item.id),
|
||||
)
|
||||
.sort((a, b) => a.position - b.position);
|
||||
|
||||
const currentIndex = siblings.findIndex(
|
||||
(item) => item.id === navigationMenuItemId,
|
||||
);
|
||||
if (currentIndex < 0 || currentIndex >= siblings.length - 1) {
|
||||
return draft;
|
||||
}
|
||||
|
||||
const next = siblings[currentIndex + 1];
|
||||
const nextNext = siblings[currentIndex + 2];
|
||||
const newPosition = getPositionBetween(next.position, nextNext?.position);
|
||||
|
||||
return draft.map((item) =>
|
||||
item.id === navigationMenuItemId
|
||||
? { ...item, position: newPosition }
|
||||
: item,
|
||||
);
|
||||
const moveDown = async (navigationMenuItemId: string) => {
|
||||
const siblings = getSortedSiblings(navigationMenuItemId);
|
||||
if (!isDefined(siblings)) {
|
||||
return;
|
||||
}
|
||||
const currentIndex = siblings.findIndex(
|
||||
(item) => item.id === navigationMenuItemId,
|
||||
);
|
||||
if (currentIndex < 0 || currentIndex >= siblings.length - 1) {
|
||||
return;
|
||||
}
|
||||
const next = siblings[currentIndex + 1];
|
||||
const nextNext = siblings[currentIndex + 2];
|
||||
await updateItem(navigationMenuItemId, {
|
||||
position: getPositionBetween(next.position, nextNext?.position),
|
||||
});
|
||||
};
|
||||
|
||||
const remove = (navigationMenuItemId: string) => {
|
||||
setNavigationMenuItemsDraft((draft) => {
|
||||
if (!draft) {
|
||||
return draft;
|
||||
}
|
||||
const remove = async (navigationMenuItemId: string) => {
|
||||
const itemToRemove = currentItems.find(
|
||||
(item) => item.id === navigationMenuItemId,
|
||||
);
|
||||
if (!isDefined(itemToRemove)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const itemToRemove = draft.find(
|
||||
(item) => item.id === navigationMenuItemId,
|
||||
);
|
||||
if (!itemToRemove) {
|
||||
return draft;
|
||||
}
|
||||
if (isNavigationMenuItemFolder(itemToRemove)) {
|
||||
const childIds = currentItems
|
||||
.filter((item) => item.folderId === navigationMenuItemId)
|
||||
.map((item) => item.id);
|
||||
await deleteItems([navigationMenuItemId, ...childIds]);
|
||||
return;
|
||||
}
|
||||
|
||||
const isFolder = isNavigationMenuItemFolder(itemToRemove);
|
||||
|
||||
if (isFolder) {
|
||||
return draft.filter(
|
||||
(item) =>
|
||||
item.id !== navigationMenuItemId &&
|
||||
item.folderId !== navigationMenuItemId,
|
||||
);
|
||||
}
|
||||
|
||||
return draft.filter((item) => item.id !== navigationMenuItemId);
|
||||
});
|
||||
await deleteItems([navigationMenuItemId]);
|
||||
};
|
||||
|
||||
const moveToFolder = (
|
||||
const moveToFolder = async (
|
||||
navigationMenuItemId: string,
|
||||
targetFolderId: string | null,
|
||||
) => {
|
||||
setNavigationMenuItemsDraft((draft) => {
|
||||
if (!draft) {
|
||||
return draft;
|
||||
const itemToMove = currentItems.find(
|
||||
(item) => item.id === navigationMenuItemId,
|
||||
);
|
||||
if (!isDefined(itemToMove)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isFolder = isNavigationMenuItemFolder(itemToMove);
|
||||
if (isFolder && targetFolderId === navigationMenuItemId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Block moving a folder into one of its own descendants (would orphan the subtree).
|
||||
if (isFolder && isDefined(targetFolderId)) {
|
||||
const descendantFolderIds = new Set<string>();
|
||||
const collectDescendants = (folderId: string) => {
|
||||
currentItems
|
||||
.filter(
|
||||
(item) =>
|
||||
isNavigationMenuItemFolder(item) && item.folderId === folderId,
|
||||
)
|
||||
.forEach((item) => {
|
||||
descendantFolderIds.add(item.id);
|
||||
collectDescendants(item.id);
|
||||
});
|
||||
};
|
||||
collectDescendants(navigationMenuItemId);
|
||||
if (descendantFolderIds.has(targetFolderId)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const itemToMove = draft.find((item) => item.id === navigationMenuItemId);
|
||||
if (!itemToMove) {
|
||||
return draft;
|
||||
}
|
||||
const itemsInTargetFolder = currentItems.filter((item) =>
|
||||
targetFolderId === null
|
||||
? !isDefined(item.folderId)
|
||||
: item.folderId === targetFolderId,
|
||||
);
|
||||
const maxPositionInTarget =
|
||||
itemsInTargetFolder.length > 0
|
||||
? Math.max(...itemsInTargetFolder.map((item) => item.position))
|
||||
: -1;
|
||||
|
||||
const isFolder = isNavigationMenuItemFolder(itemToMove);
|
||||
if (isFolder && targetFolderId === navigationMenuItemId) {
|
||||
return draft;
|
||||
}
|
||||
|
||||
if (isFolder && isDefined(targetFolderId)) {
|
||||
const descendantFolderIds = new Set<string>();
|
||||
const collectDescendants = (folderId: string) => {
|
||||
draft
|
||||
?.filter(
|
||||
(item) =>
|
||||
isNavigationMenuItemFolder(item) && item.folderId === folderId,
|
||||
)
|
||||
.forEach((item) => {
|
||||
descendantFolderIds.add(item.id);
|
||||
collectDescendants(item.id);
|
||||
});
|
||||
};
|
||||
collectDescendants(navigationMenuItemId);
|
||||
if (descendantFolderIds.has(targetFolderId)) {
|
||||
return draft;
|
||||
}
|
||||
}
|
||||
|
||||
const itemsInTargetFolder = draft.filter((item) =>
|
||||
targetFolderId === null
|
||||
? !isDefined(item.folderId)
|
||||
: item.folderId === targetFolderId,
|
||||
);
|
||||
const maxPositionInTarget =
|
||||
itemsInTargetFolder.length > 0
|
||||
? Math.max(...itemsInTargetFolder.map((item) => item.position))
|
||||
: -1;
|
||||
const newPosition = maxPositionInTarget + 1;
|
||||
|
||||
return draft.map((item) =>
|
||||
item.id === navigationMenuItemId
|
||||
? {
|
||||
...item,
|
||||
folderId: targetFolderId ?? undefined,
|
||||
position: newPosition,
|
||||
}
|
||||
: item,
|
||||
);
|
||||
await updateItem(navigationMenuItemId, {
|
||||
folderId: targetFolderId ?? null,
|
||||
position: maxPositionInTarget + 1,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/common/utils/isNavigationMenuItemFolder';
|
||||
import { useNavigationMenuItemEditController } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemEditController';
|
||||
|
||||
export const useNavigationMenuItemSectionAllFolders = () => {
|
||||
const { currentItems } = useNavigationMenuItemEditController();
|
||||
|
||||
const allFolders = currentItems
|
||||
.filter(isNavigationMenuItemFolder)
|
||||
.map((item) => ({
|
||||
id: item.id,
|
||||
name: item.name ?? 'Folder',
|
||||
folderId: item.folderId,
|
||||
icon: item.icon,
|
||||
color: item.color,
|
||||
}));
|
||||
|
||||
return { allFolders };
|
||||
};
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import { navigationMenuItemEditSectionState } from '@/navigation-menu-item/common/states/navigationMenuItemEditSectionState';
|
||||
import { useNavigationMenuItemsByFolder } from '@/navigation-menu-item/display/folder/hooks/useNavigationMenuItemsByFolder';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
|
||||
export const useNavigationMenuItemSectionFolders = () => {
|
||||
const navigationMenuItemEditSection = useAtomStateValue(
|
||||
navigationMenuItemEditSectionState,
|
||||
);
|
||||
const {
|
||||
workspaceNavigationMenuItemsByFolder,
|
||||
userNavigationMenuItemsByFolder,
|
||||
} = useNavigationMenuItemsByFolder();
|
||||
|
||||
const folders =
|
||||
navigationMenuItemEditSection === 'workspace'
|
||||
? workspaceNavigationMenuItemsByFolder
|
||||
: userNavigationMenuItemsByFolder;
|
||||
|
||||
const sectionFolders = folders.map((folder) => ({
|
||||
id: folder.id,
|
||||
name: folder.folderName,
|
||||
icon: folder.icon,
|
||||
color: folder.color,
|
||||
}));
|
||||
|
||||
return { sectionFolders };
|
||||
};
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { navigationMenuItemEditSectionState } from '@/navigation-menu-item/common/states/navigationMenuItemEditSectionState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
|
||||
type UseNavigationMenuItemTitleEditParams = {
|
||||
itemId: string | null;
|
||||
itemName: string;
|
||||
defaultLabel: string;
|
||||
persistName: (name: string) => void;
|
||||
};
|
||||
|
||||
// Title editing for a navigation menu item, section-aware: the workspace
|
||||
// section writes the in-memory draft on every keystroke (live preview), while
|
||||
// a personal favorite persists immediately, so keystrokes are buffered locally
|
||||
// (keyed by item id, self-invalidating on item switch) and committed on save.
|
||||
export const useNavigationMenuItemTitleEdit = ({
|
||||
itemId,
|
||||
itemName,
|
||||
defaultLabel,
|
||||
persistName,
|
||||
}: UseNavigationMenuItemTitleEditParams) => {
|
||||
const navigationMenuItemEditSection = useAtomStateValue(
|
||||
navigationMenuItemEditSectionState,
|
||||
);
|
||||
const isDraftMode = navigationMenuItemEditSection === 'workspace';
|
||||
const [localNameEdit, setLocalNameEdit] = useState<{
|
||||
itemId: string;
|
||||
text: string;
|
||||
} | null>(null);
|
||||
|
||||
const bufferedName =
|
||||
localNameEdit?.itemId === itemId ? localNameEdit.text : null;
|
||||
const value = isDraftMode ? itemName : (bufferedName ?? itemName);
|
||||
|
||||
const handleChange = (text: string) => {
|
||||
if (isDraftMode) {
|
||||
persistName(text);
|
||||
} else if (itemId !== null) {
|
||||
setLocalNameEdit({ itemId, text });
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
const trimmed = value.trim();
|
||||
const finalName = trimmed.length > 0 ? trimmed : defaultLabel;
|
||||
if (finalName !== itemName) {
|
||||
persistName(finalName);
|
||||
}
|
||||
if (!isDraftMode) {
|
||||
setLocalNameEdit(null);
|
||||
}
|
||||
};
|
||||
|
||||
return { value, handleChange, handleSave };
|
||||
};
|
||||
+11
-9
@@ -6,18 +6,20 @@ import { viewsSelector } from '@/views/states/selectors/viewsSelector';
|
||||
import { ViewKey } from '@/views/types/ViewKey';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
type NavigationMenuItemDraft = Pick<
|
||||
type NavigationMenuItemForDedup = Pick<
|
||||
NavigationMenuItem,
|
||||
'id' | 'type' | 'viewId' | 'targetObjectMetadataId'
|
||||
>;
|
||||
|
||||
export const useNavigationMenuObjectMetadataFromDraft = (
|
||||
currentDraft: NavigationMenuItemDraft[],
|
||||
// Derives which objects/views are already present in the given section's items,
|
||||
// so the add pickers can hide or disable what's already there.
|
||||
export const useNavigationMenuObjectMetadataForSection = (
|
||||
sectionItems: NavigationMenuItemForDedup[],
|
||||
) => {
|
||||
const views = useAtomStateValue(viewsSelector);
|
||||
|
||||
const objectMetadataIdsInWorkspace =
|
||||
getObjectMetadataIdsInDraft(currentDraft);
|
||||
const objectMetadataIdsAlreadyAdded =
|
||||
getObjectMetadataIdsInDraft(sectionItems);
|
||||
|
||||
const objectMetadataIdsWithIndexView = new Set(
|
||||
views
|
||||
@@ -29,17 +31,17 @@ export const useNavigationMenuObjectMetadataFromDraft = (
|
||||
views.map((view) => view.objectMetadataId),
|
||||
);
|
||||
|
||||
const viewIdsInWorkspace = new Set(
|
||||
currentDraft.flatMap((item) =>
|
||||
const viewIdsAlreadyAdded = new Set(
|
||||
sectionItems.flatMap((item) =>
|
||||
isDefined(item.viewId) ? [item.viewId] : [],
|
||||
),
|
||||
);
|
||||
|
||||
return {
|
||||
views,
|
||||
objectMetadataIdsInWorkspace,
|
||||
objectMetadataIdsAlreadyAdded,
|
||||
objectMetadataIdsWithIndexView,
|
||||
objectMetadataIdsWithAnyView,
|
||||
viewIdsInWorkspace,
|
||||
viewIdsAlreadyAdded,
|
||||
};
|
||||
};
|
||||
+2
-2
@@ -1,12 +1,12 @@
|
||||
import { useNavigationMenuItemSectionItems } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemSectionItems';
|
||||
import { selectedNavigationMenuItemIdInEditModeState } from '@/navigation-menu-item/common/states/selectedNavigationMenuItemIdInEditModeState';
|
||||
import { useNavigationMenuItemEditSectionItems } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemEditSectionItems';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
|
||||
export const useSelectedNavigationMenuItemEditItem = () => {
|
||||
const selectedNavigationMenuItemIdInEditMode = useAtomStateValue(
|
||||
selectedNavigationMenuItemIdInEditModeState,
|
||||
);
|
||||
const items = useNavigationMenuItemSectionItems();
|
||||
const items = useNavigationMenuItemEditSectionItems();
|
||||
|
||||
const selectedItem = selectedNavigationMenuItemIdInEditMode
|
||||
? items.find((item) => item.id === selectedNavigationMenuItemIdInEditMode)
|
||||
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/states/navigationMenuItemsDraftState';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
|
||||
export type UpdateNavigationMenuItemInDraftUpdates = {
|
||||
color?: string;
|
||||
name?: string;
|
||||
icon?: string;
|
||||
link?: string;
|
||||
};
|
||||
|
||||
export const useUpdateNavigationMenuItemInDraft = () => {
|
||||
const setNavigationMenuItemsDraft = useSetAtomState(
|
||||
navigationMenuItemsDraftState,
|
||||
);
|
||||
|
||||
const updateNavigationMenuItemInDraft = (
|
||||
navigationMenuItemId: string,
|
||||
updates: UpdateNavigationMenuItemInDraftUpdates,
|
||||
) => {
|
||||
setNavigationMenuItemsDraft((draft) => {
|
||||
if (!draft) return draft;
|
||||
|
||||
return draft.map((item) =>
|
||||
item.id === navigationMenuItemId ? { ...item, ...updates } : item,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
return { updateNavigationMenuItemInDraft };
|
||||
};
|
||||
-68
@@ -1,68 +0,0 @@
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { isDefined, normalizeUrl } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
import type { NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_LINK } from '@/navigation-menu-item/common/constants/NavigationMenuItemDefaultColorLink';
|
||||
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/states/navigationMenuItemsDraftState';
|
||||
import { computeInsertIndexAndPosition } from '@/navigation-menu-item/common/utils/computeInsertIndexAndPosition';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
|
||||
export const useAddLinkToNavigationMenuDraft = () => {
|
||||
const setNavigationMenuItemsDraft = useSetAtomState(
|
||||
navigationMenuItemsDraftState,
|
||||
);
|
||||
|
||||
const addLinkToDraft = (
|
||||
label: string,
|
||||
url: string,
|
||||
currentDraft: NavigationMenuItem[],
|
||||
targetFolderId?: string | null,
|
||||
targetIndex?: number,
|
||||
): string => {
|
||||
const normalizedUrl = normalizeUrl(url);
|
||||
const folderId = targetFolderId ?? null;
|
||||
|
||||
const itemsInFolder = currentDraft.filter(
|
||||
(item) =>
|
||||
(item.folderId ?? null) === folderId &&
|
||||
!isDefined(item.userWorkspaceId),
|
||||
);
|
||||
const index = targetIndex ?? itemsInFolder.length;
|
||||
|
||||
const { flatIndex, position } = computeInsertIndexAndPosition(
|
||||
currentDraft,
|
||||
folderId,
|
||||
index,
|
||||
);
|
||||
|
||||
const newItemId = v4();
|
||||
const newItem: NavigationMenuItem = {
|
||||
__typename: 'NavigationMenuItem',
|
||||
id: newItemId,
|
||||
type: NavigationMenuItemType.LINK,
|
||||
viewId: undefined,
|
||||
targetObjectMetadataId: undefined,
|
||||
targetRecordId: undefined,
|
||||
folderId: folderId ?? undefined,
|
||||
position,
|
||||
userWorkspaceId: undefined,
|
||||
name: label.trim() || 'Link',
|
||||
link: normalizedUrl,
|
||||
color: DEFAULT_NAVIGATION_MENU_ITEM_COLOR_LINK,
|
||||
applicationId: undefined,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const newDraft = [
|
||||
...currentDraft.slice(0, flatIndex),
|
||||
newItem,
|
||||
...currentDraft.slice(flatIndex),
|
||||
];
|
||||
setNavigationMenuItemsDraft(newDraft);
|
||||
return newItemId;
|
||||
};
|
||||
|
||||
return { addLinkToDraft };
|
||||
};
|
||||
-33
@@ -1,33 +0,0 @@
|
||||
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/states/navigationMenuItemsDraftState';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import { isNavigationMenuItemLink } from '@/navigation-menu-item/common/utils/isNavigationMenuItemLink';
|
||||
|
||||
export const useUpdateLinkInDraft = () => {
|
||||
const setNavigationMenuItemsDraft = useSetAtomState(
|
||||
navigationMenuItemsDraftState,
|
||||
);
|
||||
|
||||
const updateLinkInDraft = (
|
||||
linkId: string,
|
||||
updates: { name?: string; link?: string },
|
||||
) => {
|
||||
setNavigationMenuItemsDraft((draft) => {
|
||||
if (!draft) return draft;
|
||||
|
||||
return draft.map((item) => {
|
||||
if (item.id !== linkId || !isNavigationMenuItemLink(item)) return item;
|
||||
|
||||
const updated = { ...item };
|
||||
if (updates.name !== undefined) {
|
||||
updated.name = updates.name.trim() || 'Link';
|
||||
}
|
||||
if (updates.link !== undefined && updates.link.trim() !== '') {
|
||||
updated.link = updates.link.trim();
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return { updateLinkInDraft };
|
||||
};
|
||||
-73
@@ -1,73 +0,0 @@
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
import type { NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/states/navigationMenuItemsDraftState';
|
||||
import { computeInsertIndexAndPosition } from '@/navigation-menu-item/common/utils/computeInsertIndexAndPosition';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
|
||||
export type AddObjectToNavigationMenuDraftParams = {
|
||||
objectMetadataId: string;
|
||||
currentDraft: NavigationMenuItem[];
|
||||
targetFolderId?: string | null;
|
||||
targetIndex?: number;
|
||||
color?: string | null;
|
||||
};
|
||||
|
||||
export const useAddObjectToNavigationMenuDraft = () => {
|
||||
const setNavigationMenuItemsDraft = useSetAtomState(
|
||||
navigationMenuItemsDraftState,
|
||||
);
|
||||
|
||||
const addObjectToDraft = ({
|
||||
objectMetadataId,
|
||||
currentDraft,
|
||||
targetFolderId,
|
||||
targetIndex,
|
||||
color,
|
||||
}: AddObjectToNavigationMenuDraftParams): string => {
|
||||
const folderId = targetFolderId ?? null;
|
||||
|
||||
const itemsInFolder = currentDraft.filter(
|
||||
(item) =>
|
||||
(item.folderId ?? null) === folderId &&
|
||||
!isDefined(item.userWorkspaceId),
|
||||
);
|
||||
const index = targetIndex ?? itemsInFolder.length;
|
||||
|
||||
const { flatIndex, position } = computeInsertIndexAndPosition(
|
||||
currentDraft,
|
||||
folderId,
|
||||
index,
|
||||
);
|
||||
|
||||
const newItemId = v4();
|
||||
const newItem: NavigationMenuItem = {
|
||||
__typename: 'NavigationMenuItem',
|
||||
id: newItemId,
|
||||
type: NavigationMenuItemType.OBJECT,
|
||||
viewId: undefined,
|
||||
targetObjectMetadataId: objectMetadataId,
|
||||
position,
|
||||
userWorkspaceId: undefined,
|
||||
targetRecordId: undefined,
|
||||
folderId: folderId ?? undefined,
|
||||
name: undefined,
|
||||
applicationId: undefined,
|
||||
color,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const newDraft = [
|
||||
...currentDraft.slice(0, flatIndex),
|
||||
newItem,
|
||||
...currentDraft.slice(flatIndex),
|
||||
];
|
||||
setNavigationMenuItemsDraft(newDraft);
|
||||
return newItemId;
|
||||
};
|
||||
|
||||
return { addObjectToDraft };
|
||||
};
|
||||
-92
@@ -1,92 +0,0 @@
|
||||
import { v4 } from 'uuid';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import type { NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/states/navigationMenuItemsDraftState';
|
||||
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { computeInsertIndexAndPosition } from '@/navigation-menu-item/common/utils/computeInsertIndexAndPosition';
|
||||
|
||||
type SearchRecord = {
|
||||
recordId: string;
|
||||
objectNameSingular: string;
|
||||
label: string;
|
||||
imageUrl?: string | null;
|
||||
};
|
||||
|
||||
type SearchRecordWithOptionalMetadataId = SearchRecord & {
|
||||
objectMetadataId?: string;
|
||||
};
|
||||
|
||||
export const useAddRecordToNavigationMenuDraft = () => {
|
||||
const setNavigationMenuItemsDraft = useSetAtomState(
|
||||
navigationMenuItemsDraftState,
|
||||
);
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
|
||||
|
||||
const addRecordToDraft = (
|
||||
searchRecord: SearchRecordWithOptionalMetadataId,
|
||||
currentDraft: NavigationMenuItem[],
|
||||
targetFolderId?: string | null,
|
||||
targetIndex?: number,
|
||||
): string | undefined => {
|
||||
const objectMetadataId =
|
||||
searchRecord.objectMetadataId ??
|
||||
objectMetadataItems.find(
|
||||
(item) => item.nameSingular === searchRecord.objectNameSingular,
|
||||
)?.id;
|
||||
|
||||
if (!isDefined(objectMetadataId)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const folderId = targetFolderId ?? null;
|
||||
|
||||
const itemsInFolder = currentDraft.filter(
|
||||
(item) =>
|
||||
(item.folderId ?? null) === folderId &&
|
||||
!isDefined(item.userWorkspaceId),
|
||||
);
|
||||
const index = targetIndex ?? itemsInFolder.length;
|
||||
|
||||
const { flatIndex, position } = computeInsertIndexAndPosition(
|
||||
currentDraft,
|
||||
folderId,
|
||||
index,
|
||||
);
|
||||
|
||||
const newItemId = v4();
|
||||
const newItem: NavigationMenuItem = {
|
||||
__typename: 'NavigationMenuItem',
|
||||
id: newItemId,
|
||||
type: NavigationMenuItemType.RECORD,
|
||||
viewId: undefined,
|
||||
targetObjectMetadataId: objectMetadataId,
|
||||
targetRecordId: searchRecord.recordId,
|
||||
targetRecordIdentifier: {
|
||||
id: searchRecord.recordId,
|
||||
labelIdentifier: searchRecord.label,
|
||||
imageIdentifier: searchRecord.imageUrl ?? null,
|
||||
},
|
||||
position,
|
||||
userWorkspaceId: undefined,
|
||||
folderId: folderId ?? undefined,
|
||||
name: undefined,
|
||||
applicationId: undefined,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const newDraft = [
|
||||
...currentDraft.slice(0, flatIndex),
|
||||
newItem,
|
||||
...currentDraft.slice(flatIndex),
|
||||
];
|
||||
setNavigationMenuItemsDraft(newDraft);
|
||||
return newItemId;
|
||||
};
|
||||
|
||||
return { addRecordToDraft };
|
||||
};
|
||||
+6
-7
@@ -2,8 +2,8 @@ import { CommandMenuItemDropdown } from '@/command-menu/components/CommandMenuIt
|
||||
import { useUpdateMetadataStoreDraft } from '@/metadata-store/hooks/useUpdateMetadataStoreDraft';
|
||||
import { type FlatObjectMetadataItem } from '@/metadata-store/types/FlatObjectMetadataItem';
|
||||
import { isValidObjectNavigationMenuItem } from '@/navigation-menu-item/common/utils/isValidObjectNavigationMenuItem';
|
||||
import { useNavigationMenuItemEditController } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemEditController';
|
||||
import { useSelectedNavigationMenuItemEditItem } from '@/navigation-menu-item/edit/hooks/useSelectedNavigationMenuItemEditItem';
|
||||
import { useUpdateNavigationMenuItemInDraft } from '@/navigation-menu-item/edit/hooks/useUpdateNavigationMenuItemInDraft';
|
||||
import { ThemeColorPickerMenu } from '@/ui/input/components/ThemeColorPickerMenu';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
@@ -32,8 +32,7 @@ export const SidePanelEditColorOption = ({
|
||||
color,
|
||||
}: SidePanelEditColorOptionProps) => {
|
||||
const { t } = useLingui();
|
||||
const { updateNavigationMenuItemInDraft } =
|
||||
useUpdateNavigationMenuItemInDraft();
|
||||
const { updateItem, isDraftMode } = useNavigationMenuItemEditController();
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
const { updateInDraft, applyChanges } = useUpdateMetadataStoreDraft();
|
||||
const { selectedItem } = useSelectedNavigationMenuItemEditItem();
|
||||
@@ -42,11 +41,11 @@ export const SidePanelEditColorOption = ({
|
||||
const colorLabel = DEFAULT_COLOR_LABELS[themeColor] ?? capitalize(themeColor);
|
||||
|
||||
const handleSelectColor = (selectedColor: ThemeColor) => {
|
||||
updateNavigationMenuItemInDraft(navigationMenuItemId, {
|
||||
color: selectedColor,
|
||||
});
|
||||
void updateItem(navigationMenuItemId, { color: selectedColor });
|
||||
|
||||
if (isValidObjectNavigationMenuItem(selectedItem)) {
|
||||
// A personal favorite's color is item-local; only workspace customization
|
||||
// recolors the shared object metadata.
|
||||
if (isDraftMode && isValidObjectNavigationMenuItem(selectedItem)) {
|
||||
updateInDraft('objectMetadataItems', [
|
||||
{
|
||||
id: selectedItem.targetObjectMetadataId,
|
||||
|
||||
+23
-4
@@ -4,6 +4,7 @@ import { useState } from 'react';
|
||||
import { ensureAbsoluteUrl } from 'twenty-shared/utils';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
import { navigationMenuItemEditSectionState } from '@/navigation-menu-item/common/states/navigationMenuItemEditSectionState';
|
||||
import { extractDomainFromUrl } from '@/navigation-menu-item/display/link/utils/extractDomainFromUrl';
|
||||
import {
|
||||
type OrganizeActionsProps,
|
||||
@@ -14,6 +15,7 @@ import { getOrganizeActionsSelectableItemIds } from '@/navigation-menu-item/edit
|
||||
import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
|
||||
import { SidePanelList } from '@/side-panel/components/SidePanelList';
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
|
||||
type SidePanelEditLinkItemViewProps = OrganizeActionsProps & {
|
||||
selectedItem: NavigationMenuItem;
|
||||
@@ -39,6 +41,10 @@ export const SidePanelEditLinkItemView = ({
|
||||
showMoveToFolder = false,
|
||||
}: SidePanelEditLinkItemViewProps) => {
|
||||
const { t } = useLingui();
|
||||
const navigationMenuItemEditSection = useAtomStateValue(
|
||||
navigationMenuItemEditSectionState,
|
||||
);
|
||||
const isDraftMode = navigationMenuItemEditSection === 'workspace';
|
||||
const [urlEditInput, setUrlEditInput] = useState('');
|
||||
const [lastAutoSetName, setLastAutoSetName] = useState<string | null>(null);
|
||||
|
||||
@@ -57,7 +63,9 @@ export const SidePanelEditLinkItemView = ({
|
||||
|
||||
const handleUrlChange = (value: string) => {
|
||||
setUrlEditInput(value);
|
||||
if (!canAutoUpdateName) return;
|
||||
// A personal favorite persists immediately, so the auto-derived name is
|
||||
// applied on blur (see handleUrlBlur) rather than on every keystroke.
|
||||
if (!isDraftMode || !canAutoUpdateName) return;
|
||||
const trimmed = value.trim();
|
||||
if (!isNonEmptyString(trimmed)) return;
|
||||
const domain = extractDomainFromUrl(ensureAbsoluteUrl(trimmed));
|
||||
@@ -69,10 +77,21 @@ export const SidePanelEditLinkItemView = ({
|
||||
|
||||
const handleUrlBlur = (event: React.FocusEvent<HTMLInputElement>) => {
|
||||
const value = event.target.value.trim();
|
||||
if (isNonEmptyString(value)) {
|
||||
onUpdateLink(selectedItem.id, { link: ensureAbsoluteUrl(value) });
|
||||
setUrlEditInput('');
|
||||
if (!isNonEmptyString(value)) {
|
||||
return;
|
||||
}
|
||||
const absoluteUrl = ensureAbsoluteUrl(value);
|
||||
const updates: { link: string; name?: string } = { link: absoluteUrl };
|
||||
const autoName =
|
||||
!isDraftMode && canAutoUpdateName
|
||||
? extractDomainFromUrl(absoluteUrl)
|
||||
: undefined;
|
||||
if (autoName !== undefined) {
|
||||
setLastAutoSetName(autoName);
|
||||
updates.name = autoName;
|
||||
}
|
||||
onUpdateLink(selectedItem.id, updates);
|
||||
setUrlEditInput('');
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
+6
-7
@@ -4,7 +4,7 @@ import { IconApps } from 'twenty-ui/display';
|
||||
|
||||
import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
|
||||
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
|
||||
import { useDraftNavigationMenuItems } from '@/navigation-menu-item/edit/hooks/useDraftNavigationMenuItems';
|
||||
import { useNavigationMenuItemEditController } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemEditController';
|
||||
import { useSelectedNavigationMenuItemEditItem } from '@/navigation-menu-item/edit/hooks/useSelectedNavigationMenuItemEditItem';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { useQuery } from '@apollo/client/react';
|
||||
@@ -20,14 +20,13 @@ export const SidePanelEditOwnerSection = ({
|
||||
const { t } = useLingui();
|
||||
|
||||
const { selectedItem } = useSelectedNavigationMenuItemEditItem();
|
||||
const { currentDraft } = useDraftNavigationMenuItems();
|
||||
const { currentItems } = useNavigationMenuItemEditController();
|
||||
|
||||
const applicationIdFromDraft =
|
||||
isDefined(selectedItem) && isDefined(currentDraft)
|
||||
? currentDraft.find((item) => item.id === selectedItem.id)?.applicationId
|
||||
: undefined;
|
||||
const applicationIdFromSection = isDefined(selectedItem)
|
||||
? currentItems.find((item) => item.id === selectedItem.id)?.applicationId
|
||||
: undefined;
|
||||
|
||||
const applicationId = applicationIdProp ?? applicationIdFromDraft;
|
||||
const applicationId = applicationIdProp ?? applicationIdFromSection;
|
||||
|
||||
const { data } = useQuery(FindOneApplicationDocument, {
|
||||
variables: { id: applicationId ?? '' },
|
||||
|
||||
+9
-10
@@ -1,13 +1,13 @@
|
||||
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
|
||||
import { pendingInsertionNavigationMenuItemState } from '@/navigation-menu-item/common/states/pendingInsertionNavigationMenuItemState';
|
||||
import { useDraftNavigationMenuItemsAllFolders } from '@/navigation-menu-item/edit/hooks/useDraftNavigationMenuItemsAllFolders';
|
||||
import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemsDraftState';
|
||||
import { useNavigationMenuItemSectionAllFolders } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemSectionAllFolders';
|
||||
import { useNavigationMenuItemEditController } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemEditController';
|
||||
import { useSelectedNavigationMenuItemEditItem } from '@/navigation-menu-item/edit/hooks/useSelectedNavigationMenuItemEditItem';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
|
||||
import { selectedNavigationMenuItemIdInEditModeState } from '@/navigation-menu-item/common/states/selectedNavigationMenuItemIdInEditModeState';
|
||||
import { useSelectedNavigationMenuItemEditItemLabel } from '@/navigation-menu-item/edit/hooks/useSelectedNavigationMenuItemEditItemLabel';
|
||||
import { useUpdateLinkInDraft } from '@/navigation-menu-item/edit/link/hooks/useUpdateLinkInDraft';
|
||||
import { buildNavigationMenuLinkUpdate } from '@/navigation-menu-item/common/utils/buildNavigationMenuLinkUpdate';
|
||||
import { SidePanelEditColorOption } from '@/navigation-menu-item/edit/side-panel/components/SidePanelEditColorOption';
|
||||
import { SidePanelEditLinkItemView } from '@/navigation-menu-item/edit/side-panel/components/SidePanelEditLinkItemView';
|
||||
import { SidePanelEditObjectViewBase } from '@/navigation-menu-item/edit/side-panel/components/SidePanelEditObjectViewBase';
|
||||
@@ -49,7 +49,7 @@ export const SidePanelNavigationMenuItemEditPage = () => {
|
||||
const { selectedItemLabel } = useSelectedNavigationMenuItemEditItemLabel();
|
||||
const { selectedItem } = useSelectedNavigationMenuItemEditItem();
|
||||
const selectedItemType = selectedItem?.type ?? null;
|
||||
const { allFolders } = useDraftNavigationMenuItemsAllFolders();
|
||||
const { allFolders } = useNavigationMenuItemSectionAllFolders();
|
||||
|
||||
const { navigateToSidePanelSubPage } = useSidePanelSubPageHistory();
|
||||
const openFolderPicker = () =>
|
||||
@@ -65,8 +65,7 @@ export const SidePanelNavigationMenuItemEditPage = () => {
|
||||
onAddAfter,
|
||||
} = useNavigationMenuItemEditOrganizeActions();
|
||||
|
||||
const { updateLinkInDraft } = useUpdateLinkInDraft();
|
||||
const { workspaceNavigationMenuItems } = useNavigationMenuItemsDraftState();
|
||||
const { currentItems, updateItem } = useNavigationMenuItemEditController();
|
||||
const setPendingInsertionNavigationMenuItem = useSetAtomState(
|
||||
pendingInsertionNavigationMenuItemState,
|
||||
);
|
||||
@@ -83,7 +82,7 @@ export const SidePanelNavigationMenuItemEditPage = () => {
|
||||
if (!selectedItem || selectedItem.type !== NavigationMenuItemType.FOLDER) {
|
||||
return;
|
||||
}
|
||||
const folderItemCount = workspaceNavigationMenuItems.filter(
|
||||
const folderItemCount = currentItems.filter(
|
||||
(item) => item.folderId === selectedItem.id,
|
||||
).length;
|
||||
setPendingInsertionNavigationMenuItem({
|
||||
@@ -143,9 +142,9 @@ export const SidePanelNavigationMenuItemEditPage = () => {
|
||||
<SidePanelEditLinkItemView
|
||||
key={selectedItem.id}
|
||||
selectedItem={selectedItem}
|
||||
onUpdateLink={(linkId, updates) =>
|
||||
updateLinkInDraft(linkId, updates)
|
||||
}
|
||||
onUpdateLink={(linkId, updates) => {
|
||||
void updateItem(linkId, buildNavigationMenuLinkUpdate(updates));
|
||||
}}
|
||||
onOpenFolderPicker={openFolderPicker}
|
||||
showMoveToFolder={canMoveToOtherFolder}
|
||||
canMoveUp={canMoveUp}
|
||||
|
||||
+20
-17
@@ -1,16 +1,16 @@
|
||||
import { useState } from 'react';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
|
||||
import { getObjectColorWithFallback } from '@/object-metadata/utils/getObjectColorWithFallback';
|
||||
import { useDraftNavigationMenuItems } from '@/navigation-menu-item/edit/hooks/useDraftNavigationMenuItems';
|
||||
import { useNavigationMenuObjectMetadataFromDraft } from '@/navigation-menu-item/edit/hooks/useNavigationMenuObjectMetadataFromDraft';
|
||||
import { useOpenNavigationMenuItemInSidePanel } from '@/navigation-menu-item/edit/hooks/useOpenNavigationMenuItemInSidePanel';
|
||||
import { pendingInsertionNavigationMenuItemState } from '@/navigation-menu-item/common/states/pendingInsertionNavigationMenuItemState';
|
||||
import { useAddObjectToNavigationMenuDraft } from '@/navigation-menu-item/edit/object/hooks/useAddObjectToNavigationMenuDraft';
|
||||
import { useNavigationMenuItemEditController } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemEditController';
|
||||
import { useNavigationMenuObjectMetadataForSection } from '@/navigation-menu-item/edit/hooks/useNavigationMenuObjectMetadataForSection';
|
||||
import { useOpenNavigationMenuItemInSidePanel } from '@/navigation-menu-item/edit/hooks/useOpenNavigationMenuItemInSidePanel';
|
||||
import { SidePanelObjectPickerSubView } from '@/navigation-menu-item/edit/side-panel/components/SidePanelObjectPickerSubView';
|
||||
import { getAvailableObjectMetadataForNewSidebarItem } from '@/navigation-menu-item/edit/side-panel/utils/getAvailableObjectMetadataForNewSidebarItem';
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
||||
import { getObjectColorWithFallback } from '@/object-metadata/utils/getObjectColorWithFallback';
|
||||
import { useSidePanelSubPageHistory } from '@/side-panel/hooks/useSidePanelSubPageHistory';
|
||||
import { SidePanelSubPages } from '@/side-panel/types/SidePanelSubPages';
|
||||
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
||||
@@ -22,9 +22,8 @@ export const SidePanelNewSidebarItemObjectSubPage = () => {
|
||||
const { getIcon } = useIcons();
|
||||
const [objectSearchInput, setObjectSearchInput] = useState('');
|
||||
|
||||
const { currentDraft } = useDraftNavigationMenuItems();
|
||||
const { currentItems, createItem } = useNavigationMenuItemEditController();
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
const { addObjectToDraft } = useAddObjectToNavigationMenuDraft();
|
||||
const { openNavigationMenuItemInSidePanel } =
|
||||
useOpenNavigationMenuItemInSidePanel();
|
||||
const { activeNonSystemObjectMetadataItems } =
|
||||
@@ -36,8 +35,8 @@ export const SidePanelNewSidebarItemObjectSubPage = () => {
|
||||
const {
|
||||
views,
|
||||
objectMetadataIdsWithIndexView,
|
||||
objectMetadataIdsInWorkspace,
|
||||
} = useNavigationMenuObjectMetadataFromDraft(currentDraft);
|
||||
objectMetadataIdsAlreadyAdded,
|
||||
} = useNavigationMenuObjectMetadataForSection(currentItems);
|
||||
|
||||
const objectMetadataIdsWithDisplayableViews = new Set(
|
||||
views
|
||||
@@ -56,16 +55,20 @@ export const SidePanelNewSidebarItemObjectSubPage = () => {
|
||||
const handleSelectObject = (
|
||||
objectMetadataItem: EnrichedObjectMetadataItem,
|
||||
) => {
|
||||
if (objectMetadataIdsInWorkspace.has(objectMetadataItem.id)) {
|
||||
if (objectMetadataIdsAlreadyAdded.has(objectMetadataItem.id)) {
|
||||
return;
|
||||
}
|
||||
const itemId = addObjectToDraft({
|
||||
objectMetadataId: objectMetadataItem.id,
|
||||
currentDraft,
|
||||
targetFolderId: pendingInsertionNavigationMenuItem?.folderId,
|
||||
targetIndex: pendingInsertionNavigationMenuItem?.position,
|
||||
color: getObjectColorWithFallback(objectMetadataItem),
|
||||
});
|
||||
const itemId = createItem(
|
||||
{
|
||||
type: NavigationMenuItemType.OBJECT,
|
||||
targetObjectMetadataId: objectMetadataItem.id,
|
||||
color: getObjectColorWithFallback(objectMetadataItem),
|
||||
},
|
||||
{
|
||||
targetFolderId: pendingInsertionNavigationMenuItem?.folderId,
|
||||
targetIndex: pendingInsertionNavigationMenuItem?.position,
|
||||
},
|
||||
);
|
||||
setPendingInsertionNavigationMenuItem(null);
|
||||
openNavigationMenuItemInSidePanel({
|
||||
itemId,
|
||||
|
||||
+21
-18
@@ -1,19 +1,19 @@
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useState } from 'react';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
|
||||
import { getObjectColorWithFallback } from '@/object-metadata/utils/getObjectColorWithFallback';
|
||||
import { useDraftNavigationMenuItems } from '@/navigation-menu-item/edit/hooks/useDraftNavigationMenuItems';
|
||||
import { useNavigationMenuObjectMetadataFromDraft } from '@/navigation-menu-item/edit/hooks/useNavigationMenuObjectMetadataFromDraft';
|
||||
import { useOpenNavigationMenuItemInSidePanel } from '@/navigation-menu-item/edit/hooks/useOpenNavigationMenuItemInSidePanel';
|
||||
import { pendingInsertionNavigationMenuItemState } from '@/navigation-menu-item/common/states/pendingInsertionNavigationMenuItemState';
|
||||
import { useAddObjectToNavigationMenuDraft } from '@/navigation-menu-item/edit/object/hooks/useAddObjectToNavigationMenuDraft';
|
||||
import { useNavigationMenuItemEditController } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemEditController';
|
||||
import { useNavigationMenuObjectMetadataForSection } from '@/navigation-menu-item/edit/hooks/useNavigationMenuObjectMetadataForSection';
|
||||
import { useOpenNavigationMenuItemInSidePanel } from '@/navigation-menu-item/edit/hooks/useOpenNavigationMenuItemInSidePanel';
|
||||
import { SidePanelSystemObjectPickerSubView } from '@/navigation-menu-item/edit/side-panel/components/SidePanelSystemObjectPickerSubView';
|
||||
import { getAvailableObjectMetadataForNewSidebarItem } from '@/navigation-menu-item/edit/side-panel/utils/getAvailableObjectMetadataForNewSidebarItem';
|
||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
||||
import { getObjectColorWithFallback } from '@/object-metadata/utils/getObjectColorWithFallback';
|
||||
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
||||
import { ViewKey } from '@/views/types/ViewKey';
|
||||
import { useState } from 'react';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
|
||||
export const SidePanelNewSidebarItemObjectSystemPickerSubPage = () => {
|
||||
@@ -21,9 +21,8 @@ export const SidePanelNewSidebarItemObjectSystemPickerSubPage = () => {
|
||||
const { getIcon } = useIcons();
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
|
||||
const { currentDraft } = useDraftNavigationMenuItems();
|
||||
const { currentItems, createItem } = useNavigationMenuItemEditController();
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
const { addObjectToDraft } = useAddObjectToNavigationMenuDraft();
|
||||
const { openNavigationMenuItemInSidePanel } =
|
||||
useOpenNavigationMenuItemInSidePanel();
|
||||
const { activeNonSystemObjectMetadataItems } =
|
||||
@@ -35,8 +34,8 @@ export const SidePanelNewSidebarItemObjectSystemPickerSubPage = () => {
|
||||
const {
|
||||
views,
|
||||
objectMetadataIdsWithIndexView,
|
||||
objectMetadataIdsInWorkspace,
|
||||
} = useNavigationMenuObjectMetadataFromDraft(currentDraft);
|
||||
objectMetadataIdsAlreadyAdded,
|
||||
} = useNavigationMenuObjectMetadataForSection(currentItems);
|
||||
|
||||
const objectMetadataIdsWithDisplayableViews = new Set(
|
||||
views
|
||||
@@ -55,16 +54,20 @@ export const SidePanelNewSidebarItemObjectSystemPickerSubPage = () => {
|
||||
const handleSelectObject = (
|
||||
objectMetadataItem: EnrichedObjectMetadataItem,
|
||||
) => {
|
||||
if (objectMetadataIdsInWorkspace.has(objectMetadataItem.id)) {
|
||||
if (objectMetadataIdsAlreadyAdded.has(objectMetadataItem.id)) {
|
||||
return;
|
||||
}
|
||||
const itemId = addObjectToDraft({
|
||||
objectMetadataId: objectMetadataItem.id,
|
||||
currentDraft,
|
||||
targetFolderId: pendingInsertionNavigationMenuItem?.folderId,
|
||||
targetIndex: pendingInsertionNavigationMenuItem?.position,
|
||||
color: getObjectColorWithFallback(objectMetadataItem),
|
||||
});
|
||||
const itemId = createItem(
|
||||
{
|
||||
type: NavigationMenuItemType.OBJECT,
|
||||
targetObjectMetadataId: objectMetadataItem.id,
|
||||
color: getObjectColorWithFallback(objectMetadataItem),
|
||||
},
|
||||
{
|
||||
targetFolderId: pendingInsertionNavigationMenuItem?.folderId,
|
||||
targetIndex: pendingInsertionNavigationMenuItem?.position,
|
||||
},
|
||||
);
|
||||
setPendingInsertionNavigationMenuItem(null);
|
||||
openNavigationMenuItemInSidePanel({
|
||||
itemId,
|
||||
|
||||
+29
-24
@@ -1,18 +1,18 @@
|
||||
import { Avatar, IconFolder, useIcons } from 'twenty-ui/display';
|
||||
|
||||
import { useAddRecordToNavigationMenuDraft } from '@/navigation-menu-item/edit/record/hooks/useAddRecordToNavigationMenuDraft';
|
||||
import { useDraftNavigationMenuItems } from '@/navigation-menu-item/edit/hooks/useDraftNavigationMenuItems';
|
||||
import { useOpenNavigationMenuItemInSidePanel } from '@/navigation-menu-item/edit/hooks/useOpenNavigationMenuItemInSidePanel';
|
||||
import { pendingInsertionNavigationMenuItemState } from '@/navigation-menu-item/common/states/pendingInsertionNavigationMenuItemState';
|
||||
import type { AddToNavigationDragPayload } from '@/navigation-menu-item/common/types/add-to-navigation-drag-payload';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { SidePanelItemWithAddToNavigationDrag } from '@/side-panel/components/SidePanelItemWithAddToNavigationDrag';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
||||
import { Avatar, useIcons } from 'twenty-ui/display';
|
||||
import {
|
||||
CoreObjectNameSingular,
|
||||
NavigationMenuItemType,
|
||||
} from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { pendingInsertionNavigationMenuItemState } from '@/navigation-menu-item/common/states/pendingInsertionNavigationMenuItemState';
|
||||
import type { AddToNavigationDragPayload } from '@/navigation-menu-item/common/types/add-to-navigation-drag-payload';
|
||||
import { useNavigationMenuItemEditController } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemEditController';
|
||||
import { useOpenNavigationMenuItemInSidePanel } from '@/navigation-menu-item/edit/hooks/useOpenNavigationMenuItemInSidePanel';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { SidePanelItemWithAddToNavigationDrag } from '@/side-panel/components/SidePanelItemWithAddToNavigationDrag';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
||||
|
||||
type SearchRecord = {
|
||||
recordId: string;
|
||||
@@ -31,8 +31,7 @@ export const SidePanelNewSidebarItemRecordItem = ({
|
||||
dragIndex,
|
||||
}: SidePanelNewSidebarItemRecordItemProps) => {
|
||||
const { getIcon } = useIcons();
|
||||
const { addRecordToDraft } = useAddRecordToNavigationMenuDraft();
|
||||
const { currentDraft } = useDraftNavigationMenuItems();
|
||||
const { createItem } = useNavigationMenuItemEditController();
|
||||
const [
|
||||
pendingInsertionNavigationMenuItem,
|
||||
setPendingInsertionNavigationMenuItem,
|
||||
@@ -53,24 +52,30 @@ export const SidePanelNewSidebarItemRecordItem = ({
|
||||
};
|
||||
|
||||
const handleSelectRecord = () => {
|
||||
const itemId = addRecordToDraft(
|
||||
if (!isDefined(objectMetadataItem)) {
|
||||
return;
|
||||
}
|
||||
const itemId = createItem(
|
||||
{
|
||||
recordId: record.recordId,
|
||||
objectNameSingular: record.objectNameSingular,
|
||||
label: record.label,
|
||||
imageUrl: record.imageUrl,
|
||||
type: NavigationMenuItemType.RECORD,
|
||||
targetObjectMetadataId: objectMetadataItem.id,
|
||||
targetRecordId: record.recordId,
|
||||
targetRecordIdentifier: {
|
||||
id: record.recordId,
|
||||
labelIdentifier: record.label,
|
||||
imageIdentifier: record.imageUrl ?? null,
|
||||
},
|
||||
},
|
||||
{
|
||||
targetFolderId: pendingInsertionNavigationMenuItem?.folderId ?? null,
|
||||
targetIndex: pendingInsertionNavigationMenuItem?.position,
|
||||
},
|
||||
currentDraft,
|
||||
pendingInsertionNavigationMenuItem?.folderId ?? null,
|
||||
pendingInsertionNavigationMenuItem?.position,
|
||||
);
|
||||
setPendingInsertionNavigationMenuItem(null);
|
||||
openNavigationMenuItemInSidePanel({
|
||||
itemId,
|
||||
pageTitle: record.label,
|
||||
pageIcon: objectMetadataItem
|
||||
? getIcon(objectMetadataItem.icon)
|
||||
: IconFolder,
|
||||
pageIcon: getIcon(objectMetadataItem.icon),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
+5
-5
@@ -4,7 +4,7 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import { useDebounce } from 'use-debounce';
|
||||
|
||||
import { MAX_SEARCH_RESULTS } from '@/command-menu/constants/MaxSearchResults';
|
||||
import { useDraftNavigationMenuItems } from '@/navigation-menu-item/edit/hooks/useDraftNavigationMenuItems';
|
||||
import { useNavigationMenuItemEditController } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemEditController';
|
||||
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
|
||||
import { useReadableObjectMetadataItems } from '@/object-metadata/hooks/useReadableObjectMetadataItems';
|
||||
import { SidePanelAddToNavigationDroppable } from '@/side-panel/components/SidePanelAddToNavigationDroppable';
|
||||
@@ -27,7 +27,7 @@ type SearchRecordBase = {
|
||||
|
||||
export const SidePanelNewSidebarItemRecordSubPage = () => {
|
||||
const { t } = useLingui();
|
||||
const { currentDraft } = useDraftNavigationMenuItems();
|
||||
const { currentItems } = useNavigationMenuItemEditController();
|
||||
const [recordSearchInput, setRecordSearchInput] = useState('');
|
||||
const [deferredRecordSearchInput] = useDebounce(recordSearchInput, 300);
|
||||
const coreClient = useApolloCoreClient();
|
||||
@@ -65,8 +65,8 @@ export const SidePanelNewSidebarItemRecordSubPage = () => {
|
||||
},
|
||||
);
|
||||
|
||||
const workspaceRecordIds = new Set(
|
||||
currentDraft.flatMap((item) =>
|
||||
const recordIdsAlreadyAdded = new Set(
|
||||
currentItems.flatMap((item) =>
|
||||
isDefined(item.targetRecordId) ? [item.targetRecordId] : [],
|
||||
),
|
||||
);
|
||||
@@ -74,7 +74,7 @@ export const SidePanelNewSidebarItemRecordSubPage = () => {
|
||||
const searchRecords =
|
||||
searchData?.search?.edges?.map((edge) => edge.node) ?? [];
|
||||
const availableSearchRecords = searchRecords.filter(
|
||||
(record) => !workspaceRecordIds.has(record.recordId),
|
||||
(record) => !recordIdsAlreadyAdded.has(record.recordId),
|
||||
) as SearchRecordBase[];
|
||||
|
||||
const isEmpty = availableSearchRecords.length === 0 && !recordSearchLoading;
|
||||
|
||||
+6
-6
@@ -1,5 +1,5 @@
|
||||
import { useDraftNavigationMenuItems } from '@/navigation-menu-item/edit/hooks/useDraftNavigationMenuItems';
|
||||
import { useNavigationMenuObjectMetadataFromDraft } from '@/navigation-menu-item/edit/hooks/useNavigationMenuObjectMetadataFromDraft';
|
||||
import { useNavigationMenuItemEditController } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemEditController';
|
||||
import { useNavigationMenuObjectMetadataForSection } from '@/navigation-menu-item/edit/hooks/useNavigationMenuObjectMetadataForSection';
|
||||
import { SidePanelNewSidebarItemViewObjectPickerSubView } from '@/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemViewObjectPickerSubView';
|
||||
import { getAvailableObjectMetadataForNewSidebarItem } from '@/navigation-menu-item/edit/side-panel/utils/getAvailableObjectMetadataForNewSidebarItem';
|
||||
import { isViewDisplayableInNavigationMenu } from '@/navigation-menu-item/edit/side-panel/utils/isViewDisplayableInNavigationMenu';
|
||||
@@ -19,19 +19,19 @@ export const SidePanelNewSidebarItemViewObjectPickerSubPage = () => {
|
||||
selectedObjectMetadataIdForViewFlowState,
|
||||
);
|
||||
|
||||
const { currentDraft } = useDraftNavigationMenuItems();
|
||||
const { currentItems } = useNavigationMenuItemEditController();
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
const { activeNonSystemObjectMetadataItems } =
|
||||
useFilteredObjectMetadataItems();
|
||||
const { views, objectMetadataIdsWithIndexView, viewIdsInWorkspace } =
|
||||
useNavigationMenuObjectMetadataFromDraft(currentDraft);
|
||||
const { views, objectMetadataIdsWithIndexView, viewIdsAlreadyAdded } =
|
||||
useNavigationMenuObjectMetadataForSection(currentItems);
|
||||
|
||||
const objectMetadataIdsWithDisplayableViews = new Set(
|
||||
views
|
||||
.filter(
|
||||
(view) =>
|
||||
isViewDisplayableInNavigationMenu(view) &&
|
||||
!viewIdsInWorkspace.has(view.id),
|
||||
!viewIdsAlreadyAdded.has(view.id),
|
||||
)
|
||||
.map((view) => view.objectMetadataId),
|
||||
);
|
||||
|
||||
+14
-12
@@ -1,8 +1,7 @@
|
||||
import { ObjectIconWithViewOverlay } from '@/navigation-menu-item/display/view/components/ObjectIconWithViewOverlay';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { useAddViewToNavigationMenuDraft } from '@/navigation-menu-item/edit/view/hooks/useAddViewToNavigationMenuDraft';
|
||||
import { useDraftNavigationMenuItems } from '@/navigation-menu-item/edit/hooks/useDraftNavigationMenuItems';
|
||||
import { useNavigationMenuObjectMetadataFromDraft } from '@/navigation-menu-item/edit/hooks/useNavigationMenuObjectMetadataFromDraft';
|
||||
import { useNavigationMenuItemEditController } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemEditController';
|
||||
import { useNavigationMenuObjectMetadataForSection } from '@/navigation-menu-item/edit/hooks/useNavigationMenuObjectMetadataForSection';
|
||||
import { useOpenNavigationMenuItemInSidePanel } from '@/navigation-menu-item/edit/hooks/useOpenNavigationMenuItemInSidePanel';
|
||||
import { isViewDisplayableInNavigationMenu } from '@/navigation-menu-item/edit/side-panel/utils/isViewDisplayableInNavigationMenu';
|
||||
import { pendingInsertionNavigationMenuItemState } from '@/navigation-menu-item/common/states/pendingInsertionNavigationMenuItemState';
|
||||
@@ -32,8 +31,7 @@ export const SidePanelNewSidebarItemViewPickerSubView = ({
|
||||
const { t } = useLingui();
|
||||
const { getIcon } = useIcons();
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const { addViewToDraft } = useAddViewToNavigationMenuDraft();
|
||||
const { currentDraft } = useDraftNavigationMenuItems();
|
||||
const { currentItems, createItem } = useNavigationMenuItemEditController();
|
||||
const [
|
||||
pendingInsertionNavigationMenuItem,
|
||||
setPendingInsertionNavigationMenuItem,
|
||||
@@ -41,7 +39,7 @@ export const SidePanelNewSidebarItemViewPickerSubView = ({
|
||||
const { openNavigationMenuItemInSidePanel } =
|
||||
useOpenNavigationMenuItemInSidePanel();
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
const { views } = useNavigationMenuObjectMetadataFromDraft(currentDraft);
|
||||
const { views } = useNavigationMenuObjectMetadataForSection(currentItems);
|
||||
|
||||
const viewsForSelectedObject = views
|
||||
.filter(
|
||||
@@ -74,12 +72,16 @@ export const SidePanelNewSidebarItemViewPickerSubView = ({
|
||||
: undefined;
|
||||
|
||||
const handleSelectView = (view: View) => {
|
||||
const itemId = addViewToDraft(
|
||||
view.id,
|
||||
currentDraft,
|
||||
pendingInsertionNavigationMenuItem?.folderId ?? null,
|
||||
pendingInsertionNavigationMenuItem?.position,
|
||||
selectedObjectIconColor,
|
||||
const itemId = createItem(
|
||||
{
|
||||
type: NavigationMenuItemType.VIEW,
|
||||
viewId: view.id,
|
||||
color: selectedObjectIconColor,
|
||||
},
|
||||
{
|
||||
targetFolderId: pendingInsertionNavigationMenuItem?.folderId ?? null,
|
||||
targetIndex: pendingInsertionNavigationMenuItem?.position,
|
||||
},
|
||||
);
|
||||
setPendingInsertionNavigationMenuItem(null);
|
||||
openNavigationMenuItemInSidePanel({
|
||||
|
||||
+6
-6
@@ -1,5 +1,5 @@
|
||||
import { useDraftNavigationMenuItems } from '@/navigation-menu-item/edit/hooks/useDraftNavigationMenuItems';
|
||||
import { useNavigationMenuObjectMetadataFromDraft } from '@/navigation-menu-item/edit/hooks/useNavigationMenuObjectMetadataFromDraft';
|
||||
import { useNavigationMenuItemEditController } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemEditController';
|
||||
import { useNavigationMenuObjectMetadataForSection } from '@/navigation-menu-item/edit/hooks/useNavigationMenuObjectMetadataForSection';
|
||||
import { SidePanelNewSidebarItemViewSystemSubView } from '@/navigation-menu-item/edit/side-panel/components/SidePanelNewSidebarItemViewSystemSubView';
|
||||
import { getAvailableObjectMetadataForNewSidebarItem } from '@/navigation-menu-item/edit/side-panel/utils/getAvailableObjectMetadataForNewSidebarItem';
|
||||
import { isViewDisplayableInNavigationMenu } from '@/navigation-menu-item/edit/side-panel/utils/isViewDisplayableInNavigationMenu';
|
||||
@@ -19,19 +19,19 @@ export const SidePanelNewSidebarItemViewSystemPickerSubPage = () => {
|
||||
selectedObjectMetadataIdForViewFlowState,
|
||||
);
|
||||
|
||||
const { currentDraft } = useDraftNavigationMenuItems();
|
||||
const { currentItems } = useNavigationMenuItemEditController();
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
const { activeNonSystemObjectMetadataItems } =
|
||||
useFilteredObjectMetadataItems();
|
||||
const { views, objectMetadataIdsWithIndexView, viewIdsInWorkspace } =
|
||||
useNavigationMenuObjectMetadataFromDraft(currentDraft);
|
||||
const { views, objectMetadataIdsWithIndexView, viewIdsAlreadyAdded } =
|
||||
useNavigationMenuObjectMetadataForSection(currentItems);
|
||||
|
||||
const objectMetadataIdsWithDisplayableViews = new Set(
|
||||
views
|
||||
.filter(
|
||||
(view) =>
|
||||
isViewDisplayableInNavigationMenu(view) &&
|
||||
!viewIdsInWorkspace.has(view.id),
|
||||
!viewIdsAlreadyAdded.has(view.id),
|
||||
)
|
||||
.map((view) => view.objectMetadataId),
|
||||
);
|
||||
|
||||
+6
-6
@@ -3,8 +3,8 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
|
||||
import { ObjectMetadataIcon } from '@/object-metadata/components/ObjectMetadataIcon';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { useDraftNavigationMenuItems } from '@/navigation-menu-item/edit/hooks/useDraftNavigationMenuItems';
|
||||
import { useNavigationMenuObjectMetadataFromDraft } from '@/navigation-menu-item/edit/hooks/useNavigationMenuObjectMetadataFromDraft';
|
||||
import { useNavigationMenuItemEditController } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemEditController';
|
||||
import { useNavigationMenuObjectMetadataForSection } from '@/navigation-menu-item/edit/hooks/useNavigationMenuObjectMetadataForSection';
|
||||
import { getObjectColorWithFallback } from '@/object-metadata/utils/getObjectColorWithFallback';
|
||||
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
|
||||
import { SidePanelItemWithAddToNavigationDrag } from '@/side-panel/components/SidePanelItemWithAddToNavigationDrag';
|
||||
@@ -28,10 +28,10 @@ export const SidePanelObjectMenuItem = ({
|
||||
disableDrag = false,
|
||||
}: SidePanelObjectMenuItemProps) => {
|
||||
const { t } = useLingui();
|
||||
const { currentDraft } = useDraftNavigationMenuItems();
|
||||
const { objectMetadataIdsInWorkspace } =
|
||||
useNavigationMenuObjectMetadataFromDraft(currentDraft);
|
||||
const isAlreadyInNavbar = objectMetadataIdsInWorkspace.has(
|
||||
const { currentItems } = useNavigationMenuItemEditController();
|
||||
const { objectMetadataIdsAlreadyAdded } =
|
||||
useNavigationMenuObjectMetadataForSection(currentItems);
|
||||
const isAlreadyInNavbar = objectMetadataIdsAlreadyAdded.has(
|
||||
objectMetadataItem.id,
|
||||
);
|
||||
const defaultViewId = useAtomFamilySelectorValue(
|
||||
|
||||
+15
-20
@@ -1,22 +1,16 @@
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { IconFolder } from 'twenty-ui/display';
|
||||
|
||||
import { useAddFolderToNavigationMenuDraft } from '@/navigation-menu-item/edit/folder/hooks/useAddFolderToNavigationMenuDraft';
|
||||
import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemsDraftState';
|
||||
import { useOpenNavigationMenuItemInSidePanel } from '@/navigation-menu-item/edit/hooks/useOpenNavigationMenuItemInSidePanel';
|
||||
import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER } from '@/navigation-menu-item/common/constants/NavigationMenuItemDefaultColorFolder';
|
||||
import { pendingInsertionNavigationMenuItemState } from '@/navigation-menu-item/common/states/pendingInsertionNavigationMenuItemState';
|
||||
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/states/navigationMenuItemsDraftState';
|
||||
import { useNavigationMenuItemEditController } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemEditController';
|
||||
import { useOpenNavigationMenuItemInSidePanel } from '@/navigation-menu-item/edit/hooks/useOpenNavigationMenuItemInSidePanel';
|
||||
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
|
||||
export const useAddFolderToNavigationMenu = () => {
|
||||
const { t } = useLingui();
|
||||
const { addFolderToDraft } = useAddFolderToNavigationMenuDraft();
|
||||
const { workspaceNavigationMenuItems } = useNavigationMenuItemsDraftState();
|
||||
const navigationMenuItemsDraft = useAtomStateValue(
|
||||
navigationMenuItemsDraftState,
|
||||
);
|
||||
const { createItem } = useNavigationMenuItemEditController();
|
||||
const { openNavigationMenuItemInSidePanel } =
|
||||
useOpenNavigationMenuItemInSidePanel();
|
||||
const [
|
||||
@@ -24,16 +18,17 @@ export const useAddFolderToNavigationMenu = () => {
|
||||
setPendingInsertionNavigationMenuItem,
|
||||
] = useAtomState(pendingInsertionNavigationMenuItemState);
|
||||
|
||||
const currentDraft = isDefined(navigationMenuItemsDraft)
|
||||
? navigationMenuItemsDraft
|
||||
: workspaceNavigationMenuItems;
|
||||
|
||||
const handleAddFolder = () => {
|
||||
const itemId = addFolderToDraft(
|
||||
t`New folder`,
|
||||
currentDraft,
|
||||
pendingInsertionNavigationMenuItem?.folderId ?? null,
|
||||
pendingInsertionNavigationMenuItem?.position,
|
||||
const itemId = createItem(
|
||||
{
|
||||
type: NavigationMenuItemType.FOLDER,
|
||||
name: t`New folder`,
|
||||
color: DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER,
|
||||
},
|
||||
{
|
||||
targetFolderId: pendingInsertionNavigationMenuItem?.folderId ?? null,
|
||||
targetIndex: pendingInsertionNavigationMenuItem?.position,
|
||||
},
|
||||
);
|
||||
|
||||
setPendingInsertionNavigationMenuItem(null);
|
||||
|
||||
+17
-21
@@ -1,22 +1,17 @@
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { normalizeUrl } from 'twenty-shared/utils';
|
||||
import { IconLink } from 'twenty-ui/display';
|
||||
|
||||
import { useAddLinkToNavigationMenuDraft } from '@/navigation-menu-item/edit/link/hooks/useAddLinkToNavigationMenuDraft';
|
||||
import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemsDraftState';
|
||||
import { useOpenNavigationMenuItemInSidePanel } from '@/navigation-menu-item/edit/hooks/useOpenNavigationMenuItemInSidePanel';
|
||||
import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_LINK } from '@/navigation-menu-item/common/constants/NavigationMenuItemDefaultColorLink';
|
||||
import { pendingInsertionNavigationMenuItemState } from '@/navigation-menu-item/common/states/pendingInsertionNavigationMenuItemState';
|
||||
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/states/navigationMenuItemsDraftState';
|
||||
import { useNavigationMenuItemEditController } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemEditController';
|
||||
import { useOpenNavigationMenuItemInSidePanel } from '@/navigation-menu-item/edit/hooks/useOpenNavigationMenuItemInSidePanel';
|
||||
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
|
||||
export const useAddLinkToNavigationMenu = () => {
|
||||
const { t } = useLingui();
|
||||
const { addLinkToDraft } = useAddLinkToNavigationMenuDraft();
|
||||
const { workspaceNavigationMenuItems } = useNavigationMenuItemsDraftState();
|
||||
const navigationMenuItemsDraft = useAtomStateValue(
|
||||
navigationMenuItemsDraftState,
|
||||
);
|
||||
const { createItem } = useNavigationMenuItemEditController();
|
||||
const { openNavigationMenuItemInSidePanel } =
|
||||
useOpenNavigationMenuItemInSidePanel();
|
||||
const [
|
||||
@@ -24,17 +19,18 @@ export const useAddLinkToNavigationMenu = () => {
|
||||
setPendingInsertionNavigationMenuItem,
|
||||
] = useAtomState(pendingInsertionNavigationMenuItemState);
|
||||
|
||||
const currentDraft = isDefined(navigationMenuItemsDraft)
|
||||
? navigationMenuItemsDraft
|
||||
: workspaceNavigationMenuItems;
|
||||
|
||||
const handleAddLink = () => {
|
||||
const itemId = addLinkToDraft(
|
||||
t`Link label`,
|
||||
'www.example.com',
|
||||
currentDraft,
|
||||
pendingInsertionNavigationMenuItem?.folderId ?? null,
|
||||
pendingInsertionNavigationMenuItem?.position,
|
||||
const itemId = createItem(
|
||||
{
|
||||
type: NavigationMenuItemType.LINK,
|
||||
name: t`Link label`,
|
||||
link: normalizeUrl('www.example.com'),
|
||||
color: DEFAULT_NAVIGATION_MENU_ITEM_COLOR_LINK,
|
||||
},
|
||||
{
|
||||
targetFolderId: pendingInsertionNavigationMenuItem?.folderId ?? null,
|
||||
targetIndex: pendingInsertionNavigationMenuItem?.position,
|
||||
},
|
||||
);
|
||||
|
||||
setPendingInsertionNavigationMenuItem(null);
|
||||
|
||||
+6
-6
@@ -1,8 +1,8 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { useDraftNavigationMenuItemsAllFolders } from '@/navigation-menu-item/edit/hooks/useDraftNavigationMenuItemsAllFolders';
|
||||
import { useDraftNavigationMenuItemsWorkspaceFolders } from '@/navigation-menu-item/edit/hooks/useDraftNavigationMenuItemsWorkspaceFolders';
|
||||
import { useNavigationMenuItemSectionAllFolders } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemSectionAllFolders';
|
||||
import { useNavigationMenuItemSectionFolders } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemSectionFolders';
|
||||
import { useNavigationMenuItemMoveRemove } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemMoveRemove';
|
||||
import { useSelectedNavigationMenuItemEditItem } from '@/navigation-menu-item/edit/hooks/useSelectedNavigationMenuItemEditItem';
|
||||
import { selectedNavigationMenuItemIdInEditModeState } from '@/navigation-menu-item/common/states/selectedNavigationMenuItemIdInEditModeState';
|
||||
@@ -46,8 +46,8 @@ export const useFolderPickerSelectionData = () => {
|
||||
);
|
||||
const { selectedItem } = useSelectedNavigationMenuItemEditItem();
|
||||
const selectedItemType = selectedItem?.type ?? null;
|
||||
const { allFolders } = useDraftNavigationMenuItemsAllFolders();
|
||||
const { workspaceFolders } = useDraftNavigationMenuItemsWorkspaceFolders();
|
||||
const { allFolders } = useNavigationMenuItemSectionAllFolders();
|
||||
const { sectionFolders } = useNavigationMenuItemSectionFolders();
|
||||
|
||||
const selectedFolderId =
|
||||
selectedItemType === NavigationMenuItemType.FOLDER
|
||||
@@ -83,11 +83,11 @@ export const useFolderPickerSelectionData = () => {
|
||||
|
||||
const foldersToShow = includeNoFolderOption
|
||||
? folders
|
||||
: excludeCurrentFolder(workspaceFolders, currentFolderId);
|
||||
: excludeCurrentFolder(sectionFolders, currentFolderId);
|
||||
|
||||
const handleSelectFolder = (folderId: string | null) => {
|
||||
if (isDefined(selectedNavigationMenuItemIdInEditMode)) {
|
||||
moveToFolder(selectedNavigationMenuItemIdInEditMode, folderId);
|
||||
void moveToFolder(selectedNavigationMenuItemIdInEditMode, folderId);
|
||||
closeSidePanelMenu();
|
||||
}
|
||||
};
|
||||
|
||||
+9
-19
@@ -8,10 +8,9 @@ import { IconDotsVertical } from 'twenty-ui/display';
|
||||
import { pendingInsertionNavigationMenuItemState } from '@/navigation-menu-item/common/states/pendingInsertionNavigationMenuItemState';
|
||||
import { selectedNavigationMenuItemIdInEditModeState } from '@/navigation-menu-item/common/states/selectedNavigationMenuItemIdInEditModeState';
|
||||
import { type PendingInsertionNavigationMenuItem } from '@/navigation-menu-item/common/types/PendingInsertionNavigationMenuItem';
|
||||
import { useNavigationMenuItemSectionItems } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemSectionItems';
|
||||
import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/display/utils/getNavigationMenuItemComputedLink';
|
||||
import { useNavigationMenuItemEditSectionItems } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemEditSectionItems';
|
||||
import { useNavigationMenuItemMoveRemove } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemMoveRemove';
|
||||
import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemsDraftState';
|
||||
import { type OrganizeActionsProps } from '@/navigation-menu-item/edit/side-panel/components/SidePanelEditOrganizeActions';
|
||||
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
|
||||
import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel';
|
||||
@@ -25,16 +24,12 @@ import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
const computeInsertionPosition = (
|
||||
selectedItem: { id: string; folderId?: string | null },
|
||||
workspaceNavigationMenuItems: NavigationMenuItem[],
|
||||
sectionItems: NavigationMenuItem[],
|
||||
offset: 0 | 1,
|
||||
): PendingInsertionNavigationMenuItem | null => {
|
||||
const folderId = selectedItem.folderId ?? null;
|
||||
const itemsInFolderSorted = workspaceNavigationMenuItems
|
||||
.filter(
|
||||
(item) =>
|
||||
(item.folderId ?? null) === folderId &&
|
||||
!isDefined(item.userWorkspaceId),
|
||||
)
|
||||
const itemsInFolderSorted = sectionItems
|
||||
.filter((item) => (item.folderId ?? null) === folderId)
|
||||
.sort((a, b) => a.position - b.position);
|
||||
const selectedIndexSorted = itemsInFolderSorted.findIndex(
|
||||
(item) => item.id === selectedItem.id,
|
||||
@@ -66,8 +61,7 @@ export const useNavigationMenuItemEditOrganizeActions =
|
||||
const setPendingInsertionNavigationMenuItem = useSetAtomState(
|
||||
pendingInsertionNavigationMenuItemState,
|
||||
);
|
||||
const { workspaceNavigationMenuItems } = useNavigationMenuItemsDraftState();
|
||||
const items = useNavigationMenuItemSectionItems();
|
||||
const items = useNavigationMenuItemEditSectionItems();
|
||||
const { moveUp, moveDown, remove } = useNavigationMenuItemMoveRemove();
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
|
||||
const views = useAtomStateValue(viewsSelector);
|
||||
@@ -99,13 +93,13 @@ export const useNavigationMenuItemEditOrganizeActions =
|
||||
|
||||
const handleMoveUp = () => {
|
||||
if (canMoveUp && isDefined(selectedNavigationMenuItemIdInEditMode)) {
|
||||
moveUp(selectedNavigationMenuItemIdInEditMode);
|
||||
void moveUp(selectedNavigationMenuItemIdInEditMode);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMoveDown = () => {
|
||||
if (canMoveDown && isDefined(selectedNavigationMenuItemIdInEditMode)) {
|
||||
moveDown(selectedNavigationMenuItemIdInEditMode);
|
||||
void moveDown(selectedNavigationMenuItemIdInEditMode);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -118,7 +112,7 @@ export const useNavigationMenuItemEditOrganizeActions =
|
||||
siblings[selectedIndexInSiblings + 1] ??
|
||||
siblings[selectedIndexInSiblings - 1];
|
||||
|
||||
remove(selectedNavigationMenuItemIdInEditMode);
|
||||
void remove(selectedNavigationMenuItemIdInEditMode);
|
||||
|
||||
if (isDefined(nextItem)) {
|
||||
setSelectedNavigationMenuItemIdInEditMode(nextItem.id);
|
||||
@@ -144,11 +138,7 @@ export const useNavigationMenuItemEditOrganizeActions =
|
||||
|
||||
const handleAddAtOffset = (offset: 0 | 1) => {
|
||||
if (!isDefined(selectedItem)) return;
|
||||
const insertion = computeInsertionPosition(
|
||||
selectedItem,
|
||||
workspaceNavigationMenuItems,
|
||||
offset,
|
||||
);
|
||||
const insertion = computeInsertionPosition(selectedItem, items, offset);
|
||||
if (!insertion) return;
|
||||
|
||||
const title =
|
||||
|
||||
-65
@@ -1,65 +0,0 @@
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
import type { NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/common/states/navigationMenuItemsDraftState';
|
||||
import { computeInsertIndexAndPosition } from '@/navigation-menu-item/common/utils/computeInsertIndexAndPosition';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
|
||||
export const useAddViewToNavigationMenuDraft = () => {
|
||||
const setNavigationMenuItemsDraft = useSetAtomState(
|
||||
navigationMenuItemsDraftState,
|
||||
);
|
||||
|
||||
const addViewToDraft = (
|
||||
viewId: string,
|
||||
currentDraft: NavigationMenuItem[],
|
||||
targetFolderId?: string | null,
|
||||
targetIndex?: number,
|
||||
color?: string | null,
|
||||
): string => {
|
||||
const folderId = targetFolderId ?? null;
|
||||
|
||||
const itemsInFolder = currentDraft.filter(
|
||||
(item) =>
|
||||
(item.folderId ?? null) === folderId &&
|
||||
!isDefined(item.userWorkspaceId),
|
||||
);
|
||||
const index = targetIndex ?? itemsInFolder.length;
|
||||
|
||||
const { flatIndex, position } = computeInsertIndexAndPosition(
|
||||
currentDraft,
|
||||
folderId,
|
||||
index,
|
||||
);
|
||||
|
||||
const newItemId = v4();
|
||||
const newItem: NavigationMenuItem = {
|
||||
__typename: 'NavigationMenuItem',
|
||||
id: newItemId,
|
||||
type: NavigationMenuItemType.VIEW,
|
||||
viewId,
|
||||
targetObjectMetadataId: undefined,
|
||||
position,
|
||||
userWorkspaceId: undefined,
|
||||
targetRecordId: undefined,
|
||||
folderId: folderId ?? undefined,
|
||||
name: undefined,
|
||||
applicationId: undefined,
|
||||
color,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const newDraft = [
|
||||
...currentDraft.slice(0, flatIndex),
|
||||
newItem,
|
||||
...currentDraft.slice(flatIndex),
|
||||
];
|
||||
setNavigationMenuItemsDraft(newDraft);
|
||||
return newItemId;
|
||||
};
|
||||
|
||||
return { addViewToDraft };
|
||||
};
|
||||
@@ -1,12 +1,14 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { TintedIconTile, useIcons } from 'twenty-ui/display';
|
||||
|
||||
import { FOLDER_ICON_DEFAULT } from '@/navigation-menu-item/common/constants/FolderIconDefault';
|
||||
import { selectedNavigationMenuItemIdInEditModeState } from '@/navigation-menu-item/common/states/selectedNavigationMenuItemIdInEditModeState';
|
||||
import { useNavigationMenuItemSectionItems } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemSectionItems';
|
||||
import { useUpdateFolderInDraft } from '@/navigation-menu-item/edit/folder/hooks/useUpdateFolderInDraft';
|
||||
import { useNavigationMenuItemEditController } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemEditController';
|
||||
import { useNavigationMenuItemEditSectionItems } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemEditSectionItems';
|
||||
import { useNavigationMenuItemTitleEdit } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemTitleEdit';
|
||||
import { SidePanelPageInfoLayout } from '@/side-panel/components/SidePanelPageInfoLayout';
|
||||
import { sidePanelPageInfoState } from '@/side-panel/states/sidePanelPageInfoState';
|
||||
import { sidePanelShouldFocusTitleInputComponentState } from '@/side-panel/states/sidePanelShouldFocusTitleInputComponentState';
|
||||
@@ -31,8 +33,8 @@ export const SidePanelFolderInfo = () => {
|
||||
const selectedNavigationMenuItemIdInEditMode = useAtomStateValue(
|
||||
selectedNavigationMenuItemIdInEditModeState,
|
||||
);
|
||||
const items = useNavigationMenuItemSectionItems();
|
||||
const { updateFolderInDraft } = useUpdateFolderInDraft();
|
||||
const items = useNavigationMenuItemEditSectionItems();
|
||||
const { updateItem } = useNavigationMenuItemEditController();
|
||||
|
||||
const defaultLabel = t`New folder`;
|
||||
const placeholder = t`Folder name`;
|
||||
@@ -45,23 +47,18 @@ export const SidePanelFolderInfo = () => {
|
||||
)
|
||||
: undefined;
|
||||
|
||||
if (!selectedItem) return null;
|
||||
const { value, handleChange, handleSave } = useNavigationMenuItemTitleEdit({
|
||||
itemId: selectedItem?.id ?? null,
|
||||
itemName: selectedItem?.name ?? defaultLabel,
|
||||
defaultLabel,
|
||||
persistName: (name) => {
|
||||
if (isDefined(selectedItem)) {
|
||||
void updateItem(selectedItem.id, { name });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const itemId = selectedItem.id;
|
||||
const itemName = selectedItem.name ?? defaultLabel;
|
||||
|
||||
const handleChange = (text: string) => {
|
||||
updateFolderInDraft(itemId, { name: text });
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
const trimmed = itemName.trim();
|
||||
const finalName = trimmed.length > 0 ? trimmed : defaultLabel;
|
||||
|
||||
if (finalName !== itemName) {
|
||||
updateFolderInDraft(itemId, { name: finalName });
|
||||
}
|
||||
};
|
||||
if (!isDefined(selectedItem)) return null;
|
||||
|
||||
const selectedIconKey = selectedItem.icon ?? FOLDER_ICON_DEFAULT;
|
||||
const FolderIconComponent = getIcon(selectedIconKey);
|
||||
@@ -73,7 +70,7 @@ export const SidePanelFolderInfo = () => {
|
||||
dropdownId="side-panel-folder-icon-picker"
|
||||
selectedIconKey={selectedIconKey}
|
||||
onChange={({ iconKey }) =>
|
||||
updateFolderInDraft(itemId, { icon: iconKey })
|
||||
void updateItem(selectedItem.id, { icon: iconKey })
|
||||
}
|
||||
clickableComponent={
|
||||
<StyledClickableIconWrapper>
|
||||
@@ -87,9 +84,9 @@ export const SidePanelFolderInfo = () => {
|
||||
}
|
||||
title={
|
||||
<TitleInput
|
||||
instanceId={`folder-name-${itemId}`}
|
||||
instanceId={`folder-name-${selectedItem.id}`}
|
||||
sizeVariant="sm"
|
||||
value={itemName}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
placeholder={placeholder}
|
||||
onEnter={handleSave}
|
||||
|
||||
+17
-8
@@ -5,9 +5,11 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import { type IconComponent } from 'twenty-ui/display';
|
||||
|
||||
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
|
||||
import { AddToNavigationDragHandle } from '@/navigation-menu-item/display/dnd/components/AddToNavigationDragHandle';
|
||||
import { addToNavPayloadRegistryState } from '@/navigation-menu-item/common/states/addToNavPayloadRegistryState';
|
||||
import { navigationMenuItemEditSectionState } from '@/navigation-menu-item/common/states/navigationMenuItemEditSectionState';
|
||||
import type { AddToNavigationDragPayload } from '@/navigation-menu-item/common/types/add-to-navigation-drag-payload';
|
||||
import { AddToNavigationDragHandle } from '@/navigation-menu-item/display/dnd/components/AddToNavigationDragHandle';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
|
||||
const CommandMenuItemWithAddToNavigationDragDndKit = lazy(() =>
|
||||
@@ -62,9 +64,16 @@ export const SidePanelItemWithAddToNavigationDrag = ({
|
||||
const setAddToNavPayloadRegistry = useSetAtomState(
|
||||
addToNavPayloadRegistryState,
|
||||
);
|
||||
const navigationMenuItemEditSection = useAtomStateValue(
|
||||
navigationMenuItemEditSectionState,
|
||||
);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
const showDragAffordance = !disabled && !disableDrag && isHovered;
|
||||
// Favorites are added by click only; drag-to-add targets the workspace
|
||||
// sidebar and runs through layout-customization mode.
|
||||
const effectiveDisableDrag =
|
||||
disableDrag || navigationMenuItemEditSection === 'favorite';
|
||||
const showDragAffordance = !disabled && !effectiveDisableDrag && isHovered;
|
||||
const contextualDescription = showDragAffordance
|
||||
? t`Drag to add to navbar`
|
||||
: description;
|
||||
@@ -76,12 +85,12 @@ export const SidePanelItemWithAddToNavigationDrag = ({
|
||||
payload={payload}
|
||||
isHovered={showDragAffordance}
|
||||
disabled={disabled}
|
||||
disableDrag={disableDrag}
|
||||
disableDrag={effectiveDisableDrag}
|
||||
/>
|
||||
);
|
||||
|
||||
const registerPayload = () => {
|
||||
if (!disabled && !disableDrag && isDefined(dragIndex)) {
|
||||
if (!disabled && !effectiveDisableDrag && isDefined(dragIndex)) {
|
||||
setAddToNavPayloadRegistry((prev) => new Map(prev).set(id, payload));
|
||||
}
|
||||
};
|
||||
@@ -89,15 +98,15 @@ export const SidePanelItemWithAddToNavigationDrag = ({
|
||||
const menuItemContent = (
|
||||
<StyledDraggableMenuItem
|
||||
$disabled={disabled}
|
||||
$disableDrag={disableDrag}
|
||||
$disableDrag={effectiveDisableDrag}
|
||||
onMouseEnter={() => {
|
||||
if (!disabled && !disableDrag) {
|
||||
if (!disabled && !effectiveDisableDrag) {
|
||||
setIsHovered(true);
|
||||
registerPayload();
|
||||
}
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
if (!disabled && !disableDrag) setIsHovered(false);
|
||||
if (!disabled && !effectiveDisableDrag) setIsHovered(false);
|
||||
}}
|
||||
onMouseDown={registerPayload}
|
||||
>
|
||||
@@ -112,7 +121,7 @@ export const SidePanelItemWithAddToNavigationDrag = ({
|
||||
</StyledDraggableMenuItem>
|
||||
);
|
||||
|
||||
if (!isDefined(dragIndex) || disableDrag) {
|
||||
if (!isDefined(dragIndex) || effectiveDisableDrag) {
|
||||
return menuItemContent;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconLink, IconWorld } from 'twenty-ui/display';
|
||||
|
||||
import { LinkIconWithLinkOverlay } from '@/navigation-menu-item/display/link/components/LinkIconWithLinkOverlay';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { useUpdateLinkInDraft } from '@/navigation-menu-item/edit/link/hooks/useUpdateLinkInDraft';
|
||||
import { useNavigationMenuItemSectionItems } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemSectionItems';
|
||||
import { selectedNavigationMenuItemIdInEditModeState } from '@/navigation-menu-item/common/states/selectedNavigationMenuItemIdInEditModeState';
|
||||
import { LinkIconWithLinkOverlay } from '@/navigation-menu-item/display/link/components/LinkIconWithLinkOverlay';
|
||||
import { useNavigationMenuItemEditSectionItems } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemEditSectionItems';
|
||||
import { useNavigationMenuItemTitleEdit } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemTitleEdit';
|
||||
import { useNavigationMenuItemEditController } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemEditController';
|
||||
import { SidePanelPageInfoLayout } from '@/side-panel/components/SidePanelPageInfoLayout';
|
||||
import { sidePanelPageInfoState } from '@/side-panel/states/sidePanelPageInfoState';
|
||||
import { sidePanelShouldFocusTitleInputComponentState } from '@/side-panel/states/sidePanelShouldFocusTitleInputComponentState';
|
||||
@@ -24,8 +26,8 @@ export const SidePanelLinkInfo = () => {
|
||||
const selectedNavigationMenuItemIdInEditMode = useAtomStateValue(
|
||||
selectedNavigationMenuItemIdInEditModeState,
|
||||
);
|
||||
const items = useNavigationMenuItemSectionItems();
|
||||
const { updateLinkInDraft } = useUpdateLinkInDraft();
|
||||
const items = useNavigationMenuItemEditSectionItems();
|
||||
const { updateItem } = useNavigationMenuItemEditController();
|
||||
|
||||
const defaultLabel = t`Link label`;
|
||||
const placeholder = t`Link label`;
|
||||
@@ -38,23 +40,18 @@ export const SidePanelLinkInfo = () => {
|
||||
)
|
||||
: undefined;
|
||||
|
||||
if (!selectedItem) return null;
|
||||
const { value, handleChange, handleSave } = useNavigationMenuItemTitleEdit({
|
||||
itemId: selectedItem?.id ?? null,
|
||||
itemName: selectedItem?.name ?? defaultLabel,
|
||||
defaultLabel,
|
||||
persistName: (name) => {
|
||||
if (isDefined(selectedItem)) {
|
||||
void updateItem(selectedItem.id, { name });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const itemId = selectedItem.id;
|
||||
const itemName = selectedItem.name ?? defaultLabel;
|
||||
|
||||
const handleChange = (text: string) => {
|
||||
updateLinkInDraft(itemId, { name: text });
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
const trimmed = itemName.trim();
|
||||
const finalName = trimmed.length > 0 ? trimmed : defaultLabel;
|
||||
|
||||
if (finalName !== itemName) {
|
||||
updateLinkInDraft(itemId, { name: finalName });
|
||||
}
|
||||
};
|
||||
if (!isDefined(selectedItem)) return null;
|
||||
|
||||
return (
|
||||
<SidePanelPageInfoLayout
|
||||
@@ -68,9 +65,9 @@ export const SidePanelLinkInfo = () => {
|
||||
}
|
||||
title={
|
||||
<TitleInput
|
||||
instanceId={`link-label-${itemId}`}
|
||||
instanceId={`link-label-${selectedItem.id}`}
|
||||
sizeVariant="sm"
|
||||
value={itemName}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
placeholder={placeholder}
|
||||
onEnter={handleSave}
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from 'twenty-ui/display';
|
||||
|
||||
import { selectedNavigationMenuItemIdInEditModeState } from '@/navigation-menu-item/common/states/selectedNavigationMenuItemIdInEditModeState';
|
||||
import { useNavigationMenuItemSectionItems } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemSectionItems';
|
||||
import { useNavigationMenuItemEditSectionItems } from '@/navigation-menu-item/edit/hooks/useNavigationMenuItemEditSectionItems';
|
||||
import { SidePanelAskAiInfo } from '@/side-panel/components/SidePanelAskAiInfo';
|
||||
import { SidePanelFolderInfo } from '@/side-panel/components/SidePanelFolderInfo';
|
||||
import { SidePanelLinkInfo } from '@/side-panel/components/SidePanelLinkInfo';
|
||||
@@ -38,7 +38,7 @@ export const SidePanelPageInfo = ({ pageChip }: SidePanelPageInfoProps) => {
|
||||
const selectedNavigationMenuItemIdInEditMode = useAtomStateValue(
|
||||
selectedNavigationMenuItemIdInEditModeState,
|
||||
);
|
||||
const items = useNavigationMenuItemSectionItems();
|
||||
const items = useNavigationMenuItemEditSectionItems();
|
||||
|
||||
if (!isDefined(pageChip)) {
|
||||
return null;
|
||||
|
||||
+2
@@ -1,6 +1,7 @@
|
||||
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { addToNavPayloadRegistryState } from '@/navigation-menu-item/common/states/addToNavPayloadRegistryState';
|
||||
import { navigationMenuItemEditSectionState } from '@/navigation-menu-item/common/states/navigationMenuItemEditSectionState';
|
||||
import { pendingInsertionNavigationMenuItemState } from '@/navigation-menu-item/common/states/pendingInsertionNavigationMenuItemState';
|
||||
import { selectedNavigationMenuItemIdInEditModeState } from '@/navigation-menu-item/common/states/selectedNavigationMenuItemIdInEditModeState';
|
||||
import { viewableRecordIdState } from '@/object-record/record-side-panel/states/viewableRecordIdState';
|
||||
@@ -110,6 +111,7 @@ export const useSidePanelCloseAnimationCompleteCleanup = () => {
|
||||
store.set(sidePanelNavigationStackState.atom, []);
|
||||
store.set(selectedNavigationMenuItemIdInEditModeState.atom, null);
|
||||
store.set(pendingInsertionNavigationMenuItemState.atom, null);
|
||||
store.set(navigationMenuItemEditSectionState.atom, 'workspace');
|
||||
store.set(addToNavPayloadRegistryState.atom, new Map());
|
||||
resetSelectedItem();
|
||||
store.set(hasUserSelectedSidePanelListItemState.atom, false);
|
||||
|
||||
Reference in New Issue
Block a user