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:
+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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user