Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8fb086e880 | ||
|
|
267d8ca3b0 |
@@ -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 1–4 are pure low-hanging fruit — just deletions and a rename, no risk of regressions.
|
||||
|
||||
Items 5–9 are small refactors that pay off quickly across many files.
|
||||
|
||||
Items 10–13 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,
|
||||
);
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+2
-2
@@ -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 &&
|
||||
|
||||
+2
-2
@@ -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) =>
|
||||
|
||||
+2
-2
@@ -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
-2
@@ -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 ??
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+2
-2
@@ -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
-2
@@ -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);
|
||||
|
||||
|
||||
+2
-2
@@ -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) =>
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+2
-2
@@ -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 =
|
||||
|
||||
+122
-61
@@ -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>
|
||||
)}
|
||||
|
||||
+77
-53
@@ -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}
|
||||
|
||||
+63
-39
@@ -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>
|
||||
)}
|
||||
|
||||
+2
-2
@@ -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 = ({
|
||||
|
||||
+66
-28
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
+26
-8
@@ -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} />
|
||||
)}
|
||||
|
||||
+39
-18
@@ -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={
|
||||
|
||||
+39
-18
@@ -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;
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
export enum NavigationMenuItemType {
|
||||
FOLDER = 'FOLDER',
|
||||
LINK = 'LINK',
|
||||
OBJECT = 'OBJECT',
|
||||
RECORD = 'RECORD',
|
||||
VIEW = 'VIEW',
|
||||
}
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+1
-1
@@ -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';
|
||||
|
||||
+5
-40
@@ -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({
|
||||
|
||||
+2
-2
@@ -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) {
|
||||
|
||||
+22
-8
@@ -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;
|
||||
|
||||
+8
-92
@@ -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,
|
||||
|
||||
+42
-12
@@ -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 {
|
||||
|
||||
+16
-25
@@ -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],
|
||||
);
|
||||
|
||||
-11
@@ -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;
|
||||
};
|
||||
-60
@@ -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('');
|
||||
});
|
||||
});
|
||||
+242
@@ -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');
|
||||
});
|
||||
});
|
||||
+9
-9
@@ -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,
|
||||
);
|
||||
|
||||
+9
-17
@@ -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);
|
||||
});
|
||||
|
||||
-360
@@ -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');
|
||||
});
|
||||
});
|
||||
-31
@@ -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,
|
||||
};
|
||||
};
|
||||
+51
@@ -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);
|
||||
};
|
||||
+6
-6
@@ -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;
|
||||
|
||||
+76
@@ -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 '';
|
||||
}
|
||||
};
|
||||
+49
@@ -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 ?? '';
|
||||
}
|
||||
};
|
||||
+33
@@ -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;
|
||||
}
|
||||
};
|
||||
+22
-22
@@ -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;
|
||||
|
||||
+7
-7
@@ -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;
|
||||
};
|
||||
|
||||
-179
@@ -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);
|
||||
};
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+49
-20
@@ -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}
|
||||
/>
|
||||
)
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+17
-5
@@ -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}
|
||||
|
||||
+4
-5
@@ -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}
|
||||
|
||||
+31
-31
@@ -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,
|
||||
)
|
||||
|
||||
+2
-2
@@ -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 (
|
||||
|
||||
+4
-4
@@ -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}
|
||||
|
||||
+2
-2
@@ -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,
|
||||
);
|
||||
|
||||
+2
-2
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
+4
-7
@@ -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;
|
||||
|
||||
+5
-8
@@ -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,
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+2
-2
@@ -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(
|
||||
|
||||
+2
-2
@@ -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(
|
||||
|
||||
+2
-2
@@ -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
-2
@@ -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),
|
||||
});
|
||||
Vendored
+2
-2
@@ -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 = <
|
||||
|
||||
+2
-2
@@ -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
-2
@@ -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();
|
||||
|
||||
|
||||
+2
-2
@@ -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
-2
@@ -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(() => {
|
||||
|
||||
+2
-2
@@ -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)
|
||||
|
||||
+2
-2
@@ -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(
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+2
-2
@@ -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) =>
|
||||
|
||||
+2
-2
@@ -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);
|
||||
|
||||
|
||||
+2
-2
@@ -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) =>
|
||||
|
||||
+2
-2
@@ -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) =>
|
||||
|
||||
+2
-2
@@ -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) =>
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+2
-2
@@ -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) => {
|
||||
|
||||
+2
-2
@@ -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[],
|
||||
|
||||
+2
-2
@@ -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>
|
||||
);
|
||||
+2
-2
@@ -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;
|
||||
|
||||
+8
-8
@@ -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
Reference in New Issue
Block a user