From db54469c8a70d4dd02de0c80242c751bc2696d9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20M?= Date: Fri, 16 Aug 2024 21:20:02 +0200 Subject: [PATCH] feat: soft delete (#6576) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement soft delete on standards and custom objects. This is a temporary solution, when we drop `pg_graphql` we should rely on the `softDelete` functions of TypeORM. --------- Co-authored-by: Félix Malfait Co-authored-by: Lucas Bordeau --- packages/twenty-front/.storybook/main.ts | 11 + .../src/generated-metadata/graphql.ts | 22 +- .../twenty-front/src/generated/graphql.tsx | 24 ++- .../components/EventIconDynamicComponent.tsx | 5 +- .../components/EventRowMainObject.tsx | 11 + .../components/InformationBanner.tsx | 28 +-- .../InformationBannerDeletedRecord.tsx | 37 ++++ .../constants/InformationBannerHeight.ts | 1 + .../hooks/useDeleteManyRecords.ts | 4 +- .../hooks/useDestroyManyRecordMutation.ts | 41 ++++ .../hooks/useDestroyManyRecords.ts | 115 ++++++++++ .../object-record/hooks/useFindOneRecord.ts | 3 + .../hooks/useFindOneRecordQuery.ts | 12 ++ .../hooks/useRestoreManyRecords.ts | 94 ++++++++ .../hooks/useRestoreManyRecordsMutation.ts | 41 ++++ .../object-filter-dropdown/types/Filter.ts | 1 + .../hooks/useHandleToggleTrashColumnFilter.ts | 67 ++++++ .../RecordIndexOptionsDropdownContent.tsx | 15 ++ .../components/RecordShowContainer.tsx | 55 +++-- ...ordForShowPageOperationSignatureFactory.ts | 2 +- .../record-show/hooks/useRecordShowPage.ts | 1 + ...DestroyManyRecordsMutationResponseField.ts | 5 + ...RestoreManyRecordsMutationResponseField.ts | 5 + .../SettingsNavigationDrawerItems.tsx | 3 +- .../components/SettingsPageContainer.tsx | 18 +- .../objects/SettingsObjectCoverImage.tsx | 3 +- ...grationDatabaseConnectionShowContainer.tsx | 3 +- ...tegrationEditDatabaseConnectionContent.tsx | 5 +- .../twenty-front/src/modules/types/AppPath.ts | 2 +- .../ui/input/button/components/Button.tsx | 1 + .../button/components/FloatingButton.tsx | 12 +- .../src/modules/ui/layout/page/PageHeader.tsx | 19 +- .../src/modules/ui/layout/page/PagePanel.tsx | 12 +- .../ui/layout/page/SubMenuTopBarContainer.tsx | 15 +- .../components/ShowPageMoreButton.tsx | 59 ++++- .../bread-crumb/components/Breadcrumb.tsx | 10 +- .../views/components/SortOrFilterChip.tsx | 52 ++++- .../views/components/VariantFilterChip.tsx | 30 +++ .../views/components/ViewBarDetails.tsx | 47 +++- .../src/modules/views/types/ViewFilter.ts | 1 + .../src/pages/settings/Releases.tsx | 4 +- .../src/pages/settings/SettingsProfile.tsx | 10 +- .../src/pages/settings/SettingsWorkspace.tsx | 10 +- .../settings/SettingsWorkspaceMembers.tsx | 9 +- .../settings/accounts/SettingsAccounts.tsx | 7 +- .../accounts/SettingsAccountsCalendars.tsx | 11 +- .../accounts/SettingsAccountsEmails.tsx | 11 +- .../settings/accounts/SettingsNewAccount.tsx | 11 +- .../crm-migration/SettingsCRMMigration.tsx | 11 +- .../settings/data-model/SettingsNewObject.tsx | 43 ++-- .../SettingsObjectDetailPageContent.tsx | 18 +- .../data-model/SettingsObjectEdit.tsx | 36 ++-- .../data-model/SettingsObjectFieldEdit.tsx | 28 +-- .../SettingsObjectNewFieldStep1.tsx | 45 ++-- .../data-model/SettingsObjectOverview.tsx | 17 +- .../settings/data-model/SettingsObjects.tsx | 39 ++-- .../developers/SettingsDevelopers.tsx | 14 +- .../SettingsDevelopersApiKeyDetail.tsx | 25 +-- .../api-keys/SettingsDevelopersApiKeysNew.tsx | 45 ++-- .../SettingsDevelopersWebhookDetail.tsx | 39 ++-- .../SettingsDevelopersWebhooksNew.tsx | 39 ++-- .../SettingsIntegrationDatabase.tsx | 8 +- ...tingsIntegrationEditDatabaseConnection.tsx | 13 +- ...ttingsIntegrationNewDatabaseConnection.tsx | 51 ++--- .../integrations/SettingsIntegrations.tsx | 7 +- .../components/SettingsAppearance.tsx | 10 +- .../ResetServerlessFunctionStatesEffect.tsx | 4 +- .../SettingsServerlessFunctionDetail.tsx | 47 ++-- .../SettingsServerlessFunctions.tsx | 37 ++-- .../SettingsServerlessFunctionsNew.tsx | 53 ++--- .../twenty-front/src/utils/title-utils.ts | 4 - packages/twenty-front/vite.config.ts | 3 - packages/twenty-server/package.json | 3 +- .../migrations/1723038077987-addSoftDelete.ts | 17 ++ .../factories/args-string.factory.ts | 9 + .../factories/fields-string.factory.ts | 11 +- .../factories/find-many-query.factory.ts | 1 + .../factories/find-one-query.factory.ts | 2 + .../factories/relation-field-alias.factory.ts | 9 +- .../interfaces/record.interface.ts | 1 + ...rkspace-query-builder-options.interface.ts | 1 + .../listeners/entity-events-to-db.listener.ts | 9 +- .../utils/with-soft-deleted.util.ts | 29 +++ .../types/workspace-query-hook.type.ts | 8 +- .../workspace-query-runner.service.ts | 201 ++++++++++++++++-- .../destroy-many-resolver.factory.ts | 42 ++++ .../factories/factories.ts | 6 + .../restore-many-resolver.factory.ts | 42 ++++ .../workspace-resolvers-builder.interface.ts | 14 +- .../workspace-resolver.factory.ts | 6 + .../factories/root-type.factory.ts | 22 +- .../utils/__tests__/get-resolver-args.spec.ts | 6 + .../utils/get-resolver-args.util.ts | 14 ++ .../interfaces/object-metadata.interface.ts | 1 + .../object-metadata/object-metadata.entity.ts | 3 + .../twenty-orm/base.workspace-entity.ts | 19 +- .../twenty-orm/custom.workspace-entity.ts | 6 +- ...s => workspace-custom-entity.decorator.ts} | 9 +- .../decorators/workspace-entity.decorator.ts | 2 + .../workspace-is-primary-field.decorator.ts | 2 +- .../factories/entity-schema-column.factory.ts | 6 + .../factories/entity-schema.factory.ts | 1 + ...orkspace-entity-metadata-args.interface.ts | 7 +- ...extended-entity-metadata-args.interface.ts | 5 + .../utils/__tests__/get-resolver-name.spec.ts | 2 + .../engine/utils/get-resolver-name.util.ts | 4 + .../constants/standard-field-ids.ts | 1 + .../factories/standard-field.factory.ts | 8 + .../factories/standard-object.factory.ts | 1 + .../company.workspace-entity.ts | 1 + .../note-target.workspace-entity.ts | 1 + .../standard-objects/note.workspace-entity.ts | 1 + .../opportunity.workspace-entity.ts | 1 + .../person.workspace-entity.ts | 1 + .../task-target.workspace-entity.ts | 1 + .../standard-objects/task.workspace-entity.ts | 1 + .../src/display/banner/components/Banner.tsx | 17 +- .../display/icon/components/TablerIcons.ts | 2 + 118 files changed, 1670 insertions(+), 487 deletions(-) create mode 100644 packages/twenty-front/src/modules/information-banner/components/deleted-record/InformationBannerDeletedRecord.tsx create mode 100644 packages/twenty-front/src/modules/information-banner/constants/InformationBannerHeight.ts create mode 100644 packages/twenty-front/src/modules/object-record/hooks/useDestroyManyRecordMutation.ts create mode 100644 packages/twenty-front/src/modules/object-record/hooks/useDestroyManyRecords.ts create mode 100644 packages/twenty-front/src/modules/object-record/hooks/useRestoreManyRecords.ts create mode 100644 packages/twenty-front/src/modules/object-record/hooks/useRestoreManyRecordsMutation.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleToggleTrashColumnFilter.ts create mode 100644 packages/twenty-front/src/modules/object-record/utils/getDestroyManyRecordsMutationResponseField.ts create mode 100644 packages/twenty-front/src/modules/object-record/utils/getRestoreManyRecordsMutationResponseField.ts create mode 100644 packages/twenty-front/src/modules/views/components/VariantFilterChip.tsx create mode 100644 packages/twenty-server/src/database/typeorm/metadata/migrations/1723038077987-addSoftDelete.ts create mode 100644 packages/twenty-server/src/engine/api/graphql/workspace-query-runner/utils/with-soft-deleted.util.ts create mode 100644 packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/destroy-many-resolver.factory.ts create mode 100644 packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/restore-many-resolver.factory.ts rename packages/twenty-server/src/engine/twenty-orm/decorators/{workspace-custom-object.decorator.ts => workspace-custom-entity.decorator.ts} (63%) diff --git a/packages/twenty-front/.storybook/main.ts b/packages/twenty-front/.storybook/main.ts index 633df49b3dae..3bd4d16b2a57 100644 --- a/packages/twenty-front/.storybook/main.ts +++ b/packages/twenty-front/.storybook/main.ts @@ -45,5 +45,16 @@ const config: StorybookConfig = { name: '@storybook/react-vite', options: {}, }, + viteFinal: async (config) => { + // Merge custom configuration into the default config + const { mergeConfig } = await import('vite'); + + return mergeConfig(config, { + // Add dependencies to pre-optimization + optimizeDeps: { + exclude: ['@tabler/icons-react'], + }, + }); + }, }; export default config; diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index f3c3f0e781f2..f5e61e07afd5 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -344,9 +344,9 @@ export type FieldConnection = { /** Type of the field */ export enum FieldMetadataType { + Actor = 'ACTOR', Address = 'ADDRESS', Boolean = 'BOOLEAN', - Actor = 'ACTOR', Currency = 'CURRENCY', Date = 'DATE', DateTime = 'DATE_TIME', @@ -452,13 +452,13 @@ export type Mutation = { generateTransientToken: TransientToken; impersonate: Verify; renewToken: AuthTokens; + runWorkflowVersion: WorkflowTriggerResult; sendInviteLink: SendInviteLink; signUp: LoginToken; skipSyncEmailOnboardingStep: OnboardingStepSuccess; syncRemoteTable: RemoteTable; syncRemoteTableSchemaChanges: RemoteTable; track: Analytics; - triggerWorkflow: WorkflowTriggerResult; unsyncRemoteTable: RemoteTable; updateBillingSubscription: UpdateBillingEntity; updateOneField: Field; @@ -610,6 +610,11 @@ export type MutationRenewTokenArgs = { }; +export type MutationRunWorkflowVersionArgs = { + input: RunWorkflowVersionInput; +}; + + export type MutationSendInviteLinkArgs = { emails: Array; }; @@ -639,11 +644,6 @@ export type MutationTrackArgs = { }; -export type MutationTriggerWorkflowArgs = { - workflowVersionId: Scalars['String']['input']; -}; - - export type MutationUnsyncRemoteTableArgs = { input: RemoteTableInput; }; @@ -1001,6 +1001,13 @@ export enum RemoteTableStatus { Synced = 'SYNCED' } +export type RunWorkflowVersionInput = { + /** Execution result in JSON format */ + payload?: InputMaybe; + /** Workflow version ID */ + workflowVersionId: Scalars['String']['input']; +}; + export type SendInviteLink = { __typename?: 'SendInviteLink'; /** Boolean that confirms query was dispatched */ @@ -1400,6 +1407,7 @@ export type WorkspaceFeatureFlagsArgs = { export enum WorkspaceActivationStatus { Active = 'ACTIVE', Inactive = 'INACTIVE', + OngoingCreation = 'ONGOING_CREATION', PendingCreation = 'PENDING_CREATION' } diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index ce20dd81f9d7..cb973a45cf52 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -1,5 +1,5 @@ -import * as Apollo from '@apollo/client'; import { gql } from '@apollo/client'; +import * as Apollo from '@apollo/client'; export type Maybe = T | null; export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; @@ -249,9 +249,9 @@ export type FieldConnection = { /** Type of the field */ export enum FieldMetadataType { + Actor = 'ACTOR', Address = 'ADDRESS', Boolean = 'BOOLEAN', - Actor = 'ACTOR', Currency = 'CURRENCY', Date = 'DATE', DateTime = 'DATE_TIME', @@ -344,11 +344,11 @@ export type Mutation = { generateTransientToken: TransientToken; impersonate: Verify; renewToken: AuthTokens; + runWorkflowVersion: WorkflowTriggerResult; sendInviteLink: SendInviteLink; signUp: LoginToken; skipSyncEmailOnboardingStep: OnboardingStepSuccess; track: Analytics; - triggerWorkflow: WorkflowTriggerResult; updateBillingSubscription: UpdateBillingEntity; updateOneObject: Object; updateOneServerlessFunction: ServerlessFunction; @@ -457,6 +457,11 @@ export type MutationRenewTokenArgs = { }; +export type MutationRunWorkflowVersionArgs = { + input: RunWorkflowVersionInput; +}; + + export type MutationSendInviteLinkArgs = { emails: Array; }; @@ -476,11 +481,6 @@ export type MutationTrackArgs = { }; -export type MutationTriggerWorkflowArgs = { - workflowVersionId: Scalars['String']; -}; - - export type MutationUpdateOneObjectArgs = { input: UpdateOneObjectInput; }; @@ -743,6 +743,13 @@ export enum RemoteTableStatus { Synced = 'SYNCED' } +export type RunWorkflowVersionInput = { + /** Execution result in JSON format */ + payload?: InputMaybe; + /** Workflow version ID */ + workflowVersionId: Scalars['String']; +}; + export type SendInviteLink = { __typename?: 'SendInviteLink'; /** Boolean that confirms query was dispatched */ @@ -1087,6 +1094,7 @@ export type WorkspaceFeatureFlagsArgs = { export enum WorkspaceActivationStatus { Active = 'ACTIVE', Inactive = 'INACTIVE', + OngoingCreation = 'ONGOING_CREATION', PendingCreation = 'PENDING_CREATION' } diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/rows/components/EventIconDynamicComponent.tsx b/packages/twenty-front/src/modules/activities/timelineActivities/rows/components/EventIconDynamicComponent.tsx index d1b35d7f2293..ecb7bc90d51f 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/rows/components/EventIconDynamicComponent.tsx +++ b/packages/twenty-front/src/modules/activities/timelineActivities/rows/components/EventIconDynamicComponent.tsx @@ -1,4 +1,4 @@ -import { IconCirclePlus, IconEditCircle, useIcons } from 'twenty-ui'; +import { IconCirclePlus, IconEditCircle, IconTrash, useIcons } from 'twenty-ui'; import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; @@ -19,6 +19,9 @@ export const EventIconDynamicComponent = ({ if (eventAction === 'updated') { return ; } + if (eventAction === 'deleted') { + return ; + } const IconComponent = getIcon(linkedObjectMetadataItem?.icon); diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/rows/main-object/components/EventRowMainObject.tsx b/packages/twenty-front/src/modules/activities/timelineActivities/rows/main-object/components/EventRowMainObject.tsx index f2dcc69055fa..053e9217bb66 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/rows/main-object/components/EventRowMainObject.tsx +++ b/packages/twenty-front/src/modules/activities/timelineActivities/rows/main-object/components/EventRowMainObject.tsx @@ -45,6 +45,17 @@ export const EventRowMainObject = ({ /> ); } + case 'deleted': { + return ( + + + {labelIdentifierValue} + + was deleted by + {authorFullName} + + ); + } default: return null; } diff --git a/packages/twenty-front/src/modules/information-banner/components/InformationBanner.tsx b/packages/twenty-front/src/modules/information-banner/components/InformationBanner.tsx index 2581dd48934c..39d2437bf6f7 100644 --- a/packages/twenty-front/src/modules/information-banner/components/InformationBanner.tsx +++ b/packages/twenty-front/src/modules/information-banner/components/InformationBanner.tsx @@ -1,6 +1,6 @@ import { Button } from '@/ui/input/button/components/Button'; import styled from '@emotion/styled'; -import { Banner, IconComponent } from 'twenty-ui'; +import { Banner, BannerVariant, IconComponent } from 'twenty-ui'; const StyledBanner = styled(Banner)` position: absolute; @@ -14,26 +14,30 @@ const StyledText = styled.div` export const InformationBanner = ({ message, + variant = 'default', buttonTitle, buttonIcon, buttonOnClick, }: { message: string; - buttonTitle: string; + variant?: BannerVariant; + buttonTitle?: string; buttonIcon?: IconComponent; - buttonOnClick: () => void; + buttonOnClick?: () => void; }) => { return ( - + {message} -