feat: soft delete (#6576)
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 <felix.malfait@gmail.com> Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
@ -45,5 +45,16 @@ const config: StorybookConfig = {
|
|||||||
name: '@storybook/react-vite',
|
name: '@storybook/react-vite',
|
||||||
options: {},
|
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;
|
export default config;
|
||||||
|
|||||||
@ -344,9 +344,9 @@ export type FieldConnection = {
|
|||||||
|
|
||||||
/** Type of the field */
|
/** Type of the field */
|
||||||
export enum FieldMetadataType {
|
export enum FieldMetadataType {
|
||||||
|
Actor = 'ACTOR',
|
||||||
Address = 'ADDRESS',
|
Address = 'ADDRESS',
|
||||||
Boolean = 'BOOLEAN',
|
Boolean = 'BOOLEAN',
|
||||||
Actor = 'ACTOR',
|
|
||||||
Currency = 'CURRENCY',
|
Currency = 'CURRENCY',
|
||||||
Date = 'DATE',
|
Date = 'DATE',
|
||||||
DateTime = 'DATE_TIME',
|
DateTime = 'DATE_TIME',
|
||||||
@ -452,13 +452,13 @@ export type Mutation = {
|
|||||||
generateTransientToken: TransientToken;
|
generateTransientToken: TransientToken;
|
||||||
impersonate: Verify;
|
impersonate: Verify;
|
||||||
renewToken: AuthTokens;
|
renewToken: AuthTokens;
|
||||||
|
runWorkflowVersion: WorkflowTriggerResult;
|
||||||
sendInviteLink: SendInviteLink;
|
sendInviteLink: SendInviteLink;
|
||||||
signUp: LoginToken;
|
signUp: LoginToken;
|
||||||
skipSyncEmailOnboardingStep: OnboardingStepSuccess;
|
skipSyncEmailOnboardingStep: OnboardingStepSuccess;
|
||||||
syncRemoteTable: RemoteTable;
|
syncRemoteTable: RemoteTable;
|
||||||
syncRemoteTableSchemaChanges: RemoteTable;
|
syncRemoteTableSchemaChanges: RemoteTable;
|
||||||
track: Analytics;
|
track: Analytics;
|
||||||
triggerWorkflow: WorkflowTriggerResult;
|
|
||||||
unsyncRemoteTable: RemoteTable;
|
unsyncRemoteTable: RemoteTable;
|
||||||
updateBillingSubscription: UpdateBillingEntity;
|
updateBillingSubscription: UpdateBillingEntity;
|
||||||
updateOneField: Field;
|
updateOneField: Field;
|
||||||
@ -610,6 +610,11 @@ export type MutationRenewTokenArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationRunWorkflowVersionArgs = {
|
||||||
|
input: RunWorkflowVersionInput;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationSendInviteLinkArgs = {
|
export type MutationSendInviteLinkArgs = {
|
||||||
emails: Array<Scalars['String']['input']>;
|
emails: Array<Scalars['String']['input']>;
|
||||||
};
|
};
|
||||||
@ -639,11 +644,6 @@ export type MutationTrackArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationTriggerWorkflowArgs = {
|
|
||||||
workflowVersionId: Scalars['String']['input'];
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export type MutationUnsyncRemoteTableArgs = {
|
export type MutationUnsyncRemoteTableArgs = {
|
||||||
input: RemoteTableInput;
|
input: RemoteTableInput;
|
||||||
};
|
};
|
||||||
@ -1001,6 +1001,13 @@ export enum RemoteTableStatus {
|
|||||||
Synced = 'SYNCED'
|
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 = {
|
export type SendInviteLink = {
|
||||||
__typename?: 'SendInviteLink';
|
__typename?: 'SendInviteLink';
|
||||||
/** Boolean that confirms query was dispatched */
|
/** Boolean that confirms query was dispatched */
|
||||||
@ -1400,6 +1407,7 @@ export type WorkspaceFeatureFlagsArgs = {
|
|||||||
export enum WorkspaceActivationStatus {
|
export enum WorkspaceActivationStatus {
|
||||||
Active = 'ACTIVE',
|
Active = 'ACTIVE',
|
||||||
Inactive = 'INACTIVE',
|
Inactive = 'INACTIVE',
|
||||||
|
OngoingCreation = 'ONGOING_CREATION',
|
||||||
PendingCreation = 'PENDING_CREATION'
|
PendingCreation = 'PENDING_CREATION'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import * as Apollo from '@apollo/client';
|
|
||||||
import { gql } from '@apollo/client';
|
import { gql } from '@apollo/client';
|
||||||
|
import * as Apollo from '@apollo/client';
|
||||||
export type Maybe<T> = T | null;
|
export type Maybe<T> = T | null;
|
||||||
export type InputMaybe<T> = Maybe<T>;
|
export type InputMaybe<T> = Maybe<T>;
|
||||||
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
|
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
|
||||||
@ -249,9 +249,9 @@ export type FieldConnection = {
|
|||||||
|
|
||||||
/** Type of the field */
|
/** Type of the field */
|
||||||
export enum FieldMetadataType {
|
export enum FieldMetadataType {
|
||||||
|
Actor = 'ACTOR',
|
||||||
Address = 'ADDRESS',
|
Address = 'ADDRESS',
|
||||||
Boolean = 'BOOLEAN',
|
Boolean = 'BOOLEAN',
|
||||||
Actor = 'ACTOR',
|
|
||||||
Currency = 'CURRENCY',
|
Currency = 'CURRENCY',
|
||||||
Date = 'DATE',
|
Date = 'DATE',
|
||||||
DateTime = 'DATE_TIME',
|
DateTime = 'DATE_TIME',
|
||||||
@ -344,11 +344,11 @@ export type Mutation = {
|
|||||||
generateTransientToken: TransientToken;
|
generateTransientToken: TransientToken;
|
||||||
impersonate: Verify;
|
impersonate: Verify;
|
||||||
renewToken: AuthTokens;
|
renewToken: AuthTokens;
|
||||||
|
runWorkflowVersion: WorkflowTriggerResult;
|
||||||
sendInviteLink: SendInviteLink;
|
sendInviteLink: SendInviteLink;
|
||||||
signUp: LoginToken;
|
signUp: LoginToken;
|
||||||
skipSyncEmailOnboardingStep: OnboardingStepSuccess;
|
skipSyncEmailOnboardingStep: OnboardingStepSuccess;
|
||||||
track: Analytics;
|
track: Analytics;
|
||||||
triggerWorkflow: WorkflowTriggerResult;
|
|
||||||
updateBillingSubscription: UpdateBillingEntity;
|
updateBillingSubscription: UpdateBillingEntity;
|
||||||
updateOneObject: Object;
|
updateOneObject: Object;
|
||||||
updateOneServerlessFunction: ServerlessFunction;
|
updateOneServerlessFunction: ServerlessFunction;
|
||||||
@ -457,6 +457,11 @@ export type MutationRenewTokenArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationRunWorkflowVersionArgs = {
|
||||||
|
input: RunWorkflowVersionInput;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationSendInviteLinkArgs = {
|
export type MutationSendInviteLinkArgs = {
|
||||||
emails: Array<Scalars['String']>;
|
emails: Array<Scalars['String']>;
|
||||||
};
|
};
|
||||||
@ -476,11 +481,6 @@ export type MutationTrackArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationTriggerWorkflowArgs = {
|
|
||||||
workflowVersionId: Scalars['String'];
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export type MutationUpdateOneObjectArgs = {
|
export type MutationUpdateOneObjectArgs = {
|
||||||
input: UpdateOneObjectInput;
|
input: UpdateOneObjectInput;
|
||||||
};
|
};
|
||||||
@ -743,6 +743,13 @@ export enum RemoteTableStatus {
|
|||||||
Synced = 'SYNCED'
|
Synced = 'SYNCED'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type RunWorkflowVersionInput = {
|
||||||
|
/** Execution result in JSON format */
|
||||||
|
payload?: InputMaybe<Scalars['JSON']>;
|
||||||
|
/** Workflow version ID */
|
||||||
|
workflowVersionId: Scalars['String'];
|
||||||
|
};
|
||||||
|
|
||||||
export type SendInviteLink = {
|
export type SendInviteLink = {
|
||||||
__typename?: 'SendInviteLink';
|
__typename?: 'SendInviteLink';
|
||||||
/** Boolean that confirms query was dispatched */
|
/** Boolean that confirms query was dispatched */
|
||||||
@ -1087,6 +1094,7 @@ export type WorkspaceFeatureFlagsArgs = {
|
|||||||
export enum WorkspaceActivationStatus {
|
export enum WorkspaceActivationStatus {
|
||||||
Active = 'ACTIVE',
|
Active = 'ACTIVE',
|
||||||
Inactive = 'INACTIVE',
|
Inactive = 'INACTIVE',
|
||||||
|
OngoingCreation = 'ONGOING_CREATION',
|
||||||
PendingCreation = 'PENDING_CREATION'
|
PendingCreation = 'PENDING_CREATION'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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 { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity';
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
@ -19,6 +19,9 @@ export const EventIconDynamicComponent = ({
|
|||||||
if (eventAction === 'updated') {
|
if (eventAction === 'updated') {
|
||||||
return <IconEditCircle />;
|
return <IconEditCircle />;
|
||||||
}
|
}
|
||||||
|
if (eventAction === 'deleted') {
|
||||||
|
return <IconTrash />;
|
||||||
|
}
|
||||||
|
|
||||||
const IconComponent = getIcon(linkedObjectMetadataItem?.icon);
|
const IconComponent = getIcon(linkedObjectMetadataItem?.icon);
|
||||||
|
|
||||||
|
|||||||
@ -45,6 +45,17 @@ export const EventRowMainObject = ({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
case 'deleted': {
|
||||||
|
return (
|
||||||
|
<StyledMainContainer>
|
||||||
|
<StyledEventRowItemColumn>
|
||||||
|
{labelIdentifierValue}
|
||||||
|
</StyledEventRowItemColumn>
|
||||||
|
<StyledEventRowItemAction>was deleted by</StyledEventRowItemAction>
|
||||||
|
<StyledEventRowItemColumn>{authorFullName}</StyledEventRowItemColumn>
|
||||||
|
</StyledMainContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { Button } from '@/ui/input/button/components/Button';
|
import { Button } from '@/ui/input/button/components/Button';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { Banner, IconComponent } from 'twenty-ui';
|
import { Banner, BannerVariant, IconComponent } from 'twenty-ui';
|
||||||
|
|
||||||
const StyledBanner = styled(Banner)`
|
const StyledBanner = styled(Banner)`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -14,26 +14,30 @@ const StyledText = styled.div`
|
|||||||
|
|
||||||
export const InformationBanner = ({
|
export const InformationBanner = ({
|
||||||
message,
|
message,
|
||||||
|
variant = 'default',
|
||||||
buttonTitle,
|
buttonTitle,
|
||||||
buttonIcon,
|
buttonIcon,
|
||||||
buttonOnClick,
|
buttonOnClick,
|
||||||
}: {
|
}: {
|
||||||
message: string;
|
message: string;
|
||||||
buttonTitle: string;
|
variant?: BannerVariant;
|
||||||
|
buttonTitle?: string;
|
||||||
buttonIcon?: IconComponent;
|
buttonIcon?: IconComponent;
|
||||||
buttonOnClick: () => void;
|
buttonOnClick?: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<StyledBanner>
|
<StyledBanner variant={variant}>
|
||||||
<StyledText>{message}</StyledText>
|
<StyledText>{message}</StyledText>
|
||||||
<Button
|
{buttonTitle && buttonOnClick && (
|
||||||
variant="secondary"
|
<Button
|
||||||
title={buttonTitle}
|
variant="secondary"
|
||||||
Icon={buttonIcon}
|
title={buttonTitle}
|
||||||
size="small"
|
Icon={buttonIcon}
|
||||||
inverted
|
size="small"
|
||||||
onClick={buttonOnClick}
|
inverted
|
||||||
/>
|
onClick={buttonOnClick}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</StyledBanner>
|
</StyledBanner>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export const INFORMATION_BANNER_HEIGHT = '40px';
|
||||||
@ -13,7 +13,7 @@ import { isDefined } from '~/utils/isDefined';
|
|||||||
import { sleep } from '~/utils/sleep';
|
import { sleep } from '~/utils/sleep';
|
||||||
import { capitalize } from '~/utils/string/capitalize';
|
import { capitalize } from '~/utils/string/capitalize';
|
||||||
|
|
||||||
type useDeleteOneRecordProps = {
|
type useDeleteManyRecordProps = {
|
||||||
objectNameSingular: string;
|
objectNameSingular: string;
|
||||||
refetchFindManyQuery?: boolean;
|
refetchFindManyQuery?: boolean;
|
||||||
};
|
};
|
||||||
@ -25,7 +25,7 @@ type DeleteManyRecordsOptions = {
|
|||||||
|
|
||||||
export const useDeleteManyRecords = ({
|
export const useDeleteManyRecords = ({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
}: useDeleteOneRecordProps) => {
|
}: useDeleteManyRecordProps) => {
|
||||||
const apiConfig = useRecoilValue(apiConfigState);
|
const apiConfig = useRecoilValue(apiConfigState);
|
||||||
|
|
||||||
const mutationPageSize =
|
const mutationPageSize =
|
||||||
|
|||||||
@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -0,0 +1,115 @@
|
|||||||
|
import { useApolloClient } from '@apollo/client';
|
||||||
|
|
||||||
|
import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect';
|
||||||
|
import { apiConfigState } from '@/client-config/states/apiConfigState';
|
||||||
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
|
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||||
|
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
|
||||||
|
import { DEFAULT_MUTATION_BATCH_SIZE } from '@/object-record/constants/DefaultMutationBatchSize';
|
||||||
|
import { useDestroyManyRecordsMutation } from '@/object-record/hooks/useDestroyManyRecordMutation';
|
||||||
|
import { getDestroyManyRecordsMutationResponseField } from '@/object-record/utils/getDestroyManyRecordsMutationResponseField';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
import { sleep } from '~/utils/sleep';
|
||||||
|
import { capitalize } from '~/utils/string/capitalize';
|
||||||
|
|
||||||
|
type useDestroyManyRecordProps = {
|
||||||
|
objectNameSingular: string;
|
||||||
|
refetchFindManyQuery?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type DestroyManyRecordsOptions = {
|
||||||
|
skipOptimisticEffect?: boolean;
|
||||||
|
delayInMsBetweenRequests?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDestroyManyRecords = ({
|
||||||
|
objectNameSingular,
|
||||||
|
}: useDestroyManyRecordProps) => {
|
||||||
|
const apiConfig = useRecoilValue(apiConfigState);
|
||||||
|
|
||||||
|
const mutationPageSize =
|
||||||
|
apiConfig?.mutationMaximumAffectedRecords ?? DEFAULT_MUTATION_BATCH_SIZE;
|
||||||
|
|
||||||
|
const apolloClient = useApolloClient();
|
||||||
|
|
||||||
|
const { objectMetadataItem } = useObjectMetadataItem({
|
||||||
|
objectNameSingular,
|
||||||
|
});
|
||||||
|
|
||||||
|
const getRecordFromCache = useGetRecordFromCache({
|
||||||
|
objectNameSingular,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { destroyManyRecordsMutation } = useDestroyManyRecordsMutation({
|
||||||
|
objectNameSingular,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { objectMetadataItems } = useObjectMetadataItems();
|
||||||
|
|
||||||
|
const mutationResponseField = getDestroyManyRecordsMutationResponseField(
|
||||||
|
objectMetadataItem.namePlural,
|
||||||
|
);
|
||||||
|
|
||||||
|
const destroyManyRecords = async (
|
||||||
|
idsToDestroy: string[],
|
||||||
|
options?: DestroyManyRecordsOptions,
|
||||||
|
) => {
|
||||||
|
const numberOfBatches = Math.ceil(idsToDestroy.length / mutationPageSize);
|
||||||
|
|
||||||
|
const destroyedRecords = [];
|
||||||
|
|
||||||
|
for (let batchIndex = 0; batchIndex < numberOfBatches; batchIndex++) {
|
||||||
|
const batchIds = idsToDestroy.slice(
|
||||||
|
batchIndex * mutationPageSize,
|
||||||
|
(batchIndex + 1) * mutationPageSize,
|
||||||
|
);
|
||||||
|
|
||||||
|
const destroyedRecordsResponse = await apolloClient.mutate({
|
||||||
|
mutation: destroyManyRecordsMutation,
|
||||||
|
variables: {
|
||||||
|
filter: { id: { in: batchIds } },
|
||||||
|
},
|
||||||
|
optimisticResponse: options?.skipOptimisticEffect
|
||||||
|
? undefined
|
||||||
|
: {
|
||||||
|
[mutationResponseField]: batchIds.map((idToDestroy) => ({
|
||||||
|
__typename: capitalize(objectNameSingular),
|
||||||
|
id: idToDestroy,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
update: options?.skipOptimisticEffect
|
||||||
|
? undefined
|
||||||
|
: (cache, { data }) => {
|
||||||
|
const records = data?.[mutationResponseField];
|
||||||
|
|
||||||
|
if (!records?.length) return;
|
||||||
|
|
||||||
|
const cachedRecords = records
|
||||||
|
.map((record) => getRecordFromCache(record.id, cache))
|
||||||
|
.filter(isDefined);
|
||||||
|
|
||||||
|
triggerDeleteRecordsOptimisticEffect({
|
||||||
|
cache,
|
||||||
|
objectMetadataItem,
|
||||||
|
recordsToDelete: cachedRecords,
|
||||||
|
objectMetadataItems,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const destroyedRecordsForThisBatch =
|
||||||
|
destroyedRecordsResponse.data?.[mutationResponseField] ?? [];
|
||||||
|
|
||||||
|
destroyedRecords.push(...destroyedRecordsForThisBatch);
|
||||||
|
|
||||||
|
if (isDefined(options?.delayInMsBetweenRequests)) {
|
||||||
|
await sleep(options.delayInMsBetweenRequests);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return destroyedRecords;
|
||||||
|
};
|
||||||
|
|
||||||
|
return { destroyManyRecords };
|
||||||
|
};
|
||||||
@ -17,11 +17,13 @@ export const useFindOneRecord = <T extends ObjectRecord = ObjectRecord>({
|
|||||||
recordGqlFields,
|
recordGqlFields,
|
||||||
onCompleted,
|
onCompleted,
|
||||||
skip,
|
skip,
|
||||||
|
withSoftDeleted = false,
|
||||||
}: ObjectMetadataItemIdentifier & {
|
}: ObjectMetadataItemIdentifier & {
|
||||||
objectRecordId: string | undefined;
|
objectRecordId: string | undefined;
|
||||||
recordGqlFields?: RecordGqlOperationGqlRecordFields;
|
recordGqlFields?: RecordGqlOperationGqlRecordFields;
|
||||||
onCompleted?: (data: T) => void;
|
onCompleted?: (data: T) => void;
|
||||||
skip?: boolean;
|
skip?: boolean;
|
||||||
|
withSoftDeleted?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const { objectMetadataItem } = useObjectMetadataItem({
|
const { objectMetadataItem } = useObjectMetadataItem({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
@ -33,6 +35,7 @@ export const useFindOneRecord = <T extends ObjectRecord = ObjectRecord>({
|
|||||||
const { findOneRecordQuery } = useFindOneRecordQuery({
|
const { findOneRecordQuery } = useFindOneRecordQuery({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
recordGqlFields: computedRecordGqlFields,
|
recordGqlFields: computedRecordGqlFields,
|
||||||
|
withSoftDeleted,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data, loading, error } = useQuery<{
|
const { data, loading, error } = useQuery<{
|
||||||
|
|||||||
@ -10,9 +10,11 @@ import { capitalize } from '~/utils/string/capitalize';
|
|||||||
export const useFindOneRecordQuery = ({
|
export const useFindOneRecordQuery = ({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
recordGqlFields,
|
recordGqlFields,
|
||||||
|
withSoftDeleted = false,
|
||||||
}: {
|
}: {
|
||||||
objectNameSingular: string;
|
objectNameSingular: string;
|
||||||
recordGqlFields?: RecordGqlOperationGqlRecordFields;
|
recordGqlFields?: RecordGqlOperationGqlRecordFields;
|
||||||
|
withSoftDeleted?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const { objectMetadataItem } = useObjectMetadataItem({
|
const { objectMetadataItem } = useObjectMetadataItem({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
@ -25,6 +27,16 @@ export const useFindOneRecordQuery = ({
|
|||||||
objectMetadataItem.nameSingular,
|
objectMetadataItem.nameSingular,
|
||||||
)}($objectRecordId: ID!) {
|
)}($objectRecordId: ID!) {
|
||||||
${objectMetadataItem.nameSingular}(filter: {
|
${objectMetadataItem.nameSingular}(filter: {
|
||||||
|
${
|
||||||
|
withSoftDeleted
|
||||||
|
? `
|
||||||
|
or: [
|
||||||
|
{ deletedAt: { is: NULL } },
|
||||||
|
{ deletedAt: { is: NOT_NULL } }
|
||||||
|
],
|
||||||
|
`
|
||||||
|
: ''
|
||||||
|
}
|
||||||
id: {
|
id: {
|
||||||
eq: $objectRecordId
|
eq: $objectRecordId
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,94 @@
|
|||||||
|
import { useApolloClient } from '@apollo/client';
|
||||||
|
|
||||||
|
import { apiConfigState } from '@/client-config/states/apiConfigState';
|
||||||
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
|
import { DEFAULT_MUTATION_BATCH_SIZE } from '@/object-record/constants/DefaultMutationBatchSize';
|
||||||
|
import { useRestoreManyRecordsMutation } from '@/object-record/hooks/useRestoreManyRecordsMutation';
|
||||||
|
import { getRestoreManyRecordsMutationResponseField } from '@/object-record/utils/getRestoreManyRecordsMutationResponseField';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
import { sleep } from '~/utils/sleep';
|
||||||
|
import { capitalize } from '~/utils/string/capitalize';
|
||||||
|
|
||||||
|
type useRestoreManyRecordProps = {
|
||||||
|
objectNameSingular: string;
|
||||||
|
refetchFindManyQuery?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type RestoreManyRecordsOptions = {
|
||||||
|
skipOptimisticEffect?: boolean;
|
||||||
|
delayInMsBetweenRequests?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useRestoreManyRecords = ({
|
||||||
|
objectNameSingular,
|
||||||
|
}: useRestoreManyRecordProps) => {
|
||||||
|
const apiConfig = useRecoilValue(apiConfigState);
|
||||||
|
|
||||||
|
const mutationPageSize =
|
||||||
|
apiConfig?.mutationMaximumAffectedRecords ?? DEFAULT_MUTATION_BATCH_SIZE;
|
||||||
|
|
||||||
|
const apolloClient = useApolloClient();
|
||||||
|
|
||||||
|
const { objectMetadataItem } = useObjectMetadataItem({
|
||||||
|
objectNameSingular,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { restoreManyRecordsMutation } = useRestoreManyRecordsMutation({
|
||||||
|
objectNameSingular,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mutationResponseField = getRestoreManyRecordsMutationResponseField(
|
||||||
|
objectMetadataItem.namePlural,
|
||||||
|
);
|
||||||
|
|
||||||
|
const restoreManyRecords = async (
|
||||||
|
idsToRestore: string[],
|
||||||
|
options?: RestoreManyRecordsOptions,
|
||||||
|
) => {
|
||||||
|
const numberOfBatches = Math.ceil(idsToRestore.length / mutationPageSize);
|
||||||
|
|
||||||
|
const restoredRecords = [];
|
||||||
|
|
||||||
|
for (let batchIndex = 0; batchIndex < numberOfBatches; batchIndex++) {
|
||||||
|
const batchIds = idsToRestore.slice(
|
||||||
|
batchIndex * mutationPageSize,
|
||||||
|
(batchIndex + 1) * mutationPageSize,
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO: fix optimistic effect
|
||||||
|
const findOneQueryName = `FindOne${capitalize(objectNameSingular)}`;
|
||||||
|
const findManyQueryName = `FindMany${capitalize(objectMetadataItem.namePlural)}`;
|
||||||
|
|
||||||
|
const restoredRecordsResponse = await apolloClient.mutate({
|
||||||
|
mutation: restoreManyRecordsMutation,
|
||||||
|
refetchQueries: [findOneQueryName, findManyQueryName],
|
||||||
|
variables: {
|
||||||
|
filter: { id: { in: batchIds } },
|
||||||
|
},
|
||||||
|
optimisticResponse: options?.skipOptimisticEffect
|
||||||
|
? undefined
|
||||||
|
: {
|
||||||
|
[mutationResponseField]: batchIds.map((idToRestore) => ({
|
||||||
|
__typename: capitalize(objectNameSingular),
|
||||||
|
id: idToRestore,
|
||||||
|
deletedAt: null,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const restoredRecordsForThisBatch =
|
||||||
|
restoredRecordsResponse.data?.[mutationResponseField] ?? [];
|
||||||
|
|
||||||
|
restoredRecords.push(...restoredRecordsForThisBatch);
|
||||||
|
|
||||||
|
if (isDefined(options?.delayInMsBetweenRequests)) {
|
||||||
|
await sleep(options.delayInMsBetweenRequests);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return restoredRecords;
|
||||||
|
};
|
||||||
|
|
||||||
|
return { restoreManyRecords };
|
||||||
|
};
|
||||||
@ -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 { getRestoreManyRecordsMutationResponseField } from '@/object-record/utils/getRestoreManyRecordsMutationResponseField';
|
||||||
|
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||||
|
import { capitalize } from '~/utils/string/capitalize';
|
||||||
|
|
||||||
|
export const useRestoreManyRecordsMutation = ({
|
||||||
|
objectNameSingular,
|
||||||
|
}: {
|
||||||
|
objectNameSingular: string;
|
||||||
|
}) => {
|
||||||
|
const { objectMetadataItem } = useObjectMetadataItem({
|
||||||
|
objectNameSingular,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isUndefinedOrNull(objectMetadataItem)) {
|
||||||
|
return { restoreManyRecordsMutation: EMPTY_MUTATION };
|
||||||
|
}
|
||||||
|
|
||||||
|
const capitalizedObjectName = capitalize(objectMetadataItem.namePlural);
|
||||||
|
|
||||||
|
const mutationResponseField = getRestoreManyRecordsMutationResponseField(
|
||||||
|
objectMetadataItem.namePlural,
|
||||||
|
);
|
||||||
|
|
||||||
|
const restoreManyRecordsMutation = gql`
|
||||||
|
mutation RestoreMany${capitalizedObjectName}($filter: ${capitalize(
|
||||||
|
objectMetadataItem.nameSingular,
|
||||||
|
)}FilterInput!) {
|
||||||
|
${mutationResponseField}(filter: $filter) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
restoreManyRecordsMutation,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -4,6 +4,7 @@ import { FilterDefinition } from './FilterDefinition';
|
|||||||
|
|
||||||
export type Filter = {
|
export type Filter = {
|
||||||
id: string;
|
id: string;
|
||||||
|
variant?: 'default' | 'danger';
|
||||||
fieldMetadataId: string;
|
fieldMetadataId: string;
|
||||||
value: string;
|
value: string;
|
||||||
displayValue: string;
|
displayValue: string;
|
||||||
|
|||||||
@ -0,0 +1,67 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
|
import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata';
|
||||||
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
|
import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions';
|
||||||
|
import { Filter } from '@/object-record/object-filter-dropdown/types/Filter';
|
||||||
|
import { useCombinedViewFilters } from '@/views/hooks/useCombinedViewFilters';
|
||||||
|
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
|
type UseHandleToggleTrashColumnFilterProps = {
|
||||||
|
objectNameSingular: string;
|
||||||
|
viewBarId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useHandleToggleTrashColumnFilter = ({
|
||||||
|
viewBarId,
|
||||||
|
objectNameSingular,
|
||||||
|
}: UseHandleToggleTrashColumnFilterProps) => {
|
||||||
|
const { objectMetadataItem } = useObjectMetadataItem({
|
||||||
|
objectNameSingular,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { columnDefinitions } =
|
||||||
|
useColumnDefinitionsFromFieldMetadata(objectMetadataItem);
|
||||||
|
|
||||||
|
const { upsertCombinedViewFilter } = useCombinedViewFilters(viewBarId);
|
||||||
|
|
||||||
|
const handleToggleTrashColumnFilter = useCallback(() => {
|
||||||
|
const trashFieldMetadata = objectMetadataItem.fields.find(
|
||||||
|
(field) => field.name === 'deletedAt',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isDefined(trashFieldMetadata)) return;
|
||||||
|
|
||||||
|
const correspondingColumnDefinition = columnDefinitions.find(
|
||||||
|
(columnDefinition) =>
|
||||||
|
columnDefinition.fieldMetadataId === trashFieldMetadata.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isDefined(correspondingColumnDefinition)) return;
|
||||||
|
|
||||||
|
const filterType = getFilterTypeFromFieldType(
|
||||||
|
correspondingColumnDefinition?.type,
|
||||||
|
);
|
||||||
|
|
||||||
|
const newFilter: Filter = {
|
||||||
|
id: v4(),
|
||||||
|
variant: 'danger',
|
||||||
|
fieldMetadataId: trashFieldMetadata.id,
|
||||||
|
operand: ViewFilterOperand.IsNotEmpty,
|
||||||
|
displayValue: '',
|
||||||
|
definition: {
|
||||||
|
label: 'Trash',
|
||||||
|
iconName: 'IconTrash',
|
||||||
|
fieldMetadataId: trashFieldMetadata.id,
|
||||||
|
type: filterType,
|
||||||
|
},
|
||||||
|
value: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
upsertCombinedViewFilter(newFilter);
|
||||||
|
}, [columnDefinitions, objectMetadataItem.fields, upsertCombinedViewFilter]);
|
||||||
|
|
||||||
|
return handleToggleTrashColumnFilter;
|
||||||
|
};
|
||||||
@ -8,6 +8,7 @@ import {
|
|||||||
IconFileImport,
|
IconFileImport,
|
||||||
IconSettings,
|
IconSettings,
|
||||||
IconTag,
|
IconTag,
|
||||||
|
IconTrash,
|
||||||
} from 'twenty-ui';
|
} from 'twenty-ui';
|
||||||
|
|
||||||
import { useObjectNamePluralFromSingular } from '@/object-metadata/hooks/useObjectNamePluralFromSingular';
|
import { useObjectNamePluralFromSingular } from '@/object-metadata/hooks/useObjectNamePluralFromSingular';
|
||||||
@ -37,6 +38,7 @@ import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
|
|||||||
import { ViewType } from '@/views/types/ViewType';
|
import { ViewType } from '@/views/types/ViewType';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { useSetRecoilState } from 'recoil';
|
import { useSetRecoilState } from 'recoil';
|
||||||
|
import { useHandleToggleTrashColumnFilter } from '@/object-record/record-index/hooks/useHandleToggleTrashColumnFilter';
|
||||||
|
|
||||||
type RecordIndexOptionsMenu = 'fields' | 'hiddenFields';
|
type RecordIndexOptionsMenu = 'fields' | 'hiddenFields';
|
||||||
|
|
||||||
@ -88,6 +90,11 @@ export const RecordIndexOptionsDropdownContent = ({
|
|||||||
hiddenTableColumns,
|
hiddenTableColumns,
|
||||||
} = useRecordIndexOptionsForTable(recordIndexId);
|
} = useRecordIndexOptionsForTable(recordIndexId);
|
||||||
|
|
||||||
|
const handleToggleTrashColumnFilter = useHandleToggleTrashColumnFilter({
|
||||||
|
objectNameSingular,
|
||||||
|
viewBarId: recordIndexId,
|
||||||
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
visibleBoardFields,
|
visibleBoardFields,
|
||||||
hiddenBoardFields,
|
hiddenBoardFields,
|
||||||
@ -153,6 +160,14 @@ export const RecordIndexOptionsDropdownContent = ({
|
|||||||
LeftIcon={IconFileExport}
|
LeftIcon={IconFileExport}
|
||||||
text={displayedExportProgress(progress)}
|
text={displayedExportProgress(progress)}
|
||||||
/>
|
/>
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
handleToggleTrashColumnFilter();
|
||||||
|
closeDropdown();
|
||||||
|
}}
|
||||||
|
LeftIcon={IconTrash}
|
||||||
|
text="Trash"
|
||||||
|
/>
|
||||||
</DropdownMenuItemsContainer>
|
</DropdownMenuItemsContainer>
|
||||||
)}
|
)}
|
||||||
{currentMenu === 'fields' && (
|
{currentMenu === 'fields' && (
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { useRecoilState, useRecoilValue } from 'recoil';
|
|||||||
import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell';
|
import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell';
|
||||||
import { Note } from '@/activities/types/Note';
|
import { Note } from '@/activities/types/Note';
|
||||||
import { Task } from '@/activities/types/Task';
|
import { Task } from '@/activities/types/Task';
|
||||||
|
import { InformationBannerDeletedRecord } from '@/information-banner/components/deleted-record/InformationBannerDeletedRecord';
|
||||||
import { useLabelIdentifierFieldMetadataItem } from '@/object-metadata/hooks/useLabelIdentifierFieldMetadataItem';
|
import { useLabelIdentifierFieldMetadataItem } from '@/object-metadata/hooks/useLabelIdentifierFieldMetadataItem';
|
||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
@ -126,7 +127,11 @@ export const RecordShowContainer = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const { inlineFieldMetadataItems, relationFieldMetadataItems } = groupBy(
|
const { inlineFieldMetadataItems, relationFieldMetadataItems } = groupBy(
|
||||||
availableFieldMetadataItems,
|
availableFieldMetadataItems.filter(
|
||||||
|
(fieldMetadataItem) =>
|
||||||
|
fieldMetadataItem.name !== 'createdAt' &&
|
||||||
|
fieldMetadataItem.name !== 'deletedAt',
|
||||||
|
),
|
||||||
(fieldMetadataItem) =>
|
(fieldMetadataItem) =>
|
||||||
fieldMetadataItem.type === FieldMetadataType.Relation
|
fieldMetadataItem.type === FieldMetadataType.Relation
|
||||||
? 'relationFieldMetadataItems'
|
? 'relationFieldMetadataItems'
|
||||||
@ -301,25 +306,33 @@ export const RecordShowContainer = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ShowPageContainer>
|
<>
|
||||||
<ShowPageLeftContainer forceMobile={isMobile}>
|
{recordFromStore && recordFromStore.deletedAt && (
|
||||||
{!isMobile && summaryCard}
|
<InformationBannerDeletedRecord
|
||||||
{!isMobile && fieldsBox}
|
recordId={objectRecordId}
|
||||||
</ShowPageLeftContainer>
|
objectNameSingular={objectNameSingular}
|
||||||
<ShowPageRightContainer
|
/>
|
||||||
targetableObject={{
|
)}
|
||||||
id: objectRecordId,
|
<ShowPageContainer>
|
||||||
targetObjectNameSingular: objectMetadataItem?.nameSingular,
|
<ShowPageLeftContainer forceMobile={isMobile}>
|
||||||
}}
|
{!isMobile && summaryCard}
|
||||||
timeline
|
{!isMobile && fieldsBox}
|
||||||
tasks
|
</ShowPageLeftContainer>
|
||||||
notes
|
<ShowPageRightContainer
|
||||||
emails
|
targetableObject={{
|
||||||
isInRightDrawer={isInRightDrawer}
|
id: objectRecordId,
|
||||||
summaryCard={isMobile ? summaryCard : <></>}
|
targetObjectNameSingular: objectMetadataItem?.nameSingular,
|
||||||
fieldsBox={fieldsBox}
|
}}
|
||||||
loading={isPrefetchLoading || loading || recordLoading}
|
timeline
|
||||||
/>
|
tasks
|
||||||
</ShowPageContainer>
|
notes
|
||||||
|
emails
|
||||||
|
isInRightDrawer={isInRightDrawer}
|
||||||
|
summaryCard={isMobile ? summaryCard : <></>}
|
||||||
|
fieldsBox={fieldsBox}
|
||||||
|
loading={isPrefetchLoading || loading || recordLoading}
|
||||||
|
/>
|
||||||
|
</ShowPageContainer>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -29,7 +29,7 @@ export const buildFindOneRecordForShowPageOperationSignature: RecordGqlOperation
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
...(objectMetadataItem.nameSingular === 'Note'
|
...(objectMetadataItem.nameSingular === CoreObjectNameSingular.Note
|
||||||
? {
|
? {
|
||||||
noteTargets: {
|
noteTargets: {
|
||||||
id: true,
|
id: true,
|
||||||
|
|||||||
@ -50,6 +50,7 @@ export const useRecordShowPage = (
|
|||||||
objectRecordId,
|
objectRecordId,
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
recordGqlFields: FIND_ONE_RECORD_FOR_SHOW_PAGE_OPERATION_SIGNATURE.fields,
|
recordGqlFields: FIND_ONE_RECORD_FOR_SHOW_PAGE_OPERATION_SIGNATURE.fields,
|
||||||
|
withSoftDeleted: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@ -0,0 +1,5 @@
|
|||||||
|
import { capitalize } from '~/utils/string/capitalize';
|
||||||
|
|
||||||
|
export const getDestroyManyRecordsMutationResponseField = (
|
||||||
|
objectNamePlural: string,
|
||||||
|
) => `destroy${capitalize(objectNamePlural)}`;
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
import { capitalize } from '~/utils/string/capitalize';
|
||||||
|
|
||||||
|
export const getRestoreManyRecordsMutationResponseField = (
|
||||||
|
objectNamePlural: string,
|
||||||
|
) => `restore${capitalize(objectNamePlural)}`;
|
||||||
@ -7,13 +7,13 @@ import {
|
|||||||
IconColorSwatch,
|
IconColorSwatch,
|
||||||
IconCurrencyDollar,
|
IconCurrencyDollar,
|
||||||
IconDoorEnter,
|
IconDoorEnter,
|
||||||
|
IconFunction,
|
||||||
IconHierarchy2,
|
IconHierarchy2,
|
||||||
IconMail,
|
IconMail,
|
||||||
IconRocket,
|
IconRocket,
|
||||||
IconSettings,
|
IconSettings,
|
||||||
IconUserCircle,
|
IconUserCircle,
|
||||||
IconUsers,
|
IconUsers,
|
||||||
IconFunction,
|
|
||||||
} from 'twenty-ui';
|
} from 'twenty-ui';
|
||||||
|
|
||||||
import { useAuth } from '@/auth/hooks/useAuth';
|
import { useAuth } from '@/auth/hooks/useAuth';
|
||||||
@ -49,7 +49,6 @@ export const SettingsNavigationDrawerItems = () => {
|
|||||||
path={SettingsPath.Appearance}
|
path={SettingsPath.Appearance}
|
||||||
Icon={IconColorSwatch}
|
Icon={IconColorSwatch}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<NavigationDrawerItemGroup>
|
<NavigationDrawerItemGroup>
|
||||||
<SettingsNavigationDrawerItem
|
<SettingsNavigationDrawerItem
|
||||||
label="Accounts"
|
label="Accounts"
|
||||||
|
|||||||
@ -1,9 +1,12 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
import { OBJECT_SETTINGS_WIDTH } from '@/settings/data-model/constants/ObjectSettings';
|
import { OBJECT_SETTINGS_WIDTH } from '@/settings/data-model/constants/ObjectSettings';
|
||||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
|
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
||||||
|
|
||||||
const StyledSettingsPageContainer = styled.div<{ width?: number }>`
|
const StyledSettingsPageContainer = styled.div<{ width?: number }>`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -21,4 +24,17 @@ const StyledSettingsPageContainer = styled.div<{ width?: number }>`
|
|||||||
}};
|
}};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export { StyledSettingsPageContainer as SettingsPageContainer };
|
const StyledScrollWrapper = styled(ScrollWrapper)`
|
||||||
|
background-color: ${({ theme }) => theme.background.secondary};
|
||||||
|
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const SettingsPageContainer = ({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
}) => (
|
||||||
|
<StyledScrollWrapper>
|
||||||
|
<StyledSettingsPageContainer>{children}</StyledSettingsPageContainer>
|
||||||
|
</StyledScrollWrapper>
|
||||||
|
);
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { IconEye } from 'twenty-ui';
|
|||||||
import { FloatingButton } from '@/ui/input/button/components/FloatingButton';
|
import { FloatingButton } from '@/ui/input/button/components/FloatingButton';
|
||||||
import { Card } from '@/ui/layout/card/components/Card';
|
import { Card } from '@/ui/layout/card/components/Card';
|
||||||
|
|
||||||
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
import DarkCoverImage from '../assets/cover-dark.png';
|
import DarkCoverImage from '../assets/cover-dark.png';
|
||||||
import LightCoverImage from '../assets/cover-light.png';
|
import LightCoverImage from '../assets/cover-light.png';
|
||||||
|
|
||||||
@ -34,7 +35,7 @@ export const SettingsObjectCoverImage = () => {
|
|||||||
Icon={IconEye}
|
Icon={IconEye}
|
||||||
title="Visualize"
|
title="Visualize"
|
||||||
size="small"
|
size="small"
|
||||||
to="/settings/objects/overview"
|
to={'/settings/' + SettingsPath.ObjectOverview}
|
||||||
/>
|
/>
|
||||||
</StyledButtonContainer>
|
</StyledButtonContainer>
|
||||||
</StyledCoverImageContainer>
|
</StyledCoverImageContainer>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { Section } from '@react-email/components';
|
import { Section } from '@react-email/components';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { H2Title } from 'twenty-ui';
|
import { H2Title } from 'twenty-ui';
|
||||||
|
|
||||||
import { useDeleteOneDatabaseConnection } from '@/databases/hooks/useDeleteOneDatabaseConnection';
|
import { useDeleteOneDatabaseConnection } from '@/databases/hooks/useDeleteOneDatabaseConnection';
|
||||||
@ -31,6 +31,7 @@ export const SettingsIntegrationDatabaseConnectionShowContainer = () => {
|
|||||||
SettingsPath.Integrations,
|
SettingsPath.Integrations,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// TODO: move breadcrumb to header?
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Breadcrumb
|
<Breadcrumb
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { FormProvider, useForm } from 'react-hook-form';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Section } from '@react-email/components';
|
import { Section } from '@react-email/components';
|
||||||
import pick from 'lodash.pick';
|
import pick from 'lodash.pick';
|
||||||
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { H2Title } from 'twenty-ui';
|
import { H2Title } from 'twenty-ui';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
@ -94,6 +94,7 @@ export const SettingsIntegrationEditDatabaseConnectionContent = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: move breadcrumb to header?
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FormProvider
|
<FormProvider
|
||||||
|
|||||||
@ -15,7 +15,7 @@ export enum AppPath {
|
|||||||
|
|
||||||
// Onboarded
|
// Onboarded
|
||||||
Index = '/',
|
Index = '/',
|
||||||
TasksPage = '/tasks',
|
TasksPage = '/objects/tasks',
|
||||||
OpportunitiesPage = '/objects/opportunities',
|
OpportunitiesPage = '/objects/opportunities',
|
||||||
|
|
||||||
RecordIndexPage = '/objects/:objectNamePlural',
|
RecordIndexPage = '/objects/:objectNamePlural',
|
||||||
|
|||||||
@ -336,6 +336,7 @@ const StyledButton = styled('button', {
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
font-family: ${({ theme }) => theme.font.family};
|
font-family: ${({ theme }) => theme.font.family};
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
font-size: ${({ theme }) => theme.font.size.md};
|
||||||
gap: ${({ theme }) => theme.spacing(1)};
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
height: ${({ size }) => (size === 'small' ? '24px' : '32px')};
|
height: ${({ size }) => (size === 'small' ? '24px' : '32px')};
|
||||||
justify-content: ${({ justify }) => justify};
|
justify-content: ${({ justify }) => justify};
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import isPropValid from '@emotion/is-prop-valid';
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
@ -19,12 +20,11 @@ export type FloatingButtonProps = {
|
|||||||
to?: string;
|
to?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const shouldForwardProp = (prop: string) =>
|
const StyledButton = styled('button', {
|
||||||
!['applyBlur', 'applyShadow', 'focus', 'position', 'size', 'to'].includes(
|
shouldForwardProp: (prop) =>
|
||||||
prop,
|
!['applyBlur', 'applyShadow', 'focus', 'position', 'size'].includes(prop) &&
|
||||||
);
|
isPropValid(prop),
|
||||||
|
})<
|
||||||
const StyledButton = styled('button', { shouldForwardProp })<
|
|
||||||
Pick<
|
Pick<
|
||||||
FloatingButtonProps,
|
FloatingButtonProps,
|
||||||
| 'size'
|
| 'size'
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { ComponentProps, ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import {
|
import {
|
||||||
IconChevronDown,
|
IconChevronDown,
|
||||||
@ -18,7 +18,7 @@ import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
|||||||
|
|
||||||
export const PAGE_BAR_MIN_HEIGHT = 40;
|
export const PAGE_BAR_MIN_HEIGHT = 40;
|
||||||
|
|
||||||
const StyledTopBarContainer = styled.div`
|
const StyledTopBarContainer = styled.div<{ width?: number }>`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: ${({ theme }) => theme.background.noisy};
|
background: ${({ theme }) => theme.background.noisy};
|
||||||
color: ${({ theme }) => theme.font.color.primary};
|
color: ${({ theme }) => theme.font.color.primary};
|
||||||
@ -31,6 +31,7 @@ const StyledTopBarContainer = styled.div`
|
|||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
padding-right: ${({ theme }) => theme.spacing(3)};
|
padding-right: ${({ theme }) => theme.spacing(3)};
|
||||||
z-index: 20;
|
z-index: 20;
|
||||||
|
width: ${({ width }) => width + 'px' || '100%'};
|
||||||
|
|
||||||
@media (max-width: ${MOBILE_VIEWPORT}px) {
|
@media (max-width: ${MOBILE_VIEWPORT}px) {
|
||||||
padding-left: ${({ theme }) => theme.spacing(3)};
|
padding-left: ${({ theme }) => theme.spacing(3)};
|
||||||
@ -76,8 +77,8 @@ const StyledTopBarButtonContainer = styled.div`
|
|||||||
margin-right: ${({ theme }) => theme.spacing(1)};
|
margin-right: ${({ theme }) => theme.spacing(1)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type PageHeaderProps = ComponentProps<'div'> & {
|
type PageHeaderProps = {
|
||||||
title: string;
|
title: ReactNode;
|
||||||
hasClosePageButton?: boolean;
|
hasClosePageButton?: boolean;
|
||||||
onClosePage?: () => void;
|
onClosePage?: () => void;
|
||||||
hasPaginationButtons?: boolean;
|
hasPaginationButtons?: boolean;
|
||||||
@ -87,6 +88,7 @@ type PageHeaderProps = ComponentProps<'div'> & {
|
|||||||
navigateToNextRecord?: () => void;
|
navigateToNextRecord?: () => void;
|
||||||
Icon: IconComponent;
|
Icon: IconComponent;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
|
width?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PageHeader = ({
|
export const PageHeader = ({
|
||||||
@ -100,13 +102,14 @@ export const PageHeader = ({
|
|||||||
navigateToNextRecord,
|
navigateToNextRecord,
|
||||||
Icon,
|
Icon,
|
||||||
children,
|
children,
|
||||||
|
width,
|
||||||
}: PageHeaderProps) => {
|
}: PageHeaderProps) => {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isNavigationDrawerOpen = useRecoilValue(isNavigationDrawerOpenState);
|
const isNavigationDrawerOpen = useRecoilValue(isNavigationDrawerOpenState);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledTopBarContainer>
|
<StyledTopBarContainer width={width}>
|
||||||
<StyledLeftContainer>
|
<StyledLeftContainer>
|
||||||
{!isMobile && !isNavigationDrawerOpen && (
|
{!isMobile && !isNavigationDrawerOpen && (
|
||||||
<StyledTopBarButtonContainer>
|
<StyledTopBarButtonContainer>
|
||||||
@ -143,7 +146,11 @@ export const PageHeader = ({
|
|||||||
)}
|
)}
|
||||||
{Icon && <Icon size={theme.icon.size.md} />}
|
{Icon && <Icon size={theme.icon.size.md} />}
|
||||||
<StyledTitleContainer data-testid="top-bar-title">
|
<StyledTitleContainer data-testid="top-bar-title">
|
||||||
<OverflowingTextWithTooltip text={title} />
|
{typeof title === 'string' ? (
|
||||||
|
<OverflowingTextWithTooltip text={title} />
|
||||||
|
) : (
|
||||||
|
title
|
||||||
|
)}
|
||||||
</StyledTitleContainer>
|
</StyledTitleContainer>
|
||||||
</StyledTopBarIconStyledTitleContainer>
|
</StyledTopBarIconStyledTitleContainer>
|
||||||
</StyledLeftContainer>
|
</StyledLeftContainer>
|
||||||
|
|||||||
@ -1,15 +1,21 @@
|
|||||||
import React from 'react';
|
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
const StyledPanel = styled.div`
|
const StyledPanel = styled.div`
|
||||||
background: ${({ theme }) => theme.background.primary};
|
background: ${({ theme }) => theme.background.primary};
|
||||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: auto;
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const PagePanel = ({ children }: { children: React.ReactNode }) => (
|
type PagePanelProps = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
hasInformationBar?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PagePanel = ({ children }: PagePanelProps) => (
|
||||||
<StyledPanel>{children}</StyledPanel>
|
<StyledPanel>{children}</StyledPanel>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,16 +1,18 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { JSX } from 'react';
|
import { JSX, ReactNode } from 'react';
|
||||||
import { IconComponent } from 'twenty-ui';
|
import { IconComponent } from 'twenty-ui';
|
||||||
|
|
||||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||||
|
|
||||||
import { InformationBannerWrapper } from '@/information-banner/components/InformationBannerWrapper';
|
import { InformationBannerWrapper } from '@/information-banner/components/InformationBannerWrapper';
|
||||||
|
import { OBJECT_SETTINGS_WIDTH } from '@/settings/data-model/constants/ObjectSettings';
|
||||||
import { PageBody } from './PageBody';
|
import { PageBody } from './PageBody';
|
||||||
import { PageHeader } from './PageHeader';
|
import { PageHeader } from './PageHeader';
|
||||||
|
|
||||||
type SubMenuTopBarContainerProps = {
|
type SubMenuTopBarContainerProps = {
|
||||||
children: JSX.Element | JSX.Element[];
|
children: JSX.Element | JSX.Element[];
|
||||||
title: string;
|
title: string | ReactNode;
|
||||||
|
actionButton?: ReactNode;
|
||||||
Icon: IconComponent;
|
Icon: IconComponent;
|
||||||
className?: string;
|
className?: string;
|
||||||
};
|
};
|
||||||
@ -25,6 +27,7 @@ const StyledContainer = styled.div<{ isMobile: boolean }>`
|
|||||||
export const SubMenuTopBarContainer = ({
|
export const SubMenuTopBarContainer = ({
|
||||||
children,
|
children,
|
||||||
title,
|
title,
|
||||||
|
actionButton,
|
||||||
Icon,
|
Icon,
|
||||||
className,
|
className,
|
||||||
}: SubMenuTopBarContainerProps) => {
|
}: SubMenuTopBarContainerProps) => {
|
||||||
@ -32,7 +35,13 @@ export const SubMenuTopBarContainer = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer isMobile={isMobile} className={className}>
|
<StyledContainer isMobile={isMobile} className={className}>
|
||||||
{isMobile && <PageHeader title={title} Icon={Icon} />}
|
<PageHeader
|
||||||
|
title={title}
|
||||||
|
Icon={Icon}
|
||||||
|
width={OBJECT_SETTINGS_WIDTH + 4 * 8}
|
||||||
|
>
|
||||||
|
{actionButton}
|
||||||
|
</PageHeader>
|
||||||
<PageBody>
|
<PageBody>
|
||||||
<InformationBannerWrapper />
|
<InformationBannerWrapper />
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { IconDotsVertical, IconTrash } from 'twenty-ui';
|
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||||
|
import { IconDotsVertical, IconRestore, IconTrash } from 'twenty-ui';
|
||||||
|
|
||||||
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
|
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
|
||||||
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
|
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
|
||||||
@ -11,6 +11,9 @@ import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
|||||||
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||||
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
|
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
|
||||||
|
|
||||||
|
import { useDestroyManyRecords } from '@/object-record/hooks/useDestroyManyRecords';
|
||||||
|
import { useRestoreManyRecords } from '@/object-record/hooks/useRestoreManyRecords';
|
||||||
|
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||||
import { Dropdown } from '../../dropdown/components/Dropdown';
|
import { Dropdown } from '../../dropdown/components/Dropdown';
|
||||||
import { DropdownMenu } from '../../dropdown/components/DropdownMenu';
|
import { DropdownMenu } from '../../dropdown/components/DropdownMenu';
|
||||||
|
|
||||||
@ -32,6 +35,12 @@ export const ShowPageMoreButton = ({
|
|||||||
const { deleteOneRecord } = useDeleteOneRecord({
|
const { deleteOneRecord } = useDeleteOneRecord({
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
});
|
});
|
||||||
|
const { destroyManyRecords } = useDestroyManyRecords({
|
||||||
|
objectNameSingular,
|
||||||
|
});
|
||||||
|
const { restoreManyRecords } = useRestoreManyRecords({
|
||||||
|
objectNameSingular,
|
||||||
|
});
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
deleteOneRecord(recordId);
|
deleteOneRecord(recordId);
|
||||||
@ -39,6 +48,21 @@ export const ShowPageMoreButton = ({
|
|||||||
navigate(navigationMemorizedUrl, { replace: true });
|
navigate(navigationMemorizedUrl, { replace: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDestroy = () => {
|
||||||
|
destroyManyRecords([recordId]);
|
||||||
|
closeDropdown();
|
||||||
|
navigate(navigationMemorizedUrl, { replace: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRestore = () => {
|
||||||
|
restoreManyRecords([recordId]);
|
||||||
|
closeDropdown();
|
||||||
|
};
|
||||||
|
|
||||||
|
const [recordFromStore] = useRecoilState<any>(
|
||||||
|
recordStoreFamilyState(recordId),
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
@ -56,12 +80,29 @@ export const ShowPageMoreButton = ({
|
|||||||
dropdownComponents={
|
dropdownComponents={
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuItemsContainer>
|
<DropdownMenuItemsContainer>
|
||||||
<MenuItem
|
{recordFromStore && !recordFromStore.deletedAt && (
|
||||||
onClick={handleDelete}
|
<MenuItem
|
||||||
accent="danger"
|
onClick={handleDelete}
|
||||||
LeftIcon={IconTrash}
|
accent="danger"
|
||||||
text="Delete"
|
LeftIcon={IconTrash}
|
||||||
/>
|
text="Delete"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{recordFromStore && recordFromStore.deletedAt && (
|
||||||
|
<>
|
||||||
|
<MenuItem
|
||||||
|
onClick={handleDestroy}
|
||||||
|
accent="danger"
|
||||||
|
LeftIcon={IconTrash}
|
||||||
|
text="Destroy"
|
||||||
|
/>
|
||||||
|
<MenuItem
|
||||||
|
onClick={handleRestore}
|
||||||
|
LeftIcon={IconRestore}
|
||||||
|
text="Restore"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</DropdownMenuItemsContainer>
|
</DropdownMenuItemsContainer>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
import { Fragment } from 'react';
|
import { Fragment } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import styled from '@emotion/styled';
|
|
||||||
|
|
||||||
type BreadcrumbProps = {
|
type BreadcrumbProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -9,10 +9,10 @@ type BreadcrumbProps = {
|
|||||||
|
|
||||||
const StyledWrapper = styled.nav`
|
const StyledWrapper = styled.nav`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: ${({ theme }) => theme.font.color.extraLight};
|
color: ${({ theme }) => theme.font.color.secondary};
|
||||||
display: flex;
|
display: flex;
|
||||||
font-size: ${({ theme }) => theme.font.size.lg};
|
font-size: ${({ theme }) => theme.font.size.md};
|
||||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
// font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||||
gap: ${({ theme }) => theme.spacing(2)};
|
gap: ${({ theme }) => theme.spacing(2)};
|
||||||
line-height: ${({ theme }) => theme.text.lineHeight.lg};
|
line-height: ${({ theme }) => theme.text.lineHeight.lg};
|
||||||
`;
|
`;
|
||||||
@ -23,7 +23,7 @@ const StyledLink = styled(Link)`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledText = styled.span`
|
const StyledText = styled.span`
|
||||||
color: ${({ theme }) => theme.font.color.tertiary};
|
color: ${({ theme }) => theme.font.color.primary};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Breadcrumb = ({ className, links }: BreadcrumbProps) => (
|
export const Breadcrumb = ({ className, links }: BreadcrumbProps) => (
|
||||||
|
|||||||
@ -2,12 +2,37 @@ import { useTheme } from '@emotion/react';
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { IconComponent, IconX } from 'twenty-ui';
|
import { IconComponent, IconX } from 'twenty-ui';
|
||||||
|
|
||||||
const StyledChip = styled.div`
|
const StyledChip = styled.div<{ variant: SortOrFitlerChipVariant }>`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: ${({ theme }) => theme.accent.quaternary};
|
background-color: ${({ theme, variant }) => {
|
||||||
border: 1px solid ${({ theme }) => theme.accent.tertiary};
|
switch (variant) {
|
||||||
|
case 'danger':
|
||||||
|
return theme.background.danger;
|
||||||
|
case 'default':
|
||||||
|
default:
|
||||||
|
return theme.accent.quaternary;
|
||||||
|
}
|
||||||
|
}};
|
||||||
|
border: 1px solid
|
||||||
|
${({ theme, variant }) => {
|
||||||
|
switch (variant) {
|
||||||
|
case 'danger':
|
||||||
|
return theme.border.color.danger;
|
||||||
|
case 'default':
|
||||||
|
default:
|
||||||
|
return theme.accent.tertiary;
|
||||||
|
}
|
||||||
|
}};
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: ${({ theme }) => theme.color.blue};
|
color: ${({ theme, variant }) => {
|
||||||
|
switch (variant) {
|
||||||
|
case 'danger':
|
||||||
|
return theme.color.red;
|
||||||
|
case 'default':
|
||||||
|
default:
|
||||||
|
return theme.color.blue;
|
||||||
|
}
|
||||||
|
}};
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@ -24,7 +49,7 @@ const StyledIcon = styled.div`
|
|||||||
margin-right: ${({ theme }) => theme.spacing(1)};
|
margin-right: ${({ theme }) => theme.spacing(1)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledDelete = styled.div`
|
const StyledDelete = styled.div<{ variant: SortOrFitlerChipVariant }>`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -33,7 +58,15 @@ const StyledDelete = styled.div`
|
|||||||
margin-top: 1px;
|
margin-top: 1px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: ${({ theme }) => theme.accent.secondary};
|
background-color: ${({ theme, variant }) => {
|
||||||
|
switch (variant) {
|
||||||
|
case 'danger':
|
||||||
|
return theme.color.red20;
|
||||||
|
case 'default':
|
||||||
|
default:
|
||||||
|
return theme.accent.secondary;
|
||||||
|
}
|
||||||
|
}};
|
||||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@ -42,9 +75,12 @@ const StyledLabelKey = styled.div`
|
|||||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
type SortOrFitlerChipVariant = 'default' | 'danger';
|
||||||
|
|
||||||
type SortOrFilterChipProps = {
|
type SortOrFilterChipProps = {
|
||||||
labelKey?: string;
|
labelKey?: string;
|
||||||
labelValue: string;
|
labelValue: string;
|
||||||
|
variant?: SortOrFitlerChipVariant;
|
||||||
Icon?: IconComponent;
|
Icon?: IconComponent;
|
||||||
onRemove: () => void;
|
onRemove: () => void;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
@ -54,6 +90,7 @@ type SortOrFilterChipProps = {
|
|||||||
export const SortOrFilterChip = ({
|
export const SortOrFilterChip = ({
|
||||||
labelKey,
|
labelKey,
|
||||||
labelValue,
|
labelValue,
|
||||||
|
variant = 'default',
|
||||||
Icon,
|
Icon,
|
||||||
onRemove,
|
onRemove,
|
||||||
testId,
|
testId,
|
||||||
@ -67,7 +104,7 @@ export const SortOrFilterChip = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledChip onClick={onClick}>
|
<StyledChip onClick={onClick} variant={variant}>
|
||||||
{Icon && (
|
{Icon && (
|
||||||
<StyledIcon>
|
<StyledIcon>
|
||||||
<Icon size={theme.icon.size.sm} />
|
<Icon size={theme.icon.size.sm} />
|
||||||
@ -76,6 +113,7 @@ export const SortOrFilterChip = ({
|
|||||||
{labelKey && <StyledLabelKey>{labelKey}</StyledLabelKey>}
|
{labelKey && <StyledLabelKey>{labelKey}</StyledLabelKey>}
|
||||||
{labelValue}
|
{labelValue}
|
||||||
<StyledDelete
|
<StyledDelete
|
||||||
|
variant={variant}
|
||||||
onClick={handleDeleteClick}
|
onClick={handleDeleteClick}
|
||||||
data-testid={'remove-icon-' + testId}
|
data-testid={'remove-icon-' + testId}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -0,0 +1,30 @@
|
|||||||
|
import { useIcons } from 'twenty-ui';
|
||||||
|
|
||||||
|
import { Filter } from '@/object-record/object-filter-dropdown/types/Filter';
|
||||||
|
import { SortOrFilterChip } from '@/views/components/SortOrFilterChip';
|
||||||
|
import { useCombinedViewFilters } from '@/views/hooks/useCombinedViewFilters';
|
||||||
|
|
||||||
|
type VariantFilterChipProps = {
|
||||||
|
viewFilter: Filter;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const VariantFilterChip = ({ viewFilter }: VariantFilterChipProps) => {
|
||||||
|
const { removeCombinedViewFilter } = useCombinedViewFilters();
|
||||||
|
|
||||||
|
const { getIcon } = useIcons();
|
||||||
|
|
||||||
|
const handleRemoveClick = () => {
|
||||||
|
removeCombinedViewFilter(viewFilter.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SortOrFilterChip
|
||||||
|
key={viewFilter.fieldMetadataId}
|
||||||
|
testId={viewFilter.fieldMetadataId}
|
||||||
|
variant={viewFilter.variant}
|
||||||
|
labelValue={viewFilter.definition.label}
|
||||||
|
Icon={getIcon(viewFilter.definition.iconName)}
|
||||||
|
onRemove={handleRemoveClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,9 +1,10 @@
|
|||||||
import { ReactNode } from 'react';
|
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import { ReactNode, useMemo } from 'react';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
import { AddObjectFilterFromDetailsButton } from '@/object-record/object-filter-dropdown/components/AddObjectFilterFromDetailsButton';
|
import { AddObjectFilterFromDetailsButton } from '@/object-record/object-filter-dropdown/components/AddObjectFilterFromDetailsButton';
|
||||||
import { ObjectFilterDropdownScope } from '@/object-record/object-filter-dropdown/scopes/ObjectFilterDropdownScope';
|
import { ObjectFilterDropdownScope } from '@/object-record/object-filter-dropdown/scopes/ObjectFilterDropdownScope';
|
||||||
|
import { Filter } from '@/object-record/object-filter-dropdown/types/Filter';
|
||||||
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
|
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
|
||||||
import { EditableFilterDropdownButton } from '@/views/components/EditableFilterDropdownButton';
|
import { EditableFilterDropdownButton } from '@/views/components/EditableFilterDropdownButton';
|
||||||
import { EditableSortChip } from '@/views/components/EditableSortChip';
|
import { EditableSortChip } from '@/views/components/EditableSortChip';
|
||||||
@ -14,6 +15,7 @@ import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
|
|||||||
import { useResetCurrentView } from '@/views/hooks/useResetCurrentView';
|
import { useResetCurrentView } from '@/views/hooks/useResetCurrentView';
|
||||||
import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
|
import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
|
||||||
import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts';
|
import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts';
|
||||||
|
import { VariantFilterChip } from './VariantFilterChip';
|
||||||
|
|
||||||
export type ViewBarDetailsProps = {
|
export type ViewBarDetailsProps = {
|
||||||
hasFilterButton?: boolean;
|
hasFilterButton?: boolean;
|
||||||
@ -118,6 +120,29 @@ export const ViewBarDetails = ({
|
|||||||
const { resetCurrentView } = useResetCurrentView();
|
const { resetCurrentView } = useResetCurrentView();
|
||||||
const canResetView = canPersistView && !hasFiltersQueryParams;
|
const canResetView = canPersistView && !hasFiltersQueryParams;
|
||||||
|
|
||||||
|
const { otherViewFilters, defaultViewFilters } = useMemo(() => {
|
||||||
|
if (!currentViewWithCombinedFiltersAndSorts) {
|
||||||
|
return {
|
||||||
|
otherViewFilters: [],
|
||||||
|
defaultViewFilters: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const otherViewFilters =
|
||||||
|
currentViewWithCombinedFiltersAndSorts.viewFilters.filter(
|
||||||
|
(viewFilter) => viewFilter.variant && viewFilter.variant !== 'default',
|
||||||
|
);
|
||||||
|
const defaultViewFilters =
|
||||||
|
currentViewWithCombinedFiltersAndSorts.viewFilters.filter(
|
||||||
|
(viewFilter) => !viewFilter.variant || viewFilter.variant === 'default',
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
otherViewFilters,
|
||||||
|
defaultViewFilters,
|
||||||
|
};
|
||||||
|
}, [currentViewWithCombinedFiltersAndSorts]);
|
||||||
|
|
||||||
const handleCancelClick = () => {
|
const handleCancelClick = () => {
|
||||||
resetCurrentView();
|
resetCurrentView();
|
||||||
};
|
};
|
||||||
@ -136,6 +161,22 @@ export const ViewBarDetails = ({
|
|||||||
<StyledBar>
|
<StyledBar>
|
||||||
<StyledFilterContainer>
|
<StyledFilterContainer>
|
||||||
<StyledChipcontainer>
|
<StyledChipcontainer>
|
||||||
|
{otherViewFilters.map((viewFilter) => (
|
||||||
|
<VariantFilterChip
|
||||||
|
key={viewFilter.fieldMetadataId}
|
||||||
|
// Why do we have two types, Filter and ViewFilter?
|
||||||
|
// Why key defition is already present in the Filter type and added on the fly here with mapViewFiltersToFilters ?
|
||||||
|
// Also as filter is spread into viewFilter, definition is present
|
||||||
|
// FixMe: Ugly hack to make it work
|
||||||
|
viewFilter={viewFilter as unknown as Filter}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{!!otherViewFilters.length &&
|
||||||
|
!!currentViewWithCombinedFiltersAndSorts?.viewSorts?.length && (
|
||||||
|
<StyledSeperatorContainer>
|
||||||
|
<StyledSeperator />
|
||||||
|
</StyledSeperatorContainer>
|
||||||
|
)}
|
||||||
{mapViewSortsToSorts(
|
{mapViewSortsToSorts(
|
||||||
currentViewWithCombinedFiltersAndSorts?.viewSorts ?? [],
|
currentViewWithCombinedFiltersAndSorts?.viewSorts ?? [],
|
||||||
availableSortDefinitions,
|
availableSortDefinitions,
|
||||||
@ -143,13 +184,13 @@ export const ViewBarDetails = ({
|
|||||||
<EditableSortChip key={sort.fieldMetadataId} viewSort={sort} />
|
<EditableSortChip key={sort.fieldMetadataId} viewSort={sort} />
|
||||||
))}
|
))}
|
||||||
{!!currentViewWithCombinedFiltersAndSorts?.viewSorts?.length &&
|
{!!currentViewWithCombinedFiltersAndSorts?.viewSorts?.length &&
|
||||||
!!currentViewWithCombinedFiltersAndSorts?.viewFilters?.length && (
|
!!defaultViewFilters.length && (
|
||||||
<StyledSeperatorContainer>
|
<StyledSeperatorContainer>
|
||||||
<StyledSeperator />
|
<StyledSeperator />
|
||||||
</StyledSeperatorContainer>
|
</StyledSeperatorContainer>
|
||||||
)}
|
)}
|
||||||
{mapViewFiltersToFilters(
|
{mapViewFiltersToFilters(
|
||||||
currentViewWithCombinedFiltersAndSorts?.viewFilters ?? [],
|
defaultViewFilters,
|
||||||
availableFilterDefinitions,
|
availableFilterDefinitions,
|
||||||
).map((viewFilter) => (
|
).map((viewFilter) => (
|
||||||
<ObjectFilterDropdownScope
|
<ObjectFilterDropdownScope
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { ViewFilterOperand } from './ViewFilterOperand';
|
|||||||
export type ViewFilter = {
|
export type ViewFilter = {
|
||||||
__typename: 'ViewFilter';
|
__typename: 'ViewFilter';
|
||||||
id: string;
|
id: string;
|
||||||
|
variant?: 'default' | 'danger';
|
||||||
fieldMetadataId: string;
|
fieldMetadataId: string;
|
||||||
operand: ViewFilterOperand;
|
operand: ViewFilterOperand;
|
||||||
value: string;
|
value: string;
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import React, { useEffect, useState } from 'react';
|
|||||||
import rehypeStringify from 'rehype-stringify';
|
import rehypeStringify from 'rehype-stringify';
|
||||||
import remarkParse from 'remark-parse';
|
import remarkParse from 'remark-parse';
|
||||||
import remarkRehype from 'remark-rehype';
|
import remarkRehype from 'remark-rehype';
|
||||||
import { H1Title, IconSettings } from 'twenty-ui';
|
import { H1Title, IconRocket } from 'twenty-ui';
|
||||||
import { unified } from 'unified';
|
import { unified } from 'unified';
|
||||||
import { visit } from 'unist-util-visit';
|
import { visit } from 'unist-util-visit';
|
||||||
|
|
||||||
@ -108,7 +108,7 @@ export const Releases = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SubMenuTopBarContainer Icon={IconSettings} title="Releases">
|
<SubMenuTopBarContainer Icon={IconRocket} title="Releases">
|
||||||
<SettingsPageContainer>
|
<SettingsPageContainer>
|
||||||
<StyledH1Title title="Releases" />
|
<StyledH1Title title="Releases" />
|
||||||
<ScrollWrapper>
|
<ScrollWrapper>
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import styled from '@emotion/styled';
|
import { H2Title, IconUserCircle } from 'twenty-ui';
|
||||||
import { H1Title, H2Title, IconSettings } from 'twenty-ui';
|
|
||||||
|
|
||||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||||
import { ChangePassword } from '@/settings/profile/components/ChangePassword';
|
import { ChangePassword } from '@/settings/profile/components/ChangePassword';
|
||||||
@ -10,14 +9,9 @@ import { ProfilePictureUploader } from '@/settings/profile/components/ProfilePic
|
|||||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
||||||
import { Section } from '@/ui/layout/section/components/Section';
|
import { Section } from '@/ui/layout/section/components/Section';
|
||||||
|
|
||||||
const StyledH1Title = styled(H1Title)`
|
|
||||||
margin-bottom: 0;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const SettingsProfile = () => (
|
export const SettingsProfile = () => (
|
||||||
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
<SubMenuTopBarContainer Icon={IconUserCircle} title="Profile">
|
||||||
<SettingsPageContainer>
|
<SettingsPageContainer>
|
||||||
<StyledH1Title title="Profile" />
|
|
||||||
<Section>
|
<Section>
|
||||||
<H2Title title="Picture" />
|
<H2Title title="Picture" />
|
||||||
<ProfilePictureUploader />
|
<ProfilePictureUploader />
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import styled from '@emotion/styled';
|
import { H2Title, IconSettings } from 'twenty-ui';
|
||||||
import { H1Title, H2Title, IconSettings } from 'twenty-ui';
|
|
||||||
|
|
||||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||||
import { DeleteWorkspace } from '@/settings/profile/components/DeleteWorkspace';
|
import { DeleteWorkspace } from '@/settings/profile/components/DeleteWorkspace';
|
||||||
@ -9,14 +8,9 @@ import { WorkspaceLogoUploader } from '@/settings/workspace/components/Workspace
|
|||||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
||||||
import { Section } from '@/ui/layout/section/components/Section';
|
import { Section } from '@/ui/layout/section/components/Section';
|
||||||
|
|
||||||
const StyledH1Title = styled(H1Title)`
|
|
||||||
margin-bottom: 0;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const SettingsWorkspace = () => (
|
export const SettingsWorkspace = () => (
|
||||||
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
<SubMenuTopBarContainer Icon={IconSettings} title="General">
|
||||||
<SettingsPageContainer>
|
<SettingsPageContainer>
|
||||||
<StyledH1Title title="General" />
|
|
||||||
<Section>
|
<Section>
|
||||||
<H2Title title="Picture" />
|
<H2Title title="Picture" />
|
||||||
<WorkspaceLogoUploader />
|
<WorkspaceLogoUploader />
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { H1Title, H2Title, IconSettings, IconTrash } from 'twenty-ui';
|
import { H2Title, IconTrash, IconUsers } from 'twenty-ui';
|
||||||
|
|
||||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||||
@ -18,10 +18,6 @@ import { WorkspaceInviteLink } from '@/workspace/components/WorkspaceInviteLink'
|
|||||||
import { WorkspaceInviteTeam } from '@/workspace/components/WorkspaceInviteTeam';
|
import { WorkspaceInviteTeam } from '@/workspace/components/WorkspaceInviteTeam';
|
||||||
import { WorkspaceMemberCard } from '@/workspace/components/WorkspaceMemberCard';
|
import { WorkspaceMemberCard } from '@/workspace/components/WorkspaceMemberCard';
|
||||||
|
|
||||||
const StyledH1Title = styled(H1Title)`
|
|
||||||
margin-bottom: 0;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const StyledButtonContainer = styled.div`
|
const StyledButtonContainer = styled.div`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -50,9 +46,8 @@ export const SettingsWorkspaceMembers = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
<SubMenuTopBarContainer Icon={IconUsers} title="Members">
|
||||||
<SettingsPageContainer>
|
<SettingsPageContainer>
|
||||||
<StyledH1Title title="Members" />
|
|
||||||
<Section>
|
<Section>
|
||||||
<H2Title
|
<H2Title
|
||||||
title="Invite by email"
|
title="Invite by email"
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { H2Title, IconSettings } from 'twenty-ui';
|
import { H2Title, IconAt } from 'twenty-ui';
|
||||||
|
|
||||||
import { ConnectedAccount } from '@/accounts/types/ConnectedAccount';
|
import { ConnectedAccount } from '@/accounts/types/ConnectedAccount';
|
||||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||||
@ -14,7 +14,6 @@ import { SettingsAccountsSettingsSection } from '@/settings/accounts/components/
|
|||||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
||||||
import { Section } from '@/ui/layout/section/components/Section';
|
import { Section } from '@/ui/layout/section/components/Section';
|
||||||
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
|
|
||||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||||
|
|
||||||
export const SettingsAccounts = () => {
|
export const SettingsAccounts = () => {
|
||||||
@ -37,10 +36,8 @@ export const SettingsAccounts = () => {
|
|||||||
const isBlocklistEnabled = useIsFeatureEnabled('IS_BLOCKLIST_ENABLED');
|
const isBlocklistEnabled = useIsFeatureEnabled('IS_BLOCKLIST_ENABLED');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
<SubMenuTopBarContainer Icon={IconAt} title="Account">
|
||||||
<SettingsPageContainer>
|
<SettingsPageContainer>
|
||||||
<Breadcrumb links={[{ children: 'Accounts' }]} />
|
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<SettingsAccountLoader />
|
<SettingsAccountLoader />
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
import { IconSettings } from 'twenty-ui';
|
|
||||||
|
|
||||||
import { SettingsAccountsCalendarChannelsContainer } from '@/settings/accounts/components/SettingsAccountsCalendarChannelsContainer';
|
import { SettingsAccountsCalendarChannelsContainer } from '@/settings/accounts/components/SettingsAccountsCalendarChannelsContainer';
|
||||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||||
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
||||||
@ -7,11 +5,13 @@ import { SettingsPath } from '@/types/SettingsPath';
|
|||||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
||||||
import { Section } from '@/ui/layout/section/components/Section';
|
import { Section } from '@/ui/layout/section/components/Section';
|
||||||
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
|
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
|
||||||
|
import { IconCalendarEvent } from 'twenty-ui';
|
||||||
|
|
||||||
export const SettingsAccountsCalendars = () => {
|
export const SettingsAccountsCalendars = () => {
|
||||||
return (
|
return (
|
||||||
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
<SubMenuTopBarContainer
|
||||||
<SettingsPageContainer>
|
Icon={IconCalendarEvent}
|
||||||
|
title={
|
||||||
<Breadcrumb
|
<Breadcrumb
|
||||||
links={[
|
links={[
|
||||||
{
|
{
|
||||||
@ -21,6 +21,9 @@ export const SettingsAccountsCalendars = () => {
|
|||||||
{ children: 'Calendars' },
|
{ children: 'Calendars' },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SettingsPageContainer>
|
||||||
<Section>
|
<Section>
|
||||||
<SettingsAccountsCalendarChannelsContainer />
|
<SettingsAccountsCalendarChannelsContainer />
|
||||||
</Section>
|
</Section>
|
||||||
|
|||||||
@ -1,20 +1,23 @@
|
|||||||
import { IconSettings } from 'twenty-ui';
|
|
||||||
|
|
||||||
import { SettingsAccountsMessageChannelsContainer } from '@/settings/accounts/components/SettingsAccountsMessageChannelsContainer';
|
import { SettingsAccountsMessageChannelsContainer } from '@/settings/accounts/components/SettingsAccountsMessageChannelsContainer';
|
||||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
||||||
import { Section } from '@/ui/layout/section/components/Section';
|
import { Section } from '@/ui/layout/section/components/Section';
|
||||||
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
|
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
|
||||||
|
import { IconMail } from 'twenty-ui';
|
||||||
|
|
||||||
export const SettingsAccountsEmails = () => (
|
export const SettingsAccountsEmails = () => (
|
||||||
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
<SubMenuTopBarContainer
|
||||||
<SettingsPageContainer>
|
Icon={IconMail}
|
||||||
|
title={
|
||||||
<Breadcrumb
|
<Breadcrumb
|
||||||
links={[
|
links={[
|
||||||
{ children: 'Accounts', href: '/settings/accounts' },
|
{ children: 'Accounts', href: '/settings/accounts' },
|
||||||
{ children: 'Emails' },
|
{ children: 'Emails' },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SettingsPageContainer>
|
||||||
<Section>
|
<Section>
|
||||||
<SettingsAccountsMessageChannelsContainer />
|
<SettingsAccountsMessageChannelsContainer />
|
||||||
</Section>
|
</Section>
|
||||||
|
|||||||
@ -1,20 +1,23 @@
|
|||||||
import { IconSettings } from 'twenty-ui';
|
|
||||||
|
|
||||||
import { SettingsNewAccountSection } from '@/settings/accounts/components/SettingsNewAccountSection';
|
import { SettingsNewAccountSection } from '@/settings/accounts/components/SettingsNewAccountSection';
|
||||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
||||||
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
|
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
|
||||||
|
import { IconAt } from 'twenty-ui';
|
||||||
|
|
||||||
export const SettingsNewAccount = () => {
|
export const SettingsNewAccount = () => {
|
||||||
return (
|
return (
|
||||||
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
<SubMenuTopBarContainer
|
||||||
<SettingsPageContainer>
|
Icon={IconAt}
|
||||||
|
title={
|
||||||
<Breadcrumb
|
<Breadcrumb
|
||||||
links={[
|
links={[
|
||||||
{ children: 'Accounts', href: '/settings/accounts' },
|
{ children: 'Accounts', href: '/settings/accounts' },
|
||||||
{ children: `New` },
|
{ children: `New` },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SettingsPageContainer>
|
||||||
<SettingsNewAccountSection />
|
<SettingsNewAccountSection />
|
||||||
</SettingsPageContainer>
|
</SettingsPageContainer>
|
||||||
</SubMenuTopBarContainer>
|
</SubMenuTopBarContainer>
|
||||||
|
|||||||
@ -16,12 +16,13 @@ const REVERT_PUBLIC_KEY = 'pk_live_a87fee8c-28c7-494f-99a3-996ff89f9918';
|
|||||||
export const SettingsCRMMigration = () => {
|
export const SettingsCRMMigration = () => {
|
||||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
||||||
return (
|
return (
|
||||||
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
<SubMenuTopBarContainer
|
||||||
|
Icon={IconSettings}
|
||||||
|
title={<Breadcrumb links={[{ children: 'Migrate' }]} />}
|
||||||
|
actionButton={<SettingsReadDocumentationButton />}
|
||||||
|
>
|
||||||
<SettingsPageContainer>
|
<SettingsPageContainer>
|
||||||
<SettingsHeaderContainer>
|
<SettingsHeaderContainer></SettingsHeaderContainer>
|
||||||
<Breadcrumb links={[{ children: 'Migrate' }]} />
|
|
||||||
<SettingsReadDocumentationButton />
|
|
||||||
</SettingsHeaderContainer>
|
|
||||||
<Section>
|
<Section>
|
||||||
<RevertConnect
|
<RevertConnect
|
||||||
config={{
|
config={{
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { H2Title, IconSettings } from 'twenty-ui';
|
import { H2Title, IconHierarchy2 } from 'twenty-ui';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { useCreateOneObjectMetadataItem } from '@/object-metadata/hooks/useCreateOneObjectMetadataItem';
|
import { useCreateOneObjectMetadataItem } from '@/object-metadata/hooks/useCreateOneObjectMetadataItem';
|
||||||
@ -70,25 +70,30 @@ export const SettingsNewObject = () => {
|
|||||||
return (
|
return (
|
||||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
<FormProvider {...formConfig}>
|
<FormProvider {...formConfig}>
|
||||||
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
<SubMenuTopBarContainer
|
||||||
|
Icon={IconHierarchy2}
|
||||||
|
title={
|
||||||
|
<Breadcrumb
|
||||||
|
links={[
|
||||||
|
{
|
||||||
|
children: 'Objects',
|
||||||
|
href: settingsObjectsPagePath,
|
||||||
|
},
|
||||||
|
{ children: 'New' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
actionButton={
|
||||||
|
<SaveAndCancelButtons
|
||||||
|
isSaveDisabled={!canSave}
|
||||||
|
isCancelDisabled={isSubmitting}
|
||||||
|
onCancel={() => navigate(settingsObjectsPagePath)}
|
||||||
|
onSave={formConfig.handleSubmit(handleSave)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
<SettingsPageContainer>
|
<SettingsPageContainer>
|
||||||
<SettingsHeaderContainer>
|
<SettingsHeaderContainer></SettingsHeaderContainer>
|
||||||
<Breadcrumb
|
|
||||||
links={[
|
|
||||||
{
|
|
||||||
children: 'Objects',
|
|
||||||
href: settingsObjectsPagePath,
|
|
||||||
},
|
|
||||||
{ children: 'New' },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<SaveAndCancelButtons
|
|
||||||
isSaveDisabled={!canSave}
|
|
||||||
isCancelDisabled={isSubmitting}
|
|
||||||
onCancel={() => navigate(settingsObjectsPagePath)}
|
|
||||||
onSave={formConfig.handleSubmit(handleSave)}
|
|
||||||
/>
|
|
||||||
</SettingsHeaderContainer>
|
|
||||||
<Section>
|
<Section>
|
||||||
<H2Title
|
<H2Title
|
||||||
title="About"
|
title="About"
|
||||||
|
|||||||
@ -1,12 +1,7 @@
|
|||||||
import styled from '@emotion/styled';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import { H2Title, IconPlus, IconSettings } from 'twenty-ui';
|
|
||||||
|
|
||||||
import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem';
|
import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem';
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
|
||||||
|
|
||||||
import { getDisabledFieldMetadataItems } from '@/object-metadata/utils/getDisabledFieldMetadataItems';
|
import { getDisabledFieldMetadataItems } from '@/object-metadata/utils/getDisabledFieldMetadataItems';
|
||||||
|
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||||
import { SettingsObjectSummaryCard } from '@/settings/data-model/object-details/components/SettingsObjectSummaryCard';
|
import { SettingsObjectSummaryCard } from '@/settings/data-model/object-details/components/SettingsObjectSummaryCard';
|
||||||
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
||||||
import { SettingsPath } from '@/types/SettingsPath';
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
@ -15,7 +10,10 @@ import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'
|
|||||||
import { Section } from '@/ui/layout/section/components/Section';
|
import { Section } from '@/ui/layout/section/components/Section';
|
||||||
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
|
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
|
||||||
import { UndecoratedLink } from '@/ui/navigation/link/components/UndecoratedLink';
|
import { UndecoratedLink } from '@/ui/navigation/link/components/UndecoratedLink';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
import { isNonEmptyArray } from '@sniptt/guards';
|
import { isNonEmptyArray } from '@sniptt/guards';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { H2Title, IconHierarchy2, IconPlus } from 'twenty-ui';
|
||||||
import { SettingsObjectFieldTable } from '~/pages/settings/data-model/SettingsObjectFieldTable';
|
import { SettingsObjectFieldTable } from '~/pages/settings/data-model/SettingsObjectFieldTable';
|
||||||
|
|
||||||
const StyledDiv = styled.div`
|
const StyledDiv = styled.div`
|
||||||
@ -49,14 +47,18 @@ export const SettingsObjectDetailPageContent = ({
|
|||||||
const shouldDisplayAddFieldButton = !objectMetadataItem.isRemote;
|
const shouldDisplayAddFieldButton = !objectMetadataItem.isRemote;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
<SubMenuTopBarContainer
|
||||||
<SettingsPageContainer>
|
Icon={IconHierarchy2}
|
||||||
|
title={
|
||||||
<Breadcrumb
|
<Breadcrumb
|
||||||
links={[
|
links={[
|
||||||
{ children: 'Objects', href: '/settings/objects' },
|
{ children: 'Objects', href: '/settings/objects' },
|
||||||
{ children: objectMetadataItem.labelPlural },
|
{ children: objectMetadataItem.labelPlural },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SettingsPageContainer>
|
||||||
<Section>
|
<Section>
|
||||||
<H2Title title="About" description="Manage your object" />
|
<H2Title title="About" description="Manage your object" />
|
||||||
<SettingsObjectSummaryCard
|
<SettingsObjectSummaryCard
|
||||||
|
|||||||
@ -5,12 +5,13 @@ import pick from 'lodash.pick';
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { H2Title, IconArchive, IconSettings } from 'twenty-ui';
|
import { H2Title, IconArchive, IconHierarchy2 } from 'twenty-ui';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||||
import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem';
|
import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem';
|
||||||
import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug';
|
import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug';
|
||||||
|
import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
|
||||||
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
||||||
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
|
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
|
||||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||||
@ -30,7 +31,6 @@ import { Button } from '@/ui/input/button/components/Button';
|
|||||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
||||||
import { Section } from '@/ui/layout/section/components/Section';
|
import { Section } from '@/ui/layout/section/components/Section';
|
||||||
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
|
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
|
||||||
import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
|
|
||||||
|
|
||||||
const objectEditFormSchema = z
|
const objectEditFormSchema = z
|
||||||
.object({})
|
.object({})
|
||||||
@ -108,22 +108,26 @@ export const SettingsObjectEdit = () => {
|
|||||||
return (
|
return (
|
||||||
<RecordFieldValueSelectorContextProvider>
|
<RecordFieldValueSelectorContextProvider>
|
||||||
<FormProvider {...formConfig}>
|
<FormProvider {...formConfig}>
|
||||||
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
<SubMenuTopBarContainer
|
||||||
|
Icon={IconHierarchy2}
|
||||||
|
title={
|
||||||
|
<Breadcrumb
|
||||||
|
links={[
|
||||||
|
{
|
||||||
|
children: 'Objects',
|
||||||
|
href: settingsObjectsPagePath,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
children: activeObjectMetadataItem.labelPlural,
|
||||||
|
href: `${settingsObjectsPagePath}/${objectSlug}`,
|
||||||
|
},
|
||||||
|
{ children: 'Edit' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
<SettingsPageContainer>
|
<SettingsPageContainer>
|
||||||
<SettingsHeaderContainer>
|
<SettingsHeaderContainer>
|
||||||
<Breadcrumb
|
|
||||||
links={[
|
|
||||||
{
|
|
||||||
children: 'Objects',
|
|
||||||
href: settingsObjectsPagePath,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
children: activeObjectMetadataItem.labelPlural,
|
|
||||||
href: `${settingsObjectsPagePath}/${objectSlug}`,
|
|
||||||
},
|
|
||||||
{ children: 'Edit' },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
{activeObjectMetadataItem.isCustom && (
|
{activeObjectMetadataItem.isCustom && (
|
||||||
<SaveAndCancelButtons
|
<SaveAndCancelButtons
|
||||||
isSaveDisabled={!canSave}
|
isSaveDisabled={!canSave}
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import pick from 'lodash.pick';
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { H2Title, IconArchive, IconSettings } from 'twenty-ui';
|
import { H2Title, IconArchive, IconHierarchy2 } from 'twenty-ui';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem';
|
import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem';
|
||||||
@ -172,19 +172,23 @@ export const SettingsObjectFieldEdit = () => {
|
|||||||
<RecordFieldValueSelectorContextProvider>
|
<RecordFieldValueSelectorContextProvider>
|
||||||
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||||
<FormProvider {...formConfig}>
|
<FormProvider {...formConfig}>
|
||||||
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
<SubMenuTopBarContainer
|
||||||
|
Icon={IconHierarchy2}
|
||||||
|
title={
|
||||||
|
<Breadcrumb
|
||||||
|
links={[
|
||||||
|
{ children: 'Objects', href: '/settings/objects' },
|
||||||
|
{
|
||||||
|
children: activeObjectMetadataItem.labelPlural,
|
||||||
|
href: `/settings/objects/${objectSlug}`,
|
||||||
|
},
|
||||||
|
{ children: activeMetadataField.label },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
<SettingsPageContainer>
|
<SettingsPageContainer>
|
||||||
<SettingsHeaderContainer>
|
<SettingsHeaderContainer>
|
||||||
<Breadcrumb
|
|
||||||
links={[
|
|
||||||
{ children: 'Objects', href: '/settings/objects' },
|
|
||||||
{
|
|
||||||
children: activeObjectMetadataItem.labelPlural,
|
|
||||||
href: `/settings/objects/${objectSlug}`,
|
|
||||||
},
|
|
||||||
{ children: activeMetadataField.label },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
{shouldDisplaySaveAndCancel && (
|
{shouldDisplaySaveAndCancel && (
|
||||||
<SaveAndCancelButtons
|
<SaveAndCancelButtons
|
||||||
isSaveDisabled={!canSave}
|
isSaveDisabled={!canSave}
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import { H2Title, IconPlus, IconSettings } from 'twenty-ui';
|
|||||||
|
|
||||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||||
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
||||||
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
|
|
||||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||||
|
|
||||||
import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem';
|
import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem';
|
||||||
@ -85,27 +84,31 @@ export const SettingsObjectNewFieldStep1 = () => {
|
|||||||
if (!activeObjectMetadataItem) return null;
|
if (!activeObjectMetadataItem) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
<SubMenuTopBarContainer
|
||||||
<SettingsPageContainer>
|
Icon={IconSettings}
|
||||||
<SettingsHeaderContainer>
|
title={
|
||||||
<Breadcrumb
|
<Breadcrumb
|
||||||
links={[
|
links={[
|
||||||
{ children: 'Objects', href: '/settings/objects' },
|
{ children: 'Objects', href: '/settings/objects' },
|
||||||
{
|
{
|
||||||
children: activeObjectMetadataItem.labelPlural,
|
children: activeObjectMetadataItem.labelPlural,
|
||||||
href: `/settings/objects/${objectSlug}`,
|
href: `/settings/objects/${objectSlug}`,
|
||||||
},
|
},
|
||||||
{ children: 'New Field' },
|
{ children: 'New Field' },
|
||||||
]}
|
]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
actionButton={
|
||||||
|
!activeObjectMetadataItem.isRemote && (
|
||||||
|
<SaveAndCancelButtons
|
||||||
|
isSaveDisabled={!canSave}
|
||||||
|
onCancel={() => navigate(`/settings/objects/${objectSlug}`)}
|
||||||
|
onSave={handleSave}
|
||||||
/>
|
/>
|
||||||
{!activeObjectMetadataItem.isRemote && (
|
)
|
||||||
<SaveAndCancelButtons
|
}
|
||||||
isSaveDisabled={!canSave}
|
>
|
||||||
onCancel={() => navigate(`/settings/objects/${objectSlug}`)}
|
<SettingsPageContainer>
|
||||||
onSave={handleSave}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</SettingsHeaderContainer>
|
|
||||||
<StyledSection>
|
<StyledSection>
|
||||||
<H2Title
|
<H2Title
|
||||||
title="Check deactivated fields"
|
title="Check deactivated fields"
|
||||||
|
|||||||
@ -1,12 +1,25 @@
|
|||||||
import { ReactFlowProvider } from 'reactflow';
|
import { ReactFlowProvider } from 'reactflow';
|
||||||
import { IconSettings } from 'twenty-ui';
|
|
||||||
|
|
||||||
import { SettingsDataModelOverview } from '@/settings/data-model/graph-overview/components/SettingsDataModelOverview';
|
import { SettingsDataModelOverview } from '@/settings/data-model/graph-overview/components/SettingsDataModelOverview';
|
||||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
||||||
|
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
|
||||||
|
import { IconHierarchy2 } from 'twenty-ui';
|
||||||
|
|
||||||
export const SettingsObjectOverview = () => {
|
export const SettingsObjectOverview = () => {
|
||||||
return (
|
return (
|
||||||
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
<SubMenuTopBarContainer
|
||||||
|
Icon={IconHierarchy2}
|
||||||
|
title={
|
||||||
|
<Breadcrumb
|
||||||
|
links={[
|
||||||
|
{ children: 'Data model', href: '/settings/objects' },
|
||||||
|
{
|
||||||
|
children: 'Overview',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
<ReactFlowProvider>
|
<ReactFlowProvider>
|
||||||
<SettingsDataModelOverview />
|
<SettingsDataModelOverview />
|
||||||
</ReactFlowProvider>
|
</ReactFlowProvider>
|
||||||
|
|||||||
@ -1,19 +1,12 @@
|
|||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import {
|
import { H2Title, IconChevronRight, IconHierarchy2, IconPlus } from 'twenty-ui';
|
||||||
H1Title,
|
|
||||||
H2Title,
|
|
||||||
IconChevronRight,
|
|
||||||
IconPlus,
|
|
||||||
IconSettings,
|
|
||||||
} from 'twenty-ui';
|
|
||||||
|
|
||||||
import { useDeleteOneObjectMetadataItem } from '@/object-metadata/hooks/useDeleteOneObjectMetadataItem';
|
import { useDeleteOneObjectMetadataItem } from '@/object-metadata/hooks/useDeleteOneObjectMetadataItem';
|
||||||
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
|
||||||
import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem';
|
import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem';
|
||||||
import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug';
|
import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug';
|
||||||
import { useCombinedGetTotalCount } from '@/object-record/multiple-objects/hooks/useCombinedGetTotalCount';
|
import { useCombinedGetTotalCount } from '@/object-record/multiple-objects/hooks/useCombinedGetTotalCount';
|
||||||
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
|
|
||||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||||
import {
|
import {
|
||||||
SettingsObjectMetadataItemTableRow,
|
SettingsObjectMetadataItemTableRow,
|
||||||
@ -42,10 +35,6 @@ const StyledIconChevronRight = styled(IconChevronRight)`
|
|||||||
color: ${({ theme }) => theme.font.color.tertiary};
|
color: ${({ theme }) => theme.font.color.tertiary};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const StyledH1Title = styled(H1Title)`
|
|
||||||
margin-bottom: 0;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const SettingsObjects = () => {
|
export const SettingsObjects = () => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
@ -115,19 +104,21 @@ export const SettingsObjects = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
<SubMenuTopBarContainer
|
||||||
|
Icon={IconHierarchy2}
|
||||||
|
title="Data model"
|
||||||
|
actionButton={
|
||||||
|
<UndecoratedLink to={getSettingsPagePath(SettingsPath.NewObject)}>
|
||||||
|
<Button
|
||||||
|
Icon={IconPlus}
|
||||||
|
title="Add object"
|
||||||
|
accent="blue"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</UndecoratedLink>
|
||||||
|
}
|
||||||
|
>
|
||||||
<SettingsPageContainer>
|
<SettingsPageContainer>
|
||||||
<SettingsHeaderContainer>
|
|
||||||
<StyledH1Title title="Objects" />
|
|
||||||
<UndecoratedLink to={getSettingsPagePath(SettingsPath.NewObject)}>
|
|
||||||
<Button
|
|
||||||
Icon={IconPlus}
|
|
||||||
title="Add object"
|
|
||||||
accent="blue"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</UndecoratedLink>
|
|
||||||
</SettingsHeaderContainer>
|
|
||||||
<>
|
<>
|
||||||
<SettingsObjectCoverImage />
|
<SettingsObjectCoverImage />
|
||||||
<Section>
|
<Section>
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { H2Title, IconPlus, IconSettings } from 'twenty-ui';
|
import { H2Title, IconCode, IconPlus } from 'twenty-ui';
|
||||||
|
|
||||||
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
|
|
||||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||||
import { SettingsApiKeysTable } from '@/settings/developers/components/SettingsApiKeysTable';
|
import { SettingsApiKeysTable } from '@/settings/developers/components/SettingsApiKeysTable';
|
||||||
import { SettingsReadDocumentationButton } from '@/settings/developers/components/SettingsReadDocumentationButton';
|
import { SettingsReadDocumentationButton } from '@/settings/developers/components/SettingsReadDocumentationButton';
|
||||||
@ -9,7 +8,6 @@ import { SettingsWebhooksTable } from '@/settings/developers/components/Settings
|
|||||||
import { Button } from '@/ui/input/button/components/Button';
|
import { Button } from '@/ui/input/button/components/Button';
|
||||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
||||||
import { Section } from '@/ui/layout/section/components/Section';
|
import { Section } from '@/ui/layout/section/components/Section';
|
||||||
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
|
|
||||||
|
|
||||||
const StyledButtonContainer = styled.div`
|
const StyledButtonContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -19,12 +17,12 @@ const StyledButtonContainer = styled.div`
|
|||||||
|
|
||||||
export const SettingsDevelopers = () => {
|
export const SettingsDevelopers = () => {
|
||||||
return (
|
return (
|
||||||
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
<SubMenuTopBarContainer
|
||||||
|
Icon={IconCode}
|
||||||
|
title="Developers"
|
||||||
|
actionButton={<SettingsReadDocumentationButton />}
|
||||||
|
>
|
||||||
<SettingsPageContainer>
|
<SettingsPageContainer>
|
||||||
<SettingsHeaderContainer>
|
|
||||||
<Breadcrumb links={[{ children: 'Developers' }]} />
|
|
||||||
<SettingsReadDocumentationButton />
|
|
||||||
</SettingsHeaderContainer>
|
|
||||||
<Section>
|
<Section>
|
||||||
<H2Title
|
<H2Title
|
||||||
title="API keys"
|
title="API keys"
|
||||||
|
|||||||
@ -4,16 +4,16 @@ import { DateTime } from 'luxon';
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
import { H2Title, IconRepeat, IconSettings, IconTrash } from 'twenty-ui';
|
import { H2Title, IconCode, IconRepeat, IconTrash } from 'twenty-ui';
|
||||||
|
|
||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
||||||
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||||
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
|
|
||||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||||
import { ApiKeyInput } from '@/settings/developers/components/ApiKeyInput';
|
import { ApiKeyInput } from '@/settings/developers/components/ApiKeyInput';
|
||||||
import { ApiKeyNameInput } from '@/settings/developers/components/ApiKeyNameInput';
|
import { ApiKeyNameInput } from '@/settings/developers/components/ApiKeyNameInput';
|
||||||
|
import { apiKeyTokenState } from '@/settings/developers/states/generatedApiKeyTokenState';
|
||||||
import { ApiKey } from '@/settings/developers/types/api-key/ApiKey';
|
import { ApiKey } from '@/settings/developers/types/api-key/ApiKey';
|
||||||
import { computeNewExpirationDate } from '@/settings/developers/utils/compute-new-expiration-date';
|
import { computeNewExpirationDate } from '@/settings/developers/utils/compute-new-expiration-date';
|
||||||
import { formatExpiration } from '@/settings/developers/utils/format-expiration';
|
import { formatExpiration } from '@/settings/developers/utils/format-expiration';
|
||||||
@ -24,7 +24,6 @@ import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'
|
|||||||
import { Section } from '@/ui/layout/section/components/Section';
|
import { Section } from '@/ui/layout/section/components/Section';
|
||||||
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
|
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
|
||||||
import { useGenerateApiKeyTokenMutation } from '~/generated/graphql';
|
import { useGenerateApiKeyTokenMutation } from '~/generated/graphql';
|
||||||
import { apiKeyTokenState } from '@/settings/developers/states/generatedApiKeyTokenState';
|
|
||||||
|
|
||||||
const StyledInfo = styled.span`
|
const StyledInfo = styled.span`
|
||||||
color: ${({ theme }) => theme.font.color.light};
|
color: ${({ theme }) => theme.font.color.light};
|
||||||
@ -121,16 +120,18 @@ export const SettingsDevelopersApiKeyDetail = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{apiKeyData?.name && (
|
{apiKeyData?.name && (
|
||||||
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
<SubMenuTopBarContainer
|
||||||
|
Icon={IconCode}
|
||||||
|
title={
|
||||||
|
<Breadcrumb
|
||||||
|
links={[
|
||||||
|
{ children: 'Developers', href: '/settings/developers' },
|
||||||
|
{ children: `${apiKeyName} API Key` },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
<SettingsPageContainer>
|
<SettingsPageContainer>
|
||||||
<SettingsHeaderContainer>
|
|
||||||
<Breadcrumb
|
|
||||||
links={[
|
|
||||||
{ children: 'Developers', href: '/settings/developers' },
|
|
||||||
{ children: `${apiKeyName} API Key` },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</SettingsHeaderContainer>
|
|
||||||
<Section>
|
<Section>
|
||||||
{apiKeyToken ? (
|
{apiKeyToken ? (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@ -1,25 +1,24 @@
|
|||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { H2Title, IconSettings } from 'twenty-ui';
|
import { H2Title, IconCode } from 'twenty-ui';
|
||||||
|
|
||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
||||||
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
||||||
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
|
|
||||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||||
import { EXPIRATION_DATES } from '@/settings/developers/constants/ExpirationDates';
|
import { EXPIRATION_DATES } from '@/settings/developers/constants/ExpirationDates';
|
||||||
|
import { apiKeyTokenState } from '@/settings/developers/states/generatedApiKeyTokenState';
|
||||||
import { ApiKey } from '@/settings/developers/types/api-key/ApiKey';
|
import { ApiKey } from '@/settings/developers/types/api-key/ApiKey';
|
||||||
import { Select } from '@/ui/input/components/Select';
|
import { Select } from '@/ui/input/components/Select';
|
||||||
import { TextInput } from '@/ui/input/components/TextInput';
|
import { TextInput } from '@/ui/input/components/TextInput';
|
||||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
||||||
import { Section } from '@/ui/layout/section/components/Section';
|
import { Section } from '@/ui/layout/section/components/Section';
|
||||||
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
|
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
|
||||||
|
import { useSetRecoilState } from 'recoil';
|
||||||
|
import { Key } from 'ts-key-enum';
|
||||||
import { useGenerateApiKeyTokenMutation } from '~/generated/graphql';
|
import { useGenerateApiKeyTokenMutation } from '~/generated/graphql';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
import { Key } from 'ts-key-enum';
|
|
||||||
import { apiKeyTokenState } from '@/settings/developers/states/generatedApiKeyTokenState';
|
|
||||||
import { useSetRecoilState } from 'recoil';
|
|
||||||
|
|
||||||
export const SettingsDevelopersApiKeysNew = () => {
|
export const SettingsDevelopersApiKeysNew = () => {
|
||||||
const [generateOneApiKeyToken] = useGenerateApiKeyTokenMutation();
|
const [generateOneApiKeyToken] = useGenerateApiKeyTokenMutation();
|
||||||
@ -64,23 +63,27 @@ export const SettingsDevelopersApiKeysNew = () => {
|
|||||||
};
|
};
|
||||||
const canSave = !!formValues.name && createOneApiKey;
|
const canSave = !!formValues.name && createOneApiKey;
|
||||||
return (
|
return (
|
||||||
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
<SubMenuTopBarContainer
|
||||||
|
Icon={IconCode}
|
||||||
|
title={
|
||||||
|
<Breadcrumb
|
||||||
|
links={[
|
||||||
|
{ children: 'Developers', href: '/settings/developers' },
|
||||||
|
{ children: 'New API Key' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
actionButton={
|
||||||
|
<SaveAndCancelButtons
|
||||||
|
isSaveDisabled={!canSave}
|
||||||
|
onCancel={() => {
|
||||||
|
navigate('/settings/developers');
|
||||||
|
}}
|
||||||
|
onSave={handleSave}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
<SettingsPageContainer>
|
<SettingsPageContainer>
|
||||||
<SettingsHeaderContainer>
|
|
||||||
<Breadcrumb
|
|
||||||
links={[
|
|
||||||
{ children: 'Developers', href: '/settings/developers' },
|
|
||||||
{ children: 'New API Key' },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<SaveAndCancelButtons
|
|
||||||
isSaveDisabled={!canSave}
|
|
||||||
onCancel={() => {
|
|
||||||
navigate('/settings/developers');
|
|
||||||
}}
|
|
||||||
onSave={handleSave}
|
|
||||||
/>
|
|
||||||
</SettingsHeaderContainer>
|
|
||||||
<Section>
|
<Section>
|
||||||
<H2Title title="Name" description="Name of your API key" />
|
<H2Title title="Name" description="Name of your API key" />
|
||||||
<TextInput
|
<TextInput
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { H2Title, IconSettings, IconTrash } from 'twenty-ui';
|
import { H2Title, IconCode, IconTrash } from 'twenty-ui';
|
||||||
|
|
||||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
@ -9,7 +9,6 @@ import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
|
|||||||
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||||
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
||||||
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
|
|
||||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||||
import { Webhook } from '@/settings/developers/types/webhook/Webhook';
|
import { Webhook } from '@/settings/developers/types/webhook/Webhook';
|
||||||
import { Button } from '@/ui/input/button/components/Button';
|
import { Button } from '@/ui/input/button/components/Button';
|
||||||
@ -88,23 +87,27 @@ export const SettingsDevelopersWebhooksDetail = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{webhookData?.targetUrl && (
|
{webhookData?.targetUrl && (
|
||||||
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
<SubMenuTopBarContainer
|
||||||
|
Icon={IconCode}
|
||||||
|
title={
|
||||||
|
<Breadcrumb
|
||||||
|
links={[
|
||||||
|
{ children: 'Developers', href: '/settings/developers' },
|
||||||
|
{ children: 'Webhook' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
actionButton={
|
||||||
|
<SaveAndCancelButtons
|
||||||
|
isSaveDisabled={!isDirty}
|
||||||
|
onCancel={() => {
|
||||||
|
navigate('/settings/developers');
|
||||||
|
}}
|
||||||
|
onSave={handleSave}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
<SettingsPageContainer>
|
<SettingsPageContainer>
|
||||||
<SettingsHeaderContainer>
|
|
||||||
<Breadcrumb
|
|
||||||
links={[
|
|
||||||
{ children: 'Developers', href: '/settings/developers' },
|
|
||||||
{ children: 'Webhook' },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<SaveAndCancelButtons
|
|
||||||
isSaveDisabled={!isDirty}
|
|
||||||
onCancel={() => {
|
|
||||||
navigate('/settings/developers');
|
|
||||||
}}
|
|
||||||
onSave={handleSave}
|
|
||||||
/>
|
|
||||||
</SettingsHeaderContainer>
|
|
||||||
<Section>
|
<Section>
|
||||||
<H2Title
|
<H2Title
|
||||||
title="Endpoint URL"
|
title="Endpoint URL"
|
||||||
|
|||||||
@ -1,11 +1,10 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { H2Title, IconSettings } from 'twenty-ui';
|
import { H2Title, IconCode } from 'twenty-ui';
|
||||||
|
|
||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
||||||
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
||||||
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
|
|
||||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||||
import { Webhook } from '@/settings/developers/types/webhook/Webhook';
|
import { Webhook } from '@/settings/developers/types/webhook/Webhook';
|
||||||
import { TextInput } from '@/ui/input/components/TextInput';
|
import { TextInput } from '@/ui/input/components/TextInput';
|
||||||
@ -35,23 +34,27 @@ export const SettingsDevelopersWebhooksNew = () => {
|
|||||||
};
|
};
|
||||||
const canSave = !!formValues.targetUrl && createOneWebhook;
|
const canSave = !!formValues.targetUrl && createOneWebhook;
|
||||||
return (
|
return (
|
||||||
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
<SubMenuTopBarContainer
|
||||||
|
Icon={IconCode}
|
||||||
|
title={
|
||||||
|
<Breadcrumb
|
||||||
|
links={[
|
||||||
|
{ children: 'Developers', href: '/settings/developers' },
|
||||||
|
{ children: 'New webhook' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
actionButton={
|
||||||
|
<SaveAndCancelButtons
|
||||||
|
isSaveDisabled={!canSave}
|
||||||
|
onCancel={() => {
|
||||||
|
navigate('/settings/developers');
|
||||||
|
}}
|
||||||
|
onSave={handleSave}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
<SettingsPageContainer>
|
<SettingsPageContainer>
|
||||||
<SettingsHeaderContainer>
|
|
||||||
<Breadcrumb
|
|
||||||
links={[
|
|
||||||
{ children: 'Developers', href: '/settings/developers' },
|
|
||||||
{ children: 'New webhook' },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<SaveAndCancelButtons
|
|
||||||
isSaveDisabled={!canSave}
|
|
||||||
onCancel={() => {
|
|
||||||
navigate('/settings/developers');
|
|
||||||
}}
|
|
||||||
onSave={handleSave}
|
|
||||||
/>
|
|
||||||
</SettingsHeaderContainer>
|
|
||||||
<Section>
|
<Section>
|
||||||
<H2Title
|
<H2Title
|
||||||
title="Endpoint URL"
|
title="Endpoint URL"
|
||||||
|
|||||||
@ -42,8 +42,9 @@ export const SettingsIntegrationDatabase = () => {
|
|||||||
if (!isIntegrationAvailable) return null;
|
if (!isIntegrationAvailable) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
<SubMenuTopBarContainer
|
||||||
<SettingsPageContainer>
|
Icon={IconSettings}
|
||||||
|
title={
|
||||||
<Breadcrumb
|
<Breadcrumb
|
||||||
links={[
|
links={[
|
||||||
{
|
{
|
||||||
@ -53,6 +54,9 @@ export const SettingsIntegrationDatabase = () => {
|
|||||||
{ children: integration.text },
|
{ children: integration.text },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SettingsPageContainer>
|
||||||
<SettingsIntegrationPreview
|
<SettingsIntegrationPreview
|
||||||
integrationLogoUrl={integration.from.image}
|
integrationLogoUrl={integration.from.image}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -3,10 +3,21 @@ import { IconSettings } from 'twenty-ui';
|
|||||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||||
import { SettingsIntegrationEditDatabaseConnectionContainer } from '@/settings/integrations/database-connection/components/SettingsIntegrationEditDatabaseConnectionContainer';
|
import { SettingsIntegrationEditDatabaseConnectionContainer } from '@/settings/integrations/database-connection/components/SettingsIntegrationEditDatabaseConnectionContainer';
|
||||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
||||||
|
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
|
||||||
|
|
||||||
export const SettingsIntegrationEditDatabaseConnection = () => {
|
export const SettingsIntegrationEditDatabaseConnection = () => {
|
||||||
return (
|
return (
|
||||||
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
<SubMenuTopBarContainer
|
||||||
|
Icon={IconSettings}
|
||||||
|
title={
|
||||||
|
<Breadcrumb
|
||||||
|
links={[
|
||||||
|
// TODO
|
||||||
|
{ children: 'Edit connection' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
<SettingsPageContainer>
|
<SettingsPageContainer>
|
||||||
<SettingsIntegrationEditDatabaseConnectionContainer />
|
<SettingsIntegrationEditDatabaseConnectionContainer />
|
||||||
</SettingsPageContainer>
|
</SettingsPageContainer>
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import { z } from 'zod';
|
|||||||
import { useCreateOneDatabaseConnection } from '@/databases/hooks/useCreateOneDatabaseConnection';
|
import { useCreateOneDatabaseConnection } from '@/databases/hooks/useCreateOneDatabaseConnection';
|
||||||
import { getForeignDataWrapperType } from '@/databases/utils/getForeignDataWrapperType';
|
import { getForeignDataWrapperType } from '@/databases/utils/getForeignDataWrapperType';
|
||||||
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
||||||
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
|
|
||||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||||
import {
|
import {
|
||||||
SettingsIntegrationDatabaseConnectionForm,
|
SettingsIntegrationDatabaseConnectionForm,
|
||||||
@ -132,34 +131,38 @@ export const SettingsIntegrationNewDatabaseConnection = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
<SubMenuTopBarContainer
|
||||||
|
Icon={IconSettings}
|
||||||
|
title={
|
||||||
|
<Breadcrumb
|
||||||
|
links={[
|
||||||
|
{
|
||||||
|
children: 'Integrations',
|
||||||
|
href: settingsIntegrationsPagePath,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
children: integration.text,
|
||||||
|
href: `${settingsIntegrationsPagePath}/${databaseKey}`,
|
||||||
|
},
|
||||||
|
{ children: 'New' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
actionButton={
|
||||||
|
<SaveAndCancelButtons
|
||||||
|
isSaveDisabled={!canSave}
|
||||||
|
onCancel={() =>
|
||||||
|
navigate(`${settingsIntegrationsPagePath}/${databaseKey}`)
|
||||||
|
}
|
||||||
|
onSave={handleSave}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
<SettingsPageContainer>
|
<SettingsPageContainer>
|
||||||
<FormProvider
|
<FormProvider
|
||||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
{...formConfig}
|
{...formConfig}
|
||||||
>
|
>
|
||||||
<SettingsHeaderContainer>
|
|
||||||
<Breadcrumb
|
|
||||||
links={[
|
|
||||||
{
|
|
||||||
children: 'Integrations',
|
|
||||||
href: settingsIntegrationsPagePath,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
children: integration.text,
|
|
||||||
href: `${settingsIntegrationsPagePath}/${databaseKey}`,
|
|
||||||
},
|
|
||||||
{ children: 'New' },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<SaveAndCancelButtons
|
|
||||||
isSaveDisabled={!canSave}
|
|
||||||
onCancel={() =>
|
|
||||||
navigate(`${settingsIntegrationsPagePath}/${databaseKey}`)
|
|
||||||
}
|
|
||||||
onSave={handleSave}
|
|
||||||
/>
|
|
||||||
</SettingsHeaderContainer>
|
|
||||||
<Section>
|
<Section>
|
||||||
<H2Title
|
<H2Title
|
||||||
title="Connect a new database"
|
title="Connect a new database"
|
||||||
|
|||||||
@ -1,18 +1,15 @@
|
|||||||
import { IconSettings } from 'twenty-ui';
|
|
||||||
|
|
||||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||||
import { SettingsIntegrationGroup } from '@/settings/integrations/components/SettingsIntegrationGroup';
|
import { SettingsIntegrationGroup } from '@/settings/integrations/components/SettingsIntegrationGroup';
|
||||||
import { useSettingsIntegrationCategories } from '@/settings/integrations/hooks/useSettingsIntegrationCategories';
|
import { useSettingsIntegrationCategories } from '@/settings/integrations/hooks/useSettingsIntegrationCategories';
|
||||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
||||||
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
|
import { IconApps } from 'twenty-ui';
|
||||||
|
|
||||||
export const SettingsIntegrations = () => {
|
export const SettingsIntegrations = () => {
|
||||||
const integrationCategories = useSettingsIntegrationCategories();
|
const integrationCategories = useSettingsIntegrationCategories();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
<SubMenuTopBarContainer Icon={IconApps} title="Integrations">
|
||||||
<SettingsPageContainer>
|
<SettingsPageContainer>
|
||||||
<Breadcrumb links={[{ children: 'Integrations' }]} />
|
|
||||||
{integrationCategories.map((group) => (
|
{integrationCategories.map((group) => (
|
||||||
<SettingsIntegrationGroup key={group.key} integrationGroup={group} />
|
<SettingsIntegrationGroup key={group.key} integrationGroup={group} />
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import styled from '@emotion/styled';
|
import { H2Title, IconColorSwatch } from 'twenty-ui';
|
||||||
import { H1Title, H2Title, IconSettings } from 'twenty-ui';
|
|
||||||
|
|
||||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||||
import { ColorSchemePicker } from '@/ui/input/color-scheme/components/ColorSchemePicker';
|
import { ColorSchemePicker } from '@/ui/input/color-scheme/components/ColorSchemePicker';
|
||||||
@ -8,17 +7,12 @@ import { Section } from '@/ui/layout/section/components/Section';
|
|||||||
import { useColorScheme } from '@/ui/theme/hooks/useColorScheme';
|
import { useColorScheme } from '@/ui/theme/hooks/useColorScheme';
|
||||||
import { DateTimeSettings } from '~/pages/settings/profile/appearance/components/DateTimeSettings';
|
import { DateTimeSettings } from '~/pages/settings/profile/appearance/components/DateTimeSettings';
|
||||||
|
|
||||||
const StyledH1Title = styled(H1Title)`
|
|
||||||
margin-bottom: 0;
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const SettingsAppearance = () => {
|
export const SettingsAppearance = () => {
|
||||||
const { colorScheme, setColorScheme } = useColorScheme();
|
const { colorScheme, setColorScheme } = useColorScheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
<SubMenuTopBarContainer Icon={IconColorSwatch} title="Appearance">
|
||||||
<SettingsPageContainer>
|
<SettingsPageContainer>
|
||||||
<StyledH1Title title="Appearance" />
|
|
||||||
<Section>
|
<Section>
|
||||||
<H2Title title="Theme" />
|
<H2Title title="Theme" />
|
||||||
<ColorSchemePicker value={colorScheme} onChange={setColorScheme} />
|
<ColorSchemePicker value={colorScheme} onChange={setColorScheme} />
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { useResetRecoilState } from 'recoil';
|
import { settingsServerlessFunctionCodeEditorOutputParamsState } from '@/settings/serverless-functions/states/settingsServerlessFunctionCodeEditorOutputParamsState';
|
||||||
import { settingsServerlessFunctionInputState } from '@/settings/serverless-functions/states/settingsServerlessFunctionInputState';
|
import { settingsServerlessFunctionInputState } from '@/settings/serverless-functions/states/settingsServerlessFunctionInputState';
|
||||||
import { settingsServerlessFunctionOutputState } from '@/settings/serverless-functions/states/settingsServerlessFunctionOutputState';
|
import { settingsServerlessFunctionOutputState } from '@/settings/serverless-functions/states/settingsServerlessFunctionOutputState';
|
||||||
import { settingsServerlessFunctionCodeEditorOutputParamsState } from '@/settings/serverless-functions/states/settingsServerlessFunctionCodeEditorOutputParamsState';
|
import { useResetRecoilState } from 'recoil';
|
||||||
|
|
||||||
export const ResetServerlessFunctionStatesEffect = () => {
|
export const ResetServerlessFunctionStatesEffect = () => {
|
||||||
const resetSettingsServerlessFunctionInput = useResetRecoilState(
|
const resetSettingsServerlessFunctionInput = useResetRecoilState(
|
||||||
|
|||||||
@ -1,25 +1,24 @@
|
|||||||
import { useParams } from 'react-router-dom';
|
|
||||||
import { IconCode, IconSettings, IconTestPipe } from 'twenty-ui';
|
|
||||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
|
||||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||||
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
|
import { SettingsServerlessFunctionCodeEditorTab } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionCodeEditorTab';
|
||||||
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
|
import { SettingsServerlessFunctionSettingsTab } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionSettingsTab';
|
||||||
|
import { SettingsServerlessFunctionTestTab } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTestTab';
|
||||||
|
import { SettingsServerlessFunctionTestTabEffect } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTestTabEffect';
|
||||||
|
import { useExecuteOneServerlessFunction } from '@/settings/serverless-functions/hooks/useExecuteOneServerlessFunction';
|
||||||
|
import { useServerlessFunctionUpdateFormState } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState';
|
||||||
|
import { useUpdateOneServerlessFunction } from '@/settings/serverless-functions/hooks/useUpdateOneServerlessFunction';
|
||||||
|
import { settingsServerlessFunctionInputState } from '@/settings/serverless-functions/states/settingsServerlessFunctionInputState';
|
||||||
|
import { settingsServerlessFunctionOutputState } from '@/settings/serverless-functions/states/settingsServerlessFunctionOutputState';
|
||||||
|
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||||
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||||
|
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
||||||
import { Section } from '@/ui/layout/section/components/Section';
|
import { Section } from '@/ui/layout/section/components/Section';
|
||||||
import { TabList } from '@/ui/layout/tab/components/TabList';
|
import { TabList } from '@/ui/layout/tab/components/TabList';
|
||||||
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
|
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
|
||||||
|
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||||
import { SettingsServerlessFunctionCodeEditorTab } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionCodeEditorTab';
|
import { IconCode, IconFunction, IconSettings, IconTestPipe } from 'twenty-ui';
|
||||||
import { SettingsServerlessFunctionSettingsTab } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionSettingsTab';
|
|
||||||
import { useServerlessFunctionUpdateFormState } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState';
|
|
||||||
import { SettingsServerlessFunctionTestTab } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTestTab';
|
|
||||||
import { useExecuteOneServerlessFunction } from '@/settings/serverless-functions/hooks/useExecuteOneServerlessFunction';
|
|
||||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
|
||||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
|
||||||
import { useUpdateOneServerlessFunction } from '@/settings/serverless-functions/hooks/useUpdateOneServerlessFunction';
|
|
||||||
import { useDebouncedCallback } from 'use-debounce';
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
import { SettingsServerlessFunctionTestTabEffect } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTestTabEffect';
|
|
||||||
import { settingsServerlessFunctionOutputState } from '@/settings/serverless-functions/states/settingsServerlessFunctionOutputState';
|
|
||||||
import { settingsServerlessFunctionInputState } from '@/settings/serverless-functions/states/settingsServerlessFunctionInputState';
|
|
||||||
|
|
||||||
const TAB_LIST_COMPONENT_ID = 'serverless-function-detail';
|
const TAB_LIST_COMPONENT_ID = 'serverless-function-detail';
|
||||||
|
|
||||||
@ -145,16 +144,18 @@ export const SettingsServerlessFunctionDetail = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
formValues.name && (
|
formValues.name && (
|
||||||
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
<SubMenuTopBarContainer
|
||||||
|
Icon={IconFunction}
|
||||||
|
title={
|
||||||
|
<Breadcrumb
|
||||||
|
links={[
|
||||||
|
{ children: 'Functions', href: '/settings/functions' },
|
||||||
|
{ children: `${formValues.name}` },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
<SettingsPageContainer>
|
<SettingsPageContainer>
|
||||||
<SettingsHeaderContainer>
|
|
||||||
<Breadcrumb
|
|
||||||
links={[
|
|
||||||
{ children: 'Functions', href: '/settings/functions' },
|
|
||||||
{ children: `${formValues.name}` },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</SettingsHeaderContainer>
|
|
||||||
<Section>
|
<Section>
|
||||||
<TabList tabListId={TAB_LIST_COMPONENT_ID} tabs={tabs} />
|
<TabList tabListId={TAB_LIST_COMPONENT_ID} tabs={tabs} />
|
||||||
</Section>
|
</Section>
|
||||||
|
|||||||
@ -1,31 +1,36 @@
|
|||||||
import { IconPlus, IconSettings } from 'twenty-ui';
|
|
||||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
|
||||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
|
||||||
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
|
|
||||||
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
|
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
|
||||||
import { Section } from '@/ui/layout/section/components/Section';
|
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||||
import { SettingsServerlessFunctionsTable } from '@/settings/serverless-functions/components/SettingsServerlessFunctionsTable';
|
import { SettingsServerlessFunctionsTable } from '@/settings/serverless-functions/components/SettingsServerlessFunctionsTable';
|
||||||
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
||||||
import { SettingsPath } from '@/types/SettingsPath';
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
import { Button } from '@/ui/input/button/components/Button';
|
import { Button } from '@/ui/input/button/components/Button';
|
||||||
|
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
||||||
|
import { Section } from '@/ui/layout/section/components/Section';
|
||||||
|
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
|
||||||
import { UndecoratedLink } from '@/ui/navigation/link/components/UndecoratedLink';
|
import { UndecoratedLink } from '@/ui/navigation/link/components/UndecoratedLink';
|
||||||
|
import { IconFunction, IconPlus } from 'twenty-ui';
|
||||||
|
|
||||||
export const SettingsServerlessFunctions = () => {
|
export const SettingsServerlessFunctions = () => {
|
||||||
return (
|
return (
|
||||||
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
<SubMenuTopBarContainer
|
||||||
|
Icon={IconFunction}
|
||||||
|
title="Functions"
|
||||||
|
actionButton={
|
||||||
|
<UndecoratedLink
|
||||||
|
to={getSettingsPagePath(SettingsPath.NewServerlessFunction)}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
Icon={IconPlus}
|
||||||
|
title="New Function"
|
||||||
|
accent="blue"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</UndecoratedLink>
|
||||||
|
}
|
||||||
|
>
|
||||||
<SettingsPageContainer>
|
<SettingsPageContainer>
|
||||||
<SettingsHeaderContainer>
|
<SettingsHeaderContainer>
|
||||||
<Breadcrumb links={[{ children: 'Functions' }]} />
|
<Breadcrumb links={[{ children: 'Functions' }]} />
|
||||||
<UndecoratedLink
|
|
||||||
to={getSettingsPagePath(SettingsPath.NewServerlessFunction)}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
Icon={IconPlus}
|
|
||||||
title="New Function"
|
|
||||||
accent="blue"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</UndecoratedLink>
|
|
||||||
</SettingsHeaderContainer>
|
</SettingsHeaderContainer>
|
||||||
<Section>
|
<Section>
|
||||||
<SettingsServerlessFunctionsTable />
|
<SettingsServerlessFunctionsTable />
|
||||||
|
|||||||
@ -1,19 +1,18 @@
|
|||||||
import { IconSettings } from 'twenty-ui';
|
|
||||||
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
|
|
||||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
|
||||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
|
||||||
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
|
|
||||||
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
||||||
|
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||||
|
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
||||||
|
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { useCreateOneServerlessFunction } from '@/settings/serverless-functions/hooks/useCreateOneServerlessFunction';
|
|
||||||
import { DEFAULT_CODE } from '@/ui/input/code-editor/components/CodeEditor';
|
|
||||||
import { ServerlessFunctionNewFormValues } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState';
|
|
||||||
import { SettingsServerlessFunctionNewForm } from '@/settings/serverless-functions/components/SettingsServerlessFunctionNewForm';
|
import { SettingsServerlessFunctionNewForm } from '@/settings/serverless-functions/components/SettingsServerlessFunctionNewForm';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { useCreateOneServerlessFunction } from '@/settings/serverless-functions/hooks/useCreateOneServerlessFunction';
|
||||||
import { useState } from 'react';
|
import { ServerlessFunctionNewFormValues } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState';
|
||||||
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
||||||
import { SettingsPath } from '@/types/SettingsPath';
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
|
import { DEFAULT_CODE } from '@/ui/input/code-editor/components/CodeEditor';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { IconFunction } from 'twenty-ui';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
export const SettingsServerlessFunctionsNew = () => {
|
export const SettingsServerlessFunctionsNew = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -54,23 +53,27 @@ export const SettingsServerlessFunctionsNew = () => {
|
|||||||
const canSave = !!formValues.name && createOneServerlessFunction;
|
const canSave = !!formValues.name && createOneServerlessFunction;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
<SubMenuTopBarContainer
|
||||||
|
Icon={IconFunction}
|
||||||
|
title={
|
||||||
|
<Breadcrumb
|
||||||
|
links={[
|
||||||
|
{ children: 'Functions', href: '/settings/functions' },
|
||||||
|
{ children: 'New' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
actionButton={
|
||||||
|
<SaveAndCancelButtons
|
||||||
|
isSaveDisabled={!canSave}
|
||||||
|
onCancel={() => {
|
||||||
|
navigate('/settings/functions');
|
||||||
|
}}
|
||||||
|
onSave={handleSave}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
<SettingsPageContainer>
|
<SettingsPageContainer>
|
||||||
<SettingsHeaderContainer>
|
|
||||||
<Breadcrumb
|
|
||||||
links={[
|
|
||||||
{ children: 'Functions', href: '/settings/functions' },
|
|
||||||
{ children: 'New' },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<SaveAndCancelButtons
|
|
||||||
isSaveDisabled={!canSave}
|
|
||||||
onCancel={() => {
|
|
||||||
navigate('/settings/functions');
|
|
||||||
}}
|
|
||||||
onSave={handleSave}
|
|
||||||
/>
|
|
||||||
</SettingsHeaderContainer>
|
|
||||||
<SettingsServerlessFunctionNewForm
|
<SettingsServerlessFunctionNewForm
|
||||||
formValues={formValues}
|
formValues={formValues}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
|||||||
@ -49,10 +49,6 @@ export const getPageTitleFromPath = (pathname: string): string => {
|
|||||||
return 'Create Workspace';
|
return 'Create Workspace';
|
||||||
case AppPath.CreateProfile:
|
case AppPath.CreateProfile:
|
||||||
return 'Create Profile';
|
return 'Create Profile';
|
||||||
case AppPath.TasksPage:
|
|
||||||
return 'Tasks';
|
|
||||||
case AppPath.OpportunitiesPage:
|
|
||||||
return 'Opportunities';
|
|
||||||
case SettingsPathPrefixes.Appearance:
|
case SettingsPathPrefixes.Appearance:
|
||||||
return SettingsPageTitles.Appearance;
|
return SettingsPageTitles.Appearance;
|
||||||
case SettingsPathPrefixes.Accounts:
|
case SettingsPathPrefixes.Accounts:
|
||||||
|
|||||||
@ -104,8 +104,5 @@ export default defineConfig(({ command, mode }) => {
|
|||||||
localsConvention: 'camelCaseOnly',
|
localsConvention: 'camelCaseOnly',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
optimizeDeps: {
|
|
||||||
exclude: ['@tabler/icons-react'],
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@ -11,7 +11,8 @@
|
|||||||
"command:prod": "node dist/src/command/command",
|
"command:prod": "node dist/src/command/command",
|
||||||
"worker:prod": "node dist/src/queue-worker/queue-worker",
|
"worker:prod": "node dist/src/queue-worker/queue-worker",
|
||||||
"database:init:prod": "npx ts-node ./scripts/setup-db.ts && yarn database:migrate:prod",
|
"database:init:prod": "npx ts-node ./scripts/setup-db.ts && yarn database:migrate:prod",
|
||||||
"database:migrate:prod": "npx -y typeorm migration:run -d dist/src/database/typeorm/metadata/metadata.datasource && npx -y typeorm migration:run -d dist/src/database/typeorm/core/core.datasource"
|
"database:migrate:prod": "npx -y typeorm migration:run -d dist/src/database/typeorm/metadata/metadata.datasource && npx -y typeorm migration:run -d dist/src/database/typeorm/core/core.datasource",
|
||||||
|
"typeorm": "../../node_modules/typeorm/.bin/typeorm"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@graphql-yoga/nestjs": "patch:@graphql-yoga/nestjs@2.1.0#./patches/@graphql-yoga-nestjs-npm-2.1.0-cb509e6047.patch",
|
"@graphql-yoga/nestjs": "patch:@graphql-yoga/nestjs@2.1.0#./patches/@graphql-yoga-nestjs-npm-2.1.0-cb509e6047.patch",
|
||||||
|
|||||||
@ -0,0 +1,17 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class AddSoftDelete1723038077987 implements MigrationInterface {
|
||||||
|
name = 'AddSoftDelete1723038077987';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "metadata"."objectMetadata" ADD "isSoftDeletable" boolean`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "metadata"."objectMetadata" DROP COLUMN "isSoftDeletable"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,6 +3,7 @@ import { Injectable } from '@nestjs/common';
|
|||||||
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
||||||
|
|
||||||
import { stringifyWithoutKeyQuote } from 'src/engine/api/graphql/workspace-query-builder/utils/stringify-without-key-quote.util';
|
import { stringifyWithoutKeyQuote } from 'src/engine/api/graphql/workspace-query-builder/utils/stringify-without-key-quote.util';
|
||||||
|
import { isDefined } from 'src/utils/is-defined';
|
||||||
|
|
||||||
import { ArgsAliasFactory } from './args-alias.factory';
|
import { ArgsAliasFactory } from './args-alias.factory';
|
||||||
|
|
||||||
@ -13,10 +14,18 @@ export class ArgsStringFactory {
|
|||||||
create(
|
create(
|
||||||
initialArgs: Record<string, any> | undefined,
|
initialArgs: Record<string, any> | undefined,
|
||||||
fieldMetadataCollection: FieldMetadataInterface[],
|
fieldMetadataCollection: FieldMetadataInterface[],
|
||||||
|
softDeletable?: boolean,
|
||||||
): string | null {
|
): string | null {
|
||||||
if (!initialArgs) {
|
if (!initialArgs) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if (softDeletable) {
|
||||||
|
initialArgs.filter = {
|
||||||
|
and: [initialArgs.filter, { deletedAt: { is: 'NULL' } }].filter(
|
||||||
|
isDefined,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
let argsString = '';
|
let argsString = '';
|
||||||
const computedArgs = this.argsAliasFactory.create(
|
const computedArgs = this.argsAliasFactory.create(
|
||||||
initialArgs,
|
initialArgs,
|
||||||
|
|||||||
@ -22,19 +22,23 @@ export class FieldsStringFactory {
|
|||||||
private readonly relationFieldAliasFactory: RelationFieldAliasFactory,
|
private readonly relationFieldAliasFactory: RelationFieldAliasFactory,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
create(
|
async create(
|
||||||
info: GraphQLResolveInfo,
|
info: GraphQLResolveInfo,
|
||||||
fieldMetadataCollection: FieldMetadataInterface[],
|
fieldMetadataCollection: FieldMetadataInterface[],
|
||||||
objectMetadataCollection: ObjectMetadataInterface[],
|
objectMetadataCollection: ObjectMetadataInterface[],
|
||||||
|
withSoftDeleted?: boolean,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const selectedFields: Partial<Record> = graphqlFields(info);
|
const selectedFields: Partial<Record> = graphqlFields(info);
|
||||||
|
|
||||||
return this.createFieldsStringRecursive(
|
const res = await this.createFieldsStringRecursive(
|
||||||
info,
|
info,
|
||||||
selectedFields,
|
selectedFields,
|
||||||
fieldMetadataCollection,
|
fieldMetadataCollection,
|
||||||
objectMetadataCollection,
|
objectMetadataCollection,
|
||||||
|
withSoftDeleted ?? false,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
async createFieldsStringRecursive(
|
async createFieldsStringRecursive(
|
||||||
@ -42,6 +46,7 @@ export class FieldsStringFactory {
|
|||||||
selectedFields: Partial<Record>,
|
selectedFields: Partial<Record>,
|
||||||
fieldMetadataCollection: FieldMetadataInterface[],
|
fieldMetadataCollection: FieldMetadataInterface[],
|
||||||
objectMetadataCollection: ObjectMetadataInterface[],
|
objectMetadataCollection: ObjectMetadataInterface[],
|
||||||
|
withSoftDeleted: boolean,
|
||||||
accumulator = '',
|
accumulator = '',
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const fieldMetadataMap = new Map(
|
const fieldMetadataMap = new Map(
|
||||||
@ -65,6 +70,7 @@ export class FieldsStringFactory {
|
|||||||
fieldMetadata,
|
fieldMetadata,
|
||||||
objectMetadataCollection,
|
objectMetadataCollection,
|
||||||
info,
|
info,
|
||||||
|
withSoftDeleted,
|
||||||
);
|
);
|
||||||
|
|
||||||
fieldAlias = alias;
|
fieldAlias = alias;
|
||||||
@ -91,6 +97,7 @@ export class FieldsStringFactory {
|
|||||||
fieldValue,
|
fieldValue,
|
||||||
fieldMetadataCollection,
|
fieldMetadataCollection,
|
||||||
objectMetadataCollection,
|
objectMetadataCollection,
|
||||||
|
withSoftDeleted,
|
||||||
accumulator,
|
accumulator,
|
||||||
);
|
);
|
||||||
accumulator += `}\n`;
|
accumulator += `}\n`;
|
||||||
|
|||||||
@ -36,6 +36,7 @@ export class FindManyQueryFactory {
|
|||||||
const argsString = this.argsStringFactory.create(
|
const argsString = this.argsStringFactory.create(
|
||||||
args,
|
args,
|
||||||
options.fieldMetadataCollection,
|
options.fieldMetadataCollection,
|
||||||
|
!options.withSoftDeleted && !!options.objectMetadataItem.isSoftDeletable,
|
||||||
);
|
);
|
||||||
|
|
||||||
return `
|
return `
|
||||||
|
|||||||
@ -26,10 +26,12 @@ export class FindOneQueryFactory {
|
|||||||
options.info,
|
options.info,
|
||||||
options.fieldMetadataCollection,
|
options.fieldMetadataCollection,
|
||||||
options.objectMetadataCollection,
|
options.objectMetadataCollection,
|
||||||
|
options.withSoftDeleted,
|
||||||
);
|
);
|
||||||
const argsString = this.argsStringFactory.create(
|
const argsString = this.argsStringFactory.create(
|
||||||
args,
|
args,
|
||||||
options.fieldMetadataCollection,
|
options.fieldMetadataCollection,
|
||||||
|
!options.withSoftDeleted && !!options.objectMetadataItem.isSoftDeletable,
|
||||||
);
|
);
|
||||||
|
|
||||||
return `
|
return `
|
||||||
|
|||||||
@ -12,7 +12,6 @@ import {
|
|||||||
RelationDirection,
|
RelationDirection,
|
||||||
} from 'src/engine/utils/deduce-relation-direction.util';
|
} from 'src/engine/utils/deduce-relation-direction.util';
|
||||||
import { getFieldArgumentsByKey } from 'src/engine/api/graphql/workspace-query-builder/utils/get-field-arguments-by-key.util';
|
import { getFieldArgumentsByKey } from 'src/engine/api/graphql/workspace-query-builder/utils/get-field-arguments-by-key.util';
|
||||||
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
|
|
||||||
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
||||||
import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
|
import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util';
|
||||||
|
|
||||||
@ -27,7 +26,6 @@ export class RelationFieldAliasFactory {
|
|||||||
@Inject(forwardRef(() => FieldsStringFactory))
|
@Inject(forwardRef(() => FieldsStringFactory))
|
||||||
private readonly fieldsStringFactory: CircularDep<FieldsStringFactory>,
|
private readonly fieldsStringFactory: CircularDep<FieldsStringFactory>,
|
||||||
private readonly argsStringFactory: ArgsStringFactory,
|
private readonly argsStringFactory: ArgsStringFactory,
|
||||||
private readonly objectMetadataService: ObjectMetadataService,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
create(
|
create(
|
||||||
@ -36,6 +34,7 @@ export class RelationFieldAliasFactory {
|
|||||||
fieldMetadata: FieldMetadataInterface,
|
fieldMetadata: FieldMetadataInterface,
|
||||||
objectMetadataCollection: ObjectMetadataInterface[],
|
objectMetadataCollection: ObjectMetadataInterface[],
|
||||||
info: GraphQLResolveInfo,
|
info: GraphQLResolveInfo,
|
||||||
|
withSoftDeleted?: boolean,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
if (!isRelationFieldMetadataType(fieldMetadata.type)) {
|
if (!isRelationFieldMetadataType(fieldMetadata.type)) {
|
||||||
throw new Error(`Field ${fieldMetadata.name} is not a relation field`);
|
throw new Error(`Field ${fieldMetadata.name} is not a relation field`);
|
||||||
@ -47,6 +46,7 @@ export class RelationFieldAliasFactory {
|
|||||||
fieldMetadata,
|
fieldMetadata,
|
||||||
objectMetadataCollection,
|
objectMetadataCollection,
|
||||||
info,
|
info,
|
||||||
|
withSoftDeleted,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,6 +56,7 @@ export class RelationFieldAliasFactory {
|
|||||||
fieldMetadata: FieldMetadataInterface,
|
fieldMetadata: FieldMetadataInterface,
|
||||||
objectMetadataCollection: ObjectMetadataInterface[],
|
objectMetadataCollection: ObjectMetadataInterface[],
|
||||||
info: GraphQLResolveInfo,
|
info: GraphQLResolveInfo,
|
||||||
|
withSoftDeleted?: boolean,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const relationMetadata =
|
const relationMetadata =
|
||||||
fieldMetadata.fromRelationMetadata ?? fieldMetadata.toRelationMetadata;
|
fieldMetadata.fromRelationMetadata ?? fieldMetadata.toRelationMetadata;
|
||||||
@ -98,9 +99,11 @@ export class RelationFieldAliasFactory {
|
|||||||
relationDirection === RelationDirection.FROM
|
relationDirection === RelationDirection.FROM
|
||||||
) {
|
) {
|
||||||
const args = getFieldArgumentsByKey(info, fieldKey);
|
const args = getFieldArgumentsByKey(info, fieldKey);
|
||||||
|
|
||||||
const argsString = this.argsStringFactory.create(
|
const argsString = this.argsStringFactory.create(
|
||||||
args,
|
args,
|
||||||
referencedObjectMetadata.fields ?? [],
|
referencedObjectMetadata.fields ?? [],
|
||||||
|
!withSoftDeleted && !!referencedObjectMetadata.isSoftDeletable,
|
||||||
);
|
);
|
||||||
const fieldsString =
|
const fieldsString =
|
||||||
await this.fieldsStringFactory.createFieldsStringRecursive(
|
await this.fieldsStringFactory.createFieldsStringRecursive(
|
||||||
@ -108,6 +111,7 @@ export class RelationFieldAliasFactory {
|
|||||||
fieldValue,
|
fieldValue,
|
||||||
referencedObjectMetadata.fields ?? [],
|
referencedObjectMetadata.fields ?? [],
|
||||||
objectMetadataCollection,
|
objectMetadataCollection,
|
||||||
|
withSoftDeleted ?? false,
|
||||||
);
|
);
|
||||||
|
|
||||||
return `
|
return `
|
||||||
@ -137,6 +141,7 @@ export class RelationFieldAliasFactory {
|
|||||||
fieldValue,
|
fieldValue,
|
||||||
referencedObjectMetadata.fields ?? [],
|
referencedObjectMetadata.fields ?? [],
|
||||||
objectMetadataCollection,
|
objectMetadataCollection,
|
||||||
|
withSoftDeleted ?? false,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Otherwise it means it's a relation destination is of kind ONE
|
// Otherwise it means it's a relation destination is of kind ONE
|
||||||
|
|||||||
@ -3,6 +3,7 @@ export interface Record {
|
|||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
deletedAt: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RecordFilter = {
|
export type RecordFilter = {
|
||||||
|
|||||||
@ -8,4 +8,5 @@ export interface WorkspaceQueryBuilderOptions {
|
|||||||
info: GraphQLResolveInfo;
|
info: GraphQLResolveInfo;
|
||||||
fieldMetadataCollection: FieldMetadataInterface[];
|
fieldMetadataCollection: FieldMetadataInterface[];
|
||||||
objectMetadataCollection: ObjectMetadataInterface[];
|
objectMetadataCollection: ObjectMetadataInterface[];
|
||||||
|
withSoftDeleted?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,11 +35,10 @@ export class EntityEventsToDbListener {
|
|||||||
return this.handle(payload);
|
return this.handle(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
// @OnEvent('*.deleted') - TODO: implement when we soft delete has been implemented
|
@OnEvent('*.deleted')
|
||||||
// ....
|
async handleDelete(payload: ObjectRecordUpdateEvent<any>) {
|
||||||
|
return this.handle(payload);
|
||||||
// @OnEvent('*.restored') - TODO: implement when we soft delete has been implemented
|
}
|
||||||
// ....
|
|
||||||
|
|
||||||
private async handle(payload: ObjectRecordBaseEvent) {
|
private async handle(payload: ObjectRecordBaseEvent) {
|
||||||
if (!payload.objectMetadata?.isAuditLogged) {
|
if (!payload.objectMetadata?.isAuditLogged) {
|
||||||
|
|||||||
@ -0,0 +1,29 @@
|
|||||||
|
import { RecordFilter } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface';
|
||||||
|
|
||||||
|
import { isDefined } from 'src/utils/is-defined';
|
||||||
|
|
||||||
|
export const withSoftDeleted = <T extends RecordFilter>(
|
||||||
|
filter: T | undefined | null,
|
||||||
|
): boolean => {
|
||||||
|
if (!isDefined(filter)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(filter)) {
|
||||||
|
return filter.some((item) => withSoftDeleted(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(filter)) {
|
||||||
|
if (key === 'deletedAt') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'object' && value !== null) {
|
||||||
|
if (withSoftDeleted(value)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
@ -3,9 +3,11 @@ import {
|
|||||||
CreateOneResolverArgs,
|
CreateOneResolverArgs,
|
||||||
DeleteManyResolverArgs,
|
DeleteManyResolverArgs,
|
||||||
DeleteOneResolverArgs,
|
DeleteOneResolverArgs,
|
||||||
|
DestroyManyResolverArgs,
|
||||||
FindDuplicatesResolverArgs,
|
FindDuplicatesResolverArgs,
|
||||||
FindManyResolverArgs,
|
FindManyResolverArgs,
|
||||||
FindOneResolverArgs,
|
FindOneResolverArgs,
|
||||||
|
RestoreManyResolverArgs,
|
||||||
UpdateManyResolverArgs,
|
UpdateManyResolverArgs,
|
||||||
UpdateOneResolverArgs,
|
UpdateOneResolverArgs,
|
||||||
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||||
@ -33,4 +35,8 @@ export type WorkspacePreQueryHookPayload<T> = T extends 'createMany'
|
|||||||
? UpdateOneResolverArgs
|
? UpdateOneResolverArgs
|
||||||
: T extends 'findDuplicates'
|
: T extends 'findDuplicates'
|
||||||
? FindDuplicatesResolverArgs
|
? FindDuplicatesResolverArgs
|
||||||
: never;
|
: T extends 'restoreMany'
|
||||||
|
? RestoreManyResolverArgs
|
||||||
|
: T extends 'destroyMany'
|
||||||
|
? DestroyManyResolverArgs
|
||||||
|
: never;
|
||||||
|
|||||||
@ -15,10 +15,12 @@ import {
|
|||||||
CreateOneResolverArgs,
|
CreateOneResolverArgs,
|
||||||
DeleteManyResolverArgs,
|
DeleteManyResolverArgs,
|
||||||
DeleteOneResolverArgs,
|
DeleteOneResolverArgs,
|
||||||
|
DestroyManyResolverArgs,
|
||||||
FindDuplicatesResolverArgs,
|
FindDuplicatesResolverArgs,
|
||||||
FindManyResolverArgs,
|
FindManyResolverArgs,
|
||||||
FindOneResolverArgs,
|
FindOneResolverArgs,
|
||||||
ResolverArgsType,
|
ResolverArgsType,
|
||||||
|
RestoreManyResolverArgs,
|
||||||
UpdateManyResolverArgs,
|
UpdateManyResolverArgs,
|
||||||
UpdateOneResolverArgs,
|
UpdateOneResolverArgs,
|
||||||
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||||
@ -34,6 +36,7 @@ import {
|
|||||||
} from 'src/engine/api/graphql/workspace-query-runner/jobs/call-webhook-jobs.job';
|
} from 'src/engine/api/graphql/workspace-query-runner/jobs/call-webhook-jobs.job';
|
||||||
import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util';
|
import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util';
|
||||||
import { parseResult } from 'src/engine/api/graphql/workspace-query-runner/utils/parse-result.util';
|
import { parseResult } from 'src/engine/api/graphql/workspace-query-runner/utils/parse-result.util';
|
||||||
|
import { withSoftDeleted } from 'src/engine/api/graphql/workspace-query-runner/utils/with-soft-deleted.util';
|
||||||
import { WorkspaceQueryHookService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.service';
|
import { WorkspaceQueryHookService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.service';
|
||||||
import {
|
import {
|
||||||
WorkspaceQueryRunnerException,
|
WorkspaceQueryRunnerException,
|
||||||
@ -108,7 +111,10 @@ export class WorkspaceQueryRunnerService {
|
|||||||
|
|
||||||
const query = await this.workspaceQueryBuilderFactory.findMany(
|
const query = await this.workspaceQueryBuilderFactory.findMany(
|
||||||
computedArgs,
|
computedArgs,
|
||||||
options,
|
{
|
||||||
|
...options,
|
||||||
|
withSoftDeleted: withSoftDeleted(args.filter),
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await this.execute(query, authContext.workspace.id);
|
const result = await this.execute(query, authContext.workspace.id);
|
||||||
@ -159,7 +165,10 @@ export class WorkspaceQueryRunnerService {
|
|||||||
|
|
||||||
const query = await this.workspaceQueryBuilderFactory.findOne(
|
const query = await this.workspaceQueryBuilderFactory.findOne(
|
||||||
computedArgs,
|
computedArgs,
|
||||||
options,
|
{
|
||||||
|
...options,
|
||||||
|
withSoftDeleted: withSoftDeleted(args.filter),
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await this.execute(query, authContext.workspace.id);
|
const result = await this.execute(query, authContext.workspace.id);
|
||||||
@ -540,6 +549,7 @@ export class WorkspaceQueryRunnerService {
|
|||||||
options: WorkspaceQueryRunnerOptions,
|
options: WorkspaceQueryRunnerOptions,
|
||||||
): Promise<Record[] | undefined> {
|
): Promise<Record[] | undefined> {
|
||||||
const { authContext, objectMetadataItem } = options;
|
const { authContext, objectMetadataItem } = options;
|
||||||
|
let query: string;
|
||||||
|
|
||||||
assertMutationNotOnRemoteObject(objectMetadataItem);
|
assertMutationNotOnRemoteObject(objectMetadataItem);
|
||||||
|
|
||||||
@ -555,13 +565,25 @@ export class WorkspaceQueryRunnerService {
|
|||||||
args,
|
args,
|
||||||
);
|
);
|
||||||
|
|
||||||
const query = await this.workspaceQueryBuilderFactory.deleteMany(
|
if (objectMetadataItem.isSoftDeletable) {
|
||||||
hookedArgs,
|
query = await this.workspaceQueryBuilderFactory.updateMany(
|
||||||
{
|
{
|
||||||
|
filter: hookedArgs.filter,
|
||||||
|
data: {
|
||||||
|
deletedAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...options,
|
||||||
|
atMost: maximumRecordAffected,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
query = await this.workspaceQueryBuilderFactory.deleteMany(hookedArgs, {
|
||||||
...options,
|
...options,
|
||||||
atMost: maximumRecordAffected,
|
atMost: maximumRecordAffected,
|
||||||
},
|
});
|
||||||
);
|
}
|
||||||
|
|
||||||
const result = await this.execute(query, authContext.workspace.id);
|
const result = await this.execute(query, authContext.workspace.id);
|
||||||
|
|
||||||
@ -569,7 +591,7 @@ export class WorkspaceQueryRunnerService {
|
|||||||
await this.parseResult<PGGraphQLMutation<Record>>(
|
await this.parseResult<PGGraphQLMutation<Record>>(
|
||||||
result,
|
result,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
'deleteFrom',
|
objectMetadataItem.isSoftDeletable ? 'update' : 'deleteFrom',
|
||||||
authContext.workspace.id,
|
authContext.workspace.id,
|
||||||
)
|
)
|
||||||
)?.records;
|
)?.records;
|
||||||
@ -596,6 +618,148 @@ export class WorkspaceQueryRunnerService {
|
|||||||
return parsedResults;
|
return parsedResults;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async destroyMany<
|
||||||
|
Record extends IRecord = IRecord,
|
||||||
|
Filter extends RecordFilter = RecordFilter,
|
||||||
|
>(
|
||||||
|
args: DestroyManyResolverArgs<Filter>,
|
||||||
|
options: WorkspaceQueryRunnerOptions,
|
||||||
|
): Promise<Record[] | undefined> {
|
||||||
|
const { authContext, objectMetadataItem } = options;
|
||||||
|
|
||||||
|
assertMutationNotOnRemoteObject(objectMetadataItem);
|
||||||
|
|
||||||
|
if (!objectMetadataItem.isSoftDeletable) {
|
||||||
|
throw new WorkspaceQueryRunnerException(
|
||||||
|
'This method is reserved to objects that can be soft-deleted, use delete instead',
|
||||||
|
WorkspaceQueryRunnerExceptionCode.DATA_NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const maximumRecordAffected = this.environmentService.get(
|
||||||
|
'MUTATION_MAXIMUM_AFFECTED_RECORDS',
|
||||||
|
);
|
||||||
|
|
||||||
|
const hookedArgs =
|
||||||
|
await this.workspaceQueryHookService.executePreQueryHooks(
|
||||||
|
authContext,
|
||||||
|
objectMetadataItem.nameSingular,
|
||||||
|
'destroyMany',
|
||||||
|
args,
|
||||||
|
);
|
||||||
|
|
||||||
|
const query = await this.workspaceQueryBuilderFactory.deleteMany(
|
||||||
|
{
|
||||||
|
filter: {
|
||||||
|
...hookedArgs.filter,
|
||||||
|
deletedAt: { is: 'NOT_NULL' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...options,
|
||||||
|
atMost: maximumRecordAffected,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await this.execute(query, authContext.workspace.id);
|
||||||
|
|
||||||
|
const parsedResults = (
|
||||||
|
await this.parseResult<PGGraphQLMutation<Record>>(
|
||||||
|
result,
|
||||||
|
objectMetadataItem,
|
||||||
|
'deleteFrom',
|
||||||
|
authContext.workspace.id,
|
||||||
|
)
|
||||||
|
)?.records;
|
||||||
|
|
||||||
|
await this.triggerWebhooks<Record>(
|
||||||
|
parsedResults,
|
||||||
|
CallWebhookJobsJobOperation.delete,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
|
||||||
|
return parsedResults;
|
||||||
|
}
|
||||||
|
|
||||||
|
async restoreMany<
|
||||||
|
Record extends IRecord = IRecord,
|
||||||
|
Filter extends RecordFilter = RecordFilter,
|
||||||
|
>(
|
||||||
|
args: RestoreManyResolverArgs<Filter>,
|
||||||
|
options: WorkspaceQueryRunnerOptions,
|
||||||
|
): Promise<Record[] | undefined> {
|
||||||
|
const { authContext, objectMetadataItem } = options;
|
||||||
|
|
||||||
|
assertMutationNotOnRemoteObject(objectMetadataItem);
|
||||||
|
|
||||||
|
if (!objectMetadataItem.isSoftDeletable) {
|
||||||
|
throw new WorkspaceQueryRunnerException(
|
||||||
|
'This method is reserved to objects that can be soft-deleted',
|
||||||
|
WorkspaceQueryRunnerExceptionCode.DATA_NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const maximumRecordAffected = this.environmentService.get(
|
||||||
|
'MUTATION_MAXIMUM_AFFECTED_RECORDS',
|
||||||
|
);
|
||||||
|
|
||||||
|
const hookedArgs =
|
||||||
|
await this.workspaceQueryHookService.executePreQueryHooks(
|
||||||
|
authContext,
|
||||||
|
objectMetadataItem.nameSingular,
|
||||||
|
'restoreMany',
|
||||||
|
args,
|
||||||
|
);
|
||||||
|
|
||||||
|
const query = await this.workspaceQueryBuilderFactory.updateMany(
|
||||||
|
{
|
||||||
|
filter: {
|
||||||
|
...hookedArgs.filter,
|
||||||
|
deletedAt: { is: 'NOT_NULL' },
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
deletedAt: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...options,
|
||||||
|
atMost: maximumRecordAffected,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await this.execute(query, authContext.workspace.id);
|
||||||
|
|
||||||
|
const parsedResults = (
|
||||||
|
await this.parseResult<PGGraphQLMutation<Record>>(
|
||||||
|
result,
|
||||||
|
objectMetadataItem,
|
||||||
|
'update',
|
||||||
|
authContext.workspace.id,
|
||||||
|
)
|
||||||
|
)?.records;
|
||||||
|
|
||||||
|
await this.triggerWebhooks<Record>(
|
||||||
|
parsedResults,
|
||||||
|
CallWebhookJobsJobOperation.delete,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
|
||||||
|
parsedResults.forEach((record) => {
|
||||||
|
this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.created`, {
|
||||||
|
name: `${objectMetadataItem.nameSingular}.created`,
|
||||||
|
workspaceId: authContext.workspace.id,
|
||||||
|
userId: authContext.user?.id,
|
||||||
|
recordId: record.id,
|
||||||
|
objectMetadata: objectMetadataItem,
|
||||||
|
properties: {
|
||||||
|
after: this.removeNestedProperties(record),
|
||||||
|
},
|
||||||
|
} satisfies ObjectRecordCreateEvent<any>);
|
||||||
|
});
|
||||||
|
|
||||||
|
return parsedResults;
|
||||||
|
}
|
||||||
|
|
||||||
async deleteOne<Record extends IRecord = IRecord>(
|
async deleteOne<Record extends IRecord = IRecord>(
|
||||||
args: DeleteOneResolverArgs,
|
args: DeleteOneResolverArgs,
|
||||||
options: WorkspaceQueryRunnerOptions,
|
options: WorkspaceQueryRunnerOptions,
|
||||||
@ -606,6 +770,7 @@ export class WorkspaceQueryRunnerService {
|
|||||||
authContext.workspace.id,
|
authContext.workspace.id,
|
||||||
objectMetadataItem.nameSingular,
|
objectMetadataItem.nameSingular,
|
||||||
);
|
);
|
||||||
|
let query: string;
|
||||||
|
|
||||||
assertMutationNotOnRemoteObject(objectMetadataItem);
|
assertMutationNotOnRemoteObject(objectMetadataItem);
|
||||||
assertIsValidUuid(args.id);
|
assertIsValidUuid(args.id);
|
||||||
@ -618,10 +783,22 @@ export class WorkspaceQueryRunnerService {
|
|||||||
args,
|
args,
|
||||||
);
|
);
|
||||||
|
|
||||||
const query = await this.workspaceQueryBuilderFactory.deleteOne(
|
if (objectMetadataItem.isSoftDeletable) {
|
||||||
hookedArgs,
|
query = await this.workspaceQueryBuilderFactory.updateOne(
|
||||||
options,
|
{
|
||||||
);
|
id: hookedArgs.id,
|
||||||
|
data: {
|
||||||
|
deletedAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
query = await this.workspaceQueryBuilderFactory.deleteOne(
|
||||||
|
hookedArgs,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const existingRecord = await repository.findOne({
|
const existingRecord = await repository.findOne({
|
||||||
where: { id: args.id },
|
where: { id: args.id },
|
||||||
@ -633,7 +810,7 @@ export class WorkspaceQueryRunnerService {
|
|||||||
await this.parseResult<PGGraphQLMutation<Record>>(
|
await this.parseResult<PGGraphQLMutation<Record>>(
|
||||||
result,
|
result,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
'deleteFrom',
|
objectMetadataItem.isSoftDeletable ? 'update' : 'deleteFrom',
|
||||||
authContext.workspace.id,
|
authContext.workspace.id,
|
||||||
)
|
)
|
||||||
)?.records;
|
)?.records;
|
||||||
|
|||||||
@ -0,0 +1,42 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
|
||||||
|
import {
|
||||||
|
DestroyManyResolverArgs,
|
||||||
|
Resolver,
|
||||||
|
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||||
|
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
|
||||||
|
|
||||||
|
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
|
||||||
|
import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DestroyManyResolverFactory
|
||||||
|
implements WorkspaceResolverBuilderFactoryInterface
|
||||||
|
{
|
||||||
|
public static methodName = 'destroyMany' as const;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
create(
|
||||||
|
context: WorkspaceSchemaBuilderContext,
|
||||||
|
): Resolver<DestroyManyResolverArgs> {
|
||||||
|
const internalContext = context;
|
||||||
|
|
||||||
|
return async (_source, args, context, info) => {
|
||||||
|
try {
|
||||||
|
return await this.workspaceQueryRunnerService.destroyMany(args, {
|
||||||
|
authContext: internalContext.authContext,
|
||||||
|
objectMetadataItem: internalContext.objectMetadataItem,
|
||||||
|
info,
|
||||||
|
fieldMetadataCollection: internalContext.fieldMetadataCollection,
|
||||||
|
objectMetadataCollection: internalContext.objectMetadataCollection,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { DestroyManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/destroy-many-resolver.factory';
|
||||||
|
import { RestoreManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/restore-many-resolver.factory';
|
||||||
import { UpdateManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory';
|
import { UpdateManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory';
|
||||||
|
|
||||||
import { CreateManyResolverFactory } from './create-many-resolver.factory';
|
import { CreateManyResolverFactory } from './create-many-resolver.factory';
|
||||||
@ -19,6 +21,8 @@ export const workspaceResolverBuilderFactories = [
|
|||||||
DeleteOneResolverFactory,
|
DeleteOneResolverFactory,
|
||||||
UpdateManyResolverFactory,
|
UpdateManyResolverFactory,
|
||||||
DeleteManyResolverFactory,
|
DeleteManyResolverFactory,
|
||||||
|
DestroyManyResolverFactory,
|
||||||
|
RestoreManyResolverFactory,
|
||||||
];
|
];
|
||||||
|
|
||||||
export const workspaceResolverBuilderMethodNames = {
|
export const workspaceResolverBuilderMethodNames = {
|
||||||
@ -34,5 +38,7 @@ export const workspaceResolverBuilderMethodNames = {
|
|||||||
DeleteOneResolverFactory.methodName,
|
DeleteOneResolverFactory.methodName,
|
||||||
UpdateManyResolverFactory.methodName,
|
UpdateManyResolverFactory.methodName,
|
||||||
DeleteManyResolverFactory.methodName,
|
DeleteManyResolverFactory.methodName,
|
||||||
|
DestroyManyResolverFactory.methodName,
|
||||||
|
RestoreManyResolverFactory.methodName,
|
||||||
],
|
],
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@ -0,0 +1,42 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface';
|
||||||
|
import {
|
||||||
|
Resolver,
|
||||||
|
RestoreManyResolverArgs,
|
||||||
|
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||||
|
import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface';
|
||||||
|
|
||||||
|
import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util';
|
||||||
|
import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RestoreManyResolverFactory
|
||||||
|
implements WorkspaceResolverBuilderFactoryInterface
|
||||||
|
{
|
||||||
|
public static methodName = 'restoreMany' as const;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
create(
|
||||||
|
context: WorkspaceSchemaBuilderContext,
|
||||||
|
): Resolver<RestoreManyResolverArgs> {
|
||||||
|
const internalContext = context;
|
||||||
|
|
||||||
|
return async (_source, args, context, info) => {
|
||||||
|
try {
|
||||||
|
return await this.workspaceQueryRunnerService.restoreMany(args, {
|
||||||
|
authContext: internalContext.authContext,
|
||||||
|
objectMetadataItem: internalContext.objectMetadataItem,
|
||||||
|
info,
|
||||||
|
fieldMetadataCollection: internalContext.fieldMetadataCollection,
|
||||||
|
objectMetadataCollection: internalContext.objectMetadataCollection,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
workspaceQueryRunnerGraphqlApiExceptionHandler(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -20,6 +20,8 @@ export enum ResolverArgsType {
|
|||||||
UpdateMany = 'UpdateMany',
|
UpdateMany = 'UpdateMany',
|
||||||
DeleteOne = 'DeleteOne',
|
DeleteOne = 'DeleteOne',
|
||||||
DeleteMany = 'DeleteMany',
|
DeleteMany = 'DeleteMany',
|
||||||
|
RestoreMany = 'RestoreMany',
|
||||||
|
DestroyMany = 'DestroyMany',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FindManyResolverArgs<
|
export interface FindManyResolverArgs<
|
||||||
@ -82,6 +84,14 @@ export interface DeleteManyResolverArgs<Filter = any> {
|
|||||||
filter: Filter;
|
filter: Filter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RestoreManyResolverArgs<Filter = any> {
|
||||||
|
filter: Filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DestroyManyResolverArgs<Filter = any> {
|
||||||
|
filter: Filter;
|
||||||
|
}
|
||||||
|
|
||||||
export type WorkspaceResolverBuilderQueryMethodNames =
|
export type WorkspaceResolverBuilderQueryMethodNames =
|
||||||
(typeof workspaceResolverBuilderMethodNames.queries)[number];
|
(typeof workspaceResolverBuilderMethodNames.queries)[number];
|
||||||
|
|
||||||
@ -106,4 +116,6 @@ export type ResolverArgs =
|
|||||||
| FindOneResolverArgs
|
| FindOneResolverArgs
|
||||||
| FindDuplicatesResolverArgs
|
| FindDuplicatesResolverArgs
|
||||||
| UpdateManyResolverArgs
|
| UpdateManyResolverArgs
|
||||||
| UpdateOneResolverArgs;
|
| UpdateOneResolverArgs
|
||||||
|
| DestroyManyResolverArgs
|
||||||
|
| RestoreManyResolverArgs;
|
||||||
|
|||||||
@ -5,6 +5,8 @@ import { IResolvers } from '@graphql-tools/utils';
|
|||||||
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
||||||
|
|
||||||
import { DeleteManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/delete-many-resolver.factory';
|
import { DeleteManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/delete-many-resolver.factory';
|
||||||
|
import { DestroyManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/destroy-many-resolver.factory';
|
||||||
|
import { RestoreManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/restore-many-resolver.factory';
|
||||||
import { UpdateManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory';
|
import { UpdateManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory';
|
||||||
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||||
import { getResolverName } from 'src/engine/utils/get-resolver-name.util';
|
import { getResolverName } from 'src/engine/utils/get-resolver-name.util';
|
||||||
@ -36,6 +38,8 @@ export class WorkspaceResolverFactory {
|
|||||||
private readonly deleteOneResolverFactory: DeleteOneResolverFactory,
|
private readonly deleteOneResolverFactory: DeleteOneResolverFactory,
|
||||||
private readonly updateManyResolverFactory: UpdateManyResolverFactory,
|
private readonly updateManyResolverFactory: UpdateManyResolverFactory,
|
||||||
private readonly deleteManyResolverFactory: DeleteManyResolverFactory,
|
private readonly deleteManyResolverFactory: DeleteManyResolverFactory,
|
||||||
|
private readonly restoreManyResolverFactory: RestoreManyResolverFactory,
|
||||||
|
private readonly destroyManyResolverFactory: DestroyManyResolverFactory,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async create(
|
async create(
|
||||||
@ -56,6 +60,8 @@ export class WorkspaceResolverFactory {
|
|||||||
['deleteOne', this.deleteOneResolverFactory],
|
['deleteOne', this.deleteOneResolverFactory],
|
||||||
['updateMany', this.updateManyResolverFactory],
|
['updateMany', this.updateManyResolverFactory],
|
||||||
['deleteMany', this.deleteManyResolverFactory],
|
['deleteMany', this.deleteManyResolverFactory],
|
||||||
|
['restoreMany', this.restoreManyResolverFactory],
|
||||||
|
['destroyMany', this.destroyManyResolverFactory],
|
||||||
]);
|
]);
|
||||||
const resolvers: IResolvers = {
|
const resolvers: IResolvers = {
|
||||||
Query: {},
|
Query: {},
|
||||||
|
|||||||
@ -2,14 +2,14 @@ import { Injectable, Logger } from '@nestjs/common';
|
|||||||
|
|
||||||
import { GraphQLFieldConfigMap, GraphQLObjectType } from 'graphql';
|
import { GraphQLFieldConfigMap, GraphQLObjectType } from 'graphql';
|
||||||
|
|
||||||
import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
|
||||||
import { WorkspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
import { WorkspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||||
|
import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||||
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
||||||
|
|
||||||
import { TypeDefinitionsStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/type-definitions.storage';
|
|
||||||
import { getResolverName } from 'src/engine/utils/get-resolver-name.util';
|
|
||||||
import { getResolverArgs } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-resolver-args.util';
|
|
||||||
import { TypeMapperService } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service';
|
import { TypeMapperService } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service';
|
||||||
|
import { TypeDefinitionsStorage } from 'src/engine/api/graphql/workspace-schema-builder/storages/type-definitions.storage';
|
||||||
|
import { getResolverArgs } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-resolver-args.util';
|
||||||
|
import { getResolverName } from 'src/engine/utils/get-resolver-name.util';
|
||||||
|
|
||||||
import { ArgsFactory } from './args.factory';
|
import { ArgsFactory } from './args.factory';
|
||||||
import { ObjectTypeDefinitionKind } from './object-type-definition.factory';
|
import { ObjectTypeDefinitionKind } from './object-type-definition.factory';
|
||||||
@ -101,13 +101,17 @@ export class RootTypeFactory {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const allowedMethodNames = [
|
||||||
|
'updateMany',
|
||||||
|
'deleteMany',
|
||||||
|
'createMany',
|
||||||
|
'findDuplicates',
|
||||||
|
'restoreMany',
|
||||||
|
'destroyMany',
|
||||||
|
];
|
||||||
|
|
||||||
const outputType = this.typeMapperService.mapToGqlType(objectType, {
|
const outputType = this.typeMapperService.mapToGqlType(objectType, {
|
||||||
isArray: [
|
isArray: allowedMethodNames.includes(methodName),
|
||||||
'updateMany',
|
|
||||||
'deleteMany',
|
|
||||||
'createMany',
|
|
||||||
'findDuplicates',
|
|
||||||
].includes(methodName),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
fieldConfigMap[name] = {
|
fieldConfigMap[name] = {
|
||||||
|
|||||||
@ -50,6 +50,12 @@ describe('getResolverArgs', () => {
|
|||||||
deleteOne: {
|
deleteOne: {
|
||||||
id: { type: GraphQLID, isNullable: false },
|
id: { type: GraphQLID, isNullable: false },
|
||||||
},
|
},
|
||||||
|
restoreMany: {
|
||||||
|
filter: { kind: InputTypeDefinitionKind.Filter, isNullable: false },
|
||||||
|
},
|
||||||
|
destroyMany: {
|
||||||
|
filter: { kind: InputTypeDefinitionKind.Filter, isNullable: false },
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Test each resolver type
|
// Test each resolver type
|
||||||
|
|||||||
@ -116,6 +116,20 @@ export const getResolverArgs = (
|
|||||||
isNullable: false,
|
isNullable: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
case 'restoreMany':
|
||||||
|
return {
|
||||||
|
filter: {
|
||||||
|
kind: InputTypeDefinitionKind.Filter,
|
||||||
|
isNullable: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
case 'destroyMany':
|
||||||
|
return {
|
||||||
|
filter: {
|
||||||
|
kind: InputTypeDefinitionKind.Filter,
|
||||||
|
isNullable: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown resolver type: ${type}`);
|
throw new Error(`Unknown resolver type: ${type}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,4 +20,5 @@ export interface ObjectMetadataInterface {
|
|||||||
isAuditLogged: boolean;
|
isAuditLogged: boolean;
|
||||||
labelIdentifierFieldMetadataId?: string | null;
|
labelIdentifierFieldMetadataId?: string | null;
|
||||||
imageIdentifierFieldMetadataId?: string | null;
|
imageIdentifierFieldMetadataId?: string | null;
|
||||||
|
isSoftDeletable?: boolean | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -69,6 +69,9 @@ export class ObjectMetadataEntity implements ObjectMetadataInterface {
|
|||||||
@Column({ default: true })
|
@Column({ default: true })
|
||||||
isAuditLogged: boolean;
|
isAuditLogged: boolean;
|
||||||
|
|
||||||
|
@Column({ nullable: true, type: 'boolean' })
|
||||||
|
isSoftDeletable?: boolean | null;
|
||||||
|
|
||||||
@Column({ nullable: true, type: 'uuid' })
|
@Column({ nullable: true, type: 'uuid' })
|
||||||
labelIdentifierFieldMetadataId?: string | null;
|
labelIdentifierFieldMetadataId?: string | null;
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||||
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
||||||
import { WorkspaceIsPimaryField } from 'src/engine/twenty-orm/decorators/workspace-is-primary-field.decorator';
|
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
||||||
|
import { WorkspaceIsPrimaryField } from 'src/engine/twenty-orm/decorators/workspace-is-primary-field.decorator';
|
||||||
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
||||||
import { BASE_OBJECT_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
import { BASE_OBJECT_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||||
|
|
||||||
@ -13,7 +14,7 @@ export abstract class BaseWorkspaceEntity {
|
|||||||
defaultValue: 'uuid',
|
defaultValue: 'uuid',
|
||||||
icon: 'Icon123',
|
icon: 'Icon123',
|
||||||
})
|
})
|
||||||
@WorkspaceIsPimaryField()
|
@WorkspaceIsPrimaryField()
|
||||||
@WorkspaceIsSystem()
|
@WorkspaceIsSystem()
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
@ -25,7 +26,7 @@ export abstract class BaseWorkspaceEntity {
|
|||||||
icon: 'IconCalendar',
|
icon: 'IconCalendar',
|
||||||
defaultValue: 'now',
|
defaultValue: 'now',
|
||||||
})
|
})
|
||||||
createdAt: Date;
|
createdAt: string;
|
||||||
|
|
||||||
@WorkspaceField({
|
@WorkspaceField({
|
||||||
standardId: BASE_OBJECT_STANDARD_FIELD_IDS.updatedAt,
|
standardId: BASE_OBJECT_STANDARD_FIELD_IDS.updatedAt,
|
||||||
@ -35,5 +36,15 @@ export abstract class BaseWorkspaceEntity {
|
|||||||
icon: 'IconCalendarClock',
|
icon: 'IconCalendarClock',
|
||||||
defaultValue: 'now',
|
defaultValue: 'now',
|
||||||
})
|
})
|
||||||
updatedAt: Date;
|
updatedAt: string;
|
||||||
|
|
||||||
|
@WorkspaceField({
|
||||||
|
standardId: BASE_OBJECT_STANDARD_FIELD_IDS.deletedAt,
|
||||||
|
type: FieldMetadataType.DATE_TIME,
|
||||||
|
label: 'Deleted at',
|
||||||
|
description: 'Date when the record was deleted',
|
||||||
|
icon: 'IconCalendarMinus',
|
||||||
|
})
|
||||||
|
@WorkspaceIsNullable()
|
||||||
|
deletedAt?: string | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import {
|
|||||||
RelationOnDeleteAction,
|
RelationOnDeleteAction,
|
||||||
} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
} from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity';
|
||||||
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity';
|
||||||
import { WorkspaceCustomObject } from 'src/engine/twenty-orm/decorators/workspace-custom-object.decorator';
|
import { WorkspaceCustomEntity } from 'src/engine/twenty-orm/decorators/workspace-custom-entity.decorator';
|
||||||
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator';
|
||||||
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator';
|
||||||
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator';
|
||||||
@ -21,7 +21,9 @@ import { NoteTargetWorkspaceEntity } from 'src/modules/note/standard-objects/not
|
|||||||
import { TaskTargetWorkspaceEntity } from 'src/modules/task/standard-objects/task-target.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 { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity';
|
||||||
|
|
||||||
@WorkspaceCustomObject()
|
@WorkspaceCustomEntity({
|
||||||
|
softDelete: true,
|
||||||
|
})
|
||||||
export class CustomWorkspaceEntity extends BaseWorkspaceEntity {
|
export class CustomWorkspaceEntity extends BaseWorkspaceEntity {
|
||||||
@WorkspaceField({
|
@WorkspaceField({
|
||||||
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.name,
|
standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.name,
|
||||||
|
|||||||
@ -1,7 +1,13 @@
|
|||||||
import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage';
|
import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage';
|
||||||
import { TypedReflect } from 'src/utils/typed-reflect';
|
import { TypedReflect } from 'src/utils/typed-reflect';
|
||||||
|
|
||||||
export function WorkspaceCustomObject(): ClassDecorator {
|
interface WorkspaceCustomEntityOptions {
|
||||||
|
softDelete?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WorkspaceCustomEntity(
|
||||||
|
options: WorkspaceCustomEntityOptions = {},
|
||||||
|
): ClassDecorator {
|
||||||
return (target) => {
|
return (target) => {
|
||||||
const gate = TypedReflect.getMetadata(
|
const gate = TypedReflect.getMetadata(
|
||||||
'workspace:gate-metadata-args',
|
'workspace:gate-metadata-args',
|
||||||
@ -11,6 +17,7 @@ export function WorkspaceCustomObject(): ClassDecorator {
|
|||||||
metadataArgsStorage.addExtendedEntities({
|
metadataArgsStorage.addExtendedEntities({
|
||||||
target,
|
target,
|
||||||
gate,
|
gate,
|
||||||
|
softDelete: options.softDelete,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -12,6 +12,7 @@ interface WorkspaceEntityOptions {
|
|||||||
icon?: string;
|
icon?: string;
|
||||||
labelIdentifierStandardId?: string;
|
labelIdentifierStandardId?: string;
|
||||||
imageIdentifierStandardId?: string;
|
imageIdentifierStandardId?: string;
|
||||||
|
softDelete?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WorkspaceEntity(
|
export function WorkspaceEntity(
|
||||||
@ -47,6 +48,7 @@ export function WorkspaceEntity(
|
|||||||
isAuditLogged,
|
isAuditLogged,
|
||||||
isSystem,
|
isSystem,
|
||||||
gate,
|
gate,
|
||||||
|
softDelete: options.softDelete,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { TypedReflect } from 'src/utils/typed-reflect';
|
import { TypedReflect } from 'src/utils/typed-reflect';
|
||||||
|
|
||||||
export function WorkspaceIsPimaryField(): PropertyDecorator {
|
export function WorkspaceIsPrimaryField(): PropertyDecorator {
|
||||||
return (object, propertyKey) => {
|
return (object, propertyKey) => {
|
||||||
TypedReflect.defineMetadata(
|
TypedReflect.defineMetadata(
|
||||||
'workspace:is-primary-field-metadata-args',
|
'workspace:is-primary-field-metadata-args',
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user