diff --git a/packages/twenty-front/src/modules/favorites/components/Favorites.tsx b/packages/twenty-front/src/modules/favorites/components/Favorites.tsx index 1c14dc8b3..36d545c6f 100644 --- a/packages/twenty-front/src/modules/favorites/components/Favorites.tsx +++ b/packages/twenty-front/src/modules/favorites/components/Favorites.tsx @@ -1,6 +1,6 @@ import styled from '@emotion/styled'; import { useRecoilValue } from 'recoil'; -import { Avatar } from 'twenty-ui'; +import { Avatar, isDefined } from 'twenty-ui'; import { FavoritesSkeletonLoader } from '@/favorites/components/FavoritesSkeletonLoader'; import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading'; @@ -11,6 +11,7 @@ import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/compo import { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle'; import { useNavigationSection } from '@/ui/navigation/navigation-drawer/hooks/useNavigationSection'; +import { currentUserState } from '@/auth/states/currentUserState'; import { useFavorites } from '../hooks/useFavorites'; const StyledContainer = styled(NavigationDrawerSection)` @@ -34,6 +35,8 @@ const StyledNavigationDrawerItem = styled(NavigationDrawerItem)` `; export const Favorites = () => { + const currentUser = useRecoilValue(currentUserState); + const { favorites, handleReorderFavorite } = useFavorites(); const loading = useIsPrefetchLoading(); @@ -41,7 +44,7 @@ export const Favorites = () => { useNavigationSection('Favorites'); const isNavigationSectionOpen = useRecoilValue(isNavigationSectionOpenState); - if (loading) { + if (loading && isDefined(currentUser)) { return ; } diff --git a/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataNavItems.tsx b/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataNavItems.tsx index ed14eaf67..f6d954441 100644 --- a/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataNavItems.tsx +++ b/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataNavItems.tsx @@ -2,8 +2,9 @@ import React from 'react'; import { useLocation } from 'react-router-dom'; import { useRecoilValue } from 'recoil'; -import { useIcons } from 'twenty-ui'; +import { isDefined, useIcons } from 'twenty-ui'; +import { currentUserState } from '@/auth/states/currentUserState'; import { ObjectMetadataNavItemsSkeletonLoader } from '@/object-metadata/components/ObjectMetadataNavItemsSkeletonLoader'; import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading'; @@ -18,6 +19,8 @@ import { View } from '@/views/types/View'; import { getObjectMetadataItemViews } from '@/views/utils/getObjectMetadataItemViews'; export const ObjectMetadataNavItems = ({ isRemote }: { isRemote: boolean }) => { + const currentUser = useRecoilValue(currentUserState); + const { toggleNavigationSection, isNavigationSectionOpenState } = useNavigationSection('Objects' + (isRemote ? 'Remote' : 'Workspace')); const isNavigationSectionOpen = useRecoilValue(isNavigationSectionOpenState); @@ -33,7 +36,7 @@ export const ObjectMetadataNavItems = ({ isRemote }: { isRemote: boolean }) => { const { records: views } = usePrefetchedData(PrefetchKey.AllViews); const loading = useIsPrefetchLoading(); - if (loading) { + if (loading && isDefined(currentUser)) { return ; } diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle.tsx index 4f8386e12..d208dccf1 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle.tsx @@ -1,7 +1,10 @@ import styled from '@emotion/styled'; +import { currentUserState } from '@/auth/states/currentUserState'; import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading'; import { NavigationDrawerSectionTitleSkeletonLoader } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitleSkeletonLoader'; +import { useRecoilValue } from 'recoil'; +import { isDefined } from 'twenty-ui'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; type NavigationDrawerSectionTitleProps = { @@ -32,9 +35,10 @@ export const NavigationDrawerSectionTitle = ({ onClick, label, }: NavigationDrawerSectionTitleProps) => { + const currentUser = useRecoilValue(currentUserState); const loading = useIsPrefetchLoading(); - if (loading) { + if (loading && isDefined(currentUser)) { return ; } return {label}; diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts index 7cc2e4ea1..141ed5f18 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts @@ -50,6 +50,7 @@ import { CUSTOM_OBJECT_STANDARD_FIELD_IDS, FAVORITE_STANDARD_FIELD_IDS, NOTE_TARGET_STANDARD_FIELD_IDS, + TASK_TARGET_STANDARD_FIELD_IDS, TIMELINE_ACTIVITY_STANDARD_FIELD_IDS, } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; import { @@ -255,13 +256,13 @@ export class ObjectMetadataService extends TypeOrmQueryService { type: RelationMetadataType; @@ -27,6 +27,7 @@ export function WorkspaceDynamicRelation( target, propertyKey.toString(), ) ?? false; + const gate = TypedReflect.getMetadata( 'workspace:gate-metadata-args', target, diff --git a/packages/twenty-server/src/engine/twenty-orm/factories/workspace-datasource.factory.ts b/packages/twenty-server/src/engine/twenty-orm/factories/workspace-datasource.factory.ts index a3e4fd135..45c4d2a81 100644 --- a/packages/twenty-server/src/engine/twenty-orm/factories/workspace-datasource.factory.ts +++ b/packages/twenty-server/src/engine/twenty-orm/factories/workspace-datasource.factory.ts @@ -26,20 +26,20 @@ export class WorkspaceDatasourceFactory { public async create( workspaceId: string, - cacheVersion: string | null, + workspaceSchemaVersion: string | null, ): Promise { - const desiredCacheVersion = - cacheVersion ?? + const desiredWorkspaceSchemaVersion = + workspaceSchemaVersion ?? (await this.workspaceCacheVersionService.getVersion(workspaceId)); - if (!desiredCacheVersion) { + if (!desiredWorkspaceSchemaVersion) { throw new Error('Cache version not found'); } - const latestCacheVersion = + const latestWorkspaceSchemaVersion = await this.workspaceCacheVersionService.getVersion(workspaceId); - if (latestCacheVersion !== desiredCacheVersion) { + if (latestWorkspaceSchemaVersion !== desiredWorkspaceSchemaVersion) { throw new Error('Cache version mismatch'); } @@ -70,7 +70,7 @@ export class WorkspaceDatasourceFactory { } const workspaceDataSource = await workspaceDataSourceCacheInstance.execute( - `${workspaceId}-${cacheVersion}`, + `${workspaceId}-${latestWorkspaceSchemaVersion}`, async () => { const dataSourceMetadata = await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceId( diff --git a/packages/twenty-server/src/engine/twenty-orm/storage/cache-manager.storage.ts b/packages/twenty-server/src/engine/twenty-orm/storage/cache-manager.storage.ts index 6fe25c595..6dee55b50 100644 --- a/packages/twenty-server/src/engine/twenty-orm/storage/cache-manager.storage.ts +++ b/packages/twenty-server/src/engine/twenty-orm/storage/cache-manager.storage.ts @@ -12,7 +12,6 @@ export class CacheManager { ): Promise { const [workspaceId] = cacheKey.split('-'); - // If the cacheKey exists, return the cached value if (this.cache.has(cacheKey)) { return this.cache.get(cacheKey)!; } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts index 8c33607e9..38e43ffdc 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts @@ -203,6 +203,8 @@ export const FAVORITE_STANDARD_FIELD_IDS = { company: '20202020-cff5-4682-8bf9-069169e08279', opportunity: '20202020-dabc-48e1-8318-2781a2b32aa2', workflow: '20202020-b11b-4dc8-999a-6bd0a947b463', + task: '20202020-1b1b-4b3b-8b1b-7f8d6a1d7d5c', + note: '20202020-1f25-43fe-8b00-af212fdde824', custom: '20202020-855a-4bc8-9861-79deef37011f', }; @@ -272,6 +274,7 @@ export const NOTE_STANDARD_FIELD_IDS = { noteTargets: '20202020-1f25-43fe-8b00-af212fdde823', attachments: '20202020-4986-4c92-bf19-39934b149b16', timelineActivities: '20202020-7030-42f8-929c-1a57b25d6bce', + favorites: '20202020-4d1d-41ac-b13b-621631298d67', }; export const NOTE_TARGET_STANDARD_FIELD_IDS = { @@ -334,6 +337,7 @@ export const TASK_STANDARD_FIELD_IDS = { attachments: '20202020-794d-4783-a8ff-cecdb15be139', assignee: '20202020-065a-4f42-a906-e20422c1753f', timelineActivities: '20202020-c778-4278-99ee-23a2837aee64', + favorites: '20202020-4d1d-41ac-b13b-621631298d65', }; export const TASK_TARGET_STANDARD_FIELD_IDS = { diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-field.factory.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-field.factory.ts index eff6dedbd..44ba0da72 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-field.factory.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-field.factory.ts @@ -1,22 +1,22 @@ import { Injectable } from '@nestjs/common'; -import { WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface'; import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface'; +import { WorkspaceDynamicRelationMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-dynamic-relation-metadata-args.interface'; +import { WorkspaceEntityMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-entity-metadata-args.interface'; +import { WorkspaceFieldMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-field-metadata-args.interface'; +import { WorkspaceRelationMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-relation-metadata-args.interface'; import { PartialComputedFieldMetadata, PartialFieldMetadata, } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-field-metadata.interface'; -import { WorkspaceEntityMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-entity-metadata-args.interface'; -import { WorkspaceFieldMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-field-metadata-args.interface'; -import { WorkspaceDynamicRelationMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-dynamic-relation-metadata-args.interface'; -import { WorkspaceRelationMetadataArgs } from 'src/engine/twenty-orm/interfaces/workspace-relation-metadata-args.interface'; +import { WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface'; -import { isGatedAndNotEnabled } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/is-gate-and-not-enabled.util'; -import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage'; -import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { createDeterministicUuid } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/create-deterministic-uuid.util'; +import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; +import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage'; import { getJoinColumn } from 'src/engine/twenty-orm/utils/get-join-column.util'; +import { createDeterministicUuid } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/create-deterministic-uuid.util'; +import { isGatedAndNotEnabled } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/is-gate-and-not-enabled.util'; @Injectable() export class StandardFieldFactory { @@ -163,9 +163,7 @@ export class StandardFieldFactory { workspaceId: context.workspaceId, isNullable: workspaceFieldMetadataArgs.isNullable, isCustom: workspaceFieldMetadataArgs.isDeprecated ? true : false, - isSystem: - workspaceEntityMetadataArgs?.isSystem || - workspaceFieldMetadataArgs.isSystem, + isSystem: workspaceFieldMetadataArgs.isSystem ?? false, }, ]; } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.service.ts index 4ef6a446e..117ca1e59 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.service.ts @@ -84,7 +84,7 @@ export class WorkspaceSyncMetadataService { workspaceFeatureFlagsMap, ); - // 2 - Sync standard fields on custom objects + // 2 - Sync standard fields on standard and custom objects const workspaceFieldMigrations = await this.workspaceSyncFieldMetadataService.synchronize( context, diff --git a/packages/twenty-server/src/modules/favorite/standard-objects/favorite.workspace-entity.ts b/packages/twenty-server/src/modules/favorite/standard-objects/favorite.workspace-entity.ts index e09681a23..a3cb30729 100644 --- a/packages/twenty-server/src/modules/favorite/standard-objects/favorite.workspace-entity.ts +++ b/packages/twenty-server/src/modules/favorite/standard-objects/favorite.workspace-entity.ts @@ -17,8 +17,10 @@ import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-re import { FAVORITE_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids'; import { CompanyWorkspaceEntity } from 'src/modules/company/standard-objects/company.workspace-entity'; +import { NoteWorkspaceEntity } from 'src/modules/note/standard-objects/note.workspace-entity'; import { OpportunityWorkspaceEntity } from 'src/modules/opportunity/standard-objects/opportunity.workspace-entity'; import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity'; +import { TaskWorkspaceEntity } from 'src/modules/task/standard-objects/task.workspace-entity'; import { WorkflowWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; @@ -124,6 +126,36 @@ export class FavoriteWorkspaceEntity extends BaseWorkspaceEntity { }) workflowId: string; + @WorkspaceRelation({ + standardId: FAVORITE_STANDARD_FIELD_IDS.task, + type: RelationMetadataType.MANY_TO_ONE, + label: 'Task', + description: 'Favorite task', + icon: 'IconCheckbox', + inverseSideTarget: () => TaskWorkspaceEntity, + inverseSideFieldKey: 'favorites', + }) + @WorkspaceIsNullable() + task: Relation | null; + + @WorkspaceJoinColumn('task') + taskId: string; + + @WorkspaceRelation({ + standardId: FAVORITE_STANDARD_FIELD_IDS.note, + type: RelationMetadataType.MANY_TO_ONE, + label: 'Note', + description: 'Favorite note', + icon: 'IconNotes', + inverseSideTarget: () => NoteWorkspaceEntity, + inverseSideFieldKey: 'favorites', + }) + @WorkspaceIsNullable() + note: Relation | null; + + @WorkspaceJoinColumn('note') + noteId: string; + @WorkspaceDynamicRelation({ type: RelationMetadataType.MANY_TO_ONE, argsFactory: (oppositeObjectMetadata) => ({ @@ -132,7 +164,7 @@ export class FavoriteWorkspaceEntity extends BaseWorkspaceEntity { label: oppositeObjectMetadata.labelSingular, description: `Favorite ${oppositeObjectMetadata.labelSingular}`, joinColumn: `${oppositeObjectMetadata.nameSingular}Id`, - icon: 'IconBuildingSkyscraper', + icon: 'IconHeart', }), inverseSideTarget: () => CustomWorkspaceEntity, inverseSideFieldKey: 'favorites', diff --git a/packages/twenty-server/src/modules/note/standard-objects/note.workspace-entity.ts b/packages/twenty-server/src/modules/note/standard-objects/note.workspace-entity.ts index 33e48b1c4..7b5c96118 100644 --- a/packages/twenty-server/src/modules/note/standard-objects/note.workspace-entity.ts +++ b/packages/twenty-server/src/modules/note/standard-objects/note.workspace-entity.ts @@ -18,6 +18,7 @@ import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-re import { NOTE_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids'; import { AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity'; +import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity'; import { NoteTargetWorkspaceEntity } from 'src/modules/note/standard-objects/note-target.workspace-entity'; import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity'; @@ -110,4 +111,16 @@ export class NoteWorkspaceEntity extends BaseWorkspaceEntity { }) @WorkspaceIsNullable() timelineActivities: Relation; + + @WorkspaceRelation({ + standardId: NOTE_STANDARD_FIELD_IDS.favorites, + type: RelationMetadataType.ONE_TO_MANY, + label: 'Favorites', + description: 'Favorites linked to the note', + icon: 'IconHeart', + inverseSideTarget: () => FavoriteWorkspaceEntity, + onDelete: RelationOnDeleteAction.CASCADE, + }) + @WorkspaceIsSystem() + favorites: Relation; } diff --git a/packages/twenty-server/src/modules/task/standard-objects/task.workspace-entity.ts b/packages/twenty-server/src/modules/task/standard-objects/task.workspace-entity.ts index 40a77ddc5..03f0c8800 100644 --- a/packages/twenty-server/src/modules/task/standard-objects/task.workspace-entity.ts +++ b/packages/twenty-server/src/modules/task/standard-objects/task.workspace-entity.ts @@ -19,6 +19,7 @@ import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-re import { TASK_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids'; import { AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity'; +import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity'; import { TaskTargetWorkspaceEntity } from 'src/modules/task/standard-objects/task-target.workspace-entity'; import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; @@ -164,4 +165,16 @@ export class TaskWorkspaceEntity extends BaseWorkspaceEntity { }) @WorkspaceIsNullable() timelineActivities: Relation; + + @WorkspaceRelation({ + standardId: TASK_STANDARD_FIELD_IDS.favorites, + type: RelationMetadataType.ONE_TO_MANY, + label: 'Favorites', + description: 'Favorites linked to the task', + icon: 'IconHeart', + inverseSideTarget: () => FavoriteWorkspaceEntity, + onDelete: RelationOnDeleteAction.CASCADE, + }) + @WorkspaceIsSystem() + favorites: Relation; } diff --git a/packages/twenty-server/src/modules/timeline/standard-objects/timeline-activity.workspace-entity.ts b/packages/twenty-server/src/modules/timeline/standard-objects/timeline-activity.workspace-entity.ts index de4cfc6de..db6698bf8 100644 --- a/packages/twenty-server/src/modules/timeline/standard-objects/timeline-activity.workspace-entity.ts +++ b/packages/twenty-server/src/modules/timeline/standard-objects/timeline-activity.workspace-entity.ts @@ -212,7 +212,7 @@ export class TimelineActivityWorkspaceEntity extends BaseWorkspaceEntity { standardId: TIMELINE_ACTIVITY_STANDARD_FIELD_IDS.custom, name: oppositeObjectMetadata.nameSingular, label: oppositeObjectMetadata.labelSingular, - description: `Event ${oppositeObjectMetadata.labelSingular}`, + description: `Timeline Activity ${oppositeObjectMetadata.labelSingular}`, joinColumn: `${oppositeObjectMetadata.nameSingular}Id`, icon: 'IconTimeline', }),