Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
0e98ab1
feat: add workspace trash retention properties to workspace entity an…
mabdullahabaid Sep 29, 2025
19cc5d7
feat: add workspace trash cleanup cron with SOLID refactoring
mabdullahabaid Sep 30, 2025
a5042b5
feat: add trashRetentionDays update mutation with auto-recompute next…
mabdullahabaid Sep 30, 2025
cf34036
Merge branch 'main' into trash-deletion
mabdullahabaid Sep 30, 2025
2645a37
refactor: remove WorkspaceTrashCleanupResolver
mabdullahabaid Sep 30, 2025
69ce83f
feat: frontend implementation
mabdullahabaid Oct 2, 2025
9647be1
refactor: trash cleanup to use BullMQ rate limiting, remove nextTrash…
mabdullahabaid Oct 2, 2025
06f49a8
fix: add a unit test for trash-cleanup
mabdullahabaid Oct 2, 2025
4e4790e
Merge branch 'main' into trash-deletion
mabdullahabaid Oct 2, 2025
97f1088
fix: generate graphql types
mabdullahabaid Oct 2, 2025
5fff113
fix: remove unrelated change
mabdullahabaid Oct 2, 2025
6d700f3
fix: resolve a couple comments
mabdullahabaid Oct 2, 2025
f47ecb8
Merge branch 'main' into trash-deletion
FelixMalfait Oct 3, 2025
03c2cda
fix: remove the extra SettingsOptionCardContentInput and update Setti…
mabdullahabaid Oct 5, 2025
2eb344a
fix: resolve issues in workspace-trash-cleanup.cron.job.ts file and e…
mabdullahabaid Oct 5, 2025
9b93ee0
fix: ensure workspace-trash-cleanup.job.ts follows the established co…
mabdullahabaid Oct 5, 2025
faf8429
Merge branch 'main' into trash-deletion
mabdullahabaid Oct 8, 2025
3eecdab
feat: revamp the trash deletion logic to respect twenty orm
mabdullahabaid Oct 9, 2025
8095ccd
Merge branch 'main' into trash-deletion
mabdullahabaid Oct 9, 2025
e7e4eb4
chore: move the trash-cleanup one folder up
mabdullahabaid Oct 9, 2025
2df8dbd
fix: regenerate graphql metadata
mabdullahabaid Oct 9, 2025
bfb47e7
Merge branch 'main' into trash-deletion
mabdullahabaid Oct 12, 2025
491ba1c
fix: move batch size and total deletion to constants
mabdullahabaid Oct 13, 2025
b02e4f1
Merge branch 'main' into trash-deletion
mabdullahabaid Oct 13, 2025
8abe940
fix: remove database constraint and resolve imports per the updated path
mabdullahabaid Oct 13, 2025
82d9efc
Merge branch 'main' into trash-deletion
FelixMalfait Oct 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions packages/twenty-front/src/generated-metadata/graphql.ts

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions packages/twenty-front/src/generated/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4077,6 +4077,7 @@ export type UpdateWorkspaceInput = {
isTwoFactorAuthenticationEnforced?: InputMaybe<Scalars['Boolean']>;
logo?: InputMaybe<Scalars['String']>;
subdomain?: InputMaybe<Scalars['String']>;
trashRetentionDays?: InputMaybe<Scalars['Float']>;
};

export type UpsertFieldPermissionsInput = {
Expand Down Expand Up @@ -4362,6 +4363,7 @@ export type Workspace = {
logo?: Maybe<Scalars['String']>;
metadataVersion: Scalars['Float'];
subdomain: Scalars['String'];
trashRetentionDays: Scalars['Float'];
updatedAt: Scalars['DateTime'];
version?: Maybe<Scalars['String']>;
viewFields?: Maybe<Array<CoreViewField>>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ const mockWorkspace = {
customUrl: 'test.com',
},
isTwoFactorAuthenticationEnforced: false,
trashRetentionDays: 14,
};

const createMockOptions = (): Options<any> => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export type CurrentWorkspace = Pick<
| 'workspaceUrls'
| 'metadataVersion'
| 'isTwoFactorAuthenticationEnforced'
| 'trashRetentionDays'
> & {
defaultRole?: Omit<Role, 'workspaceMembers' | 'agents' | 'apiKeys'> | null;
defaultAgent?: { id: string } | null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const Wrapper = getJestMetadataAndApolloMocksAndActionMenuWrapper({
},
],
isTwoFactorAuthenticationEnforced: false,
trashRetentionDays: 14,
});
},
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@ type SettingsCounterProps = {
minValue?: number;
maxValue?: number;
disabled?: boolean;
showButtons?: boolean;
};

const StyledCounterContainer = styled.div`
const StyledCounterContainer = styled.div<{ showButtons: boolean }>`
align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
margin-left: auto;
width: ${({ theme }) => theme.spacing(30)};
width: ${({ theme, showButtons }) =>
showButtons ? theme.spacing(30) : theme.spacing(16)};
`;

const StyledTextInput = styled(SettingsTextInput)`
Expand All @@ -34,11 +36,12 @@ export const SettingsCounter = ({
value,
onChange,
minValue = 0,
maxValue = 100,
maxValue,
disabled = false,
showButtons = true,
}: SettingsCounterProps) => {
const handleIncrementCounter = () => {
if (value < maxValue) {
if (maxValue === undefined || value < maxValue) {
onChange(value + 1);
}
};
Expand All @@ -60,22 +63,24 @@ export const SettingsCounter = ({
return;
}

if (castedNumber > maxValue) {
if (maxValue !== undefined && castedNumber > maxValue) {
onChange(maxValue);
return;
}
onChange(castedNumber);
};

return (
<StyledCounterContainer>
<IconButton
size="small"
Icon={IconMinus}
variant="secondary"
onClick={handleDecrementCounter}
disabled={disabled}
/>
<StyledCounterContainer showButtons={showButtons}>
{showButtons && (
<IconButton
size="small"
Icon={IconMinus}
variant="secondary"
onClick={handleDecrementCounter}
disabled={disabled}
/>
)}
<StyledTextInput
instanceId="settings-counter-input"
name="counter"
Expand All @@ -84,13 +89,15 @@ export const SettingsCounter = ({
onChange={handleTextInputChange}
disabled={disabled}
/>
<IconButton
size="small"
Icon={IconPlus}
variant="secondary"
onClick={handleIncrementCounter}
disabled={disabled}
/>
{showButtons && (
<IconButton
size="small"
Icon={IconPlus}
variant="secondary"
onClick={handleIncrementCounter}
disabled={disabled}
/>
)}
</StyledCounterContainer>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type SettingsOptionCardContentCounterProps = {
onChange: (value: number) => void;
minValue?: number;
maxValue?: number;
showButtons?: boolean;
};

export const SettingsOptionCardContentCounter = ({
Expand All @@ -28,6 +29,7 @@ export const SettingsOptionCardContentCounter = ({
onChange,
minValue,
maxValue,
showButtons = true,
}: SettingsOptionCardContentCounterProps) => {
return (
<StyledSettingsOptionCardContent disabled={disabled}>
Expand All @@ -50,6 +52,7 @@ export const SettingsOptionCardContentCounter = ({
minValue={minValue}
maxValue={maxValue}
disabled={disabled}
showButtons={showButtons}
/>
</StyledSettingsOptionCardContent>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const SettingsOptionCardContentCounterWrapper = (
disabled={args.disabled}
minValue={args.minValue}
maxValue={args.maxValue}
showButtons={args.showButtons}
/>
</StyledContainer>
);
Expand All @@ -50,6 +51,7 @@ export const Default: Story = {
value: 5,
minValue: 1,
maxValue: 10,
showButtons: true,
},
argTypes: {
Icon: { control: false },
Expand All @@ -64,6 +66,7 @@ export const WithoutIcon: Story = {
value: 20,
minValue: 10,
maxValue: 50,
showButtons: true,
},
};

Expand All @@ -76,5 +79,17 @@ export const Disabled: Story = {
disabled: true,
minValue: 1,
maxValue: 10,
showButtons: true,
},
};

export const WithoutButtons: Story = {
args: {
Icon: IconUsers,
title: 'Trash Retention',
description: 'Adjust the number of days before deletion',
value: 14,
minValue: 0,
showButtons: false,
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export const USER_QUERY_FRAGMENT = gql`
id
}
isTwoFactorAuthenticationEnforced
trashRetentionDays
}
availableWorkspaces {
...AvailableWorkspacesFragment
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
import styled from '@emotion/styled';
import { Trans, useLingui } from '@lingui/react/macro';
import { useDebouncedCallback } from 'use-debounce';

import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
import { SettingsOptionCardContentCounter } from '@/settings/components/SettingsOptions/SettingsOptionCardContentCounter';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsSSOIdentitiesProvidersListCard } from '@/settings/security/components/SSO/SettingsSSOIdentitiesProvidersListCard';
import { SettingsSecurityAuthProvidersOptionsList } from '@/settings/security/components/SettingsSecurityAuthProvidersOptionsList';

import { ToggleImpersonate } from '@/settings/workspace/components/ToggleImpersonate';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { useRecoilValue } from 'recoil';
import { ApolloError } from '@apollo/client';
import { useRecoilState, useRecoilValue } from 'recoil';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath } from 'twenty-shared/utils';
import { Tag } from 'twenty-ui/components';
import { H2Title, IconLock } from 'twenty-ui/display';
import { Section } from 'twenty-ui/layout';
import { H2Title, IconLock, IconTrash } from 'twenty-ui/display';
import { Card, Section } from 'twenty-ui/layout';
import { useUpdateWorkspaceMutation } from '~/generated-metadata/graphql';

const StyledContainer = styled.div`
width: 100%;
Expand All @@ -32,8 +37,50 @@ const StyledSection = styled(Section)`

export const SettingsSecurity = () => {
const { t } = useLingui();
const { enqueueErrorSnackBar } = useSnackBar();

const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState);
const [currentWorkspace, setCurrentWorkspace] = useRecoilState(
currentWorkspaceState,
);
const [updateWorkspace] = useUpdateWorkspaceMutation();

const saveWorkspace = useDebouncedCallback(async (value: number) => {
try {
if (!currentWorkspace?.id) {
throw new Error('User is not logged in');
}

await updateWorkspace({
variables: {
input: {
trashRetentionDays: value,
},
},
});
} catch (err) {
enqueueErrorSnackBar({
apolloError: err instanceof ApolloError ? err : undefined,
});
}
}, 500);

const handleTrashRetentionDaysChange = (value: number) => {
if (!currentWorkspace) {
return;
}

if (value === currentWorkspace.trashRetentionDays) {
return;
}

setCurrentWorkspace({
...currentWorkspace,
trashRetentionDays: value,
});

saveWorkspace(value);
};

return (
<SubMenuTopBarContainer
Expand Down Expand Up @@ -82,6 +129,23 @@ export const SettingsSecurity = () => {
<ToggleImpersonate />
</Section>
)}
<Section>
<H2Title
title={t`Other`}
description={t`Other security settings`}
/>
<Card rounded>
<SettingsOptionCardContentCounter
Icon={IconTrash}
title={t`Erasure of soft-deleted records`}
description={t`Permanent deletion. Enter the number of days.`}
value={currentWorkspace?.trashRetentionDays ?? 14}
onChange={handleTrashRetentionDaysChange}
minValue={0}
showButtons={false}
/>
</Card>
</Section>
</StyledMainContent>
</SettingsPageContainer>
</SubMenuTopBarContainer>
Expand Down
2 changes: 2 additions & 0 deletions packages/twenty-front/src/testing/mock-data/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export const mockCurrentWorkspace: Workspace = {
createdAt: '2023-04-26T10:23:42.33625+00:00',
updatedAt: '2023-04-26T10:23:42.33625+00:00',
metadataVersion: 1,
trashRetentionDays: 14,
currentBillingSubscription: {
__typename: 'BillingSubscription',
id: '7efbc3f7-6e5e-4128-957e-8d86808cdf6a',
Expand Down Expand Up @@ -151,6 +152,7 @@ export const mockCurrentWorkspace: Workspace = {
databaseSchema: '',
databaseUrl: '',
isTwoFactorAuthenticationEnforced: false,
__typename: 'Workspace',
};

export const mockedWorkspaceMemberData: WorkspaceMember = {
Expand Down
2 changes: 1 addition & 1 deletion packages/twenty-server/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,4 @@ FRONTEND_URL=http://localhost:3001
# CLOUDFLARE_WEBHOOK_SECRET=
# IS_CONFIG_VARIABLES_IN_DB_ENABLED=false
# ANALYTICS_ENABLED=
# CLICKHOUSE_URL=http://default:clickhousePassword@localhost:8123/twenty
# CLICKHOUSE_URL=http://default:clickhousePassword@localhost:8123/twenty
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { CheckCustomDomainValidRecordsCronCommand } from 'src/engine/core-module
import { CronTriggerCronCommand } from 'src/engine/metadata-modules/cron-trigger/crons/commands/cron-trigger.cron.command';
import { CleanOnboardingWorkspacesCronCommand } from 'src/engine/workspace-manager/workspace-cleaner/commands/clean-onboarding-workspaces.cron.command';
import { CleanSuspendedWorkspacesCronCommand } from 'src/engine/workspace-manager/workspace-cleaner/commands/clean-suspended-workspaces.cron.command';
import { TrashCleanupCronCommand } from 'src/engine/trash-cleanup/commands/trash-cleanup.cron.command';
import { CalendarEventListFetchCronCommand } from 'src/modules/calendar/calendar-event-import-manager/crons/commands/calendar-event-list-fetch.cron.command';
import { CalendarEventsImportCronCommand } from 'src/modules/calendar/calendar-event-import-manager/crons/commands/calendar-import.cron.command';
import { CalendarOngoingStaleCronCommand } from 'src/modules/calendar/calendar-event-import-manager/crons/commands/calendar-ongoing-stale.cron.command';
Expand Down Expand Up @@ -42,6 +43,7 @@ export class CronRegisterAllCommand extends CommandRunner {
private readonly cronTriggerCronCommand: CronTriggerCronCommand,
private readonly cleanSuspendedWorkspacesCronCommand: CleanSuspendedWorkspacesCronCommand,
private readonly cleanOnboardingWorkspacesCronCommand: CleanOnboardingWorkspacesCronCommand,
private readonly trashCleanupCronCommand: TrashCleanupCronCommand,
) {
super();
}
Expand Down Expand Up @@ -110,6 +112,10 @@ export class CronRegisterAllCommand extends CommandRunner {
name: 'CleanOnboardingWorkspaces',
command: this.cleanOnboardingWorkspacesCronCommand,
},
{
name: 'TrashCleanup',
command: this.trashCleanupCronCommand,
},
];

let successCount = 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadat
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
import { DevSeederModule } from 'src/engine/workspace-manager/dev-seeder/dev-seeder.module';
import { WorkspaceCleanerModule } from 'src/engine/workspace-manager/workspace-cleaner/workspace-cleaner.module';
import { TrashCleanupModule } from 'src/engine/trash-cleanup/trash-cleanup.module';
import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module';
import { CalendarEventImportManagerModule } from 'src/modules/calendar/calendar-event-import-manager/calendar-event-import-manager.module';
import { MessagingImportManagerModule } from 'src/modules/messaging/message-import-manager/messaging-import-manager.module';
Expand Down Expand Up @@ -50,6 +51,7 @@ import { AutomatedTriggerModule } from 'src/modules/workflow/workflow-trigger/au
CronTriggerModule,
DatabaseEventTriggerModule,
WorkspaceCleanerModule,
TrashCleanupModule,
PublicDomainModule,
],
providers: [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { type MigrationInterface, type QueryRunner } from 'typeorm';

export class AddWorkspaceTrashRetention1760356369619
implements MigrationInterface
{
name = 'AddWorkspaceTrashRetention1760356369619';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."workspace" ADD "trashRetentionDays" integer NOT NULL DEFAULT '14'`,
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."workspace" DROP COLUMN "trashRetentionDays"`,
);
}
}
Loading
Loading