Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ffcf608897 |
@@ -4337,7 +4337,7 @@ export type CreateOneFieldMetadataItemMutationVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type CreateOneFieldMetadataItemMutation = { __typename?: 'Mutation', createOneField: { __typename?: 'Field', id: string, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isUnique?: boolean | null, isNullable?: boolean | null, createdAt: string, updatedAt: string, settings?: any | null, defaultValue?: any | null, options?: any | null, isLabelSyncedWithName?: boolean | null } };
|
||||
export type CreateOneFieldMetadataItemMutation = { __typename?: 'Mutation', createOneField: { __typename?: 'Field', id: string, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isUnique?: boolean | null, isNullable?: boolean | null, createdAt: string, updatedAt: string, settings?: any | null, defaultValue?: any | null, options?: any | null, isLabelSyncedWithName?: boolean | null, object?: { __typename?: 'Object', id: string } | null } };
|
||||
|
||||
export type UpdateOneFieldMetadataItemMutationVariables = Exact<{
|
||||
idToUpdate: Scalars['UUID'];
|
||||
@@ -4345,7 +4345,7 @@ export type UpdateOneFieldMetadataItemMutationVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type UpdateOneFieldMetadataItemMutation = { __typename?: 'Mutation', updateOneField: { __typename?: 'Field', id: string, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isUnique?: boolean | null, isNullable?: boolean | null, createdAt: string, updatedAt: string, settings?: any | null, isLabelSyncedWithName?: boolean | null } };
|
||||
export type UpdateOneFieldMetadataItemMutation = { __typename?: 'Mutation', updateOneField: { __typename?: 'Field', id: string, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isUnique?: boolean | null, isNullable?: boolean | null, createdAt: string, updatedAt: string, settings?: any | null, isLabelSyncedWithName?: boolean | null, object?: { __typename?: 'Object', id: string } | null } };
|
||||
|
||||
export type UpdateOneObjectMetadataItemMutationVariables = Exact<{
|
||||
idToUpdate: Scalars['UUID'];
|
||||
@@ -4367,7 +4367,7 @@ export type DeleteOneFieldMetadataItemMutationVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type DeleteOneFieldMetadataItemMutation = { __typename?: 'Mutation', deleteOneField: { __typename?: 'Field', id: string, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isUnique?: boolean | null, isNullable?: boolean | null, createdAt: string, updatedAt: string, settings?: any | null } };
|
||||
export type DeleteOneFieldMetadataItemMutation = { __typename?: 'Mutation', deleteOneField: { __typename?: 'Field', id: string, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isUnique?: boolean | null, isNullable?: boolean | null, createdAt: string, updatedAt: string, settings?: any | null, object?: { __typename?: 'Object', id: string } | null } };
|
||||
|
||||
export type ObjectMetadataItemsQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
@@ -7944,6 +7944,9 @@ export const CreateOneFieldMetadataItemDocument = gql`
|
||||
defaultValue
|
||||
options
|
||||
isLabelSyncedWithName
|
||||
object {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -7990,6 +7993,9 @@ export const UpdateOneFieldMetadataItemDocument = gql`
|
||||
updatedAt
|
||||
settings
|
||||
isLabelSyncedWithName
|
||||
object {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -8131,6 +8137,9 @@ export const DeleteOneFieldMetadataItemDocument = gql`
|
||||
createdAt
|
||||
updatedAt
|
||||
settings
|
||||
object {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -41,6 +41,9 @@ export const CREATE_ONE_FIELD_METADATA_ITEM = gql`
|
||||
defaultValue
|
||||
options
|
||||
isLabelSyncedWithName
|
||||
object {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -65,6 +68,9 @@ export const UPDATE_ONE_FIELD_METADATA_ITEM = gql`
|
||||
updatedAt
|
||||
settings
|
||||
isLabelSyncedWithName
|
||||
object {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -132,6 +138,9 @@ export const DELETE_ONE_FIELD_METADATA_ITEM = gql`
|
||||
createdAt
|
||||
updatedAt
|
||||
settings
|
||||
object {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
+8
-41
@@ -22,48 +22,9 @@ export const queries = {
|
||||
createdAt
|
||||
updatedAt
|
||||
settings
|
||||
}
|
||||
}
|
||||
`,
|
||||
findManyViewsQuery: gql`
|
||||
query FindManyViews(
|
||||
$filter: ViewFilterInput
|
||||
$orderBy: [ViewOrderByInput]
|
||||
$lastCursor: String
|
||||
$limit: Int
|
||||
) {
|
||||
views(
|
||||
filter: $filter
|
||||
orderBy: $orderBy
|
||||
first: $limit
|
||||
after: $lastCursor
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
__typename
|
||||
id
|
||||
viewGroups {
|
||||
edges {
|
||||
node {
|
||||
__typename
|
||||
fieldMetadataId
|
||||
fieldValue
|
||||
id
|
||||
isVisible
|
||||
position
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
cursor
|
||||
object {
|
||||
id
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
hasPreviousPage
|
||||
startCursor
|
||||
endCursor
|
||||
}
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
`,
|
||||
@@ -86,6 +47,9 @@ export const queries = {
|
||||
updatedAt
|
||||
settings
|
||||
isLabelSyncedWithName
|
||||
object {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
@@ -108,6 +72,9 @@ export const queries = {
|
||||
defaultValue
|
||||
options
|
||||
isLabelSyncedWithName
|
||||
object {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
|
||||
+8
-29
@@ -74,31 +74,6 @@ const fieldRelationMetadataItem: FieldMetadataItem = {
|
||||
};
|
||||
|
||||
const mocks = [
|
||||
{
|
||||
request: {
|
||||
query: queries.findManyViewsQuery,
|
||||
variables: {
|
||||
filter: {
|
||||
objectMetadataId: { eq: '25611fce-6637-4089-b0ca-91afeec95784' },
|
||||
},
|
||||
},
|
||||
},
|
||||
result: jest.fn(() => ({
|
||||
data: {
|
||||
views: {
|
||||
__typename: 'ViewConnection',
|
||||
totalCount: 0,
|
||||
pageInfo: {
|
||||
__typename: 'PageInfo',
|
||||
hasNextPage: false,
|
||||
startCursor: '',
|
||||
endCursor: '',
|
||||
},
|
||||
edges: [],
|
||||
},
|
||||
},
|
||||
})),
|
||||
},
|
||||
{
|
||||
request: {
|
||||
query: GET_CURRENT_USER,
|
||||
@@ -219,7 +194,10 @@ describe('useFieldMetadataItem', () => {
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
const res = await result.current.deleteMetadataField(fieldMetadataItem);
|
||||
const res = await result.current.deleteMetadataField({
|
||||
idToDelete: fieldMetadataItem.id,
|
||||
objectMetadataId,
|
||||
});
|
||||
|
||||
expect(res.data).toEqual({
|
||||
deleteOneField: responseData.default,
|
||||
@@ -233,9 +211,10 @@ describe('useFieldMetadataItem', () => {
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
const res = await result.current.deleteMetadataField(
|
||||
fieldRelationMetadataItem,
|
||||
);
|
||||
const res = await result.current.deleteMetadataField({
|
||||
idToDelete: fieldRelationMetadataItem.id,
|
||||
objectMetadataId,
|
||||
});
|
||||
|
||||
expect(res.data).toEqual({
|
||||
deleteOneField: responseData.fieldRelation,
|
||||
|
||||
+4
-4
@@ -9,7 +9,7 @@ import {
|
||||
import { CREATE_ONE_FIELD_METADATA_ITEM } from '../graphql/mutations';
|
||||
|
||||
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems';
|
||||
import { useRefreshCachedViews } from '@/views/hooks/useRefreshViews';
|
||||
import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId';
|
||||
|
||||
export const useCreateOneFieldMetadataItem = () => {
|
||||
const { refreshObjectMetadataItems } =
|
||||
@@ -20,7 +20,8 @@ export const useCreateOneFieldMetadataItem = () => {
|
||||
CreateOneFieldMetadataItemMutationVariables
|
||||
>(CREATE_ONE_FIELD_METADATA_ITEM);
|
||||
|
||||
const { refreshCachedViews } = useRefreshCachedViews();
|
||||
const { refreshCoreViewsByObjectMetadataId } =
|
||||
useRefreshCoreViewsByObjectMetadataId();
|
||||
|
||||
const createOneFieldMetadataItem = async (input: CreateFieldInput) => {
|
||||
const result = await mutate({
|
||||
@@ -33,8 +34,7 @@ export const useCreateOneFieldMetadataItem = () => {
|
||||
|
||||
await refreshObjectMetadataItems();
|
||||
|
||||
await refreshCachedViews();
|
||||
|
||||
await refreshCoreViewsByObjectMetadataId(input.objectMetadataId);
|
||||
return result;
|
||||
};
|
||||
|
||||
|
||||
+11
-4
@@ -9,13 +9,14 @@ import {
|
||||
import { CREATE_ONE_OBJECT_METADATA_ITEM } from '../graphql/mutations';
|
||||
|
||||
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems';
|
||||
import { useRefreshCachedViews } from '@/views/hooks/useRefreshViews';
|
||||
import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useCreateOneObjectMetadataItem = () => {
|
||||
const { refreshCachedViews } = useRefreshCachedViews();
|
||||
|
||||
const { refreshObjectMetadataItems } =
|
||||
useRefreshObjectMetadataItems('network-only');
|
||||
const { refreshCoreViewsByObjectMetadataId } =
|
||||
useRefreshCoreViewsByObjectMetadataId();
|
||||
|
||||
const [mutate] = useMutation<
|
||||
CreateOneObjectMetadataItemMutation,
|
||||
@@ -30,7 +31,13 @@ export const useCreateOneObjectMetadataItem = () => {
|
||||
});
|
||||
|
||||
await refreshObjectMetadataItems();
|
||||
refreshCachedViews();
|
||||
|
||||
if (isDefined(createdObjectMetadata.data?.createOneObject?.id)) {
|
||||
await refreshCoreViewsByObjectMetadataId(
|
||||
createdObjectMetadata.data.createOneObject.id,
|
||||
);
|
||||
}
|
||||
|
||||
return createdObjectMetadata;
|
||||
};
|
||||
|
||||
|
||||
+11
-9
@@ -5,10 +5,10 @@ import {
|
||||
type DeleteOneFieldMetadataItemMutationVariables,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
|
||||
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems';
|
||||
import { recordIndexKanbanAggregateOperationState } from '@/object-record/record-index/states/recordIndexKanbanAggregateOperationState';
|
||||
import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations';
|
||||
import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { DELETE_ONE_FIELD_METADATA_ITEM } from '../graphql/mutations';
|
||||
|
||||
@@ -20,14 +20,14 @@ export const useDeleteOneFieldMetadataItem = () => {
|
||||
|
||||
const { refreshObjectMetadataItems } =
|
||||
useRefreshObjectMetadataItems('network-only');
|
||||
const { refreshCoreViewsByObjectMetadataId } =
|
||||
useRefreshCoreViewsByObjectMetadataId();
|
||||
|
||||
const [
|
||||
recordIndexKanbanAggregateOperation,
|
||||
setRecordIndexKanbanAggregateOperation,
|
||||
] = useRecoilState(recordIndexKanbanAggregateOperationState);
|
||||
|
||||
const apolloCoreClient = useApolloCoreClient();
|
||||
|
||||
const resetRecordIndexKanbanAggregateOperation = async (
|
||||
idToDelete: DeleteOneFieldMetadataItemMutationVariables['idToDelete'],
|
||||
) => {
|
||||
@@ -37,14 +37,15 @@ export const useDeleteOneFieldMetadataItem = () => {
|
||||
fieldMetadataId: null,
|
||||
});
|
||||
}
|
||||
await apolloCoreClient.refetchQueries({
|
||||
include: ['FindManyViews'],
|
||||
});
|
||||
};
|
||||
|
||||
const deleteOneFieldMetadataItem = async (
|
||||
idToDelete: DeleteOneFieldMetadataItemMutationVariables['idToDelete'],
|
||||
) => {
|
||||
const deleteOneFieldMetadataItem = async ({
|
||||
idToDelete,
|
||||
objectMetadataId,
|
||||
}: {
|
||||
idToDelete: DeleteOneFieldMetadataItemMutationVariables['idToDelete'];
|
||||
objectMetadataId: string;
|
||||
}) => {
|
||||
const result = await mutate({
|
||||
variables: {
|
||||
idToDelete,
|
||||
@@ -54,6 +55,7 @@ export const useDeleteOneFieldMetadataItem = () => {
|
||||
await resetRecordIndexKanbanAggregateOperation(idToDelete);
|
||||
|
||||
await refreshObjectMetadataItems();
|
||||
await refreshCoreViewsByObjectMetadataId(objectMetadataId);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
+5
@@ -8,6 +8,7 @@ import {
|
||||
import { DELETE_ONE_OBJECT_METADATA_ITEM } from '../graphql/mutations';
|
||||
|
||||
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems';
|
||||
import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId';
|
||||
|
||||
export const useDeleteOneObjectMetadataItem = () => {
|
||||
const [mutate] = useMutation<
|
||||
@@ -18,6 +19,9 @@ export const useDeleteOneObjectMetadataItem = () => {
|
||||
const { refreshObjectMetadataItems } =
|
||||
useRefreshObjectMetadataItems('network-only');
|
||||
|
||||
const { refreshCoreViewsByObjectMetadataId } =
|
||||
useRefreshCoreViewsByObjectMetadataId();
|
||||
|
||||
const deleteOneObjectMetadataItem = async (
|
||||
idToDelete: DeleteOneObjectMetadataItemMutationVariables['idToDelete'],
|
||||
) => {
|
||||
@@ -28,6 +32,7 @@ export const useDeleteOneObjectMetadataItem = () => {
|
||||
});
|
||||
|
||||
await refreshObjectMetadataItems();
|
||||
await refreshCoreViewsByObjectMetadataId(idToDelete);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { type Field } from '~/generated-metadata/graphql';
|
||||
|
||||
import { type FieldMetadataItem } from '../types/FieldMetadataItem';
|
||||
import { formatFieldMetadataItemInput } from '../utils/formatFieldMetadataItemInput';
|
||||
|
||||
import { type RelationCreationPayload } from 'twenty-shared/types';
|
||||
@@ -65,14 +64,10 @@ export const useFieldMetadataItem = () => {
|
||||
updatePayload: { isActive: false },
|
||||
});
|
||||
|
||||
const deleteMetadataField = (metadataField: FieldMetadataItem) => {
|
||||
return deleteOneFieldMetadataItem(metadataField.id);
|
||||
};
|
||||
|
||||
return {
|
||||
activateMetadataField,
|
||||
createMetadataField,
|
||||
deactivateMetadataField,
|
||||
deleteMetadataField,
|
||||
deleteMetadataField: deleteOneFieldMetadataItem,
|
||||
};
|
||||
};
|
||||
|
||||
+6
-70
@@ -1,4 +1,4 @@
|
||||
import { useApolloClient, useMutation } from '@apollo/client';
|
||||
import { useMutation } from '@apollo/client';
|
||||
|
||||
import {
|
||||
type UpdateOneFieldMetadataItemMutation,
|
||||
@@ -7,43 +7,15 @@ import {
|
||||
|
||||
import { UPDATE_ONE_FIELD_METADATA_ITEM } from '../graphql/mutations';
|
||||
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
|
||||
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useFindManyRecordsQuery } from '@/object-record/hooks/useFindManyRecordsQuery';
|
||||
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
|
||||
import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection';
|
||||
import { type RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection';
|
||||
import { useSetRecordGroups } from '@/object-record/record-group/hooks/useSetRecordGroups';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId';
|
||||
|
||||
export const useUpdateOneFieldMetadataItem = () => {
|
||||
const apolloClient = useApolloClient();
|
||||
const apolloCoreClient = useApolloCoreClient();
|
||||
const { refreshObjectMetadataItems } =
|
||||
useRefreshObjectMetadataItems('network-only');
|
||||
|
||||
const { setRecordGroupsFromViewGroups } = useSetRecordGroups();
|
||||
const cache = useApolloClient().cache;
|
||||
|
||||
const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
|
||||
|
||||
const { findManyRecordsQuery: findManyViewsQuery } = useFindManyRecordsQuery({
|
||||
objectNameSingular: CoreObjectNameSingular.View,
|
||||
recordGqlFields: {
|
||||
id: true,
|
||||
viewGroups: {
|
||||
id: true,
|
||||
fieldMetadataId: true,
|
||||
isVisible: true,
|
||||
fieldValue: true,
|
||||
position: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
const { refreshCoreViewsByObjectMetadataId } =
|
||||
useRefreshCoreViewsByObjectMetadataId();
|
||||
|
||||
const [mutate] = useMutation<
|
||||
UpdateOneFieldMetadataItemMutation,
|
||||
@@ -76,44 +48,8 @@ export const useUpdateOneFieldMetadataItem = () => {
|
||||
},
|
||||
});
|
||||
|
||||
const objectMetadataItemsRefreshed = await refreshObjectMetadataItems();
|
||||
|
||||
const { data } = await apolloClient.query({ query: GET_CURRENT_USER });
|
||||
setCurrentWorkspace(data?.currentUser?.currentWorkspace);
|
||||
|
||||
const { data: viewConnection } = await apolloCoreClient.query<{
|
||||
views: RecordGqlConnection;
|
||||
}>({
|
||||
query: findManyViewsQuery,
|
||||
variables: {
|
||||
filter: {
|
||||
objectMetadataId: {
|
||||
eq: objectMetadataId,
|
||||
},
|
||||
},
|
||||
},
|
||||
fetchPolicy: 'network-only',
|
||||
});
|
||||
|
||||
const viewRecords = getRecordsFromRecordConnection({
|
||||
recordConnection: viewConnection?.views,
|
||||
});
|
||||
|
||||
for (const view of viewRecords) {
|
||||
const correspondingObjectMetadataItemRefreshed =
|
||||
objectMetadataItemsRefreshed?.find(
|
||||
(item) => item.id === objectMetadataId,
|
||||
);
|
||||
|
||||
if (isDefined(correspondingObjectMetadataItemRefreshed)) {
|
||||
setRecordGroupsFromViewGroups(
|
||||
view.id,
|
||||
view.viewGroups,
|
||||
correspondingObjectMetadataItemRefreshed,
|
||||
);
|
||||
}
|
||||
cache.evict({ id: `Views:${view.id}` });
|
||||
}
|
||||
await refreshObjectMetadataItems();
|
||||
await refreshCoreViewsByObjectMetadataId(objectMetadataId);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ export const visibleRecordFieldsComponentSelector = createComponentSelector({
|
||||
),
|
||||
);
|
||||
|
||||
return filteredVisibleAndReadableRecordFields.toSorted(
|
||||
return [...filteredVisibleAndReadableRecordFields].sort(
|
||||
sortByProperty('position'),
|
||||
);
|
||||
},
|
||||
|
||||
+11
-6
@@ -1,3 +1,4 @@
|
||||
import { useDeleteOneFieldMetadataItem } from '@/object-metadata/hooks/useDeleteOneFieldMetadataItem';
|
||||
import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem';
|
||||
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
|
||||
import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem';
|
||||
@@ -108,11 +109,10 @@ export const SettingsObjectFieldItemTableRow = ({
|
||||
fieldName: fieldMetadataItem.name,
|
||||
});
|
||||
|
||||
const {
|
||||
activateMetadataField,
|
||||
deactivateMetadataField,
|
||||
deleteMetadataField,
|
||||
} = useFieldMetadataItem();
|
||||
const { activateMetadataField, deactivateMetadataField } =
|
||||
useFieldMetadataItem();
|
||||
|
||||
const { deleteOneFieldMetadataItem } = useDeleteOneFieldMetadataItem();
|
||||
|
||||
const handleDisableField = async (
|
||||
activeFieldMetadatItem: FieldMetadataItem,
|
||||
@@ -293,7 +293,12 @@ export const SettingsObjectFieldItemTableRow = ({
|
||||
onActivate={() =>
|
||||
activateMetadataField(fieldMetadataItem.id, objectMetadataItem.id)
|
||||
}
|
||||
onDelete={() => deleteMetadataField(fieldMetadataItem)}
|
||||
onDelete={() =>
|
||||
deleteOneFieldMetadataItem({
|
||||
idToDelete: fieldMetadataItem.id,
|
||||
objectMetadataId: objectMetadataItem.id,
|
||||
})
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<LightIconButton
|
||||
|
||||
@@ -44,7 +44,10 @@ export const EditableFilterChip = ({
|
||||
: recordFilter.label;
|
||||
|
||||
const labelKey = `${fieldNameLabel}`;
|
||||
const labelValue = getRecordFilterLabelValue(recordFilter);
|
||||
const labelValue = getRecordFilterLabelValue({
|
||||
recordFilter,
|
||||
fieldMetadataOptions: fieldMetadataItem.options ?? [],
|
||||
});
|
||||
|
||||
return (
|
||||
<SortOrFilterChip
|
||||
|
||||
+2
-2
@@ -16,7 +16,7 @@ import { type ViewFilter } from '@/views/types/ViewFilter';
|
||||
import { convertViewFilterOperandToCore } from '@/views/utils/convertViewFilterOperandToCore';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { isNull } from '@sniptt/guards';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { isDefined, parseJson } from 'twenty-shared/utils';
|
||||
import { type CoreViewFilter } from '~/generated/graphql';
|
||||
|
||||
export const usePersistViewFilterRecords = () => {
|
||||
@@ -86,7 +86,7 @@ export const usePersistViewFilterRecords = () => {
|
||||
variables: {
|
||||
id: viewFilter.id,
|
||||
input: {
|
||||
value: viewFilter.value,
|
||||
value: parseJson(viewFilter.value),
|
||||
operand: convertViewFilterOperandToCore(viewFilter.operand),
|
||||
positionInViewFilterGroup: viewFilter.positionInViewFilterGroup,
|
||||
viewFilterGroupId: viewFilter.viewFilterGroupId,
|
||||
|
||||
+8
-4
@@ -66,7 +66,7 @@ export const useComputeRecordRelationFilterLabelValue = ({
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return { labelValue: t`Loading...` };
|
||||
return { labelValue: t`: Loading...` };
|
||||
}
|
||||
|
||||
const labelValueItems = [
|
||||
@@ -83,9 +83,13 @@ export const useComputeRecordRelationFilterLabelValue = ({
|
||||
labelValue:
|
||||
labelValueItems.length > 0
|
||||
? getRecordFilterLabelValue({
|
||||
...recordFilter,
|
||||
displayValue: filterDisplayValue,
|
||||
recordFilter: {
|
||||
...recordFilter,
|
||||
displayValue: filterDisplayValue,
|
||||
},
|
||||
})
|
||||
: getRecordFilterLabelValue(recordFilter),
|
||||
: getRecordFilterLabelValue({
|
||||
recordFilter,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
+88
-5
@@ -1,4 +1,15 @@
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { currentRecordFieldsComponentState } from '@/object-record/record-field/states/currentRecordFieldsComponentState';
|
||||
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
|
||||
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
|
||||
import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId';
|
||||
import { coreViewsByObjectMetadataIdFamilySelector } from '@/views/states/coreViewsByObjectMetadataIdFamilySelector';
|
||||
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
|
||||
import { getFilterableFieldsWithVectorSearch } from '@/views/utils/getFilterableFieldsWithVectorSearch';
|
||||
|
||||
import { mapViewFieldToRecordField } from '@/views/utils/mapViewFieldToRecordField';
|
||||
import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
|
||||
import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useFindManyCoreViewsLazyQuery } from '~/generated/graphql';
|
||||
@@ -17,6 +28,22 @@ export const useRefreshCoreViewsByObjectMetadataId = () => {
|
||||
fetchPolicy: 'network-only',
|
||||
});
|
||||
|
||||
if (!isDefined(result.data?.getCoreViews)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const objectMetadataItems = snapshot
|
||||
.getLoadable(objectMetadataItemsState)
|
||||
.getValue();
|
||||
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(objectMetadataItem) => objectMetadataItem.id === objectMetadataId,
|
||||
);
|
||||
|
||||
if (!isDefined(objectMetadataItem)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const coreViewsForObjectMetadataId = snapshot
|
||||
.getLoadable(
|
||||
coreViewsByObjectMetadataIdFamilySelector(objectMetadataId),
|
||||
@@ -24,13 +51,69 @@ export const useRefreshCoreViewsByObjectMetadataId = () => {
|
||||
.getValue();
|
||||
|
||||
if (
|
||||
isDefined(result.data?.getCoreViews) &&
|
||||
!isDeeplyEqual(coreViewsForObjectMetadataId, result.data.getCoreViews)
|
||||
isDeeplyEqual(coreViewsForObjectMetadataId, result.data.getCoreViews)
|
||||
) {
|
||||
set(
|
||||
coreViewsByObjectMetadataIdFamilySelector(objectMetadataId),
|
||||
result.data.getCoreViews,
|
||||
return;
|
||||
}
|
||||
|
||||
set(
|
||||
coreViewsByObjectMetadataIdFamilySelector(objectMetadataId),
|
||||
result.data.getCoreViews,
|
||||
);
|
||||
|
||||
for (const coreView of result.data.getCoreViews) {
|
||||
const existingView = coreViewsForObjectMetadataId.find(
|
||||
(coreViewForObjectMetadata) =>
|
||||
coreViewForObjectMetadata.id === coreView.id,
|
||||
);
|
||||
|
||||
if (!isDefined(existingView)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isDeeplyEqual(coreView.viewFields, existingView.viewFields)) {
|
||||
const view = convertCoreViewToView(coreView);
|
||||
set(
|
||||
currentRecordFieldsComponentState.atomFamily({
|
||||
instanceId: getRecordIndexIdFromObjectNamePluralAndViewId(
|
||||
objectMetadataItem.namePlural,
|
||||
view.id,
|
||||
),
|
||||
}),
|
||||
view.viewFields
|
||||
.filter(isDefined)
|
||||
.map((viewField) => mapViewFieldToRecordField(viewField)),
|
||||
);
|
||||
}
|
||||
|
||||
if (!isDeeplyEqual(coreView.viewFilters, existingView.viewFilters)) {
|
||||
const view = convertCoreViewToView(coreView);
|
||||
set(
|
||||
currentRecordFiltersComponentState.atomFamily({
|
||||
instanceId: getRecordIndexIdFromObjectNamePluralAndViewId(
|
||||
objectMetadataItem.namePlural,
|
||||
view.id,
|
||||
),
|
||||
}),
|
||||
mapViewFiltersToFilters(
|
||||
view.viewFilters,
|
||||
getFilterableFieldsWithVectorSearch(objectMetadataItem),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (!isDeeplyEqual(coreView.viewSorts, existingView.viewSorts)) {
|
||||
const view = convertCoreViewToView(coreView);
|
||||
set(
|
||||
currentRecordSortsComponentState.atomFamily({
|
||||
instanceId: getRecordIndexIdFromObjectNamePluralAndViewId(
|
||||
objectMetadataItem.namePlural,
|
||||
view.id,
|
||||
),
|
||||
}),
|
||||
mapViewSortsToSorts(view.viewSorts),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
[findManyCoreViewsLazy],
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useLazyFindManyRecords } from '@/object-record/hooks/useLazyFindManyRecords';
|
||||
import { findAllViewsOperationSignatureFactory } from '@/prefetch/graphql/operation-signatures/factories/findAllViewsOperationSignatureFactory';
|
||||
|
||||
export const useRefreshCachedViews = () => {
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
const findAllViewsOperationSignature = findAllViewsOperationSignatureFactory({
|
||||
objectMetadataItem: objectMetadataItems.find(
|
||||
(item) => item.nameSingular === CoreObjectNameSingular.View,
|
||||
),
|
||||
});
|
||||
|
||||
const { findManyRecordsLazy: refreshCachedViews } = useLazyFindManyRecords({
|
||||
objectNameSingular: CoreObjectNameSingular.View,
|
||||
filter: findAllViewsOperationSignature.variables.filter,
|
||||
recordGqlFields: findAllViewsOperationSignature.fields,
|
||||
fetchPolicy: 'network-only',
|
||||
});
|
||||
|
||||
return {
|
||||
refreshCachedViews,
|
||||
};
|
||||
};
|
||||
@@ -1,10 +1,17 @@
|
||||
import { type FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { getOperandLabelShort } from '@/object-record/object-filter-dropdown/utils/getOperandLabel';
|
||||
import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter';
|
||||
import { RecordFilterOperand } from '@/object-record/record-filter/types/RecordFilterOperand';
|
||||
import { isEmptinessOperand } from '@/object-record/record-filter/utils/isEmptinessOperand';
|
||||
import { isRecordFilterConsideredEmpty } from '@/object-record/record-filter/utils/isRecordFilterConsideredEmpty';
|
||||
|
||||
export const getRecordFilterLabelValue = (recordFilter: RecordFilter) => {
|
||||
export const getRecordFilterLabelValue = ({
|
||||
recordFilter,
|
||||
fieldMetadataOptions,
|
||||
}: {
|
||||
recordFilter: RecordFilter;
|
||||
fieldMetadataOptions?: FieldMetadataItemOption[];
|
||||
}) => {
|
||||
const operandLabelShort = getOperandLabelShort(recordFilter.operand);
|
||||
const operandIsEmptiness = isEmptinessOperand(recordFilter.operand);
|
||||
const recordFilterIsEmpty = isRecordFilterConsideredEmpty(recordFilter);
|
||||
@@ -22,6 +29,20 @@ export const getRecordFilterLabelValue = (recordFilter: RecordFilter) => {
|
||||
return `${operandLabelShort} ${recordFilter.displayValue}`;
|
||||
}
|
||||
}
|
||||
if (recordFilter.type === 'SELECT' || recordFilter.type === 'MULTI_SELECT') {
|
||||
const valueArray = JSON.parse(recordFilter.value);
|
||||
|
||||
if (!Array.isArray(valueArray)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const optionLabels = valueArray.map(
|
||||
(value) =>
|
||||
fieldMetadataOptions?.find((option) => option.value === value)?.label,
|
||||
);
|
||||
|
||||
return `${operandLabelShort} ${optionLabels.join(', ')}`;
|
||||
}
|
||||
|
||||
if (!operandIsEmptiness && !recordFilterIsEmpty) {
|
||||
return `${operandLabelShort} ${recordFilter.displayValue}`;
|
||||
|
||||
+1
-16
@@ -8,7 +8,6 @@ import {
|
||||
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
|
||||
type RunOnWorkspaceArgs,
|
||||
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { ViewFieldEntity } from 'src/engine/core-modules/view/entities/view-field.entity';
|
||||
import { ViewFilterGroupEntity } from 'src/engine/core-modules/view/entities/view-filter-group.entity';
|
||||
@@ -77,11 +76,10 @@ export class MigrateViewsToCoreCommand extends ActiveOrSuspendedWorkspacesMigrat
|
||||
|
||||
if (options.dryRun) {
|
||||
this.logger.log(
|
||||
`DRY RUN: Would enable IS_CORE_VIEW_SYNCING_ENABLED feature flag for workspace ${workspaceId}`,
|
||||
`DRY RUN: Would migrate views to core schema for workspace ${workspaceId}`,
|
||||
);
|
||||
} else {
|
||||
await queryRunner.commitTransaction();
|
||||
await this.enableCoreViewSyncingFeatureFlag(workspaceId);
|
||||
this.logger.log(
|
||||
`Successfully migrated views to core schema for workspace ${workspaceId}`,
|
||||
);
|
||||
@@ -670,17 +668,4 @@ export class MigrateViewsToCoreCommand extends ActiveOrSuspendedWorkspacesMigrat
|
||||
await repository.insert(coreViewFilterGroup);
|
||||
}
|
||||
}
|
||||
|
||||
private async enableCoreViewSyncingFeatureFlag(
|
||||
workspaceId: string,
|
||||
): Promise<void> {
|
||||
await this.featureFlagService.enableFeatureFlags(
|
||||
[FeatureFlagKey.IS_CORE_VIEW_SYNCING_ENABLED],
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`Enabled IS_CORE_VIEW_SYNCING_ENABLED feature flag for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+26
-2
@@ -16,6 +16,13 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import { CreateViewFieldInput } from 'src/engine/core-modules/view/dtos/inputs/create-view-field.input';
|
||||
import { UpdateViewFieldInput } from 'src/engine/core-modules/view/dtos/inputs/update-view-field.input';
|
||||
import { type ViewFieldEntity } from 'src/engine/core-modules/view/entities/view-field.entity';
|
||||
import {
|
||||
generateViewFieldExceptionMessage,
|
||||
generateViewFieldUserFriendlyExceptionMessage,
|
||||
ViewFieldException,
|
||||
ViewFieldExceptionCode,
|
||||
ViewFieldExceptionMessageKey,
|
||||
} from 'src/engine/core-modules/view/exceptions/view-field.exception';
|
||||
import { ViewFieldRestApiExceptionFilter } from 'src/engine/core-modules/view/filters/view-field-rest-api-exception.filter';
|
||||
import { ViewFieldService } from 'src/engine/core-modules/view/services/view-field.service';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
@@ -44,8 +51,25 @@ export class ViewFieldController {
|
||||
async findOne(
|
||||
@Param('id') id: string,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
): Promise<ViewFieldEntity | null> {
|
||||
return this.viewFieldService.findById(id, workspace.id);
|
||||
): Promise<ViewFieldEntity> {
|
||||
const viewField = await this.viewFieldService.findById(id, workspace.id);
|
||||
|
||||
if (!isDefined(viewField)) {
|
||||
throw new ViewFieldException(
|
||||
generateViewFieldExceptionMessage(
|
||||
ViewFieldExceptionMessageKey.VIEW_FIELD_NOT_FOUND,
|
||||
id,
|
||||
),
|
||||
ViewFieldExceptionCode.VIEW_FIELD_NOT_FOUND,
|
||||
{
|
||||
userFriendlyMessage: generateViewFieldUserFriendlyExceptionMessage(
|
||||
ViewFieldExceptionMessageKey.VIEW_FIELD_NOT_FOUND,
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return viewField;
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
|
||||
+30
-2
@@ -16,6 +16,13 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import { CreateViewFilterGroupInput } from 'src/engine/core-modules/view/dtos/inputs/create-view-filter-group.input';
|
||||
import { UpdateViewFilterGroupInput } from 'src/engine/core-modules/view/dtos/inputs/update-view-filter-group.input';
|
||||
import { type ViewFilterGroupDTO } from 'src/engine/core-modules/view/dtos/view-filter-group.dto';
|
||||
import {
|
||||
generateViewFilterGroupExceptionMessage,
|
||||
generateViewFilterGroupUserFriendlyExceptionMessage,
|
||||
ViewFilterGroupException,
|
||||
ViewFilterGroupExceptionCode,
|
||||
ViewFilterGroupExceptionMessageKey,
|
||||
} from 'src/engine/core-modules/view/exceptions/view-filter-group.exception';
|
||||
import { ViewFilterGroupRestApiExceptionFilter } from 'src/engine/core-modules/view/filters/view-filter-group-rest-api-exception.filter';
|
||||
import { ViewFilterGroupService } from 'src/engine/core-modules/view/services/view-filter-group.service';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
@@ -46,8 +53,29 @@ export class ViewFilterGroupController {
|
||||
async findOne(
|
||||
@Param('id') id: string,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
): Promise<ViewFilterGroupDTO | null> {
|
||||
return this.viewFilterGroupService.findById(id, workspace.id);
|
||||
): Promise<ViewFilterGroupDTO> {
|
||||
const viewFilterGroup = await this.viewFilterGroupService.findById(
|
||||
id,
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
if (!isDefined(viewFilterGroup)) {
|
||||
throw new ViewFilterGroupException(
|
||||
generateViewFilterGroupExceptionMessage(
|
||||
ViewFilterGroupExceptionMessageKey.VIEW_FILTER_GROUP_NOT_FOUND,
|
||||
id,
|
||||
),
|
||||
ViewFilterGroupExceptionCode.VIEW_FILTER_GROUP_NOT_FOUND,
|
||||
{
|
||||
userFriendlyMessage:
|
||||
generateViewFilterGroupUserFriendlyExceptionMessage(
|
||||
ViewFilterGroupExceptionMessageKey.VIEW_FILTER_GROUP_NOT_FOUND,
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return viewFilterGroup;
|
||||
}
|
||||
|
||||
@Post()
|
||||
|
||||
+27
-3
@@ -15,7 +15,14 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { CreateViewFilterInput } from 'src/engine/core-modules/view/dtos/inputs/create-view-filter.input';
|
||||
import { UpdateViewFilterInput } from 'src/engine/core-modules/view/dtos/inputs/update-view-filter.input';
|
||||
import { type ViewFilterDTO } from 'src/engine/core-modules/view/dtos/view-filter.dto';
|
||||
import { ViewFilterDTO } from 'src/engine/core-modules/view/dtos/view-filter.dto';
|
||||
import {
|
||||
generateViewFilterExceptionMessage,
|
||||
generateViewFilterUserFriendlyExceptionMessage,
|
||||
ViewFilterException,
|
||||
ViewFilterExceptionCode,
|
||||
ViewFilterExceptionMessageKey,
|
||||
} from 'src/engine/core-modules/view/exceptions/view-filter.exception';
|
||||
import { ViewFilterRestApiExceptionFilter } from 'src/engine/core-modules/view/filters/view-filter-rest-api-exception.filter';
|
||||
import { ViewFilterService } from 'src/engine/core-modules/view/services/view-filter.service';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
@@ -44,8 +51,25 @@ export class ViewFilterController {
|
||||
async findOne(
|
||||
@Param('id') id: string,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
): Promise<ViewFilterDTO | null> {
|
||||
return this.viewFilterService.findById(id, workspace.id);
|
||||
): Promise<ViewFilterDTO> {
|
||||
const viewFilter = await this.viewFilterService.findById(id, workspace.id);
|
||||
|
||||
if (!isDefined(viewFilter)) {
|
||||
throw new ViewFilterException(
|
||||
generateViewFilterExceptionMessage(
|
||||
ViewFilterExceptionMessageKey.VIEW_FILTER_NOT_FOUND,
|
||||
id,
|
||||
),
|
||||
ViewFilterExceptionCode.VIEW_FILTER_NOT_FOUND,
|
||||
{
|
||||
userFriendlyMessage: generateViewFilterUserFriendlyExceptionMessage(
|
||||
ViewFilterExceptionMessageKey.VIEW_FILTER_NOT_FOUND,
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return viewFilter;
|
||||
}
|
||||
|
||||
@Post()
|
||||
|
||||
+26
-2
@@ -16,6 +16,13 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import { CreateViewGroupInput } from 'src/engine/core-modules/view/dtos/inputs/create-view-group.input';
|
||||
import { UpdateViewGroupInput } from 'src/engine/core-modules/view/dtos/inputs/update-view-group.input';
|
||||
import { type ViewGroupDTO } from 'src/engine/core-modules/view/dtos/view-group.dto';
|
||||
import {
|
||||
generateViewGroupExceptionMessage,
|
||||
generateViewGroupUserFriendlyExceptionMessage,
|
||||
ViewGroupException,
|
||||
ViewGroupExceptionCode,
|
||||
ViewGroupExceptionMessageKey,
|
||||
} from 'src/engine/core-modules/view/exceptions/view-group.exception';
|
||||
import { ViewGroupRestApiExceptionFilter } from 'src/engine/core-modules/view/filters/view-group-rest-api-exception.filter';
|
||||
import { ViewGroupService } from 'src/engine/core-modules/view/services/view-group.service';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
@@ -44,8 +51,25 @@ export class ViewGroupController {
|
||||
async findOne(
|
||||
@Param('id') id: string,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
): Promise<ViewGroupDTO | null> {
|
||||
return this.viewGroupService.findById(id, workspace.id);
|
||||
): Promise<ViewGroupDTO> {
|
||||
const viewGroup = await this.viewGroupService.findById(id, workspace.id);
|
||||
|
||||
if (!isDefined(viewGroup)) {
|
||||
throw new ViewGroupException(
|
||||
generateViewGroupExceptionMessage(
|
||||
ViewGroupExceptionMessageKey.VIEW_GROUP_NOT_FOUND,
|
||||
id,
|
||||
),
|
||||
ViewGroupExceptionCode.VIEW_GROUP_NOT_FOUND,
|
||||
{
|
||||
userFriendlyMessage: generateViewGroupUserFriendlyExceptionMessage(
|
||||
ViewGroupExceptionMessageKey.VIEW_GROUP_NOT_FOUND,
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return viewGroup;
|
||||
}
|
||||
|
||||
@Post()
|
||||
|
||||
+26
-2
@@ -16,6 +16,13 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import { CreateViewSortInput } from 'src/engine/core-modules/view/dtos/inputs/create-view-sort.input';
|
||||
import { UpdateViewSortInput } from 'src/engine/core-modules/view/dtos/inputs/update-view-sort.input';
|
||||
import { type ViewSortDTO } from 'src/engine/core-modules/view/dtos/view-sort.dto';
|
||||
import {
|
||||
ViewSortException,
|
||||
ViewSortExceptionCode,
|
||||
ViewSortExceptionMessageKey,
|
||||
generateViewSortExceptionMessage,
|
||||
generateViewSortUserFriendlyExceptionMessage,
|
||||
} from 'src/engine/core-modules/view/exceptions/view-sort.exception';
|
||||
import { ViewSortRestApiExceptionFilter } from 'src/engine/core-modules/view/filters/view-sort-rest-api-exception.filter';
|
||||
import { ViewSortService } from 'src/engine/core-modules/view/services/view-sort.service';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
@@ -44,8 +51,25 @@ export class ViewSortController {
|
||||
async findOne(
|
||||
@Param('id') id: string,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
): Promise<ViewSortDTO | null> {
|
||||
return this.viewSortService.findById(id, workspace.id);
|
||||
): Promise<ViewSortDTO> {
|
||||
const viewSort = await this.viewSortService.findById(id, workspace.id);
|
||||
|
||||
if (!isDefined(viewSort)) {
|
||||
throw new ViewSortException(
|
||||
generateViewSortExceptionMessage(
|
||||
ViewSortExceptionMessageKey.VIEW_SORT_NOT_FOUND,
|
||||
id,
|
||||
),
|
||||
ViewSortExceptionCode.VIEW_SORT_NOT_FOUND,
|
||||
{
|
||||
userFriendlyMessage: generateViewSortUserFriendlyExceptionMessage(
|
||||
ViewSortExceptionMessageKey.VIEW_SORT_NOT_FOUND,
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return viewSort;
|
||||
}
|
||||
|
||||
@Post()
|
||||
|
||||
@@ -16,6 +16,13 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import { CreateViewInput } from 'src/engine/core-modules/view/dtos/inputs/create-view.input';
|
||||
import { UpdateViewInput } from 'src/engine/core-modules/view/dtos/inputs/update-view.input';
|
||||
import { type ViewDTO } from 'src/engine/core-modules/view/dtos/view.dto';
|
||||
import {
|
||||
generateViewExceptionMessage,
|
||||
generateViewUserFriendlyExceptionMessage,
|
||||
ViewException,
|
||||
ViewExceptionCode,
|
||||
ViewExceptionMessageKey,
|
||||
} from 'src/engine/core-modules/view/exceptions/view.exception';
|
||||
import { ViewRestApiExceptionFilter } from 'src/engine/core-modules/view/filters/view-rest-api-exception.filter';
|
||||
import { ViewService } from 'src/engine/core-modules/view/services/view.service';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
@@ -47,8 +54,25 @@ export class ViewController {
|
||||
async findOne(
|
||||
@Param('id') id: string,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
): Promise<ViewDTO | null> {
|
||||
return this.viewService.findById(id, workspace.id);
|
||||
): Promise<ViewDTO> {
|
||||
const view = await this.viewService.findById(id, workspace.id);
|
||||
|
||||
if (!isDefined(view)) {
|
||||
throw new ViewException(
|
||||
generateViewExceptionMessage(
|
||||
ViewExceptionMessageKey.VIEW_NOT_FOUND,
|
||||
id,
|
||||
),
|
||||
ViewExceptionCode.VIEW_NOT_FOUND,
|
||||
{
|
||||
userFriendlyMessage: generateViewUserFriendlyExceptionMessage(
|
||||
ViewExceptionMessageKey.VIEW_NOT_FOUND,
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Post()
|
||||
|
||||
@@ -25,6 +25,7 @@ export enum ViewFieldExceptionMessageKey {
|
||||
VIEW_FIELD_NOT_FOUND = 'VIEW_FIELD_NOT_FOUND',
|
||||
INVALID_VIEW_FIELD_DATA = 'INVALID_VIEW_FIELD_DATA',
|
||||
FIELD_METADATA_ID_REQUIRED = 'FIELD_METADATA_ID_REQUIRED',
|
||||
VIEW_FIELD_ALREADY_EXISTS = 'VIEW_FIELD_ALREADY_EXISTS',
|
||||
}
|
||||
|
||||
export const generateViewFieldExceptionMessage = (
|
||||
@@ -42,6 +43,8 @@ export const generateViewFieldExceptionMessage = (
|
||||
return `Invalid view field data${id ? ` for view field id: ${id}` : ''}`;
|
||||
case ViewFieldExceptionMessageKey.FIELD_METADATA_ID_REQUIRED:
|
||||
return 'FieldMetadataId is required';
|
||||
case ViewFieldExceptionMessageKey.VIEW_FIELD_ALREADY_EXISTS:
|
||||
return 'View field already exists';
|
||||
default:
|
||||
assertUnreachable(key);
|
||||
}
|
||||
@@ -57,5 +60,7 @@ export const generateViewFieldUserFriendlyExceptionMessage = (
|
||||
return t`ViewId is required to create a view field.`;
|
||||
case ViewFieldExceptionMessageKey.FIELD_METADATA_ID_REQUIRED:
|
||||
return t`FieldMetadataId is required to create a view field.`;
|
||||
case ViewFieldExceptionMessageKey.VIEW_FIELD_ALREADY_EXISTS:
|
||||
return t`View field already exists.`;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -107,9 +107,29 @@ export class ViewFieldService {
|
||||
);
|
||||
}
|
||||
|
||||
const viewField = this.viewFieldRepository.create(viewFieldData);
|
||||
try {
|
||||
const viewField = this.viewFieldRepository.create(viewFieldData);
|
||||
|
||||
return this.viewFieldRepository.save(viewField);
|
||||
return await this.viewFieldRepository.save(viewField);
|
||||
} catch (error) {
|
||||
if (
|
||||
error.message.includes('duplicate key value violates unique constraint')
|
||||
) {
|
||||
throw new ViewFieldException(
|
||||
generateViewFieldExceptionMessage(
|
||||
ViewFieldExceptionMessageKey.VIEW_FIELD_ALREADY_EXISTS,
|
||||
),
|
||||
ViewFieldExceptionCode.INVALID_VIEW_FIELD_DATA,
|
||||
{
|
||||
userFriendlyMessage: generateViewFieldUserFriendlyExceptionMessage(
|
||||
ViewFieldExceptionMessageKey.VIEW_FIELD_ALREADY_EXISTS,
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async update(
|
||||
|
||||
+2
-2
@@ -10,6 +10,7 @@ import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
|
||||
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
||||
import { ActorModule } from 'src/engine/core-modules/actor/actor.module';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { CoreViewModule } from 'src/engine/core-modules/view/view.module';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||
import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto';
|
||||
@@ -35,7 +36,6 @@ import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-
|
||||
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
||||
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module';
|
||||
import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-v2.module';
|
||||
import { ViewModule } from 'src/modules/view/view.module';
|
||||
|
||||
import { FieldMetadataEntity } from './field-metadata.entity';
|
||||
|
||||
@@ -60,7 +60,7 @@ import { FieldMetadataService } from './services/field-metadata.service';
|
||||
TypeORMModule,
|
||||
ActorModule,
|
||||
FeatureFlagModule,
|
||||
ViewModule,
|
||||
CoreViewModule,
|
||||
PermissionsModule,
|
||||
WorkspaceMetadataCacheModule,
|
||||
WorkspaceMigrationV2Module,
|
||||
|
||||
+14
-4
@@ -1,8 +1,11 @@
|
||||
import { type EachTestingContext } from 'twenty-shared/testing';
|
||||
|
||||
import { type ViewFieldService } from 'src/engine/core-modules/view/services/view-field.service';
|
||||
import { type ViewFilterService } from 'src/engine/core-modules/view/services/view-filter.service';
|
||||
import { type ViewGroupService } from 'src/engine/core-modules/view/services/view-group.service';
|
||||
import { type ViewService } from 'src/engine/core-modules/view/services/view.service';
|
||||
import { type FieldMetadataDefaultOption } from 'src/engine/metadata-modules/field-metadata/dtos/options.input';
|
||||
import { FieldMetadataRelatedRecordsService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-related-records.service';
|
||||
import { type TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
|
||||
type GetOptionsDifferencesTestContext = EachTestingContext<{
|
||||
oldOptions: FieldMetadataDefaultOption[];
|
||||
@@ -21,11 +24,18 @@ type GetOptionsDifferencesTestContext = EachTestingContext<{
|
||||
describe('FieldMetadataRelatedRecordsService', () => {
|
||||
describe('getOptionsDifferences', () => {
|
||||
let service: FieldMetadataRelatedRecordsService;
|
||||
let twentyORMGlobalManager: TwentyORMGlobalManager;
|
||||
let viewService: ViewService = {} as ViewService;
|
||||
let viewFieldService: ViewFieldService = {} as ViewFieldService;
|
||||
let viewFilterService: ViewFilterService = {} as ViewFilterService;
|
||||
let viewGroupService: ViewGroupService = {} as ViewGroupService;
|
||||
|
||||
beforeEach(() => {
|
||||
twentyORMGlobalManager = {} as TwentyORMGlobalManager;
|
||||
service = new FieldMetadataRelatedRecordsService(twentyORMGlobalManager);
|
||||
service = new FieldMetadataRelatedRecordsService(
|
||||
viewService,
|
||||
viewFieldService,
|
||||
viewFilterService,
|
||||
viewGroupService,
|
||||
);
|
||||
});
|
||||
|
||||
const testCases: GetOptionsDifferencesTestContext[] = [
|
||||
|
||||
+138
-153
@@ -1,11 +1,16 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import { MAX_OPTIONS_TO_DISPLAY } from 'twenty-shared/constants';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { isDefined, parseJson } from 'twenty-shared/utils';
|
||||
import { In } from 'typeorm';
|
||||
|
||||
import { settings } from 'src/engine/constants/settings';
|
||||
import { ViewGroupEntity } from 'src/engine/core-modules/view/entities/view-group.entity';
|
||||
import { ViewEntity } from 'src/engine/core-modules/view/entities/view.entity';
|
||||
import { ViewKey } from 'src/engine/core-modules/view/enums/view-key.enum';
|
||||
import { ViewFieldService } from 'src/engine/core-modules/view/services/view-field.service';
|
||||
import { ViewFilterService } from 'src/engine/core-modules/view/services/view-filter.service';
|
||||
import { ViewGroupService } from 'src/engine/core-modules/view/services/view-group.service';
|
||||
import { ViewService } from 'src/engine/core-modules/view/services/view.service';
|
||||
import {
|
||||
type FieldMetadataComplexOption,
|
||||
type FieldMetadataDefaultOption,
|
||||
@@ -17,12 +22,6 @@ import {
|
||||
} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception';
|
||||
import { isSelectFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-select-field-metadata-type.util';
|
||||
import { type SelectOrMultiSelectFieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/utils/is-select-or-multi-select-field-metadata.util';
|
||||
import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
|
||||
import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { type ViewFilterWorkspaceEntity } from 'src/modules/view/standard-objects/view-filter.workspace-entity';
|
||||
import { type ViewGroupWorkspaceEntity } from 'src/modules/view/standard-objects/view-group.workspace-entity';
|
||||
import { type ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
|
||||
|
||||
type Differences<T> = {
|
||||
created: T[];
|
||||
@@ -37,7 +36,10 @@ type GetOptionsDifferences = Differences<
|
||||
@Injectable()
|
||||
export class FieldMetadataRelatedRecordsService {
|
||||
constructor(
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
private readonly viewService: ViewService,
|
||||
private readonly viewFieldService: ViewFieldService,
|
||||
private readonly viewFilterService: ViewFilterService,
|
||||
private readonly viewGroupService: ViewGroupService,
|
||||
) {}
|
||||
|
||||
public async updateRelatedViewGroups(
|
||||
@@ -51,9 +53,9 @@ export class FieldMetadataRelatedRecordsService {
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const views = await this.getFieldMetadataViewWithRelation(
|
||||
newFieldMetadata,
|
||||
'viewGroups',
|
||||
const objectMetadataViews = await this.viewService.findByObjectMetadataId(
|
||||
newFieldMetadata.workspaceId,
|
||||
newFieldMetadata.objectMetadataId,
|
||||
);
|
||||
|
||||
const { created, updated, deleted } = this.getOptionsDifferences(
|
||||
@@ -61,38 +63,38 @@ export class FieldMetadataRelatedRecordsService {
|
||||
newFieldMetadata.options ?? [],
|
||||
);
|
||||
|
||||
const viewGroupRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewGroupWorkspaceEntity>(
|
||||
newFieldMetadata.workspaceId,
|
||||
'viewGroup',
|
||||
);
|
||||
|
||||
for (const view of views) {
|
||||
for (const view of objectMetadataViews) {
|
||||
if (view.viewGroups.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const valuesToDelete = deleted.map((option) => option.value);
|
||||
|
||||
if (valuesToDelete.length > 0) {
|
||||
await viewGroupRepository.delete({
|
||||
fieldMetadataId: newFieldMetadata.id,
|
||||
fieldValue: In(valuesToDelete),
|
||||
});
|
||||
for (const valueToDelete of valuesToDelete) {
|
||||
const viewGroupsToDelete = view.viewGroups.filter(
|
||||
(group) => group.fieldValue === valueToDelete,
|
||||
);
|
||||
|
||||
for (const viewGroup of viewGroupsToDelete) {
|
||||
await this.viewGroupService.destroy(
|
||||
viewGroup.id,
|
||||
newFieldMetadata.workspaceId,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
const maxPosition = this.getMaxPosition(view.viewGroups);
|
||||
|
||||
const viewGroupsToCreate = created.map((option, index) =>
|
||||
viewGroupRepository.create({
|
||||
for (const [index, option] of created.entries()) {
|
||||
this.viewGroupService.create({
|
||||
fieldMetadataId: newFieldMetadata.id,
|
||||
fieldValue: option.value,
|
||||
position: maxPosition + index,
|
||||
isVisible: true,
|
||||
viewId: view.id,
|
||||
}),
|
||||
);
|
||||
|
||||
if (viewGroupsToCreate.length > 0) {
|
||||
await viewGroupRepository.insert(viewGroupsToCreate);
|
||||
workspaceId: newFieldMetadata.workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
for (const { old: oldOption, new: newOption } of updated) {
|
||||
@@ -106,37 +108,54 @@ export class FieldMetadataRelatedRecordsService {
|
||||
);
|
||||
}
|
||||
|
||||
await viewGroupRepository.update(
|
||||
{ id: existingViewGroup.id },
|
||||
this.viewGroupService.update(
|
||||
existingViewGroup.id,
|
||||
newFieldMetadata.workspaceId,
|
||||
{ fieldValue: newOption.value },
|
||||
);
|
||||
}
|
||||
|
||||
await this.syncNoValueViewGroup(
|
||||
newFieldMetadata,
|
||||
view,
|
||||
viewGroupRepository,
|
||||
);
|
||||
await this.syncNoValueViewGroup(newFieldMetadata, view);
|
||||
}
|
||||
}
|
||||
|
||||
private computeViewFilterDisplayValue(
|
||||
newViewFilterOptions: FieldMetadataDefaultOption[],
|
||||
): string {
|
||||
if (newViewFilterOptions.length > MAX_OPTIONS_TO_DISPLAY) {
|
||||
return `${newViewFilterOptions.length} options`;
|
||||
}
|
||||
public async resetViewKanbanAggregateOperation(
|
||||
fieldMetadata: Pick<
|
||||
FieldMetadataEntity,
|
||||
'id' | 'workspaceId' | 'objectMetadataId'
|
||||
>,
|
||||
): Promise<void> {
|
||||
const views = await this.viewService.findByObjectMetadataId(
|
||||
fieldMetadata.workspaceId,
|
||||
fieldMetadata.objectMetadataId,
|
||||
);
|
||||
|
||||
return newViewFilterOptions.map((option) => option.label).join(', ');
|
||||
const viewsHavingFieldAsAggregateOperation = views.filter(
|
||||
(view) =>
|
||||
view.kanbanAggregateOperationFieldMetadataId === fieldMetadata.id,
|
||||
);
|
||||
|
||||
for (const view of viewsHavingFieldAsAggregateOperation) {
|
||||
await this.viewService.update(view.id, fieldMetadata.workspaceId, {
|
||||
kanbanAggregateOperationFieldMetadataId: null,
|
||||
kanbanAggregateOperation: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async updateRelatedViewFilters(
|
||||
oldFieldMetadata: SelectOrMultiSelectFieldMetadataEntity,
|
||||
newFieldMetadata: SelectOrMultiSelectFieldMetadataEntity,
|
||||
): Promise<void> {
|
||||
const views = await this.getFieldMetadataViewWithRelation(
|
||||
newFieldMetadata,
|
||||
'viewFilters',
|
||||
const views = await this.viewService.findByObjectMetadataId(
|
||||
newFieldMetadata.workspaceId,
|
||||
newFieldMetadata.objectMetadataId,
|
||||
);
|
||||
|
||||
const fieldMetadataViews = views.filter((view) =>
|
||||
view.viewFilters.some(
|
||||
(filter) => filter.fieldMetadataId === newFieldMetadata.id,
|
||||
),
|
||||
);
|
||||
|
||||
const alsoCompareLabel = true;
|
||||
@@ -156,29 +175,29 @@ export class FieldMetadataRelatedRecordsService {
|
||||
return;
|
||||
}
|
||||
|
||||
const viewFilterRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewFilterWorkspaceEntity>(
|
||||
newFieldMetadata.workspaceId,
|
||||
'viewFilter',
|
||||
);
|
||||
|
||||
for (const filter of views) {
|
||||
if (filter.viewFilters.length === 0) {
|
||||
for (const view of fieldMetadataViews) {
|
||||
if (view.viewFilters.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const viewFilter of filter.viewFilters) {
|
||||
const viewFilterValue = parseJson<string[]>(viewFilter.value);
|
||||
for (const viewFilter of view.viewFilters) {
|
||||
if (!isDefined(viewFilter.value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Note below assertion could be removed after https://github.com/twentyhq/core-team-issues/issues/1009 completion
|
||||
if (!isDefined(viewFilterValue) || !Array.isArray(viewFilterValue)) {
|
||||
// TODO: all view filter value should be stored as JSON, this is ongoing work (we are missing a command to migrate the data)
|
||||
const parsedValue = isNonEmptyString(viewFilter.value)
|
||||
? parseJson(viewFilter.value)
|
||||
: viewFilter.value;
|
||||
|
||||
if (!isDefined(parsedValue) || !Array.isArray(parsedValue)) {
|
||||
throw new FieldMetadataException(
|
||||
`Unexpected invalid view filter value for filter ${viewFilter.id}`,
|
||||
FieldMetadataExceptionCode.INTERNAL_SERVER_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
const viewFilterOptions = viewFilterValue
|
||||
const viewFilterOptions = parsedValue
|
||||
.map((value) => {
|
||||
if (!isDefined(oldFieldMetadata.options)) {
|
||||
return undefined;
|
||||
@@ -198,7 +217,10 @@ export class FieldMetadataRelatedRecordsService {
|
||||
);
|
||||
|
||||
if (afterDeleteViewFilterOptions.length === 0) {
|
||||
await viewFilterRepository.delete({ id: viewFilter.id });
|
||||
await this.viewFilterService.destroy(
|
||||
viewFilter.id,
|
||||
newFieldMetadata.workspaceId,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -213,18 +235,15 @@ export class FieldMetadataRelatedRecordsService {
|
||||
: viewFilterOption;
|
||||
});
|
||||
|
||||
const displayValue = this.computeViewFilterDisplayValue(
|
||||
afterUpdateAndDeleteViewFilterOptions,
|
||||
);
|
||||
const value = JSON.stringify(
|
||||
afterUpdateAndDeleteViewFilterOptions.map((option) => option.value),
|
||||
const value = afterUpdateAndDeleteViewFilterOptions.map(
|
||||
(option) => option.value,
|
||||
);
|
||||
|
||||
await viewFilterRepository.update(
|
||||
{ id: viewFilter.id },
|
||||
await this.viewFilterService.update(
|
||||
viewFilter.id,
|
||||
newFieldMetadata.workspaceId,
|
||||
{
|
||||
value,
|
||||
displayValue,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -233,8 +252,7 @@ export class FieldMetadataRelatedRecordsService {
|
||||
|
||||
async syncNoValueViewGroup(
|
||||
fieldMetadata: SelectOrMultiSelectFieldMetadataEntity,
|
||||
view: ViewWorkspaceEntity,
|
||||
viewGroupRepository: WorkspaceRepository<ViewGroupWorkspaceEntity>,
|
||||
view: ViewEntity,
|
||||
): Promise<void> {
|
||||
const noValueGroup = view.viewGroups.find(
|
||||
(group) => group.fieldValue === '',
|
||||
@@ -242,17 +260,20 @@ export class FieldMetadataRelatedRecordsService {
|
||||
|
||||
if (fieldMetadata.isNullable && !noValueGroup) {
|
||||
const maxPosition = this.getMaxPosition(view.viewGroups);
|
||||
const newGroup = viewGroupRepository.create({
|
||||
|
||||
this.viewGroupService.create({
|
||||
fieldMetadataId: fieldMetadata.id,
|
||||
fieldValue: '',
|
||||
position: maxPosition + 1,
|
||||
isVisible: true,
|
||||
viewId: view.id,
|
||||
workspaceId: fieldMetadata.workspaceId,
|
||||
});
|
||||
|
||||
await viewGroupRepository.insert(newGroup);
|
||||
} else if (!fieldMetadata.isNullable && noValueGroup) {
|
||||
await viewGroupRepository.delete({ id: noValueGroup.id });
|
||||
await this.viewGroupService.destroy(
|
||||
noValueGroup.id,
|
||||
fieldMetadata.workspaceId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,27 +327,7 @@ export class FieldMetadataRelatedRecordsService {
|
||||
return differences;
|
||||
}
|
||||
|
||||
private async getFieldMetadataViewWithRelation(
|
||||
fieldMetadata: SelectOrMultiSelectFieldMetadataEntity,
|
||||
relation: keyof Pick<ViewWorkspaceEntity, 'viewGroups' | 'viewFilters'>,
|
||||
): Promise<ViewWorkspaceEntity[]> {
|
||||
const viewRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewWorkspaceEntity>(
|
||||
fieldMetadata.workspaceId,
|
||||
'view',
|
||||
);
|
||||
|
||||
return viewRepository.find({
|
||||
where: {
|
||||
[relation]: {
|
||||
fieldMetadataId: fieldMetadata.id,
|
||||
},
|
||||
},
|
||||
relations: [relation],
|
||||
});
|
||||
}
|
||||
|
||||
private getMaxPosition(viewGroups: ViewGroupWorkspaceEntity[]): number {
|
||||
private getMaxPosition(viewGroups: ViewGroupEntity[]): number {
|
||||
return viewGroups.reduce((max, group) => Math.max(max, group.position), 0);
|
||||
}
|
||||
|
||||
@@ -334,69 +335,53 @@ export class FieldMetadataRelatedRecordsService {
|
||||
createdFieldMetadatas: FieldMetadataEntity[],
|
||||
workspaceId: string,
|
||||
) {
|
||||
const workspaceDataSource =
|
||||
await this.twentyORMGlobalManager.getDataSourceForWorkspace({
|
||||
workspaceId,
|
||||
});
|
||||
const views = await this.viewService.findByWorkspaceId(workspaceId);
|
||||
|
||||
await workspaceDataSource.transaction(
|
||||
async (workspaceEntityManager: WorkspaceEntityManager) => {
|
||||
const viewsRepository = workspaceEntityManager.getRepository('view', {
|
||||
shouldBypassPermissionChecks: true,
|
||||
});
|
||||
for (const createdFieldMetadata of createdFieldMetadatas) {
|
||||
const objectViews = views.filter(
|
||||
(view) =>
|
||||
view.objectMetadataId === createdFieldMetadata.objectMetadataId,
|
||||
);
|
||||
|
||||
const viewFieldsRepository = workspaceEntityManager.getRepository(
|
||||
'viewField',
|
||||
{
|
||||
shouldBypassPermissionChecks: true,
|
||||
},
|
||||
);
|
||||
if (objectViews.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const createdFieldMetadata of createdFieldMetadatas) {
|
||||
const views = await viewsRepository.find({
|
||||
where: {
|
||||
objectMetadataId: createdFieldMetadata.objectMetadataId,
|
||||
},
|
||||
});
|
||||
const indexView = objectViews.find((view) => view.key === ViewKey.INDEX);
|
||||
|
||||
if (!isEmpty(views)) {
|
||||
const view = views[0];
|
||||
const existingViewFields = await viewFieldsRepository.find({
|
||||
where: {
|
||||
viewId: view.id,
|
||||
},
|
||||
});
|
||||
if (!indexView) {
|
||||
return;
|
||||
}
|
||||
const isVisible =
|
||||
indexView.viewFields.length < settings.maxVisibleViewFields;
|
||||
|
||||
const isVisible =
|
||||
existingViewFields.length < settings.maxVisibleViewFields;
|
||||
const createdFieldIsAlreadyInView = indexView.viewFields.some(
|
||||
(existingViewField) =>
|
||||
existingViewField.fieldMetadataId === createdFieldMetadata.id,
|
||||
);
|
||||
|
||||
const createdFieldIsAlreadyInView = existingViewFields.some(
|
||||
(existingViewField) =>
|
||||
existingViewField.fieldMetadataId === createdFieldMetadata.id,
|
||||
);
|
||||
if (createdFieldIsAlreadyInView) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!createdFieldIsAlreadyInView) {
|
||||
const lastPosition = existingViewFields
|
||||
.map((viewField) => viewField.position)
|
||||
.reduce((acc, position) => {
|
||||
if (position > acc) {
|
||||
return position;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, -1);
|
||||
|
||||
await viewFieldsRepository.insert({
|
||||
fieldMetadataId: createdFieldMetadata.id,
|
||||
position: lastPosition + 1,
|
||||
isVisible,
|
||||
size: 180,
|
||||
viewId: view.id,
|
||||
});
|
||||
}
|
||||
const lastPosition = indexView.viewFields
|
||||
.map((viewField) => viewField.position)
|
||||
.reduce((acc, position) => {
|
||||
if (position > acc) {
|
||||
return position;
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return acc;
|
||||
}, -1);
|
||||
|
||||
await this.viewFieldService.create({
|
||||
fieldMetadataId: createdFieldMetadata.id,
|
||||
position: lastPosition + 1,
|
||||
isVisible,
|
||||
size: 180,
|
||||
viewId: indexView.id,
|
||||
workspaceId: createdFieldMetadata.workspaceId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-7
@@ -72,7 +72,6 @@ import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
||||
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service';
|
||||
import { ViewService } from 'src/modules/view/services/view.service';
|
||||
|
||||
type GenerateMigrationArgs = {
|
||||
fieldMetadata: FieldMetadataEntity<
|
||||
@@ -95,7 +94,6 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
private readonly fieldMetadataRelatedRecordsService: FieldMetadataRelatedRecordsService,
|
||||
private readonly viewService: ViewService,
|
||||
private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService,
|
||||
private readonly featureFlagService: FeatureFlagService,
|
||||
private readonly fieldMetadataValidationService: FieldMetadataValidationService,
|
||||
@@ -468,6 +466,10 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
);
|
||||
}
|
||||
|
||||
await this.fieldMetadataRelatedRecordsService.resetViewKanbanAggregateOperation(
|
||||
fieldMetadata,
|
||||
);
|
||||
|
||||
if (isFieldMetadataTypeRelation(fieldMetadata)) {
|
||||
const fieldMetadataIdsToDelete: string[] = [];
|
||||
const isRelationTargetMorphRelation = isFieldMetadataTypeMorphRelation(
|
||||
@@ -607,11 +609,6 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
|
||||
await this.viewService.resetKanbanAggregateOperationByFieldMetadataId({
|
||||
workspaceId,
|
||||
fieldMetadataId: fieldMetadata.id,
|
||||
});
|
||||
|
||||
await this.workspaceMetadataVersionService.incrementMetadataVersion(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
+2
@@ -12,6 +12,7 @@ import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
||||
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { ViewEntity } from 'src/engine/core-modules/view/entities/view.entity';
|
||||
import { CoreViewModule } from 'src/engine/core-modules/view/view.module';
|
||||
import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||
@@ -70,6 +71,7 @@ import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspa
|
||||
WorkspaceDataSourceModule,
|
||||
FeatureFlagModule,
|
||||
WorkspaceMigrationV2Module,
|
||||
CoreViewModule,
|
||||
],
|
||||
services: [
|
||||
ObjectMetadataService,
|
||||
|
||||
+28
-36
@@ -1,16 +1,26 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { ViewDTO } from 'src/engine/core-modules/view/dtos/view.dto';
|
||||
import { ViewEntity } from 'src/engine/core-modules/view/entities/view.entity';
|
||||
import { ViewKey } from 'src/engine/core-modules/view/enums/view-key.enum';
|
||||
import { ViewType } from 'src/engine/core-modules/view/enums/view-type.enum';
|
||||
import { ViewFieldService } from 'src/engine/core-modules/view/services/view-field.service';
|
||||
import { ViewService } from 'src/engine/core-modules/view/services/view.service';
|
||||
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { type FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity';
|
||||
import { type ViewFieldWorkspaceEntity } from 'src/modules/view/standard-objects/view-field.workspace-entity';
|
||||
import { type ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
|
||||
|
||||
@Injectable()
|
||||
export class ObjectMetadataRelatedRecordsService {
|
||||
constructor(
|
||||
private readonly viewService: ViewService,
|
||||
private readonly viewFieldService: ViewFieldService,
|
||||
@InjectRepository(ViewEntity)
|
||||
private readonly viewRepository: Repository<ViewEntity>,
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {}
|
||||
|
||||
@@ -25,19 +35,14 @@ export class ObjectMetadataRelatedRecordsService {
|
||||
|
||||
private async createView(
|
||||
objectMetadata: ObjectMetadataEntity,
|
||||
): Promise<ViewWorkspaceEntity> {
|
||||
const viewRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewWorkspaceEntity>(
|
||||
objectMetadata.workspaceId,
|
||||
'view',
|
||||
);
|
||||
|
||||
return await viewRepository.save({
|
||||
): Promise<ViewDTO> {
|
||||
return await this.viewService.create({
|
||||
objectMetadataId: objectMetadata.id,
|
||||
type: 'table',
|
||||
type: ViewType.TABLE,
|
||||
name: `All ${objectMetadata.labelPlural}`,
|
||||
key: 'INDEX',
|
||||
key: ViewKey.INDEX,
|
||||
icon: 'IconList',
|
||||
workspaceId: objectMetadata.workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -45,12 +50,6 @@ export class ObjectMetadataRelatedRecordsService {
|
||||
objectMetadata: ObjectMetadataEntity,
|
||||
viewId: string,
|
||||
): Promise<void> {
|
||||
const viewFieldRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewFieldWorkspaceEntity>(
|
||||
objectMetadata.workspaceId,
|
||||
'viewField',
|
||||
);
|
||||
|
||||
const viewFields = objectMetadata.fields
|
||||
.filter((field) => field.name !== 'id' && field.name !== 'deletedAt')
|
||||
.map((field, index) => ({
|
||||
@@ -59,9 +58,12 @@ export class ObjectMetadataRelatedRecordsService {
|
||||
isVisible: true,
|
||||
size: 180,
|
||||
viewId: viewId,
|
||||
workspaceId: objectMetadata.workspaceId,
|
||||
}));
|
||||
|
||||
await viewFieldRepository.insert(viewFields);
|
||||
for (const viewField of viewFields) {
|
||||
await this.viewFieldService.create(viewField);
|
||||
}
|
||||
}
|
||||
|
||||
public async createViewWorkspaceFavorite(
|
||||
@@ -91,14 +93,12 @@ export class ObjectMetadataRelatedRecordsService {
|
||||
>,
|
||||
workspaceId: string,
|
||||
) {
|
||||
const viewRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewWorkspaceEntity>(
|
||||
await this.viewRepository.update(
|
||||
{
|
||||
objectMetadataId: updatedObjectMetadata.id,
|
||||
key: ViewKey.INDEX,
|
||||
workspaceId,
|
||||
'view',
|
||||
);
|
||||
|
||||
await viewRepository.update(
|
||||
{ objectMetadataId: updatedObjectMetadata.id, key: 'INDEX' },
|
||||
},
|
||||
{
|
||||
name: `All ${updatedObjectMetadata.labelPlural}`,
|
||||
...(isDefined(updatedObjectMetadata.icon)
|
||||
@@ -112,17 +112,9 @@ export class ObjectMetadataRelatedRecordsService {
|
||||
objectMetadata: ObjectMetadataEntity,
|
||||
workspaceId: string,
|
||||
) {
|
||||
const viewRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'view',
|
||||
{
|
||||
shouldBypassPermissionChecks: true,
|
||||
},
|
||||
);
|
||||
|
||||
await viewRepository.delete({
|
||||
await this.viewRepository.delete({
|
||||
objectMetadataId: objectMetadata.id,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
-10
@@ -55,11 +55,6 @@ export const seedFeatureFlags = async (
|
||||
workspaceId: workspaceId,
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
key: FeatureFlagKey.IS_CORE_VIEW_ENABLED,
|
||||
workspaceId: workspaceId,
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
key: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
|
||||
workspaceId: workspaceId,
|
||||
@@ -70,11 +65,6 @@ export const seedFeatureFlags = async (
|
||||
workspaceId: workspaceId,
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
key: FeatureFlagKey.IS_CORE_VIEW_SYNCING_ENABLED,
|
||||
workspaceId: workspaceId,
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
key: FeatureFlagKey.IS_PAGE_LAYOUT_ENABLED,
|
||||
workspaceId: workspaceId,
|
||||
|
||||
@@ -137,17 +137,13 @@ export class WorkspaceManagerService {
|
||||
featureFlags,
|
||||
);
|
||||
|
||||
if (featureFlags[FeatureFlagKey.IS_CORE_VIEW_SYNCING_ENABLED]) {
|
||||
this.logger.log(`Prefilling core views for workspace ${workspaceId}`);
|
||||
|
||||
await prefillCoreViews({
|
||||
coreDataSource: this.coreDataSource,
|
||||
workspaceId,
|
||||
objectMetadataItems: createdObjectMetadata,
|
||||
schemaName: dataSourceMetadata.schema,
|
||||
featureFlags,
|
||||
});
|
||||
}
|
||||
await prefillCoreViews({
|
||||
coreDataSource: this.coreDataSource,
|
||||
workspaceId,
|
||||
objectMetadataItems: createdObjectMetadata,
|
||||
schemaName: dataSourceMetadata.schema,
|
||||
featureFlags,
|
||||
});
|
||||
}
|
||||
|
||||
public async delete(workspaceId: string): Promise<void> {
|
||||
|
||||
+1
-6
@@ -1,6 +1 @@
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
|
||||
export const DEFAULT_FEATURE_FLAGS = [
|
||||
FeatureFlagKey.IS_CORE_VIEW_ENABLED,
|
||||
FeatureFlagKey.IS_CORE_VIEW_SYNCING_ENABLED,
|
||||
];
|
||||
export const DEFAULT_FEATURE_FLAGS = [];
|
||||
|
||||
@@ -1,209 +0,0 @@
|
||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { type ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event';
|
||||
import { type ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event';
|
||||
import { type ObjectRecordDestroyEvent } from 'src/engine/core-modules/event-emitter/types/object-record-destroy.event';
|
||||
import { type ObjectRecordDiff } from 'src/engine/core-modules/event-emitter/types/object-record-diff';
|
||||
import { type ObjectRecordRestoreEvent } from 'src/engine/core-modules/event-emitter/types/object-record-restore.event';
|
||||
import { type ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event';
|
||||
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
|
||||
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
|
||||
import { type WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event.type';
|
||||
import {
|
||||
ViewException,
|
||||
ViewExceptionCode,
|
||||
} from 'src/modules/view/views.exception';
|
||||
|
||||
type EntityWithId = { id: string };
|
||||
|
||||
type SyncOperations<T extends EntityWithId> = {
|
||||
create: (workspaceId: string, entity: T) => Promise<void>;
|
||||
update: (
|
||||
workspaceId: string,
|
||||
entity: T,
|
||||
diff?: Partial<ObjectRecordDiff<T>>,
|
||||
) => Promise<void>;
|
||||
delete: (workspaceId: string, entity: Pick<T, 'id'>) => Promise<void>;
|
||||
destroy: (workspaceId: string, entity: Pick<T, 'id'>) => Promise<void>;
|
||||
restore: (workspaceId: string, entity: Pick<T, 'id'>) => Promise<void>;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export abstract class BaseViewSyncListener<T extends EntityWithId> {
|
||||
@Inject(FeatureFlagService)
|
||||
protected readonly featureFlagService: FeatureFlagService;
|
||||
|
||||
@Inject(ExceptionHandlerService)
|
||||
protected readonly exceptionHandlerService: ExceptionHandlerService;
|
||||
|
||||
protected readonly logger: Logger;
|
||||
|
||||
constructor(
|
||||
protected readonly syncOperations: SyncOperations<T>,
|
||||
loggerName: string,
|
||||
protected readonly entityTypeName: string,
|
||||
) {
|
||||
this.logger = new Logger(loggerName);
|
||||
}
|
||||
|
||||
protected async handleCreated(
|
||||
batchEvent: WorkspaceEventBatch<ObjectRecordCreateEvent<T>>,
|
||||
): Promise<void> {
|
||||
const isEnabled = await this.isFeatureFlagEnabled(batchEvent.workspaceId);
|
||||
|
||||
if (!isEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const event of batchEvent.events) {
|
||||
try {
|
||||
await this.syncOperations.create(
|
||||
batchEvent.workspaceId,
|
||||
event.properties.after,
|
||||
);
|
||||
} catch (error) {
|
||||
this.captureException(
|
||||
error,
|
||||
batchEvent.workspaceId,
|
||||
'create',
|
||||
event.properties.after.id,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected async handleUpdated(
|
||||
batchEvent: WorkspaceEventBatch<ObjectRecordUpdateEvent<T>>,
|
||||
): Promise<void> {
|
||||
const isEnabled = await this.isFeatureFlagEnabled(batchEvent.workspaceId);
|
||||
|
||||
if (!isEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const event of batchEvent.events) {
|
||||
try {
|
||||
await this.syncOperations.update(
|
||||
batchEvent.workspaceId,
|
||||
event.properties.after,
|
||||
event.properties.diff,
|
||||
);
|
||||
} catch (error) {
|
||||
this.captureException(
|
||||
error,
|
||||
batchEvent.workspaceId,
|
||||
'update',
|
||||
event.properties.after.id,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected async handleDeleted(
|
||||
batchEvent: WorkspaceEventBatch<ObjectRecordDeleteEvent<T>>,
|
||||
): Promise<void> {
|
||||
const isEnabled = await this.isFeatureFlagEnabled(batchEvent.workspaceId);
|
||||
|
||||
if (!isEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const event of batchEvent.events) {
|
||||
try {
|
||||
await this.syncOperations.delete(
|
||||
batchEvent.workspaceId,
|
||||
event.properties.before,
|
||||
);
|
||||
} catch (error) {
|
||||
this.captureException(
|
||||
error,
|
||||
batchEvent.workspaceId,
|
||||
'delete',
|
||||
event.properties.before.id,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected async handleDestroyed(
|
||||
batchEvent: WorkspaceEventBatch<ObjectRecordDestroyEvent<T>>,
|
||||
): Promise<void> {
|
||||
const isEnabled = await this.isFeatureFlagEnabled(batchEvent.workspaceId);
|
||||
|
||||
if (!isEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const event of batchEvent.events) {
|
||||
try {
|
||||
await this.syncOperations.destroy(
|
||||
batchEvent.workspaceId,
|
||||
event.properties.before,
|
||||
);
|
||||
} catch (error) {
|
||||
this.captureException(
|
||||
error,
|
||||
batchEvent.workspaceId,
|
||||
'destroy',
|
||||
event.properties.before.id,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected async handleRestored(
|
||||
batchEvent: WorkspaceEventBatch<ObjectRecordRestoreEvent<T>>,
|
||||
): Promise<void> {
|
||||
const isEnabled = await this.isFeatureFlagEnabled(batchEvent.workspaceId);
|
||||
|
||||
if (!isEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const event of batchEvent.events) {
|
||||
try {
|
||||
await this.syncOperations.restore(
|
||||
batchEvent.workspaceId,
|
||||
event.properties.after,
|
||||
);
|
||||
} catch (error) {
|
||||
this.captureException(
|
||||
error,
|
||||
batchEvent.workspaceId,
|
||||
'restore',
|
||||
event.properties.after.id,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async isFeatureFlagEnabled(workspaceId: string): Promise<boolean> {
|
||||
const featureFlags =
|
||||
await this.featureFlagService.getWorkspaceFeatureFlagsMap(workspaceId);
|
||||
|
||||
return featureFlags.IS_CORE_VIEW_SYNCING_ENABLED;
|
||||
}
|
||||
|
||||
private captureException(
|
||||
error: Error,
|
||||
workspaceId: string,
|
||||
operation: string,
|
||||
entityId: string,
|
||||
) {
|
||||
const viewException = new ViewException(
|
||||
`Failed to sync ${this.entityTypeName} ${entityId} to core: ${error.message}`,
|
||||
ViewExceptionCode.CORE_VIEW_SYNC_ERROR,
|
||||
);
|
||||
|
||||
this.exceptionHandlerService.captureExceptions([viewException], {
|
||||
workspace: {
|
||||
id: workspaceId,
|
||||
},
|
||||
additionalData: {
|
||||
entityId: entityId,
|
||||
entityType: this.entityTypeName,
|
||||
operation: operation,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { OnDatabaseBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-batch-event.decorator';
|
||||
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
||||
import { type ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event';
|
||||
import { type ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event';
|
||||
import { type ObjectRecordDestroyEvent } from 'src/engine/core-modules/event-emitter/types/object-record-destroy.event';
|
||||
import { type ObjectRecordRestoreEvent } from 'src/engine/core-modules/event-emitter/types/object-record-restore.event';
|
||||
import { type ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event';
|
||||
import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event.type';
|
||||
import { ViewFieldSyncService } from 'src/modules/view/services/view-field-sync.service';
|
||||
import { type ViewFieldWorkspaceEntity } from 'src/modules/view/standard-objects/view-field.workspace-entity';
|
||||
|
||||
import { BaseViewSyncListener } from './base-view-sync.listener';
|
||||
|
||||
@Injectable()
|
||||
export class ViewFieldListener extends BaseViewSyncListener<ViewFieldWorkspaceEntity> {
|
||||
constructor(viewFieldSyncService: ViewFieldSyncService) {
|
||||
super(
|
||||
{
|
||||
create:
|
||||
viewFieldSyncService.createCoreViewField.bind(viewFieldSyncService),
|
||||
update:
|
||||
viewFieldSyncService.updateCoreViewField.bind(viewFieldSyncService),
|
||||
delete:
|
||||
viewFieldSyncService.deleteCoreViewField.bind(viewFieldSyncService),
|
||||
destroy:
|
||||
viewFieldSyncService.destroyCoreViewField.bind(viewFieldSyncService),
|
||||
restore:
|
||||
viewFieldSyncService.restoreCoreViewField.bind(viewFieldSyncService),
|
||||
},
|
||||
ViewFieldListener.name,
|
||||
'view field',
|
||||
);
|
||||
}
|
||||
|
||||
@OnDatabaseBatchEvent('viewField', DatabaseEventAction.CREATED)
|
||||
async handleViewFieldCreated(
|
||||
batchEvent: WorkspaceEventBatch<
|
||||
ObjectRecordCreateEvent<ViewFieldWorkspaceEntity>
|
||||
>,
|
||||
) {
|
||||
return this.handleCreated(batchEvent);
|
||||
}
|
||||
|
||||
@OnDatabaseBatchEvent('viewField', DatabaseEventAction.UPDATED)
|
||||
async handleViewFieldUpdated(
|
||||
batchEvent: WorkspaceEventBatch<
|
||||
ObjectRecordUpdateEvent<ViewFieldWorkspaceEntity>
|
||||
>,
|
||||
) {
|
||||
return this.handleUpdated(batchEvent);
|
||||
}
|
||||
|
||||
@OnDatabaseBatchEvent('viewField', DatabaseEventAction.DELETED)
|
||||
async handleViewFieldDeleted(
|
||||
batchEvent: WorkspaceEventBatch<
|
||||
ObjectRecordDeleteEvent<ViewFieldWorkspaceEntity>
|
||||
>,
|
||||
) {
|
||||
return this.handleDeleted(batchEvent);
|
||||
}
|
||||
|
||||
@OnDatabaseBatchEvent('viewField', DatabaseEventAction.DESTROYED)
|
||||
async handleViewFieldDestroyed(
|
||||
batchEvent: WorkspaceEventBatch<
|
||||
ObjectRecordDestroyEvent<ViewFieldWorkspaceEntity>
|
||||
>,
|
||||
) {
|
||||
return this.handleDestroyed(batchEvent);
|
||||
}
|
||||
|
||||
@OnDatabaseBatchEvent('viewField', DatabaseEventAction.RESTORED)
|
||||
async handleViewFieldRestored(
|
||||
batchEvent: WorkspaceEventBatch<
|
||||
ObjectRecordRestoreEvent<ViewFieldWorkspaceEntity>
|
||||
>,
|
||||
) {
|
||||
return this.handleRestored(batchEvent);
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { OnDatabaseBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-batch-event.decorator';
|
||||
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
||||
import { type ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event';
|
||||
import { type ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event';
|
||||
import { type ObjectRecordDestroyEvent } from 'src/engine/core-modules/event-emitter/types/object-record-destroy.event';
|
||||
import { type ObjectRecordRestoreEvent } from 'src/engine/core-modules/event-emitter/types/object-record-restore.event';
|
||||
import { type ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event';
|
||||
import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event.type';
|
||||
import { ViewFilterGroupSyncService } from 'src/modules/view/services/view-filter-group-sync.service';
|
||||
import { type ViewFilterGroupWorkspaceEntity } from 'src/modules/view/standard-objects/view-filter-group.workspace-entity';
|
||||
|
||||
import { BaseViewSyncListener } from './base-view-sync.listener';
|
||||
|
||||
@Injectable()
|
||||
export class ViewFilterGroupListener extends BaseViewSyncListener<ViewFilterGroupWorkspaceEntity> {
|
||||
constructor(viewFilterGroupSyncService: ViewFilterGroupSyncService) {
|
||||
super(
|
||||
{
|
||||
create: viewFilterGroupSyncService.createCoreViewFilterGroup.bind(
|
||||
viewFilterGroupSyncService,
|
||||
),
|
||||
update: viewFilterGroupSyncService.updateCoreViewFilterGroup.bind(
|
||||
viewFilterGroupSyncService,
|
||||
),
|
||||
delete: viewFilterGroupSyncService.deleteCoreViewFilterGroup.bind(
|
||||
viewFilterGroupSyncService,
|
||||
),
|
||||
destroy: viewFilterGroupSyncService.destroyCoreViewFilterGroup.bind(
|
||||
viewFilterGroupSyncService,
|
||||
),
|
||||
restore: viewFilterGroupSyncService.restoreCoreViewFilterGroup.bind(
|
||||
viewFilterGroupSyncService,
|
||||
),
|
||||
},
|
||||
ViewFilterGroupListener.name,
|
||||
'view filter group',
|
||||
);
|
||||
}
|
||||
|
||||
@OnDatabaseBatchEvent('viewFilterGroup', DatabaseEventAction.CREATED)
|
||||
async handleViewFilterGroupCreated(
|
||||
batchEvent: WorkspaceEventBatch<
|
||||
ObjectRecordCreateEvent<ViewFilterGroupWorkspaceEntity>
|
||||
>,
|
||||
) {
|
||||
return this.handleCreated(batchEvent);
|
||||
}
|
||||
|
||||
@OnDatabaseBatchEvent('viewFilterGroup', DatabaseEventAction.UPDATED)
|
||||
async handleViewFilterGroupUpdated(
|
||||
batchEvent: WorkspaceEventBatch<
|
||||
ObjectRecordUpdateEvent<ViewFilterGroupWorkspaceEntity>
|
||||
>,
|
||||
) {
|
||||
return this.handleUpdated(batchEvent);
|
||||
}
|
||||
|
||||
@OnDatabaseBatchEvent('viewFilterGroup', DatabaseEventAction.DELETED)
|
||||
async handleViewFilterGroupDeleted(
|
||||
batchEvent: WorkspaceEventBatch<
|
||||
ObjectRecordDeleteEvent<ViewFilterGroupWorkspaceEntity>
|
||||
>,
|
||||
) {
|
||||
return this.handleDeleted(batchEvent);
|
||||
}
|
||||
|
||||
@OnDatabaseBatchEvent('viewFilterGroup', DatabaseEventAction.DESTROYED)
|
||||
async handleViewFilterGroupDestroyed(
|
||||
batchEvent: WorkspaceEventBatch<
|
||||
ObjectRecordDestroyEvent<ViewFilterGroupWorkspaceEntity>
|
||||
>,
|
||||
) {
|
||||
return this.handleDestroyed(batchEvent);
|
||||
}
|
||||
|
||||
@OnDatabaseBatchEvent('viewFilterGroup', DatabaseEventAction.RESTORED)
|
||||
async handleViewFilterGroupRestored(
|
||||
batchEvent: WorkspaceEventBatch<
|
||||
ObjectRecordRestoreEvent<ViewFilterGroupWorkspaceEntity>
|
||||
>,
|
||||
) {
|
||||
return this.handleRestored(batchEvent);
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { OnDatabaseBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-batch-event.decorator';
|
||||
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
||||
import { type ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event';
|
||||
import { type ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event';
|
||||
import { type ObjectRecordDestroyEvent } from 'src/engine/core-modules/event-emitter/types/object-record-destroy.event';
|
||||
import { type ObjectRecordRestoreEvent } from 'src/engine/core-modules/event-emitter/types/object-record-restore.event';
|
||||
import { type ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event';
|
||||
import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event.type';
|
||||
import { ViewFilterSyncService } from 'src/modules/view/services/view-filter-sync.service';
|
||||
import { type ViewFilterWorkspaceEntity } from 'src/modules/view/standard-objects/view-filter.workspace-entity';
|
||||
|
||||
import { BaseViewSyncListener } from './base-view-sync.listener';
|
||||
|
||||
@Injectable()
|
||||
export class ViewFilterListener extends BaseViewSyncListener<ViewFilterWorkspaceEntity> {
|
||||
constructor(viewFilterSyncService: ViewFilterSyncService) {
|
||||
super(
|
||||
{
|
||||
create: viewFilterSyncService.createCoreViewFilter.bind(
|
||||
viewFilterSyncService,
|
||||
),
|
||||
update: viewFilterSyncService.updateCoreViewFilter.bind(
|
||||
viewFilterSyncService,
|
||||
),
|
||||
delete: viewFilterSyncService.deleteCoreViewFilter.bind(
|
||||
viewFilterSyncService,
|
||||
),
|
||||
destroy: viewFilterSyncService.destroyCoreViewFilter.bind(
|
||||
viewFilterSyncService,
|
||||
),
|
||||
restore: viewFilterSyncService.restoreCoreViewFilter.bind(
|
||||
viewFilterSyncService,
|
||||
),
|
||||
},
|
||||
ViewFilterListener.name,
|
||||
'view filter',
|
||||
);
|
||||
}
|
||||
|
||||
@OnDatabaseBatchEvent('viewFilter', DatabaseEventAction.CREATED)
|
||||
async handleViewFilterCreated(
|
||||
batchEvent: WorkspaceEventBatch<
|
||||
ObjectRecordCreateEvent<ViewFilterWorkspaceEntity>
|
||||
>,
|
||||
) {
|
||||
return this.handleCreated(batchEvent);
|
||||
}
|
||||
|
||||
@OnDatabaseBatchEvent('viewFilter', DatabaseEventAction.UPDATED)
|
||||
async handleViewFilterUpdated(
|
||||
batchEvent: WorkspaceEventBatch<
|
||||
ObjectRecordUpdateEvent<ViewFilterWorkspaceEntity>
|
||||
>,
|
||||
) {
|
||||
return this.handleUpdated(batchEvent);
|
||||
}
|
||||
|
||||
@OnDatabaseBatchEvent('viewFilter', DatabaseEventAction.DELETED)
|
||||
async handleViewFilterDeleted(
|
||||
batchEvent: WorkspaceEventBatch<
|
||||
ObjectRecordDeleteEvent<ViewFilterWorkspaceEntity>
|
||||
>,
|
||||
) {
|
||||
return this.handleDeleted(batchEvent);
|
||||
}
|
||||
|
||||
@OnDatabaseBatchEvent('viewFilter', DatabaseEventAction.DESTROYED)
|
||||
async handleViewFilterDestroyed(
|
||||
batchEvent: WorkspaceEventBatch<
|
||||
ObjectRecordDestroyEvent<ViewFilterWorkspaceEntity>
|
||||
>,
|
||||
) {
|
||||
return this.handleDestroyed(batchEvent);
|
||||
}
|
||||
|
||||
@OnDatabaseBatchEvent('viewFilter', DatabaseEventAction.RESTORED)
|
||||
async handleViewFilterRestored(
|
||||
batchEvent: WorkspaceEventBatch<
|
||||
ObjectRecordRestoreEvent<ViewFilterWorkspaceEntity>
|
||||
>,
|
||||
) {
|
||||
return this.handleRestored(batchEvent);
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { OnDatabaseBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-batch-event.decorator';
|
||||
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
||||
import { type ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event';
|
||||
import { type ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event';
|
||||
import { type ObjectRecordDestroyEvent } from 'src/engine/core-modules/event-emitter/types/object-record-destroy.event';
|
||||
import { type ObjectRecordRestoreEvent } from 'src/engine/core-modules/event-emitter/types/object-record-restore.event';
|
||||
import { type ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event';
|
||||
import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event.type';
|
||||
import { ViewGroupSyncService } from 'src/modules/view/services/view-group-sync.service';
|
||||
import { type ViewGroupWorkspaceEntity } from 'src/modules/view/standard-objects/view-group.workspace-entity';
|
||||
|
||||
import { BaseViewSyncListener } from './base-view-sync.listener';
|
||||
|
||||
@Injectable()
|
||||
export class ViewGroupListener extends BaseViewSyncListener<ViewGroupWorkspaceEntity> {
|
||||
constructor(viewGroupSyncService: ViewGroupSyncService) {
|
||||
super(
|
||||
{
|
||||
create:
|
||||
viewGroupSyncService.createCoreViewGroup.bind(viewGroupSyncService),
|
||||
update:
|
||||
viewGroupSyncService.updateCoreViewGroup.bind(viewGroupSyncService),
|
||||
delete:
|
||||
viewGroupSyncService.deleteCoreViewGroup.bind(viewGroupSyncService),
|
||||
destroy:
|
||||
viewGroupSyncService.destroyCoreViewGroup.bind(viewGroupSyncService),
|
||||
restore:
|
||||
viewGroupSyncService.restoreCoreViewGroup.bind(viewGroupSyncService),
|
||||
},
|
||||
ViewGroupListener.name,
|
||||
'view group',
|
||||
);
|
||||
}
|
||||
|
||||
@OnDatabaseBatchEvent('viewGroup', DatabaseEventAction.CREATED)
|
||||
async handleViewGroupCreated(
|
||||
batchEvent: WorkspaceEventBatch<
|
||||
ObjectRecordCreateEvent<ViewGroupWorkspaceEntity>
|
||||
>,
|
||||
) {
|
||||
return this.handleCreated(batchEvent);
|
||||
}
|
||||
|
||||
@OnDatabaseBatchEvent('viewGroup', DatabaseEventAction.UPDATED)
|
||||
async handleViewGroupUpdated(
|
||||
batchEvent: WorkspaceEventBatch<
|
||||
ObjectRecordUpdateEvent<ViewGroupWorkspaceEntity>
|
||||
>,
|
||||
) {
|
||||
return this.handleUpdated(batchEvent);
|
||||
}
|
||||
|
||||
@OnDatabaseBatchEvent('viewGroup', DatabaseEventAction.DELETED)
|
||||
async handleViewGroupDeleted(
|
||||
batchEvent: WorkspaceEventBatch<
|
||||
ObjectRecordDeleteEvent<ViewGroupWorkspaceEntity>
|
||||
>,
|
||||
) {
|
||||
return this.handleDeleted(batchEvent);
|
||||
}
|
||||
|
||||
@OnDatabaseBatchEvent('viewGroup', DatabaseEventAction.DESTROYED)
|
||||
async handleViewGroupDestroyed(
|
||||
batchEvent: WorkspaceEventBatch<
|
||||
ObjectRecordDestroyEvent<ViewGroupWorkspaceEntity>
|
||||
>,
|
||||
) {
|
||||
return this.handleDestroyed(batchEvent);
|
||||
}
|
||||
|
||||
@OnDatabaseBatchEvent('viewGroup', DatabaseEventAction.RESTORED)
|
||||
async handleViewGroupRestored(
|
||||
batchEvent: WorkspaceEventBatch<
|
||||
ObjectRecordRestoreEvent<ViewGroupWorkspaceEntity>
|
||||
>,
|
||||
) {
|
||||
return this.handleRestored(batchEvent);
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { OnDatabaseBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-batch-event.decorator';
|
||||
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
||||
import { type ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event';
|
||||
import { type ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event';
|
||||
import { type ObjectRecordDestroyEvent } from 'src/engine/core-modules/event-emitter/types/object-record-destroy.event';
|
||||
import { type ObjectRecordRestoreEvent } from 'src/engine/core-modules/event-emitter/types/object-record-restore.event';
|
||||
import { type ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event';
|
||||
import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event.type';
|
||||
import { ViewSortSyncService } from 'src/modules/view/services/view-sort-sync.service';
|
||||
import { type ViewSortWorkspaceEntity } from 'src/modules/view/standard-objects/view-sort.workspace-entity';
|
||||
|
||||
import { BaseViewSyncListener } from './base-view-sync.listener';
|
||||
|
||||
@Injectable()
|
||||
export class ViewSortListener extends BaseViewSyncListener<ViewSortWorkspaceEntity> {
|
||||
constructor(viewSortSyncService: ViewSortSyncService) {
|
||||
super(
|
||||
{
|
||||
create:
|
||||
viewSortSyncService.createCoreViewSort.bind(viewSortSyncService),
|
||||
update:
|
||||
viewSortSyncService.updateCoreViewSort.bind(viewSortSyncService),
|
||||
delete:
|
||||
viewSortSyncService.deleteCoreViewSort.bind(viewSortSyncService),
|
||||
destroy:
|
||||
viewSortSyncService.destroyCoreViewSort.bind(viewSortSyncService),
|
||||
restore:
|
||||
viewSortSyncService.restoreCoreViewSort.bind(viewSortSyncService),
|
||||
},
|
||||
ViewSortListener.name,
|
||||
'view sort',
|
||||
);
|
||||
}
|
||||
|
||||
@OnDatabaseBatchEvent('viewSort', DatabaseEventAction.CREATED)
|
||||
async handleViewSortCreated(
|
||||
batchEvent: WorkspaceEventBatch<
|
||||
ObjectRecordCreateEvent<ViewSortWorkspaceEntity>
|
||||
>,
|
||||
) {
|
||||
return this.handleCreated(batchEvent);
|
||||
}
|
||||
|
||||
@OnDatabaseBatchEvent('viewSort', DatabaseEventAction.UPDATED)
|
||||
async handleViewSortUpdated(
|
||||
batchEvent: WorkspaceEventBatch<
|
||||
ObjectRecordUpdateEvent<ViewSortWorkspaceEntity>
|
||||
>,
|
||||
) {
|
||||
return this.handleUpdated(batchEvent);
|
||||
}
|
||||
|
||||
@OnDatabaseBatchEvent('viewSort', DatabaseEventAction.DELETED)
|
||||
async handleViewSortDeleted(
|
||||
batchEvent: WorkspaceEventBatch<
|
||||
ObjectRecordDeleteEvent<ViewSortWorkspaceEntity>
|
||||
>,
|
||||
) {
|
||||
return this.handleDeleted(batchEvent);
|
||||
}
|
||||
|
||||
@OnDatabaseBatchEvent('viewSort', DatabaseEventAction.DESTROYED)
|
||||
async handleViewSortDestroyed(
|
||||
batchEvent: WorkspaceEventBatch<
|
||||
ObjectRecordDestroyEvent<ViewSortWorkspaceEntity>
|
||||
>,
|
||||
) {
|
||||
return this.handleDestroyed(batchEvent);
|
||||
}
|
||||
|
||||
@OnDatabaseBatchEvent('viewSort', DatabaseEventAction.RESTORED)
|
||||
async handleViewSortRestored(
|
||||
batchEvent: WorkspaceEventBatch<
|
||||
ObjectRecordRestoreEvent<ViewSortWorkspaceEntity>
|
||||
>,
|
||||
) {
|
||||
return this.handleRestored(batchEvent);
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { OnDatabaseBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-batch-event.decorator';
|
||||
import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action';
|
||||
import { type ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event';
|
||||
import { type ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event';
|
||||
import { type ObjectRecordDestroyEvent } from 'src/engine/core-modules/event-emitter/types/object-record-destroy.event';
|
||||
import { type ObjectRecordRestoreEvent } from 'src/engine/core-modules/event-emitter/types/object-record-restore.event';
|
||||
import { type ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event';
|
||||
import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event.type';
|
||||
import { ViewSyncService } from 'src/modules/view/services/view-sync.service';
|
||||
import { type ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
|
||||
|
||||
import { BaseViewSyncListener } from './base-view-sync.listener';
|
||||
|
||||
@Injectable()
|
||||
export class ViewListener extends BaseViewSyncListener<ViewWorkspaceEntity> {
|
||||
constructor(viewSyncService: ViewSyncService) {
|
||||
super(
|
||||
{
|
||||
create: viewSyncService.createCoreView.bind(viewSyncService),
|
||||
update: viewSyncService.updateCoreView.bind(viewSyncService),
|
||||
delete: viewSyncService.deleteCoreView.bind(viewSyncService),
|
||||
destroy: viewSyncService.destroyCoreView.bind(viewSyncService),
|
||||
restore: viewSyncService.restoreCoreView.bind(viewSyncService),
|
||||
},
|
||||
ViewListener.name,
|
||||
'view',
|
||||
);
|
||||
}
|
||||
|
||||
@OnDatabaseBatchEvent('view', DatabaseEventAction.CREATED)
|
||||
async handleViewCreated(
|
||||
batchEvent: WorkspaceEventBatch<
|
||||
ObjectRecordCreateEvent<ViewWorkspaceEntity>
|
||||
>,
|
||||
) {
|
||||
return this.handleCreated(batchEvent);
|
||||
}
|
||||
|
||||
@OnDatabaseBatchEvent('view', DatabaseEventAction.UPDATED)
|
||||
async handleViewUpdated(
|
||||
batchEvent: WorkspaceEventBatch<
|
||||
ObjectRecordUpdateEvent<ViewWorkspaceEntity>
|
||||
>,
|
||||
) {
|
||||
return this.handleUpdated(batchEvent);
|
||||
}
|
||||
|
||||
@OnDatabaseBatchEvent('view', DatabaseEventAction.DELETED)
|
||||
async handleViewDeleted(
|
||||
batchEvent: WorkspaceEventBatch<
|
||||
ObjectRecordDeleteEvent<ViewWorkspaceEntity>
|
||||
>,
|
||||
) {
|
||||
return this.handleDeleted(batchEvent);
|
||||
}
|
||||
|
||||
@OnDatabaseBatchEvent('view', DatabaseEventAction.DESTROYED)
|
||||
async handleViewDestroyed(
|
||||
batchEvent: WorkspaceEventBatch<
|
||||
ObjectRecordDestroyEvent<ViewWorkspaceEntity>
|
||||
>,
|
||||
) {
|
||||
return this.handleDestroyed(batchEvent);
|
||||
}
|
||||
|
||||
@OnDatabaseBatchEvent('view', DatabaseEventAction.RESTORED)
|
||||
async handleViewRestored(
|
||||
batchEvent: WorkspaceEventBatch<
|
||||
ObjectRecordRestoreEvent<ViewWorkspaceEntity>
|
||||
>,
|
||||
) {
|
||||
return this.handleRestored(batchEvent);
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { type WorkspacePreQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
|
||||
import { type DeleteManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
|
||||
import {
|
||||
GraphqlQueryRunnerException,
|
||||
GraphqlQueryRunnerExceptionCode,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
||||
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
|
||||
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
|
||||
@WorkspaceQueryHook(`view.deleteMany`)
|
||||
export class ViewDeleteManyPreQueryHook
|
||||
implements WorkspacePreQueryHookInstance
|
||||
{
|
||||
constructor() {}
|
||||
|
||||
async execute(
|
||||
_authContext: AuthContext,
|
||||
_objectName: string,
|
||||
_payload: DeleteManyResolverArgs,
|
||||
): Promise<DeleteManyResolverArgs> {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
'Method not implemented',
|
||||
GraphqlQueryRunnerExceptionCode.NOT_IMPLEMENTED,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
import { type WorkspacePreQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
|
||||
import { type DeleteOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
|
||||
import {
|
||||
GraphqlQueryRunnerException,
|
||||
GraphqlQueryRunnerExceptionCode,
|
||||
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
|
||||
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
|
||||
import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
|
||||
@WorkspaceQueryHook(`view.deleteOne`)
|
||||
export class ViewDeleteOnePreQueryHook
|
||||
implements WorkspacePreQueryHookInstance
|
||||
{
|
||||
constructor(
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
authContext: AuthContext,
|
||||
_objectName: string,
|
||||
payload: DeleteOneResolverArgs,
|
||||
): Promise<DeleteOneResolverArgs> {
|
||||
const workspace = authContext.workspace;
|
||||
|
||||
workspaceValidator.assertIsDefinedOrThrow(workspace);
|
||||
|
||||
const targettedViewId = payload.id;
|
||||
const viewRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspace.id,
|
||||
'view',
|
||||
);
|
||||
|
||||
const view = await viewRepository.findOne({
|
||||
where: { id: targettedViewId },
|
||||
});
|
||||
|
||||
if (!view) {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
'View not found',
|
||||
GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
if (view.key === 'INDEX') {
|
||||
throw new GraphqlQueryRunnerException(
|
||||
'Cannot delete INDEX view',
|
||||
GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { type ObjectRecordDiff } from 'src/engine/core-modules/event-emitter/types/object-record-diff';
|
||||
import { ViewFieldEntity } from 'src/engine/core-modules/view/entities/view-field.entity';
|
||||
import { type ViewFieldWorkspaceEntity } from 'src/modules/view/standard-objects/view-field.workspace-entity';
|
||||
|
||||
@Injectable()
|
||||
export class ViewFieldSyncService {
|
||||
constructor(
|
||||
@InjectRepository(ViewFieldEntity)
|
||||
private readonly coreViewFieldRepository: Repository<ViewFieldEntity>,
|
||||
) {}
|
||||
|
||||
private parseUpdateDataFromDiff(
|
||||
diff: Partial<ObjectRecordDiff<ViewFieldWorkspaceEntity>>,
|
||||
): Partial<ViewFieldEntity> {
|
||||
const updateData: Record<string, unknown> = {};
|
||||
|
||||
for (const key of Object.keys(diff)) {
|
||||
const diffValue = diff[key as keyof ViewFieldWorkspaceEntity];
|
||||
|
||||
if (isDefined(diffValue)) {
|
||||
updateData[key] = diffValue.after;
|
||||
}
|
||||
}
|
||||
|
||||
return updateData as Partial<ViewFieldEntity>;
|
||||
}
|
||||
|
||||
public async createCoreViewField(
|
||||
workspaceId: string,
|
||||
workspaceViewField: ViewFieldWorkspaceEntity,
|
||||
): Promise<void> {
|
||||
const coreViewField: Partial<ViewFieldEntity> = {
|
||||
id: workspaceViewField.id,
|
||||
fieldMetadataId: workspaceViewField.fieldMetadataId,
|
||||
viewId: workspaceViewField.viewId,
|
||||
position: workspaceViewField.position,
|
||||
isVisible: workspaceViewField.isVisible,
|
||||
size: workspaceViewField.size,
|
||||
workspaceId,
|
||||
createdAt: new Date(workspaceViewField.createdAt),
|
||||
updatedAt: new Date(workspaceViewField.updatedAt),
|
||||
deletedAt: workspaceViewField.deletedAt
|
||||
? new Date(workspaceViewField.deletedAt)
|
||||
: null,
|
||||
};
|
||||
|
||||
await this.coreViewFieldRepository.save(coreViewField);
|
||||
}
|
||||
|
||||
public async updateCoreViewField(
|
||||
workspaceId: string,
|
||||
workspaceViewField: Pick<ViewFieldWorkspaceEntity, 'id'>,
|
||||
diff?: Partial<ObjectRecordDiff<ViewFieldWorkspaceEntity>>,
|
||||
): Promise<void> {
|
||||
if (!diff || Object.keys(diff).length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updateData = this.parseUpdateDataFromDiff(diff);
|
||||
|
||||
if (Object.keys(updateData).length > 0) {
|
||||
await this.coreViewFieldRepository.update(
|
||||
{ id: workspaceViewField.id, workspaceId },
|
||||
updateData,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteCoreViewField(
|
||||
workspaceId: string,
|
||||
workspaceViewField: Pick<ViewFieldWorkspaceEntity, 'id'>,
|
||||
): Promise<void> {
|
||||
await this.coreViewFieldRepository.softDelete({
|
||||
id: workspaceViewField.id,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
public async destroyCoreViewField(
|
||||
workspaceId: string,
|
||||
workspaceViewField: Pick<ViewFieldWorkspaceEntity, 'id'>,
|
||||
): Promise<void> {
|
||||
await this.coreViewFieldRepository.delete({
|
||||
id: workspaceViewField.id,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
public async restoreCoreViewField(
|
||||
workspaceId: string,
|
||||
workspaceViewField: Pick<ViewFieldWorkspaceEntity, 'id'>,
|
||||
): Promise<void> {
|
||||
await this.coreViewFieldRepository.restore({
|
||||
id: workspaceViewField.id,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { type ObjectRecordDiff } from 'src/engine/core-modules/event-emitter/types/object-record-diff';
|
||||
import { ViewFilterGroupEntity } from 'src/engine/core-modules/view/entities/view-filter-group.entity';
|
||||
import { type ViewFilterGroupLogicalOperator } from 'src/engine/core-modules/view/enums/view-filter-group-logical-operator';
|
||||
import { type ViewFilterGroupWorkspaceEntity } from 'src/modules/view/standard-objects/view-filter-group.workspace-entity';
|
||||
|
||||
@Injectable()
|
||||
export class ViewFilterGroupSyncService {
|
||||
constructor(
|
||||
@InjectRepository(ViewFilterGroupEntity)
|
||||
private readonly coreViewFilterGroupRepository: Repository<ViewFilterGroupEntity>,
|
||||
) {}
|
||||
|
||||
private parseUpdateDataFromDiff(
|
||||
diff: Partial<ObjectRecordDiff<ViewFilterGroupWorkspaceEntity>>,
|
||||
): Partial<ViewFilterGroupEntity> {
|
||||
const updateData: Record<string, unknown> = {};
|
||||
|
||||
for (const key of Object.keys(diff)) {
|
||||
const diffValue = diff[key as keyof ViewFilterGroupWorkspaceEntity];
|
||||
|
||||
if (isDefined(diffValue)) {
|
||||
if (key === 'logicalOperator') {
|
||||
updateData[key] = diffValue.after as ViewFilterGroupLogicalOperator;
|
||||
} else {
|
||||
updateData[key] = diffValue.after;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return updateData as Partial<ViewFilterGroupEntity>;
|
||||
}
|
||||
|
||||
public async createCoreViewFilterGroup(
|
||||
workspaceId: string,
|
||||
workspaceViewFilterGroup: ViewFilterGroupWorkspaceEntity,
|
||||
): Promise<void> {
|
||||
const coreViewFilterGroup: Partial<ViewFilterGroupEntity> = {
|
||||
id: workspaceViewFilterGroup.id,
|
||||
viewId: workspaceViewFilterGroup.viewId,
|
||||
logicalOperator:
|
||||
workspaceViewFilterGroup.logicalOperator as ViewFilterGroupLogicalOperator,
|
||||
parentViewFilterGroupId: workspaceViewFilterGroup.parentViewFilterGroupId,
|
||||
positionInViewFilterGroup:
|
||||
workspaceViewFilterGroup.positionInViewFilterGroup,
|
||||
workspaceId,
|
||||
createdAt: new Date(workspaceViewFilterGroup.createdAt),
|
||||
updatedAt: new Date(workspaceViewFilterGroup.updatedAt),
|
||||
deletedAt: workspaceViewFilterGroup.deletedAt
|
||||
? new Date(workspaceViewFilterGroup.deletedAt)
|
||||
: null,
|
||||
};
|
||||
|
||||
await this.coreViewFilterGroupRepository.save(coreViewFilterGroup);
|
||||
}
|
||||
|
||||
public async updateCoreViewFilterGroup(
|
||||
workspaceId: string,
|
||||
workspaceViewFilterGroup: Pick<ViewFilterGroupWorkspaceEntity, 'id'>,
|
||||
diff?: Partial<ObjectRecordDiff<ViewFilterGroupWorkspaceEntity>>,
|
||||
): Promise<void> {
|
||||
if (!diff || Object.keys(diff).length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updateData = this.parseUpdateDataFromDiff(diff);
|
||||
|
||||
if (Object.keys(updateData).length > 0) {
|
||||
await this.coreViewFilterGroupRepository.update(
|
||||
{ id: workspaceViewFilterGroup.id, workspaceId },
|
||||
updateData,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteCoreViewFilterGroup(
|
||||
workspaceId: string,
|
||||
workspaceViewFilterGroup: Pick<ViewFilterGroupWorkspaceEntity, 'id'>,
|
||||
): Promise<void> {
|
||||
await this.coreViewFilterGroupRepository.softDelete({
|
||||
id: workspaceViewFilterGroup.id,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
public async destroyCoreViewFilterGroup(
|
||||
workspaceId: string,
|
||||
workspaceViewFilterGroup: Pick<ViewFilterGroupWorkspaceEntity, 'id'>,
|
||||
): Promise<void> {
|
||||
await this.coreViewFilterGroupRepository.delete({
|
||||
id: workspaceViewFilterGroup.id,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
public async restoreCoreViewFilterGroup(
|
||||
workspaceId: string,
|
||||
workspaceViewFilterGroup: ViewFilterGroupWorkspaceEntity,
|
||||
): Promise<void> {
|
||||
await this.coreViewFilterGroupRepository.restore({
|
||||
id: workspaceViewFilterGroup.id,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { type ViewFilterOperand as SharedViewFilterOperand } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { type ObjectRecordDiff } from 'src/engine/core-modules/event-emitter/types/object-record-diff';
|
||||
import { ViewFilterEntity } from 'src/engine/core-modules/view/entities/view-filter.entity';
|
||||
import { type ViewFilterWorkspaceEntity } from 'src/modules/view/standard-objects/view-filter.workspace-entity';
|
||||
import { convertViewFilterOperandToCoreOperand } from 'src/modules/view/utils/convert-view-filter-operand-to-core-operand.util';
|
||||
import { convertViewFilterWorkspaceValueToCoreValue } from 'src/modules/view/utils/convert-view-filter-workspace-value-to-core-value';
|
||||
|
||||
@Injectable()
|
||||
export class ViewFilterSyncService {
|
||||
constructor(
|
||||
@InjectRepository(ViewFilterEntity)
|
||||
private readonly coreViewFilterRepository: Repository<ViewFilterEntity>,
|
||||
) {}
|
||||
|
||||
private parseUpdateDataFromDiff(
|
||||
diff: Partial<ObjectRecordDiff<ViewFilterWorkspaceEntity>>,
|
||||
): Partial<ViewFilterEntity> {
|
||||
const updateData: Record<string, unknown> = {};
|
||||
|
||||
for (const key of Object.keys(diff)) {
|
||||
if (key === 'displayValue') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const diffValue = diff[key as keyof ViewFilterWorkspaceEntity];
|
||||
|
||||
if (isDefined(diffValue)) {
|
||||
if (key === 'value' && typeof diffValue.after === 'string') {
|
||||
updateData[key] = convertViewFilterWorkspaceValueToCoreValue(
|
||||
diffValue.after,
|
||||
);
|
||||
} else if (key === 'operand' && diffValue.after) {
|
||||
updateData[key] = convertViewFilterOperandToCoreOperand(
|
||||
diffValue.after as SharedViewFilterOperand,
|
||||
);
|
||||
} else {
|
||||
updateData[key] = diffValue.after;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return updateData as Partial<ViewFilterEntity>;
|
||||
}
|
||||
|
||||
public async createCoreViewFilter(
|
||||
workspaceId: string,
|
||||
workspaceViewFilter: ViewFilterWorkspaceEntity,
|
||||
): Promise<void> {
|
||||
if (!workspaceViewFilter.viewId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const coreViewFilter: Partial<ViewFilterEntity> = {
|
||||
id: workspaceViewFilter.id,
|
||||
fieldMetadataId: workspaceViewFilter.fieldMetadataId,
|
||||
viewId: workspaceViewFilter.viewId,
|
||||
operand: convertViewFilterOperandToCoreOperand(
|
||||
workspaceViewFilter.operand as SharedViewFilterOperand,
|
||||
),
|
||||
value: convertViewFilterWorkspaceValueToCoreValue(
|
||||
workspaceViewFilter.value,
|
||||
),
|
||||
viewFilterGroupId: workspaceViewFilter.viewFilterGroupId,
|
||||
workspaceId,
|
||||
createdAt: new Date(workspaceViewFilter.createdAt),
|
||||
updatedAt: new Date(workspaceViewFilter.updatedAt),
|
||||
deletedAt: workspaceViewFilter.deletedAt
|
||||
? new Date(workspaceViewFilter.deletedAt)
|
||||
: null,
|
||||
};
|
||||
|
||||
await this.coreViewFilterRepository.save(coreViewFilter);
|
||||
}
|
||||
|
||||
public async updateCoreViewFilter(
|
||||
workspaceId: string,
|
||||
workspaceViewFilter: ViewFilterWorkspaceEntity,
|
||||
diff?: Partial<ObjectRecordDiff<ViewFilterWorkspaceEntity>>,
|
||||
): Promise<void> {
|
||||
if (!workspaceViewFilter.viewId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!diff || Object.keys(diff).length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updateData = this.parseUpdateDataFromDiff(diff);
|
||||
|
||||
if (Object.keys(updateData).length > 0) {
|
||||
await this.coreViewFilterRepository.update(
|
||||
{ id: workspaceViewFilter.id, workspaceId },
|
||||
updateData,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteCoreViewFilter(
|
||||
workspaceId: string,
|
||||
workspaceViewFilter: Pick<ViewFilterWorkspaceEntity, 'id'>,
|
||||
): Promise<void> {
|
||||
await this.coreViewFilterRepository.softDelete({
|
||||
id: workspaceViewFilter.id,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
public async destroyCoreViewFilter(
|
||||
workspaceId: string,
|
||||
workspaceViewFilter: Pick<ViewFilterWorkspaceEntity, 'id'>,
|
||||
): Promise<void> {
|
||||
await this.coreViewFilterRepository.delete({
|
||||
id: workspaceViewFilter.id,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
public async restoreCoreViewFilter(
|
||||
workspaceId: string,
|
||||
workspaceViewFilter: Pick<ViewFilterWorkspaceEntity, 'id'>,
|
||||
): Promise<void> {
|
||||
await this.coreViewFilterRepository.restore({
|
||||
id: workspaceViewFilter.id,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { type ObjectRecordDiff } from 'src/engine/core-modules/event-emitter/types/object-record-diff';
|
||||
import { ViewGroupEntity } from 'src/engine/core-modules/view/entities/view-group.entity';
|
||||
import { type ViewGroupWorkspaceEntity } from 'src/modules/view/standard-objects/view-group.workspace-entity';
|
||||
|
||||
@Injectable()
|
||||
export class ViewGroupSyncService {
|
||||
constructor(
|
||||
@InjectRepository(ViewGroupEntity)
|
||||
private readonly coreViewGroupRepository: Repository<ViewGroupEntity>,
|
||||
) {}
|
||||
|
||||
private parseUpdateDataFromDiff(
|
||||
diff: Partial<ObjectRecordDiff<ViewGroupWorkspaceEntity>>,
|
||||
): Partial<ViewGroupEntity> {
|
||||
const updateData: Record<string, unknown> = {};
|
||||
|
||||
for (const key of Object.keys(diff)) {
|
||||
const diffValue = diff[key as keyof ViewGroupWorkspaceEntity];
|
||||
|
||||
if (isDefined(diffValue)) {
|
||||
updateData[key] = diffValue.after;
|
||||
}
|
||||
}
|
||||
|
||||
return updateData as Partial<ViewGroupEntity>;
|
||||
}
|
||||
|
||||
public async createCoreViewGroup(
|
||||
workspaceId: string,
|
||||
workspaceViewGroup: ViewGroupWorkspaceEntity,
|
||||
): Promise<void> {
|
||||
if (!workspaceViewGroup.viewId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const coreViewGroup: Partial<ViewGroupEntity> = {
|
||||
id: workspaceViewGroup.id,
|
||||
fieldMetadataId: workspaceViewGroup.fieldMetadataId,
|
||||
viewId: workspaceViewGroup.viewId,
|
||||
fieldValue: workspaceViewGroup.fieldValue,
|
||||
isVisible: workspaceViewGroup.isVisible,
|
||||
position: workspaceViewGroup.position,
|
||||
workspaceId,
|
||||
createdAt: new Date(workspaceViewGroup.createdAt),
|
||||
updatedAt: new Date(workspaceViewGroup.updatedAt),
|
||||
deletedAt: workspaceViewGroup.deletedAt
|
||||
? new Date(workspaceViewGroup.deletedAt)
|
||||
: null,
|
||||
};
|
||||
|
||||
await this.coreViewGroupRepository.save(coreViewGroup);
|
||||
}
|
||||
|
||||
public async updateCoreViewGroup(
|
||||
workspaceId: string,
|
||||
workspaceViewGroup: ViewGroupWorkspaceEntity,
|
||||
diff?: Partial<ObjectRecordDiff<ViewGroupWorkspaceEntity>>,
|
||||
): Promise<void> {
|
||||
if (!workspaceViewGroup.viewId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!diff || Object.keys(diff).length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updateData = this.parseUpdateDataFromDiff(diff);
|
||||
|
||||
if (Object.keys(updateData).length > 0) {
|
||||
await this.coreViewGroupRepository.update(
|
||||
{ id: workspaceViewGroup.id, workspaceId },
|
||||
updateData,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteCoreViewGroup(
|
||||
workspaceId: string,
|
||||
workspaceViewGroup: Pick<ViewGroupWorkspaceEntity, 'id'>,
|
||||
): Promise<void> {
|
||||
await this.coreViewGroupRepository.softDelete({
|
||||
id: workspaceViewGroup.id,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
public async destroyCoreViewGroup(
|
||||
workspaceId: string,
|
||||
workspaceViewGroup: Pick<ViewGroupWorkspaceEntity, 'id'>,
|
||||
): Promise<void> {
|
||||
await this.coreViewGroupRepository.delete({
|
||||
id: workspaceViewGroup.id,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
public async restoreCoreViewGroup(
|
||||
workspaceId: string,
|
||||
workspaceViewGroup: Pick<ViewGroupWorkspaceEntity, 'id'>,
|
||||
): Promise<void> {
|
||||
await this.coreViewGroupRepository.restore({
|
||||
id: workspaceViewGroup.id,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { type ObjectRecordDiff } from 'src/engine/core-modules/event-emitter/types/object-record-diff';
|
||||
import { ViewSortEntity } from 'src/engine/core-modules/view/entities/view-sort.entity';
|
||||
import { type ViewSortDirection } from 'src/engine/core-modules/view/enums/view-sort-direction';
|
||||
import { type ViewSortWorkspaceEntity } from 'src/modules/view/standard-objects/view-sort.workspace-entity';
|
||||
|
||||
@Injectable()
|
||||
export class ViewSortSyncService {
|
||||
constructor(
|
||||
@InjectRepository(ViewSortEntity)
|
||||
private readonly coreViewSortRepository: Repository<ViewSortEntity>,
|
||||
) {}
|
||||
|
||||
private parseUpdateDataFromDiff(
|
||||
diff: Partial<ObjectRecordDiff<ViewSortWorkspaceEntity>>,
|
||||
): Partial<ViewSortEntity> {
|
||||
const updateData: Record<string, unknown> = {};
|
||||
|
||||
for (const key of Object.keys(diff)) {
|
||||
const diffValue = diff[key as keyof ViewSortWorkspaceEntity];
|
||||
|
||||
if (isDefined(diffValue)) {
|
||||
if (key === 'direction') {
|
||||
updateData[key] = (
|
||||
diffValue.after as string
|
||||
).toUpperCase() as ViewSortDirection;
|
||||
} else {
|
||||
updateData[key] = diffValue.after;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return updateData as Partial<ViewSortEntity>;
|
||||
}
|
||||
|
||||
public async createCoreViewSort(
|
||||
workspaceId: string,
|
||||
workspaceViewSort: ViewSortWorkspaceEntity,
|
||||
): Promise<void> {
|
||||
if (!workspaceViewSort.viewId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const direction =
|
||||
workspaceViewSort.direction.toUpperCase() as ViewSortDirection;
|
||||
|
||||
const coreViewSort: Partial<ViewSortEntity> = {
|
||||
id: workspaceViewSort.id,
|
||||
fieldMetadataId: workspaceViewSort.fieldMetadataId,
|
||||
viewId: workspaceViewSort.viewId,
|
||||
direction: direction,
|
||||
workspaceId,
|
||||
createdAt: new Date(workspaceViewSort.createdAt),
|
||||
updatedAt: new Date(workspaceViewSort.updatedAt),
|
||||
deletedAt: workspaceViewSort.deletedAt
|
||||
? new Date(workspaceViewSort.deletedAt)
|
||||
: null,
|
||||
};
|
||||
|
||||
await this.coreViewSortRepository.save(coreViewSort);
|
||||
}
|
||||
|
||||
public async updateCoreViewSort(
|
||||
workspaceId: string,
|
||||
workspaceViewSort: ViewSortWorkspaceEntity,
|
||||
diff?: Partial<ObjectRecordDiff<ViewSortWorkspaceEntity>>,
|
||||
): Promise<void> {
|
||||
if (!workspaceViewSort.viewId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!diff || Object.keys(diff).length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updateData = this.parseUpdateDataFromDiff(diff);
|
||||
|
||||
if (Object.keys(updateData).length > 0) {
|
||||
await this.coreViewSortRepository.update(
|
||||
{ id: workspaceViewSort.id, workspaceId },
|
||||
updateData,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteCoreViewSort(
|
||||
workspaceId: string,
|
||||
workspaceViewSort: Pick<ViewSortWorkspaceEntity, 'id'>,
|
||||
): Promise<void> {
|
||||
await this.coreViewSortRepository.softDelete({
|
||||
id: workspaceViewSort.id,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
public async destroyCoreViewSort(
|
||||
workspaceId: string,
|
||||
workspaceViewSort: Pick<ViewSortWorkspaceEntity, 'id'>,
|
||||
): Promise<void> {
|
||||
await this.coreViewSortRepository.delete({
|
||||
id: workspaceViewSort.id,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
public async restoreCoreViewSort(
|
||||
workspaceId: string,
|
||||
workspaceViewSort: Pick<ViewSortWorkspaceEntity, 'id'>,
|
||||
): Promise<void> {
|
||||
await this.coreViewSortRepository.restore({
|
||||
id: workspaceViewSort.id,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { type ObjectRecordDiff } from 'src/engine/core-modules/event-emitter/types/object-record-diff';
|
||||
import { ViewEntity } from 'src/engine/core-modules/view/entities/view.entity';
|
||||
import { ViewKey } from 'src/engine/core-modules/view/enums/view-key.enum';
|
||||
import { ViewOpenRecordIn } from 'src/engine/core-modules/view/enums/view-open-record-in';
|
||||
import { ViewType } from 'src/engine/core-modules/view/enums/view-type.enum';
|
||||
import { type ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
|
||||
|
||||
@Injectable()
|
||||
export class ViewSyncService {
|
||||
constructor(
|
||||
@InjectRepository(ViewEntity)
|
||||
private readonly coreViewRepository: Repository<ViewEntity>,
|
||||
) {}
|
||||
|
||||
private parseUpdateDataFromDiff(
|
||||
diff: Partial<ObjectRecordDiff<ViewWorkspaceEntity>>,
|
||||
): Partial<ViewEntity> {
|
||||
const updateData: Record<string, unknown> = {};
|
||||
|
||||
for (const key of Object.keys(diff)) {
|
||||
if (key === 'kanbanFieldMetadataId') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const diffValue = diff[key as keyof ViewWorkspaceEntity];
|
||||
|
||||
if (isDefined(diffValue)) {
|
||||
if (key === 'openRecordIn') {
|
||||
updateData[key] =
|
||||
diffValue.after === 'SIDE_PANEL'
|
||||
? ViewOpenRecordIn.SIDE_PANEL
|
||||
: ViewOpenRecordIn.RECORD_PAGE;
|
||||
} else if (key === 'type') {
|
||||
updateData[key] =
|
||||
diffValue.after === 'table' ? ViewType.TABLE : ViewType.KANBAN;
|
||||
} else if (key === 'key') {
|
||||
updateData[key] =
|
||||
diffValue.after === 'INDEX' || diffValue.after === ViewKey.INDEX
|
||||
? ViewKey.INDEX
|
||||
: null;
|
||||
} else {
|
||||
updateData[key] = diffValue.after;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return updateData as Partial<ViewEntity>;
|
||||
}
|
||||
|
||||
public async createCoreView(
|
||||
workspaceId: string,
|
||||
workspaceView: ViewWorkspaceEntity,
|
||||
): Promise<void> {
|
||||
let viewName = workspaceView.name;
|
||||
|
||||
if (workspaceView.key === 'INDEX') {
|
||||
// All INDEX views use the template for consistency
|
||||
viewName = 'All {objectLabelPlural}';
|
||||
}
|
||||
|
||||
const coreView: Partial<ViewEntity> = {
|
||||
id: workspaceView.id,
|
||||
name: viewName,
|
||||
objectMetadataId: workspaceView.objectMetadataId,
|
||||
type: workspaceView.type === 'table' ? ViewType.TABLE : ViewType.KANBAN,
|
||||
key:
|
||||
workspaceView.key === 'INDEX' || workspaceView.key === ViewKey.INDEX
|
||||
? ViewKey.INDEX
|
||||
: null,
|
||||
icon: workspaceView.icon,
|
||||
position: workspaceView.position,
|
||||
isCompact: workspaceView.isCompact,
|
||||
isCustom: workspaceView.key !== 'INDEX',
|
||||
openRecordIn:
|
||||
workspaceView.openRecordIn === 'SIDE_PANEL'
|
||||
? ViewOpenRecordIn.SIDE_PANEL
|
||||
: ViewOpenRecordIn.RECORD_PAGE,
|
||||
kanbanAggregateOperation: workspaceView.kanbanAggregateOperation,
|
||||
kanbanAggregateOperationFieldMetadataId:
|
||||
workspaceView.kanbanAggregateOperationFieldMetadataId,
|
||||
workspaceId,
|
||||
createdAt: new Date(workspaceView.createdAt),
|
||||
updatedAt: new Date(workspaceView.updatedAt),
|
||||
deletedAt: workspaceView.deletedAt
|
||||
? new Date(workspaceView.deletedAt)
|
||||
: null,
|
||||
};
|
||||
|
||||
await this.coreViewRepository.save(coreView);
|
||||
}
|
||||
|
||||
public async updateCoreView(
|
||||
workspaceId: string,
|
||||
workspaceView: ViewWorkspaceEntity,
|
||||
diff?: Partial<ObjectRecordDiff<ViewWorkspaceEntity>>,
|
||||
): Promise<void> {
|
||||
if (!diff || Object.keys(diff).length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updateData = this.parseUpdateDataFromDiff(diff);
|
||||
|
||||
if (Object.keys(updateData).length > 0) {
|
||||
await this.coreViewRepository.update(
|
||||
{ id: workspaceView.id, workspaceId },
|
||||
updateData,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async deleteCoreView(
|
||||
workspaceId: string,
|
||||
workspaceView: ViewWorkspaceEntity,
|
||||
): Promise<void> {
|
||||
await this.coreViewRepository.softDelete({
|
||||
id: workspaceView.id,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
public async destroyCoreView(
|
||||
workspaceId: string,
|
||||
workspaceView: ViewWorkspaceEntity,
|
||||
): Promise<void> {
|
||||
await this.coreViewRepository.delete({
|
||||
id: workspaceView.id,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
public async restoreCoreView(
|
||||
workspaceId: string,
|
||||
workspaceView: ViewWorkspaceEntity,
|
||||
): Promise<void> {
|
||||
await this.coreViewRepository.restore({
|
||||
id: workspaceView.id,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { isDefined } from 'class-validator';
|
||||
import isEmpty from 'lodash.isempty';
|
||||
|
||||
import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
|
||||
@Injectable()
|
||||
export class ViewService {
|
||||
private readonly logger = new Logger(ViewService.name);
|
||||
constructor(
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {}
|
||||
|
||||
async addFieldToViews({
|
||||
workspaceId,
|
||||
fieldId,
|
||||
viewsIds,
|
||||
positions,
|
||||
size,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
fieldId: string;
|
||||
viewsIds: string[];
|
||||
positions?: {
|
||||
[viewId: string]: number;
|
||||
}[];
|
||||
size?: number;
|
||||
}) {
|
||||
const viewFieldRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
'viewField',
|
||||
);
|
||||
|
||||
for (const viewId of viewsIds) {
|
||||
// @ts-expect-error legacy noImplicitAny
|
||||
const position = positions?.[viewId];
|
||||
const newFieldInThisView = await viewFieldRepository.findBy({
|
||||
fieldMetadataId: fieldId,
|
||||
viewId: viewId as string,
|
||||
isVisible: true,
|
||||
});
|
||||
|
||||
if (!isEmpty(newFieldInThisView)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Adding new field ${fieldId} to view ${viewId} for workspace ${workspaceId}...`,
|
||||
);
|
||||
const newViewField = viewFieldRepository.create({
|
||||
viewId: viewId,
|
||||
fieldMetadataId: fieldId,
|
||||
isVisible: true,
|
||||
...(isDefined(position) && { position: position }),
|
||||
...(isDefined(size) && { size: size }),
|
||||
});
|
||||
|
||||
await viewFieldRepository.save(newViewField);
|
||||
this.logger.log(
|
||||
`New field successfully added to view ${viewId} for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async removeFieldFromViews({
|
||||
workspaceId,
|
||||
fieldId,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
fieldId: string;
|
||||
}) {
|
||||
const viewFieldRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
'viewField',
|
||||
);
|
||||
const viewsWithField = await viewFieldRepository.find({
|
||||
where: {
|
||||
fieldMetadataId: fieldId,
|
||||
isVisible: true,
|
||||
},
|
||||
});
|
||||
|
||||
for (const viewWithField of viewsWithField) {
|
||||
const viewId = viewWithField.viewId;
|
||||
|
||||
this.logger.log(
|
||||
`Removing field ${fieldId} from view ${viewId} for workspace ${workspaceId}...`,
|
||||
);
|
||||
await viewFieldRepository.delete({
|
||||
viewId: viewWithField.viewId as string,
|
||||
fieldMetadataId: fieldId,
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`Field ${fieldId} successfully removed from view ${viewId} for workspace ${workspaceId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async getViewsIdsForObjectMetadataId({
|
||||
workspaceId,
|
||||
objectMetadataId,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
objectMetadataId: string;
|
||||
}) {
|
||||
const viewRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
'view',
|
||||
);
|
||||
|
||||
return viewRepository
|
||||
.find({
|
||||
where: {
|
||||
objectMetadataId: objectMetadataId,
|
||||
},
|
||||
})
|
||||
.then((views) => views.map((view) => view.id));
|
||||
}
|
||||
|
||||
async resetKanbanAggregateOperationByFieldMetadataId({
|
||||
workspaceId,
|
||||
fieldMetadataId,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
fieldMetadataId: string;
|
||||
}) {
|
||||
const viewRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
'view',
|
||||
);
|
||||
|
||||
await viewRepository.update(
|
||||
{ kanbanAggregateOperationFieldMetadataId: fieldMetadataId },
|
||||
{
|
||||
kanbanAggregateOperationFieldMetadataId: null,
|
||||
kanbanAggregateOperation: AggregateOperations.COUNT,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -9,20 +9,6 @@ import { ViewGroupEntity } from 'src/engine/core-modules/view/entities/view-grou
|
||||
import { ViewSortEntity } from 'src/engine/core-modules/view/entities/view-sort.entity';
|
||||
import { ViewEntity } from 'src/engine/core-modules/view/entities/view.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { ViewFieldListener } from 'src/modules/view/listeners/view-field.listener';
|
||||
import { ViewFilterGroupListener } from 'src/modules/view/listeners/view-filter-group.listener';
|
||||
import { ViewFilterListener } from 'src/modules/view/listeners/view-filter.listener';
|
||||
import { ViewGroupListener } from 'src/modules/view/listeners/view-group.listener';
|
||||
import { ViewSortListener } from 'src/modules/view/listeners/view-sort.listener';
|
||||
import { ViewListener } from 'src/modules/view/listeners/view.listener';
|
||||
import { ViewDeleteOnePreQueryHook } from 'src/modules/view/pre-hooks/view-delete-one.pre-query.hook';
|
||||
import { ViewFieldSyncService } from 'src/modules/view/services/view-field-sync.service';
|
||||
import { ViewFilterGroupSyncService } from 'src/modules/view/services/view-filter-group-sync.service';
|
||||
import { ViewFilterSyncService } from 'src/modules/view/services/view-filter-sync.service';
|
||||
import { ViewGroupSyncService } from 'src/modules/view/services/view-group-sync.service';
|
||||
import { ViewSortSyncService } from 'src/modules/view/services/view-sort-sync.service';
|
||||
import { ViewSyncService } from 'src/modules/view/services/view-sync.service';
|
||||
import { ViewService } from 'src/modules/view/services/view.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -38,22 +24,7 @@ import { ViewService } from 'src/modules/view/services/view.service';
|
||||
FeatureFlagModule,
|
||||
],
|
||||
|
||||
providers: [
|
||||
ViewService,
|
||||
ViewDeleteOnePreQueryHook,
|
||||
ViewSyncService,
|
||||
ViewFieldSyncService,
|
||||
ViewFilterSyncService,
|
||||
ViewFilterGroupSyncService,
|
||||
ViewGroupSyncService,
|
||||
ViewSortSyncService,
|
||||
ViewListener,
|
||||
ViewFieldListener,
|
||||
ViewFilterListener,
|
||||
ViewFilterGroupListener,
|
||||
ViewGroupListener,
|
||||
ViewSortListener,
|
||||
],
|
||||
exports: [ViewService],
|
||||
providers: [],
|
||||
exports: [],
|
||||
})
|
||||
export class ViewModule {}
|
||||
|
||||
+28
-21
@@ -13,7 +13,8 @@ import { PermissionsExceptionMessage } from 'src/engine/metadata-modules/permiss
|
||||
|
||||
describe('updateOneObjectRecordsPermissions', () => {
|
||||
const personId = randomUUID();
|
||||
let allPetsViewId: string;
|
||||
let messageId: string;
|
||||
let originalMessageText: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const createPersonOperation = createOneOperationFactory({
|
||||
@@ -27,34 +28,38 @@ describe('updateOneObjectRecordsPermissions', () => {
|
||||
|
||||
await makeGraphqlAPIRequest(createPersonOperation);
|
||||
|
||||
const findAllPetsViewOperation = findOneOperationFactory({
|
||||
objectMetadataSingularName: 'view',
|
||||
gqlFields: 'id',
|
||||
const findAllMessagesOperation = findOneOperationFactory({
|
||||
objectMetadataSingularName: 'message',
|
||||
gqlFields: `
|
||||
id
|
||||
text
|
||||
`,
|
||||
filter: {
|
||||
name: {
|
||||
eq: 'All Pets',
|
||||
subject: {
|
||||
eq: 'Meeting Request',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const findAllPetsViewResponse = await makeGraphqlAPIRequest(
|
||||
findAllPetsViewOperation,
|
||||
const findAllMessagesResponse = await makeGraphqlAPIRequest(
|
||||
findAllMessagesOperation,
|
||||
);
|
||||
|
||||
allPetsViewId = findAllPetsViewResponse.body.data.view.id;
|
||||
messageId = findAllMessagesResponse.body.data.message.id;
|
||||
originalMessageText = findAllMessagesResponse.body.data.message.text;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
const updateViewOperation = updateOneOperationFactory({
|
||||
objectMetadataSingularName: 'view',
|
||||
const updateMessageOperation = updateOneOperationFactory({
|
||||
objectMetadataSingularName: 'message',
|
||||
gqlFields: 'id',
|
||||
recordId: allPetsViewId,
|
||||
recordId: messageId,
|
||||
data: {
|
||||
icon: 'IconList',
|
||||
text: originalMessageText,
|
||||
},
|
||||
});
|
||||
|
||||
await makeGraphqlAPIRequest(updateViewOperation);
|
||||
await makeGraphqlAPIRequest(updateMessageOperation);
|
||||
});
|
||||
|
||||
it('should throw a permission error when user does not have permission (guest role)', async () => {
|
||||
@@ -79,23 +84,25 @@ describe('updateOneObjectRecordsPermissions', () => {
|
||||
|
||||
it('should allow to update a system object record even without update permission (guest role)', async () => {
|
||||
const graphqlOperation = updateOneOperationFactory({
|
||||
objectMetadataSingularName: 'view',
|
||||
objectMetadataSingularName: 'message',
|
||||
gqlFields: `
|
||||
id
|
||||
icon
|
||||
text
|
||||
`,
|
||||
recordId: allPetsViewId,
|
||||
recordId: messageId,
|
||||
data: {
|
||||
icon: 'IconDog',
|
||||
text: "Hello, I'm fine, thank you!",
|
||||
},
|
||||
});
|
||||
|
||||
const response = await makeGraphqlAPIRequestWithGuestRole(graphqlOperation);
|
||||
|
||||
expect(response.body.data).toBeDefined();
|
||||
expect(response.body.data.updateView).toBeDefined();
|
||||
expect(response.body.data.updateView.id).toBe(allPetsViewId);
|
||||
expect(response.body.data.updateView.icon).toBe('IconDog');
|
||||
expect(response.body.data.updateMessage).toBeDefined();
|
||||
expect(response.body.data.updateMessage.id).toBe(messageId);
|
||||
expect(response.body.data.updateMessage.text).toBe(
|
||||
"Hello, I'm fine, thank you!",
|
||||
);
|
||||
});
|
||||
|
||||
it('should update an object record when user has permission (admin role)', async () => {
|
||||
|
||||
+4
-4
@@ -41,10 +41,10 @@ describe('View Field Resolver', () => {
|
||||
},
|
||||
} = await createOneObjectMetadata({
|
||||
input: {
|
||||
nameSingular: 'myTestObject',
|
||||
namePlural: 'myTestObjects',
|
||||
labelSingular: 'My Test Object',
|
||||
labelPlural: 'My Test Objects',
|
||||
nameSingular: 'myFieldTestObject',
|
||||
namePlural: 'myFieldTestObjects',
|
||||
labelSingular: 'My Field Test Object',
|
||||
labelPlural: 'My Field Test Objects',
|
||||
icon: 'Icon123',
|
||||
},
|
||||
});
|
||||
|
||||
+4
-4
@@ -41,10 +41,10 @@ describe('View Filter Group Resolver', () => {
|
||||
},
|
||||
} = await createOneObjectMetadata({
|
||||
input: {
|
||||
nameSingular: 'myTestObject',
|
||||
namePlural: 'myTestObjects',
|
||||
labelSingular: 'My Test Object',
|
||||
labelPlural: 'My Test Objects',
|
||||
nameSingular: 'myFilterGroupTestObject',
|
||||
namePlural: 'myFilterGroupTestObjects',
|
||||
labelSingular: 'My Filter Group Test Object',
|
||||
labelPlural: 'My Filter Group Test Objects',
|
||||
icon: 'Icon123',
|
||||
},
|
||||
});
|
||||
|
||||
+4
-4
@@ -39,10 +39,10 @@ describe('View Filter Resolver', () => {
|
||||
},
|
||||
} = await createOneObjectMetadata({
|
||||
input: {
|
||||
nameSingular: 'myTestObject',
|
||||
namePlural: 'myTestObjects',
|
||||
labelSingular: 'My Test Object',
|
||||
labelPlural: 'My Test Objects',
|
||||
nameSingular: 'myFilterTestObject',
|
||||
namePlural: 'myFilterTestObjects',
|
||||
labelSingular: 'My Filter Test Object',
|
||||
labelPlural: 'My Filter Test Objects',
|
||||
icon: 'Icon123',
|
||||
},
|
||||
});
|
||||
|
||||
+4
-4
@@ -41,10 +41,10 @@ describe('View Group Resolver', () => {
|
||||
},
|
||||
} = await createOneObjectMetadata({
|
||||
input: {
|
||||
nameSingular: 'myTestObject',
|
||||
namePlural: 'myTestObjects',
|
||||
labelSingular: 'My Test Object',
|
||||
labelPlural: 'My Test Objects',
|
||||
nameSingular: 'myGroupTestObject',
|
||||
namePlural: 'myGroupTestObjects',
|
||||
labelSingular: 'My Group Test Object',
|
||||
labelPlural: 'My Group Test Objects',
|
||||
icon: 'Icon123',
|
||||
},
|
||||
});
|
||||
|
||||
+4
-4
@@ -40,10 +40,10 @@ describe('View Resolver', () => {
|
||||
},
|
||||
} = await createOneObjectMetadata({
|
||||
input: {
|
||||
nameSingular: 'myTestObject',
|
||||
namePlural: 'myTestObjects',
|
||||
labelSingular: 'My Test Object',
|
||||
labelPlural: 'My Test Objects',
|
||||
nameSingular: 'myViewTestObject',
|
||||
namePlural: 'myViewTestObjects',
|
||||
labelSingular: 'My View Test Object',
|
||||
labelPlural: 'My View Test Objects',
|
||||
icon: 'Icon123',
|
||||
},
|
||||
});
|
||||
|
||||
+4
-4
@@ -42,10 +42,10 @@ describe('View Sort Resolver', () => {
|
||||
},
|
||||
} = await createOneObjectMetadata({
|
||||
input: {
|
||||
nameSingular: 'myTestObject',
|
||||
namePlural: 'myTestObjects',
|
||||
labelSingular: 'My Test Object',
|
||||
labelPlural: 'My Test Objects',
|
||||
nameSingular: 'mySortTestObject',
|
||||
namePlural: 'mySortTestObjects',
|
||||
labelSingular: 'My Sort Test Object',
|
||||
labelPlural: 'My Sort Test Objects',
|
||||
icon: 'Icon123',
|
||||
},
|
||||
});
|
||||
|
||||
+101
-105
@@ -1,49 +1,43 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`update-one-field-metadata-related-record MULTI_SELECT should delete related view filter if all select field options got deleted 1`] = `
|
||||
exports[`update-one-field-metadata-related-record MULTI_SELECT should handle adding new options while maintaining existing view filter 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "NOT_FOUND",
|
||||
"subCode": "RECORD_NOT_FOUND",
|
||||
"userFriendlyMessage": "An error occurred.",
|
||||
},
|
||||
"message": "Record not found",
|
||||
"name": "NotFoundError",
|
||||
},
|
||||
"OPTION_0",
|
||||
"OPTION_1",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`update-one-field-metadata-related-record MULTI_SELECT should handle adding new options while maintaining existing view filter 1`] = `
|
||||
{
|
||||
"displayValue": "2 options",
|
||||
"id": Any<String>,
|
||||
"value": "["OPTION_0","OPTION_1"]",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`update-one-field-metadata-related-record MULTI_SELECT should handle no changes update of options while maintaining existing view filter values 1`] = `
|
||||
{
|
||||
"displayValue": "10 options",
|
||||
"id": Any<String>,
|
||||
"value": "["OPTION_0","OPTION_1","OPTION_2","OPTION_3","OPTION_4","OPTION_5","OPTION_6","OPTION_7","OPTION_8","OPTION_9"]",
|
||||
}
|
||||
[
|
||||
"OPTION_0",
|
||||
"OPTION_1",
|
||||
"OPTION_2",
|
||||
"OPTION_3",
|
||||
"OPTION_4",
|
||||
"OPTION_5",
|
||||
"OPTION_6",
|
||||
"OPTION_7",
|
||||
"OPTION_8",
|
||||
"OPTION_9",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`update-one-field-metadata-related-record MULTI_SELECT should handle partial deletion of selected options in view filter 1`] = `
|
||||
{
|
||||
"displayValue": "6 options",
|
||||
"id": Any<String>,
|
||||
"value": "["OPTION_4","OPTION_5","OPTION_6","OPTION_7","OPTION_8","OPTION_9"]",
|
||||
}
|
||||
[
|
||||
"OPTION_4",
|
||||
"OPTION_5",
|
||||
"OPTION_6",
|
||||
"OPTION_7",
|
||||
"OPTION_8",
|
||||
"OPTION_9",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`update-one-field-metadata-related-record MULTI_SELECT should handle reordering of options while maintaining view filter values 1`] = `
|
||||
{
|
||||
"displayValue": "2 options",
|
||||
"id": Any<String>,
|
||||
"value": "["OPTION_0","OPTION_1"]",
|
||||
}
|
||||
[
|
||||
"OPTION_0",
|
||||
"OPTION_1",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`update-one-field-metadata-related-record MULTI_SELECT should throw error if view filter value is not a stringified JSON array 1`] = `
|
||||
@@ -60,81 +54,79 @@ exports[`update-one-field-metadata-related-record MULTI_SELECT should throw erro
|
||||
`;
|
||||
|
||||
exports[`update-one-field-metadata-related-record MULTI_SELECT should update display value with options label if less than 3 options are selected 1`] = `
|
||||
{
|
||||
"displayValue": "Option 8, Option 9",
|
||||
"id": Any<String>,
|
||||
"value": "["OPTION_8","OPTION_9"]",
|
||||
}
|
||||
[
|
||||
"OPTION_8",
|
||||
"OPTION_9",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`update-one-field-metadata-related-record MULTI_SELECT should update related multi selected options view filter 1`] = `
|
||||
{
|
||||
"displayValue": "10 options",
|
||||
"id": Any<String>,
|
||||
"value": "["OPTION_0_UPDATED","OPTION_1","OPTION_2_UPDATED","OPTION_3","OPTION_4_UPDATED","OPTION_5","OPTION_6_UPDATED","OPTION_7","OPTION_8_UPDATED","OPTION_9"]",
|
||||
}
|
||||
[
|
||||
"OPTION_0_UPDATED",
|
||||
"OPTION_1",
|
||||
"OPTION_2_UPDATED",
|
||||
"OPTION_3",
|
||||
"OPTION_4_UPDATED",
|
||||
"OPTION_5",
|
||||
"OPTION_6_UPDATED",
|
||||
"OPTION_7",
|
||||
"OPTION_8_UPDATED",
|
||||
"OPTION_9",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`update-one-field-metadata-related-record MULTI_SELECT should update related solo selected option view filter 1`] = `
|
||||
{
|
||||
"displayValue": "Option 5 updated",
|
||||
"id": Any<String>,
|
||||
"value": "["OPTION_5_UPDATED"]",
|
||||
}
|
||||
[
|
||||
"OPTION_5_UPDATED",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`update-one-field-metadata-related-record MULTI_SELECT should update the display value on an option label change only 1`] = `
|
||||
{
|
||||
"displayValue": "Option 0 updated, Option 1 updated, Option 2 updated",
|
||||
"id": Any<String>,
|
||||
"value": "["OPTION_0","OPTION_1","OPTION_2"]",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`update-one-field-metadata-related-record SELECT should delete related view filter if all select field options got deleted 1`] = `
|
||||
[
|
||||
{
|
||||
"extensions": {
|
||||
"code": "NOT_FOUND",
|
||||
"subCode": "RECORD_NOT_FOUND",
|
||||
"userFriendlyMessage": "An error occurred.",
|
||||
},
|
||||
"message": "Record not found",
|
||||
"name": "NotFoundError",
|
||||
},
|
||||
"OPTION_0",
|
||||
"OPTION_1",
|
||||
"OPTION_2",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`update-one-field-metadata-related-record SELECT should handle adding new options while maintaining existing view filter 1`] = `
|
||||
{
|
||||
"displayValue": "2 options",
|
||||
"id": Any<String>,
|
||||
"value": "["OPTION_0","OPTION_1"]",
|
||||
}
|
||||
[
|
||||
"OPTION_0",
|
||||
"OPTION_1",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`update-one-field-metadata-related-record SELECT should handle no changes update of options while maintaining existing view filter values 1`] = `
|
||||
{
|
||||
"displayValue": "10 options",
|
||||
"id": Any<String>,
|
||||
"value": "["OPTION_0","OPTION_1","OPTION_2","OPTION_3","OPTION_4","OPTION_5","OPTION_6","OPTION_7","OPTION_8","OPTION_9"]",
|
||||
}
|
||||
[
|
||||
"OPTION_0",
|
||||
"OPTION_1",
|
||||
"OPTION_2",
|
||||
"OPTION_3",
|
||||
"OPTION_4",
|
||||
"OPTION_5",
|
||||
"OPTION_6",
|
||||
"OPTION_7",
|
||||
"OPTION_8",
|
||||
"OPTION_9",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`update-one-field-metadata-related-record SELECT should handle partial deletion of selected options in view filter 1`] = `
|
||||
{
|
||||
"displayValue": "6 options",
|
||||
"id": Any<String>,
|
||||
"value": "["OPTION_4","OPTION_5","OPTION_6","OPTION_7","OPTION_8","OPTION_9"]",
|
||||
}
|
||||
[
|
||||
"OPTION_4",
|
||||
"OPTION_5",
|
||||
"OPTION_6",
|
||||
"OPTION_7",
|
||||
"OPTION_8",
|
||||
"OPTION_9",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`update-one-field-metadata-related-record SELECT should handle reordering of options while maintaining view filter values 1`] = `
|
||||
{
|
||||
"displayValue": "2 options",
|
||||
"id": Any<String>,
|
||||
"value": "["OPTION_0","OPTION_1"]",
|
||||
}
|
||||
[
|
||||
"OPTION_0",
|
||||
"OPTION_1",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`update-one-field-metadata-related-record SELECT should throw error if view filter value is not a stringified JSON array 1`] = `
|
||||
@@ -151,33 +143,37 @@ exports[`update-one-field-metadata-related-record SELECT should throw error if v
|
||||
`;
|
||||
|
||||
exports[`update-one-field-metadata-related-record SELECT should update display value with options label if less than 3 options are selected 1`] = `
|
||||
{
|
||||
"displayValue": "Option 8, Option 9",
|
||||
"id": Any<String>,
|
||||
"value": "["OPTION_8","OPTION_9"]",
|
||||
}
|
||||
[
|
||||
"OPTION_8",
|
||||
"OPTION_9",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`update-one-field-metadata-related-record SELECT should update related multi selected options view filter 1`] = `
|
||||
{
|
||||
"displayValue": "10 options",
|
||||
"id": Any<String>,
|
||||
"value": "["OPTION_0_UPDATED","OPTION_1","OPTION_2_UPDATED","OPTION_3","OPTION_4_UPDATED","OPTION_5","OPTION_6_UPDATED","OPTION_7","OPTION_8_UPDATED","OPTION_9"]",
|
||||
}
|
||||
[
|
||||
"OPTION_0_UPDATED",
|
||||
"OPTION_1",
|
||||
"OPTION_2_UPDATED",
|
||||
"OPTION_3",
|
||||
"OPTION_4_UPDATED",
|
||||
"OPTION_5",
|
||||
"OPTION_6_UPDATED",
|
||||
"OPTION_7",
|
||||
"OPTION_8_UPDATED",
|
||||
"OPTION_9",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`update-one-field-metadata-related-record SELECT should update related solo selected option view filter 1`] = `
|
||||
{
|
||||
"displayValue": "Option 5 updated",
|
||||
"id": Any<String>,
|
||||
"value": "["OPTION_5_UPDATED"]",
|
||||
}
|
||||
[
|
||||
"OPTION_5_UPDATED",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`update-one-field-metadata-related-record SELECT should update the display value on an option label change only 1`] = `
|
||||
{
|
||||
"displayValue": "Option 0 updated, Option 1 updated, Option 2 updated",
|
||||
"id": Any<String>,
|
||||
"value": "["OPTION_0","OPTION_1","OPTION_2"]",
|
||||
}
|
||||
[
|
||||
"OPTION_0",
|
||||
"OPTION_1",
|
||||
"OPTION_2",
|
||||
]
|
||||
`;
|
||||
|
||||
+27
-53
@@ -1,6 +1,3 @@
|
||||
import { createOneOperationFactory } from 'test/integration/graphql/utils/create-one-operation-factory.util';
|
||||
import { findOneOperationFactory } from 'test/integration/graphql/utils/find-one-operation-factory.util';
|
||||
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
|
||||
import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util';
|
||||
import { deleteOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/delete-one-field-metadata.util';
|
||||
import { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/update-one-field-metadata.util';
|
||||
@@ -10,8 +7,15 @@ import {
|
||||
} from 'test/integration/metadata/suites/object-metadata/constants/test-object-names.constant';
|
||||
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
|
||||
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
|
||||
import {
|
||||
createTestViewWithRestApi,
|
||||
findViewByIdWithRestApi,
|
||||
} from 'test/integration/rest/utils/view-rest-api.util';
|
||||
import { generateRecordName } from 'test/integration/utils/generate-record-name';
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
|
||||
import { ViewType } from 'src/engine/core-modules/view/enums/view-type.enum';
|
||||
|
||||
describe('deleteOne', () => {
|
||||
@@ -45,28 +49,15 @@ describe('deleteOne', () => {
|
||||
|
||||
testFieldId = createdFieldData.createOneField.id;
|
||||
|
||||
// create view
|
||||
const graphqlOperation = createOneOperationFactory({
|
||||
objectMetadataSingularName: 'View',
|
||||
gqlFields: `
|
||||
id
|
||||
kanbanAggregateOperationFieldMetadataId
|
||||
kanbanAggregateOperation
|
||||
`,
|
||||
data: {
|
||||
kanbanAggregateOperationFieldMetadataId: testFieldId,
|
||||
kanbanAggregateOperation: 'MAX',
|
||||
objectMetadataId: listingObjectId,
|
||||
name: 'By Type',
|
||||
type: ViewType.KANBAN,
|
||||
icon: 'IconLayoutKanban',
|
||||
},
|
||||
const createdView = await createTestViewWithRestApi({
|
||||
name: generateRecordName('By Type'),
|
||||
objectMetadataId: listingObjectId,
|
||||
type: ViewType.KANBAN,
|
||||
kanbanAggregateOperationFieldMetadataId: testFieldId,
|
||||
kanbanAggregateOperation: AggregateOperations.MAX,
|
||||
icon: 'IconLayoutKanban',
|
||||
});
|
||||
|
||||
const response = await makeGraphqlAPIRequest(graphqlOperation);
|
||||
|
||||
const createdView = response.body.data.createView;
|
||||
|
||||
viewId = createdView.id;
|
||||
});
|
||||
afterEach(async () => {
|
||||
@@ -76,30 +67,17 @@ describe('deleteOne', () => {
|
||||
});
|
||||
});
|
||||
it('should reset kanban aggregate operation when deleting a field used as kanbanAggregateOperationFieldMetadataId', async () => {
|
||||
// Arrange
|
||||
// 1. Check that view has expcted kanbanAggregateOperationFieldMetadataId and kanbanAggregateOperation
|
||||
const findViewOperation = findOneOperationFactory({
|
||||
objectMetadataSingularName: 'view',
|
||||
gqlFields: `
|
||||
id
|
||||
kanbanAggregateOperationFieldMetadataId
|
||||
kanbanAggregateOperation
|
||||
`,
|
||||
filter: {
|
||||
id: {
|
||||
eq: viewId,
|
||||
},
|
||||
},
|
||||
});
|
||||
const viewThatShouldBeUpdated = await findViewByIdWithRestApi(viewId);
|
||||
|
||||
const viewResponse = await makeGraphqlAPIRequest(findViewOperation);
|
||||
if (!isDefined(viewThatShouldBeUpdated)) {
|
||||
throw new Error('View not found, this should not happen');
|
||||
}
|
||||
|
||||
expect(
|
||||
viewResponse.body.data.view.kanbanAggregateOperationFieldMetadataId,
|
||||
viewThatShouldBeUpdated.kanbanAggregateOperationFieldMetadataId,
|
||||
).toBe(testFieldId);
|
||||
expect(viewResponse.body.data.view.kanbanAggregateOperation).toBe('MAX');
|
||||
expect(viewThatShouldBeUpdated.kanbanAggregateOperation).toBe('MAX');
|
||||
|
||||
// Deactivate field to be able to delete it after
|
||||
await updateOneFieldMetadata({
|
||||
input: {
|
||||
idToUpdate: testFieldId,
|
||||
@@ -111,26 +89,22 @@ describe('deleteOne', () => {
|
||||
`,
|
||||
});
|
||||
|
||||
// Act
|
||||
const { data } = await deleteOneFieldMetadata({
|
||||
input: { idToDelete: testFieldId },
|
||||
});
|
||||
|
||||
// Assert
|
||||
// 1. Field is deleted
|
||||
expect(data.deleteOneField.id).toBe(testFieldId);
|
||||
|
||||
// 2. Kanban aggregate operation has been reset on view using this field as kanbanAggregateOperationFieldMetadataId
|
||||
const updatedViewResponse =
|
||||
await makeGraphqlAPIRequest(findViewOperation);
|
||||
const updatedViewResponse = await findViewByIdWithRestApi(viewId);
|
||||
|
||||
if (!isDefined(updatedViewResponse)) {
|
||||
throw new Error('View not found, this should not happen');
|
||||
}
|
||||
|
||||
expect(
|
||||
updatedViewResponse.body.data.view
|
||||
.kanbanAggregateOperationFieldMetadataId,
|
||||
updatedViewResponse.kanbanAggregateOperationFieldMetadataId,
|
||||
).toBeNull();
|
||||
expect(updatedViewResponse.body.data.view.kanbanAggregateOperation).toBe(
|
||||
'COUNT',
|
||||
);
|
||||
expect(updatedViewResponse.kanbanAggregateOperation).toBe(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+63
-109
@@ -1,22 +1,31 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { createOneOperation } from 'test/integration/graphql/utils/create-one-operation.util';
|
||||
import { findOneOperation } from 'test/integration/graphql/utils/find-one-operation.util';
|
||||
import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util';
|
||||
import { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/update-one-field-metadata.util';
|
||||
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
|
||||
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
|
||||
import { getMockCreateObjectInput } from 'test/integration/metadata/suites/object-metadata/utils/generate-mock-create-object-metadata-input';
|
||||
import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util';
|
||||
import {
|
||||
createTestViewFilterWithRestApi,
|
||||
createTestViewWithRestApi,
|
||||
findViewFilterWithRestApi,
|
||||
} from 'test/integration/rest/utils/view-rest-api.util';
|
||||
import { type EachTestingContext } from 'twenty-shared/testing';
|
||||
import {
|
||||
type EnumFieldMetadataType,
|
||||
FieldMetadataType,
|
||||
} from 'twenty-shared/types';
|
||||
import { isDefined, parseJson } from 'twenty-shared/utils';
|
||||
import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { ViewFilterOperand } from 'src/engine/core-modules/view/enums/view-filter-operand';
|
||||
import { ViewType } from 'src/engine/core-modules/view/enums/view-type.enum';
|
||||
import { type ViewFilterValue } from 'src/engine/core-modules/view/types/view-filter-value.type';
|
||||
import {
|
||||
type FieldMetadataComplexOption,
|
||||
type FieldMetadataDefaultOption,
|
||||
} from 'src/engine/metadata-modules/field-metadata/dtos/options.input';
|
||||
import { SEED_APPLE_WORKSPACE_ID } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-workspaces.util';
|
||||
|
||||
type Option = FieldMetadataDefaultOption | FieldMetadataComplexOption;
|
||||
|
||||
@@ -28,7 +37,7 @@ const generateOption = (index: number): Option => ({
|
||||
});
|
||||
const generateOptions = (length: number) =>
|
||||
Array.from({ length }, (_value, index) => generateOption(index));
|
||||
const updateOption = ({ value, label, ...option }: Option) => ({
|
||||
const fakeOptionUpdate = ({ value, label, ...option }: Option) => ({
|
||||
...option,
|
||||
value: `${value}_UPDATED`,
|
||||
label: `${label} updated`,
|
||||
@@ -39,7 +48,6 @@ const ALL_OPTIONS = generateOptions(10);
|
||||
const isEven = (_value: unknown, index: number) => index % 2 === 0;
|
||||
|
||||
type ViewFilterUpdate = {
|
||||
displayValue: string;
|
||||
value: string[];
|
||||
};
|
||||
|
||||
@@ -68,6 +76,13 @@ describe('update-one-field-metadata-related-record', () => {
|
||||
options,
|
||||
type: fieldMetadataType,
|
||||
}: FieldMetadataOptionsAndType) => {
|
||||
await updateFeatureFlag({
|
||||
expectToFail: false,
|
||||
featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
|
||||
value: false,
|
||||
workspaceId: SEED_APPLE_WORKSPACE_ID,
|
||||
});
|
||||
|
||||
const singular = faker.lorem.words();
|
||||
const plural = singular + faker.lorem.word();
|
||||
const {
|
||||
@@ -101,25 +116,23 @@ describe('update-one-field-metadata-related-record', () => {
|
||||
`,
|
||||
});
|
||||
|
||||
const {
|
||||
data: { createOneResponse: createOneView },
|
||||
} = await createOneOperation<{
|
||||
id: string;
|
||||
objectMetadataId: string;
|
||||
type: string;
|
||||
}>({
|
||||
objectMetadataSingularName: 'view',
|
||||
input: {
|
||||
id: faker.string.uuid(),
|
||||
objectMetadataId: createOneObject.id,
|
||||
type: 'table',
|
||||
},
|
||||
const createdView = await createTestViewWithRestApi({
|
||||
id: faker.string.uuid(),
|
||||
name: 'Test View',
|
||||
objectMetadataId: createOneObject.id,
|
||||
type: ViewType.TABLE,
|
||||
});
|
||||
|
||||
return { createOneObject, createOneField, createOneView };
|
||||
return { createOneObject, createOneField, createdView };
|
||||
};
|
||||
|
||||
afterEach(async () => {
|
||||
await updateFeatureFlag({
|
||||
expectToFail: false,
|
||||
featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED,
|
||||
value: true,
|
||||
workspaceId: SEED_APPLE_WORKSPACE_ID,
|
||||
});
|
||||
if (isDefined(idToDelete)) {
|
||||
await deleteOneObjectMetadata({
|
||||
input: { idToDelete: idToDelete },
|
||||
@@ -142,7 +155,7 @@ describe('update-one-field-metadata-related-record', () => {
|
||||
context: {
|
||||
updateOptions: (options) =>
|
||||
options.map((option, index) =>
|
||||
isEven(option, index) ? updateOption(option) : option,
|
||||
isEven(option, index) ? fakeOptionUpdate(option) : option,
|
||||
),
|
||||
},
|
||||
},
|
||||
@@ -150,10 +163,9 @@ describe('update-one-field-metadata-related-record', () => {
|
||||
title: 'should update related solo selected option view filter',
|
||||
context: {
|
||||
createViewFilter: {
|
||||
displayValue: ALL_OPTIONS[5].label,
|
||||
value: [ALL_OPTIONS[5].value],
|
||||
},
|
||||
updateOptions: (options) => [updateOption(options[5])],
|
||||
updateOptions: (options) => [fakeOptionUpdate(options[5])],
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -168,7 +180,6 @@ describe('update-one-field-metadata-related-record', () => {
|
||||
'should handle reordering of options while maintaining view filter values',
|
||||
context: {
|
||||
createViewFilter: {
|
||||
displayValue: '2 options',
|
||||
value: ALL_OPTIONS.slice(0, 2).map((option) => option.value),
|
||||
},
|
||||
updateOptions: (options) => [...options].reverse(),
|
||||
@@ -190,7 +201,6 @@ describe('update-one-field-metadata-related-record', () => {
|
||||
type: fieldType,
|
||||
},
|
||||
createViewFilter: {
|
||||
displayValue: '2 options',
|
||||
value: ALL_OPTIONS.slice(0, 2).map((option) => option.value),
|
||||
},
|
||||
updateOptions: (options) => [
|
||||
@@ -199,27 +209,6 @@ describe('update-one-field-metadata-related-record', () => {
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
title:
|
||||
'should update display value with options label if less than 3 options are selected',
|
||||
context: {
|
||||
updateOptions: (options) => options.slice(8),
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'should update the display value on an option label change only',
|
||||
context: {
|
||||
createViewFilter: {
|
||||
displayValue: 'Option 3',
|
||||
value: ALL_OPTIONS.slice(0, 3).map((option) => option.value),
|
||||
},
|
||||
updateOptions: (options) =>
|
||||
options.map((option) => ({
|
||||
...option,
|
||||
label: `${option.label} updated`,
|
||||
})),
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
test.each(testCases)(
|
||||
@@ -228,34 +217,20 @@ describe('update-one-field-metadata-related-record', () => {
|
||||
context: {
|
||||
expected,
|
||||
createViewFilter = {
|
||||
displayValue: '10 options',
|
||||
value: ALL_OPTIONS.map((option) => option.value),
|
||||
},
|
||||
fieldMetadata = { options: ALL_OPTIONS, type: fieldType },
|
||||
updateOptions,
|
||||
},
|
||||
}) => {
|
||||
const { createOneField, createOneView } =
|
||||
const { createOneField, createdView } =
|
||||
await createObjectSelectFieldAndView(fieldMetadata);
|
||||
const {
|
||||
data: { createOneResponse: createOneViewFilter },
|
||||
} = await createOneOperation<{
|
||||
id: string;
|
||||
viewId: string;
|
||||
fieldMetadataId: string;
|
||||
operand: string;
|
||||
value: string;
|
||||
displayValue: string;
|
||||
}>({
|
||||
objectMetadataSingularName: 'viewFilter',
|
||||
input: {
|
||||
id: faker.string.uuid(),
|
||||
viewId: createOneView.id,
|
||||
fieldMetadataId: createOneField.id,
|
||||
operand: 'is',
|
||||
value: JSON.stringify(createViewFilter.value),
|
||||
displayValue: createViewFilter.displayValue,
|
||||
},
|
||||
|
||||
const createdViewFilter = await createTestViewFilterWithRestApi({
|
||||
viewId: createdView.id,
|
||||
fieldMetadataId: createOneField.id,
|
||||
operand: ViewFilterOperand.IS,
|
||||
value: createViewFilter.value,
|
||||
});
|
||||
|
||||
const optionsWithIds = createOneField.options;
|
||||
@@ -278,41 +253,31 @@ describe('update-one-field-metadata-related-record', () => {
|
||||
`,
|
||||
});
|
||||
|
||||
const {
|
||||
data: { findResponse },
|
||||
errors,
|
||||
} = await findOneOperation({
|
||||
gqlFields: `
|
||||
id
|
||||
displayValue
|
||||
value
|
||||
`,
|
||||
objectMetadataSingularName: 'viewFilter',
|
||||
filter: {
|
||||
id: { eq: createOneViewFilter.id },
|
||||
},
|
||||
});
|
||||
const updatedViewFilter = await findViewFilterWithRestApi(
|
||||
createdViewFilter.id,
|
||||
);
|
||||
|
||||
if (expected !== undefined) {
|
||||
expect(findResponse).toBe(expected);
|
||||
expect(errors).toMatchSnapshot();
|
||||
expect(updatedViewFilter).toBe(expected);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedViewFilterValues = parseJson<string[]>(findResponse.value);
|
||||
if (!isDefined(updatedViewFilter)) {
|
||||
throw new Error(
|
||||
'updatedViewFilter is not defined but should be at this point',
|
||||
);
|
||||
}
|
||||
|
||||
expect(parsedViewFilterValues).not.toBeNull();
|
||||
if (parsedViewFilterValues === null) {
|
||||
expect(updatedViewFilter.value).not.toBeNull();
|
||||
if (updatedViewFilter.value === null) {
|
||||
throw new Error('Invariant parsedValue should not be null');
|
||||
}
|
||||
expect(updatedOptions.map((option) => option.value)).toEqual(
|
||||
expect.arrayContaining(parsedViewFilterValues),
|
||||
expect.arrayContaining(updatedViewFilter.value as string[]),
|
||||
);
|
||||
|
||||
expect(findResponse).toMatchSnapshot({
|
||||
id: expect.any(String),
|
||||
});
|
||||
expect(updatedViewFilter.value).toMatchSnapshot();
|
||||
},
|
||||
);
|
||||
|
||||
@@ -335,7 +300,7 @@ describe('update-one-field-metadata-related-record', () => {
|
||||
test.each(failingTestCases)(
|
||||
'$title',
|
||||
async ({ context: { createViewFilterValue } }) => {
|
||||
const { createOneField, createOneView } =
|
||||
const { createOneField, createdView } =
|
||||
await createObjectSelectFieldAndView({
|
||||
options: ALL_OPTIONS,
|
||||
type: fieldType,
|
||||
@@ -343,23 +308,12 @@ describe('update-one-field-metadata-related-record', () => {
|
||||
|
||||
const viewFilterId = '20202020-e3b5-4fa7-85aa-9b1950fc7bf5';
|
||||
|
||||
await createOneOperation<{
|
||||
id: string;
|
||||
viewId: string;
|
||||
fieldMetadataId: string;
|
||||
operand: string;
|
||||
value: string;
|
||||
displayValue: string;
|
||||
}>({
|
||||
objectMetadataSingularName: 'viewFilter',
|
||||
input: {
|
||||
id: viewFilterId,
|
||||
viewId: createOneView.id,
|
||||
fieldMetadataId: createOneField.id,
|
||||
operand: 'is',
|
||||
value: createViewFilterValue as unknown as string,
|
||||
displayValue: '10 options',
|
||||
},
|
||||
await createTestViewFilterWithRestApi({
|
||||
id: viewFilterId,
|
||||
viewId: createdView.id,
|
||||
fieldMetadataId: createOneField.id,
|
||||
operand: ViewFilterOperand.IS,
|
||||
value: createViewFilterValue as unknown as ViewFilterValue,
|
||||
});
|
||||
|
||||
const optionsWithIds = createOneField.options;
|
||||
@@ -368,7 +322,7 @@ describe('update-one-field-metadata-related-record', () => {
|
||||
throw new Error('optionsWithIds is not defined');
|
||||
}
|
||||
const updatePayload = {
|
||||
options: optionsWithIds.map((option) => updateOption(option)),
|
||||
options: optionsWithIds.map((option) => fakeOptionUpdate(option)),
|
||||
};
|
||||
const { errors, data } = await updateOneFieldMetadata({
|
||||
input: {
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
|
||||
import {
|
||||
type UpdateOneFieldFactoryInput,
|
||||
updateOneFieldMetadataQueryFactory,
|
||||
} from 'test/integration/metadata/suites/field-metadata/utils/update-one-field-metadata-query-factory.util';
|
||||
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
|
||||
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
|
||||
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
|
||||
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
|
||||
@@ -22,7 +22,7 @@ export const updateOneFieldMetadata = async ({
|
||||
gqlFields,
|
||||
});
|
||||
|
||||
const response = await makeGraphqlAPIRequest(graphqlOperation);
|
||||
const response = await makeMetadataAPIRequest(graphqlOperation);
|
||||
|
||||
if (expectToFail === true) {
|
||||
warnIfNoErrorButExpectedToFail({
|
||||
|
||||
@@ -7,6 +7,7 @@ import { createOneObjectMetadata } from 'test/integration/metadata/suites/object
|
||||
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
|
||||
import { makeRestAPIRequest } from 'test/integration/rest/utils/make-rest-api-request.util';
|
||||
import {
|
||||
assertRestApiErrorNotFoundResponse,
|
||||
assertRestApiErrorResponse,
|
||||
assertRestApiSuccessfulResponse,
|
||||
} from 'test/integration/rest/utils/rest-test-assertions.util';
|
||||
@@ -219,8 +220,7 @@ describe('View Field REST API', () => {
|
||||
bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN,
|
||||
});
|
||||
|
||||
assertRestApiSuccessfulResponse(response);
|
||||
expect(response.body).toEqual({});
|
||||
assertRestApiErrorNotFoundResponse(response);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -308,8 +308,7 @@ describe('View Field REST API', () => {
|
||||
bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN,
|
||||
});
|
||||
|
||||
assertRestApiSuccessfulResponse(getResponse);
|
||||
expect(getResponse.body).toEqual({});
|
||||
assertRestApiErrorNotFoundResponse(getResponse);
|
||||
});
|
||||
|
||||
it('should return 404 error when deleting non-existent view field', async () => {
|
||||
@@ -319,14 +318,7 @@ describe('View Field REST API', () => {
|
||||
bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN,
|
||||
});
|
||||
|
||||
assertRestApiErrorResponse(
|
||||
response,
|
||||
404,
|
||||
generateViewFieldExceptionMessage(
|
||||
ViewFieldExceptionMessageKey.VIEW_FIELD_NOT_FOUND,
|
||||
TEST_NOT_EXISTING_VIEW_FIELD_ID,
|
||||
),
|
||||
);
|
||||
assertRestApiErrorNotFoundResponse(response);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+4
-12
@@ -6,6 +6,7 @@ import { createOneObjectMetadata } from 'test/integration/metadata/suites/object
|
||||
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
|
||||
import { makeRestAPIRequest } from 'test/integration/rest/utils/make-rest-api-request.util';
|
||||
import {
|
||||
assertRestApiErrorNotFoundResponse,
|
||||
assertRestApiErrorResponse,
|
||||
assertRestApiSuccessfulResponse,
|
||||
} from 'test/integration/rest/utils/rest-test-assertions.util';
|
||||
@@ -316,8 +317,7 @@ describe('View Filter Group REST API', () => {
|
||||
bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN,
|
||||
});
|
||||
|
||||
assertRestApiSuccessfulResponse(response);
|
||||
expect(response.body).toEqual({});
|
||||
assertRestApiErrorNotFoundResponse(response);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -464,8 +464,7 @@ describe('View Filter Group REST API', () => {
|
||||
bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN,
|
||||
});
|
||||
|
||||
assertRestApiSuccessfulResponse(response);
|
||||
expect(response.body).toEqual({});
|
||||
assertRestApiErrorNotFoundResponse(response);
|
||||
});
|
||||
|
||||
it('should return 404 error when deleting non-existent filter group', async () => {
|
||||
@@ -475,14 +474,7 @@ describe('View Filter Group REST API', () => {
|
||||
bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN,
|
||||
});
|
||||
|
||||
assertRestApiErrorResponse(
|
||||
response,
|
||||
404,
|
||||
generateViewFilterGroupExceptionMessage(
|
||||
ViewFilterGroupExceptionMessageKey.VIEW_FILTER_GROUP_NOT_FOUND,
|
||||
TEST_NOT_EXISTING_VIEW_FILTER_GROUP_ID,
|
||||
),
|
||||
);
|
||||
assertRestApiErrorNotFoundResponse(response);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+5
-25
@@ -7,7 +7,7 @@ import { createOneObjectMetadata } from 'test/integration/metadata/suites/object
|
||||
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
|
||||
import { makeRestAPIRequest } from 'test/integration/rest/utils/make-rest-api-request.util';
|
||||
import {
|
||||
assertRestApiErrorResponse,
|
||||
assertRestApiErrorNotFoundResponse,
|
||||
assertRestApiSuccessfulResponse,
|
||||
} from 'test/integration/rest/utils/rest-test-assertions.util';
|
||||
import {
|
||||
@@ -22,10 +22,6 @@ import {
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { ViewFilterOperand } from 'src/engine/core-modules/view/enums/view-filter-operand';
|
||||
import {
|
||||
generateViewFilterExceptionMessage,
|
||||
ViewFilterExceptionMessageKey,
|
||||
} from 'src/engine/core-modules/view/exceptions/view-filter.exception';
|
||||
|
||||
describe('View Filter REST API', () => {
|
||||
let testObjectMetadataId: string;
|
||||
@@ -232,8 +228,7 @@ describe('View Filter REST API', () => {
|
||||
bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN,
|
||||
});
|
||||
|
||||
assertRestApiSuccessfulResponse(response);
|
||||
expect(response.body).toEqual({});
|
||||
assertRestApiErrorNotFoundResponse(response);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -282,14 +277,7 @@ describe('View Filter REST API', () => {
|
||||
bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN,
|
||||
});
|
||||
|
||||
assertRestApiErrorResponse(
|
||||
response,
|
||||
404,
|
||||
generateViewFilterExceptionMessage(
|
||||
ViewFilterExceptionMessageKey.VIEW_FILTER_NOT_FOUND,
|
||||
TEST_NOT_EXISTING_VIEW_FILTER_ID,
|
||||
),
|
||||
);
|
||||
assertRestApiErrorNotFoundResponse(response);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -316,8 +304,7 @@ describe('View Filter REST API', () => {
|
||||
bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN,
|
||||
});
|
||||
|
||||
assertRestApiSuccessfulResponse(getResponse);
|
||||
expect(getResponse.body).toEqual({});
|
||||
assertRestApiErrorNotFoundResponse(getResponse);
|
||||
});
|
||||
|
||||
it('should return 404 error when deleting non-existent view filter', async () => {
|
||||
@@ -327,14 +314,7 @@ describe('View Filter REST API', () => {
|
||||
bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN,
|
||||
});
|
||||
|
||||
assertRestApiErrorResponse(
|
||||
response,
|
||||
404,
|
||||
generateViewFilterExceptionMessage(
|
||||
ViewFilterExceptionMessageKey.VIEW_FILTER_NOT_FOUND,
|
||||
TEST_NOT_EXISTING_VIEW_FILTER_ID,
|
||||
),
|
||||
);
|
||||
assertRestApiErrorNotFoundResponse(response);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import { createOneObjectMetadata } from 'test/integration/metadata/suites/object
|
||||
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
|
||||
import { makeRestAPIRequest } from 'test/integration/rest/utils/make-rest-api-request.util';
|
||||
import {
|
||||
assertRestApiErrorNotFoundResponse,
|
||||
assertRestApiErrorResponse,
|
||||
assertRestApiSuccessfulResponse,
|
||||
} from 'test/integration/rest/utils/rest-test-assertions.util';
|
||||
@@ -373,14 +374,7 @@ describe('View Group REST API', () => {
|
||||
bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN,
|
||||
});
|
||||
|
||||
assertRestApiErrorResponse(
|
||||
response,
|
||||
404,
|
||||
generateViewGroupExceptionMessage(
|
||||
ViewGroupExceptionMessageKey.VIEW_GROUP_NOT_FOUND,
|
||||
TEST_NOT_EXISTING_VIEW_GROUP_ID,
|
||||
),
|
||||
);
|
||||
assertRestApiErrorNotFoundResponse(response);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -408,14 +402,7 @@ describe('View Group REST API', () => {
|
||||
bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN,
|
||||
});
|
||||
|
||||
assertRestApiErrorResponse(
|
||||
response,
|
||||
404,
|
||||
generateViewGroupExceptionMessage(
|
||||
ViewGroupExceptionMessageKey.VIEW_GROUP_NOT_FOUND,
|
||||
TEST_NOT_EXISTING_VIEW_GROUP_ID,
|
||||
),
|
||||
);
|
||||
assertRestApiErrorNotFoundResponse(response);
|
||||
});
|
||||
|
||||
it('should return success even when group is already deleted', async () => {
|
||||
@@ -438,14 +425,7 @@ describe('View Group REST API', () => {
|
||||
bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN,
|
||||
});
|
||||
|
||||
assertRestApiErrorResponse(
|
||||
deleteResponse2,
|
||||
404,
|
||||
generateViewGroupExceptionMessage(
|
||||
ViewGroupExceptionMessageKey.VIEW_GROUP_NOT_FOUND,
|
||||
TEST_NOT_EXISTING_VIEW_GROUP_ID,
|
||||
),
|
||||
);
|
||||
assertRestApiErrorNotFoundResponse(deleteResponse2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import { createOneObjectMetadata } from 'test/integration/metadata/suites/object
|
||||
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
|
||||
import { makeRestAPIRequest } from 'test/integration/rest/utils/make-rest-api-request.util';
|
||||
import {
|
||||
assertRestApiErrorNotFoundResponse,
|
||||
assertRestApiErrorResponse,
|
||||
assertRestApiSuccessfulResponse,
|
||||
} from 'test/integration/rest/utils/rest-test-assertions.util';
|
||||
@@ -199,8 +200,7 @@ describe('View Sort REST API', () => {
|
||||
bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN,
|
||||
});
|
||||
|
||||
assertRestApiSuccessfulResponse(response);
|
||||
expect(response.body).toEqual({});
|
||||
assertRestApiErrorNotFoundResponse(response);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -243,14 +243,7 @@ describe('View Sort REST API', () => {
|
||||
bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN,
|
||||
});
|
||||
|
||||
assertRestApiErrorResponse(
|
||||
response,
|
||||
404,
|
||||
generateViewSortExceptionMessage(
|
||||
ViewSortExceptionMessageKey.VIEW_SORT_NOT_FOUND,
|
||||
TEST_NOT_EXISTING_VIEW_SORT_ID,
|
||||
),
|
||||
);
|
||||
assertRestApiErrorNotFoundResponse(response);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -276,8 +269,7 @@ describe('View Sort REST API', () => {
|
||||
bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN,
|
||||
});
|
||||
|
||||
assertRestApiSuccessfulResponse(getResponse);
|
||||
expect(getResponse.body).toEqual({});
|
||||
assertRestApiErrorNotFoundResponse(getResponse);
|
||||
});
|
||||
|
||||
it('should return 404 error when deleting non-existent view sort', async () => {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createOneObjectMetadata } from 'test/integration/metadata/suites/object
|
||||
import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util';
|
||||
import { makeRestAPIRequest } from 'test/integration/rest/utils/make-rest-api-request.util';
|
||||
import {
|
||||
assertRestApiErrorResponse,
|
||||
assertRestApiErrorNotFoundResponse,
|
||||
assertRestApiSuccessfulResponse,
|
||||
} from 'test/integration/rest/utils/rest-test-assertions.util';
|
||||
import {
|
||||
@@ -19,10 +19,6 @@ import {
|
||||
import { ViewKey } from 'src/engine/core-modules/view/enums/view-key.enum';
|
||||
import { ViewOpenRecordIn } from 'src/engine/core-modules/view/enums/view-open-record-in';
|
||||
import { ViewType } from 'src/engine/core-modules/view/enums/view-type.enum';
|
||||
import {
|
||||
ViewExceptionMessageKey,
|
||||
generateViewExceptionMessage,
|
||||
} from 'src/engine/core-modules/view/exceptions/view.exception';
|
||||
|
||||
describe('View REST API', () => {
|
||||
let testObjectMetadataId: string;
|
||||
@@ -173,8 +169,7 @@ describe('View REST API', () => {
|
||||
bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN,
|
||||
});
|
||||
|
||||
assertRestApiSuccessfulResponse(response);
|
||||
expect(response.body).toEqual({});
|
||||
assertRestApiErrorNotFoundResponse(response);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -231,14 +226,7 @@ describe('View REST API', () => {
|
||||
bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN,
|
||||
});
|
||||
|
||||
assertRestApiErrorResponse(
|
||||
response,
|
||||
404,
|
||||
generateViewExceptionMessage(
|
||||
ViewExceptionMessageKey.VIEW_NOT_FOUND,
|
||||
TEST_NOT_EXISTING_VIEW_ID,
|
||||
),
|
||||
);
|
||||
assertRestApiErrorNotFoundResponse(response);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -271,8 +259,7 @@ describe('View REST API', () => {
|
||||
bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN,
|
||||
});
|
||||
|
||||
assertRestApiSuccessfulResponse(getResponse);
|
||||
expect(getResponse.body).toEqual({});
|
||||
assertRestApiErrorNotFoundResponse(getResponse);
|
||||
});
|
||||
|
||||
it('should return 404 error when deleting non-existent view', async () => {
|
||||
@@ -282,14 +269,7 @@ describe('View REST API', () => {
|
||||
bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN,
|
||||
});
|
||||
|
||||
assertRestApiErrorResponse(
|
||||
response,
|
||||
404,
|
||||
generateViewExceptionMessage(
|
||||
ViewExceptionMessageKey.VIEW_NOT_FOUND,
|
||||
TEST_NOT_EXISTING_VIEW_ID,
|
||||
),
|
||||
);
|
||||
assertRestApiErrorNotFoundResponse(response);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,3 +32,16 @@ export const assertRestApiErrorResponse = <T = Record<string, unknown>>(
|
||||
expect(response.body.message).toContain(expectedErrorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
export const assertRestApiErrorNotFoundResponse = (
|
||||
response: RestResponse<{ statusCode: number; messages: [] }>,
|
||||
expectedStatus = 404,
|
||||
expectedErrorMessage?: string,
|
||||
) => {
|
||||
expect(response.status).toBe(expectedStatus);
|
||||
expect(response.body.statusCode).toBe(expectedStatus);
|
||||
|
||||
if (expectedErrorMessage && response.body.message) {
|
||||
expect(response.body.message).toContain(expectedErrorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -15,6 +15,38 @@ import { type ViewEntity } from 'src/engine/core-modules/view/entities/view.enti
|
||||
import { ViewOpenRecordIn } from 'src/engine/core-modules/view/enums/view-open-record-in';
|
||||
import { ViewType } from 'src/engine/core-modules/view/enums/view-type.enum';
|
||||
|
||||
export const findViewByIdWithRestApi = async (
|
||||
viewId: string,
|
||||
): Promise<ViewEntity | null> => {
|
||||
const response = await makeRestAPIRequest({
|
||||
method: 'get',
|
||||
path: `/metadata/views/${viewId}`,
|
||||
bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN,
|
||||
});
|
||||
|
||||
if (response.status === 404) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return response.body;
|
||||
};
|
||||
|
||||
export const findViewFilterWithRestApi = async (
|
||||
viewFilterId: string,
|
||||
): Promise<ViewFilterEntity | null> => {
|
||||
const response = await makeRestAPIRequest({
|
||||
method: 'get',
|
||||
path: `/metadata/viewFilters/${viewFilterId}`,
|
||||
bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN,
|
||||
});
|
||||
|
||||
if (response.status === 404) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return response.body;
|
||||
};
|
||||
|
||||
export const createTestViewWithRestApi = async (
|
||||
overrides: Partial<ViewEntity> = {},
|
||||
): Promise<ViewEntity> => {
|
||||
|
||||
Reference in New Issue
Block a user