feat: add color property to ObjectMetadata for object icon customization (#18672)
## Summary - Adds a `color` column to `ObjectMetadataEntity` with full GraphQL support so object icon colors are persisted at the metadata level - Adds a `type` column to `NavigationMenuItemEntity` (enum: `OBJECT`, `VIEW`, `FOLDER`, `LINK`, `RECORD`) replacing field-based type inference - Updates frontend to read object colors from `objectMetadata.color` (falling back to standard defaults) in the sidebar nav, record index header, and record show breadcrumb - Simplifies `NavigationMenuItemIcon` color resolution via `getEffectiveNavigationMenuItemColor` util ## Color rules | Item type | Color source | Editable in sidebar? | |-----------|-------------|---------------------| | **Object** | `objectMetadata.color` | Yes — persisted to `objectMetadata.color` on Save | | **Folder** | `navigationMenuItem.color` | Yes | | **Link** | Fixed default (`DEFAULT_NAVIGATION_MENU_ITEM_COLOR_LINK`) | No | | **View** | `objectMetadata.color` (from the parent object) | No | | **Record** | None | No | - **Object** items represent the whole object (e.g. "Companies") and point to the INDEX view. Changing their color updates `objectMetadata.color` via `useSaveObjectMetadataColorsFromDraft`. - **View** items represent specific non-INDEX views. Their color comes from the parent object's metadata (read-only). - Only **folders** store their color on `navigationMenuItem.color` — enforced by `hasNavigationMenuItemOwnColor` util. - `getEffectiveNavigationMenuItemColor` returns `objectColor` for both OBJECT and VIEW items, folder's own color for folders, and the fixed default for links. ## NavigationMenuItemType enum - Shared enum created in `twenty-shared` with values: `OBJECT`, `VIEW`, `FOLDER`, `LINK`, `RECORD` - Registered as a GraphQL enum on the backend - Replaces string literals across entity, DTOs, input, converters, and frontend hooks - Migration backfills existing rows: INDEX views → `OBJECT`, non-INDEX views → `VIEW`, based on join with the view table ## Design decisions - **OBJECT vs VIEW distinction**: Items pointing to INDEX views are typed as `OBJECT` (represent the whole object, color editable). Items pointing to non-INDEX views are typed as `VIEW` (specific view, color read-only from parent object). - **Dual color storage**: `navigationMenuItem.color` is preserved for folders only. Objects use `objectMetadata.color` as their source of truth. - **Type discriminator**: The `type` column replaces field-based inference (checking `viewId`, `link`, `targetRecordId` presence) with an explicit enum, simplifying `isNavigationMenuItemLink` / `isNavigationMenuItemFolder` to simple `item.type ===` checks. - **No settings page color picker**: Object color editing is done from the sidebar edit panel, not the data model settings page. ## Test plan - [ ] Verify objects display their default standard colors in the sidebar - [ ] Verify object color editing works in the sidebar edit panel (persists to objectMetadata.color) - [ ] Verify folder color editing works in the sidebar edit panel - [ ] Verify views, links, and records do NOT show a color picker in the sidebar edit panel - [ ] Run `npx nx typecheck twenty-front` and `npx nx typecheck twenty-server` - [ ] Verify the database migrations add `color` to `objectMetadata` and `type` to `navigationMenuItem` Made with [Cursor](https://cursor.com)
This commit is contained in:
@@ -474,7 +474,7 @@ const createExampleNavigationMenuItem = async ({
|
||||
const universalIdentifier = v4();
|
||||
|
||||
const content = `import { defineNavigationMenuItem } from 'twenty-sdk';
|
||||
import { EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER } from 'src/views/example-view';
|
||||
import { EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER } from 'src/views/example-view';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: '${universalIdentifier}',
|
||||
@@ -482,6 +482,7 @@ export default defineNavigationMenuItem({
|
||||
icon: 'IconList',
|
||||
color: 'blue',
|
||||
position: 0,
|
||||
type: 'VIEW',
|
||||
viewUniversalIdentifier: EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
`;
|
||||
|
||||
+4
-2
@@ -1,8 +1,10 @@
|
||||
import { defineNavigationMenuItem } from 'twenty-sdk';
|
||||
import { ALL_POST_CARD_RECIPIENTS_VIEW_ID } from '../views/all-post-card-recipients.view';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/post-card-recipient.object';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: 'c1a2b3c4-0003-4a7b-8c9d-0e1f2a3b4c5d',
|
||||
position: 2,
|
||||
viewUniversalIdentifier: ALL_POST_CARD_RECIPIENTS_VIEW_ID,
|
||||
type: NavigationMenuItemType.OBJECT,
|
||||
targetObjectUniversalIdentifier: POST_CARD_RECIPIENT_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
|
||||
+4
-2
@@ -1,8 +1,10 @@
|
||||
import { defineNavigationMenuItem } from 'twenty-sdk';
|
||||
import { ALL_POST_CARDS_VIEW_ID } from '../views/all-post-cards.view';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: 'c1a2b3c4-0001-4a7b-8c9d-0e1f2a3b4c5d',
|
||||
position: 0,
|
||||
viewUniversalIdentifier: ALL_POST_CARDS_VIEW_ID,
|
||||
type: NavigationMenuItemType.OBJECT,
|
||||
targetObjectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
|
||||
+4
-2
@@ -1,8 +1,10 @@
|
||||
import { defineNavigationMenuItem } from 'twenty-sdk';
|
||||
import { ALL_RECIPIENTS_VIEW_ID } from '../views/all-recipients.view';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { RECIPIENT_UNIVERSAL_IDENTIFIER } from '../objects/recipient.object';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: 'c1a2b3c4-0002-4a7b-8c9d-0e1f2a3b4c5d',
|
||||
position: 1,
|
||||
viewUniversalIdentifier: ALL_RECIPIENTS_VIEW_ID,
|
||||
type: NavigationMenuItemType.OBJECT,
|
||||
targetObjectUniversalIdentifier: RECIPIENT_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
|
||||
+3
-1
@@ -1,5 +1,6 @@
|
||||
import { defineNavigationMenuItem } from 'twenty-sdk';
|
||||
import { EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER } from 'src/views/example-view';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER } from 'src/views/example-view';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: '10f90627-e9c2-44b7-9742-bed77e3d1b17',
|
||||
@@ -7,5 +8,6 @@ export default defineNavigationMenuItem({
|
||||
icon: 'IconList',
|
||||
color: 'blue',
|
||||
position: 0,
|
||||
type: NavigationMenuItemType.VIEW,
|
||||
viewUniversalIdentifier: EXAMPLE_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
|
||||
+2
@@ -1,10 +1,12 @@
|
||||
import { CALL_RECORDING_VIEW_UNIVERSAL_IDENTIFIER } from 'src/views/call-recording-view';
|
||||
import { defineNavigationMenuItem } from 'twenty-sdk';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: '5248a62d-7d2e-43a7-ba45-6e8f61876a71',
|
||||
name: 'Call recordings',
|
||||
icon: 'IconPhone',
|
||||
position: 0,
|
||||
type: NavigationMenuItemType.VIEW,
|
||||
viewUniversalIdentifier: CALL_RECORDING_VIEW_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
|
||||
+2
@@ -1,4 +1,5 @@
|
||||
import { defineNavigationMenuItem } from 'twenty-sdk';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { UNIVERSAL_IDENTIFIERS } from 'src/constants/universal-identifiers.constant';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
@@ -6,6 +7,7 @@ export default defineNavigationMenuItem({
|
||||
name: 'Self host user',
|
||||
icon: 'IconList',
|
||||
position: 1,
|
||||
type: NavigationMenuItemType.VIEW,
|
||||
viewUniversalIdentifier:
|
||||
UNIVERSAL_IDENTIFIERS.views.selfHostingUserView.universalIdentifier,
|
||||
});
|
||||
|
||||
File diff suppressed because one or more lines are too long
+8
-10
@@ -1,15 +1,13 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useContext, type ReactNode } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
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 { 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 type { AddToNavigationDragPayload } from '@/navigation-menu-item/types/add-to-navigation-drag-payload';
|
||||
import { getEffectiveNavigationMenuItemColor } from '@/navigation-menu-item/utils/getEffectiveNavigationMenuItemColor';
|
||||
|
||||
const StyledIconSlot = styled.div<{
|
||||
$hasFixedSize: boolean;
|
||||
@@ -83,14 +81,14 @@ export const AddToNavigationDragHandle = ({
|
||||
disableDrag = false,
|
||||
}: AddToNavigationDragHandleProps) => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const effectiveColor =
|
||||
payload.type === 'object' && isNonEmptyString(payload.iconColor)
|
||||
const objectColor =
|
||||
payload.type === NavigationMenuItemType.OBJECT
|
||||
? payload.iconColor
|
||||
: payload.type === 'folder'
|
||||
? DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER
|
||||
: payload.type === 'link'
|
||||
? DEFAULT_NAVIGATION_MENU_ITEM_COLOR_LINK
|
||||
: undefined;
|
||||
: undefined;
|
||||
const effectiveColor = getEffectiveNavigationMenuItemColor(
|
||||
{ itemType: payload.type as NavigationMenuItemType },
|
||||
objectColor,
|
||||
);
|
||||
const hasBackgroundColor =
|
||||
payload.type !== NavigationMenuItemType.RECORD &&
|
||||
isDefined(effectiveColor) &&
|
||||
|
||||
+5
-3
@@ -8,8 +8,8 @@ import { StyledNavigationMenuItemIconContainer } from '@/navigation-menu-item/co
|
||||
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 { getNavigationMenuItemIconStyleFromColor } from '@/navigation-menu-item/utils/getNavigationMenuItemIconStyleFromColor';
|
||||
import { getEffectiveNavigationMenuItemColor } from '@/navigation-menu-item/utils/getEffectiveNavigationMenuItemColor';
|
||||
import { getNavigationMenuItemIconStyleFromColor } from '@/navigation-menu-item/utils/getNavigationMenuItemIconStyleFromColor';
|
||||
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
|
||||
import { useGetStandardObjectIcon } from '@/object-metadata/hooks/useGetStandardObjectIcon';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
@@ -73,8 +73,10 @@ export const NavigationMenuItemIcon = ({
|
||||
const iconToUse =
|
||||
StandardIcon ??
|
||||
(navigationMenuItem.Icon ? getIcon(navigationMenuItem.Icon) : undefined);
|
||||
const effectiveColor =
|
||||
getEffectiveNavigationMenuItemColor(navigationMenuItem);
|
||||
const effectiveColor = getEffectiveNavigationMenuItemColor(
|
||||
navigationMenuItem,
|
||||
objectNavItemColor,
|
||||
);
|
||||
const useStyledIcon = !isRecord && isNonEmptyString(effectiveColor);
|
||||
const iconStyle = useStyledIcon
|
||||
? getNavigationMenuItemIconStyleFromColor(effectiveColor)
|
||||
|
||||
+1
@@ -39,6 +39,7 @@ export const WorkspaceNavigationMenuItemFolderSubItem = ({
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
|
||||
const views = useAtomStateValue(viewsSelector);
|
||||
const objectMetadataItem =
|
||||
navigationMenuItem.itemType === NavigationMenuItemType.OBJECT ||
|
||||
navigationMenuItem.itemType === NavigationMenuItemType.VIEW ||
|
||||
navigationMenuItem.itemType === NavigationMenuItemType.RECORD
|
||||
? getObjectMetadataForNavigationMenuItem(
|
||||
|
||||
+6
-3
@@ -28,7 +28,7 @@ import { filterWorkspaceNavigationMenuItems } from '@/navigation-menu-item/utils
|
||||
import { preloadWorkspaceDndKit } from '@/navigation/preloadWorkspaceDndKit';
|
||||
import { NavigationDrawerSectionForWorkspaceItems } from '@/object-metadata/components/NavigationDrawerSectionForWorkspaceItems';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { navigationMenuItemsState } from '@/navigation-menu-item/states/navigationMenuItemsState';
|
||||
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';
|
||||
@@ -47,7 +47,9 @@ export const WorkspaceNavigationMenuItems = () => {
|
||||
const { workspaceNavigationMenuItemsSorted } = useSortedNavigationMenuItems();
|
||||
const store = useStore();
|
||||
const enterEditMode = () => {
|
||||
const currentNavigationMenuItems = store.get(navigationMenuItemsState.atom);
|
||||
const currentNavigationMenuItems = store.get(
|
||||
navigationMenuItemsSelector.atom,
|
||||
);
|
||||
const workspaceNavigationMenuItems = filterWorkspaceNavigationMenuItems(
|
||||
currentNavigationMenuItems,
|
||||
);
|
||||
@@ -106,7 +108,8 @@ export const WorkspaceNavigationMenuItems = () => {
|
||||
if (objectMetadataItem) {
|
||||
openNavigationMenuItemInSidePanel({
|
||||
pageTitle:
|
||||
item.itemType === NavigationMenuItemType.VIEW
|
||||
item.itemType === NavigationMenuItemType.VIEW ||
|
||||
item.itemType === NavigationMenuItemType.OBJECT
|
||||
? item.labelIdentifier
|
||||
: objectMetadataItem.labelSingular,
|
||||
pageIcon: getIcon(objectMetadataItem.icon),
|
||||
|
||||
+5
-5
@@ -1,7 +1,7 @@
|
||||
export enum NavigationMenuItemType {
|
||||
FOLDER = 'folder',
|
||||
LINK = 'link',
|
||||
OBJECT = 'object',
|
||||
RECORD = 'record',
|
||||
VIEW = 'view',
|
||||
FOLDER = 'FOLDER',
|
||||
LINK = 'LINK',
|
||||
OBJECT = 'OBJECT',
|
||||
RECORD = 'RECORD',
|
||||
VIEW = 'VIEW',
|
||||
}
|
||||
|
||||
+1
@@ -3,6 +3,7 @@ import { gql } from '@apollo/client';
|
||||
export const NAVIGATION_MENU_ITEM_FRAGMENT = gql`
|
||||
fragment NavigationMenuItemFields on NavigationMenuItem {
|
||||
id
|
||||
type
|
||||
userWorkspaceId
|
||||
targetRecordId
|
||||
targetObjectMetadataId
|
||||
|
||||
+2
@@ -1,3 +1,4 @@
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
import type { NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
@@ -37,6 +38,7 @@ export const useAddFolderToNavigationMenuDraft = () => {
|
||||
const newItem: NavigationMenuItem = {
|
||||
__typename: 'NavigationMenuItem',
|
||||
id: newItemId,
|
||||
type: NavigationMenuItemType.FOLDER,
|
||||
viewId: undefined,
|
||||
targetObjectMetadataId: undefined,
|
||||
targetRecordId: undefined,
|
||||
|
||||
+2
@@ -1,3 +1,4 @@
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
import type { NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
@@ -40,6 +41,7 @@ export const useAddLinkToNavigationMenuDraft = () => {
|
||||
const newItem: NavigationMenuItem = {
|
||||
__typename: 'NavigationMenuItem',
|
||||
id: newItemId,
|
||||
type: NavigationMenuItemType.LINK,
|
||||
viewId: undefined,
|
||||
targetObjectMetadataId: undefined,
|
||||
targetRecordId: undefined,
|
||||
|
||||
+3
-2
@@ -1,3 +1,4 @@
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
import type { NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
@@ -13,7 +14,6 @@ export const useAddObjectToNavigationMenuDraft = () => {
|
||||
|
||||
const addObjectToDraft = (
|
||||
objectMetadataId: string,
|
||||
defaultViewId: string,
|
||||
currentDraft: NavigationMenuItem[],
|
||||
targetFolderId?: string | null,
|
||||
targetIndex?: number,
|
||||
@@ -38,7 +38,8 @@ export const useAddObjectToNavigationMenuDraft = () => {
|
||||
const newItem: NavigationMenuItem = {
|
||||
__typename: 'NavigationMenuItem',
|
||||
id: newItemId,
|
||||
viewId: defaultViewId,
|
||||
type: NavigationMenuItemType.OBJECT,
|
||||
viewId: undefined,
|
||||
targetObjectMetadataId: objectMetadataId,
|
||||
position,
|
||||
userWorkspaceId: undefined,
|
||||
|
||||
+2
@@ -1,4 +1,5 @@
|
||||
import { v4 } from 'uuid';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import type { NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
@@ -60,6 +61,7 @@ export const useAddRecordToNavigationMenuDraft = () => {
|
||||
const newItem: NavigationMenuItem = {
|
||||
__typename: 'NavigationMenuItem',
|
||||
id: newItemId,
|
||||
type: NavigationMenuItemType.RECORD,
|
||||
viewId: undefined,
|
||||
targetObjectMetadataId: objectMetadataId,
|
||||
targetRecordId: searchRecord.recordId,
|
||||
|
||||
+2
@@ -1,3 +1,4 @@
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
import type { NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
@@ -37,6 +38,7 @@ export const useAddViewToNavigationMenuDraft = () => {
|
||||
const newItem: NavigationMenuItem = {
|
||||
__typename: 'NavigationMenuItem',
|
||||
id: newItemId,
|
||||
type: NavigationMenuItemType.VIEW,
|
||||
viewId,
|
||||
targetObjectMetadataId: undefined,
|
||||
position,
|
||||
|
||||
+3
@@ -1,3 +1,4 @@
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useMutation } from '@apollo/client/react';
|
||||
import { CreateNavigationMenuItemDocument } from '~/generated-metadata/graphql';
|
||||
@@ -42,6 +43,7 @@ export const useCreateNavigationMenuItem = () => {
|
||||
await createNavigationMenuItemMutation({
|
||||
variables: {
|
||||
input: {
|
||||
type: NavigationMenuItemType.VIEW,
|
||||
viewId: targetRecord.id,
|
||||
userWorkspaceId: currentWorkspaceMemberId,
|
||||
folderId,
|
||||
@@ -75,6 +77,7 @@ export const useCreateNavigationMenuItem = () => {
|
||||
await createNavigationMenuItemMutation({
|
||||
variables: {
|
||||
input: {
|
||||
type: NavigationMenuItemType.RECORD,
|
||||
targetRecordId: targetRecord.id,
|
||||
targetObjectMetadataId: objectMetadataItem.id,
|
||||
userWorkspaceId: currentWorkspaceMemberId,
|
||||
|
||||
+2
@@ -1,3 +1,4 @@
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useMutation } from '@apollo/client/react';
|
||||
import { CreateNavigationMenuItemDocument } from '~/generated-metadata/graphql';
|
||||
@@ -40,6 +41,7 @@ export const useCreateNavigationMenuItemFolder = () => {
|
||||
await createNavigationMenuItemMutation({
|
||||
variables: {
|
||||
input: {
|
||||
type: NavigationMenuItemType.FOLDER,
|
||||
name,
|
||||
targetRecordId: null,
|
||||
targetObjectMetadataId: null,
|
||||
|
||||
-1
@@ -139,7 +139,6 @@ export const useHandleAddToNavigationDrop = () => {
|
||||
);
|
||||
const newItemId = addObjectToDraft(
|
||||
payload.objectMetadataId,
|
||||
payload.defaultViewId,
|
||||
currentDraft,
|
||||
folderId,
|
||||
index,
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@ import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMembe
|
||||
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
|
||||
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/navigationMenuItemsDraftState';
|
||||
import { filterWorkspaceNavigationMenuItems } from '@/navigation-menu-item/utils/filterWorkspaceNavigationMenuItems';
|
||||
import { navigationMenuItemsState } from '@/navigation-menu-item/states/navigationMenuItemsState';
|
||||
import { navigationMenuItemsSelector } from '@/navigation-menu-item/states/navigationMenuItemsSelector';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
@@ -17,7 +17,7 @@ type NavigationMenuItemsData = {
|
||||
export const useNavigationMenuItemsData = (): NavigationMenuItemsData => {
|
||||
const currentWorkspaceMember = useAtomStateValue(currentWorkspaceMemberState);
|
||||
const currentWorkspaceMemberId = currentWorkspaceMember?.id;
|
||||
const navigationMenuItems = useAtomStateValue(navigationMenuItemsState);
|
||||
const navigationMenuItems = useAtomStateValue(navigationMenuItemsSelector);
|
||||
const isNavigationMenuInEditMode = useAtomStateValue(
|
||||
isNavigationMenuInEditModeState,
|
||||
);
|
||||
|
||||
+2
-2
@@ -4,14 +4,14 @@ import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
import { isNavigationMenuInEditModeState } from '@/navigation-menu-item/states/isNavigationMenuInEditModeState';
|
||||
import { filterWorkspaceNavigationMenuItems } from '@/navigation-menu-item/utils/filterWorkspaceNavigationMenuItems';
|
||||
import { navigationMenuItemsDraftState } from '@/navigation-menu-item/states/navigationMenuItemsDraftState';
|
||||
import { navigationMenuItemsState } from '@/navigation-menu-item/states/navigationMenuItemsState';
|
||||
import { navigationMenuItemsSelector } from '@/navigation-menu-item/states/navigationMenuItemsSelector';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
|
||||
export const useNavigationMenuItemsDraftState = () => {
|
||||
const isNavigationMenuInEditMode = useAtomStateValue(
|
||||
isNavigationMenuInEditModeState,
|
||||
);
|
||||
const navigationMenuItems = useAtomStateValue(navigationMenuItemsState);
|
||||
const navigationMenuItems = useAtomStateValue(navigationMenuItemsSelector);
|
||||
const navigationMenuItemsDraft = useAtomStateValue(
|
||||
navigationMenuItemsDraftState,
|
||||
);
|
||||
|
||||
+15
-12
@@ -1,17 +1,20 @@
|
||||
import { useWorkspaceSectionItems } from '@/navigation-menu-item/hooks/useWorkspaceSectionItems';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
|
||||
import { getStandardObjectIconColor } from '@/navigation-menu-item/utils/getStandardObjectIconColor';
|
||||
import { ViewKey } from '@/views/types/ViewKey';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
|
||||
export const useObjectNavItemColor = (objectNameSingular: string): string => {
|
||||
const items = useWorkspaceSectionItems();
|
||||
const objectNavItem = items.find(
|
||||
(item) =>
|
||||
'viewKey' in item &&
|
||||
item.viewKey === ViewKey.INDEX &&
|
||||
item.objectNameSingular === objectNameSingular,
|
||||
);
|
||||
return (
|
||||
(objectNavItem && 'color' in objectNavItem ? objectNavItem.color : null) ??
|
||||
getStandardObjectIconColor(objectNameSingular)
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(item) => item.nameSingular === objectNameSingular,
|
||||
);
|
||||
|
||||
const storedColor = objectMetadataItem?.color;
|
||||
const fallbackColor = getStandardObjectIconColor(objectNameSingular);
|
||||
|
||||
if (isNonEmptyString(storedColor)) {
|
||||
return storedColor;
|
||||
}
|
||||
|
||||
return fallbackColor;
|
||||
};
|
||||
|
||||
+46
-3
@@ -1,4 +1,5 @@
|
||||
import { useCallback } from 'react';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { useMutation } from '@apollo/client/react';
|
||||
@@ -12,7 +13,9 @@ import { filterWorkspaceNavigationMenuItems } from '@/navigation-menu-item/utils
|
||||
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/utils/isNavigationMenuItemFolder';
|
||||
import { isNavigationMenuItemLink } from '@/navigation-menu-item/utils/isNavigationMenuItemLink';
|
||||
import { orderFoldersForCreation } from '@/navigation-menu-item/utils/orderFoldersForCreation';
|
||||
import { navigationMenuItemsState } from '@/navigation-menu-item/states/navigationMenuItemsState';
|
||||
import { navigationMenuItemsSelector } from '@/navigation-menu-item/states/navigationMenuItemsSelector';
|
||||
import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { useStore } from 'jotai';
|
||||
|
||||
export const useSaveNavigationMenuItemsDraft = () => {
|
||||
@@ -24,15 +27,46 @@ export const useSaveNavigationMenuItemsDraft = () => {
|
||||
refetchQueries: ['FindManyNavigationMenuItems'],
|
||||
},
|
||||
);
|
||||
const { updateOneObjectMetadataItem } = useUpdateOneObjectMetadataItem();
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const saveDraft = useCallback(async () => {
|
||||
const draft = store.get(navigationMenuItemsDraftState.atom);
|
||||
const currentItems = store.get(navigationMenuItemsState.atom);
|
||||
const currentItems = store.get(navigationMenuItemsSelector.atom);
|
||||
|
||||
if (!draft) return;
|
||||
|
||||
const objectMetadataItems = store.get(objectMetadataItemsState.atom);
|
||||
|
||||
for (const draftItem of draft) {
|
||||
if (draftItem.type !== NavigationMenuItemType.OBJECT) {
|
||||
continue;
|
||||
}
|
||||
if (!isDefined(draftItem.targetObjectMetadataId)) {
|
||||
continue;
|
||||
}
|
||||
if (!isDefined(draftItem.color)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(item) => item.id === draftItem.targetObjectMetadataId,
|
||||
);
|
||||
|
||||
if (!isDefined(objectMetadataItem)) {
|
||||
continue;
|
||||
}
|
||||
if (objectMetadataItem.color === draftItem.color) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await updateOneObjectMetadataItem({
|
||||
idToUpdate: draftItem.targetObjectMetadataId,
|
||||
updatePayload: { color: draftItem.color },
|
||||
});
|
||||
}
|
||||
|
||||
const workspaceItems = filterWorkspaceNavigationMenuItems(currentItems);
|
||||
const topLevelWorkspace = workspaceItems.filter(
|
||||
(item) => !isDefined(item.folderId),
|
||||
@@ -131,13 +165,17 @@ export const useSaveNavigationMenuItemsDraft = () => {
|
||||
const iconChanged =
|
||||
isNavigationMenuItemFolder(draftItem) &&
|
||||
(original.icon ?? null) !== (draftItem.icon ?? null);
|
||||
const colorChanged =
|
||||
isNavigationMenuItemFolder(draftItem) &&
|
||||
(original.color ?? null) !== (draftItem.color ?? null);
|
||||
|
||||
if (
|
||||
positionChanged ||
|
||||
folderIdChanged ||
|
||||
nameChanged ||
|
||||
linkChanged ||
|
||||
iconChanged
|
||||
iconChanged ||
|
||||
colorChanged
|
||||
) {
|
||||
const updateInput: {
|
||||
id: string;
|
||||
@@ -146,6 +184,7 @@ export const useSaveNavigationMenuItemsDraft = () => {
|
||||
name?: string;
|
||||
link?: string | null;
|
||||
icon?: string | null;
|
||||
color?: string | null;
|
||||
} = { id: draftItem.id };
|
||||
|
||||
if (positionChanged) {
|
||||
@@ -174,6 +213,9 @@ export const useSaveNavigationMenuItemsDraft = () => {
|
||||
if (iconChanged && isNavigationMenuItemFolder(draftItem)) {
|
||||
updateInput.icon = draftItem.icon ?? null;
|
||||
}
|
||||
if (colorChanged) {
|
||||
updateInput.color = draftItem.color ?? null;
|
||||
}
|
||||
|
||||
await updateNavigationMenuItem(updateInput);
|
||||
}
|
||||
@@ -182,6 +224,7 @@ export const useSaveNavigationMenuItemsDraft = () => {
|
||||
updateNavigationMenuItem,
|
||||
deleteNavigationMenuItem,
|
||||
createNavigationMenuItemMutation,
|
||||
updateOneObjectMetadataItem,
|
||||
store,
|
||||
]);
|
||||
|
||||
|
||||
+1
@@ -12,6 +12,7 @@ const getLabelForItemType = (
|
||||
return item.name ?? 'Folder';
|
||||
case NavigationMenuItemType.LINK:
|
||||
return item.name ?? 'Link';
|
||||
case NavigationMenuItemType.OBJECT:
|
||||
case NavigationMenuItemType.VIEW:
|
||||
return item.labelIdentifier ?? objectLabelSingular ?? '';
|
||||
default:
|
||||
|
||||
+4
@@ -3,6 +3,7 @@ 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';
|
||||
@@ -83,6 +84,9 @@ export const useSortedNavigationMenuItems = () => {
|
||||
if (isNavigationMenuItemLink(item)) {
|
||||
return true;
|
||||
}
|
||||
if (isNavigationMenuItemObject(item)) {
|
||||
return isDefined(item.targetObjectMetadataId);
|
||||
}
|
||||
if (isDefined(item.viewId)) {
|
||||
return views.some((view) => view.id === item.viewId);
|
||||
}
|
||||
|
||||
+2
-2
@@ -2,10 +2,10 @@ import { metadataStoreState } from '@/metadata-store/states/metadataStoreState';
|
||||
import { createAtomSelector } from '@/ui/utilities/state/jotai/utils/createAtomSelector';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
export const navigationMenuItemsState = createAtomSelector<
|
||||
export const navigationMenuItemsSelector = createAtomSelector<
|
||||
NavigationMenuItem[]
|
||||
>({
|
||||
key: 'navigationMenuItemsState',
|
||||
key: 'navigationMenuItemsSelector',
|
||||
get: ({ get }) => {
|
||||
const entry = get(metadataStoreState, 'navigationMenuItems');
|
||||
|
||||
+5
-6
@@ -1,19 +1,18 @@
|
||||
export type AddToNavigationDragPayloadObject = {
|
||||
type: 'object';
|
||||
type: 'OBJECT';
|
||||
objectMetadataId: string;
|
||||
defaultViewId: string;
|
||||
label: string;
|
||||
iconColor?: string;
|
||||
};
|
||||
|
||||
export type AddToNavigationDragPayloadView = {
|
||||
type: 'view';
|
||||
type: 'VIEW';
|
||||
viewId: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export type AddToNavigationDragPayloadRecord = {
|
||||
type: 'record';
|
||||
type: 'RECORD';
|
||||
recordId: string;
|
||||
objectMetadataId: string;
|
||||
objectNameSingular: string;
|
||||
@@ -22,13 +21,13 @@ export type AddToNavigationDragPayloadRecord = {
|
||||
};
|
||||
|
||||
export type AddToNavigationDragPayloadFolder = {
|
||||
type: 'folder';
|
||||
type: 'FOLDER';
|
||||
folderId: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type AddToNavigationDragPayloadLink = {
|
||||
type: 'link';
|
||||
type: 'LINK';
|
||||
linkId: string;
|
||||
name: string;
|
||||
link: string;
|
||||
|
||||
+11
-30
@@ -1,45 +1,26 @@
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
|
||||
import { isNavigationMenuItemFolder } from '@/navigation-menu-item/utils/isNavigationMenuItemFolder';
|
||||
|
||||
describe('isNavigationMenuItemFolder', () => {
|
||||
it('should return true only when item has name and no link/view/record metadata', () => {
|
||||
it('should return true when type is folder', () => {
|
||||
expect(
|
||||
isNavigationMenuItemFolder({
|
||||
name: 'My Folder',
|
||||
link: null,
|
||||
viewId: null,
|
||||
targetRecordId: null,
|
||||
targetObjectMetadataId: null,
|
||||
}),
|
||||
isNavigationMenuItemFolder({ type: NavigationMenuItemType.FOLDER }),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when name is missing or when link/view/record is defined', () => {
|
||||
it('should return false for other types', () => {
|
||||
expect(
|
||||
isNavigationMenuItemFolder({
|
||||
name: undefined,
|
||||
link: null,
|
||||
viewId: null,
|
||||
targetRecordId: null,
|
||||
targetObjectMetadataId: null,
|
||||
}),
|
||||
isNavigationMenuItemFolder({ type: NavigationMenuItemType.LINK }),
|
||||
).toBe(false);
|
||||
expect(
|
||||
isNavigationMenuItemFolder({
|
||||
name: 'My Folder',
|
||||
link: 'https://example.com',
|
||||
viewId: null,
|
||||
targetRecordId: null,
|
||||
targetObjectMetadataId: null,
|
||||
}),
|
||||
isNavigationMenuItemFolder({ type: NavigationMenuItemType.VIEW }),
|
||||
).toBe(false);
|
||||
expect(
|
||||
isNavigationMenuItemFolder({
|
||||
name: 'My Folder',
|
||||
link: null,
|
||||
viewId: 'view-1',
|
||||
targetRecordId: null,
|
||||
targetObjectMetadataId: null,
|
||||
}),
|
||||
isNavigationMenuItemFolder({ type: NavigationMenuItemType.RECORD }),
|
||||
).toBe(false);
|
||||
expect(
|
||||
isNavigationMenuItemFolder({ type: NavigationMenuItemType.OBJECT }),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
+9
-43
@@ -1,60 +1,26 @@
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
|
||||
import { isNavigationMenuItemLink } from '@/navigation-menu-item/utils/isNavigationMenuItemLink';
|
||||
|
||||
describe('isNavigationMenuItemLink', () => {
|
||||
it('should return true only when item has non-empty link and no view/record metadata', () => {
|
||||
it('should return true when type is link', () => {
|
||||
expect(
|
||||
isNavigationMenuItemLink({
|
||||
link: 'https://example.com',
|
||||
viewId: null,
|
||||
targetRecordId: null,
|
||||
targetObjectMetadataId: null,
|
||||
}),
|
||||
isNavigationMenuItemLink({ type: NavigationMenuItemType.LINK }),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when link is missing, empty or only whitespace', () => {
|
||||
it('should return false for other types', () => {
|
||||
expect(
|
||||
isNavigationMenuItemLink({
|
||||
link: '',
|
||||
viewId: null,
|
||||
targetRecordId: null,
|
||||
targetObjectMetadataId: null,
|
||||
}),
|
||||
isNavigationMenuItemLink({ type: NavigationMenuItemType.FOLDER }),
|
||||
).toBe(false);
|
||||
expect(
|
||||
isNavigationMenuItemLink({
|
||||
link: ' ',
|
||||
viewId: null,
|
||||
targetRecordId: null,
|
||||
targetObjectMetadataId: null,
|
||||
}),
|
||||
isNavigationMenuItemLink({ type: NavigationMenuItemType.VIEW }),
|
||||
).toBe(false);
|
||||
expect(
|
||||
isNavigationMenuItemLink({
|
||||
link: undefined,
|
||||
viewId: null,
|
||||
targetRecordId: null,
|
||||
targetObjectMetadataId: null,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when viewId, targetRecordId or targetObjectMetadataId is defined', () => {
|
||||
expect(
|
||||
isNavigationMenuItemLink({
|
||||
link: 'https://example.com',
|
||||
viewId: 'view-1',
|
||||
targetRecordId: null,
|
||||
targetObjectMetadataId: null,
|
||||
}),
|
||||
isNavigationMenuItemLink({ type: NavigationMenuItemType.RECORD }),
|
||||
).toBe(false);
|
||||
expect(
|
||||
isNavigationMenuItemLink({
|
||||
link: 'https://example.com',
|
||||
viewId: null,
|
||||
targetRecordId: 'record-1',
|
||||
targetObjectMetadataId: null,
|
||||
}),
|
||||
isNavigationMenuItemLink({ type: NavigationMenuItemType.OBJECT }),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
+4
@@ -1,3 +1,5 @@
|
||||
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';
|
||||
@@ -324,6 +326,7 @@ describe('sortNavigationMenuItems', () => {
|
||||
[
|
||||
{
|
||||
id: 'link-1',
|
||||
type: NavigationMenuItemType.LINK,
|
||||
link: 'https://example.com',
|
||||
name: 'My Link',
|
||||
position: 1,
|
||||
@@ -341,6 +344,7 @@ describe('sortNavigationMenuItems', () => {
|
||||
[
|
||||
{
|
||||
id: 'link-2',
|
||||
type: NavigationMenuItemType.LINK,
|
||||
link: 'example.com',
|
||||
position: 2,
|
||||
} as NavigationMenuItem,
|
||||
|
||||
+5
@@ -7,12 +7,14 @@ 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';
|
||||
|
||||
export const buildCreateNavigationMenuItemInput = (
|
||||
draftItem: NavigationMenuItem,
|
||||
resolveFolderId: (draftFolderId: string) => string,
|
||||
): CreateNavigationMenuItemInput => {
|
||||
const input: CreateNavigationMenuItemInput = {
|
||||
type: draftItem.type,
|
||||
position: draftItem.position,
|
||||
};
|
||||
|
||||
@@ -28,6 +30,9 @@ export const buildCreateNavigationMenuItemInput = (
|
||||
: linkUrl
|
||||
? `https://${linkUrl}`
|
||||
: undefined;
|
||||
} else if (isNavigationMenuItemObject(draftItem)) {
|
||||
input.targetObjectMetadataId =
|
||||
draftItem.targetObjectMetadataId ?? undefined;
|
||||
} else if (isDefined(draftItem.viewId)) {
|
||||
input.viewId = draftItem.viewId;
|
||||
} else if (isDefined(draftItem.targetRecordId)) {
|
||||
|
||||
+14
-6
@@ -3,16 +3,24 @@ 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 { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/types/processed-navigation-menu-item';
|
||||
|
||||
export const getEffectiveNavigationMenuItemColor = (
|
||||
navigationMenuItem: ProcessedNavigationMenuItem,
|
||||
navigationMenuItem: {
|
||||
itemType: NavigationMenuItemType;
|
||||
color?: string | null;
|
||||
},
|
||||
objectColor?: string,
|
||||
): string | undefined => {
|
||||
if (isNonEmptyString(navigationMenuItem.color)) {
|
||||
return navigationMenuItem.color;
|
||||
}
|
||||
if (navigationMenuItem.itemType === NavigationMenuItemType.FOLDER) {
|
||||
return DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER;
|
||||
return isNonEmptyString(navigationMenuItem.color)
|
||||
? navigationMenuItem.color
|
||||
: DEFAULT_NAVIGATION_MENU_ITEM_COLOR_FOLDER;
|
||||
}
|
||||
if (navigationMenuItem.itemType === NavigationMenuItemType.OBJECT) {
|
||||
return objectColor;
|
||||
}
|
||||
if (navigationMenuItem.itemType === NavigationMenuItemType.VIEW) {
|
||||
return objectColor;
|
||||
}
|
||||
if (navigationMenuItem.itemType === NavigationMenuItemType.LINK) {
|
||||
return DEFAULT_NAVIGATION_MENU_ITEM_COLOR_LINK;
|
||||
|
||||
+10
@@ -19,6 +19,16 @@ export const getObjectMetadataForNavigationMenuItem = (
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
navigationMenuItem.itemType === NavigationMenuItemType.OBJECT &&
|
||||
isDefined(navigationMenuItem.targetObjectMetadataId)
|
||||
) {
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(meta) => meta.id === navigationMenuItem.targetObjectMetadataId,
|
||||
);
|
||||
return objectMetadataItem ?? null;
|
||||
}
|
||||
|
||||
if (
|
||||
navigationMenuItem.itemType === NavigationMenuItemType.VIEW &&
|
||||
isDefined(navigationMenuItem.viewId)
|
||||
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
|
||||
export const hasNavigationMenuItemOwnColor = (item: { type?: string | null }) =>
|
||||
item.type === NavigationMenuItemType.FOLDER;
|
||||
+4
-3
@@ -6,9 +6,10 @@ export const isLocationMatchingNavigationMenuItem = (
|
||||
currentViewPath: string,
|
||||
navigationMenuItem: Pick<ProcessedNavigationMenuItem, 'itemType' | 'link'>,
|
||||
) => {
|
||||
const isViewItem =
|
||||
navigationMenuItem.itemType === NavigationMenuItemType.VIEW;
|
||||
return isViewItem
|
||||
const isViewBasedItem =
|
||||
navigationMenuItem.itemType === NavigationMenuItemType.VIEW ||
|
||||
navigationMenuItem.itemType === NavigationMenuItemType.OBJECT;
|
||||
return isViewBasedItem
|
||||
? navigationMenuItem.link === currentViewPath
|
||||
: navigationMenuItem.link === currentPath;
|
||||
};
|
||||
|
||||
+5
-14
@@ -1,15 +1,6 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
export const isNavigationMenuItemFolder = (item: {
|
||||
name?: string | null;
|
||||
link?: string | null;
|
||||
folderId?: string | null;
|
||||
viewId?: string | null;
|
||||
targetRecordId?: string | null;
|
||||
targetObjectMetadataId?: string | null;
|
||||
}) =>
|
||||
isDefined(item.name) &&
|
||||
!isDefined(item.link) &&
|
||||
!isDefined(item.targetRecordId) &&
|
||||
!isDefined(item.targetObjectMetadataId) &&
|
||||
!isDefined(item.viewId);
|
||||
export const isNavigationMenuItemFolder = (
|
||||
item: Pick<NavigationMenuItem, 'type'>,
|
||||
) => item.type === NavigationMenuItemType.FOLDER;
|
||||
|
||||
+5
-12
@@ -1,13 +1,6 @@
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
export const isNavigationMenuItemLink = (item: {
|
||||
link?: string | null;
|
||||
viewId?: string | null;
|
||||
targetRecordId?: string | null;
|
||||
targetObjectMetadataId?: string | null;
|
||||
}) =>
|
||||
isDefined(item.link) &&
|
||||
(item.link ?? '').trim() !== '' &&
|
||||
!isDefined(item.viewId) &&
|
||||
!isDefined(item.targetRecordId) &&
|
||||
!isDefined(item.targetObjectMetadataId);
|
||||
export const isNavigationMenuItemLink = (
|
||||
item: Pick<NavigationMenuItem, 'type'>,
|
||||
) => item.type === NavigationMenuItemType.LINK;
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
export const isNavigationMenuItemObject = (
|
||||
item: Pick<NavigationMenuItem, 'type'>,
|
||||
) => item.type === NavigationMenuItemType.OBJECT;
|
||||
+39
@@ -26,6 +26,45 @@ export const sortNavigationMenuItems = (
|
||||
): 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,
|
||||
|
||||
+1
-1
@@ -94,7 +94,7 @@ export const FavoritesDragDropProviderContent = ({
|
||||
destination.droppableId,
|
||||
);
|
||||
const isFolderOverFolder =
|
||||
payload?.type === 'folder' && folderId !== null;
|
||||
payload?.type === 'FOLDER' && folderId !== null;
|
||||
setForbiddenDropTargetId(isFolderOverFolder ? dropTargetId : null);
|
||||
} else {
|
||||
setForbiddenDropTargetId(null);
|
||||
|
||||
@@ -229,7 +229,7 @@ export const useWorkspaceDndKit = (): {
|
||||
resolved.destination.droppableId,
|
||||
);
|
||||
const folderDrag =
|
||||
getPayload()?.type === 'folder' && isDefined(folderId);
|
||||
getPayload()?.type === 'FOLDER' && isDefined(folderId);
|
||||
setForbiddenDropTargetId(
|
||||
folderDrag ? resolved.effectiveDropTargetId : null,
|
||||
);
|
||||
|
||||
@@ -9,5 +9,5 @@ export const isFolderDrag = (
|
||||
payload: AddToNavPayload,
|
||||
sourceItem: NavigationMenuItem | undefined,
|
||||
): boolean =>
|
||||
payload?.type === 'folder' ||
|
||||
payload?.type === 'FOLDER' ||
|
||||
(isDefined(sourceItem) && isNavigationMenuItemFolder(sourceItem));
|
||||
|
||||
+5
-8
@@ -2,7 +2,6 @@ import { ObjectIconWithViewOverlay } from '@/navigation-menu-item/components/Obj
|
||||
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 { getStandardObjectIconColor } from '@/navigation-menu-item/utils/getStandardObjectIconColor';
|
||||
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/utils/sortNavigationMenuItems';
|
||||
import { lastVisitedViewPerObjectMetadataItemState } from '@/navigation/states/lastVisitedViewPerObjectMetadataItemState';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
@@ -52,7 +51,9 @@ export const NavigationDrawerItemForObjectMetadataItem = ({
|
||||
const isRecord =
|
||||
navigationMenuItem?.itemType === NavigationMenuItemType.RECORD;
|
||||
const isView = navigationMenuItem?.itemType === NavigationMenuItemType.VIEW;
|
||||
const hasCustomLink = isRecord || isView;
|
||||
const isObject =
|
||||
navigationMenuItem?.itemType === NavigationMenuItemType.OBJECT;
|
||||
const hasCustomLink = isRecord || isView || isObject;
|
||||
|
||||
const navigationPath = hasCustomLink
|
||||
? navigationMenuItem!.link
|
||||
@@ -63,7 +64,7 @@ export const NavigationDrawerItemForObjectMetadataItem = ({
|
||||
);
|
||||
|
||||
const isActive = hasCustomLink
|
||||
? (isView ? currentPathWithSearch : currentPath) ===
|
||||
? (isView || isObject ? currentPathWithSearch : currentPath) ===
|
||||
navigationMenuItem!.link
|
||||
: currentPath ===
|
||||
getAppPath(AppPath.RecordIndexPage, {
|
||||
@@ -114,11 +115,7 @@ export const NavigationDrawerItemForObjectMetadataItem = ({
|
||||
)
|
||||
: getIcon(objectMetadataItem.icon);
|
||||
|
||||
const iconThemeColor = !isRecord
|
||||
? isDefined(navigationMenuItem?.color)
|
||||
? navigationMenuItem.color
|
||||
: (getStandardObjectIconColor(objectMetadataItem.nameSingular) ?? 'gray')
|
||||
: undefined;
|
||||
const iconThemeColor = !isRecord ? objectNavItemColor : undefined;
|
||||
|
||||
const secondaryLabel =
|
||||
isRecord || isViewWithCustomName
|
||||
|
||||
+2
-1
@@ -96,6 +96,7 @@ export const NavigationDrawerSectionForWorkspaceItems = ({
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
type === NavigationMenuItemType.OBJECT ||
|
||||
type === NavigationMenuItemType.VIEW ||
|
||||
type === NavigationMenuItemType.RECORD
|
||||
) {
|
||||
@@ -123,7 +124,7 @@ export const NavigationDrawerSectionForWorkspaceItems = ({
|
||||
? () => {
|
||||
const type = item.itemType;
|
||||
const objectMetadataItem =
|
||||
type === 'view' || type === 'record'
|
||||
type === 'OBJECT' || type === 'VIEW' || type === 'RECORD'
|
||||
? getObjectMetadataForNavigationMenuItem(
|
||||
item as ProcessedNavigationMenuItem,
|
||||
objectMetadataItems,
|
||||
|
||||
@@ -8,6 +8,7 @@ export const OBJECT_METADATA_FRAGMENT = gql`
|
||||
namePlural
|
||||
labelSingular
|
||||
labelPlural
|
||||
color
|
||||
description
|
||||
icon
|
||||
isCustom
|
||||
|
||||
@@ -10,6 +10,7 @@ export const CREATE_ONE_OBJECT_METADATA_ITEM = gql`
|
||||
labelPlural
|
||||
description
|
||||
icon
|
||||
color
|
||||
isCustom
|
||||
isActive
|
||||
isSearchable
|
||||
@@ -90,6 +91,7 @@ export const UPDATE_ONE_OBJECT_METADATA_ITEM = gql`
|
||||
labelPlural
|
||||
description
|
||||
icon
|
||||
color
|
||||
isCustom
|
||||
isActive
|
||||
isSearchable
|
||||
@@ -112,6 +114,7 @@ export const DELETE_ONE_OBJECT_METADATA_ITEM = gql`
|
||||
labelPlural
|
||||
description
|
||||
icon
|
||||
color
|
||||
isCustom
|
||||
isActive
|
||||
isSearchable
|
||||
|
||||
+1
@@ -27,6 +27,7 @@ export const responseData = {
|
||||
labelPlural: 'View Filters',
|
||||
description: '',
|
||||
icon: '',
|
||||
color: null,
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSearchable: false,
|
||||
|
||||
+2
@@ -10,6 +10,7 @@ export const query = gql`
|
||||
labelPlural
|
||||
description
|
||||
icon
|
||||
color
|
||||
isCustom
|
||||
isActive
|
||||
isSearchable
|
||||
@@ -32,6 +33,7 @@ export const responseData = {
|
||||
labelPlural: '',
|
||||
description: '',
|
||||
icon: '',
|
||||
color: null,
|
||||
isCustom: false,
|
||||
isActive: true,
|
||||
isSearchable: false,
|
||||
|
||||
+3
-19
@@ -1,11 +1,8 @@
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
|
||||
import { NavigationMenuItemStyleIcon } from '@/navigation-menu-item/components/NavigationMenuItemStyleIcon';
|
||||
import { useNavigationMenuItemsData } from '@/navigation-menu-item/hooks/useNavigationMenuItemsData';
|
||||
import { getStandardObjectIconColor } from '@/navigation-menu-item/utils/getStandardObjectIconColor';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue';
|
||||
import { indexViewIdFromObjectMetadataItemFamilySelector } from '@/views/states/selectors/indexViewIdFromObjectMetadataItemFamilySelector';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useIcons } from 'twenty-ui/display';
|
||||
|
||||
@@ -14,11 +11,6 @@ export const RecordIndexPageHeaderIcon = ({
|
||||
}: {
|
||||
objectMetadataItem?: ObjectMetadataItem;
|
||||
}) => {
|
||||
const { workspaceNavigationMenuItems } = useNavigationMenuItemsData();
|
||||
const coreIndexViewId = useAtomFamilySelectorValue(
|
||||
indexViewIdFromObjectMetadataItemFamilySelector,
|
||||
{ objectMetadataItemId: objectMetadataItem?.id ?? '' },
|
||||
);
|
||||
const { getIcon } = useIcons();
|
||||
const ObjectIcon = getIcon(objectMetadataItem?.icon);
|
||||
|
||||
@@ -26,17 +18,9 @@ export const RecordIndexPageHeaderIcon = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
const navItem = isDefined(coreIndexViewId)
|
||||
? workspaceNavigationMenuItems.find(
|
||||
(item) => item.viewId === coreIndexViewId,
|
||||
)
|
||||
: undefined;
|
||||
const navigationMenuItemColor = isNonEmptyString(navItem?.color)
|
||||
? navItem.color
|
||||
: undefined;
|
||||
const iconColor =
|
||||
navigationMenuItemColor ??
|
||||
getStandardObjectIconColor(objectMetadataItem?.nameSingular ?? '');
|
||||
const iconColor = isNonEmptyString(objectMetadataItem?.color)
|
||||
? objectMetadataItem.color
|
||||
: getStandardObjectIconColor(objectMetadataItem?.nameSingular ?? '');
|
||||
|
||||
return (
|
||||
<NavigationMenuItemStyleIcon
|
||||
|
||||
+16
-4
@@ -1,3 +1,7 @@
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
|
||||
import { NavigationMenuItemStyleIcon } from '@/navigation-menu-item/components/NavigationMenuItemStyleIcon';
|
||||
import { getStandardObjectIconColor } from '@/navigation-menu-item/utils/getStandardObjectIconColor';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||
@@ -9,10 +13,9 @@ import { useRecordShowPagePagination } from '@/object-record/record-show/hooks/u
|
||||
import { RecordTitleCell } from '@/object-record/record-title-cell/components/RecordTitleCell';
|
||||
import { RecordTitleCellContainerType } from '@/object-record/record-title-cell/types/RecordTitleCellContainerType';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useContext } from 'react';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledEditableTitleContainer = styled.div`
|
||||
align-items: center;
|
||||
@@ -52,7 +55,6 @@ export const ObjectRecordShowPageBreadcrumb = ({
|
||||
objectLabel: string;
|
||||
labelIdentifierFieldMetadataItem?: FieldMetadataItem;
|
||||
}) => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const { loading } = useFindOneRecord({
|
||||
objectNameSingular,
|
||||
objectRecordId,
|
||||
@@ -82,6 +84,11 @@ export const ObjectRecordShowPageBreadcrumb = ({
|
||||
objectNameSingular,
|
||||
objectRecordId,
|
||||
);
|
||||
|
||||
const iconColor = isNonEmptyString(objectMetadataItem?.color)
|
||||
? objectMetadataItem.color
|
||||
: getStandardObjectIconColor(objectNameSingular);
|
||||
|
||||
if (loading) {
|
||||
return null;
|
||||
}
|
||||
@@ -93,7 +100,12 @@ export const ObjectRecordShowPageBreadcrumb = ({
|
||||
navigateToIndexView();
|
||||
}}
|
||||
>
|
||||
{isDefined(HeaderIcon) && <HeaderIcon size={theme.icon.size.md} />}
|
||||
{isDefined(HeaderIcon) && (
|
||||
<NavigationMenuItemStyleIcon
|
||||
Icon={HeaderIcon}
|
||||
color={iconColor ?? undefined}
|
||||
/>
|
||||
)}
|
||||
{objectLabel}
|
||||
<span>{' / '}</span>
|
||||
</StyledEditableTitlePrefix>
|
||||
|
||||
+4
-4
@@ -7,7 +7,6 @@ import { NavigationMenuItemType } from '@/navigation-menu-item/constants/Navigat
|
||||
import { useSelectedNavigationMenuItemEditItem } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItem';
|
||||
import { useSelectedNavigationMenuItemEditItemLabel } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItemLabel';
|
||||
import { useSelectedNavigationMenuItemEditItemObjectMetadata } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItemObjectMetadata';
|
||||
import { ViewKey } from '@/views/types/ViewKey';
|
||||
|
||||
export const SidePanelObjectViewRecordInfo = () => {
|
||||
const { t } = useLingui();
|
||||
@@ -25,19 +24,20 @@ export const SidePanelObjectViewRecordInfo = () => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isViewOrRecord = [
|
||||
const isObjectViewOrRecord = [
|
||||
NavigationMenuItemType.OBJECT,
|
||||
NavigationMenuItemType.VIEW,
|
||||
NavigationMenuItemType.RECORD,
|
||||
].includes(processedItem.itemType);
|
||||
|
||||
if (!isViewOrRecord) {
|
||||
if (!isObjectViewOrRecord) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const label =
|
||||
processedItem.itemType === NavigationMenuItemType.RECORD
|
||||
? selectedItemObjectMetadata?.labelSingular
|
||||
: processedItem.viewKey === ViewKey.INDEX
|
||||
: processedItem.itemType === NavigationMenuItemType.OBJECT
|
||||
? t`Object`
|
||||
: t`View`;
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ export const SidePanelPageInfo = ({ pageChip }: SidePanelPageInfoProps) => {
|
||||
}
|
||||
|
||||
if (
|
||||
itemType === NavigationMenuItemType.OBJECT ||
|
||||
itemType === NavigationMenuItemType.VIEW ||
|
||||
itemType === NavigationMenuItemType.RECORD
|
||||
) {
|
||||
|
||||
+30
-5
@@ -1,3 +1,8 @@
|
||||
import { useObjectNavItemColor } from '@/navigation-menu-item/hooks/useObjectNavItemColor';
|
||||
import { navigationMenuItemsSelector } from '@/navigation-menu-item/states/navigationMenuItemsSelector';
|
||||
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/types/processed-navigation-menu-item';
|
||||
import { getEffectiveNavigationMenuItemColor } from '@/navigation-menu-item/utils/getEffectiveNavigationMenuItemColor';
|
||||
import { parseThemeColor } from '@/navigation-menu-item/utils/parseThemeColor';
|
||||
import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
|
||||
import { SidePanelList } from '@/side-panel/components/SidePanelList';
|
||||
import { SidePanelEditColorOption } from '@/side-panel/pages/navigation-menu-item/components/SidePanelEditColorOption';
|
||||
@@ -6,13 +11,15 @@ import {
|
||||
SidePanelEditOrganizeActions,
|
||||
} from '@/side-panel/pages/navigation-menu-item/components/SidePanelEditOrganizeActions';
|
||||
import { getOrganizeActionsSelectableItemIds } from '@/side-panel/pages/navigation-menu-item/utils/getOrganizeActionsSelectableItemIds';
|
||||
import { useSelectedNavigationMenuItemEditItem } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItem';
|
||||
import { parseThemeColor } from '@/navigation-menu-item/utils/parseThemeColor';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type SidePanelEditObjectViewBaseProps = OrganizeActionsProps & {
|
||||
onOpenFolderPicker: () => void;
|
||||
showColorOption?: boolean;
|
||||
selectedItem?: ProcessedNavigationMenuItem | null;
|
||||
};
|
||||
|
||||
export const SidePanelEditObjectViewBase = ({
|
||||
@@ -25,18 +32,36 @@ export const SidePanelEditObjectViewBase = ({
|
||||
onAddBefore,
|
||||
onAddAfter,
|
||||
showColorOption = false,
|
||||
selectedItem,
|
||||
}: SidePanelEditObjectViewBaseProps) => {
|
||||
const { t } = useLingui();
|
||||
const { selectedItem } = useSelectedNavigationMenuItemEditItem();
|
||||
const selectableItemIds = getOrganizeActionsSelectableItemIds(true);
|
||||
const objectColor = useObjectNavItemColor(
|
||||
selectedItem?.objectNameSingular ?? '',
|
||||
);
|
||||
|
||||
const navigationMenuItems = useAtomStateValue(navigationMenuItemsSelector);
|
||||
const persistedNavItem = navigationMenuItems.find(
|
||||
(item) => item.id === selectedItem?.id,
|
||||
);
|
||||
const hasUserChangedColor =
|
||||
isNonEmptyString(selectedItem?.color) &&
|
||||
selectedItem.color !== (persistedNavItem?.color ?? undefined);
|
||||
|
||||
const effectiveColor = isDefined(selectedItem)
|
||||
? getEffectiveNavigationMenuItemColor(selectedItem, objectColor)
|
||||
: undefined;
|
||||
const displayColor = hasUserChangedColor
|
||||
? selectedItem.color
|
||||
: effectiveColor;
|
||||
|
||||
return (
|
||||
<SidePanelList commandGroups={[]} selectableItemIds={selectableItemIds}>
|
||||
{showColorOption && selectedItem && (
|
||||
{showColorOption && isDefined(selectedItem) && (
|
||||
<SidePanelGroup heading={t`Customize`}>
|
||||
<SidePanelEditColorOption
|
||||
navigationMenuItemId={selectedItem.id}
|
||||
color={parseThemeColor(selectedItem.color)}
|
||||
color={parseThemeColor(displayColor)}
|
||||
/>
|
||||
</SidePanelGroup>
|
||||
)}
|
||||
|
||||
+17
-19
@@ -4,9 +4,9 @@ import { useNavigationMenuItemsDraftState } from '@/navigation-menu-item/hooks/u
|
||||
import { useOpenAddItemToFolderPage } from '@/navigation-menu-item/hooks/useOpenAddItemToFolderPage';
|
||||
import { useSelectedNavigationMenuItemEditItem } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItem';
|
||||
import { useSelectedNavigationMenuItemEditItemLabel } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItemLabel';
|
||||
import { useSelectedNavigationMenuItemEditItemObjectMetadata } from '@/navigation-menu-item/hooks/useSelectedNavigationMenuItemEditItemObjectMetadata';
|
||||
import { useUpdateLinkInDraft } from '@/navigation-menu-item/hooks/useUpdateLinkInDraft';
|
||||
import { selectedNavigationMenuItemInEditModeState } from '@/navigation-menu-item/states/selectedNavigationMenuItemInEditModeState';
|
||||
import { type ProcessedNavigationMenuItem } from '@/navigation-menu-item/types/processed-navigation-menu-item';
|
||||
import { parseThemeColor } from '@/navigation-menu-item/utils/parseThemeColor';
|
||||
import { SidePanelGroup } from '@/side-panel/components/SidePanelGroup';
|
||||
import { SidePanelList } from '@/side-panel/components/SidePanelList';
|
||||
@@ -21,7 +21,6 @@ import { getOrganizeActionsSelectableItemIds } from '@/side-panel/pages/navigati
|
||||
import { SidePanelSubPages } from '@/side-panel/types/SidePanelSubPages';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { ViewKey } from '@/views/types/ViewKey';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
@@ -47,8 +46,6 @@ export const SidePanelNavigationMenuItemEditPage = () => {
|
||||
);
|
||||
const { selectedItemLabel } = useSelectedNavigationMenuItemEditItemLabel();
|
||||
const { selectedItem } = useSelectedNavigationMenuItemEditItem();
|
||||
const { selectedItemObjectMetadata } =
|
||||
useSelectedNavigationMenuItemEditItemObjectMetadata();
|
||||
const selectedItemType = selectedItem?.itemType ?? null;
|
||||
|
||||
const { navigateToSidePanelSubPage } = useSidePanelSubPageHistory();
|
||||
@@ -97,8 +94,22 @@ export const SidePanelNavigationMenuItemEditPage = () => {
|
||||
}
|
||||
|
||||
switch (selectedItemType) {
|
||||
case NavigationMenuItemType.VIEW:
|
||||
if (!selectedItemObjectMetadata) return null;
|
||||
case NavigationMenuItemType.OBJECT:
|
||||
return (
|
||||
<SidePanelEditObjectViewBase
|
||||
onOpenFolderPicker={openFolderPicker}
|
||||
canMoveUp={canMoveUp}
|
||||
canMoveDown={canMoveDown}
|
||||
onMoveUp={onMoveUp}
|
||||
onMoveDown={onMoveDown}
|
||||
onRemove={onRemove}
|
||||
onAddBefore={onAddBefore}
|
||||
onAddAfter={onAddAfter}
|
||||
showColorOption={isDefined(selectedItem)}
|
||||
selectedItem={selectedItem as ProcessedNavigationMenuItem | undefined}
|
||||
/>
|
||||
);
|
||||
case NavigationMenuItemType.VIEW:
|
||||
return (
|
||||
<SidePanelEditObjectViewBase
|
||||
onOpenFolderPicker={openFolderPicker}
|
||||
@@ -109,11 +120,6 @@ export const SidePanelNavigationMenuItemEditPage = () => {
|
||||
onRemove={onRemove}
|
||||
onAddBefore={onAddBefore}
|
||||
onAddAfter={onAddAfter}
|
||||
showColorOption={
|
||||
selectedItem &&
|
||||
'viewKey' in selectedItem &&
|
||||
selectedItem.viewKey === ViewKey.INDEX
|
||||
}
|
||||
/>
|
||||
);
|
||||
case NavigationMenuItemType.LINK:
|
||||
@@ -187,14 +193,6 @@ export const SidePanelNavigationMenuItemEditPage = () => {
|
||||
commandGroups={[]}
|
||||
selectableItemIds={getOrganizeActionsSelectableItemIds(true)}
|
||||
>
|
||||
{selectedItem && (
|
||||
<SidePanelGroup heading={t`Customize`}>
|
||||
<SidePanelEditColorOption
|
||||
navigationMenuItemId={selectedItem.id}
|
||||
color={parseThemeColor(selectedItem.color)}
|
||||
/>
|
||||
</SidePanelGroup>
|
||||
)}
|
||||
<SidePanelEditOrganizeActions
|
||||
canMoveUp={canMoveUp}
|
||||
canMoveDown={canMoveDown}
|
||||
|
||||
+1
-5
@@ -56,16 +56,12 @@ export const SidePanelNewSidebarItemObjectSubPage = () => {
|
||||
objectMetadataIdsWithDisplayableViews,
|
||||
});
|
||||
|
||||
const handleSelectObject = (
|
||||
objectMetadataItem: ObjectMetadataItem,
|
||||
defaultViewId: string,
|
||||
) => {
|
||||
const handleSelectObject = (objectMetadataItem: ObjectMetadataItem) => {
|
||||
if (objectMetadataIdsInWorkspace.has(objectMetadataItem.id)) {
|
||||
return;
|
||||
}
|
||||
const itemId = addObjectToDraft(
|
||||
objectMetadataItem.id,
|
||||
defaultViewId,
|
||||
currentDraft,
|
||||
addMenuItemInsertionContext?.targetFolderId,
|
||||
addMenuItemInsertionContext?.targetIndex,
|
||||
|
||||
+1
-5
@@ -55,16 +55,12 @@ export const SidePanelNewSidebarItemObjectSystemPickerSubPage = () => {
|
||||
objectMetadataIdsWithDisplayableViews,
|
||||
});
|
||||
|
||||
const handleSelectObject = (
|
||||
objectMetadataItem: ObjectMetadataItem,
|
||||
defaultViewId: string,
|
||||
) => {
|
||||
const handleSelectObject = (objectMetadataItem: ObjectMetadataItem) => {
|
||||
if (objectMetadataIdsInWorkspace.has(objectMetadataItem.id)) {
|
||||
return;
|
||||
}
|
||||
const itemId = addObjectToDraft(
|
||||
objectMetadataItem.id,
|
||||
defaultViewId,
|
||||
currentDraft,
|
||||
addMenuItemInsertionContext?.targetFolderId,
|
||||
addMenuItemInsertionContext?.targetIndex,
|
||||
|
||||
+2
-6
@@ -16,10 +16,7 @@ import { indexViewIdFromObjectMetadataItemFamilySelector } from '@/views/states/
|
||||
|
||||
type SidePanelObjectMenuItemProps = {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
onSelect: (
|
||||
objectMetadataItem: ObjectMetadataItem,
|
||||
defaultViewId: string,
|
||||
) => void;
|
||||
onSelect: (objectMetadataItem: ObjectMetadataItem) => void;
|
||||
variant: 'add' | 'edit';
|
||||
dragIndex?: number;
|
||||
disableDrag?: boolean;
|
||||
@@ -52,7 +49,7 @@ export const SidePanelObjectMenuItem = ({
|
||||
if (isDisabled || !defaultViewId) {
|
||||
return;
|
||||
}
|
||||
onSelect(objectMetadataItem, defaultViewId);
|
||||
onSelect(objectMetadataItem);
|
||||
};
|
||||
|
||||
const styledIcon = () => (
|
||||
@@ -72,7 +69,6 @@ export const SidePanelObjectMenuItem = ({
|
||||
payload={{
|
||||
type: NavigationMenuItemType.OBJECT,
|
||||
objectMetadataId: objectMetadataItem.id,
|
||||
defaultViewId: defaultViewId ?? '',
|
||||
label: objectMetadataItem.labelPlural,
|
||||
iconColor,
|
||||
}}
|
||||
|
||||
+1
-4
@@ -12,10 +12,7 @@ type SidePanelObjectPickerItemProps = {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
isViewItem: boolean;
|
||||
onSelectObjectForViewEdit?: (objectMetadataItem: ObjectMetadataItem) => void;
|
||||
onChangeObject: (
|
||||
objectMetadataItem: ObjectMetadataItem,
|
||||
defaultViewId: string,
|
||||
) => void;
|
||||
onChangeObject: (objectMetadataItem: ObjectMetadataItem) => void;
|
||||
objectMenuItemVariant?: 'add' | 'edit';
|
||||
dragIndex?: number;
|
||||
disableDrag?: boolean;
|
||||
|
||||
+1
-4
@@ -19,10 +19,7 @@ type SidePanelObjectPickerSubViewProps = {
|
||||
onOpenSystemPicker: () => void;
|
||||
isViewItem: boolean;
|
||||
onSelectObjectForViewEdit?: (objectMetadataItem: ObjectMetadataItem) => void;
|
||||
onChangeObject: (
|
||||
objectMetadataItem: ObjectMetadataItem,
|
||||
defaultViewId: string,
|
||||
) => void;
|
||||
onChangeObject: (objectMetadataItem: ObjectMetadataItem) => void;
|
||||
objectMenuItemVariant?: 'add' | 'edit';
|
||||
emptyNoResultsText?: string;
|
||||
disableDrag?: boolean;
|
||||
|
||||
+1
-4
@@ -14,10 +14,7 @@ type SidePanelSystemObjectPickerSubViewProps = {
|
||||
onSearchChange: (value: string) => void;
|
||||
isViewItem: boolean;
|
||||
onSelectObjectForViewEdit?: (objectMetadataItem: ObjectMetadataItem) => void;
|
||||
onChangeObject: (
|
||||
objectMetadataItem: ObjectMetadataItem,
|
||||
defaultViewId: string,
|
||||
) => void;
|
||||
onChangeObject: (objectMetadataItem: ObjectMetadataItem) => void;
|
||||
objectMenuItemVariant?: 'add' | 'edit';
|
||||
emptyNoResultsText?: string;
|
||||
disableDrag?: boolean;
|
||||
|
||||
+7
-3
@@ -3,6 +3,7 @@ import type { Manifest } from 'twenty-shared/application';
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
import {
|
||||
FieldMetadataType,
|
||||
NavigationMenuItemType,
|
||||
RelationOnDeleteAction,
|
||||
RelationType,
|
||||
ViewType,
|
||||
@@ -1569,19 +1570,22 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
],
|
||||
navigationMenuItems: [
|
||||
{
|
||||
type: NavigationMenuItemType.OBJECT,
|
||||
position: 2,
|
||||
universalIdentifier: 'c1a2b3c4-0003-4a7b-8c9d-0e1f2a3b4c5d',
|
||||
viewUniversalIdentifier: 'b1a2b3c4-0003-4a7b-8c9d-0e1f2a3b4c5d',
|
||||
targetObjectUniversalIdentifier: 'e1a2b3c4-5e6f-4a7b-8c9d-0e1f2a3b4c5e',
|
||||
},
|
||||
{
|
||||
type: NavigationMenuItemType.OBJECT,
|
||||
position: 0,
|
||||
universalIdentifier: 'c1a2b3c4-0001-4a7b-8c9d-0e1f2a3b4c5d',
|
||||
viewUniversalIdentifier: 'b1a2b3c4-0001-4a7b-8c9d-0e1f2a3b4c5d',
|
||||
targetObjectUniversalIdentifier: '54b589ca-eeed-4950-a176-358418b85c05',
|
||||
},
|
||||
{
|
||||
type: NavigationMenuItemType.OBJECT,
|
||||
position: 1,
|
||||
universalIdentifier: 'c1a2b3c4-0002-4a7b-8c9d-0e1f2a3b4c5d',
|
||||
viewUniversalIdentifier: 'b1a2b3c4-0002-4a7b-8c9d-0e1f2a3b4c5d',
|
||||
targetObjectUniversalIdentifier: 'd1a2b3c4-5e6f-4a7b-8c9d-0e1f2a3b4c5d',
|
||||
},
|
||||
],
|
||||
logicFunctions: [
|
||||
|
||||
@@ -260,7 +260,8 @@ export class EntityAddCommand {
|
||||
|
||||
const navFile = getNavigationMenuItemBaseFile({
|
||||
name: objectName,
|
||||
viewUniversalIdentifier,
|
||||
type: 'OBJECT',
|
||||
targetObjectUniversalIdentifier: this.lastObjectUniversalIdentifier,
|
||||
});
|
||||
|
||||
const navFolderPath = customPath
|
||||
|
||||
+16
-6
@@ -19,25 +19,35 @@ describe('getNavigationMenuItemBaseFile', () => {
|
||||
expect(result).toContain('position: 0');
|
||||
});
|
||||
|
||||
it('should include viewUniversalIdentifier when provided', () => {
|
||||
it('should include viewUniversalIdentifier when type is VIEW', () => {
|
||||
const result = getNavigationMenuItemBaseFile({
|
||||
name: 'linked-item',
|
||||
type: 'VIEW',
|
||||
viewUniversalIdentifier: 'view-uuid-123',
|
||||
});
|
||||
|
||||
expect(result).toContain("type: 'VIEW'");
|
||||
expect(result).toContain("viewUniversalIdentifier: 'view-uuid-123'");
|
||||
expect(result).not.toContain('// Link to a view:');
|
||||
});
|
||||
|
||||
it('should include commented link options when viewUniversalIdentifier is not provided', () => {
|
||||
it('should default to VIEW type when no type is provided', () => {
|
||||
const result = getNavigationMenuItemBaseFile({
|
||||
name: 'unlinked-item',
|
||||
});
|
||||
|
||||
expect(result).toContain('// Link to a view:');
|
||||
expect(result).toContain("type: 'VIEW'");
|
||||
expect(result).toContain('// viewUniversalIdentifier:');
|
||||
expect(result).toContain('// targetObjectUniversalIdentifier:');
|
||||
expect(result).toContain('// link:');
|
||||
});
|
||||
|
||||
it('should include targetObjectUniversalIdentifier when type is OBJECT', () => {
|
||||
const result = getNavigationMenuItemBaseFile({
|
||||
name: 'object-item',
|
||||
type: 'OBJECT',
|
||||
targetObjectUniversalIdentifier: 'obj-uuid-123',
|
||||
});
|
||||
|
||||
expect(result).toContain("type: 'OBJECT'");
|
||||
expect(result).toContain("targetObjectUniversalIdentifier: 'obj-uuid-123'");
|
||||
});
|
||||
|
||||
it('should generate unique UUID when not provided', () => {
|
||||
|
||||
+22
-9
@@ -4,22 +4,35 @@ import { v4 } from 'uuid';
|
||||
export const getNavigationMenuItemBaseFile = ({
|
||||
name,
|
||||
universalIdentifier = v4(),
|
||||
type,
|
||||
viewUniversalIdentifier,
|
||||
targetObjectUniversalIdentifier,
|
||||
}: {
|
||||
name: string;
|
||||
universalIdentifier?: string;
|
||||
type?: 'OBJECT' | 'VIEW' | 'LINK' | 'FOLDER';
|
||||
viewUniversalIdentifier?: string;
|
||||
targetObjectUniversalIdentifier?: string;
|
||||
}) => {
|
||||
const kebabCaseName = kebabCase(name);
|
||||
|
||||
const linkConfig = viewUniversalIdentifier
|
||||
? ` viewUniversalIdentifier: '${viewUniversalIdentifier}',`
|
||||
: ` // Link to a view:
|
||||
// viewUniversalIdentifier: '...',
|
||||
// Or link to an object:
|
||||
// targetObjectUniversalIdentifier: '...',
|
||||
// Or link to an external URL:
|
||||
// link: 'https://example.com',`;
|
||||
let typeAndConfig: string;
|
||||
|
||||
if (type === 'OBJECT' && targetObjectUniversalIdentifier) {
|
||||
typeAndConfig = ` type: 'OBJECT',
|
||||
targetObjectUniversalIdentifier: '${targetObjectUniversalIdentifier}',`;
|
||||
} else if (type === 'VIEW' && viewUniversalIdentifier) {
|
||||
typeAndConfig = ` type: 'VIEW',
|
||||
viewUniversalIdentifier: '${viewUniversalIdentifier}',`;
|
||||
} else if (type === 'LINK') {
|
||||
typeAndConfig = ` type: 'LINK',
|
||||
link: 'https://example.com',`;
|
||||
} else if (type === 'FOLDER') {
|
||||
typeAndConfig = ` type: 'FOLDER',`;
|
||||
} else {
|
||||
typeAndConfig = ` type: 'VIEW',
|
||||
// viewUniversalIdentifier: '...',`;
|
||||
}
|
||||
|
||||
return `import { defineNavigationMenuItem } from 'twenty-sdk';
|
||||
|
||||
@@ -28,7 +41,7 @@ export default defineNavigationMenuItem({
|
||||
name: '${kebabCaseName}',
|
||||
icon: 'IconList',
|
||||
position: 0,
|
||||
${linkConfig}
|
||||
${typeAndConfig}
|
||||
});
|
||||
`;
|
||||
};
|
||||
|
||||
@@ -485,6 +485,7 @@ type ObjectStandardOverrides {
|
||||
labelPlural: String
|
||||
description: String
|
||||
icon: String
|
||||
color: String
|
||||
translations: JSON
|
||||
}
|
||||
|
||||
@@ -499,6 +500,7 @@ type Object {
|
||||
icon: String
|
||||
standardOverrides: ObjectStandardOverrides
|
||||
shortcut: String
|
||||
color: String
|
||||
isCustom: Boolean!
|
||||
isRemote: Boolean!
|
||||
isActive: Boolean!
|
||||
@@ -1939,6 +1941,7 @@ type NavigationMenuItem {
|
||||
targetRecordId: UUID
|
||||
targetObjectMetadataId: UUID
|
||||
viewId: UUID
|
||||
type: NavigationMenuItemType!
|
||||
name: String
|
||||
link: String
|
||||
icon: String
|
||||
@@ -1951,6 +1954,14 @@ type NavigationMenuItem {
|
||||
targetRecordIdentifier: RecordIdentifier
|
||||
}
|
||||
|
||||
enum NavigationMenuItemType {
|
||||
VIEW
|
||||
FOLDER
|
||||
LINK
|
||||
OBJECT
|
||||
RECORD
|
||||
}
|
||||
|
||||
type LogicFunctionExecutionResult {
|
||||
"""Execution result in JSON format"""
|
||||
data: JSON
|
||||
@@ -2245,6 +2256,7 @@ type MinimalObjectMetadata {
|
||||
labelSingular: String!
|
||||
labelPlural: String!
|
||||
icon: String
|
||||
color: String
|
||||
isCustom: Boolean!
|
||||
isActive: Boolean!
|
||||
isSystem: Boolean!
|
||||
@@ -3507,6 +3519,7 @@ input CreateObjectInput {
|
||||
description: String
|
||||
icon: String
|
||||
shortcut: String
|
||||
color: String
|
||||
skipNameField: Boolean
|
||||
isRemote: Boolean
|
||||
primaryKeyColumnType: String
|
||||
@@ -3534,6 +3547,7 @@ input UpdateObjectPayload {
|
||||
description: String
|
||||
icon: String
|
||||
shortcut: String
|
||||
color: String
|
||||
isActive: Boolean
|
||||
labelIdentifierFieldMetadataId: UUID
|
||||
imageIdentifierFieldMetadataId: UUID
|
||||
@@ -3791,6 +3805,7 @@ input CreateNavigationMenuItemInput {
|
||||
targetRecordId: UUID
|
||||
targetObjectMetadataId: UUID
|
||||
viewId: UUID
|
||||
type: NavigationMenuItemType!
|
||||
name: String
|
||||
link: String
|
||||
icon: String
|
||||
|
||||
@@ -344,6 +344,7 @@ export interface ObjectStandardOverrides {
|
||||
labelPlural?: Scalars['String']
|
||||
description?: Scalars['String']
|
||||
icon?: Scalars['String']
|
||||
color?: Scalars['String']
|
||||
translations?: Scalars['JSON']
|
||||
__typename: 'ObjectStandardOverrides'
|
||||
}
|
||||
@@ -359,6 +360,7 @@ export interface Object {
|
||||
icon?: Scalars['String']
|
||||
standardOverrides?: ObjectStandardOverrides
|
||||
shortcut?: Scalars['String']
|
||||
color?: Scalars['String']
|
||||
isCustom: Scalars['Boolean']
|
||||
isRemote: Scalars['Boolean']
|
||||
isActive: Scalars['Boolean']
|
||||
@@ -1675,6 +1677,7 @@ export interface NavigationMenuItem {
|
||||
targetRecordId?: Scalars['UUID']
|
||||
targetObjectMetadataId?: Scalars['UUID']
|
||||
viewId?: Scalars['UUID']
|
||||
type: NavigationMenuItemType
|
||||
name?: Scalars['String']
|
||||
link?: Scalars['String']
|
||||
icon?: Scalars['String']
|
||||
@@ -1688,6 +1691,8 @@ export interface NavigationMenuItem {
|
||||
__typename: 'NavigationMenuItem'
|
||||
}
|
||||
|
||||
export type NavigationMenuItemType = 'VIEW' | 'FOLDER' | 'LINK' | 'OBJECT' | 'RECORD'
|
||||
|
||||
export interface LogicFunctionExecutionResult {
|
||||
/** Execution result in JSON format */
|
||||
data?: Scalars['JSON']
|
||||
@@ -1901,6 +1906,7 @@ export interface MinimalObjectMetadata {
|
||||
labelSingular: Scalars['String']
|
||||
labelPlural: Scalars['String']
|
||||
icon?: Scalars['String']
|
||||
color?: Scalars['String']
|
||||
isCustom: Scalars['Boolean']
|
||||
isActive: Scalars['Boolean']
|
||||
isSystem: Scalars['Boolean']
|
||||
@@ -3247,6 +3253,7 @@ export interface ObjectStandardOverridesGenqlSelection{
|
||||
labelPlural?: boolean | number
|
||||
description?: boolean | number
|
||||
icon?: boolean | number
|
||||
color?: boolean | number
|
||||
translations?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
@@ -3263,6 +3270,7 @@ export interface ObjectGenqlSelection{
|
||||
icon?: boolean | number
|
||||
standardOverrides?: ObjectStandardOverridesGenqlSelection
|
||||
shortcut?: boolean | number
|
||||
color?: boolean | number
|
||||
isCustom?: boolean | number
|
||||
isRemote?: boolean | number
|
||||
isActive?: boolean | number
|
||||
@@ -4666,6 +4674,7 @@ export interface NavigationMenuItemGenqlSelection{
|
||||
targetRecordId?: boolean | number
|
||||
targetObjectMetadataId?: boolean | number
|
||||
viewId?: boolean | number
|
||||
type?: boolean | number
|
||||
name?: boolean | number
|
||||
link?: boolean | number
|
||||
icon?: boolean | number
|
||||
@@ -4900,6 +4909,7 @@ export interface MinimalObjectMetadataGenqlSelection{
|
||||
labelSingular?: boolean | number
|
||||
labelPlural?: boolean | number
|
||||
icon?: boolean | number
|
||||
color?: boolean | number
|
||||
isCustom?: boolean | number
|
||||
isActive?: boolean | number
|
||||
isSystem?: boolean | number
|
||||
@@ -6006,7 +6016,7 @@ export interface CreateOneObjectInput {
|
||||
/** The object to create */
|
||||
object: CreateObjectInput}
|
||||
|
||||
export interface CreateObjectInput {nameSingular: Scalars['String'],namePlural: Scalars['String'],labelSingular: Scalars['String'],labelPlural: Scalars['String'],description?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),shortcut?: (Scalars['String'] | null),skipNameField?: (Scalars['Boolean'] | null),isRemote?: (Scalars['Boolean'] | null),primaryKeyColumnType?: (Scalars['String'] | null),primaryKeyFieldMetadataSettings?: (Scalars['JSON'] | null),isLabelSyncedWithName?: (Scalars['Boolean'] | null)}
|
||||
export interface CreateObjectInput {nameSingular: Scalars['String'],namePlural: Scalars['String'],labelSingular: Scalars['String'],labelPlural: Scalars['String'],description?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),shortcut?: (Scalars['String'] | null),color?: (Scalars['String'] | null),skipNameField?: (Scalars['Boolean'] | null),isRemote?: (Scalars['Boolean'] | null),primaryKeyColumnType?: (Scalars['String'] | null),primaryKeyFieldMetadataSettings?: (Scalars['JSON'] | null),isLabelSyncedWithName?: (Scalars['Boolean'] | null)}
|
||||
|
||||
export interface DeleteOneObjectInput {
|
||||
/** The id of the record to delete. */
|
||||
@@ -6016,7 +6026,7 @@ export interface UpdateOneObjectInput {update: UpdateObjectPayload,
|
||||
/** The id of the object to update */
|
||||
id: Scalars['UUID']}
|
||||
|
||||
export interface UpdateObjectPayload {labelSingular?: (Scalars['String'] | null),labelPlural?: (Scalars['String'] | null),nameSingular?: (Scalars['String'] | null),namePlural?: (Scalars['String'] | null),description?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),shortcut?: (Scalars['String'] | null),isActive?: (Scalars['Boolean'] | null),labelIdentifierFieldMetadataId?: (Scalars['UUID'] | null),imageIdentifierFieldMetadataId?: (Scalars['UUID'] | null),isLabelSyncedWithName?: (Scalars['Boolean'] | null)}
|
||||
export interface UpdateObjectPayload {labelSingular?: (Scalars['String'] | null),labelPlural?: (Scalars['String'] | null),nameSingular?: (Scalars['String'] | null),namePlural?: (Scalars['String'] | null),description?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),shortcut?: (Scalars['String'] | null),color?: (Scalars['String'] | null),isActive?: (Scalars['Boolean'] | null),labelIdentifierFieldMetadataId?: (Scalars['UUID'] | null),imageIdentifierFieldMetadataId?: (Scalars['UUID'] | null),isLabelSyncedWithName?: (Scalars['Boolean'] | null)}
|
||||
|
||||
export interface UpdateViewFieldInput {
|
||||
/** The id of the view field to update */
|
||||
@@ -6108,7 +6118,7 @@ export interface CreateAgentInput {name?: (Scalars['String'] | null),label: Scal
|
||||
|
||||
export interface UpdateAgentInput {id: Scalars['UUID'],name?: (Scalars['String'] | null),label?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),description?: (Scalars['String'] | null),prompt?: (Scalars['String'] | null),modelId?: (Scalars['String'] | null),roleId?: (Scalars['UUID'] | null),responseFormat?: (Scalars['JSON'] | null),modelConfiguration?: (Scalars['JSON'] | null),evaluationInputs?: (Scalars['String'][] | null)}
|
||||
|
||||
export interface CreateNavigationMenuItemInput {userWorkspaceId?: (Scalars['UUID'] | null),targetRecordId?: (Scalars['UUID'] | null),targetObjectMetadataId?: (Scalars['UUID'] | null),viewId?: (Scalars['UUID'] | null),name?: (Scalars['String'] | null),link?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),color?: (Scalars['String'] | null),folderId?: (Scalars['UUID'] | null),position?: (Scalars['Float'] | null)}
|
||||
export interface CreateNavigationMenuItemInput {userWorkspaceId?: (Scalars['UUID'] | null),targetRecordId?: (Scalars['UUID'] | null),targetObjectMetadataId?: (Scalars['UUID'] | null),viewId?: (Scalars['UUID'] | null),type: NavigationMenuItemType,name?: (Scalars['String'] | null),link?: (Scalars['String'] | null),icon?: (Scalars['String'] | null),color?: (Scalars['String'] | null),folderId?: (Scalars['UUID'] | null),position?: (Scalars['Float'] | null)}
|
||||
|
||||
export interface UpdateOneNavigationMenuItemInput {
|
||||
/** The id of the record to update */
|
||||
@@ -8694,6 +8704,14 @@ export const enumRelationType = {
|
||||
MANY_TO_ONE: 'MANY_TO_ONE' as const
|
||||
}
|
||||
|
||||
export const enumNavigationMenuItemType = {
|
||||
VIEW: 'VIEW' as const,
|
||||
FOLDER: 'FOLDER' as const,
|
||||
LINK: 'LINK' as const,
|
||||
OBJECT: 'OBJECT' as const,
|
||||
RECORD: 'RECORD' as const
|
||||
}
|
||||
|
||||
export const enumLogicFunctionExecutionStatus = {
|
||||
IDLE: 'IDLE' as const,
|
||||
SUCCESS: 'SUCCESS' as const,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+5
@@ -24,6 +24,7 @@ import { FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metada
|
||||
import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util';
|
||||
import { FlatNavigationMenuItemMaps } from 'src/engine/metadata-modules/flat-navigation-menu-item/types/flat-navigation-menu-item-maps.type';
|
||||
import { FlatNavigationMenuItem } from 'src/engine/metadata-modules/flat-navigation-menu-item/types/flat-navigation-menu-item.type';
|
||||
import { NavigationMenuItemType } from 'src/engine/metadata-modules/navigation-menu-item/enums/navigation-menu-item-type.enum';
|
||||
import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { FlatView } from 'src/engine/metadata-modules/flat-view/types/flat-view.type';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
@@ -273,6 +274,7 @@ export class MigrateFavoritesToNavigationMenuItemsCommand extends ActiveOrSuspen
|
||||
|
||||
const folderToCreate = {
|
||||
id: navigationMenuItemId,
|
||||
type: NavigationMenuItemType.FOLDER,
|
||||
universalIdentifier: navigationMenuItemId,
|
||||
userWorkspaceId,
|
||||
targetRecordId: null,
|
||||
@@ -308,6 +310,7 @@ export class MigrateFavoritesToNavigationMenuItemsCommand extends ActiveOrSuspen
|
||||
|
||||
const workspaceLevelFolderToCreate = {
|
||||
id: workspaceLevelNavigationMenuItemId,
|
||||
type: NavigationMenuItemType.FOLDER,
|
||||
universalIdentifier: workspaceLevelNavigationMenuItemId,
|
||||
userWorkspaceId: null,
|
||||
targetRecordId: null,
|
||||
@@ -516,6 +519,7 @@ export class MigrateFavoritesToNavigationMenuItemsCommand extends ActiveOrSuspen
|
||||
|
||||
flatNavigationMenuItemsToCreate.push({
|
||||
id: favorite.id,
|
||||
type: NavigationMenuItemType.VIEW,
|
||||
universalIdentifier,
|
||||
userWorkspaceId,
|
||||
targetRecordId: null,
|
||||
@@ -569,6 +573,7 @@ export class MigrateFavoritesToNavigationMenuItemsCommand extends ActiveOrSuspen
|
||||
|
||||
flatNavigationMenuItemsToCreate.push({
|
||||
id: favorite.id,
|
||||
type: NavigationMenuItemType.RECORD,
|
||||
universalIdentifier: favorite.id,
|
||||
userWorkspaceId,
|
||||
targetRecordId,
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddColorToObjectMetadata1773655278357
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'AddColorToObjectMetadata1773655278357';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."objectMetadata" ADD "color" text`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."objectMetadata" DROP COLUMN "color"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddTypeToNavigationMenuItem1773681736596
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'AddTypeToNavigationMenuItem1773681736596';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "core"."navigationMenuItem_type_enum" AS ENUM('VIEW', 'FOLDER', 'LINK', 'OBJECT', 'RECORD')`,
|
||||
);
|
||||
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."navigationMenuItem" ADD "type" "core"."navigationMenuItem_type_enum" NOT NULL DEFAULT 'VIEW'`,
|
||||
);
|
||||
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."navigationMenuItem" DROP CONSTRAINT "CHK_navigation_menu_item_target_fields"`,
|
||||
);
|
||||
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."navigationMenuItem" ADD CONSTRAINT "CHK_navigation_menu_item_type_fields" CHECK (
|
||||
("type" = 'FOLDER')
|
||||
OR ("type" = 'OBJECT' AND "targetObjectMetadataId" IS NOT NULL)
|
||||
OR ("type" = 'VIEW')
|
||||
OR ("type" = 'RECORD' AND "targetRecordId" IS NOT NULL AND "targetObjectMetadataId" IS NOT NULL)
|
||||
OR ("type" = 'LINK' AND "link" IS NOT NULL)
|
||||
)`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."navigationMenuItem" DROP CONSTRAINT "CHK_navigation_menu_item_type_fields"`,
|
||||
);
|
||||
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."navigationMenuItem" ADD CONSTRAINT "CHK_navigation_menu_item_target_fields" CHECK (("targetRecordId" IS NULL AND "targetObjectMetadataId" IS NULL) OR ("targetRecordId" IS NOT NULL AND "targetObjectMetadataId" IS NOT NULL))`,
|
||||
);
|
||||
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."navigationMenuItem" DROP COLUMN "type"`,
|
||||
);
|
||||
|
||||
await queryRunner.query(`DROP TYPE "core"."navigationMenuItem_type_enum"`);
|
||||
}
|
||||
}
|
||||
+1
@@ -135,6 +135,7 @@ export const mockPersonFlatObjectMetadata = (
|
||||
): FlatObjectMetadata => ({
|
||||
id: objectMetadataId,
|
||||
icon: 'Icon123',
|
||||
color: null,
|
||||
nameSingular: 'person',
|
||||
namePlural: 'people',
|
||||
labelSingular: 'Person',
|
||||
|
||||
+3
@@ -1,4 +1,5 @@
|
||||
import { fromNavigationMenuItemManifestToUniversalFlatNavigationMenuItem } from 'src/engine/core-modules/application/application-manifest/converters/from-navigation-menu-item-manifest-to-universal-flat-navigation-menu-item.util';
|
||||
import { NavigationMenuItemType } from 'src/engine/metadata-modules/navigation-menu-item/enums/navigation-menu-item-type.enum';
|
||||
|
||||
describe('fromNavigationMenuItemManifestToUniversalFlatNavigationMenuItem', () => {
|
||||
const now = '2026-01-01T00:00:00.000Z';
|
||||
@@ -9,6 +10,7 @@ describe('fromNavigationMenuItemManifestToUniversalFlatNavigationMenuItem', () =
|
||||
fromNavigationMenuItemManifestToUniversalFlatNavigationMenuItem({
|
||||
navigationMenuItemManifest: {
|
||||
universalIdentifier: 'nav-uuid-1',
|
||||
type: NavigationMenuItemType.VIEW,
|
||||
position: 0,
|
||||
},
|
||||
applicationUniversalIdentifier,
|
||||
@@ -34,6 +36,7 @@ describe('fromNavigationMenuItemManifestToUniversalFlatNavigationMenuItem', () =
|
||||
fromNavigationMenuItemManifestToUniversalFlatNavigationMenuItem({
|
||||
navigationMenuItemManifest: {
|
||||
universalIdentifier: 'nav-uuid-2',
|
||||
type: NavigationMenuItemType.VIEW,
|
||||
name: 'Recipes Board',
|
||||
position: 1,
|
||||
viewUniversalIdentifier: 'view-uuid-1',
|
||||
|
||||
+1
@@ -15,6 +15,7 @@ export const fromNavigationMenuItemManifestToUniversalFlatNavigationMenuItem =
|
||||
return {
|
||||
universalIdentifier: navigationMenuItemManifest.universalIdentifier,
|
||||
applicationUniversalIdentifier,
|
||||
type: navigationMenuItemManifest.type,
|
||||
name: navigationMenuItemManifest.name ?? null,
|
||||
icon: navigationMenuItemManifest.icon ?? null,
|
||||
color: navigationMenuItemManifest.color ?? null,
|
||||
|
||||
+1
@@ -18,6 +18,7 @@ export const fromObjectManifestToUniversalFlatObjectMetadata = ({
|
||||
namePlural: objectManifest.namePlural,
|
||||
labelSingular: objectManifest.labelSingular,
|
||||
labelPlural: objectManifest.labelPlural,
|
||||
color: null,
|
||||
description: objectManifest.description ?? null,
|
||||
icon: objectManifest.icon ?? null,
|
||||
standardOverrides: null,
|
||||
|
||||
+1
@@ -6,6 +6,7 @@ import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object
|
||||
const mockObjectMetadata: FlatObjectMetadata = {
|
||||
id: '1',
|
||||
icon: 'Icon123',
|
||||
color: null,
|
||||
nameSingular: 'Object',
|
||||
namePlural: 'Objects',
|
||||
labelSingular: 'Object',
|
||||
|
||||
+2
@@ -9,6 +9,7 @@ export const fromObjectMetadataEntityToObjectMetadataDto = (
|
||||
updatedAt,
|
||||
description,
|
||||
icon,
|
||||
color,
|
||||
standardOverrides,
|
||||
shortcut,
|
||||
duplicateCriteria,
|
||||
@@ -22,6 +23,7 @@ export const fromObjectMetadataEntityToObjectMetadataDto = (
|
||||
updatedAt: new Date(updatedAt),
|
||||
description: description ?? undefined,
|
||||
icon: icon ?? undefined,
|
||||
color: color ?? undefined,
|
||||
standardOverrides: standardOverrides ?? undefined,
|
||||
shortcut: shortcut ?? undefined,
|
||||
duplicateCriteria: duplicateCriteria ?? undefined,
|
||||
|
||||
+2
@@ -105,6 +105,7 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should ma
|
||||
},
|
||||
"navigationMenuItem": {
|
||||
"propertiesToCompare": [
|
||||
"type",
|
||||
"position",
|
||||
"folderUniversalIdentifier",
|
||||
"name",
|
||||
@@ -116,6 +117,7 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should ma
|
||||
},
|
||||
"objectMetadata": {
|
||||
"propertiesToCompare": [
|
||||
"color",
|
||||
"description",
|
||||
"icon",
|
||||
"isActive",
|
||||
|
||||
+10
@@ -149,6 +149,11 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
|
||||
toStringify: false,
|
||||
universalProperty: undefined,
|
||||
},
|
||||
color: {
|
||||
toCompare: true,
|
||||
toStringify: false,
|
||||
universalProperty: undefined,
|
||||
},
|
||||
description: {
|
||||
toCompare: true,
|
||||
toStringify: false,
|
||||
@@ -1070,6 +1075,11 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
|
||||
},
|
||||
},
|
||||
navigationMenuItem: {
|
||||
type: {
|
||||
toCompare: true,
|
||||
toStringify: false,
|
||||
universalProperty: undefined,
|
||||
},
|
||||
position: {
|
||||
toCompare: true,
|
||||
toStringify: false,
|
||||
|
||||
+1
@@ -38,6 +38,7 @@ type Assertions = [
|
||||
Equal<
|
||||
keyof FlatEntityUpdate<'objectMetadata'>,
|
||||
| 'icon'
|
||||
| 'color'
|
||||
| 'description'
|
||||
| 'isActive'
|
||||
| 'standardOverrides'
|
||||
|
||||
+1
@@ -69,6 +69,7 @@ export const fromCreateNavigationMenuItemInputToFlatNavigationMenuItemToCreate =
|
||||
|
||||
return {
|
||||
id,
|
||||
type: createNavigationMenuItemInput.type,
|
||||
universalIdentifier: id,
|
||||
userWorkspaceId: createNavigationMenuItemInput.userWorkspaceId ?? null,
|
||||
targetRecordId: createNavigationMenuItemInput.targetRecordId ?? null,
|
||||
|
||||
+1
@@ -5,6 +5,7 @@ export const fromFlatNavigationMenuItemToNavigationMenuItemDto = (
|
||||
flatNavigationMenuItem: FlatNavigationMenuItem,
|
||||
): NavigationMenuItemDTO => ({
|
||||
id: flatNavigationMenuItem.id,
|
||||
type: flatNavigationMenuItem.type,
|
||||
userWorkspaceId: flatNavigationMenuItem.userWorkspaceId ?? undefined,
|
||||
targetRecordId: flatNavigationMenuItem.targetRecordId ?? undefined,
|
||||
targetObjectMetadataId:
|
||||
|
||||
+1
@@ -75,6 +75,7 @@ export const fromNavigationMenuItemEntityToFlatNavigationMenuItem = ({
|
||||
|
||||
return {
|
||||
id: navigationMenuItemEntity.id,
|
||||
type: navigationMenuItemEntity.type,
|
||||
userWorkspaceId: navigationMenuItemEntity.userWorkspaceId,
|
||||
targetRecordId: navigationMenuItemEntity.targetRecordId,
|
||||
targetObjectMetadataId: navigationMenuItemEntity.targetObjectMetadataId,
|
||||
|
||||
+1
@@ -22,6 +22,7 @@ export const getFlatObjectMetadataMock = (
|
||||
fieldIds: [],
|
||||
description: 'default flat object metadata description',
|
||||
icon: 'icon',
|
||||
color: null,
|
||||
id: faker.string.uuid(),
|
||||
imageIdentifierFieldMetadataId,
|
||||
isActive: true,
|
||||
|
||||
+9
-1
@@ -2,6 +2,7 @@ import { type MetadataEntityPropertyName } from 'src/engine/metadata-modules/fla
|
||||
|
||||
export const FLAT_OBJECT_METADATA_EDITABLE_PROPERTIES = {
|
||||
custom: [
|
||||
'color',
|
||||
'description',
|
||||
'icon',
|
||||
'isActive',
|
||||
@@ -12,7 +13,14 @@ export const FLAT_OBJECT_METADATA_EDITABLE_PROPERTIES = {
|
||||
'nameSingular',
|
||||
'labelIdentifierFieldMetadataId',
|
||||
],
|
||||
standard: ['description', 'icon', 'isActive', 'labelPlural', 'labelSingular'],
|
||||
standard: [
|
||||
'color',
|
||||
'description',
|
||||
'icon',
|
||||
'isActive',
|
||||
'labelPlural',
|
||||
'labelSingular',
|
||||
],
|
||||
} as const satisfies Record<
|
||||
'standard' | 'custom',
|
||||
MetadataEntityPropertyName<'objectMetadata'>[]
|
||||
|
||||
+1
@@ -74,6 +74,7 @@ export const fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCre
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
duplicateCriteria: null,
|
||||
color: createObjectInput.color ?? null,
|
||||
description: createObjectInput.description ?? null,
|
||||
icon: createObjectInput.icon ?? null,
|
||||
isActive: true,
|
||||
|
||||
+2
@@ -7,6 +7,7 @@ export const fromFlatObjectMetadataToObjectMetadataDto = (
|
||||
const {
|
||||
createdAt,
|
||||
updatedAt,
|
||||
color,
|
||||
description,
|
||||
icon,
|
||||
standardOverrides,
|
||||
@@ -50,6 +51,7 @@ export const fromFlatObjectMetadataToObjectMetadataDto = (
|
||||
labelIdentifierFieldMetadataId,
|
||||
createdAt: new Date(createdAt),
|
||||
updatedAt: new Date(updatedAt),
|
||||
color: color ?? undefined,
|
||||
description: description ?? undefined,
|
||||
icon: icon ?? undefined,
|
||||
standardOverrides: standardOverrides ?? undefined,
|
||||
|
||||
+3
@@ -24,6 +24,9 @@ export class MinimalObjectMetadataDTO {
|
||||
@Field({ nullable: true })
|
||||
icon?: string;
|
||||
|
||||
@Field({ nullable: true })
|
||||
color?: string;
|
||||
|
||||
@Field()
|
||||
isCustom: boolean;
|
||||
|
||||
|
||||
+12
-1
@@ -1,8 +1,15 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
import { IsNumber, IsOptional, IsString, IsUUID } from 'class-validator';
|
||||
import {
|
||||
IsEnum,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
} from 'class-validator';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { NavigationMenuItemType } from 'src/engine/metadata-modules/navigation-menu-item/enums/navigation-menu-item-type.enum';
|
||||
|
||||
@InputType()
|
||||
export class CreateNavigationMenuItemInput {
|
||||
@@ -26,6 +33,10 @@ export class CreateNavigationMenuItemInput {
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
viewId?: string | null;
|
||||
|
||||
@IsEnum(NavigationMenuItemType)
|
||||
@Field(() => NavigationMenuItemType)
|
||||
type: NavigationMenuItemType;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Field(() => String, { nullable: true })
|
||||
|
||||
+6
@@ -10,6 +10,8 @@ import {
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
import { NavigationMenuItemType } from 'src/engine/metadata-modules/navigation-menu-item/enums/navigation-menu-item-type.enum';
|
||||
|
||||
import { RecordIdentifierDTO } from './record-identifier.dto';
|
||||
|
||||
@ObjectType('NavigationMenuItem')
|
||||
@@ -39,6 +41,10 @@ export class NavigationMenuItemDTO {
|
||||
@Field(() => UUIDScalarType, { nullable: true })
|
||||
viewId?: string | null;
|
||||
|
||||
@IsNotEmpty()
|
||||
@Field(() => NavigationMenuItemType)
|
||||
type: NavigationMenuItemType;
|
||||
|
||||
@IsOptional()
|
||||
@Field(() => String, { nullable: true })
|
||||
name?: string | null;
|
||||
|
||||
+15
-2
@@ -14,6 +14,7 @@ import {
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity';
|
||||
import { NavigationMenuItemType } from 'src/engine/metadata-modules/navigation-menu-item/enums/navigation-menu-item-type.enum';
|
||||
import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
|
||||
|
||||
@Entity({ name: 'navigationMenuItem', schema: 'core' })
|
||||
@@ -35,8 +36,12 @@ import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-enti
|
||||
'workspaceId',
|
||||
])
|
||||
@Check(
|
||||
'CHK_navigation_menu_item_target_fields',
|
||||
'("targetRecordId" IS NULL AND "targetObjectMetadataId" IS NULL) OR ("targetRecordId" IS NOT NULL AND "targetObjectMetadataId" IS NOT NULL)',
|
||||
'CHK_navigation_menu_item_type_fields',
|
||||
`("type" = 'FOLDER')
|
||||
OR ("type" = 'OBJECT' AND "targetObjectMetadataId" IS NOT NULL)
|
||||
OR ("type" = 'VIEW')
|
||||
OR ("type" = 'RECORD' AND "targetRecordId" IS NOT NULL AND "targetObjectMetadataId" IS NOT NULL)
|
||||
OR ("type" = 'LINK' AND "link" IS NOT NULL)`,
|
||||
)
|
||||
export class NavigationMenuItemEntity
|
||||
extends SyncableEntity
|
||||
@@ -78,6 +83,14 @@ export class NavigationMenuItemEntity
|
||||
@JoinColumn({ name: 'targetObjectMetadataId' })
|
||||
targetObjectMetadata: Relation<ObjectMetadataEntity> | null;
|
||||
|
||||
@Column({
|
||||
nullable: false,
|
||||
type: 'enum',
|
||||
enum: NavigationMenuItemType,
|
||||
default: NavigationMenuItemType.VIEW,
|
||||
})
|
||||
type: NavigationMenuItemType;
|
||||
|
||||
@Column({ nullable: true, type: 'text' })
|
||||
name: string | null;
|
||||
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import { registerEnumType } from '@nestjs/graphql';
|
||||
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
|
||||
registerEnumType(NavigationMenuItemType, {
|
||||
name: 'NavigationMenuItemType',
|
||||
});
|
||||
|
||||
export { NavigationMenuItemType };
|
||||
+5
@@ -55,6 +55,11 @@ export class CreateObjectInput {
|
||||
@Field({ nullable: true })
|
||||
shortcut?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
color?: string;
|
||||
|
||||
@HideField()
|
||||
dataSourceId: string;
|
||||
|
||||
|
||||
+3
@@ -59,6 +59,9 @@ export class ObjectMetadataDTO {
|
||||
@Field({ nullable: true })
|
||||
shortcut?: string;
|
||||
|
||||
@Field({ nullable: true })
|
||||
color?: string;
|
||||
|
||||
@FilterableField()
|
||||
isCustom: boolean;
|
||||
|
||||
|
||||
+5
@@ -26,6 +26,11 @@ export class ObjectStandardOverridesDTO {
|
||||
@Field(() => String, { nullable: true })
|
||||
icon?: string | null;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field(() => String, { nullable: true })
|
||||
color?: string | null;
|
||||
|
||||
@IsJSON()
|
||||
@IsOptional()
|
||||
@Field(() => GraphQLJSON, {
|
||||
|
||||
+5
@@ -52,6 +52,11 @@ export class UpdateObjectPayload {
|
||||
@Field({ nullable: true })
|
||||
shortcut?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
color?: string;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@Field({ nullable: true })
|
||||
|
||||
+3
@@ -60,6 +60,9 @@ export class ObjectMetadataEntity
|
||||
@Column({ nullable: true, type: 'varchar' })
|
||||
icon: string | null;
|
||||
|
||||
@Column({ nullable: true, type: 'text' })
|
||||
color: string | null;
|
||||
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
standardOverrides: JsonbProperty<ObjectStandardOverridesDTO> | null;
|
||||
|
||||
|
||||
+10
-7
@@ -24,6 +24,7 @@ import { findFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules
|
||||
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type';
|
||||
import { FlatNavigationMenuItem } from 'src/engine/metadata-modules/flat-navigation-menu-item/types/flat-navigation-menu-item.type';
|
||||
import { NavigationMenuItemType } from 'src/engine/metadata-modules/navigation-menu-item/enums/navigation-menu-item-type.enum';
|
||||
import { FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { fromCreateObjectInputToFlatObjectMetadataAndFlatFieldMetadatasToCreate } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-create-object-input-to-flat-object-metadata-and-flat-field-metadatas-to-create.util';
|
||||
import { fromDeleteObjectInputToFlatFieldMetadatasToDelete } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-delete-object-input-to-flat-field-metadatas-to-delete.util';
|
||||
@@ -469,7 +470,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
|
||||
const flatNavigationMenuItemToCreate =
|
||||
await this.computeFlatNavigationMenuItemToCreate({
|
||||
view: flatDefaultViewToCreate,
|
||||
objectMetadata: flatObjectMetadataToCreate,
|
||||
workspaceId,
|
||||
workspaceCustomApplicationId: workspaceCustomFlatApplication.id,
|
||||
workspaceCustomApplicationUniversalIdentifier:
|
||||
@@ -683,12 +684,12 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
}
|
||||
|
||||
private async computeFlatNavigationMenuItemToCreate({
|
||||
view,
|
||||
objectMetadata,
|
||||
workspaceId,
|
||||
workspaceCustomApplicationId,
|
||||
workspaceCustomApplicationUniversalIdentifier,
|
||||
}: {
|
||||
view: UniversalFlatView & { id: string };
|
||||
objectMetadata: { id: string; universalIdentifier: string };
|
||||
workspaceId: string;
|
||||
workspaceCustomApplicationId: string;
|
||||
workspaceCustomApplicationUniversalIdentifier: string;
|
||||
@@ -714,13 +715,15 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
|
||||
|
||||
return {
|
||||
id: newId,
|
||||
type: NavigationMenuItemType.OBJECT,
|
||||
universalIdentifier: newId,
|
||||
userWorkspaceId: null,
|
||||
targetRecordId: null,
|
||||
targetObjectMetadataId: null,
|
||||
targetObjectMetadataUniversalIdentifier: null,
|
||||
viewId: view.id,
|
||||
viewUniversalIdentifier: view.universalIdentifier,
|
||||
targetObjectMetadataId: objectMetadata.id,
|
||||
targetObjectMetadataUniversalIdentifier:
|
||||
objectMetadata.universalIdentifier,
|
||||
viewId: null,
|
||||
viewUniversalIdentifier: null,
|
||||
folderId: null,
|
||||
folderUniversalIdentifier: null,
|
||||
name: null,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user