Update standard fields (#6532)

In this PR:
- adding Favorites to Tasks and Notes
- fixing inconsistencies between custom object creation and sync of
standard fields of custom objects
- fixing workspaceCacheVersion not used to invalidate existing
datasource
This commit is contained in:
Charles Bochet
2024-08-04 23:22:41 +02:00
committed by GitHub
parent 03204021cb
commit 2b311b5f7b
14 changed files with 112 additions and 40 deletions

View File

@ -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 <FavoritesSkeletonLoader />;
}

View File

@ -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<View>(PrefetchKey.AllViews);
const loading = useIsPrefetchLoading();
if (loading) {
if (loading && isDefined(currentUser)) {
return <ObjectMetadataNavItemsSkeletonLoader />;
}

View File

@ -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 <NavigationDrawerSectionTitleSkeletonLoader />;
}
return <StyledTitle onClick={onClick}>{label}</StyledTitle>;

View File

@ -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<ObjectMetadataEnt
standardId: BASE_OBJECT_STANDARD_FIELD_IDS.updatedAt,
type: FieldMetadataType.DATE_TIME,
name: 'updatedAt',
label: 'Update date',
icon: 'IconCalendar',
description: 'Update date',
label: 'Last update',
icon: 'IconCalendarClock',
description: 'Last time the record was changed',
isNullable: false,
isActive: true,
isCustom: false,
isSystem: true,
isSystem: false,
workspaceId: objectMetadataInput.workspaceId,
defaultValue: 'now',
},
@ -269,7 +270,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.createdBy,
type: FieldMetadataType.ACTOR,
name: 'createdBy',
label: 'Created By',
label: 'Created by',
icon: 'IconCreativeCommonsSa',
description: 'The creator of the record',
isNullable: false,
@ -676,7 +677,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
name: 'noteTargets',
label: 'Notes',
description: `Notes tied to the ${createdObjectMetadata.labelSingular}`,
icon: 'IconCheckbox',
icon: 'IconNotes',
isNullable: true,
},
// TO
@ -746,7 +747,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
{
standardId: createForeignKeyDeterministicUuid({
objectId: createdObjectMetadata.id,
standardId: NOTE_TARGET_STANDARD_FIELD_IDS.custom,
standardId: TASK_TARGET_STANDARD_FIELD_IDS.custom,
}),
objectMetadataId: taskTargetObjectMetadata.id,
workspaceId: workspaceId,
@ -784,7 +785,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
{
standardId: createRelationDeterministicUuid({
objectId: createdObjectMetadata.id,
standardId: NOTE_TARGET_STANDARD_FIELD_IDS.custom,
standardId: TASK_TARGET_STANDARD_FIELD_IDS.custom,
}),
objectMetadataId: taskTargetObjectMetadata.id,
workspaceId: workspaceId,
@ -979,8 +980,9 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
name: 'timelineActivities',
label: 'Timeline Activities',
description: `Timeline Activities tied to the ${createdObjectMetadata.labelSingular}`,
icon: 'IconTimeline',
icon: 'IconIconTimelineEvent',
isNullable: true,
isSystem: true,
},
// TO
{
@ -996,7 +998,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
name: createdObjectMetadata.nameSingular,
label: createdObjectMetadata.labelSingular,
description: `Timeline Activity ${createdObjectMetadata.labelSingular}`,
icon: 'IconBuildingSkyscraper',
icon: 'IconTimeline',
isNullable: true,
},
]);
@ -1100,7 +1102,7 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt
name: createdObjectMetadata.nameSingular,
label: createdObjectMetadata.labelSingular,
description: `Favorite ${createdObjectMetadata.labelSingular}`,
icon: 'IconBuildingSkyscraper',
icon: 'IconHeart',
isNullable: true,
},
]);

View File

@ -2,12 +2,12 @@ import { ObjectType } from 'typeorm';
import { WorkspaceDynamicRelationMetadataArgsFactory } from 'src/engine/twenty-orm/interfaces/workspace-dynamic-relation-metadata-args.interface';
import { TypedReflect } from 'src/utils/typed-reflect';
import {
RelationMetadataType,
RelationOnDeleteAction,
} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage';
import { TypedReflect } from 'src/utils/typed-reflect';
interface WorkspaceBaseDynamicRelationOptions<TClass> {
type: RelationMetadataType;
@ -27,6 +27,7 @@ export function WorkspaceDynamicRelation<TClass extends object>(
target,
propertyKey.toString(),
) ?? false;
const gate = TypedReflect.getMetadata(
'workspace:gate-metadata-args',
target,

View File

@ -26,20 +26,20 @@ export class WorkspaceDatasourceFactory {
public async create(
workspaceId: string,
cacheVersion: string | null,
workspaceSchemaVersion: string | null,
): Promise<WorkspaceDataSource> {
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(

View File

@ -12,7 +12,6 @@ export class CacheManager<T> {
): Promise<T | null> {
const [workspaceId] = cacheKey.split('-');
// If the cacheKey exists, return the cached value
if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey)!;
}

View File

@ -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 = {

View File

@ -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,
},
];
}

View File

@ -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,

View File

@ -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<TaskWorkspaceEntity> | 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<NoteWorkspaceEntity> | 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',

View File

@ -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<TimelineActivityWorkspaceEntity[]>;
@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<FavoriteWorkspaceEntity[]>;
}

View File

@ -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<TimelineActivityWorkspaceEntity[]>;
@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<FavoriteWorkspaceEntity[]>;
}

View File

@ -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',
}),