Skip to content

Commit

Permalink
feat: soft delete (#6576)
Browse files Browse the repository at this point in the history
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 <[email protected]>
Co-authored-by: Lucas Bordeau <[email protected]>
  • Loading branch information
3 people authored Aug 16, 2024
1 parent 20d8475 commit db54469
Show file tree
Hide file tree
Showing 118 changed files with 1,670 additions and 487 deletions.
11 changes: 11 additions & 0 deletions packages/twenty-front/.storybook/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
22 changes: 15 additions & 7 deletions packages/twenty-front/src/generated-metadata/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -610,6 +610,11 @@ export type MutationRenewTokenArgs = {
};


export type MutationRunWorkflowVersionArgs = {
input: RunWorkflowVersionInput;
};


export type MutationSendInviteLinkArgs = {
emails: Array<Scalars['String']['input']>;
};
Expand Down Expand Up @@ -639,11 +644,6 @@ export type MutationTrackArgs = {
};


export type MutationTriggerWorkflowArgs = {
workflowVersionId: Scalars['String']['input'];
};


export type MutationUnsyncRemoteTableArgs = {
input: RemoteTableInput;
};
Expand Down Expand Up @@ -1001,6 +1001,13 @@ export enum RemoteTableStatus {
Synced = 'SYNCED'
}

export type RunWorkflowVersionInput = {
/** Execution result in JSON format */
payload?: InputMaybe<Scalars['JSON']['input']>;
/** Workflow version ID */
workflowVersionId: Scalars['String']['input'];
};

export type SendInviteLink = {
__typename?: 'SendInviteLink';
/** Boolean that confirms query was dispatched */
Expand Down Expand Up @@ -1400,6 +1407,7 @@ export type WorkspaceFeatureFlagsArgs = {
export enum WorkspaceActivationStatus {
Active = 'ACTIVE',
Inactive = 'INACTIVE',
OngoingCreation = 'ONGOING_CREATION',
PendingCreation = 'PENDING_CREATION'
}

Expand Down
24 changes: 16 additions & 8 deletions packages/twenty-front/src/generated/graphql.tsx
Original file line number Diff line number Diff line change
@@ -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> = T | null;
export type InputMaybe<T> = Maybe<T>;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -457,6 +457,11 @@ export type MutationRenewTokenArgs = {
};


export type MutationRunWorkflowVersionArgs = {
input: RunWorkflowVersionInput;
};


export type MutationSendInviteLinkArgs = {
emails: Array<Scalars['String']>;
};
Expand All @@ -476,11 +481,6 @@ export type MutationTrackArgs = {
};


export type MutationTriggerWorkflowArgs = {
workflowVersionId: Scalars['String'];
};


export type MutationUpdateOneObjectArgs = {
input: UpdateOneObjectInput;
};
Expand Down Expand Up @@ -743,6 +743,13 @@ export enum RemoteTableStatus {
Synced = 'SYNCED'
}

export type RunWorkflowVersionInput = {
/** Execution result in JSON format */
payload?: InputMaybe<Scalars['JSON']>;
/** Workflow version ID */
workflowVersionId: Scalars['String'];
};

export type SendInviteLink = {
__typename?: 'SendInviteLink';
/** Boolean that confirms query was dispatched */
Expand Down Expand Up @@ -1087,6 +1094,7 @@ export type WorkspaceFeatureFlagsArgs = {
export enum WorkspaceActivationStatus {
Active = 'ACTIVE',
Inactive = 'INACTIVE',
OngoingCreation = 'ONGOING_CREATION',
PendingCreation = 'PENDING_CREATION'
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -19,6 +19,9 @@ export const EventIconDynamicComponent = ({
if (eventAction === 'updated') {
return <IconEditCircle />;
}
if (eventAction === 'deleted') {
return <IconTrash />;
}

const IconComponent = getIcon(linkedObjectMetadataItem?.icon);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,17 @@ export const EventRowMainObject = ({
/>
);
}
case 'deleted': {
return (
<StyledMainContainer>
<StyledEventRowItemColumn>
{labelIdentifierValue}
</StyledEventRowItemColumn>
<StyledEventRowItemAction>was deleted by</StyledEventRowItemAction>
<StyledEventRowItemColumn>{authorFullName}</StyledEventRowItemColumn>
</StyledMainContainer>
);
}
default:
return null;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 (
<StyledBanner>
<StyledBanner variant={variant}>
<StyledText>{message}</StyledText>
<Button
variant="secondary"
title={buttonTitle}
Icon={buttonIcon}
size="small"
inverted
onClick={buttonOnClick}
/>
{buttonTitle && buttonOnClick && (
<Button
variant="secondary"
title={buttonTitle}
Icon={buttonIcon}
size="small"
inverted
onClick={buttonOnClick}
/>
)}
</StyledBanner>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { InformationBanner } from '@/information-banner/components/InformationBanner';
import { useRestoreManyRecords } from '@/object-record/hooks/useRestoreManyRecords';
import styled from '@emotion/styled';
import { IconRefresh } from 'twenty-ui';

const StyledInformationBannerDeletedRecord = styled.div`
height: 40px;
position: relative;
&:empty {
height: 0;
}
`;

export const InformationBannerDeletedRecord = ({
recordId,
objectNameSingular,
}: {
recordId: string;
objectNameSingular: string;
}) => {
const { restoreManyRecords } = useRestoreManyRecords({
objectNameSingular,
});

return (
<StyledInformationBannerDeletedRecord>
<InformationBanner
variant="danger"
message={`This record has been deleted`}
buttonTitle="Restore"
buttonIcon={IconRefresh}
buttonOnClick={() => restoreManyRecords([recordId])}
/>
</StyledInformationBannerDeletedRecord>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const INFORMATION_BANNER_HEIGHT = '40px';
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { isDefined } from '~/utils/isDefined';
import { sleep } from '~/utils/sleep';
import { capitalize } from '~/utils/string/capitalize';

type useDeleteOneRecordProps = {
type useDeleteManyRecordProps = {
objectNameSingular: string;
refetchFindManyQuery?: boolean;
};
Expand All @@ -25,7 +25,7 @@ type DeleteManyRecordsOptions = {

export const useDeleteManyRecords = ({
objectNameSingular,
}: useDeleteOneRecordProps) => {
}: useDeleteManyRecordProps) => {
const apiConfig = useRecoilValue(apiConfigState);

const mutationPageSize =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import gql from 'graphql-tag';

import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { EMPTY_MUTATION } from '@/object-record/constants/EmptyMutation';
import { getDestroyManyRecordsMutationResponseField } from '@/object-record/utils/getDestroyManyRecordsMutationResponseField';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
import { capitalize } from '~/utils/string/capitalize';

export const useDestroyManyRecordsMutation = ({
objectNameSingular,
}: {
objectNameSingular: string;
}) => {
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
});

if (isUndefinedOrNull(objectMetadataItem)) {
return { destroyManyRecordsMutation: EMPTY_MUTATION };
}

const capitalizedObjectName = capitalize(objectMetadataItem.namePlural);

const mutationResponseField = getDestroyManyRecordsMutationResponseField(
objectMetadataItem.namePlural,
);

const destroyManyRecordsMutation = gql`
mutation DestroyMany${capitalizedObjectName}($filter: ${capitalize(
objectMetadataItem.nameSingular,
)}FilterInput!) {
${mutationResponseField}(filter: $filter) {
id
}
}
`;

return {
destroyManyRecordsMutation,
};
};
Loading

0 comments on commit db54469

Please sign in to comment.