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:
Félix Malfait
2026-06-02 14:28:10 +02:00
committed by GitHub
parent 431f6ae98f
commit c18f8d6cf7
52 changed files with 932 additions and 1192 deletions
@@ -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;
}