Compare commits

...
Author SHA1 Message Date
Charles Bochet 8fb086e880 chore: remove dead code and rename objectMetadataItemsState to objectMetadataItemsSelector
Delete unused hooks (useWorkflowRunUnsafe, useGetViewById, useCreateViewFieldGroup,
useDeleteViewFieldGroup, useUpdateViewFieldGroup, useCreateManyViewFieldGroups,
useMoveViewColumns), unused SettingsSummaryCard component, and unused createEventContext
utility. Rename objectMetadataItemsState to objectMetadataItemsSelector across ~85 files
to better reflect that it is a derived selector, not a base atom.

Made-with: Cursor
2026-03-17 00:31:13 +01:00
Charles Bochet 267d8ca3b0 refactor: remove ProcessedNavigationMenuItem and derive display fields at point of use
Instead of pre-computing all display fields (label, link, icon, etc.) for every
navigation menu item upfront via sortNavigationMenuItems, each rendering
component now derives only the fields it needs from the raw NavigationMenuItem
and globally available atoms. This eliminates the redundant itemType field
(identical to type), removes the intermediate ProcessedNavigationMenuItem type,
and replaces the sort+enrich function with a pure filter+sort.

Made-with: Cursor
2026-03-17 00:20:53 +01:00
141 changed files with 1528 additions and 1670 deletions
+169
View File
@@ -0,0 +1,169 @@
# Twenty Frontend — Dead Code & Duplication Report
## 1. Unused Code (safe to delete)
### 1.1 Unused Hooks
| Hook | File | Notes |
|------|------|-------|
| `useWorkflowRunUnsafe` | `modules/workflow/hooks/useWorkflowRunUnsafe.ts` | Never imported. Duplicate of `useWorkflowRun` without schema validation. |
| `useGetViewById` | `modules/views/hooks/useGetViewById.ts` | Never imported. `useViewById` is used instead. |
| `useCreateViewFieldGroup` | `modules/views/hooks/useCreateViewFieldGroup.ts` | Never imported. CRUD done via `usePerformViewFieldGroupAPIPersist`. |
| `useDeleteViewFieldGroup` | `modules/views/hooks/useDeleteViewFieldGroup.ts` | Same as above. |
| `useUpdateViewFieldGroup` | `modules/views/hooks/useUpdateViewFieldGroup.ts` | Same as above. |
| `useCreateManyViewFieldGroups` | `modules/views/hooks/useCreateManyViewFieldGroups.ts` | Same as above. |
| `useMoveViewColumns` | `modules/views/hooks/useMoveViewColumns.ts` | Only imported by its own test file — no production usage. |
### 1.2 Unused Components
| Component | File | Notes |
|-----------|------|-------|
| `SettingsSummaryCard` | `modules/settings/components/SettingsSummaryCard.tsx` | Never imported anywhere. |
### 1.3 Unused Utilities
| Export | File | Notes |
|--------|------|-------|
| `createEventContext` + `EventContext` | `utils/createEventContext.ts` | Entire file is unused — zero imports. |
### 1.4 Filename Typo
| File | Issue |
|------|-------|
| `modules/views/hooks/useOpenCreateViewDropown.ts` | "Dropown" should be "Dropdown". The exported hook is correctly named `useOpenCreateViewDropdown`. |
---
## 2. Duplicated Logic
### 2.1 Search/filter reimplementations — **Low effort, High impact**
A central `filterBySearchQuery` utility exists in `utils/filterBySearchQuery.ts` with proper diacritic normalization, but several places reimplement search inline with plain `.toLowerCase().includes()`:
| File | Inline pattern |
|------|----------------|
| `modules/workflow/workflow-trigger/components/WorkflowEditTriggerDatabaseEventForm.tsx` | `option.label.toLowerCase().includes(searchValue.toLowerCase())` |
| `modules/workflow/workflow-variables/hooks/useVariableDropdown.ts` | `value.label?.toLowerCase().includes(searchInputValue.toLowerCase())` |
| `modules/settings/roles/.../SettingsRolePermissionsObjectLevelRecordLevelPermissionMeValueSelect.tsx` | `item.label.toLowerCase().includes(searchInput.toLowerCase())` |
| `pages/settings/ai/components/SettingsAIModelsTab.tsx` | Multi-field inline search |
**Fix:** Replace with `filterBySearchQuery` to get consistent diacritic-aware search for free.
### 2.2 `SelectInput` vs `SubMatchingSelectInput` — **Medium effort, High impact**
Both components in `modules/ui/input/components/SelectInput.tsx` and `modules/spreadsheet-import/.../SubMatchingSelectInput.tsx` have nearly identical logic:
- Same `optionsToSelect` memo with `normalizeSearchText`
- Same `optionsInDropDown` ordering (selected first)
- Same `handleOptionChange` pattern
**Fix:** Extract a shared `useSelectWithSearch(options, selectedOption)` hook.
### 2.3 `objectMetadataItemsState` + `.find()` — **Medium effort, High impact**
45+ files read `objectMetadataItemsState` and then do `.find(item => item.nameSingular === ...)` or similar lookups. Selectors like `objectMetadataItemFamilySelector` and `objectMetadataItemsByNameSingularMapSelector` already exist but are underused.
**Fix:** Introduce a `useObjectMetadataItemByName(name, 'singular' | 'plural')` hook and migrate call sites.
### 2.4 Object metadata sorting — **Low effort, Medium impact**
`a.labelSingular.localeCompare(b.labelSingular)` appears in 5+ places across hooks and utils. Same for `labelPlural`.
**Fix:** Add a shared `sortObjectMetadataByLabel(key)` comparator.
### 2.5 `objectMetadataItemsByNamePluralMapSelector` / `...SingularMapSelector` — **Low effort, Medium impact**
These two selectors are identical except for the map key (`namePlural` vs `nameSingular`).
**Fix:** Create a factory `createObjectMetadataItemsMapSelector(key)` and derive both from it.
### 2.6 Chart dropdown components — **Medium effort, Medium impact**
7 chart dropdown components share the same structure: `DropdownMenuSearchInput` + `filterBySearchQuery` + `DropdownMenuItemsContainer` + item rendering.
Files: `ChartGroupByFieldSelectionDropdownContentBase`, `ChartDataSourceDropdownContent`, `ChartRatioOptionValueSelectionDropdownContent`, `ChartAggregateOperationSelectionDropdownContent`, `ChartColorSelectionDropdownContent`, `ChartFieldSelectionForAggregateOperationDropdownContent`, `ChartGroupByFieldSelectionRelationFieldView`.
**Fix:** Extract a `ChartSettingsDropdown` wrapper that handles search + layout and accepts items via render prop.
### 2.7 Settings table row components — **Low effort, Medium impact**
6 settings table row components duplicate nearly identical styled components (`StyledNameContainer`, `StyledNameLabel`, `StyledInactiveLabel`):
- `SettingsObjectItemTableRow`
- `SettingsObjectFieldItemTableRow`
- `SettingsObjectRelationItemTableRow`
- `SettingsRolePermissionsObjectLevelObjectFieldPermissionTableRow`
- `SettingsRolePermissionsObjectLevelTableRow`
- `SettingsRoleAssignmentTableRow`
**Fix:** Extract shared styled components into `modules/settings/components/SettingsTableRowPrimitives.tsx`.
### 2.8 Date formatting scattered across 5+ files — **High effort, Medium impact**
Date utilities are spread across:
- `utils/date-utils.ts`
- `utils/format/formatDate.ts`
- `utils/string/formatDateString.ts`
- `utils/string/formatDateTimeString.ts`
- `utils/dates/formatZonedDateTimeDatePart.ts` / `formatZonedDateTimeTimePart.ts`
**Fix:** Consolidate under `utils/dates/` with clear responsibilities (parse, format, relative display).
---
## 3. Repeated Inline Patterns
### 3.1 Text truncation CSS — ~50 occurrences
```css
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
```
`EllipsisDisplay` exists in `modules/ui/field/display/components/EllipsisDisplay.tsx` but is rarely used.
**Fix:** Add a `truncateTextMixin` in shared styles. Use it in styled components instead of repeating these 3 lines.
### 3.2 Flex centering CSS — ~100+ occurrences
```css
display: flex;
align-items: center;
/* optional: justify-content: center */
```
**Fix:** Add `flexRowCenter` / `flexCenter` CSS mixins.
### 3.3 `e.stopPropagation()` wrapper — ~40 occurrences
`onClick={(e) => { e.stopPropagation(); ... }}` appears throughout.
A `StopPropagationContainer` exists in `modules/object-record/record-board/record-board-card/components/` but is barely reused.
**Fix:** Move `StopPropagationContainer` to a shared location and use it more broadly. Or add a `withStopPropagation(handler)` utility.
---
## 4. Priority Matrix
| # | Item | Effort | Impact | Type |
|---|------|--------|--------|------|
| 1 | Delete 7 unused hooks | **Very Low** | Medium | Dead code |
| 2 | Delete `SettingsSummaryCard` | **Very Low** | Low | Dead code |
| 3 | Delete `createEventContext.ts` | **Very Low** | Low | Dead code |
| 4 | Fix `useOpenCreateViewDropown` filename typo | **Very Low** | Low | Hygiene |
| 5 | Replace inline search filters with `filterBySearchQuery` | **Low** | High | Dedup |
| 6 | Add `truncateTextMixin` for ellipsis CSS | **Low** | High | Dedup |
| 7 | Extract shared settings table row styled components | **Low** | Medium | Dedup |
| 8 | Create `objectMetadataItemsMapSelector` factory | **Low** | Medium | Dedup |
| 9 | Add shared `sortObjectMetadataByLabel` comparator | **Low** | Medium | Dedup |
| 10 | Extract `useSelectWithSearch` from SelectInput/SubMatchingSelectInput | **Medium** | High | Dedup |
| 11 | Introduce `useObjectMetadataItemByName` hook | **Medium** | High | Dedup |
| 12 | Extract shared chart dropdown wrapper | **Medium** | Medium | Dedup |
| 13 | Consolidate date formatting utilities | **High** | Medium | Dedup |
Items 14 are pure low-hanging fruit — just deletions and a rename, no risk of regressions.
Items 59 are small refactors that pay off quickly across many files.
Items 1013 are larger but high-value unifications.
@@ -6,7 +6,7 @@ import { returnToPathState } from '@/auth/states/returnToPathState';
import { calendarBookingPageIdState } from '@/client-config/states/calendarBookingPageIdState';
import { useIsCurrentLocationOnAWorkspace } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspace';
import { useDefaultHomePagePath } from '@/navigation/hooks/useDefaultHomePagePath';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { useOnboardingStatus } from '@/onboarding/hooks/useOnboardingStatus';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useIsWorkspaceActivationStatusEqualsTo } from '@/workspace/hooks/useIsWorkspaceActivationStatusEqualsTo';
@@ -40,7 +40,7 @@ export const usePageChangeEffectNavigateLocation = () => {
appPaths.some((appPath) => isMatchingLocation(location, appPath));
const objectNamePlural = useParams().objectNamePlural ?? '';
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const objectMetadataItem = objectMetadataItems?.find(
(objectMetadataItem) => objectMetadataItem.namePlural === objectNamePlural,
);
@@ -3,7 +3,7 @@ import { type NoteTarget } from '@/activities/types/NoteTarget';
import { type Task } from '@/activities/types/Task';
import { type TaskTarget } from '@/activities/types/TaskTarget';
import { getActivityTargetObjectRecords } from '@/activities/utils/getActivityTargetObjectRecords';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
@@ -14,7 +14,7 @@ export const useActivityTargetObjectRecords = (
activityRecordId?: string,
activityTargets?: Nullable<NoteTarget[] | TaskTarget[]>,
) => {
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const activity = useAtomFamilyStateValue(
recordStoreFamilyState,
@@ -8,7 +8,7 @@ import { type ActivityTargetableObject } from '@/activities/types/ActivityTarget
import { type NoteTarget } from '@/activities/types/NoteTarget';
import { type TaskTarget } from '@/activities/types/TaskTarget';
import { getActivityTargetsFilter } from '@/activities/utils/getActivityTargetsFilter';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
@@ -35,7 +35,7 @@ export const useActivityTargetsForTargetableObjects = ({
limit: number;
}) => {
const objectMetadataItems = useAtomStateValue<ObjectMetadataItem[]>(
objectMetadataItemsState,
objectMetadataItemsSelector,
);
const isNoteTargetMigrated = useIsFeatureEnabled(
FeatureFlagKey.IS_NOTE_TARGET_MIGRATED,
@@ -1,5 +1,5 @@
import { type ActivityTargetWithTargetRecord } from '@/activities/types/ActivityTargetObject';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { useMultipleRecordPickerOpen } from '@/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerOpen';
import { useMultipleRecordPickerPerformSearch } from '@/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerPerformSearch';
@@ -31,7 +31,7 @@ export const useOpenActivityTargetCellEditMode = () => {
activityTargetObjectRecords,
}: OpenActivityTargetCellEditModeProps) => {
const objectMetadataItems = store
.get(objectMetadataItemsState.atom)
.get(objectMetadataItemsSelector.atom)
.filter(
(objectMetadataItem) =>
objectMetadataItem.isSearchable &&
@@ -3,7 +3,7 @@ import { type NoteTarget } from '@/activities/types/NoteTarget';
import { type TaskTarget } from '@/activities/types/TaskTarget';
import { getActivityTargetFieldNameForObject } from '@/activities/utils/getActivityTargetFieldNameForObject';
import { getJoinObjectNameSingular } from '@/activities/utils/getJoinObjectNameSingular';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
@@ -81,7 +81,7 @@ export const useUpdateActivityTargetFromCell = ({
? 'task'
: 'note';
const objectMetadataItems = store.get(objectMetadataItemsState.atom);
const objectMetadataItems = store.get(objectMetadataItemsSelector.atom);
const pickedObjectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
@@ -1,10 +1,10 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
export const useLinkedObjectObjectMetadataItem = (id: string | null) => {
const objectMetadataItems: ObjectMetadataItem[] = useAtomStateValue(
objectMetadataItemsState,
objectMetadataItemsSelector,
);
if (id === null) {
@@ -10,7 +10,7 @@ import { contextStoreCurrentViewTypeComponentState } from '@/context-store/state
import { contextStoreFiltersComponentState } from '@/context-store/states/contextStoreFiltersComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
import { getTabListInstanceIdFromPageLayoutId } from '@/page-layout/utils/getTabListInstanceIdFromPageLayoutId';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
@@ -35,7 +35,7 @@ export const useGetBrowsingContext = () => {
}),
);
const objectMetadataItems = store.get(objectMetadataItemsState.atom);
const objectMetadataItems = store.get(objectMetadataItemsSelector.atom);
const objectMetadataItem = objectMetadataItems.find(
(item) => item.id === objectMetadataItemId,
@@ -1,5 +1,5 @@
import { MentionRecordChip } from '@/mention/components/MentionRecordChip';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { RecordChip } from '@/object-record/components/RecordChip';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { createReactInlineContentSpec } from '@blocknote/react';
@@ -32,7 +32,7 @@ const LegacyMentionRenderer = ({
recordId: string;
objectMetadataId: string;
}) => {
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const objectMetadataItem = objectMetadataItems.find(
(item) => item.id === objectMetadataId,
@@ -2,7 +2,7 @@ import { type CommandMenuContextType } from '@/command-menu-item/contexts/Comman
import { CommandMenuContextProviderDefault } from '@/command-menu-item/contexts/CommandMenuContextProviderDefault';
import { CommandMenuContextProviderWorkflowObjects } from '@/command-menu-item/contexts/CommandMenuContextProviderWorkflowObjects';
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
@@ -23,7 +23,7 @@ export const CommandMenuContextProviderLegacy = ({
contextStoreCurrentObjectMetadataItemIdComponentState,
);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const objectMetadataItem =
objectMetadataItemOverride ??
@@ -7,7 +7,7 @@ import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context
import { contextStoreIsPageInEditModeComponentState } from '@/context-store/states/contextStoreIsPageInEditModeComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { useMountHeadlessFrontComponent } from '@/front-components/hooks/useMountHeadlessFrontComponent';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { useOpenFrontComponentInSidePanel } from '@/side-panel/hooks/useOpenFrontComponentInSidePanel';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
@@ -180,7 +180,7 @@ export const useCommandMenuItemFrontComponentCommands = (
contextStoreTargetedRecordsRuleComponentState,
);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const currentObjectMetadataItem = objectMetadataItems.find(
(item) => item.id === contextStoreCurrentObjectMetadataItemId,
@@ -27,7 +27,7 @@ import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context
import { contextStoreCurrentViewTypeComponentState } from '@/context-store/states/contextStoreCurrentViewTypeComponentState';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { ContextStoreViewType } from '@/context-store/types/ContextStoreViewType';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper';
import { SidePanelRouter } from '@/side-panel/components/SidePanelRouter';
import { SIDE_PANEL_COMPONENT_INSTANCE_ID } from '@/side-panel/constants/SidePanelComponentInstanceId';
@@ -102,7 +102,7 @@ const meta: Meta<typeof SidePanelRootPage> = {
},
]);
const objectMetadataItems = jotaiStore.get(objectMetadataItemsState.atom);
const objectMetadataItems = jotaiStore.get(objectMetadataItemsSelector.atom);
const companyMetadataItem = objectMetadataItems.find(
(item) => item.nameSingular === 'company',
);
@@ -2,7 +2,7 @@ import { MainContextStoreProviderEffect } from '@/context-store/components/MainC
import { metadataStoreState } from '@/metadata-store/states/metadataStoreState';
import { useIsSettingsPage } from '@/navigation/hooks/useIsSettingsPage';
import { useLastVisitedView } from '@/navigation/hooks/useLastVisitedView';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { useShowAuthModal } from '@/ui/layout/hooks/useShowAuthModal';
import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
@@ -60,7 +60,7 @@ export const MainContextStoreProvider = () => {
const [searchParams] = useSearchParams();
const viewIdQueryParamRaw = searchParams.get('viewId');
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const metadataStore = useAtomFamilyStateValue(metadataStoreState, 'views');
const views = useAtomStateValue(viewsSelector);
@@ -1,5 +1,5 @@
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
@@ -11,7 +11,7 @@ export const useContextStoreObjectMetadataItem = (
contextStoreInstanceId,
);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const objectMetadataItem = objectMetadataItems.find(
(objectMetadataItemToFind) =>
@@ -5,7 +5,7 @@ import { contextStoreFiltersComponentState } from '@/context-store/states/contex
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters';
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { useFilterValueDependencies } from '@/object-record/record-filter/hooks/useFilterValueDependencies';
import { RecordFilterOperand } from '@/object-record/record-filter/types/RecordFilterOperand';
@@ -50,7 +50,7 @@ export const useFindManyRecordsSelectedInContextStore = ({
const { filterValueDependencies } = useFilterValueDependencies();
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const allFieldMetadataItems = objectMetadataItems.flatMap(
(objectMetadataItem) => objectMetadataItem.fields,
@@ -5,7 +5,7 @@ import { IconGripVertical, type IconComponent } from 'twenty-ui/display';
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
import { NavigationMenuItemStyleIcon } from '@/navigation-menu-item/components/NavigationMenuItemStyleIcon';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { NavigationMenuItemType } from 'twenty-shared/types';
import type { AddToNavigationDragPayload } from '@/navigation-menu-item/types/add-to-navigation-drag-payload';
import { getEffectiveNavigationMenuItemColor } from '@/navigation-menu-item/utils/getEffectiveNavigationMenuItemColor';
@@ -86,7 +86,7 @@ export const AddToNavigationDragHandle = ({
? payload.iconColor
: undefined;
const effectiveColor = getEffectiveNavigationMenuItemColor(
{ itemType: payload.type as NavigationMenuItemType },
{ type: payload.type as NavigationMenuItemType },
objectColor,
);
const hasBackgroundColor =
@@ -1,11 +1,14 @@
import { Droppable } from '@hello-pangea/dnd';
import { useLingui } from '@lingui/react/macro';
import { useContext, useState } from 'react';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
import { NavigationSections } from '@/navigation-menu-item/constants/NavigationSections.constants';
import { useIsDropDisabledForSection } from '@/navigation-menu-item/hooks/useIsDropDisabledForSection';
import { createPortal } from 'react-dom';
import { useLocation, useNavigate } from 'react-router-dom';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { IconFolder, IconFolderOpen, IconHeartOff } from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input';
@@ -18,17 +21,18 @@ import { NavigationMenuItemFolderNavigationDrawerItemDropdown } from '@/navigati
import { NavigationMenuItemIcon } from '@/navigation-menu-item/components/NavigationMenuItemIcon';
import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER } from '@/navigation-menu-item/constants/NavigationMenuItemDefaultColorFolder';
import { NAVIGATION_MENU_ITEM_FOLDER_DELETE_MODAL_ID } from '@/navigation-menu-item/constants/NavigationMenuItemFolderDeleteModalId';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { NavigationMenuItemDragContext } from '@/navigation-menu-item/contexts/NavigationMenuItemDragContext';
import { useDeleteNavigationMenuItem } from '@/navigation-menu-item/hooks/useDeleteNavigationMenuItem';
import { useDeleteNavigationMenuItemFolder } from '@/navigation-menu-item/hooks/useDeleteNavigationMenuItemFolder';
import { useRenameNavigationMenuItemFolder } from '@/navigation-menu-item/hooks/useRenameNavigationMenuItemFolder';
import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/states/openNavigationMenuItemFolderIdsState';
import { getEffectiveNavigationMenuItemColor } from '@/navigation-menu-item/utils/getEffectiveNavigationMenuItemColor';
import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/utils/getNavigationMenuItemComputedLink';
import { getNavigationMenuItemLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemLabel';
import { getNavigationMenuItemObjectNameSingular } from '@/navigation-menu-item/utils/getNavigationMenuItemObjectNameSingular';
import { getNavigationMenuItemSecondaryLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemSecondaryLabel';
import { isLocationMatchingNavigationMenuItem } from '@/navigation-menu-item/utils/isLocationMatchingNavigationMenuItem';
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
@@ -45,14 +49,15 @@ import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/use
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
import { ViewKey } from '@/views/types/ViewKey';
import { isNonEmptyString } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
type CurrentWorkspaceMemberNavigationMenuItemsProps = {
folder: {
id: string;
folderName: string;
navigationMenuItems: ProcessedNavigationMenuItem[];
navigationMenuItems: NavigationMenuItem[];
};
isGroup: boolean;
isWorkspaceFolder?: boolean;
@@ -64,7 +69,8 @@ export const CurrentWorkspaceMemberNavigationMenuItems = ({
isWorkspaceFolder = false,
}: CurrentWorkspaceMemberNavigationMenuItemsProps) => {
const { t } = useLingui();
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const views = useAtomStateValue(viewsSelector);
const location = useLocation();
const navigate = useNavigate();
const currentPath = location.pathname;
@@ -105,13 +111,26 @@ export const CurrentWorkspaceMemberNavigationMenuItems = ({
}
if (!isOpen) {
const firstNonLinkItem = folder.navigationMenuItems.find(
(item) =>
item.itemType !== NavigationMenuItemType.LINK &&
isNonEmptyString(item.link),
);
if (isDefined(firstNonLinkItem?.link)) {
navigate(firstNonLinkItem.link);
const firstNonLinkItem = folder.navigationMenuItems.find((item) => {
if (item.type === NavigationMenuItemType.LINK) {
return false;
}
const computedLink = getNavigationMenuItemComputedLink(
item,
objectMetadataItems,
views,
);
return isNonEmptyString(computedLink);
});
if (isDefined(firstNonLinkItem)) {
const link = getNavigationMenuItemComputedLink(
firstNonLinkItem,
objectMetadataItems,
views,
);
if (isNonEmptyString(link)) {
navigate(link);
}
}
}
};
@@ -131,8 +150,19 @@ export const CurrentWorkspaceMemberNavigationMenuItems = ({
const { closeDropdown } = useCloseDropdown();
const selectedNavigationMenuItemIndex = folder.navigationMenuItems.findIndex(
(item) =>
isLocationMatchingNavigationMenuItem(currentPath, currentViewPath, item),
(item) => {
const computedLink = getNavigationMenuItemComputedLink(
item,
objectMetadataItems,
views,
);
return isLocationMatchingNavigationMenuItem(
currentPath,
currentViewPath,
item.type,
computedLink,
);
},
);
const { deleteNavigationMenuItem } = useDeleteNavigationMenuItem();
@@ -261,53 +291,84 @@ export const CurrentWorkspaceMemberNavigationMenuItems = ({
// oxlint-disable-next-line react/jsx-props-no-spreading
{...provided.droppableProps}
>
{folder.navigationMenuItems.map((navigationMenuItem, index) => (
<DraggableItem
key={navigationMenuItem.id}
draggableId={navigationMenuItem.id}
index={index}
isInsideScrollableContainer
itemComponent={
<NavigationDrawerSubItem
secondaryLabel={getNavigationMenuItemSecondaryLabel({
objectMetadataItems,
navigationMenuItemObjectNameSingular:
navigationMenuItem.objectNameSingular,
})}
label={navigationMenuItem.labelIdentifier}
Icon={() => (
<NavigationMenuItemIcon
navigationMenuItem={navigationMenuItem}
/>
)}
to={isDragging ? undefined : navigationMenuItem.link}
active={index === selectedNavigationMenuItemIndex}
subItemState={getNavigationSubItemLeftAdornment({
index,
arrayLength: navigationMenuItemFolderContentLength,
selectedIndex: selectedNavigationMenuItemIndex,
})}
rightOptions={
isWorkspaceFolder ? undefined : (
<LightIconButton
Icon={IconHeartOff}
onClick={(e) => {
e.stopPropagation();
deleteNavigationMenuItem(navigationMenuItem.id);
}}
accent="tertiary"
{folder.navigationMenuItems.map((navigationMenuItem, index) => {
const label = getNavigationMenuItemLabel(
navigationMenuItem,
objectMetadataItems,
views,
);
const computedLink = getNavigationMenuItemComputedLink(
navigationMenuItem,
objectMetadataItems,
views,
);
const objectNameSingular =
getNavigationMenuItemObjectNameSingular(
navigationMenuItem,
objectMetadataItems,
views,
);
const view = isDefined(navigationMenuItem.viewId)
? views.find(
(view) => view.id === navigationMenuItem.viewId,
)
: undefined;
const isIndexView = view?.key === ViewKey.INDEX;
return (
<DraggableItem
key={navigationMenuItem.id}
draggableId={navigationMenuItem.id}
index={index}
isInsideScrollableContainer
itemComponent={
<NavigationDrawerSubItem
secondaryLabel={
isIndexView
? undefined
: getNavigationMenuItemSecondaryLabel({
objectMetadataItems,
navigationMenuItemObjectNameSingular:
objectNameSingular ?? '',
})
}
label={label}
Icon={() => (
<NavigationMenuItemIcon
navigationMenuItem={navigationMenuItem}
/>
)
}
isDragging={isDragging}
triggerEvent="CLICK"
iconColor={getEffectiveNavigationMenuItemColor(
navigationMenuItem,
)}
/>
}
/>
))}
)}
to={isDragging ? undefined : computedLink}
active={index === selectedNavigationMenuItemIndex}
subItemState={getNavigationSubItemLeftAdornment({
index,
arrayLength: navigationMenuItemFolderContentLength,
selectedIndex: selectedNavigationMenuItemIndex,
})}
rightOptions={
isWorkspaceFolder ? undefined : (
<LightIconButton
Icon={IconHeartOff}
onClick={(e) => {
e.stopPropagation();
deleteNavigationMenuItem(
navigationMenuItem.id,
);
}}
accent="tertiary"
/>
)
}
isDragging={isDragging}
triggerEvent="CLICK"
iconColor={getEffectiveNavigationMenuItemColor(
navigationMenuItem,
)}
/>
}
/>
);
})}
{provided.placeholder}
</div>
)}
@@ -14,10 +14,14 @@ import { NavigationMenuItemDragContext } from '@/navigation-menu-item/contexts/N
import { useDeleteNavigationMenuItem } from '@/navigation-menu-item/hooks/useDeleteNavigationMenuItem';
import { useSortedNavigationMenuItems } from '@/navigation-menu-item/hooks/useSortedNavigationMenuItems';
import { getEffectiveNavigationMenuItemColor } from '@/navigation-menu-item/utils/getEffectiveNavigationMenuItemColor';
import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/utils/getNavigationMenuItemComputedLink';
import { getNavigationMenuItemLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemLabel';
import { getNavigationMenuItemObjectNameSingular } from '@/navigation-menu-item/utils/getNavigationMenuItemObjectNameSingular';
import { getNavigationMenuItemSecondaryLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemSecondaryLabel';
import { isLocationMatchingNavigationMenuItem } from '@/navigation-menu-item/utils/isLocationMatchingNavigationMenuItem';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
@@ -30,7 +34,8 @@ const StyledOrphanNavigationMenuItemsContainer = styled.div`
`;
export const CurrentWorkspaceMemberOrphanNavigationMenuItems = () => {
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const views = useAtomStateValue(viewsSelector);
const { navigationMenuItemsSorted } = useSortedNavigationMenuItems();
const { deleteNavigationMenuItem } = useDeleteNavigationMenuItem();
const currentPath = useLocation().pathname;
@@ -48,58 +53,77 @@ export const CurrentWorkspaceMemberOrphanNavigationMenuItems = () => {
>
{orphanNavigationMenuItems.length > 0 ? (
<>
{orphanNavigationMenuItems.map((navigationMenuItem, index) => (
<NavigationItemDropTarget
key={navigationMenuItem.id}
folderId={null}
index={index}
sectionId={NavigationSections.FAVORITES}
>
<DraggableItem
draggableId={navigationMenuItem.id}
{orphanNavigationMenuItems.map((navigationMenuItem, index) => {
const label = getNavigationMenuItemLabel(
navigationMenuItem,
objectMetadataItems,
views,
);
const computedLink = getNavigationMenuItemComputedLink(
navigationMenuItem,
objectMetadataItems,
views,
);
const objectNameSingular = getNavigationMenuItemObjectNameSingular(
navigationMenuItem,
objectMetadataItems,
views,
);
return (
<NavigationItemDropTarget
key={navigationMenuItem.id}
folderId={null}
index={index}
isInsideScrollableContainer={true}
itemComponent={
<StyledOrphanNavigationMenuItemsContainer>
<NavigationDrawerItem
secondaryLabel={getNavigationMenuItemSecondaryLabel({
objectMetadataItems,
navigationMenuItemObjectNameSingular:
navigationMenuItem.objectNameSingular,
})}
label={navigationMenuItem.labelIdentifier}
Icon={() => (
<NavigationMenuItemIcon
navigationMenuItem={navigationMenuItem}
/>
)}
iconColor={getEffectiveNavigationMenuItemColor(
navigationMenuItem,
)}
active={isLocationMatchingNavigationMenuItem(
currentPath,
currentViewPath,
navigationMenuItem,
)}
to={isDragging ? undefined : navigationMenuItem.link}
rightOptions={
<LightIconButton
Icon={IconHeartOff}
onClick={(e) => {
e.stopPropagation();
deleteNavigationMenuItem(navigationMenuItem.id);
}}
accent="tertiary"
/>
}
isDragging={isDragging}
triggerEvent="CLICK"
/>
</StyledOrphanNavigationMenuItemsContainer>
}
/>
</NavigationItemDropTarget>
))}
sectionId={NavigationSections.FAVORITES}
>
<DraggableItem
draggableId={navigationMenuItem.id}
index={index}
isInsideScrollableContainer={true}
itemComponent={
<StyledOrphanNavigationMenuItemsContainer>
<NavigationDrawerItem
secondaryLabel={getNavigationMenuItemSecondaryLabel({
objectMetadataItems,
navigationMenuItemObjectNameSingular:
objectNameSingular ?? '',
})}
label={label}
Icon={() => (
<NavigationMenuItemIcon
navigationMenuItem={navigationMenuItem}
/>
)}
iconColor={getEffectiveNavigationMenuItemColor(
navigationMenuItem,
)}
active={isLocationMatchingNavigationMenuItem(
currentPath,
currentViewPath,
navigationMenuItem.type,
computedLink,
)}
to={isDragging ? undefined : computedLink}
rightOptions={
<LightIconButton
Icon={IconHeartOff}
onClick={(e) => {
e.stopPropagation();
deleteNavigationMenuItem(navigationMenuItem.id);
}}
accent="tertiary"
/>
}
isDragging={isDragging}
triggerEvent="CLICK"
/>
</StyledOrphanNavigationMenuItemsContainer>
}
/>
</NavigationItemDropTarget>
);
})}
<NavigationItemDropTarget
folderId={null}
index={orphanNavigationMenuItems.length}
@@ -1,21 +1,25 @@
import { Droppable } from '@hello-pangea/dnd';
import { IconHeartOff } from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
import { NavigationMenuItemBackButton } from '@/navigation-menu-item/components/NavigationMenuItemBackButton';
import { NavigationMenuItemIcon } from '@/navigation-menu-item/components/NavigationMenuItemIcon';
import { useDeleteNavigationMenuItem } from '@/navigation-menu-item/hooks/useDeleteNavigationMenuItem';
import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/utils/getNavigationMenuItemComputedLink';
import { getNavigationMenuItemLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemLabel';
import { getNavigationMenuItemObjectNameSingular } from '@/navigation-menu-item/utils/getNavigationMenuItemObjectNameSingular';
import { getNavigationMenuItemSecondaryLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemSecondaryLabel';
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
type NavigationMenuItemFolderContentProps = {
folderId: string;
folderName: string;
navigationMenuItems: ProcessedNavigationMenuItem[];
navigationMenuItems: NavigationMenuItem[];
};
export const NavigationMenuItemFolderContent = ({
@@ -23,7 +27,8 @@ export const NavigationMenuItemFolderContent = ({
folderId,
navigationMenuItems,
}: NavigationMenuItemFolderContentProps) => {
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const views = useAtomStateValue(viewsSelector);
const { deleteNavigationMenuItem } = useDeleteNavigationMenuItem();
return (
@@ -36,41 +41,60 @@ export const NavigationMenuItemFolderContent = ({
// oxlint-disable-next-line react/jsx-props-no-spreading
{...provided.droppableProps}
>
{navigationMenuItems.map((navigationMenuItem, index) => (
<DraggableItem
key={navigationMenuItem.id}
draggableId={navigationMenuItem.id}
index={index}
isInsideScrollableContainer
itemComponent={
<NavigationDrawerItem
secondaryLabel={getNavigationMenuItemSecondaryLabel({
objectMetadataItems,
navigationMenuItemObjectNameSingular:
navigationMenuItem.objectNameSingular,
})}
label={navigationMenuItem.labelIdentifier}
Icon={() => (
<NavigationMenuItemIcon
navigationMenuItem={navigationMenuItem}
/>
)}
rightOptions={
<LightIconButton
Icon={IconHeartOff}
onClick={(e) => {
e.stopPropagation();
deleteNavigationMenuItem(navigationMenuItem.id);
}}
accent="tertiary"
/>
}
triggerEvent="CLICK"
to={navigationMenuItem.link}
/>
}
/>
))}
{navigationMenuItems.map((navigationMenuItem, index) => {
const label = getNavigationMenuItemLabel(
navigationMenuItem,
objectMetadataItems,
views,
);
const computedLink = getNavigationMenuItemComputedLink(
navigationMenuItem,
objectMetadataItems,
views,
);
const objectNameSingular =
getNavigationMenuItemObjectNameSingular(
navigationMenuItem,
objectMetadataItems,
views,
);
return (
<DraggableItem
key={navigationMenuItem.id}
draggableId={navigationMenuItem.id}
index={index}
isInsideScrollableContainer
itemComponent={
<NavigationDrawerItem
secondaryLabel={getNavigationMenuItemSecondaryLabel({
objectMetadataItems,
navigationMenuItemObjectNameSingular:
objectNameSingular ?? '',
})}
label={label}
Icon={() => (
<NavigationMenuItemIcon
navigationMenuItem={navigationMenuItem}
/>
)}
rightOptions={
<LightIconButton
Icon={IconHeartOff}
onClick={(e) => {
e.stopPropagation();
deleteNavigationMenuItem(navigationMenuItem.id);
}}
accent="tertiary"
/>
}
triggerEvent="CLICK"
to={computedLink}
/>
}
/>
);
})}
{provided.placeholder}
</div>
)}
@@ -1,12 +1,12 @@
import { isDefined } from 'twenty-shared/utils';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
import { NavigationMenuItemFolderContent } from '@/navigation-menu-item/components/NavigationMenuItemFolderContent';
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
type NavigationMenuItemFolderContentDispatcherEffectProps = {
folderName: string;
folderId: string;
navigationMenuItems?: ProcessedNavigationMenuItem[];
navigationMenuItems?: NavigationMenuItem[];
};
export const NavigationMenuItemFolderContentDispatcherEffect = ({
@@ -1,68 +1,86 @@
import { isNonEmptyString } from '@sniptt/guards';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { Avatar, IconLink, IconWorld, useIcons } from 'twenty-ui/display';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
import { LinkIconWithLinkOverlay } from '@/navigation-menu-item/components/LinkIconWithLinkOverlay';
import { StyledNavigationMenuItemIconContainer } from '@/navigation-menu-item/components/NavigationMenuItemIconContainer';
import { ObjectIconWithViewOverlay } from '@/navigation-menu-item/components/ObjectIconWithViewOverlay';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { useObjectNavItemColor } from '@/navigation-menu-item/hooks/useObjectNavItemColor';
import { getEffectiveNavigationMenuItemColor } from '@/navigation-menu-item/utils/getEffectiveNavigationMenuItemColor';
import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/utils/getNavigationMenuItemComputedLink';
import { getNavigationMenuItemIconStyleFromColor } from '@/navigation-menu-item/utils/getNavigationMenuItemIconStyleFromColor';
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
import { getNavigationMenuItemLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemLabel';
import { getNavigationMenuItemObjectNameSingular } from '@/navigation-menu-item/utils/getNavigationMenuItemObjectNameSingular';
import { recordIdentifierToObjectRecordIdentifier } from '@/navigation-menu-item/utils/recordIdentifierToObjectRecordIdentifier';
import { useGetStandardObjectIcon } from '@/object-metadata/hooks/useGetStandardObjectIcon';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
import { ViewKey } from '@/views/types/ViewKey';
export const NavigationMenuItemIcon = ({
navigationMenuItem,
}: {
navigationMenuItem: ProcessedNavigationMenuItem;
navigationMenuItem: NavigationMenuItem;
}) => {
const { getIcon } = useIcons();
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const { Icon: StandardIcon, IconColor } = useGetStandardObjectIcon(
navigationMenuItem.objectNameSingular ?? '',
);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const views = useAtomStateValue(viewsSelector);
const isRecord =
navigationMenuItem.itemType === NavigationMenuItemType.RECORD;
const objectNameSingular =
getNavigationMenuItemObjectNameSingular(
navigationMenuItem,
objectMetadataItems,
views,
) ?? '';
const { Icon: StandardIcon, IconColor } =
useGetStandardObjectIcon(objectNameSingular);
const isRecord = navigationMenuItem.type === NavigationMenuItemType.RECORD;
const view = isDefined(navigationMenuItem.viewId)
? views.find((view) => view.id === navigationMenuItem.viewId)
: undefined;
const isViewWithOverlay =
navigationMenuItem.itemType === NavigationMenuItemType.VIEW &&
navigationMenuItem.viewKey !== ViewKey.INDEX;
navigationMenuItem.type === NavigationMenuItemType.VIEW &&
isDefined(view) &&
view.key !== ViewKey.INDEX;
const objectMetadataItem = objectMetadataItems.find(
(item) => item.nameSingular === navigationMenuItem.objectNameSingular,
);
const objectNavItemColor = useObjectNavItemColor(
navigationMenuItem.objectNameSingular ?? '',
(item) => item.nameSingular === objectNameSingular,
);
const objectNavItemColor = useObjectNavItemColor(objectNameSingular);
const objectIconForView =
objectMetadataItem?.icon != null
? getIcon(objectMetadataItem.icon)
: StandardIcon;
const canShowViewOverlay =
isViewWithOverlay &&
isDefined(objectIconForView) &&
isDefined(navigationMenuItem.Icon);
isViewWithOverlay && isDefined(objectIconForView) && isDefined(view?.icon);
if (canShowViewOverlay) {
return (
<ObjectIconWithViewOverlay
ObjectIcon={objectIconForView}
ViewIcon={getIcon(navigationMenuItem.Icon!)}
ViewIcon={getIcon(view!.icon)}
objectColor={objectNavItemColor}
/>
);
}
if (navigationMenuItem.itemType === NavigationMenuItemType.LINK) {
if (navigationMenuItem.type === NavigationMenuItemType.LINK) {
const computedLink = getNavigationMenuItemComputedLink(
navigationMenuItem,
objectMetadataItems,
views,
);
return (
<LinkIconWithLinkOverlay
link={navigationMenuItem.link}
link={computedLink}
LinkIcon={IconLink}
DefaultIcon={IconWorld}
color={getEffectiveNavigationMenuItemColor(navigationMenuItem)}
@@ -70,9 +88,13 @@ export const NavigationMenuItemIcon = ({
);
}
const iconToUse =
StandardIcon ??
(navigationMenuItem.Icon ? getIcon(navigationMenuItem.Icon) : undefined);
const itemIcon = isRecord
? undefined
: objectMetadataItem?.icon
? getIcon(objectMetadataItem.icon)
: undefined;
const iconToUse = StandardIcon ?? itemIcon;
const effectiveColor = getEffectiveNavigationMenuItemColor(
navigationMenuItem,
objectNavItemColor,
@@ -88,14 +110,30 @@ export const NavigationMenuItemIcon = ({
? IconColor
: themeCssVariables.font.color.secondary;
const labelIdentifier = getNavigationMenuItemLabel(
navigationMenuItem,
objectMetadataItems,
views,
);
const recordIdentifier =
isRecord &&
isDefined(navigationMenuItem.targetRecordIdentifier) &&
isDefined(objectMetadataItem)
? recordIdentifierToObjectRecordIdentifier({
recordIdentifier: navigationMenuItem.targetRecordIdentifier,
objectMetadataItem,
})
: null;
const avatar = (
<Avatar
size={iconStyle ? 'sm' : 'md'}
type={navigationMenuItem.avatarType}
type={recordIdentifier?.avatarType ?? 'icon'}
Icon={iconToUse}
iconColor={iconColorToUse}
avatarUrl={navigationMenuItem.avatarUrl}
placeholder={navigationMenuItem.labelIdentifier}
avatarUrl={recordIdentifier?.avatarUrl ?? ''}
placeholder={labelIdentifier}
placeholderColorSeed={navigationMenuItem.targetRecordId ?? undefined}
/>
);
@@ -6,21 +6,24 @@ import {
import { useContext } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { ThemeContext } from 'twenty-ui/theme-constants';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
import { NavigationMenuItemIcon } from '@/navigation-menu-item/components/NavigationMenuItemIcon';
import { getNavigationMenuItemLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemLabel';
import { getNavigationMenuItemObjectNameSingular } from '@/navigation-menu-item/utils/getNavigationMenuItemObjectNameSingular';
import { getNavigationMenuItemSecondaryLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemSecondaryLabel';
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem';
import { getNavigationSubItemLeftAdornment } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemLeftAdornment';
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
import { ViewKey } from '@/views/types/ViewKey';
type WorkspaceNavigationMenuItemFolderDragCloneProps = {
draggableProvided: DraggableProvided;
draggableSnapshot: DraggableStateSnapshot;
rubric: DraggableRubric;
navigationMenuItems: ProcessedNavigationMenuItem[];
navigationMenuItems: NavigationMenuItem[];
navigationMenuItemFolderContentLength: number;
selectedNavigationMenuItemIndex: number;
};
@@ -34,13 +37,29 @@ export const WorkspaceNavigationMenuItemFolderDragClone = ({
selectedNavigationMenuItemIndex,
}: WorkspaceNavigationMenuItemFolderDragCloneProps) => {
const { theme } = useContext(ThemeContext);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const views = useAtomStateValue(viewsSelector);
const navigationMenuItem = navigationMenuItems[rubric.source.index];
if (!isDefined(navigationMenuItem)) {
return null;
}
const label = getNavigationMenuItemLabel(
navigationMenuItem,
objectMetadataItems,
views,
);
const objectNameSingular = getNavigationMenuItemObjectNameSingular(
navigationMenuItem,
objectMetadataItems,
views,
);
const view = isDefined(navigationMenuItem.viewId)
? views.find((view) => view.id === navigationMenuItem.viewId)
: undefined;
const isIndexView = view?.key === ViewKey.INDEX;
return (
<div
ref={draggableProvided.innerRef}
@@ -57,15 +76,14 @@ export const WorkspaceNavigationMenuItemFolderDragClone = ({
>
<NavigationDrawerSubItem
secondaryLabel={
navigationMenuItem.viewKey === ViewKey.INDEX
isIndexView
? undefined
: getNavigationMenuItemSecondaryLabel({
objectMetadataItems,
navigationMenuItemObjectNameSingular:
navigationMenuItem.objectNameSingular,
navigationMenuItemObjectNameSingular: objectNameSingular ?? '',
})
}
label={navigationMenuItem.labelIdentifier}
label={label}
Icon={() => (
<NavigationMenuItemIcon navigationMenuItem={navigationMenuItem} />
)}
@@ -1,13 +1,16 @@
import { NavigationMenuItemType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
import { NavigationMenuItemIcon } from '@/navigation-menu-item/components/NavigationMenuItemIcon';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { type NavigationMenuItemClickParams } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/utils/getNavigationMenuItemComputedLink';
import { getNavigationMenuItemLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemLabel';
import { getNavigationMenuItemObjectNameSingular } from '@/navigation-menu-item/utils/getNavigationMenuItemObjectNameSingular';
import { getNavigationMenuItemSecondaryLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemSecondaryLabel';
import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/utils/getObjectMetadataForNavigationMenuItem';
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem';
import { getNavigationSubItemLeftAdornment } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemLeftAdornment';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
@@ -15,7 +18,7 @@ import { viewsSelector } from '@/views/states/selectors/viewsSelector';
import { ViewKey } from '@/views/types/ViewKey';
type WorkspaceNavigationMenuItemFolderSubItemProps = {
navigationMenuItem: ProcessedNavigationMenuItem;
navigationMenuItem: NavigationMenuItem;
index: number;
arrayLength: number;
selectedNavigationMenuItemIndex: number;
@@ -36,22 +39,24 @@ export const WorkspaceNavigationMenuItemFolderSubItem = ({
const isNavigationMenuInEditMode = useAtomStateValue(
isNavigationMenuInEditModeState,
);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const views = useAtomStateValue(viewsSelector);
const objectMetadataItem =
navigationMenuItem.itemType === NavigationMenuItemType.OBJECT ||
navigationMenuItem.itemType === NavigationMenuItemType.VIEW ||
navigationMenuItem.itemType === NavigationMenuItemType.RECORD
navigationMenuItem.type === NavigationMenuItemType.OBJECT ||
navigationMenuItem.type === NavigationMenuItemType.VIEW ||
navigationMenuItem.type === NavigationMenuItemType.RECORD
? getObjectMetadataForNavigationMenuItem(
navigationMenuItem,
objectMetadataItems,
views,
)
: null;
const isEditableInEditMode =
isNavigationMenuInEditMode &&
isDefined(onNavigationMenuItemClick) &&
(navigationMenuItem.itemType === NavigationMenuItemType.LINK ||
(navigationMenuItem.type === NavigationMenuItemType.LINK ||
isDefined(objectMetadataItem));
const handleEditModeClick =
@@ -63,26 +68,42 @@ export const WorkspaceNavigationMenuItemFolderSubItem = ({
})
: undefined;
const label = getNavigationMenuItemLabel(
navigationMenuItem,
objectMetadataItems,
views,
);
const computedLink = getNavigationMenuItemComputedLink(
navigationMenuItem,
objectMetadataItems,
views,
);
const objectNameSingular = getNavigationMenuItemObjectNameSingular(
navigationMenuItem,
objectMetadataItems,
views,
);
const view = isDefined(navigationMenuItem.viewId)
? views.find((view) => view.id === navigationMenuItem.viewId)
: undefined;
const isIndexView = view?.key === ViewKey.INDEX;
return (
<NavigationDrawerSubItem
secondaryLabel={
navigationMenuItem.viewKey === ViewKey.INDEX
isIndexView
? undefined
: getNavigationMenuItemSecondaryLabel({
objectMetadataItems,
navigationMenuItemObjectNameSingular:
navigationMenuItem.objectNameSingular,
navigationMenuItemObjectNameSingular: objectNameSingular ?? '',
})
}
label={navigationMenuItem.labelIdentifier}
label={label}
Icon={() => (
<NavigationMenuItemIcon navigationMenuItem={navigationMenuItem} />
)}
to={
isContextDragging || handleEditModeClick
? undefined
: navigationMenuItem.link
}
to={isContextDragging || handleEditModeClick ? undefined : computedLink}
onClick={handleEditModeClick}
active={index === selectedNavigationMenuItemIndex}
isSelectedInEditMode={
@@ -13,7 +13,7 @@ import { LightIconButton } from 'twenty-ui/input';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { FOLDER_ICON_DEFAULT } from '@/navigation-menu-item/constants/FolderIconDefault';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { NavigationMenuItemType, SidePanelPages } from 'twenty-shared/types';
import { useOpenNavigationMenuItemInSidePanel } from '@/navigation-menu-item/hooks/useOpenNavigationMenuItemInSidePanel';
import { useSortedNavigationMenuItems } from '@/navigation-menu-item/hooks/useSortedNavigationMenuItems';
import {
@@ -25,16 +25,19 @@ import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/nav
import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/states/openNavigationMenuItemFolderIdsState';
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
import { filterWorkspaceNavigationMenuItems } from '@/navigation-menu-item/utils/filterWorkspaceNavigationMenuItems';
import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/utils/getNavigationMenuItemComputedLink';
import { getNavigationMenuItemLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemLabel';
import { preloadWorkspaceDndKit } from '@/navigation/preloadWorkspaceDndKit';
import { NavigationDrawerSectionForWorkspaceItems } from '@/object-metadata/components/NavigationDrawerSectionForWorkspaceItems';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { navigationMenuItemsSelector } from '@/navigation-menu-item/states/navigationMenuItemsSelector';
import { useNavigateSidePanel } from '@/side-panel/hooks/useNavigateSidePanel';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
import { useStore } from 'jotai';
import { SidePanelPages } from 'twenty-shared/types';
const StyledRightIconsContainer = styled.div`
align-items: center;
@@ -45,6 +48,8 @@ const StyledRightIconsContainer = styled.div`
export const WorkspaceNavigationMenuItems = () => {
const items = useWorkspaceSectionItems();
const { workspaceNavigationMenuItemsSorted } = useSortedNavigationMenuItems();
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const views = useAtomStateValue(viewsSelector);
const store = useStore();
const enterEditMode = () => {
const currentNavigationMenuItems = store.get(
@@ -88,16 +93,27 @@ export const WorkspaceNavigationMenuItems = () => {
);
openNavigationMenuItemInSidePanel({
pageTitle: t`Edit folder`,
pageIcon: getIcon(item.icon ?? item.Icon ?? FOLDER_ICON_DEFAULT),
pageIcon: getIcon(item.icon ?? FOLDER_ICON_DEFAULT),
});
const firstChild = workspaceNavigationMenuItemsSorted.find(
(navItem) =>
navItem.folderId === folderId &&
navItem.itemType !== NavigationMenuItemType.LINK &&
isNonEmptyString(navItem.link),
);
if (firstChild?.link) {
navigate(firstChild.link);
const firstChild = workspaceNavigationMenuItemsSorted.find((navItem) => {
if (navItem.folderId !== folderId) return false;
if (navItem.type === NavigationMenuItemType.LINK) return false;
const link = getNavigationMenuItemComputedLink(
navItem,
objectMetadataItems,
views,
);
return isNonEmptyString(link);
});
if (firstChild) {
const link = getNavigationMenuItemComputedLink(
firstChild,
objectMetadataItems,
views,
);
if (isNonEmptyString(link)) {
navigate(link);
}
}
};
@@ -106,16 +122,21 @@ export const WorkspaceNavigationMenuItems = () => {
objectMetadataItem: ObjectMetadataItem | null | undefined,
) => {
if (objectMetadataItem) {
const label =
item.type === NavigationMenuItemType.VIEW ||
item.type === NavigationMenuItemType.OBJECT
? getNavigationMenuItemLabel(item, objectMetadataItems, views)
: objectMetadataItem.labelSingular;
openNavigationMenuItemInSidePanel({
pageTitle:
item.itemType === NavigationMenuItemType.VIEW ||
item.itemType === NavigationMenuItemType.OBJECT
? item.labelIdentifier
: objectMetadataItem.labelSingular,
pageTitle: label,
pageIcon: getIcon(objectMetadataItem.icon),
});
}
const link = 'link' in item ? item.link : undefined;
const link = getNavigationMenuItemComputedLink(
item,
objectMetadataItems,
views,
);
if (isNonEmptyString(link)) {
navigate(link);
}
@@ -127,7 +148,7 @@ export const WorkspaceNavigationMenuItems = () => {
const { item, objectMetadataItem } = params;
setSelectedNavigationMenuItemInEditMode(item.id);
switch (item.itemType) {
switch (item.type) {
case NavigationMenuItemType.FOLDER:
openFolderAndNavigateToFirstChild(item.id, item);
break;
@@ -36,8 +36,8 @@ import { SortableDropTargetRefContext } from '@/navigation-menu-item/contexts/So
import { type NavigationMenuItemClickParams } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
import { getDndKitDropTargetId } from '@/navigation-menu-item/utils/getDndKitDropTargetId';
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
import { NavigationDrawerItemsCollapsableContainer } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemsCollapsableContainer';
import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem';
import { getNavigationSubItemLeftAdornment } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemLeftAdornment';
@@ -76,7 +76,7 @@ type WorkspaceNavigationMenuItemsFolderProps = {
folderName: string;
folderIconKey?: string | null;
folderColor?: string | null;
navigationMenuItems: ProcessedNavigationMenuItem[];
navigationMenuItems: NavigationMenuItem[];
isGroup: boolean;
isSelectedInEditMode?: boolean;
onEditModeClick?: () => void;
@@ -1,7 +0,0 @@
export enum NavigationMenuItemType {
FOLDER = 'FOLDER',
LINK = 'LINK',
OBJECT = 'OBJECT',
RECORD = 'RECORD',
VIEW = 'VIEW',
}
@@ -4,7 +4,7 @@ import { isDefined } from 'twenty-shared/utils';
import type { NavigationMenuItem } from '~/generated-metadata/graphql';
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/navigationMenuItemsDraftState';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
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/utils/computeInsertIndexAndPosition';
@@ -24,7 +24,7 @@ export const useAddRecordToNavigationMenuDraft = () => {
const setNavigationMenuItemsDraft = useSetAtomState(
navigationMenuItemsDraftState,
);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const addRecordToDraft = (
searchRecord: SearchRecordWithOptionalMetadataId,
@@ -4,14 +4,14 @@ import { useMutation } from '@apollo/client/react';
import { CreateNavigationMenuItemDocument } from '~/generated-metadata/graphql';
import { useNavigationMenuItemsData } from '@/navigation-menu-item/hooks/useNavigationMenuItemsData';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
export const useCreateNavigationMenuItem = () => {
const { navigationMenuItems, currentWorkspaceMemberId } =
useNavigationMenuItemsData();
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const [createNavigationMenuItemMutation] = useMutation(
CreateNavigationMenuItemDocument,
@@ -5,7 +5,7 @@ import { isDefined } from 'twenty-shared/utils';
import { IconFolder, IconLink, useIcons } from 'twenty-ui/display';
import { ADD_TO_NAV_SOURCE_DROPPABLE_ID } from '@/navigation-menu-item/constants/AddToNavSourceDroppableId';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { useAddFolderToNavigationMenuDraft } from '@/navigation-menu-item/hooks/useAddFolderToNavigationMenuDraft';
import { useAddLinkToNavigationMenuDraft } from '@/navigation-menu-item/hooks/useAddLinkToNavigationMenuDraft';
import { useAddObjectToNavigationMenuDraft } from '@/navigation-menu-item/hooks/useAddObjectToNavigationMenuDraft';
@@ -1,11 +1,9 @@
import { isDefined } from 'twenty-shared/utils';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
import { filterAndSortNavigationMenuItems } from '@/navigation-menu-item/utils/filterAndSortNavigationMenuItems';
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/utils/isNavigationMenuItemFolder';
import { recordIdentifierToObjectRecordIdentifier } from '@/navigation-menu-item/utils/recordIdentifierToObjectRecordIdentifier';
import { sortNavigationMenuItems } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { type ObjectRecordIdentifier } from '@/object-record/types/ObjectRecordIdentifier';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
@@ -16,7 +14,7 @@ type NavigationMenuItemFolder = {
folderName: string;
icon?: string | null;
color?: string | null;
navigationMenuItems: ReturnType<typeof sortNavigationMenuItems>[number][];
navigationMenuItems: NavigationMenuItem[];
};
type NavigationMenuItemFolderEntry = Pick<
@@ -26,7 +24,7 @@ type NavigationMenuItemFolderEntry = Pick<
export const useNavigationMenuItemsByFolder = () => {
const views = useAtomStateValue(viewsSelector);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const { navigationMenuItems, workspaceNavigationMenuItems } =
useNavigationMenuItemsData();
@@ -85,43 +83,10 @@ export const useNavigationMenuItemsByFolder = () => {
return sortedFolders.reduce<NavigationMenuItemFolder[]>((acc, folder) => {
const itemsInFolder = itemsByFolderId.get(folder.id) || [];
const targetRecordIdentifiersMap = itemsInFolder.reduce<
Map<string, ObjectRecordIdentifier>
>((map, item) => {
const itemTargetRecordId = item.targetRecordId;
if (!isDefined(itemTargetRecordId) || isDefined(item.viewId)) {
return map;
}
const targetRecordIdentifier = item.targetRecordIdentifier;
if (!isDefined(targetRecordIdentifier)) {
return map;
}
const itemObjectMetadata = objectMetadataItems.find(
(meta) => meta.id === item.targetObjectMetadataId,
);
if (isDefined(itemObjectMetadata)) {
const objectRecordIdentifier =
recordIdentifierToObjectRecordIdentifier({
recordIdentifier: targetRecordIdentifier,
objectMetadataItem: itemObjectMetadata,
});
map.set(itemTargetRecordId, objectRecordIdentifier);
}
return map;
}, new Map());
const sortedItems = sortNavigationMenuItems(
const sortedItems = filterAndSortNavigationMenuItems(
itemsInFolder,
true,
views,
objectMetadataItems,
targetRecordIdentifiersMap,
);
acc.push({
@@ -15,7 +15,7 @@ import { isNavigationMenuItemLink } from '@/navigation-menu-item/utils/isNavigat
import { orderFoldersForCreation } from '@/navigation-menu-item/utils/orderFoldersForCreation';
import { navigationMenuItemsSelector } from '@/navigation-menu-item/states/navigationMenuItemsSelector';
import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { useStore } from 'jotai';
export const useSaveNavigationMenuItemsDraft = () => {
@@ -37,7 +37,7 @@ export const useSaveNavigationMenuItemsDraft = () => {
if (!draft) return;
const objectMetadataItems = store.get(objectMetadataItemsState.atom);
const objectMetadataItems = store.get(objectMetadataItemsSelector.atom);
for (const draftItem of draft) {
if (draftItem.type !== NavigationMenuItemType.OBJECT) {
@@ -1,20 +1,31 @@
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
import { getNavigationMenuItemLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemLabel';
import { useSelectedNavigationMenuItemEditItem } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItem';
import { useSelectedNavigationMenuItemEditItemObjectMetadata } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItemObjectMetadata';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
const getLabelForItemType = (
itemType: NavigationMenuItemType,
item: { name?: string | null; labelIdentifier?: string | null },
const getLabelForItem = (
item: NavigationMenuItem,
objectMetadataItems: Parameters<typeof getNavigationMenuItemLabel>[1],
views: Parameters<typeof getNavigationMenuItemLabel>[2],
objectLabelSingular?: string | null,
): string => {
switch (itemType) {
switch (item.type) {
case NavigationMenuItemType.FOLDER:
return item.name ?? 'Folder';
case NavigationMenuItemType.LINK:
return item.name ?? 'Link';
case NavigationMenuItemType.OBJECT:
case NavigationMenuItemType.VIEW:
return item.labelIdentifier ?? objectLabelSingular ?? '';
return (
getNavigationMenuItemLabel(item, objectMetadataItems, views) ||
objectLabelSingular ||
''
);
default:
return objectLabelSingular ?? '';
}
@@ -24,11 +35,14 @@ export const useSelectedNavigationMenuItemEditItemLabel = () => {
const { selectedItem } = useSelectedNavigationMenuItemEditItem();
const { selectedItemObjectMetadata } =
useSelectedNavigationMenuItemEditItemObjectMetadata();
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const views = useAtomStateValue(viewsSelector);
const selectedItemLabel = selectedItem
? getLabelForItemType(
selectedItem.itemType,
? getLabelForItem(
selectedItem,
objectMetadataItems,
views,
selectedItemObjectMetadata?.labelSingular,
)
: null;
@@ -1,13 +1,7 @@
import { useMemo } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/utils/isNavigationMenuItemFolder';
import { isNavigationMenuItemLink } from '@/navigation-menu-item/utils/isNavigationMenuItemLink';
import { isNavigationMenuItemObject } from '@/navigation-menu-item/utils/isNavigationMenuItemObject';
import { recordIdentifierToObjectRecordIdentifier } from '@/navigation-menu-item/utils/recordIdentifierToObjectRecordIdentifier';
import { sortNavigationMenuItems } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { type ObjectRecordIdentifier } from '@/object-record/types/ObjectRecordIdentifier';
import { filterAndSortNavigationMenuItems } from '@/navigation-menu-item/utils/filterAndSortNavigationMenuItems';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
@@ -17,101 +11,23 @@ export const useSortedNavigationMenuItems = () => {
const { navigationMenuItems, workspaceNavigationMenuItems } =
useNavigationMenuItemsData();
const views = useAtomStateValue(viewsSelector);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const targetRecordIdentifiers = useMemo(() => {
const identifiersMap = new Map<string, ObjectRecordIdentifier>();
[...navigationMenuItems, ...workspaceNavigationMenuItems].forEach(
(navigationMenuItem) => {
if (isDefined(navigationMenuItem.viewId)) {
return;
}
const itemTargetRecordId = navigationMenuItem.targetRecordId;
if (!isDefined(itemTargetRecordId)) {
return;
}
const targetRecordIdentifier =
navigationMenuItem.targetRecordIdentifier;
if (!isDefined(targetRecordIdentifier)) {
return;
}
const objectMetadataItem = objectMetadataItems.find(
(item) => item.id === navigationMenuItem.targetObjectMetadataId,
);
if (!isDefined(objectMetadataItem)) {
return;
}
const objectRecordIdentifier = recordIdentifierToObjectRecordIdentifier(
{
recordIdentifier: targetRecordIdentifier,
objectMetadataItem,
},
);
identifiersMap.set(itemTargetRecordId, objectRecordIdentifier);
},
);
return identifiersMap;
}, [navigationMenuItems, workspaceNavigationMenuItems, objectMetadataItems]);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const navigationMenuItemsSorted = useMemo(() => {
return sortNavigationMenuItems(
return filterAndSortNavigationMenuItems(
navigationMenuItems,
true,
views,
objectMetadataItems,
targetRecordIdentifiers,
);
}, [
navigationMenuItems,
views,
objectMetadataItems,
targetRecordIdentifiers,
]);
}, [navigationMenuItems, views, objectMetadataItems]);
const workspaceNavigationMenuItemsSorted = useMemo(() => {
const filtered = workspaceNavigationMenuItems.filter((item) => {
if (isNavigationMenuItemFolder(item)) {
return true;
}
if (isNavigationMenuItemLink(item)) {
return true;
}
if (isNavigationMenuItemObject(item)) {
return isDefined(item.targetObjectMetadataId);
}
if (isDefined(item.viewId)) {
return views.some((view) => view.id === item.viewId);
}
const itemTargetRecordId = item.targetRecordId;
if (!isDefined(itemTargetRecordId)) {
return false;
}
const matchesTargetRecord =
targetRecordIdentifiers.has(itemTargetRecordId);
return matchesTargetRecord;
});
return sortNavigationMenuItems(
filtered,
true,
return filterAndSortNavigationMenuItems(
workspaceNavigationMenuItems,
views,
objectMetadataItems,
targetRecordIdentifiers,
);
}, [
workspaceNavigationMenuItems,
views,
objectMetadataItems,
targetRecordIdentifiers,
]);
}, [workspaceNavigationMenuItems, views, objectMetadataItems]);
return {
navigationMenuItemsSorted,
@@ -1,19 +1,23 @@
import { useLocation, useNavigate } from 'react-router-dom';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { useIsMobile } from 'twenty-ui/utilities';
import { isNonEmptyString } from '@sniptt/guards';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { openNavigationMenuItemFolderIdsState } from '@/navigation-menu-item/states/openNavigationMenuItemFolderIdsState';
import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/utils/getNavigationMenuItemComputedLink';
import { isLocationMatchingNavigationMenuItem } from '@/navigation-menu-item/utils/isLocationMatchingNavigationMenuItem';
import type { ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { currentNavigationMenuItemFolderIdState } from '@/ui/navigation/navigation-drawer/states/currentNavigationMenuItemFolderIdState';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
type UseWorkspaceFolderOpenStateParams = {
folderId: string;
navigationMenuItems: ProcessedNavigationMenuItem[];
navigationMenuItems: NavigationMenuItem[];
};
export const useWorkspaceFolderOpenState = ({
@@ -25,6 +29,8 @@ export const useWorkspaceFolderOpenState = ({
const currentPath = location.pathname;
const currentViewPath = location.pathname + location.search;
const isMobile = useIsMobile();
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const views = useAtomStateValue(viewsSelector);
const [openNavigationMenuItemFolderIds, setOpenNavigationMenuItemFolderIds] =
useAtomState(openNavigationMenuItemFolderIdsState);
@@ -48,20 +54,44 @@ export const useWorkspaceFolderOpenState = ({
}
if (!isOpen) {
const firstNonLinkItem = navigationMenuItems.find(
(item) =>
item.itemType !== NavigationMenuItemType.LINK &&
isNonEmptyString(item.link),
);
if (isDefined(firstNonLinkItem?.link)) {
navigate(firstNonLinkItem.link);
const firstNonLinkItem = navigationMenuItems.find((item) => {
if (item.type === NavigationMenuItemType.LINK) {
return false;
}
const computedLink = getNavigationMenuItemComputedLink(
item,
objectMetadataItems,
views,
);
return isNonEmptyString(computedLink);
});
if (isDefined(firstNonLinkItem)) {
const link = getNavigationMenuItemComputedLink(
firstNonLinkItem,
objectMetadataItems,
views,
);
if (isNonEmptyString(link)) {
navigate(link);
}
}
}
};
const selectedNavigationMenuItemIndex = navigationMenuItems.findIndex(
(item) =>
isLocationMatchingNavigationMenuItem(currentPath, currentViewPath, item),
(item) => {
const computedLink = getNavigationMenuItemComputedLink(
item,
objectMetadataItems,
views,
);
return isLocationMatchingNavigationMenuItem(
currentPath,
currentViewPath,
item.type,
computedLink,
);
},
);
return {
@@ -1,12 +1,11 @@
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
import { FOLDER_ICON_DEFAULT } from '@/navigation-menu-item/constants/FolderIconDefault';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/types/processed-navigation-menu-item';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/utils/getObjectMetadataForNavigationMenuItem';
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/utils/isNavigationMenuItemFolder';
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
@@ -17,32 +16,25 @@ import { useNavigationMenuItemsByFolder } from './useNavigationMenuItemsByFolder
import { useNavigationMenuItemsData } from './useNavigationMenuItemsData';
import { useSortedNavigationMenuItems } from './useSortedNavigationMenuItems';
export type FlatWorkspaceItem =
| ProcessedNavigationMenuItem
| (NavigationMenuItem & {
itemType: NavigationMenuItemType.FOLDER;
Icon: string;
});
export type NavigationMenuItemClickParams = {
item: FlatWorkspaceItem;
item: NavigationMenuItem;
objectMetadataItem?: ObjectMetadataItem | null;
};
export const useWorkspaceSectionItems = (): FlatWorkspaceItem[] => {
export const useWorkspaceSectionItems = (): NavigationMenuItem[] => {
const { workspaceNavigationMenuItems } = useNavigationMenuItemsData();
const { workspaceNavigationMenuItemsSorted } = useSortedNavigationMenuItems();
const { workspaceNavigationMenuItemsByFolder } =
useNavigationMenuItemsByFolder();
const views = useAtomStateValue(viewsSelector);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const flatWorkspaceItems = workspaceNavigationMenuItems
.filter((item) => !isDefined(item.folderId))
.sort((a, b) => a.position - b.position);
const processedObjectViewsById = new Map(
const processedItemsById = new Map(
workspaceNavigationMenuItemsSorted.map((item) => [item.id, item]),
);
@@ -53,25 +45,24 @@ export const useWorkspaceSectionItems = (): FlatWorkspaceItem[] => {
]),
);
const flatItems: FlatWorkspaceItem[] = flatWorkspaceItems.reduce<
FlatWorkspaceItem[]
const flatItems: NavigationMenuItem[] = flatWorkspaceItems.reduce<
NavigationMenuItem[]
>((acc, item) => {
if (isNavigationMenuItemFolder(item)) {
acc.push({
...item,
itemType: NavigationMenuItemType.FOLDER,
Icon: item.icon ?? FOLDER_ICON_DEFAULT,
icon: item.icon ?? FOLDER_ICON_DEFAULT,
});
} else {
const processedItem = processedObjectViewsById.get(item.id);
if (!isDefined(processedItem)) {
const validItem = processedItemsById.get(item.id);
if (!isDefined(validItem)) {
return acc;
}
if (processedItem.itemType === NavigationMenuItemType.LINK) {
acc.push(processedItem);
if (validItem.type === NavigationMenuItemType.LINK) {
acc.push(validItem);
} else {
const objectMetadataItem = getObjectMetadataForNavigationMenuItem(
processedItem,
validItem,
objectMetadataItems,
views,
);
@@ -82,7 +73,7 @@ export const useWorkspaceSectionItems = (): FlatWorkspaceItem[] => {
objectMetadataItem.id,
).canReadObjectRecords
) {
acc.push(processedItem);
acc.push(validItem);
}
}
}
@@ -90,7 +81,7 @@ export const useWorkspaceSectionItems = (): FlatWorkspaceItem[] => {
}, []);
return flatItems.flatMap((item) =>
item.itemType === NavigationMenuItemType.FOLDER
item.type === NavigationMenuItemType.FOLDER
? [item, ...(folderChildrenById.get(item.id) ?? [])]
: [item],
);
@@ -1,11 +0,0 @@
import { type ViewKey } from '@/views/types/ViewKey';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
import { type NavigationMenuItemDisplayFields } from '@/navigation-menu-item/utils/computeNavigationMenuItemDisplayFields';
import { type NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
export type ProcessedNavigationMenuItem = NavigationMenuItem &
NavigationMenuItemDisplayFields & {
viewKey?: ViewKey | null;
itemType: NavigationMenuItemType;
};
@@ -1,60 +0,0 @@
import { computeNavigationMenuItemDisplayFields } from '@/navigation-menu-item/utils/computeNavigationMenuItemDisplayFields';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { type ObjectRecordIdentifier } from '@/object-record/types/ObjectRecordIdentifier';
describe('computeNavigationMenuItemDisplayFields', () => {
const mockObjectMetadataItem: ObjectMetadataItem = {
id: 'metadata-id',
nameSingular: 'person',
namePlural: 'people',
labelSingular: 'Person',
labelPlural: 'People',
} as ObjectMetadataItem;
const mockObjectRecordIdentifier: ObjectRecordIdentifier = {
id: 'record-id',
name: 'John Doe',
avatarUrl: 'https://example.com/avatar.jpg',
avatarType: 'rounded',
linkToShowPage: '/app/objects/people/record-id',
};
it('should return null when objectMetadataItem or objectRecordIdentifier is null', () => {
expect(
computeNavigationMenuItemDisplayFields(null, mockObjectRecordIdentifier),
).toBeNull();
expect(
computeNavigationMenuItemDisplayFields(mockObjectMetadataItem, null),
).toBeNull();
});
it('should return display fields from metadata and record identifier', () => {
const result = computeNavigationMenuItemDisplayFields(
mockObjectMetadataItem,
mockObjectRecordIdentifier,
);
expect(result).toEqual({
labelIdentifier: 'John Doe',
avatarUrl: 'https://example.com/avatar.jpg',
avatarType: 'rounded',
link: '/app/objects/people/record-id',
objectNameSingular: 'person',
});
});
it('should default optional identifier fields to empty string or icon', () => {
const minimal: ObjectRecordIdentifier = {
id: 'record-id',
name: 'Jane Doe',
};
const result = computeNavigationMenuItemDisplayFields(
mockObjectMetadataItem,
minimal,
);
expect(result?.avatarUrl).toBe('');
expect(result?.avatarType).toBe('icon');
expect(result?.link).toBe('');
});
});
@@ -0,0 +1,242 @@
import { NavigationMenuItemType } from 'twenty-shared/types';
import { filterAndSortNavigationMenuItems } from '@/navigation-menu-item/utils/filterAndSortNavigationMenuItems';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { type View } from '@/views/types/View';
import { ViewKey } from '@/views/types/ViewKey';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
describe('filterAndSortNavigationMenuItems', () => {
const mockObjectMetadataItem: ObjectMetadataItem = {
id: 'metadata-id',
nameSingular: 'person',
namePlural: 'people',
labelPlural: 'People',
icon: 'IconUser',
} as ObjectMetadataItem;
const mockView: Pick<View, 'id' | 'objectMetadataId' | 'key'> = {
id: 'view-id',
objectMetadataId: 'metadata-id',
key: ViewKey.INDEX,
};
it('should return empty array when navigationMenuItems is empty', () => {
const result = filterAndSortNavigationMenuItems([], [], []);
expect(result).toEqual([]);
});
it('should keep view items when view and objectMetadata exist', () => {
const navigationMenuItem = {
id: 'item-id',
type: NavigationMenuItemType.VIEW,
viewId: 'view-id',
position: 1,
} as NavigationMenuItem;
const result = filterAndSortNavigationMenuItems(
[navigationMenuItem],
[mockView],
[mockObjectMetadataItem],
);
expect(result).toHaveLength(1);
expect(result[0].id).toBe('item-id');
});
it('should filter out view items when view is not found', () => {
const navigationMenuItem = {
id: 'item-id',
type: NavigationMenuItemType.VIEW,
viewId: 'non-existent-view-id',
position: 1,
} as NavigationMenuItem;
const result = filterAndSortNavigationMenuItems(
[navigationMenuItem],
[],
[mockObjectMetadataItem],
);
expect(result).toEqual([]);
});
it('should keep record items when targetRecordIdentifier exists', () => {
const navigationMenuItem = {
id: 'item-id',
type: NavigationMenuItemType.RECORD,
targetRecordId: 'record-id',
targetObjectMetadataId: 'metadata-id',
targetRecordIdentifier: {
id: 'record-id',
labelIdentifier: 'John Doe',
},
position: 2,
} as NavigationMenuItem;
const result = filterAndSortNavigationMenuItems(
[navigationMenuItem],
[],
[mockObjectMetadataItem],
);
expect(result).toHaveLength(1);
expect(result[0].id).toBe('item-id');
});
it('should filter out record items when targetRecordId is not defined', () => {
const navigationMenuItem = {
id: 'item-id',
type: NavigationMenuItemType.RECORD,
targetObjectMetadataId: 'metadata-id',
position: 1,
} as NavigationMenuItem;
const result = filterAndSortNavigationMenuItems(
[navigationMenuItem],
[],
[mockObjectMetadataItem],
);
expect(result).toEqual([]);
});
it('should filter out record items when objectMetadataItem is not found', () => {
const navigationMenuItem = {
id: 'item-id',
type: NavigationMenuItemType.RECORD,
targetRecordId: 'record-id',
targetObjectMetadataId: 'non-existent-metadata-id',
targetRecordIdentifier: {
id: 'record-id',
labelIdentifier: 'John',
},
position: 1,
} as NavigationMenuItem;
const result = filterAndSortNavigationMenuItems(
[navigationMenuItem],
[],
[mockObjectMetadataItem],
);
expect(result).toEqual([]);
});
it('should sort navigation menu items by position', () => {
const navigationMenuItems = [
{
id: 'item-3',
type: NavigationMenuItemType.RECORD,
targetRecordId: 'record-id-3',
targetObjectMetadataId: 'metadata-id',
targetRecordIdentifier: {
id: 'record-id-3',
labelIdentifier: 'C',
},
position: 3,
},
{
id: 'item-1',
type: NavigationMenuItemType.RECORD,
targetRecordId: 'record-id-1',
targetObjectMetadataId: 'metadata-id',
targetRecordIdentifier: {
id: 'record-id-1',
labelIdentifier: 'A',
},
position: 1,
},
{
id: 'item-2',
type: NavigationMenuItemType.RECORD,
targetRecordId: 'record-id-2',
targetObjectMetadataId: 'metadata-id',
targetRecordIdentifier: {
id: 'record-id-2',
labelIdentifier: 'B',
},
position: 2,
},
] as NavigationMenuItem[];
const result = filterAndSortNavigationMenuItems(
navigationMenuItems,
[],
[mockObjectMetadataItem],
);
expect(result).toHaveLength(3);
expect(result[0].id).toBe('item-1');
expect(result[1].id).toBe('item-2');
expect(result[2].id).toBe('item-3');
});
it('should keep link items', () => {
const result = filterAndSortNavigationMenuItems(
[
{
id: 'link-1',
type: NavigationMenuItemType.LINK,
link: 'https://example.com',
name: 'My Link',
position: 1,
} as NavigationMenuItem,
],
[],
[],
);
expect(result).toHaveLength(1);
expect(result[0].id).toBe('link-1');
});
it('should keep object items when objectMetadata exists', () => {
const result = filterAndSortNavigationMenuItems(
[
{
id: 'obj-1',
type: NavigationMenuItemType.OBJECT,
targetObjectMetadataId: 'metadata-id',
position: 1,
} as NavigationMenuItem,
],
[],
[mockObjectMetadataItem],
);
expect(result).toHaveLength(1);
expect(result[0].id).toBe('obj-1');
});
it('should filter out object items when objectMetadata is not found', () => {
const result = filterAndSortNavigationMenuItems(
[
{
id: 'obj-1',
type: NavigationMenuItemType.OBJECT,
targetObjectMetadataId: 'non-existent-id',
position: 1,
} as NavigationMenuItem,
],
[],
[mockObjectMetadataItem],
);
expect(result).toEqual([]);
});
it('should keep folder items', () => {
const result = filterAndSortNavigationMenuItems(
[
{
id: 'folder-1',
type: NavigationMenuItemType.FOLDER,
name: 'My Folder',
position: 1,
} as NavigationMenuItem,
],
[],
[],
);
expect(result).toHaveLength(1);
expect(result[0].id).toBe('folder-1');
});
});
@@ -1,4 +1,4 @@
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/utils/getObjectMetadataForNavigationMenuItem';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { type View } from '@/views/types/View';
@@ -30,7 +30,7 @@ const mockViews: View[] = [
describe('getObjectMetadataForNavigationMenuItem', () => {
it('should return null for link item type', () => {
const result = getObjectMetadataForNavigationMenuItem(
{ itemType: NavigationMenuItemType.LINK },
{ type: NavigationMenuItemType.LINK },
mockObjectMetadataItems,
mockViews,
);
@@ -39,7 +39,7 @@ describe('getObjectMetadataForNavigationMenuItem', () => {
it('should return object metadata for view item when view and metadata exist', () => {
const result = getObjectMetadataForNavigationMenuItem(
{ itemType: NavigationMenuItemType.VIEW, viewId: 'view-1' },
{ type: NavigationMenuItemType.VIEW, viewId: 'view-1' },
mockObjectMetadataItems,
mockViews,
);
@@ -49,7 +49,7 @@ describe('getObjectMetadataForNavigationMenuItem', () => {
it('should return null for view item when view is not found', () => {
const result = getObjectMetadataForNavigationMenuItem(
{ itemType: NavigationMenuItemType.VIEW, viewId: 'non-existent-view' },
{ type: NavigationMenuItemType.VIEW, viewId: 'non-existent-view' },
mockObjectMetadataItems,
mockViews,
);
@@ -61,7 +61,7 @@ describe('getObjectMetadataForNavigationMenuItem', () => {
{ id: 'orphan-view', objectMetadataId: 'non-existent-metadata' } as View,
];
const result = getObjectMetadataForNavigationMenuItem(
{ itemType: NavigationMenuItemType.VIEW, viewId: 'orphan-view' },
{ type: NavigationMenuItemType.VIEW, viewId: 'orphan-view' },
mockObjectMetadataItems,
viewsWithOrphanView,
);
@@ -71,7 +71,7 @@ describe('getObjectMetadataForNavigationMenuItem', () => {
it('should return object metadata for record item when metadata exists', () => {
const result = getObjectMetadataForNavigationMenuItem(
{
itemType: NavigationMenuItemType.RECORD,
type: NavigationMenuItemType.RECORD,
targetObjectMetadataId: 'metadata-2',
},
mockObjectMetadataItems,
@@ -83,7 +83,7 @@ describe('getObjectMetadataForNavigationMenuItem', () => {
it('should return null for record item when targetObjectMetadataId is undefined', () => {
const result = getObjectMetadataForNavigationMenuItem(
{ itemType: NavigationMenuItemType.RECORD },
{ type: NavigationMenuItemType.RECORD },
mockObjectMetadataItems,
mockViews,
);
@@ -93,7 +93,7 @@ describe('getObjectMetadataForNavigationMenuItem', () => {
it('should return null for record item when metadata is not found', () => {
const result = getObjectMetadataForNavigationMenuItem(
{
itemType: NavigationMenuItemType.RECORD,
type: NavigationMenuItemType.RECORD,
targetObjectMetadataId: 'non-existent-metadata',
},
mockObjectMetadataItems,
@@ -104,7 +104,7 @@ describe('getObjectMetadataForNavigationMenuItem', () => {
it('should return null for view item when viewId is undefined', () => {
const result = getObjectMetadataForNavigationMenuItem(
{ itemType: NavigationMenuItemType.VIEW },
{ type: NavigationMenuItemType.VIEW },
mockObjectMetadataItems,
mockViews,
);
@@ -1,4 +1,4 @@
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { isLocationMatchingNavigationMenuItem } from '@/navigation-menu-item/utils/isLocationMatchingNavigationMenuItem';
describe('isLocationMatchingNavigationMenuItem', () => {
@@ -7,20 +7,16 @@ describe('isLocationMatchingNavigationMenuItem', () => {
isLocationMatchingNavigationMenuItem(
'/app/objects/people',
'/app/objects/people?viewId=123',
{
itemType: NavigationMenuItemType.RECORD,
link: '/app/objects/people',
},
NavigationMenuItemType.RECORD,
'/app/objects/people',
),
).toBe(true);
expect(
isLocationMatchingNavigationMenuItem(
'/app/objects/companies',
'/app/objects/companies?viewId=123',
{
itemType: NavigationMenuItemType.VIEW,
link: '/app/objects/companies?viewId=123',
},
NavigationMenuItemType.VIEW,
'/app/objects/companies?viewId=123',
),
).toBe(true);
});
@@ -30,20 +26,16 @@ describe('isLocationMatchingNavigationMenuItem', () => {
isLocationMatchingNavigationMenuItem(
'/app/objects/people',
'/app/objects/people?viewId=123',
{
itemType: NavigationMenuItemType.RECORD,
link: '/app/objects/company',
},
NavigationMenuItemType.RECORD,
'/app/objects/company',
),
).toBe(false);
expect(
isLocationMatchingNavigationMenuItem(
'/app/objects/companies',
'/app/objects/companies?viewId=123',
{
itemType: NavigationMenuItemType.VIEW,
link: '/app/objects/companies?viewId=456',
},
NavigationMenuItemType.VIEW,
'/app/objects/companies?viewId=456',
),
).toBe(false);
});
@@ -1,360 +0,0 @@
import { NavigationMenuItemType } from 'twenty-shared/types';
import { sortNavigationMenuItems } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { type ObjectRecordIdentifier } from '@/object-record/types/ObjectRecordIdentifier';
import { type View } from '@/views/types/View';
import { ViewKey } from '@/views/types/ViewKey';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
jest.mock('twenty-shared/utils', () => {
const actual = jest.requireActual('twenty-shared/utils');
return {
...actual,
getAppPath: jest.fn((path, params, query) => {
const basePath = `/app/objects/${params.objectNamePlural}`;
const viewId = query?.viewId;
if (viewId !== undefined && viewId !== null) {
return `${basePath}?viewId=${viewId}`;
}
return basePath;
}),
};
});
describe('sortNavigationMenuItems', () => {
const mockObjectMetadataItem: ObjectMetadataItem = {
id: 'metadata-id',
nameSingular: 'person',
namePlural: 'people',
labelPlural: 'People',
icon: 'IconUser',
} as ObjectMetadataItem;
const mockView: Pick<
View,
'id' | 'name' | 'objectMetadataId' | 'icon' | 'key'
> = {
id: 'view-id',
name: 'All People',
objectMetadataId: 'metadata-id',
icon: 'IconUser',
key: ViewKey.INDEX,
};
const mockObjectRecordIdentifier: ObjectRecordIdentifier = {
id: 'record-id',
name: 'John Doe',
avatarUrl: 'https://example.com/avatar.jpg',
avatarType: 'rounded',
linkToShowPage: '/app/objects/people/record-id',
};
beforeEach(() => {
jest.clearAllMocks();
});
it('should return empty array when navigationMenuItems is empty', () => {
const result = sortNavigationMenuItems([], true, [], [], new Map());
expect(result).toEqual([]);
});
it('should process view link navigation menu items', () => {
const navigationMenuItem: NavigationMenuItem = {
id: 'item-id',
viewId: 'view-id',
position: 1,
} as NavigationMenuItem;
const result = sortNavigationMenuItems(
[navigationMenuItem],
true,
[mockView],
[mockObjectMetadataItem],
new Map(),
);
expect(result).toHaveLength(1);
expect(result[0]).toMatchObject({
id: 'item-id',
viewId: 'view-id',
position: 1,
labelIdentifier: 'People',
objectNameSingular: 'person',
Icon: 'IconUser',
});
expect(result[0].link).toContain('viewId=view-id');
});
it('should return null for view link when view is not found', () => {
const navigationMenuItem: NavigationMenuItem = {
id: 'item-id',
viewId: 'non-existent-view-id',
position: 1,
} as NavigationMenuItem;
const result = sortNavigationMenuItems(
[navigationMenuItem],
true,
[],
[mockObjectMetadataItem],
new Map(),
);
expect(result).toEqual([]);
});
it('should process record link navigation menu items', () => {
const navigationMenuItem: NavigationMenuItem = {
id: 'item-id',
targetRecordId: 'record-id',
targetObjectMetadataId: 'metadata-id',
position: 2,
} as NavigationMenuItem;
const targetRecordIdentifiers = new Map([
['record-id', mockObjectRecordIdentifier],
]);
const result = sortNavigationMenuItems(
[navigationMenuItem],
true,
[],
[mockObjectMetadataItem],
targetRecordIdentifiers,
);
expect(result).toHaveLength(1);
expect(result[0]).toMatchObject({
id: 'item-id',
targetRecordId: 'record-id',
targetObjectMetadataId: 'metadata-id',
position: 2,
labelIdentifier: 'John Doe',
avatarUrl: 'https://example.com/avatar.jpg',
avatarType: 'rounded',
link: '/app/objects/people/record-id',
objectNameSingular: 'person',
});
});
it('should return null for record link when targetRecordId is not defined', () => {
const navigationMenuItem: NavigationMenuItem = {
id: 'item-id',
targetObjectMetadataId: 'metadata-id',
position: 1,
} as NavigationMenuItem;
const result = sortNavigationMenuItems(
[navigationMenuItem],
true,
[],
[mockObjectMetadataItem],
new Map(),
);
expect(result).toEqual([]);
});
it('should return null for record link when objectMetadataItem is not found', () => {
const navigationMenuItem: NavigationMenuItem = {
id: 'item-id',
targetRecordId: 'record-id',
targetObjectMetadataId: 'non-existent-metadata-id',
position: 1,
} as NavigationMenuItem;
const targetRecordIdentifiers = new Map([
['record-id', mockObjectRecordIdentifier],
]);
const result = sortNavigationMenuItems(
[navigationMenuItem],
true,
[],
[mockObjectMetadataItem],
targetRecordIdentifiers,
);
expect(result).toEqual([]);
});
it('should return null for record link when targetRecordIdentifier is not found', () => {
const navigationMenuItem: NavigationMenuItem = {
id: 'item-id',
targetRecordId: 'non-existent-record-id',
targetObjectMetadataId: 'metadata-id',
position: 1,
} as NavigationMenuItem;
const result = sortNavigationMenuItems(
[navigationMenuItem],
true,
[],
[mockObjectMetadataItem],
new Map(),
);
expect(result).toEqual([]);
});
it('should return empty link when hasLinkToShowPage is false', () => {
const navigationMenuItem: NavigationMenuItem = {
id: 'item-id',
targetRecordId: 'record-id',
targetObjectMetadataId: 'metadata-id',
position: 2,
} as NavigationMenuItem;
const targetRecordIdentifiers = new Map([
['record-id', mockObjectRecordIdentifier],
]);
const result = sortNavigationMenuItems(
[navigationMenuItem],
false,
[],
[mockObjectMetadataItem],
targetRecordIdentifiers,
);
expect(result).toHaveLength(1);
expect(result[0].link).toBe('');
});
it('should sort navigation menu items by position', () => {
const navigationMenuItems: NavigationMenuItem[] = [
{
id: 'item-3',
targetRecordId: 'record-id-3',
targetObjectMetadataId: 'metadata-id',
position: 3,
},
{
id: 'item-1',
targetRecordId: 'record-id-1',
targetObjectMetadataId: 'metadata-id',
position: 1,
},
{
id: 'item-2',
targetRecordId: 'record-id-2',
targetObjectMetadataId: 'metadata-id',
position: 2,
},
] as NavigationMenuItem[];
const targetRecordIdentifiers = new Map([
['record-id-1', { ...mockObjectRecordIdentifier, id: 'record-id-1' }],
['record-id-2', { ...mockObjectRecordIdentifier, id: 'record-id-2' }],
['record-id-3', { ...mockObjectRecordIdentifier, id: 'record-id-3' }],
]);
const result = sortNavigationMenuItems(
navigationMenuItems,
true,
[],
[mockObjectMetadataItem],
targetRecordIdentifiers,
);
expect(result).toHaveLength(3);
expect(result[0].id).toBe('item-1');
expect(result[1].id).toBe('item-2');
expect(result[2].id).toBe('item-3');
});
it('should handle mixed view and record link items', () => {
const navigationMenuItems: NavigationMenuItem[] = [
{
id: 'view-item',
viewId: 'view-id',
position: 1,
},
{
id: 'record-item',
targetRecordId: 'record-id',
targetObjectMetadataId: 'metadata-id',
position: 2,
},
] as NavigationMenuItem[];
const targetRecordIdentifiers = new Map([
['record-id', mockObjectRecordIdentifier],
]);
const result = sortNavigationMenuItems(
navigationMenuItems,
true,
[mockView],
[mockObjectMetadataItem],
targetRecordIdentifiers,
);
expect(result).toHaveLength(2);
expect(result[0].id).toBe('view-item');
expect(result[0].objectNameSingular).toBe('person');
expect(result[1].id).toBe('record-item');
expect(result[1].objectNameSingular).toBe('person');
});
it('should handle navigationMenuItem with both viewId and targetRecordId (viewId takes precedence)', () => {
const navigationMenuItem: NavigationMenuItem = {
id: 'item-id',
viewId: 'view-id',
targetRecordId: 'record-id',
targetObjectMetadataId: 'metadata-id',
position: 1,
} as NavigationMenuItem;
const result = sortNavigationMenuItems(
[navigationMenuItem],
true,
[mockView],
[mockObjectMetadataItem],
new Map(),
);
expect(result).toHaveLength(1);
expect(result[0].objectNameSingular).toBe('person');
expect(result[0].viewId).toBe('view-id');
});
it('should process link items with protocol normalization and label from name or link', () => {
const withProtocol = sortNavigationMenuItems(
[
{
id: 'link-1',
type: NavigationMenuItemType.LINK,
link: 'https://example.com',
name: 'My Link',
position: 1,
} as NavigationMenuItem,
],
true,
[],
[],
new Map(),
);
expect(withProtocol[0].labelIdentifier).toBe('My Link');
expect(withProtocol[0].link).toBe('https://example.com');
const noProtocol = sortNavigationMenuItems(
[
{
id: 'link-2',
type: NavigationMenuItemType.LINK,
link: 'example.com',
position: 2,
} as NavigationMenuItem,
],
true,
[],
[],
new Map(),
);
expect(noProtocol[0].link).toBe('https://example.com');
expect(noProtocol[0].labelIdentifier).toBe('example.com');
});
});
@@ -1,31 +0,0 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { type ObjectRecordIdentifier } from '@/object-record/types/ObjectRecordIdentifier';
import { isDefined } from 'twenty-shared/utils';
export type NavigationMenuItemDisplayFields = {
labelIdentifier: string;
avatarUrl: string;
avatarType: 'icon' | 'rounded' | 'squared';
link: string;
objectNameSingular: string;
Icon?: string;
};
export const computeNavigationMenuItemDisplayFields = (
objectMetadataItem: ObjectMetadataItem | null,
objectRecordIdentifier: ObjectRecordIdentifier | null,
): NavigationMenuItemDisplayFields | null => {
if (!isDefined(objectMetadataItem) || !isDefined(objectRecordIdentifier)) {
return null;
}
const objectNameSingular = objectMetadataItem.nameSingular;
return {
labelIdentifier: objectRecordIdentifier.name,
avatarUrl: objectRecordIdentifier.avatarUrl ?? '',
avatarType: objectRecordIdentifier.avatarType ?? 'icon',
link: objectRecordIdentifier.linkToShowPage ?? '',
objectNameSingular,
};
};
@@ -0,0 +1,51 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { type View } from '@/views/types/View';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
export const filterAndSortNavigationMenuItems = (
navigationMenuItems: NavigationMenuItem[],
views: Pick<View, 'id' | 'objectMetadataId' | 'key'>[],
objectMetadataItems: Pick<ObjectMetadataItem, 'id'>[],
): NavigationMenuItem[] => {
return navigationMenuItems
.filter((item) => {
if (item.type === NavigationMenuItemType.FOLDER) {
return true;
}
if (item.type === NavigationMenuItemType.LINK) {
return true;
}
if (item.type === NavigationMenuItemType.OBJECT) {
return (
isDefined(item.targetObjectMetadataId) &&
objectMetadataItems.some(
(meta) => meta.id === item.targetObjectMetadataId,
)
);
}
if (item.type === NavigationMenuItemType.VIEW) {
if (!isDefined(item.viewId)) {
return false;
}
const view = views.find((view) => view.id === item.viewId);
return (
isDefined(view) &&
objectMetadataItems.some((meta) => meta.id === view.objectMetadataId)
);
}
if (item.type === NavigationMenuItemType.RECORD) {
return (
isDefined(item.targetRecordId) &&
isDefined(item.targetObjectMetadataId) &&
isDefined(item.targetRecordIdentifier) &&
objectMetadataItems.some(
(meta) => meta.id === item.targetObjectMetadataId,
)
);
}
return false;
})
.sort((a, b) => a.position - b.position);
};
@@ -2,27 +2,27 @@ import { isNonEmptyString } from '@sniptt/guards';
import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER } from '@/navigation-menu-item/constants/NavigationMenuItemDefaultColorFolder';
import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_LINK } from '@/navigation-menu-item/constants/NavigationMenuItemDefaultColorLink';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { NavigationMenuItemType } from 'twenty-shared/types';
export const getEffectiveNavigationMenuItemColor = (
navigationMenuItem: {
itemType: NavigationMenuItemType;
type: NavigationMenuItemType;
color?: string | null;
},
objectColor?: string,
): string | undefined => {
if (navigationMenuItem.itemType === NavigationMenuItemType.FOLDER) {
if (navigationMenuItem.type === NavigationMenuItemType.FOLDER) {
return isNonEmptyString(navigationMenuItem.color)
? navigationMenuItem.color
: DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER;
}
if (navigationMenuItem.itemType === NavigationMenuItemType.OBJECT) {
if (navigationMenuItem.type === NavigationMenuItemType.OBJECT) {
return objectColor;
}
if (navigationMenuItem.itemType === NavigationMenuItemType.VIEW) {
if (navigationMenuItem.type === NavigationMenuItemType.VIEW) {
return objectColor;
}
if (navigationMenuItem.itemType === NavigationMenuItemType.LINK) {
if (navigationMenuItem.type === NavigationMenuItemType.LINK) {
return DEFAULT_NAVIGATION_MENU_ITEM_COLOR_LINK;
}
return undefined;
@@ -0,0 +1,76 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { recordIdentifierToObjectRecordIdentifier } from '@/navigation-menu-item/utils/recordIdentifierToObjectRecordIdentifier';
import { type View } from '@/views/types/View';
import { ViewKey } from '@/views/types/ViewKey';
import { AppPath, NavigationMenuItemType } from 'twenty-shared/types';
import { getAppPath, isDefined } from 'twenty-shared/utils';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
export const getNavigationMenuItemComputedLink = (
item: NavigationMenuItem,
objectMetadataItems: ObjectMetadataItem[],
views: Pick<View, 'id' | 'objectMetadataId' | 'key'>[],
): string => {
switch (item.type) {
case NavigationMenuItemType.OBJECT: {
const objectMetadataItem = objectMetadataItems.find(
(meta) => meta.id === item.targetObjectMetadataId,
);
if (!isDefined(objectMetadataItem)) {
return '';
}
const indexView = views.find(
(view) =>
view.objectMetadataId === objectMetadataItem.id &&
view.key === ViewKey.INDEX,
);
return getAppPath(
AppPath.RecordIndexPage,
{ objectNamePlural: objectMetadataItem.namePlural },
indexView ? { viewId: indexView.id } : {},
);
}
case NavigationMenuItemType.VIEW: {
const view = views.find((view) => view.id === item.viewId);
if (!isDefined(view)) {
return '';
}
const objectMetadataItem = objectMetadataItems.find(
(meta) => meta.id === view.objectMetadataId,
);
if (!isDefined(objectMetadataItem)) {
return '';
}
return getAppPath(
AppPath.RecordIndexPage,
{ objectNamePlural: objectMetadataItem.namePlural },
{ viewId: item.viewId! },
);
}
case NavigationMenuItemType.LINK: {
const linkUrl = (item.link ?? '').trim();
if (linkUrl.startsWith('http://') || linkUrl.startsWith('https://')) {
return linkUrl;
}
return linkUrl ? `https://${linkUrl}` : '';
}
case NavigationMenuItemType.RECORD: {
const objectMetadataItem = objectMetadataItems.find(
(meta) => meta.id === item.targetObjectMetadataId,
);
if (
!isDefined(objectMetadataItem) ||
!isDefined(item.targetRecordIdentifier)
) {
return '';
}
const objectRecordIdentifier = recordIdentifierToObjectRecordIdentifier({
recordIdentifier: item.targetRecordIdentifier,
objectMetadataItem,
});
return objectRecordIdentifier.linkToShowPage ?? '';
}
default:
return '';
}
};
@@ -0,0 +1,49 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { type View } from '@/views/types/View';
import { ViewKey } from '@/views/types/ViewKey';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
export const getNavigationMenuItemLabel = (
item: NavigationMenuItem,
objectMetadataItems: Pick<
ObjectMetadataItem,
'id' | 'labelPlural' | 'nameSingular'
>[],
views: Pick<View, 'id' | 'name' | 'objectMetadataId' | 'key'>[],
): string => {
switch (item.type) {
case NavigationMenuItemType.OBJECT: {
const objectMetadataItem = objectMetadataItems.find(
(meta) => meta.id === item.targetObjectMetadataId,
);
return objectMetadataItem?.labelPlural ?? '';
}
case NavigationMenuItemType.VIEW: {
const view = views.find((view) => view.id === item.viewId);
if (!isDefined(view)) {
return '';
}
if (view.key === ViewKey.INDEX) {
const objectMetadataItem = objectMetadataItems.find(
(meta) => meta.id === view.objectMetadataId,
);
return objectMetadataItem?.labelPlural ?? view.name;
}
return view.name;
}
case NavigationMenuItemType.LINK: {
const linkUrl = (item.link ?? '').trim();
return (item.name ?? linkUrl) || 'Link';
}
case NavigationMenuItemType.RECORD: {
return item.targetRecordIdentifier?.labelIdentifier ?? '';
}
case NavigationMenuItemType.FOLDER: {
return item.name ?? 'Folder';
}
default:
return item.name ?? '';
}
};
@@ -0,0 +1,33 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { type View } from '@/views/types/View';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
export const getNavigationMenuItemObjectNameSingular = (
item: NavigationMenuItem,
objectMetadataItems: Pick<ObjectMetadataItem, 'id' | 'nameSingular'>[],
views: Pick<View, 'id' | 'objectMetadataId'>[],
): string | undefined => {
switch (item.type) {
case NavigationMenuItemType.OBJECT:
case NavigationMenuItemType.RECORD: {
const objectMetadataItem = objectMetadataItems.find(
(meta) => meta.id === item.targetObjectMetadataId,
);
return objectMetadataItem?.nameSingular;
}
case NavigationMenuItemType.VIEW: {
const view = views.find((view) => view.id === item.viewId);
if (!isDefined(view)) {
return undefined;
}
const objectMetadataItem = objectMetadataItems.find(
(meta) => meta.id === view.objectMetadataId,
);
return objectMetadataItem?.nameSingular;
}
default:
return undefined;
}
};
@@ -1,56 +1,56 @@
import { NavigationMenuItemType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/types/processed-navigation-menu-item';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { type View } from '@/views/types/View';
type NavigationMenuItemWithItemType = Pick<
ProcessedNavigationMenuItem,
'itemType' | 'viewId' | 'targetObjectMetadataId'
>;
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
export const getObjectMetadataForNavigationMenuItem = (
navigationMenuItem: NavigationMenuItemWithItemType,
navigationMenuItem: Pick<
NavigationMenuItem,
'type' | 'viewId' | 'targetObjectMetadataId'
>,
objectMetadataItems: ObjectMetadataItem[],
views: View[],
views: Pick<View, 'id' | 'objectMetadataId'>[],
): ObjectMetadataItem | null => {
if (navigationMenuItem.itemType === NavigationMenuItemType.LINK) {
if (navigationMenuItem.type === NavigationMenuItemType.LINK) {
return null;
}
if (
navigationMenuItem.itemType === NavigationMenuItemType.OBJECT &&
navigationMenuItem.type === NavigationMenuItemType.OBJECT &&
isDefined(navigationMenuItem.targetObjectMetadataId)
) {
const objectMetadataItem = objectMetadataItems.find(
(meta) => meta.id === navigationMenuItem.targetObjectMetadataId,
return (
objectMetadataItems.find(
(meta) => meta.id === navigationMenuItem.targetObjectMetadataId,
) ?? null
);
return objectMetadataItem ?? null;
}
if (
navigationMenuItem.itemType === NavigationMenuItemType.VIEW &&
navigationMenuItem.type === NavigationMenuItemType.VIEW &&
isDefined(navigationMenuItem.viewId)
) {
const view = views.find((view) => view.id === navigationMenuItem.viewId);
if (!isDefined(view)) {
return null;
}
const objectMetadataItem = objectMetadataItems.find(
(meta) => meta.id === view.objectMetadataId,
return (
objectMetadataItems.find((meta) => meta.id === view.objectMetadataId) ??
null
);
return objectMetadataItem ?? null;
}
if (
navigationMenuItem.itemType === NavigationMenuItemType.RECORD &&
navigationMenuItem.type === NavigationMenuItemType.RECORD &&
isDefined(navigationMenuItem.targetObjectMetadataId)
) {
const objectMetadataItem = objectMetadataItems.find(
(meta) => meta.id === navigationMenuItem.targetObjectMetadataId,
return (
objectMetadataItems.find(
(meta) => meta.id === navigationMenuItem.targetObjectMetadataId,
) ?? null
);
return objectMetadataItem ?? null;
}
return null;
@@ -1,15 +1,15 @@
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
import { NavigationMenuItemType } from 'twenty-shared/types';
export const isLocationMatchingNavigationMenuItem = (
currentPath: string,
currentViewPath: string,
navigationMenuItem: Pick<ProcessedNavigationMenuItem, 'itemType' | 'link'>,
navigationMenuItemType: NavigationMenuItemType,
computedLink: string,
) => {
const isViewBasedItem =
navigationMenuItem.itemType === NavigationMenuItemType.VIEW ||
navigationMenuItem.itemType === NavigationMenuItemType.OBJECT;
navigationMenuItemType === NavigationMenuItemType.VIEW ||
navigationMenuItemType === NavigationMenuItemType.OBJECT;
return isViewBasedItem
? navigationMenuItem.link === currentViewPath
: navigationMenuItem.link === currentPath;
? computedLink === currentViewPath
: computedLink === currentPath;
};
@@ -1,179 +0,0 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { type ObjectRecordIdentifier } from '@/object-record/types/ObjectRecordIdentifier';
import { type View } from '@/views/types/View';
import { ViewKey } from '@/views/types/ViewKey';
import { AppPath } from 'twenty-shared/types';
import { getAppPath, isDefined } from 'twenty-shared/utils';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/types/processed-navigation-menu-item';
import {
computeNavigationMenuItemDisplayFields,
type NavigationMenuItemDisplayFields,
} from './computeNavigationMenuItemDisplayFields';
import { isNavigationMenuItemLink } from './isNavigationMenuItemLink';
export { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
export type { ProcessedNavigationMenuItem } from '@/navigation-menu-item/types/processed-navigation-menu-item';
export const sortNavigationMenuItems = (
navigationMenuItems: NavigationMenuItem[],
hasLinkToShowPage: boolean,
views: Pick<View, 'id' | 'name' | 'objectMetadataId' | 'icon' | 'key'>[],
objectMetadataItems: ObjectMetadataItem[],
targetRecordIdentifiers: Map<string, ObjectRecordIdentifier>,
): ProcessedNavigationMenuItem[] => {
return navigationMenuItems
.map((navigationMenuItem) => {
if (
navigationMenuItem.type === NavigationMenuItemType.OBJECT &&
isDefined(navigationMenuItem.targetObjectMetadataId)
) {
const objectMetadataItem = objectMetadataItems.find(
(meta) => meta.id === navigationMenuItem.targetObjectMetadataId,
);
if (!isDefined(objectMetadataItem)) {
return null;
}
const indexView = views.find(
(view) =>
view.objectMetadataId === objectMetadataItem.id &&
view.key === ViewKey.INDEX,
);
const displayFields: NavigationMenuItemDisplayFields = {
labelIdentifier: objectMetadataItem.labelPlural,
avatarUrl: '',
avatarType: 'icon',
link: getAppPath(
AppPath.RecordIndexPage,
{ objectNamePlural: objectMetadataItem.namePlural },
indexView ? { viewId: indexView.id } : {},
),
objectNameSingular: objectMetadataItem.nameSingular,
Icon: objectMetadataItem.icon ?? 'IconBox',
};
return {
...navigationMenuItem,
...displayFields,
viewKey: ViewKey.INDEX,
itemType: NavigationMenuItemType.OBJECT,
};
}
if (isDefined(navigationMenuItem.viewId)) {
const view = views.find(
(view) => view.id === navigationMenuItem.viewId,
);
if (isDefined(view)) {
const objectMetadataItem = objectMetadataItems.find(
(meta) => meta.id === view.objectMetadataId,
);
if (!isDefined(objectMetadataItem)) {
return null;
}
const namePlural = objectMetadataItem.namePlural;
const isIndexView = view.key === ViewKey.INDEX;
const labelIdentifier =
isIndexView && isDefined(objectMetadataItem)
? objectMetadataItem.labelPlural
: view.name;
const icon =
isIndexView &&
isDefined(objectMetadataItem) &&
isDefined(objectMetadataItem.icon)
? objectMetadataItem.icon
: view.icon;
const displayFields: NavigationMenuItemDisplayFields = {
labelIdentifier,
avatarUrl: '',
avatarType: 'icon',
link: getAppPath(
AppPath.RecordIndexPage,
{ objectNamePlural: namePlural },
{ viewId: navigationMenuItem.viewId },
),
objectNameSingular: objectMetadataItem?.nameSingular ?? 'view',
Icon: icon,
};
return {
...navigationMenuItem,
...displayFields,
viewKey: view.key,
itemType: NavigationMenuItemType.VIEW,
};
}
return null;
}
if (isNavigationMenuItemLink(navigationMenuItem)) {
const linkUrl = (navigationMenuItem.link ?? '').trim();
const normalizedLink =
linkUrl.startsWith('http://') || linkUrl.startsWith('https://')
? linkUrl
: `https://${linkUrl}`;
const displayFields: NavigationMenuItemDisplayFields = {
labelIdentifier: (navigationMenuItem.name ?? linkUrl) || 'Link',
avatarUrl: '',
avatarType: 'icon',
link: normalizedLink,
objectNameSingular: 'link',
Icon: 'IconLink',
};
return {
...navigationMenuItem,
...displayFields,
itemType: NavigationMenuItemType.LINK,
};
}
if (!isDefined(navigationMenuItem.targetRecordId)) {
return null;
}
const objectMetadataItem = objectMetadataItems.find(
(item) => item.id === navigationMenuItem.targetObjectMetadataId,
);
if (!isDefined(objectMetadataItem)) {
return null;
}
const objectRecordIdentifier = targetRecordIdentifiers.get(
navigationMenuItem.targetRecordId,
);
if (!isDefined(objectRecordIdentifier)) {
return null;
}
const displayFields = computeNavigationMenuItemDisplayFields(
objectMetadataItem,
objectRecordIdentifier,
);
if (!isDefined(displayFields)) {
return null;
}
return {
...navigationMenuItem,
...displayFields,
link: hasLinkToShowPage ? displayFields.link : '',
itemType: NavigationMenuItemType.RECORD,
};
})
.filter(isDefined)
.sort((a, b) => a.position - b.position);
};
@@ -1,5 +1,5 @@
import { lastVisitedObjectMetadataItemIdState } from '@/navigation/states/lastVisitedObjectMetadataItemIdState';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { useStore } from 'jotai';
@@ -8,7 +8,7 @@ export const useSetLastVisitedObjectMetadataId = () => {
const store = useStore();
const setLastVisitedObjectMetadataId = useCallback(
({ objectMetadataItemId }: { objectMetadataItemId: string }) => {
const objectMetadataItems = store.get(objectMetadataItemsState.atom);
const objectMetadataItems = store.get(objectMetadataItemsSelector.atom);
const objectMetadataItem = objectMetadataItems.find(
(item) => item.id === objectMetadataItemId,
@@ -1,5 +1,5 @@
import { lastVisitedViewPerObjectMetadataItemState } from '@/navigation/states/lastVisitedViewPerObjectMetadataItemState';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
import { type ViewWithRelations } from '@/views/types/ViewWithRelations';
import { useCallback } from 'react';
@@ -20,7 +20,7 @@ export const useSetLastVisitedViewForObjectMetadataNamePlural = () => {
const view = views.find((view: ViewWithRelations) => view.id === viewId);
const objectMetadataItems = store.get(objectMetadataItemsState.atom);
const objectMetadataItems = store.get(objectMetadataItemsSelector.atom);
const objectMetadataItem = objectMetadataItems.find(
(item) => item.namePlural === objectNamePlural,
@@ -1,21 +1,29 @@
import { ObjectIconWithViewOverlay } from '@/navigation-menu-item/components/ObjectIconWithViewOverlay';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { useObjectNavItemColor } from '@/navigation-menu-item/hooks/useObjectNavItemColor';
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/utils/getNavigationMenuItemComputedLink';
import { getNavigationMenuItemLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemLabel';
import { recordIdentifierToObjectRecordIdentifier } from '@/navigation-menu-item/utils/recordIdentifierToObjectRecordIdentifier';
import { lastVisitedViewPerObjectMetadataItemState } from '@/navigation/states/lastVisitedViewPerObjectMetadataItemState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
import { ViewKey } from '@/views/types/ViewKey';
import { useLocation } from 'react-router-dom';
import { AppPath, CoreObjectNameSingular } from 'twenty-shared/types';
import {
AppPath,
CoreObjectNameSingular,
NavigationMenuItemType,
} from 'twenty-shared/types';
import { getAppPath, isDefined } from 'twenty-shared/utils';
import { Avatar, useIcons } from 'twenty-ui/display';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
export type NavigationDrawerItemForObjectMetadataItemProps = {
objectMetadataItem: ObjectMetadataItem;
navigationMenuItem?: ProcessedNavigationMenuItem;
navigationMenuItem?: NavigationMenuItem;
isSelectedInEditMode?: boolean;
onEditModeClick?: () => void;
onActiveItemClickWhenNotInEditMode?: () => void;
@@ -36,6 +44,8 @@ export const NavigationDrawerItemForObjectMetadataItem = ({
const lastVisitedViewPerObjectMetadataItem = useAtomStateValue(
lastVisitedViewPerObjectMetadataItemState,
);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const views = useAtomStateValue(viewsSelector);
const lastVisitedViewId =
lastVisitedViewPerObjectMetadataItem?.[objectMetadataItem.id];
@@ -48,24 +58,28 @@ export const NavigationDrawerItemForObjectMetadataItem = ({
const currentPath = location.pathname;
const currentPathWithSearch = `${location.pathname}${location.search}`;
const isRecord =
navigationMenuItem?.itemType === NavigationMenuItemType.RECORD;
const isView = navigationMenuItem?.itemType === NavigationMenuItemType.VIEW;
const isObject =
navigationMenuItem?.itemType === NavigationMenuItemType.OBJECT;
const isRecord = navigationMenuItem?.type === NavigationMenuItemType.RECORD;
const isView = navigationMenuItem?.type === NavigationMenuItemType.VIEW;
const isObject = navigationMenuItem?.type === NavigationMenuItemType.OBJECT;
const hasCustomLink = isRecord || isView || isObject;
const navigationPath = hasCustomLink
? navigationMenuItem!.link
? getNavigationMenuItemComputedLink(
navigationMenuItem!,
objectMetadataItems,
views,
)
: getAppPath(
AppPath.RecordIndexPage,
{ objectNamePlural: objectMetadataItem.namePlural },
lastVisitedViewId ? { viewId: lastVisitedViewId } : undefined,
);
const computedLink = hasCustomLink ? navigationPath : '';
const isActive = hasCustomLink
? (isView || isObject ? currentPathWithSearch : currentPath) ===
navigationMenuItem!.link
computedLink
: currentPath ===
getAppPath(AppPath.RecordIndexPage, {
objectNamePlural: objectMetadataItem.namePlural,
@@ -81,17 +95,32 @@ export const NavigationDrawerItemForObjectMetadataItem = ({
const shouldNavigate = !isNavigationMenuInEditMode;
const view = isDefined(navigationMenuItem?.viewId)
? views.find((view) => view.id === navigationMenuItem!.viewId)
: undefined;
const viewKey = view?.key ?? null;
const isViewWithCustomName =
isView &&
navigationMenuItem?.viewKey !== ViewKey.INDEX &&
isDefined(navigationMenuItem?.labelIdentifier);
isView && viewKey !== ViewKey.INDEX && isDefined(view);
const itemLabel = isDefined(navigationMenuItem)
? getNavigationMenuItemLabel(navigationMenuItem, objectMetadataItems, views)
: objectMetadataItem.labelPlural;
const label = isRecord
? navigationMenuItem!.labelIdentifier
? itemLabel
: isViewWithCustomName
? navigationMenuItem!.labelIdentifier
? itemLabel
: objectMetadataItem.labelPlural;
const recordIdentifier =
isRecord && isDefined(navigationMenuItem?.targetRecordIdentifier)
? recordIdentifierToObjectRecordIdentifier({
recordIdentifier: navigationMenuItem!.targetRecordIdentifier!,
objectMetadataItem,
})
: null;
const Icon = isRecord
? () => (
<Avatar
@@ -100,16 +129,16 @@ export const NavigationDrawerItemForObjectMetadataItem = ({
? 'squared'
: 'rounded'
}
avatarUrl={navigationMenuItem!.avatarUrl}
avatarUrl={recordIdentifier?.avatarUrl}
placeholderColorSeed={navigationMenuItem!.targetRecordId ?? undefined}
placeholder={navigationMenuItem!.labelIdentifier}
placeholder={itemLabel}
/>
)
: isViewWithCustomName && isDefined(navigationMenuItem?.Icon)
: isViewWithCustomName && isDefined(view?.icon)
? () => (
<ObjectIconWithViewOverlay
ObjectIcon={getIcon(objectMetadataItem.icon)}
ViewIcon={getIcon(navigationMenuItem!.Icon!)}
ViewIcon={getIcon(view!.icon)}
objectColor={objectNavItemColor}
/>
)
@@ -1,4 +1,4 @@
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { NavigationDrawerSectionForWorkspaceItemFolderContent } from '@/object-metadata/components/NavigationDrawerSectionForWorkspaceItemFolderContent';
import { NavigationDrawerSectionForWorkspaceItemLinkContent } from '@/object-metadata/components/NavigationDrawerSectionForWorkspaceItemLinkContent';
import { NavigationDrawerSectionForWorkspaceItemObjectContent } from '@/object-metadata/components/NavigationDrawerSectionForWorkspaceItemObjectContent';
@@ -18,7 +18,7 @@ export const NavigationDrawerSectionForWorkspaceItemContent = ({
onActiveObjectMetadataItemClick,
readOnly,
}: NavigationDrawerSectionForWorkspaceItemContentProps) => {
switch (item.itemType) {
switch (item.type) {
case NavigationMenuItemType.FOLDER:
return (
<NavigationDrawerSectionForWorkspaceItemFolderContent
@@ -24,7 +24,7 @@ export const NavigationDrawerSectionForWorkspaceItemFolderContent = ({
}: NavigationDrawerSectionForWorkspaceItemFolderContentProps) => {
const folderId = item.id;
const folderName = item.name ?? 'Folder';
const folderIconKey = item.Icon;
const folderIconKey = item.icon;
const folderColor = 'color' in item ? item.color : undefined;
const navigationMenuItems = folderChildrenById.get(item.id) ?? [];
const isGroup = folderCount > 1;
@@ -1,9 +1,12 @@
import { NavigationMenuItemIcon } from '@/navigation-menu-item/components/NavigationMenuItemIcon';
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
import type { ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
import { getNavigationMenuItemComputedLink } from '@/navigation-menu-item/utils/getNavigationMenuItemComputedLink';
import { getNavigationMenuItemLabel } from '@/navigation-menu-item/utils/getNavigationMenuItemLabel';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import type { WorkspaceSectionItemContentProps } from '@/object-metadata/components/WorkspaceSectionItemContentProps';
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
import { IconArrowUpRight } from 'twenty-ui/display';
import { themeCssVariables } from 'twenty-ui/theme-constants';
@@ -18,15 +21,24 @@ export const NavigationDrawerSectionForWorkspaceItemLinkContent = ({
const isNavigationMenuInEditMode = useAtomStateValue(
isNavigationMenuInEditModeState,
);
const linkItem = item as ProcessedNavigationMenuItem;
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const views = useAtomStateValue(viewsSelector);
const label = getNavigationMenuItemLabel(item, objectMetadataItems, views);
const computedLink = getNavigationMenuItemComputedLink(
item,
objectMetadataItems,
views,
);
return (
<NavigationDrawerItem
label={linkItem.labelIdentifier}
to={isNavigationMenuInEditMode || isDragging ? undefined : linkItem.link}
label={label}
to={isNavigationMenuInEditMode || isDragging ? undefined : computedLink}
onClick={
isNavigationMenuInEditMode ? editModeProps.onEditModeClick : undefined
}
Icon={() => <NavigationMenuItemIcon navigationMenuItem={linkItem} />}
Icon={() => <NavigationMenuItemIcon navigationMenuItem={item} />}
active={false}
isSelectedInEditMode={editModeProps.isSelectedInEditMode}
isDragging={isDragging}
@@ -1,8 +1,7 @@
import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/utils/getObjectMetadataForNavigationMenuItem';
import type { ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
import { NavigationDrawerItemForObjectMetadataItem } from '@/object-metadata/components/NavigationDrawerItemForObjectMetadataItem';
import type { WorkspaceSectionItemContentProps } from '@/object-metadata/components/WorkspaceSectionItemContentProps';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { viewsSelector } from '@/views/states/selectors/viewsSelector';
@@ -15,10 +14,10 @@ export const NavigationDrawerSectionForWorkspaceItemObjectContent = ({
isDragging,
onActiveObjectMetadataItemClick,
}: NavigationDrawerSectionForWorkspaceItemObjectContentProps) => {
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const views = useAtomStateValue(viewsSelector);
const objectMetadataItem = getObjectMetadataForNavigationMenuItem(
item as ProcessedNavigationMenuItem,
item,
objectMetadataItems,
views,
);
@@ -28,7 +27,7 @@ export const NavigationDrawerSectionForWorkspaceItemObjectContent = ({
return (
<NavigationDrawerItemForObjectMetadataItem
objectMetadataItem={objectMetadataItem}
navigationMenuItem={item as ProcessedNavigationMenuItem}
navigationMenuItem={item}
isSelectedInEditMode={editModeProps.isSelectedInEditMode}
onEditModeClick={editModeProps.onEditModeClick}
isDragging={isDragging}
@@ -1,21 +1,18 @@
import { NavigationDropTargetContext } from '@/navigation-menu-item/contexts/NavigationDropTargetContext';
import React, { lazy, Suspense, useContext } from 'react';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { AnimatedExpandableContainer } from 'twenty-ui/layout';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/constants/NavigationMenuItemDroppableIds';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import {
type FlatWorkspaceItem,
type NavigationMenuItemClickParams,
} from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
import { type NavigationMenuItemClickParams } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
import { getObjectMetadataForNavigationMenuItem } from '@/navigation-menu-item/utils/getObjectMetadataForNavigationMenuItem';
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
import type { EditModeProps } from '@/object-metadata/components/EditModeProps';
import { NavigationDrawerSectionForWorkspaceItemsListReadOnly } from '@/object-metadata/components/NavigationDrawerSectionForWorkspaceItemsListReadOnly';
import { WorkspaceSectionListEditModeFallback } from '@/object-metadata/components/WorkspaceSectionListEditModeFallback';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
@@ -40,7 +37,7 @@ const StyledWorkspaceSectionContentGapOffset = styled.div`
type NavigationDrawerSectionForWorkspaceItemsProps = {
sectionTitle: string;
items: FlatWorkspaceItem[];
items: NavigationMenuItem[];
rightIcon?: React.ReactNode;
selectedNavigationMenuItemId?: string | null;
onNavigationMenuItemClick?: (params: NavigationMenuItemClickParams) => void;
@@ -66,7 +63,7 @@ export const NavigationDrawerSectionForWorkspaceItems = ({
const views = useAtomStateValue(viewsSelector);
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const { addToNavigationFallbackDestination } = useContext(
NavigationDropTargetContext,
);
@@ -75,33 +72,34 @@ export const NavigationDrawerSectionForWorkspaceItems = ({
const isAddToNavigationDropTargetVisible =
addToNavigationFallbackDestination?.droppableId ===
NavigationMenuItemDroppableIds.WORKSPACE_ORPHAN_NAVIGATION_MENU_ITEMS;
const folderChildrenById = items.reduce<
Map<string, ProcessedNavigationMenuItem[]>
>((acc, item) => {
const folderId = item.folderId;
if (isDefined(folderId)) {
const children = acc.get(folderId) ?? [];
children.push(item as ProcessedNavigationMenuItem);
acc.set(folderId, children);
}
return acc;
}, new Map());
const folderChildrenById = items.reduce<Map<string, NavigationMenuItem[]>>(
(acc, item) => {
const folderId = item.folderId;
if (isDefined(folderId)) {
const children = acc.get(folderId) ?? [];
children.push(item);
acc.set(folderId, children);
}
return acc;
},
new Map(),
);
const filteredItems = flatItems.filter((item) => {
const type = item.itemType;
const itemType = item.type;
if (
type === NavigationMenuItemType.FOLDER ||
type === NavigationMenuItemType.LINK
itemType === NavigationMenuItemType.FOLDER ||
itemType === NavigationMenuItemType.LINK
) {
return true;
}
if (
type === NavigationMenuItemType.OBJECT ||
type === NavigationMenuItemType.VIEW ||
type === NavigationMenuItemType.RECORD
itemType === NavigationMenuItemType.OBJECT ||
itemType === NavigationMenuItemType.VIEW ||
itemType === NavigationMenuItemType.RECORD
) {
const objectMetadataItem = getObjectMetadataForNavigationMenuItem(
item as ProcessedNavigationMenuItem,
item,
objectMetadataItems,
views,
);
@@ -116,17 +114,19 @@ export const NavigationDrawerSectionForWorkspaceItems = ({
return false;
});
const getEditModeProps = (item: FlatWorkspaceItem): EditModeProps => {
const getEditModeProps = (item: NavigationMenuItem): EditModeProps => {
const itemId = item.id;
return {
isSelectedInEditMode: selectedNavigationMenuItemId === itemId,
onEditModeClick: onNavigationMenuItemClick
? () => {
const type = item.itemType;
const itemType = item.type;
const objectMetadataItem =
type === 'OBJECT' || type === 'VIEW' || type === 'RECORD'
itemType === 'OBJECT' ||
itemType === 'VIEW' ||
itemType === 'RECORD'
? getObjectMetadataForNavigationMenuItem(
item as ProcessedNavigationMenuItem,
item,
objectMetadataItems,
views,
)
@@ -1,7 +1,7 @@
import { WorkspaceDndKitDroppableSlot } from '@/navigation-menu-item/components/WorkspaceDndKitDroppableSlot';
import { WorkspaceDndKitSortableItem } from '@/navigation-menu-item/components/WorkspaceDndKitSortableItem';
import { NavigationMenuItemDroppableIds } from '@/navigation-menu-item/constants/NavigationMenuItemDroppableIds';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { NavigationDropTargetContext } from '@/navigation-menu-item/contexts/NavigationDropTargetContext';
import { useIsDropDisabledForSection } from '@/navigation-menu-item/hooks/useIsDropDisabledForSection';
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
@@ -46,7 +46,7 @@ export const WorkspaceSectionListDndKit = ({
NavigationDropTargetContext,
);
const folderCount = filteredItems.filter(
(item) => item.itemType === NavigationMenuItemType.FOLDER,
(item) => item.type === NavigationMenuItemType.FOLDER,
).length;
const isAddMenuItemButtonVisible = isNavigationMenuInEditMode;
return (
@@ -1,8 +1,8 @@
import { styled } from '@linaria/react';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import type { FlatWorkspaceItem } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
import type { NavigationMenuItem } from '~/generated-metadata/graphql';
import { NavigationMenuItemType } from 'twenty-shared/types';
import type { WorkspaceSectionListDndKitProps } from '@/object-metadata/components/WorkspaceSectionListDndKitProps';
import { NavigationDrawerSectionForWorkspaceItemContent } from '@/object-metadata/components/NavigationDrawerSectionForWorkspaceItemContent';
@@ -29,12 +29,12 @@ export const NavigationDrawerSectionForWorkspaceItemsListReadOnly = ({
onActiveObjectMetadataItemClick,
}: NavigationDrawerSectionForWorkspaceItemsListReadOnlyProps) => {
const folderCount = filteredItems.filter(
(item) => item.itemType === NavigationMenuItemType.FOLDER,
(item) => item.type === NavigationMenuItemType.FOLDER,
).length;
return (
<StyledList>
{filteredItems.map((item: FlatWorkspaceItem) => (
{filteredItems.map((item: NavigationMenuItem) => (
<NavigationDrawerSectionForWorkspaceItemContent
key={item.id}
item={item}
@@ -3,13 +3,13 @@ import React, { useMemo } from 'react';
import { allowRequestsToTwentyIconsState } from '@/client-config/states/allowRequestsToTwentyIcons';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { PreComputedChipGeneratorsContext } from '@/object-metadata/contexts/PreComputedChipGeneratorsContext';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { getRecordChipGenerators } from '@/object-record/utils/getRecordChipGenerators';
export const PreComputedChipGeneratorsProvider = ({
children,
}: React.PropsWithChildren) => {
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const allowRequestsToTwentyIcons = useAtomStateValue(
allowRequestsToTwentyIconsState,
);
@@ -5,12 +5,12 @@ import { themeCssVariables } from 'twenty-ui/theme-constants';
import { useIsMobile } from 'twenty-ui/utilities';
import { useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
import { WorkspaceNavigationMenuItemFolderSubItem } from '@/navigation-menu-item/components/WorkspaceNavigationMenuItemFolderSubItem';
import { FOLDER_ICON_DEFAULT } from '@/navigation-menu-item/constants/FolderIconDefault';
import { DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER } from '@/navigation-menu-item/constants/NavigationMenuItemDefaultColorFolder';
import { useWorkspaceFolderOpenState } from '@/navigation-menu-item/hooks/useWorkspaceFolderOpenState';
import type { ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
import { NavigationDrawerItemsCollapsableContainer } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemsCollapsableContainer';
@@ -35,7 +35,7 @@ type WorkspaceFolderReadOnlyProps = {
folderName: string;
folderIconKey?: string | null;
folderColor?: string | null;
navigationMenuItems: ProcessedNavigationMenuItem[];
navigationMenuItems: NavigationMenuItem[];
isGroup: boolean;
};
@@ -1,17 +1,14 @@
import type {
FlatWorkspaceItem,
NavigationMenuItemClickParams,
} from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
import type { ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
import type { NavigationMenuItemClickParams } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
import type { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import type { NavigationMenuItem } from '~/generated-metadata/graphql';
import type { EditModeProps } from '@/object-metadata/components/EditModeProps';
export type WorkspaceSectionItemContentProps = {
item: FlatWorkspaceItem;
item: NavigationMenuItem;
editModeProps: EditModeProps;
isDragging: boolean;
folderChildrenById: Map<string, ProcessedNavigationMenuItem[]>;
folderChildrenById: Map<string, NavigationMenuItem[]>;
folderCount: number;
selectedNavigationMenuItemId: string | null;
onNavigationMenuItemClick?: (params: NavigationMenuItemClickParams) => void;
@@ -1,16 +1,13 @@
import type {
FlatWorkspaceItem,
NavigationMenuItemClickParams,
} from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
import type { ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
import type { NavigationMenuItemClickParams } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
import type { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import type { NavigationMenuItem } from '~/generated-metadata/graphql';
import type { EditModeProps } from '@/object-metadata/components/EditModeProps';
export type WorkspaceSectionListDndKitProps = {
filteredItems: FlatWorkspaceItem[];
getEditModeProps: (item: FlatWorkspaceItem) => EditModeProps;
folderChildrenById: Map<string, ProcessedNavigationMenuItem[]>;
filteredItems: NavigationMenuItem[];
getEditModeProps: (item: NavigationMenuItem) => EditModeProps;
folderChildrenById: Map<string, NavigationMenuItem[]>;
selectedNavigationMenuItemId: string | null;
onNavigationMenuItemClick?: (params: NavigationMenuItemClickParams) => void;
onActiveObjectMetadataItemClick?: (
@@ -1,9 +1,9 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { getFieldMetadataItemById } from '@/object-metadata/utils/getFieldMetadataItemById';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
export const useFieldMetadataItemById = (fieldMetadataId: string) => {
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
return getFieldMetadataItemById({
fieldMetadataId,
@@ -1,9 +1,9 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { getFieldMetadataItemByIdOrThrow } from '@/object-metadata/utils/getFieldMetadataItemByIdOrThrow';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
export const useFieldMetadataItemByIdOrThrow = (fieldMetadataId: string) => {
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
return getFieldMetadataItemByIdOrThrow({
fieldMetadataId,
@@ -1,6 +1,6 @@
import { useCallback } from 'react';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { getFieldMetadataItemByIdOrThrow } from '@/object-metadata/utils/getFieldMetadataItemByIdOrThrow';
@@ -15,7 +15,7 @@ export const useGetFieldMetadataItemByIdOrThrow = () => {
const store = useStore();
const getFieldMetadataItemByIdOrThrowCallback = useCallback(
(fieldMetadataId: string): GetFieldMetadataItemByIdOrThrowResult => {
const objectMetadataItems = store.get(objectMetadataItemsState.atom);
const objectMetadataItems = store.get(objectMetadataItemsSelector.atom);
return getFieldMetadataItemByIdOrThrow({
fieldMetadataId,
@@ -1,9 +1,9 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { CustomError, isDefined } from 'twenty-shared/utils';
export const useGetObjectMetadataItemById = () => {
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const getObjectMetadataItemById = (objectId: string) => {
const objectMetadataItem = objectMetadataItems.find(
@@ -1,4 +1,4 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { getObjectRecordIdentifier } from '@/object-metadata/utils/getObjectRecordIdentifier';
import { type ObjectRecordIdentifier } from '@/object-record/types/ObjectRecordIdentifier';
@@ -6,7 +6,7 @@ import { type ObjectRecordIdentifier } from '@/object-record/types/ObjectRecordI
export const useGetObjectRecordIdentifierByNameSingular = (
allowRequestsToTwentyIcons: boolean,
) => {
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
return (record: any, objectNameSingular: string): ObjectRecordIdentifier => {
const objectMetadataItem = objectMetadataItems.find(
@@ -1,4 +1,4 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { CustomError, isDefined } from 'twenty-shared/utils';
@@ -7,7 +7,7 @@ export const useObjectMetadataItemById = ({
}: {
objectId: string;
}) => {
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const objectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) => objectMetadataItem.id === objectId,
@@ -2,9 +2,9 @@ import { objectMetadataItemsWithFieldsSelector } from '@/object-metadata/states/
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { createAtomSelector } from '@/ui/utilities/state/jotai/utils/createAtomSelector';
export const objectMetadataItemsState = createAtomSelector<
export const objectMetadataItemsSelector = createAtomSelector<
ObjectMetadataItem[]
>({
key: 'objectMetadataItemsState',
key: 'objectMetadataItemsSelector',
get: ({ get }) => get(objectMetadataItemsWithFieldsSelector),
});
@@ -1,5 +1,5 @@
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { getRecordConnectionFromRecords } from '@/object-record/cache/utils/getRecordConnectionFromRecords';
@@ -15,7 +15,7 @@ export const useUpsertFindManyRecordsQueryInCache = ({
}) => {
const apolloCoreClient = useApolloCoreClient();
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const upsertFindManyRecordsQueryInCache = <
@@ -1,7 +1,7 @@
import gql from 'graphql-tag';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
import { EMPTY_MUTATION } from '@/object-record/constants/EmptyMutation';
@@ -24,7 +24,7 @@ export const useCreateManyRecordsMutation = ({
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
if (isUndefinedOrNull(objectMetadataItem)) {
return { createManyRecordsMutation: EMPTY_MUTATION };
@@ -1,5 +1,5 @@
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { generateCreateOneRecordMutation } from '@/object-metadata/utils/generateCreateOneRecordMutation';
import { EMPTY_MUTATION } from '@/object-record/constants/EmptyMutation';
@@ -18,7 +18,7 @@ export const useCreateOneRecordMutation = ({
objectNameSingular,
});
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
@@ -2,7 +2,7 @@ import gql from 'graphql-tag';
import { useMemo } from 'react';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { isAggregationEnabled } from '@/object-metadata/utils/isAggregationEnabled';
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
@@ -21,7 +21,7 @@ export const useFindDuplicateRecordsQuery = ({
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const findDuplicateRecordsQuery = useMemo(
() => gql`
@@ -1,7 +1,7 @@
import { useMemo } from 'react';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { generateFindManyRecordsQuery } from '@/object-record/utils/generateFindManyRecordsQuery';
@@ -25,7 +25,7 @@ export const useFindManyRecordsQuery = ({
objectNameSingular,
});
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
@@ -2,7 +2,7 @@ import gql from 'graphql-tag';
import { useMemo } from 'react';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
import { type RecordGqlOperationGqlRecordFields } from 'twenty-shared/types';
@@ -22,7 +22,7 @@ export const useFindOneRecordQuery = ({
objectNameSingular,
});
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
@@ -1,7 +1,7 @@
import { useMemo } from 'react';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { type RecordGqlOperationGqlRecordFields } from 'twenty-shared/types';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
@@ -20,7 +20,7 @@ export const useGroupByRecordsQuery = ({
objectNameSingular,
});
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
@@ -1,7 +1,7 @@
import gql from 'graphql-tag';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
import { EMPTY_MUTATION } from '@/object-record/constants/EmptyMutation';
@@ -30,7 +30,7 @@ export const useMergeManyRecordsMutation = ({
const appliedRecordGqlFields = recordGqlFields ?? depthOneRecordGqlFields;
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
@@ -2,7 +2,7 @@ import { gql } from '@apollo/client';
import { isNonEmptyArray, isUndefined } from '@sniptt/guards';
import { useMemo } from 'react';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery';
@@ -18,7 +18,7 @@ export const useGenerateCombinedFindManyRecordsQuery = ({
}: {
operationSignatures: RecordGqlOperationSignature[];
}) => {
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
return useMemo(() => {
@@ -1,4 +1,4 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { ObjectFilterDropdownComponentInstanceContext } from '@/object-record/object-filter-dropdown/states/contexts/ObjectFilterDropdownComponentInstanceContext';
import { fieldMetadataItemIdUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/fieldMetadataItemIdUsedInDropdownComponentState';
@@ -16,7 +16,7 @@ export const fieldMetadataItemUsedInDropdownComponentSelector =
componentStateKey,
);
const objectMetadataItems = get(objectMetadataItemsState);
const objectMetadataItems = get(objectMetadataItemsSelector);
const correspondingFieldMetadataItem = objectMetadataItems
.flatMap((objectMetadataItem) => objectMetadataItem.fields)
@@ -1,4 +1,4 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { hasObjectMetadataItemPositionField } from '@/object-metadata/utils/hasObjectMetadataItemPositionField';
import { RecordCalendarComponentInstanceContext } from '@/object-record/record-calendar/states/contexts/RecordCalendarComponentInstanceContext';
@@ -26,7 +26,7 @@ export const calendarDayRecordIdsComponentFamilySelector =
recordIndexCalendarFieldMetadataIdState,
);
const objectMetadataItems = get(objectMetadataItemsState);
const objectMetadataItems = get(objectMetadataItemsSelector);
const objectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.fields.some(
@@ -1,4 +1,4 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { isActiveFieldMetadataItem } from '@/object-metadata/utils/isActiveFieldMetadataItem';
import { RecordFieldsComponentInstanceContext } from '@/object-record/record-field/states/context/RecordFieldsComponentInstanceContext';
@@ -21,7 +21,7 @@ export const visibleRecordFieldsComponentSelector = createAtomComponentSelector<
componentStateKey,
);
const objectMetadataItems = get(objectMetadataItemsState);
const objectMetadataItems = get(objectMetadataItemsSelector);
return filterVisibleAndReadableRecordFields(
currentRecordFields,
@@ -1,7 +1,7 @@
import { useCallback, useContext } from 'react';
import { useStore } from 'jotai';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
import { generateEmptyFieldValue } from '@/object-record/utils/generateEmptyFieldValue';
@@ -20,7 +20,7 @@ export const useClearField = () => {
const [updateRecord] = useUpdateRecord();
const clearField = useCallback(() => {
const objectMetadataItems = store.get(objectMetadataItemsState.atom);
const objectMetadataItems = store.get(objectMetadataItemsSelector.atom);
const foundObjectMetadataItem = objectMetadataItems.find(
(item) =>
@@ -5,7 +5,7 @@ import { type Task } from '@/activities/types/Task';
import { type TaskTarget } from '@/activities/types/TaskTarget';
import { getActivityTargetObjectRecords } from '@/activities/utils/getActivityTargetObjectRecords';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { useOpenJunctionRelationFieldInput } from '@/object-record/record-field/ui/hooks/useOpenJunctionRelationFieldInput';
import { useOpenFilesFieldInput } from '@/object-record/record-field/ui/meta-types/input/hooks/useOpenFilesFieldInput';
@@ -71,7 +71,7 @@ export const useOpenFieldInputEditMode = () => {
recordId: string;
prefix?: string;
}) => {
const objectMetadataItems = store.get(objectMetadataItemsState.atom);
const objectMetadataItems = store.get(objectMetadataItemsSelector.atom);
const currentWorkspace = store.get(currentWorkspaceState.atom);
@@ -1,4 +1,4 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { type FieldDefinition } from '@/object-record/record-field/ui/types/FieldDefinition';
import {
type FieldRelationFromManyValue,
@@ -37,7 +37,7 @@ export const useOpenJunctionRelationFieldInput = () => {
recordId: string;
prefix?: string;
}) => {
const objectMetadataItems = store.get(objectMetadataItemsState.atom);
const objectMetadataItems = store.get(objectMetadataItemsSelector.atom);
const sourceObjectMetadataId = objectMetadataItems.find(
(item) =>
@@ -1,6 +1,6 @@
import { useStore } from 'jotai';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { type FieldDefinition } from '@/object-record/record-field/ui/types/FieldDefinition';
import { type FieldMorphRelationMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
@@ -55,7 +55,7 @@ export const useOpenMorphRelationOneToManyFieldInput = () => {
),
);
const objectMetadataItems = store.get(objectMetadataItemsState.atom);
const objectMetadataItems = store.get(objectMetadataItemsSelector.atom);
const objectMetadataItemArray = objectMetadataItems.filter(
(objectMetadataItem) =>
@@ -1,4 +1,4 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import {
type FieldRelationFromManyValue,
type FieldRelationValue,
@@ -47,7 +47,7 @@ export const useOpenRelationFromManyFieldInput = () => {
recordStoreFamilySelector.selectorFamily({ recordId, fieldName }),
) as FieldRelationValue<FieldRelationFromManyValue>) ?? [];
const objectMetadataItems = store.get(objectMetadataItemsState.atom);
const objectMetadataItems = store.get(objectMetadataItemsSelector.atom);
const objectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
@@ -1,4 +1,4 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
@@ -12,7 +12,7 @@ export const hasAnySoftDeleteFilterOnViewComponentSelector =
get:
(componentStateKey) =>
({ get }) => {
const objectMetadataItems = get(objectMetadataItemsState);
const objectMetadataItems = get(objectMetadataItemsSelector);
const currentRecordFilters = get(
currentRecordFiltersComponentState,
componentStateKey,
@@ -1,6 +1,6 @@
import { useStore } from 'jotai';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState';
import { recordGroupIdsComponentState } from '@/object-record/record-group/states/recordGroupIdsComponentState';
@@ -28,7 +28,7 @@ export const useSetRecordGroups = () => {
recordIndexId: string;
objectMetadataItemId: string;
}) => {
const objectMetadataItems = store.get(objectMetadataItemsState.atom);
const objectMetadataItems = store.get(objectMetadataItemsSelector.atom);
const objectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) => objectMetadataItem.id === objectMetadataItemId,
@@ -1,4 +1,4 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { getObjectRecordIdentifier } from '@/object-metadata/utils/getObjectRecordIdentifier';
import { type ObjectRecordIdentifier } from '@/object-record/types/ObjectRecordIdentifier';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
@@ -26,7 +26,7 @@ export const recordStoreIdentifierFamilySelector = createAtomFamilySelector<
recordFromStore?.__typename ?? '',
);
const objectMetadataItems = get(objectMetadataItemsState);
const objectMetadataItems = get(objectMetadataItemsSelector);
const objectMetadataItem = objectMetadataItems.find(
(item) => item.nameSingular === objectNameSingular,
@@ -1,4 +1,4 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { getObjectRecordIdentifier } from '@/object-metadata/utils/getObjectRecordIdentifier';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { type ObjectRecordIdentifier } from '@/object-record/types/ObjectRecordIdentifier';
@@ -16,7 +16,7 @@ export const recordStoreIdentifiersFamilySelector = createAtomFamilySelector<
get:
({ recordIds, allowRequestsToTwentyIcons }) =>
({ get }) => {
const objectMetadataItems = get(objectMetadataItemsState);
const objectMetadataItems = get(objectMetadataItemsSelector);
return recordIds
.map((recordId) => {
@@ -1,4 +1,4 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { isCompositeFieldType } from '@/object-record/object-filter-dropdown/utils/isCompositeFieldType';
@@ -25,7 +25,7 @@ import { FieldMetadataType, RelationType } from '~/generated-metadata/graphql';
export const useBuildSpreadsheetImportFields = () => {
const { getIcon } = useIcons();
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const buildSpreadsheetImportFields = (
fieldMetadataItems: FieldMetadataItem[],
@@ -4,7 +4,7 @@ import { type NoteTarget } from '@/activities/types/NoteTarget';
import { type Task } from '@/activities/types/Task';
import { type TaskTarget } from '@/activities/types/TaskTarget';
import { getActivityTargetObjectRecords } from '@/activities/utils/getActivityTargetObjectRecords';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { useOpenMorphRelationManyToOneFieldInput } from '@/object-record/record-field/ui/meta-types/input/hooks/useOpenMorphRelationManyToOneFieldInput';
import { useOpenMorphRelationOneToManyFieldInput } from '@/object-record/record-field/ui/meta-types/input/hooks/useOpenMorphRelationOneToManyFieldInput';
import { useOpenRelationFromManyFieldInput } from '@/object-record/record-field/ui/meta-types/input/hooks/useOpenRelationFromManyFieldInput';
@@ -75,7 +75,7 @@ export const useOpenFieldWidgetFieldInputEditMode = () => {
const activity = store.get(recordStoreFamilyState.atomFamily(recordId));
const objectMetadataItems = store.get(objectMetadataItemsState.atom);
const objectMetadataItems = store.get(objectMetadataItemsSelector.atom);
const activityTargetObjectRecords = getActivityTargetObjectRecords({
activityRecord: activity as Task | Note,
@@ -1,41 +0,0 @@
import { styled } from '@linaria/react';
import { type ReactNode } from 'react';
import { Card, CardContent } from 'twenty-ui/layout';
import { themeCssVariables } from 'twenty-ui/theme-constants';
type SettingsSummaryCardProps = {
title: ReactNode;
rightComponent: ReactNode;
};
const StyledCardContentContainer = styled.div`
> * {
align-items: center;
display: flex;
gap: ${themeCssVariables.spacing[2]};
min-height: ${themeCssVariables.spacing[6]};
padding: ${themeCssVariables.spacing[2]};
}
`;
const StyledTitle = styled.div`
color: ${themeCssVariables.font.color.primary};
display: flex;
font-weight: ${themeCssVariables.font.weight.medium};
gap: ${themeCssVariables.spacing[2]};
margin-right: auto;
`;
export const SettingsSummaryCard = ({
title,
rightComponent,
}: SettingsSummaryCardProps) => (
<Card>
<StyledCardContentContainer>
<CardContent>
<StyledTitle>{title}</StyledTitle>
{rightComponent}
</CardContent>
</StyledCardContentContainer>
</Card>
);
@@ -1,7 +1,7 @@
import { isDefined } from 'twenty-shared/utils';
import { styled } from '@linaria/react';
import { Handle, Position } from '@xyflow/react';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { useIcons } from 'twenty-ui/display';
@@ -28,7 +28,7 @@ const StyledFieldName = styled.div`
export const ObjectFieldRow = ({ field }: ObjectFieldRowProps) => {
const { theme } = useContext(ThemeContext);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const { getIcon } = useIcons();
const relatedObjectId = field.relation?.targetObjectMetadata.id;
@@ -4,7 +4,7 @@ import { SIDE_PANEL_COMPONENT_INSTANCE_ID } from '@/side-panel/constants/SidePan
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper';
import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
@@ -34,7 +34,7 @@ export const SidePanelContainer = ({ children }: SidePanelContainerProps) => {
);
const isMobile = useIsMobile();
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const objectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
@@ -7,7 +7,7 @@ import { sidePanelPageInfoState } from '@/side-panel/states/sidePanelPageInfoSta
import { sidePanelShouldFocusTitleInputComponentState } from '@/side-panel/states/sidePanelShouldFocusTitleInputComponentState';
import { NavigationMenuItemStyleIcon } from '@/navigation-menu-item/components/NavigationMenuItemStyleIcon';
import { FOLDER_ICON_DEFAULT } from '@/navigation-menu-item/constants/FolderIconDefault';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { useUpdateFolderInDraft } from '@/navigation-menu-item/hooks/useUpdateFolderInDraft';
import { useWorkspaceSectionItems } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
@@ -41,7 +41,7 @@ export const SidePanelFolderInfo = () => {
const selectedItem = selectedNavigationMenuItemInEditMode
? items.find(
(item) =>
item.itemType === NavigationMenuItemType.FOLDER &&
item.type === NavigationMenuItemType.FOLDER &&
item.id === selectedNavigationMenuItemInEditMode,
)
: undefined;
@@ -2,7 +2,7 @@ import { useLingui } from '@lingui/react/macro';
import { IconLink, IconWorld } from 'twenty-ui/display';
import { LinkIconWithLinkOverlay } from '@/navigation-menu-item/components/LinkIconWithLinkOverlay';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { useUpdateLinkInDraft } from '@/navigation-menu-item/hooks/useUpdateLinkInDraft';
import { useWorkspaceSectionItems } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
@@ -33,7 +33,7 @@ export const SidePanelLinkInfo = () => {
const selectedItem = selectedNavigationMenuItemInEditMode
? items.find(
(item) =>
item.itemType === NavigationMenuItemType.LINK &&
item.type === NavigationMenuItemType.LINK &&
item.id === selectedNavigationMenuItemInEditMode,
)
: undefined;
@@ -3,7 +3,7 @@ import { OverflowingTextWithTooltip } from 'twenty-ui/display';
import { SidePanelPageInfoLayout } from '@/side-panel/components/SidePanelPageInfoLayout';
import { NavigationMenuItemIcon } from '@/navigation-menu-item/components/NavigationMenuItemIcon';
import { NavigationMenuItemType } from '@/navigation-menu-item/constants/NavigationMenuItemType';
import { NavigationMenuItemType } from 'twenty-shared/types';
import { useSelectedNavigationMenuItemEditItem } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItem';
import { useSelectedNavigationMenuItemEditItemLabel } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItemLabel';
import { useSelectedNavigationMenuItemEditItemObjectMetadata } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItemObjectMetadata';
@@ -15,12 +15,12 @@ export const SidePanelObjectViewRecordInfo = () => {
const { selectedItemObjectMetadata } =
useSelectedNavigationMenuItemEditItemObjectMetadata();
const processedItem =
selectedItem && selectedItem.itemType !== NavigationMenuItemType.FOLDER
const navItem =
selectedItem && selectedItem.type !== NavigationMenuItemType.FOLDER
? selectedItem
: undefined;
if (!processedItem || !selectedItemLabel) {
if (!navItem || !selectedItemLabel) {
return null;
}
@@ -28,22 +28,22 @@ export const SidePanelObjectViewRecordInfo = () => {
NavigationMenuItemType.OBJECT,
NavigationMenuItemType.VIEW,
NavigationMenuItemType.RECORD,
].includes(processedItem.itemType);
].includes(navItem.type);
if (!isObjectViewOrRecord) {
return null;
}
const label =
processedItem.itemType === NavigationMenuItemType.RECORD
navItem.type === NavigationMenuItemType.RECORD
? selectedItemObjectMetadata?.labelSingular
: processedItem.itemType === NavigationMenuItemType.OBJECT
: navItem.type === NavigationMenuItemType.OBJECT
? t`Object`
: t`View`;
return (
<SidePanelPageInfoLayout
icon={<NavigationMenuItemIcon navigationMenuItem={processedItem} />}
icon={<NavigationMenuItemIcon navigationMenuItem={navItem} />}
title={<OverflowingTextWithTooltip text={selectedItemLabel} />}
label={label}
/>

Some files were not shown because too many files have changed in this diff Show More