Core views frontend (#13932)

Parallel code path to read and write core views when
IS_CORE_VIEW_ENABLED.

Migrated view key to an enum.

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
Raphaël Bosi
2025-08-20 19:04:58 +02:00
committed by GitHub
parent deec9c96da
commit 30c42345f0
114 changed files with 5019 additions and 254 deletions
@@ -7,6 +7,7 @@ module.exports = {
documents: [
'./src/modules/auth/graphql/**/*.{ts,tsx}',
'./src/modules/users/graphql/**/*.{ts,tsx}',
'./src/modules/views/graphql/**/*.{ts,tsx}',
'./src/modules/ai/graphql/**/*.{ts,tsx}',
'./src/modules/workspace/graphql/**/*.{ts,tsx}',
+2 -2
View File
@@ -61,9 +61,9 @@ const jestConfig = {
extensionsToTreatAsEsm: ['.ts', '.tsx'],
coverageThreshold: {
global: {
statements: 54,
statements: 53,
lines: 53,
functions: 43,
functions: 42,
},
},
collectCoverageFrom: ['<rootDir>/src/**/*.ts'],
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
@@ -14,7 +14,6 @@ import { clientConfigApiStatusState } from '@/client-config/states/clientConfigA
import { supportChatState } from '@/client-config/states/supportChatState';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
import {
type AuthTokenPair,
useCheckUserExistsLazyQuery,
useGetAuthTokensFromLoginTokenMutation,
useGetAuthTokensFromOtpMutation,
@@ -24,6 +23,7 @@ import {
useSignInMutation,
useSignUpInWorkspaceMutation,
useSignUpMutation,
type AuthTokenPair,
} from '~/generated-metadata/graphql';
import { isDeveloperDefaultSignInPrefilledState } from '@/client-config/states/isDeveloperDefaultSignInPrefilledState';
@@ -14,6 +14,8 @@ import { SettingsPath } from '@/types/SettingsPath';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState';
import { type View } from '@/views/types/View';
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useMemo } from 'react';
@@ -25,7 +27,7 @@ import {
import { IconMinus, IconPlus, useIcons } from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input';
import { UndecoratedLink } from 'twenty-ui/navigation';
import { RelationType } from '~/generated-metadata/graphql';
import { FeatureFlagKey, RelationType } from '~/generated-metadata/graphql';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
import { type SettingsObjectDetailTableItem } from '~/pages/settings/data-model/types/SettingsObjectDetailTableItem';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
@@ -122,6 +124,9 @@ export const SettingsObjectFieldItemTableRow = ({
objectNameSingular: CoreObjectNameSingular.View,
});
const featureFlagMap = useFeatureFlagsMap();
const isCoreViewEnabled = featureFlagMap[FeatureFlagKey.IS_CORE_VIEW_ENABLED];
const handleDisableField = async (
activeFieldMetadatItem: FieldMetadataItem,
) => {
@@ -130,17 +135,20 @@ export const SettingsObjectFieldItemTableRow = ({
objectMetadataItem.id,
);
const deletedViewIds = prefetchViews
.map((view) => {
// TODO: replace with viewGroups.fieldMetadataId
if (view.kanbanFieldMetadataId === activeFieldMetadatItem.id) {
deleteViewFromCache(view);
return view.id;
}
// TODO: Add optimistic rendering for core views
const deletedViewIds = isCoreViewEnabled
? []
: (prefetchViews as View[])
.map((view) => {
// TODO: replace with viewGroups.fieldMetadataId
if (view.kanbanFieldMetadataId === activeFieldMetadatItem.id) {
deleteViewFromCache(view);
return view.id;
}
return null;
})
.filter(isDefined);
return null;
})
.filter(isDefined);
const [baseUrl, queryParams] = navigationMemorizedUrl.includes('?')
? navigationMemorizedUrl.split('?')
@@ -5,6 +5,12 @@ import {
import { OBJECT_PERMISSION_FRAGMENT } from '@/settings/roles/graphql/fragments/objectPermissionFragment';
import { ROLE_FRAGMENT } from '@/settings/roles/graphql/fragments/roleFragment';
import { WORKSPACE_URLS_FRAGMENT } from '@/users/graphql/fragments/workspaceUrlsFragment';
import { VIEW_FIELD_FRAGMENT } from '@/views/graphql/fragments/viewFieldFragment';
import { VIEW_FILTER_FRAGMENT } from '@/views/graphql/fragments/viewFilterFragment';
import { VIEW_FILTER_GROUP_FRAGMENT } from '@/views/graphql/fragments/viewFilterGroupFragment';
import { VIEW_FRAGMENT } from '@/views/graphql/fragments/viewFragment';
import { VIEW_GROUP_FRAGMENT } from '@/views/graphql/fragments/viewGroupFragment';
import { VIEW_SORT_FRAGMENT } from '@/views/graphql/fragments/viewSortFragment';
import { DELETED_WORKSPACE_MEMBER_QUERY_FRAGMENT } from '@/workspace-member/graphql/fragments/deletedWorkspaceMemberQueryFragment';
import { WORKSPACE_MEMBER_QUERY_FRAGMENT } from '@/workspace-member/graphql/fragments/workspaceMemberQueryFragment';
import { gql } from '@apollo/client';
@@ -96,6 +102,24 @@ export const USER_QUERY_FRAGMENT = gql`
id
}
isTwoFactorAuthenticationEnforced
views {
...ViewFragment
viewFields {
...ViewFieldFragment
}
viewFilters {
...ViewFilterFragment
}
viewFilterGroups {
...ViewFilterGroupFragment
}
viewSorts {
...ViewSortFragment
}
viewGroups {
...ViewGroupFragment
}
}
}
availableWorkspaces {
...AvailableWorkspacesFragment
@@ -108,6 +132,12 @@ export const USER_QUERY_FRAGMENT = gql`
${OBJECT_PERMISSION_FRAGMENT}
${WORKSPACE_URLS_FRAGMENT}
${ROLE_FRAGMENT}
${VIEW_FRAGMENT}
${VIEW_FIELD_FRAGMENT}
${VIEW_FILTER_FRAGMENT}
${VIEW_FILTER_GROUP_FRAGMENT}
${VIEW_SORT_FRAGMENT}
${VIEW_GROUP_FRAGMENT}
${AVAILABLE_WORKSPACES_FOR_AUTH_FRAGMENT}
${AVAILABLE_WORKSPACE_FOR_AUTH_FRAGMENT}
`;
@@ -16,7 +16,7 @@ import { getDateFormatFromWorkspaceDateFormat } from '@/localization/utils/getDa
import { getTimeFormatFromWorkspaceTimeFormat } from '@/localization/utils/getTimeFormatFromWorkspaceTimeFormat';
import { useCallback } from 'react';
import { useSetRecoilState } from 'recoil';
import { type APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations';
import { SOURCE_LOCALE, type APP_LOCALES } from 'twenty-shared/translations';
import { type ObjectPermissions } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type ColorScheme } from 'twenty-ui/input';
@@ -136,6 +136,7 @@ export const useLoadCurrentUser = () => {
}, [
getCurrentUser,
isOnAWorkspace,
setAvailableWorkspaces,
setCurrentUser,
setCurrentUserWorkspace,
setCurrentWorkspace,
@@ -143,7 +144,6 @@ export const useLoadCurrentUser = () => {
setCurrentWorkspaceMembers,
setDateTimeFormat,
setLastAuthenticateWorkspaceDomain,
setAvailableWorkspaces,
]);
return {
@@ -0,0 +1,15 @@
import { gql } from '@apollo/client';
export const VIEW_FIELD_FRAGMENT = gql`
fragment ViewFieldFragment on CoreViewField {
id
fieldMetadataId
viewId
isVisible
position
size
aggregateOperation
createdAt
updatedAt
}
`;
@@ -0,0 +1,16 @@
import { gql } from '@apollo/client';
export const VIEW_FILTER_FRAGMENT = gql`
fragment ViewFilterFragment on CoreViewFilter {
id
fieldMetadataId
operand
value
viewFilterGroupId
positionInViewFilterGroup
subFieldName
viewId
createdAt
updatedAt
}
`;
@@ -0,0 +1,13 @@
import { gql } from '@apollo/client';
export const VIEW_FILTER_GROUP_FRAGMENT = gql`
fragment ViewFilterGroupFragment on CoreViewFilterGroup {
id
parentViewFilterGroupId
logicalOperator
positionInViewFilterGroup
viewId
createdAt
updatedAt
}
`;
@@ -0,0 +1,20 @@
import { gql } from '@apollo/client';
export const VIEW_FRAGMENT = gql`
fragment ViewFragment on CoreView {
id
name
objectMetadataId
type
key
icon
position
isCompact
openRecordIn
kanbanAggregateOperation
kanbanAggregateOperationFieldMetadataId
createdAt
updatedAt
anyFieldFilterValue
}
`;
@@ -0,0 +1,14 @@
import { gql } from '@apollo/client';
export const VIEW_GROUP_FRAGMENT = gql`
fragment ViewGroupFragment on CoreViewGroup {
id
fieldMetadataId
isVisible
fieldValue
position
viewId
createdAt
updatedAt
}
`;
@@ -0,0 +1,12 @@
import { gql } from '@apollo/client';
export const VIEW_SORT_FRAGMENT = gql`
fragment ViewSortFragment on CoreViewSort {
id
fieldMetadataId
direction
viewId
createdAt
updatedAt
}
`;
@@ -0,0 +1,11 @@
import { VIEW_FRAGMENT } from '@/views/graphql/fragments/viewFragment';
import { gql } from '@apollo/client';
export const CREATE_CORE_VIEW = gql`
${VIEW_FRAGMENT}
mutation CreateCoreView($input: CreateViewInput!) {
createCoreView(input: $input) {
...ViewFragment
}
}
`;
@@ -0,0 +1,11 @@
import { VIEW_FIELD_FRAGMENT } from '@/views/graphql/fragments/viewFieldFragment';
import { gql } from '@apollo/client';
export const CREATE_CORE_VIEW_FIELD = gql`
${VIEW_FIELD_FRAGMENT}
mutation CreateCoreViewField($input: CreateViewFieldInput!) {
createCoreViewField(input: $input) {
...ViewFieldFragment
}
}
`;
@@ -0,0 +1,11 @@
import { VIEW_FILTER_FRAGMENT } from '@/views/graphql/fragments/viewFilterFragment';
import { gql } from '@apollo/client';
export const CREATE_CORE_VIEW_FILTER = gql`
${VIEW_FILTER_FRAGMENT}
mutation CreateCoreViewFilter($input: CreateViewFilterInput!) {
createCoreViewFilter(input: $input) {
...ViewFilterFragment
}
}
`;
@@ -0,0 +1,11 @@
import { VIEW_FILTER_GROUP_FRAGMENT } from '@/views/graphql/fragments/viewFilterGroupFragment';
import { gql } from '@apollo/client';
export const CREATE_CORE_VIEW_FILTER_GROUP = gql`
${VIEW_FILTER_GROUP_FRAGMENT}
mutation CreateCoreViewFilterGroup($input: CreateViewFilterGroupInput!) {
createCoreViewFilterGroup(input: $input) {
...ViewFilterGroupFragment
}
}
`;
@@ -0,0 +1,11 @@
import { VIEW_GROUP_FRAGMENT } from '@/views/graphql/fragments/viewGroupFragment';
import { gql } from '@apollo/client';
export const CREATE_CORE_VIEW_GROUP = gql`
${VIEW_GROUP_FRAGMENT}
mutation CreateCoreViewGroup($input: CreateViewGroupInput!) {
createCoreViewGroup(input: $input) {
...ViewGroupFragment
}
}
`;
@@ -0,0 +1,11 @@
import { VIEW_SORT_FRAGMENT } from '@/views/graphql/fragments/viewSortFragment';
import { gql } from '@apollo/client';
export const CREATE_CORE_VIEW_SORT = gql`
${VIEW_SORT_FRAGMENT}
mutation CreateCoreViewSort($input: CreateViewSortInput!) {
createCoreViewSort(input: $input) {
...ViewSortFragment
}
}
`;
@@ -0,0 +1,7 @@
import { gql } from '@apollo/client';
export const DELETE_CORE_VIEW = gql`
mutation DeleteCoreView($id: String!) {
deleteCoreView(id: $id)
}
`;
@@ -0,0 +1,7 @@
import { gql } from '@apollo/client';
export const DELETE_CORE_VIEW_FIELD = gql`
mutation DeleteCoreViewField($id: String!) {
deleteCoreViewField(id: $id)
}
`;
@@ -0,0 +1,7 @@
import { gql } from '@apollo/client';
export const DELETE_CORE_VIEW_FILTER = gql`
mutation DeleteCoreViewFilter($id: String!) {
deleteCoreViewFilter(id: $id)
}
`;
@@ -0,0 +1,7 @@
import { gql } from '@apollo/client';
export const DELETE_CORE_VIEW_FILTER_GROUP = gql`
mutation DeleteCoreViewFilterGroup($id: String!) {
deleteCoreViewFilterGroup(id: $id)
}
`;
@@ -0,0 +1,7 @@
import { gql } from '@apollo/client';
export const DELETE_CORE_VIEW_GROUP = gql`
mutation DeleteCoreViewGroup($id: String!) {
deleteCoreViewGroup(id: $id)
}
`;
@@ -0,0 +1,7 @@
import { gql } from '@apollo/client';
export const DELETE_CORE_VIEW_SORT = gql`
mutation DeleteCoreViewSort($id: String!) {
deleteCoreViewSort(id: $id)
}
`;
@@ -0,0 +1,7 @@
import { gql } from '@apollo/client';
export const DESTROY_CORE_VIEW = gql`
mutation DestroyCoreView($id: String!) {
destroyCoreView(id: $id)
}
`;
@@ -0,0 +1,7 @@
import { gql } from '@apollo/client';
export const DESTROY_CORE_VIEW_FIELD = gql`
mutation DestroyCoreViewField($id: String!) {
destroyCoreViewField(id: $id)
}
`;
@@ -0,0 +1,7 @@
import { gql } from '@apollo/client';
export const DESTROY_CORE_VIEW_FILTER = gql`
mutation DestroyCoreViewFilter($id: String!) {
destroyCoreViewFilter(id: $id)
}
`;
@@ -0,0 +1,7 @@
import { gql } from '@apollo/client';
export const DESTROY_CORE_VIEW_FILTER_GROUP = gql`
mutation DestroyCoreViewFilterGroup($id: String!) {
destroyCoreViewFilterGroup(id: $id)
}
`;
@@ -0,0 +1,7 @@
import { gql } from '@apollo/client';
export const DESTROY_CORE_VIEW_GROUP = gql`
mutation DestroyCoreViewGroup($id: String!) {
destroyCoreViewGroup(id: $id)
}
`;
@@ -0,0 +1,7 @@
import { gql } from '@apollo/client';
export const DESTROY_CORE_VIEW_SORT = gql`
mutation DestroyCoreViewSort($id: String!) {
destroyCoreViewSort(id: $id)
}
`;
@@ -0,0 +1,11 @@
import { VIEW_FRAGMENT } from '@/views/graphql/fragments/viewFragment';
import { gql } from '@apollo/client';
export const UPDATE_CORE_VIEW = gql`
${VIEW_FRAGMENT}
mutation UpdateCoreView($id: String!, $input: UpdateViewInput!) {
updateCoreView(id: $id, input: $input) {
...ViewFragment
}
}
`;
@@ -0,0 +1,11 @@
import { VIEW_FIELD_FRAGMENT } from '@/views/graphql/fragments/viewFieldFragment';
import { gql } from '@apollo/client';
export const UPDATE_CORE_VIEW_FIELD = gql`
${VIEW_FIELD_FRAGMENT}
mutation UpdateCoreViewField($id: String!, $input: UpdateViewFieldInput!) {
updateCoreViewField(id: $id, input: $input) {
...ViewFieldFragment
}
}
`;
@@ -0,0 +1,11 @@
import { VIEW_FILTER_FRAGMENT } from '@/views/graphql/fragments/viewFilterFragment';
import { gql } from '@apollo/client';
export const UPDATE_CORE_VIEW_FILTER = gql`
${VIEW_FILTER_FRAGMENT}
mutation UpdateCoreViewFilter($id: String!, $input: UpdateViewFilterInput!) {
updateCoreViewFilter(id: $id, input: $input) {
...ViewFilterFragment
}
}
`;
@@ -0,0 +1,14 @@
import { VIEW_FILTER_GROUP_FRAGMENT } from '@/views/graphql/fragments/viewFilterGroupFragment';
import { gql } from '@apollo/client';
export const UPDATE_CORE_VIEW_FILTER_GROUP = gql`
${VIEW_FILTER_GROUP_FRAGMENT}
mutation UpdateCoreViewFilterGroup(
$id: String!
$input: UpdateViewFilterGroupInput!
) {
updateCoreViewFilterGroup(id: $id, input: $input) {
...ViewFilterGroupFragment
}
}
`;
@@ -0,0 +1,11 @@
import { VIEW_GROUP_FRAGMENT } from '@/views/graphql/fragments/viewGroupFragment';
import { gql } from '@apollo/client';
export const UPDATE_CORE_VIEW_GROUP = gql`
${VIEW_GROUP_FRAGMENT}
mutation UpdateCoreViewGroup($id: String!, $input: UpdateViewGroupInput!) {
updateCoreViewGroup(id: $id, input: $input) {
...ViewGroupFragment
}
}
`;
@@ -0,0 +1,11 @@
import { VIEW_SORT_FRAGMENT } from '@/views/graphql/fragments/viewSortFragment';
import { gql } from '@apollo/client';
export const UPDATE_CORE_VIEW_SORT = gql`
${VIEW_SORT_FRAGMENT}
mutation UpdateCoreViewSort($id: String!, $input: UpdateViewSortInput!) {
updateCoreViewSort(id: $id, input: $input) {
...ViewSortFragment
}
}
`;
@@ -0,0 +1,11 @@
import { VIEW_FIELD_FRAGMENT } from '@/views/graphql/fragments/viewFieldFragment';
import { gql } from '@apollo/client';
export const FIND_MANY_CORE_VIEW_FIELDS = gql`
${VIEW_FIELD_FRAGMENT}
query FindManyCoreViewFields($viewId: String!) {
getCoreViewFields(viewId: $viewId) {
...ViewFieldFragment
}
}
`;
@@ -0,0 +1,11 @@
import { VIEW_FILTER_GROUP_FRAGMENT } from '@/views/graphql/fragments/viewFilterGroupFragment';
import { gql } from '@apollo/client';
export const FIND_MANY_CORE_VIEW_FILTER_GROUPS = gql`
${VIEW_FILTER_GROUP_FRAGMENT}
query FindManyCoreViewFilterGroups($viewId: String) {
getCoreViewFilterGroups(viewId: $viewId) {
...ViewFilterGroupFragment
}
}
`;
@@ -0,0 +1,11 @@
import { VIEW_FILTER_FRAGMENT } from '@/views/graphql/fragments/viewFilterFragment';
import { gql } from '@apollo/client';
export const FIND_MANY_CORE_VIEW_FILTERS = gql`
${VIEW_FILTER_FRAGMENT}
query FindManyCoreViewFilters($viewId: String) {
getCoreViewFilters(viewId: $viewId) {
...ViewFilterFragment
}
}
`;
@@ -0,0 +1,11 @@
import { VIEW_GROUP_FRAGMENT } from '@/views/graphql/fragments/viewGroupFragment';
import { gql } from '@apollo/client';
export const FIND_MANY_CORE_VIEW_GROUPS = gql`
${VIEW_GROUP_FRAGMENT}
query FindManyCoreViewGroups($viewId: String) {
getCoreViewGroups(viewId: $viewId) {
...ViewGroupFragment
}
}
`;
@@ -0,0 +1,11 @@
import { VIEW_SORT_FRAGMENT } from '@/views/graphql/fragments/viewSortFragment';
import { gql } from '@apollo/client';
export const FIND_MANY_CORE_VIEW_SORTS = gql`
${VIEW_SORT_FRAGMENT}
query FindManyCoreViewSorts($viewId: String) {
getCoreViewSorts(viewId: $viewId) {
...ViewSortFragment
}
}
`;
@@ -0,0 +1,11 @@
import { VIEW_FRAGMENT } from '@/views/graphql/fragments/viewFragment';
import { gql } from '@apollo/client';
export const FIND_MANY_CORE_VIEWS = gql`
${VIEW_FRAGMENT}
query FindManyCoreViews($objectMetadataId: String) {
getCoreViews(objectMetadataId: $objectMetadataId) {
...ViewFragment
}
}
`;
@@ -0,0 +1,11 @@
import { VIEW_FRAGMENT } from '@/views/graphql/fragments/viewFragment';
import { gql } from '@apollo/client';
export const FIND_ONE_CORE_VIEW = gql`
${VIEW_FRAGMENT}
query FindOneCoreView($id: String!) {
getCoreView(id: $id) {
...ViewFragment
}
}
`;
@@ -0,0 +1,11 @@
import { VIEW_FIELD_FRAGMENT } from '@/views/graphql/fragments/viewFieldFragment';
import { gql } from '@apollo/client';
export const FIND_ONE_CORE_VIEW_FIELD = gql`
${VIEW_FIELD_FRAGMENT}
query FindOneCoreViewField($id: String!) {
getCoreViewField(id: $id) {
...ViewFieldFragment
}
}
`;
@@ -0,0 +1,11 @@
import { VIEW_FILTER_FRAGMENT } from '@/views/graphql/fragments/viewFilterFragment';
import { gql } from '@apollo/client';
export const FIND_ONE_CORE_VIEW_FILTER = gql`
${VIEW_FILTER_FRAGMENT}
query FindOneCoreViewFilter($id: String!) {
getCoreViewFilter(id: $id) {
...ViewFilterFragment
}
}
`;
@@ -0,0 +1,11 @@
import { VIEW_FILTER_GROUP_FRAGMENT } from '@/views/graphql/fragments/viewFilterGroupFragment';
import { gql } from '@apollo/client';
export const FIND_ONE_CORE_VIEW_FILTER_GROUP = gql`
${VIEW_FILTER_GROUP_FRAGMENT}
query FindOneCoreViewFilterGroup($id: String!) {
getCoreViewFilterGroup(id: $id) {
...ViewFilterGroupFragment
}
}
`;
@@ -0,0 +1,11 @@
import { VIEW_GROUP_FRAGMENT } from '@/views/graphql/fragments/viewGroupFragment';
import { gql } from '@apollo/client';
export const FIND_ONE_CORE_VIEW_GROUP = gql`
${VIEW_GROUP_FRAGMENT}
query FindOneCoreViewGroup($id: String!) {
getCoreViewGroup(id: $id) {
...ViewGroupFragment
}
}
`;
@@ -0,0 +1,11 @@
import { VIEW_SORT_FRAGMENT } from '@/views/graphql/fragments/viewSortFragment';
import { gql } from '@apollo/client';
export const FIND_ONE_CORE_VIEW_SORT = gql`
${VIEW_SORT_FRAGMENT}
query FindOneCoreViewSort($id: String!) {
getCoreViewSort(id: $id) {
...ViewSortFragment
}
}
`;
@@ -11,12 +11,20 @@ import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordF
import { useCreateOneRecordMutation } from '@/object-record/hooks/useCreateOneRecordMutation';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useUpdateOneRecordMutation } from '@/object-record/hooks/useUpdateOneRecordMutation';
import { CREATE_CORE_VIEW_FIELD } from '@/views/graphql/mutations/createCoreViewField';
import { UPDATE_CORE_VIEW_FIELD } from '@/views/graphql/mutations/updateCoreViewField';
import { type GraphQLView } from '@/views/types/GraphQLView';
import { type ViewField } from '@/views/types/ViewField';
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
import { useApolloClient } from '@apollo/client';
import { isNull } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
import { FeatureFlagKey, type CoreViewField } from '~/generated/graphql';
export const usePersistViewFieldRecords = () => {
const featureFlags = useFeatureFlagsMap();
const isCoreViewEnabled = featureFlags[FeatureFlagKey.IS_CORE_VIEW_ENABLED];
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular: CoreObjectNameSingular.ViewField,
});
@@ -36,11 +44,12 @@ export const usePersistViewFieldRecords = () => {
const { objectMetadataItems } = useObjectMetadataItems();
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const apolloCoreClient = useApolloCoreClient();
const apolloClient = useApolloClient();
const createViewFieldRecords = useCallback(
(
viewFieldsToCreate: Omit<ViewField, 'definition'>[],
view: GraphQLView,
view: Pick<GraphQLView, 'id'>,
) => {
if (!viewFieldsToCreate.length) return;
return Promise.all(
@@ -130,8 +139,97 @@ export const usePersistViewFieldRecords = () => {
],
);
const createCoreViewFieldRecords = useCallback(
(
viewFieldsToCreate: Omit<ViewField, 'definition'>[],
view: Pick<GraphQLView, 'id'>,
) => {
if (!viewFieldsToCreate.length) return;
return Promise.all(
viewFieldsToCreate.map((viewField) =>
apolloClient.mutate({
mutation: CREATE_CORE_VIEW_FIELD,
variables: {
input: {
fieldMetadataId: viewField.fieldMetadataId,
viewId: view.id,
isVisible: viewField.isVisible,
position: viewField.position,
size: viewField.size,
} satisfies Partial<CoreViewField>,
},
update: (cache, { data }) => {
const record = data?.['createCoreViewField'];
if (!record) return;
triggerCreateRecordsOptimisticEffect({
cache,
objectMetadataItem,
recordsToCreate: [record],
objectMetadataItems,
objectPermissionsByObjectMetadataId,
});
},
}),
),
);
},
[
apolloClient,
objectMetadataItem,
objectMetadataItems,
objectPermissionsByObjectMetadataId,
],
);
const updateCoreViewFieldRecords = useCallback(
(viewFieldsToUpdate: Omit<ViewField, 'definition'>[]) => {
if (!viewFieldsToUpdate.length) return;
return Promise.all(
viewFieldsToUpdate.map((viewField) =>
apolloClient.mutate({
mutation: UPDATE_CORE_VIEW_FIELD,
variables: {
idToUpdate: viewField.id,
input: {
isVisible: viewField.isVisible,
position: viewField.position,
size: viewField.size,
aggregateOperation: viewField.aggregateOperation,
} satisfies Partial<CoreViewField>,
},
update: (cache, { data }) => {
const record = data?.['updateCoreViewField'];
if (!isDefined(record)) return;
const cachedRecord = getRecordFromCache<ViewField>(
record.id,
cache,
);
if (isNull(cachedRecord)) return;
triggerUpdateRecordOptimisticEffect({
cache,
objectMetadataItem,
currentRecord: cachedRecord,
updatedRecord: record,
objectMetadataItems,
});
},
}),
),
);
},
[apolloClient, getRecordFromCache, objectMetadataItem, objectMetadataItems],
);
return {
createViewFieldRecords,
updateViewFieldRecords,
createViewFieldRecords: isCoreViewEnabled
? createCoreViewFieldRecords
: createViewFieldRecords,
updateViewFieldRecords: isCoreViewEnabled
? updateCoreViewFieldRecords
: updateViewFieldRecords,
};
};
@@ -12,11 +12,21 @@ import { useCreateOneRecordMutation } from '@/object-record/hooks/useCreateOneRe
import { useDestroyOneRecordMutation } from '@/object-record/hooks/useDestroyOneRecordMutation';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useUpdateOneRecordMutation } from '@/object-record/hooks/useUpdateOneRecordMutation';
import { CREATE_CORE_VIEW_FILTER_GROUP } from '@/views/graphql/mutations/createCoreViewFilterGroup';
import { DESTROY_CORE_VIEW_FILTER_GROUP } from '@/views/graphql/mutations/destroyCoreViewFilterGroup';
import { UPDATE_CORE_VIEW_FILTER_GROUP } from '@/views/graphql/mutations/updateCoreViewFilterGroup';
import { type GraphQLView } from '@/views/types/GraphQLView';
import { type ViewFilterGroup } from '@/views/types/ViewFilterGroup';
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
import { useApolloClient } from '@apollo/client';
import { isNull } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
import { type CoreViewFilterGroup, FeatureFlagKey } from '~/generated/graphql';
export const usePersistViewFilterGroupRecords = () => {
const featureFlags = useFeatureFlagsMap();
const isCoreViewEnabled = featureFlags[FeatureFlagKey.IS_CORE_VIEW_ENABLED];
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular: CoreObjectNameSingular.ViewFilterGroup,
});
@@ -40,9 +50,10 @@ export const usePersistViewFilterGroupRecords = () => {
const { objectMetadataItems } = useObjectMetadataItems();
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const apolloCoreClient = useApolloCoreClient();
const apolloClient = useApolloClient();
const createViewFilterGroupRecord = useCallback(
async (viewFilterGroup: ViewFilterGroup, view: GraphQLView) => {
async (viewFilterGroup: ViewFilterGroup, view: Pick<GraphQLView, 'id'>) => {
const result = await apolloCoreClient.mutate<{
createViewFilterGroup: ViewFilterGroup;
}>({
@@ -87,7 +98,10 @@ export const usePersistViewFilterGroupRecords = () => {
);
const createViewFilterGroupRecords = useCallback(
async (viewFilterGroupsToCreate: ViewFilterGroup[], view: GraphQLView) => {
async (
viewFilterGroupsToCreate: ViewFilterGroup[],
view: Pick<GraphQLView, 'id'>,
) => {
if (!viewFilterGroupsToCreate.length) return [];
const oldToNewId = new Map<string, string>();
@@ -209,9 +223,172 @@ export const usePersistViewFilterGroupRecords = () => {
],
);
const createCoreViewFilterGroupRecord = useCallback(
async (viewFilterGroup: ViewFilterGroup, view: Pick<GraphQLView, 'id'>) => {
const result = await apolloClient.mutate<{
createCoreViewFilterGroup: ViewFilterGroup;
}>({
mutation: CREATE_CORE_VIEW_FILTER_GROUP,
variables: {
input: {
viewId: view.id,
parentViewFilterGroupId: viewFilterGroup.parentViewFilterGroupId,
logicalOperator: viewFilterGroup.logicalOperator,
positionInViewFilterGroup:
viewFilterGroup.positionInViewFilterGroup,
} satisfies Partial<CoreViewFilterGroup>,
},
update: (cache, { data }) => {
const record = data?.createCoreViewFilterGroup;
if (!isDefined(record)) return;
triggerCreateRecordsOptimisticEffect({
cache,
objectMetadataItem,
recordsToCreate: [record],
objectMetadataItems,
objectPermissionsByObjectMetadataId,
});
},
});
if (!result.data) {
throw new Error('Failed to create core view filter group');
}
return { newRecordId: result.data.createCoreViewFilterGroup.id };
},
[
apolloClient,
objectMetadataItem,
objectMetadataItems,
objectPermissionsByObjectMetadataId,
],
);
const createCoreViewFilterGroupRecords = useCallback(
async (
viewFilterGroupsToCreate: ViewFilterGroup[],
view: Pick<GraphQLView, 'id'>,
) => {
if (!viewFilterGroupsToCreate.length) return [];
const oldToNewId = new Map<string, string>();
for (const viewFilterGroupToCreate of viewFilterGroupsToCreate) {
const newParentViewFilterGroupId = isDefined(
viewFilterGroupToCreate.parentViewFilterGroupId,
)
? (oldToNewId.get(viewFilterGroupToCreate.parentViewFilterGroupId) ??
viewFilterGroupToCreate.parentViewFilterGroupId)
: undefined;
const { newRecordId } = await createCoreViewFilterGroupRecord(
{
...viewFilterGroupToCreate,
parentViewFilterGroupId: newParentViewFilterGroupId,
},
view,
);
oldToNewId.set(viewFilterGroupToCreate.id, newRecordId);
}
const newRecordIds = viewFilterGroupsToCreate.map((viewFilterGroup) => {
const newId = oldToNewId.get(viewFilterGroup.id);
if (!newId) {
throw new Error('Failed to create core view filter group');
}
return newId;
});
return newRecordIds;
},
[createCoreViewFilterGroupRecord],
);
const updateCoreViewFilterGroupRecords = useCallback(
(viewFilterGroupsToUpdate: ViewFilterGroup[]) => {
if (!viewFilterGroupsToUpdate.length) return;
return Promise.all(
viewFilterGroupsToUpdate.map((viewFilterGroup) =>
apolloClient.mutate<{ updateCoreViewFilterGroup: ViewFilterGroup }>({
mutation: UPDATE_CORE_VIEW_FILTER_GROUP,
variables: {
idToUpdate: viewFilterGroup.id,
input: {
parentViewFilterGroupId:
viewFilterGroup.parentViewFilterGroupId,
logicalOperator: viewFilterGroup.logicalOperator,
positionInViewFilterGroup:
viewFilterGroup.positionInViewFilterGroup,
} satisfies Partial<CoreViewFilterGroup>,
},
update: (cache, { data }) => {
const record = data?.updateCoreViewFilterGroup;
if (!isDefined(record)) return;
const cachedRecord = getRecordFromCache<ViewFilterGroup>(
record.id,
cache,
);
if (isNull(cachedRecord)) return;
triggerUpdateRecordOptimisticEffect({
cache,
objectMetadataItem,
currentRecord: cachedRecord,
updatedRecord: record,
objectMetadataItems,
});
},
}),
),
);
},
[apolloClient, getRecordFromCache, objectMetadataItem, objectMetadataItems],
);
const deleteCoreViewFilterGroupRecords = useCallback(
(viewFilterGroupIdsToDelete: string[]) => {
if (!viewFilterGroupIdsToDelete.length) return;
return Promise.all(
viewFilterGroupIdsToDelete.map((viewFilterGroupId) =>
apolloClient.mutate<{ destroyCoreViewFilterGroup: ViewFilterGroup }>({
mutation: DESTROY_CORE_VIEW_FILTER_GROUP,
variables: {
idToDestroy: viewFilterGroupId,
},
update: (cache, { data }) => {
const record = data?.destroyCoreViewFilterGroup;
if (!isDefined(record)) return;
const cachedRecord = getRecordFromCache(record.id, cache);
if (isNull(cachedRecord)) return;
triggerDestroyRecordsOptimisticEffect({
cache,
objectMetadataItem,
recordsToDestroy: [cachedRecord],
objectMetadataItems,
});
},
}),
),
);
},
[apolloClient, getRecordFromCache, objectMetadataItem, objectMetadataItems],
);
return {
createViewFilterGroupRecords,
updateViewFilterGroupRecords,
deleteViewFilterGroupRecords,
createViewFilterGroupRecords: isCoreViewEnabled
? createCoreViewFilterGroupRecords
: createViewFilterGroupRecords,
updateViewFilterGroupRecords: isCoreViewEnabled
? updateCoreViewFilterGroupRecords
: updateViewFilterGroupRecords,
deleteViewFilterGroupRecords: isCoreViewEnabled
? deleteCoreViewFilterGroupRecords
: deleteViewFilterGroupRecords,
};
};
@@ -12,11 +12,22 @@ import { useCreateOneRecordMutation } from '@/object-record/hooks/useCreateOneRe
import { useDestroyOneRecordMutation } from '@/object-record/hooks/useDestroyOneRecordMutation';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useUpdateOneRecordMutation } from '@/object-record/hooks/useUpdateOneRecordMutation';
import { CREATE_CORE_VIEW_FILTER } from '@/views/graphql/mutations/createCoreViewFilter';
import { DESTROY_CORE_VIEW_FILTER } from '@/views/graphql/mutations/destroyCoreViewFilter';
import { UPDATE_CORE_VIEW_FILTER } from '@/views/graphql/mutations/updateCoreViewFilter';
import { type GraphQLView } from '@/views/types/GraphQLView';
import { type ViewFilter } from '@/views/types/ViewFilter';
import { convertViewFilterOperandToCore } from '@/views/utils/convertViewFilterOperandToCore';
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
import { useApolloClient } from '@apollo/client';
import { isNull } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
import { type CoreViewFilter, FeatureFlagKey } from '~/generated/graphql';
export const usePersistViewFilterRecords = () => {
const featureFlags = useFeatureFlagsMap();
const isCoreViewEnabled = featureFlags[FeatureFlagKey.IS_CORE_VIEW_ENABLED];
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular: CoreObjectNameSingular.ViewFilter,
});
@@ -40,9 +51,9 @@ export const usePersistViewFilterRecords = () => {
const { objectMetadataItems } = useObjectMetadataItems();
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const apolloCoreClient = useApolloCoreClient();
const apolloClient = useApolloClient();
const createViewFilterRecords = useCallback(
(viewFiltersToCreate: ViewFilter[], view: GraphQLView) => {
(viewFiltersToCreate: ViewFilter[], view: Pick<GraphQLView, 'id'>) => {
if (viewFiltersToCreate.length === 0) return;
return Promise.all(
@@ -176,9 +187,134 @@ export const usePersistViewFilterRecords = () => {
],
);
const createCoreViewFilterRecords = useCallback(
(viewFiltersToCreate: ViewFilter[], view: Pick<GraphQLView, 'id'>) => {
if (viewFiltersToCreate.length === 0) return;
return Promise.all(
viewFiltersToCreate.map((viewFilter) =>
apolloClient.mutate({
mutation: CREATE_CORE_VIEW_FILTER,
variables: {
input: {
fieldMetadataId: viewFilter.fieldMetadataId,
viewId: view.id,
value: viewFilter.value,
operand: convertViewFilterOperandToCore(viewFilter.operand),
viewFilterGroupId: viewFilter.viewFilterGroupId,
positionInViewFilterGroup: viewFilter.positionInViewFilterGroup,
subFieldName: viewFilter.subFieldName ?? null,
} satisfies Partial<CoreViewFilter>,
},
update: (cache, { data }) => {
const record = data?.['createCoreViewFilter'];
if (!isDefined(record)) return;
triggerCreateRecordsOptimisticEffect({
cache,
objectMetadataItem,
recordsToCreate: [record],
objectMetadataItems,
objectPermissionsByObjectMetadataId,
});
},
}),
),
);
},
[
apolloClient,
objectMetadataItem,
objectMetadataItems,
objectPermissionsByObjectMetadataId,
],
);
const updateCoreViewFilterRecords = useCallback(
(viewFiltersToUpdate: ViewFilter[]) => {
if (!viewFiltersToUpdate.length) return;
return Promise.all(
viewFiltersToUpdate.map((viewFilter) =>
apolloClient.mutate({
mutation: UPDATE_CORE_VIEW_FILTER,
variables: {
idToUpdate: viewFilter.id,
input: {
value: viewFilter.value,
operand: convertViewFilterOperandToCore(viewFilter.operand),
positionInViewFilterGroup: viewFilter.positionInViewFilterGroup,
viewFilterGroupId: viewFilter.viewFilterGroupId,
subFieldName: viewFilter.subFieldName ?? null,
} satisfies Partial<CoreViewFilter>,
},
update: (cache, { data }) => {
const record = data?.['updateCoreViewFilter'];
if (!isDefined(record)) return;
const cachedRecord = getRecordFromCache<ViewFilter>(
record.id,
cache,
);
if (isNull(cachedRecord)) return;
triggerUpdateRecordOptimisticEffect({
cache,
objectMetadataItem,
currentRecord: cachedRecord,
updatedRecord: record,
objectMetadataItems,
});
},
}),
),
);
},
[apolloClient, getRecordFromCache, objectMetadataItem, objectMetadataItems],
);
const deleteCoreViewFilterRecords = useCallback(
(viewFilterIdsToDelete: string[]) => {
if (!viewFilterIdsToDelete.length) return;
return Promise.all(
viewFilterIdsToDelete.map((viewFilterId) =>
apolloClient.mutate({
mutation: DESTROY_CORE_VIEW_FILTER,
variables: {
idToDestroy: viewFilterId,
},
update: (cache, { data }) => {
const record = data?.['destroyCoreViewFilter'];
if (!isDefined(record)) return;
const cachedRecord = getRecordFromCache<ViewFilter>(
record.id,
cache,
);
if (isNull(cachedRecord)) return;
triggerDestroyRecordsOptimisticEffect({
cache,
objectMetadataItem,
recordsToDestroy: [cachedRecord],
objectMetadataItems,
});
},
}),
),
);
},
[apolloClient, getRecordFromCache, objectMetadataItem, objectMetadataItems],
);
return {
createViewFilterRecords,
updateViewFilterRecords,
deleteViewFilterRecords,
createViewFilterRecords: isCoreViewEnabled
? createCoreViewFilterRecords
: createViewFilterRecords,
updateViewFilterRecords: isCoreViewEnabled
? updateCoreViewFilterRecords
: updateViewFilterRecords,
deleteViewFilterRecords: isCoreViewEnabled
? deleteCoreViewFilterRecords
: deleteViewFilterRecords,
};
};
@@ -5,14 +5,25 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
import { useCreateManyRecords } from '@/object-record/hooks/useCreateManyRecords';
import { useDestroyManyRecords } from '@/object-record/hooks/useDestroyManyRecords';
import { useUpdateOneRecordMutation } from '@/object-record/hooks/useUpdateOneRecordMutation';
import { CREATE_CORE_VIEW_GROUP } from '@/views/graphql/mutations/createCoreViewGroup';
import { DESTROY_CORE_VIEW_GROUP } from '@/views/graphql/mutations/destroyCoreViewGroup';
import { UPDATE_CORE_VIEW_GROUP } from '@/views/graphql/mutations/updateCoreViewGroup';
import { type ViewGroup } from '@/views/types/ViewGroup';
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
import { useApolloClient } from '@apollo/client';
import { FeatureFlagKey } from '~/generated/graphql';
type CreateViewGroupRecordsArgs = {
viewGroupsToCreate: ViewGroup[];
viewId: string;
};
export const usePersistViewGroupRecords = () => {
const featureFlags = useFeatureFlagsMap();
const isCoreViewEnabled = featureFlags[FeatureFlagKey.IS_CORE_VIEW_ENABLED];
const apolloCoreClient = useApolloCoreClient();
const apolloClient = useApolloClient();
const { createManyRecords } = useCreateManyRecords({
objectNameSingular: CoreObjectNameSingular.ViewGroup,
@@ -98,9 +109,98 @@ export const usePersistViewGroupRecords = () => {
[destroyManyRecords],
);
const createCoreViewGroupRecords = useCallback(
({ viewGroupsToCreate, viewId }: CreateViewGroupRecordsArgs) => {
if (viewGroupsToCreate.length === 0) return;
return Promise.all(
viewGroupsToCreate.map((viewGroup) =>
apolloClient.mutate({
mutation: CREATE_CORE_VIEW_GROUP,
variables: {
input: {
viewId,
isVisible: viewGroup.isVisible,
position: viewGroup.position,
},
},
}),
),
);
},
[apolloClient],
);
const updateCoreViewGroupRecords = useCallback(
async (viewGroupsToUpdate: ViewGroup[]) => {
if (!viewGroupsToUpdate.length) return;
const mutationPromises = viewGroupsToUpdate.map((viewGroup) =>
apolloClient.mutate<{ updateCoreViewGroup: ViewGroup }>({
mutation: UPDATE_CORE_VIEW_GROUP,
variables: {
idToUpdate: viewGroup.id,
input: {
isVisible: viewGroup.isVisible,
position: viewGroup.position,
},
},
// Avoid cache being updated with stale data
fetchPolicy: 'no-cache',
}),
);
const mutationResults = await Promise.all(mutationPromises);
// FixMe: Using useUpdateOneRecord hook that call triggerUpdateRecordsOptimisticEffect is actaully causing multiple records to be created
// This is a temporary fix
mutationResults.forEach(({ data }) => {
const record = data?.['updateCoreViewGroup'];
if (!record) return;
apolloClient.cache.modify({
id: apolloClient.cache.identify({
__typename: 'CoreViewGroup',
id: record.id,
}),
fields: {
isVisible: () => record.isVisible,
position: () => record.position,
},
});
});
},
[apolloClient],
);
const deleteCoreViewGroupRecords = useCallback(
async (viewGroupsToDelete: ViewGroup[]) => {
if (!viewGroupsToDelete.length) return;
return Promise.all(
viewGroupsToDelete.map((viewGroup) =>
apolloClient.mutate({
mutation: DESTROY_CORE_VIEW_GROUP,
variables: {
idToDestroy: viewGroup.id,
},
}),
),
);
},
[apolloClient],
);
return {
createViewGroupRecords,
updateViewGroupRecords,
deleteViewGroupRecords,
createViewGroupRecords: isCoreViewEnabled
? createCoreViewGroupRecords
: createViewGroupRecords,
updateViewGroupRecords: isCoreViewEnabled
? updateCoreViewGroupRecords
: updateViewGroupRecords,
deleteViewGroupRecords: isCoreViewEnabled
? deleteCoreViewGroupRecords
: deleteViewGroupRecords,
};
};
@@ -12,11 +12,22 @@ import { useCreateOneRecordMutation } from '@/object-record/hooks/useCreateOneRe
import { useDestroyOneRecordMutation } from '@/object-record/hooks/useDestroyOneRecordMutation';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { useUpdateOneRecordMutation } from '@/object-record/hooks/useUpdateOneRecordMutation';
import { CREATE_CORE_VIEW_SORT } from '@/views/graphql/mutations/createCoreViewSort';
import { DESTROY_CORE_VIEW_SORT } from '@/views/graphql/mutations/destroyCoreViewSort';
import { UPDATE_CORE_VIEW_SORT } from '@/views/graphql/mutations/updateCoreViewSort';
import { type GraphQLView } from '@/views/types/GraphQLView';
import { type ViewSort } from '@/views/types/ViewSort';
import { convertViewSortDirectionToCore } from '@/views/utils/convertViewSortDirectionToCore';
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
import { useApolloClient } from '@apollo/client';
import { isNull } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
import { type CoreViewSort, FeatureFlagKey } from '~/generated/graphql';
export const usePersistViewSortRecords = () => {
const featureFlags = useFeatureFlagsMap();
const isCoreViewEnabled = featureFlags[FeatureFlagKey.IS_CORE_VIEW_ENABLED];
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular: CoreObjectNameSingular.ViewSort,
});
@@ -40,9 +51,9 @@ export const usePersistViewSortRecords = () => {
const { objectMetadataItems } = useObjectMetadataItems();
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const apolloCoreClient = useApolloCoreClient();
const apolloClient = useApolloClient();
const createViewSortRecords = useCallback(
(viewSortsToCreate: ViewSort[], view: GraphQLView) => {
(viewSortsToCreate: ViewSort[], view: Pick<GraphQLView, 'id'>) => {
if (!viewSortsToCreate.length) return;
return Promise.all(
viewSortsToCreate.map((viewSort) =>
@@ -165,9 +176,125 @@ export const usePersistViewSortRecords = () => {
],
);
const createCoreViewSortRecords = useCallback(
(viewSortsToCreate: ViewSort[], view: Pick<GraphQLView, 'id'>) => {
if (!viewSortsToCreate.length) return;
return Promise.all(
viewSortsToCreate.map((viewSort) =>
apolloClient.mutate({
mutation: CREATE_CORE_VIEW_SORT,
variables: {
input: {
fieldMetadataId: viewSort.fieldMetadataId,
viewId: view.id,
direction: convertViewSortDirectionToCore(viewSort.direction),
} satisfies Partial<CoreViewSort>,
},
update: (cache, { data }) => {
const record = data?.['createCoreViewSort'];
if (!isDefined(record)) return;
triggerCreateRecordsOptimisticEffect({
cache,
objectMetadataItem,
recordsToCreate: [record],
objectMetadataItems,
objectPermissionsByObjectMetadataId,
});
},
}),
),
);
},
[
apolloClient,
objectMetadataItem,
objectMetadataItems,
objectPermissionsByObjectMetadataId,
],
);
const updateCoreViewSortRecords = useCallback(
(viewSortsToUpdate: ViewSort[]) => {
if (!viewSortsToUpdate.length) return;
return Promise.all(
viewSortsToUpdate.map((viewSort) =>
apolloClient.mutate({
mutation: UPDATE_CORE_VIEW_SORT,
variables: {
idToUpdate: viewSort.id,
input: {
direction: convertViewSortDirectionToCore(viewSort.direction),
} satisfies Partial<CoreViewSort>,
},
update: (cache, { data }) => {
const record = data?.['updateCoreViewSort'];
if (!isDefined(record)) return;
const cachedRecord = getRecordFromCache<ViewSort>(
record.id,
cache,
);
if (isNull(cachedRecord)) return;
triggerUpdateRecordOptimisticEffect({
cache,
objectMetadataItem,
currentRecord: cachedRecord,
updatedRecord: record,
objectMetadataItems,
});
},
}),
),
);
},
[apolloClient, getRecordFromCache, objectMetadataItem, objectMetadataItems],
);
const deleteCoreViewSortRecords = useCallback(
(viewSortIdsToDelete: string[]) => {
if (!viewSortIdsToDelete.length) return;
return Promise.all(
viewSortIdsToDelete.map((viewSortId) =>
apolloClient.mutate({
mutation: DESTROY_CORE_VIEW_SORT,
variables: {
idToDestroy: viewSortId,
},
update: (cache, { data }) => {
const record = data?.['destroyCoreViewSort'];
if (!isDefined(record)) return;
const cachedRecord = getRecordFromCache<ViewSort>(
record.id,
cache,
);
if (isNull(cachedRecord)) return;
triggerDestroyRecordsOptimisticEffect({
cache,
objectMetadataItem,
recordsToDestroy: [cachedRecord],
objectMetadataItems,
});
},
}),
),
);
},
[apolloClient, getRecordFromCache, objectMetadataItem, objectMetadataItems],
);
return {
createViewSortRecords,
updateViewSortRecords,
deleteViewSortRecords,
createViewSortRecords: isCoreViewEnabled
? createCoreViewSortRecords
: createViewSortRecords,
updateViewSortRecords: isCoreViewEnabled
? updateCoreViewSortRecords
: updateViewSortRecords,
deleteViewSortRecords: isCoreViewEnabled
? deleteCoreViewSortRecords
: deleteViewSortRecords,
};
};
@@ -22,16 +22,23 @@ import { type View } from '@/views/types/View';
import { type ViewGroup } from '@/views/types/ViewGroup';
import { type ViewSort } from '@/views/types/ViewSort';
import { ViewType } from '@/views/types/ViewType';
import { convertViewOpenRecordInToCore } from '@/views/utils/convertViewOpenRecordInToCore';
import { convertViewTypeToCore } from '@/views/utils/convertViewTypeToCore';
import { duplicateViewFiltersAndViewFilterGroups } from '@/views/utils/duplicateViewFiltersAndViewFilterGroups';
import { mapRecordFilterGroupToViewFilterGroup } from '@/views/utils/mapRecordFilterGroupToViewFilterGroup';
import { mapRecordFilterToViewFilter } from '@/views/utils/mapRecordFilterToViewFilter';
import { mapRecordSortToViewSort } from '@/views/utils/mapRecordSortToViewSort';
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import { FeatureFlagKey, useCreateCoreViewMutation } from '~/generated/graphql';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => {
const featureFlags = useFeatureFlagsMap();
const isCoreViewEnabled = featureFlags[FeatureFlagKey.IS_CORE_VIEW_ENABLED];
const [createCoreViewMutation] = useCreateCoreViewMutation();
const currentViewIdCallbackState = useRecoilComponentCallbackState(
contextStoreCurrentViewIdComponentState,
viewBarComponentId,
@@ -90,14 +97,14 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => {
>
>,
shouldCopyFiltersAndSortsAndAggregate?: boolean,
) => {
): Promise<string | undefined> => {
const currentViewId = getSnapshotValue(
snapshot,
currentViewIdCallbackState,
);
if (!isDefined(currentViewId)) {
return;
return undefined;
}
const sourceView = snapshot
@@ -109,36 +116,65 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => {
.getValue();
if (!isDefined(sourceView)) {
return;
return undefined;
}
set(isPersistingViewFieldsState, true);
const newView = await createOneRecord({
id: id ?? v4(),
name: name ?? sourceView.name,
icon: icon ?? sourceView.icon,
key: null,
kanbanFieldMetadataId:
kanbanFieldMetadataId ?? sourceView.kanbanFieldMetadataId,
kanbanAggregateOperation: shouldCopyFiltersAndSortsAndAggregate
? sourceView.kanbanAggregateOperation
: undefined,
kanbanAggregateOperationFieldMetadataId:
shouldCopyFiltersAndSortsAndAggregate
? sourceView.kanbanAggregateOperationFieldMetadataId
: undefined,
type: type ?? sourceView.type,
objectMetadataId: sourceView.objectMetadataId,
openRecordIn: sourceView.openRecordIn,
anyFieldFilterValue: anyFieldFilterValue,
});
let newViewId: string | undefined;
if (isUndefinedOrNull(newView)) {
if (isCoreViewEnabled) {
const result = await createCoreViewMutation({
variables: {
input: {
name: name ?? sourceView.name,
icon: icon ?? sourceView.icon,
key: null,
kanbanAggregateOperation: shouldCopyFiltersAndSortsAndAggregate
? sourceView.kanbanAggregateOperation
: undefined,
kanbanAggregateOperationFieldMetadataId:
shouldCopyFiltersAndSortsAndAggregate
? sourceView.kanbanAggregateOperationFieldMetadataId
: undefined,
type: convertViewTypeToCore(type ?? sourceView.type),
objectMetadataId: sourceView.objectMetadataId,
openRecordIn: convertViewOpenRecordInToCore(
sourceView.openRecordIn,
),
anyFieldFilterValue: anyFieldFilterValue,
},
},
});
newViewId = result.data?.createCoreView?.id ?? undefined;
} else {
const createdView = await createOneRecord({
id: id ?? v4(),
name: name ?? sourceView.name,
icon: icon ?? sourceView.icon,
key: null,
kanbanFieldMetadataId:
kanbanFieldMetadataId ?? sourceView.kanbanFieldMetadataId,
kanbanAggregateOperation: shouldCopyFiltersAndSortsAndAggregate
? sourceView.kanbanAggregateOperation
: undefined,
kanbanAggregateOperationFieldMetadataId:
shouldCopyFiltersAndSortsAndAggregate
? sourceView.kanbanAggregateOperationFieldMetadataId
: undefined,
type: type ?? sourceView.type,
objectMetadataId: sourceView.objectMetadataId,
openRecordIn: sourceView.openRecordIn,
anyFieldFilterValue: anyFieldFilterValue,
});
newViewId = createdView.id;
}
if (isUndefinedOrNull(newViewId)) {
throw new Error('Failed to create view');
}
await createViewFieldRecords(sourceView.viewFields, newView);
await createViewFieldRecords(sourceView.viewFields, { id: newViewId });
if (type === ViewType.Kanban) {
if (!isDefined(kanbanFieldMetadataId)) {
@@ -171,7 +207,7 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => {
await createViewGroupRecords({
viewGroupsToCreate,
viewId: newView.id,
viewId: newViewId,
});
}
@@ -180,7 +216,7 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => {
(recordFilterGroup) =>
mapRecordFilterGroupToViewFilterGroup({
recordFilterGroup,
view: newView,
view: { id: newViewId },
}),
);
@@ -206,13 +242,16 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => {
}) satisfies ViewSort,
);
await createViewFilterGroupRecords(viewFilterGroupsToCreate, newView);
await createViewFilterRecords(viewFiltersToCreate, newView);
await createViewSortRecords(viewSortsToCreate, newView);
await createViewFilterGroupRecords(viewFilterGroupsToCreate, {
id: newViewId,
});
await createViewFilterRecords(viewFiltersToCreate, { id: newViewId });
await createViewSortRecords(viewSortsToCreate, { id: newViewId });
}
await findManyRecordsLazy();
set(isPersistingViewFieldsState, false);
return newViewId;
},
[
anyFieldFilterValue,
@@ -228,6 +267,8 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => {
currentRecordFilters,
currentRecordSorts,
currentRecordFilterGroups,
isCoreViewEnabled,
createCoreViewMutation,
],
);
@@ -5,17 +5,26 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { type GraphQLView } from '@/views/types/GraphQLView';
import { convertUpdateViewInputToCore } from '@/views/utils/convertUpdateViewInputToCore';
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
import { isDefined } from 'twenty-shared/utils';
import { useUpdateCoreViewMutation } from '~/generated-metadata/graphql';
import { FeatureFlagKey } from '~/generated/graphql';
export const useUpdateCurrentView = () => {
const currentViewIdCallbackState = useRecoilComponentCallbackState(
contextStoreCurrentViewIdComponentState,
);
const featureFlagMap = useFeatureFlagsMap();
const isCoreViewEnabled = featureFlagMap[FeatureFlagKey.IS_CORE_VIEW_ENABLED];
const { updateOneRecord } = useUpdateOneRecord({
objectNameSingular: CoreObjectNameSingular.View,
});
const [updateOneCoreView] = useUpdateCoreViewMutation();
const updateCurrentView = useRecoilCallback(
({ snapshot }) =>
async (view: Partial<GraphQLView>) => {
@@ -24,13 +33,29 @@ export const useUpdateCurrentView = () => {
.getValue();
if (isDefined(currentViewId)) {
await updateOneRecord({
idToUpdate: currentViewId,
updateOneRecordInput: view,
});
if (isCoreViewEnabled) {
const input = convertUpdateViewInputToCore(view);
await updateOneCoreView({
variables: {
id: currentViewId,
input,
},
});
} else {
await updateOneRecord({
idToUpdate: currentViewId,
updateOneRecordInput: view,
});
}
}
},
[currentViewIdCallbackState, updateOneRecord],
[
currentViewIdCallbackState,
isCoreViewEnabled,
updateOneCoreView,
updateOneRecord,
],
);
return {
@@ -1,24 +1,41 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { type GraphQLView } from '@/views/types/GraphQLView';
import { convertUpdateViewInputToCore } from '@/views/utils/convertUpdateViewInputToCore';
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { FeatureFlagKey, useUpdateCoreViewMutation } from '~/generated/graphql';
export const useUpdateView = () => {
const featureFlagMap = useFeatureFlagsMap();
const isCoreViewEnabled = featureFlagMap[FeatureFlagKey.IS_CORE_VIEW_ENABLED];
const { updateOneRecord } = useUpdateOneRecord({
objectNameSingular: CoreObjectNameSingular.View,
});
const [updateOneCoreView] = useUpdateCoreViewMutation();
const updateView = useRecoilCallback(
() => async (view: Partial<GraphQLView>) => {
if (isDefined(view.id)) {
await updateOneRecord({
idToUpdate: view.id,
updateOneRecordInput: view,
});
if (isCoreViewEnabled) {
await updateOneCoreView({
variables: {
id: view.id,
input: convertUpdateViewInputToCore(view),
},
});
} else {
await updateOneRecord({
idToUpdate: view.id,
updateOneRecordInput: view,
});
}
}
},
[updateOneRecord],
[isCoreViewEnabled, updateOneCoreView, updateOneRecord],
);
return {
@@ -0,0 +1,32 @@
import {
type AggregateOperations,
type CoreViewField,
type CoreViewFilter,
type CoreViewFilterGroup,
type CoreViewGroup,
type CoreViewSort,
type ViewKey,
type ViewOpenRecordIn,
type ViewType,
} from '~/generated-metadata/graphql';
export type CoreViewWithRelations = {
id: string;
name: string;
type: ViewType;
key?: ViewKey | null;
objectMetadataId: string;
isCompact: boolean;
viewFields: Omit<CoreViewField, 'workspaceId'>[];
viewGroups: Omit<CoreViewGroup, 'workspaceId'>[];
viewFilters: Omit<CoreViewFilter, 'workspaceId'>[];
viewFilterGroups?: Omit<CoreViewFilterGroup, 'workspaceId'>[];
viewSorts: Omit<CoreViewSort, 'workspaceId'>[];
kanbanAggregateOperation?: AggregateOperations | null;
kanbanAggregateOperationFieldMetadataId?: string | null;
position: number;
icon: string;
openRecordIn: ViewOpenRecordIn;
anyFieldFilterValue?: string | null;
__typename?: 'CoreView';
};
@@ -0,0 +1,22 @@
import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
import { type ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
import { type ViewField } from '@/views/types/ViewField';
import { type CoreViewField } from '~/generated/graphql';
export const convertCoreViewFieldToViewField = (
coreViewField: Omit<CoreViewField, 'workspaceId'>,
): ViewField => {
const viewField: ViewField = {
__typename: 'ViewField',
id: coreViewField.id,
fieldMetadataId: coreViewField.fieldMetadataId,
position: coreViewField.position,
isVisible: coreViewField.isVisible,
size: coreViewField.size,
aggregateOperation: coreViewField.aggregateOperation ?? null,
// TODO: remove this once we have refactored the view field definition
definition: undefined as unknown as ColumnDefinition<FieldMetadata>,
};
return viewField;
};
@@ -0,0 +1,21 @@
import { type ViewFilterGroup } from '@/views/types/ViewFilterGroup';
import { ViewFilterGroupLogicalOperator } from '@/views/types/ViewFilterGroupLogicalOperator';
import { type CoreViewFilterGroup } from '~/generated/graphql';
export const convertCoreViewFilterGroupToViewFilterGroup = (
coreViewFilterGroup: Omit<CoreViewFilterGroup, 'workspaceId'>,
): ViewFilterGroup => {
return {
__typename: 'ViewFilterGroup',
id: coreViewFilterGroup.id,
viewId: coreViewFilterGroup.viewId,
parentViewFilterGroupId:
coreViewFilterGroup.parentViewFilterGroupId ?? null,
logicalOperator:
coreViewFilterGroup.logicalOperator === 'AND'
? ViewFilterGroupLogicalOperator.AND
: ViewFilterGroupLogicalOperator.OR,
positionInViewFilterGroup:
coreViewFilterGroup.positionInViewFilterGroup ?? null,
};
};
@@ -0,0 +1,28 @@
import { type ViewFilter } from '@/views/types/ViewFilter';
import { type CoreViewFilter } from '~/generated/graphql';
import { convertViewFilterOperandFromCore } from '../utils/convertViewFilterOperandFromCore';
export const convertCoreViewFilterToViewFilter = (
coreViewFilter: Omit<CoreViewFilter, 'workspaceId'>,
): ViewFilter => {
return {
__typename: 'ViewFilter',
id: coreViewFilter.id,
fieldMetadataId: coreViewFilter.fieldMetadataId,
operand: convertViewFilterOperandFromCore(coreViewFilter.operand),
value:
typeof coreViewFilter.value === 'string'
? coreViewFilter.value
: JSON.stringify(coreViewFilter.value ?? ''),
displayValue:
typeof coreViewFilter.value === 'string'
? coreViewFilter.value
: JSON.stringify(coreViewFilter.value ?? ''),
createdAt: coreViewFilter.createdAt,
updatedAt: coreViewFilter.updatedAt,
viewId: coreViewFilter.viewId,
viewFilterGroupId: coreViewFilter.viewFilterGroupId ?? undefined,
positionInViewFilterGroup: coreViewFilter.positionInViewFilterGroup ?? null,
subFieldName: (coreViewFilter.subFieldName as any) ?? null,
};
};
@@ -0,0 +1,15 @@
import { type ViewGroup } from '@/views/types/ViewGroup';
import { type CoreViewGroup } from '~/generated/graphql';
export const convertCoreViewGroupToViewGroup = (
coreViewGroup: Omit<CoreViewGroup, 'workspaceId'>,
): ViewGroup => {
return {
__typename: 'ViewGroup',
id: coreViewGroup.id,
fieldMetadataId: coreViewGroup.fieldMetadataId,
isVisible: coreViewGroup.isVisible,
fieldValue: coreViewGroup.fieldValue,
position: coreViewGroup.position,
};
};
@@ -0,0 +1,8 @@
import { ViewKey } from '@/views/types/ViewKey';
import { ViewKey as CoreViewKey } from '~/generated-metadata/graphql';
export const convertCoreViewKeyToViewKey = (
coreViewKey: CoreViewKey | null | undefined,
): ViewKey | null => {
return coreViewKey === CoreViewKey.INDEX ? ViewKey.Index : null;
};
@@ -0,0 +1,10 @@
import { ViewOpenRecordInType } from '@/views/types/ViewOpenRecordInType';
import { ViewOpenRecordIn as CoreViewOpenRecordIn } from '~/generated/graphql';
export const convertCoreViewOpenRecordInToViewOpenRecordIn = (
openIn: CoreViewOpenRecordIn,
): ViewOpenRecordInType => {
return openIn === CoreViewOpenRecordIn.SIDE_PANEL
? ViewOpenRecordInType.SIDE_PANEL
: ViewOpenRecordInType.RECORD_PAGE;
};
@@ -0,0 +1,14 @@
import { type ViewSort } from '@/views/types/ViewSort';
import { type CoreViewSort, ViewSortDirection } from '~/generated/graphql';
export const convertCoreViewSortToViewSort = (
coreViewSort: Omit<CoreViewSort, 'workspaceId'>,
): ViewSort => {
return {
__typename: 'ViewSort',
id: coreViewSort.id,
fieldMetadataId: coreViewSort.fieldMetadataId,
direction:
coreViewSort.direction === ViewSortDirection.ASC ? 'asc' : 'desc',
};
};
@@ -0,0 +1,53 @@
import { type CoreViewWithRelations } from '@/views/types/CoreViewWithRelations';
import { type View } from '@/views/types/View';
import { convertCoreViewFieldToViewField } from '@/views/utils/convertCoreViewFieldToViewField';
import { convertCoreViewFilterGroupToViewFilterGroup } from '@/views/utils/convertCoreViewFilterGroupToViewFilterGroup';
import { convertCoreViewFilterToViewFilter } from '@/views/utils/convertCoreViewFilterToViewFilter';
import { convertCoreViewGroupToViewGroup } from '@/views/utils/convertCoreViewGroupToViewGroup';
import { convertCoreViewKeyToViewKey } from '@/views/utils/convertCoreViewKeyToViewKey';
import { convertCoreViewOpenRecordInToViewOpenRecordIn } from '@/views/utils/convertCoreViewOpenRecordInToViewOpenRecordIn';
import { convertCoreViewSortToViewSort } from '@/views/utils/convertCoreViewSortToViewSort';
import { convertCoreViewTypeToViewType } from '@/views/utils/convertCoreViewTypeToViewType';
export const convertCoreViewToView = (
coreView: CoreViewWithRelations,
): View => {
const convertedKey = convertCoreViewKeyToViewKey(coreView.key);
const convertedOpenRecordIn = convertCoreViewOpenRecordInToViewOpenRecordIn(
coreView.openRecordIn,
);
const convertedType = convertCoreViewTypeToViewType(coreView.type);
return {
id: coreView.id,
name: coreView.name,
type: convertedType,
key: convertedKey,
objectMetadataId: coreView.objectMetadataId,
isCompact: coreView.isCompact,
viewFields: coreView.viewFields.map((viewField) =>
convertCoreViewFieldToViewField(viewField),
),
viewGroups: coreView.viewGroups.map((viewGroup) =>
convertCoreViewGroupToViewGroup(viewGroup),
),
viewFilters: coreView.viewFilters.map((viewFilter) =>
convertCoreViewFilterToViewFilter(viewFilter),
),
viewFilterGroups: coreView.viewFilterGroups?.map(
convertCoreViewFilterGroupToViewFilterGroup,
),
viewSorts: coreView.viewSorts.map((viewSort) =>
convertCoreViewSortToViewSort(viewSort),
),
kanbanFieldMetadataId: '',
kanbanAggregateOperation: coreView.kanbanAggregateOperation ?? null,
kanbanAggregateOperationFieldMetadataId:
coreView.kanbanAggregateOperationFieldMetadataId ?? null,
position: coreView.position,
icon: coreView.icon,
openRecordIn: convertedOpenRecordIn,
anyFieldFilterValue: coreView.anyFieldFilterValue ?? null,
__typename: 'View',
};
};
@@ -0,0 +1,10 @@
import { ViewType } from '@/views/types/ViewType';
import { ViewType as CoreViewType } from '~/generated/graphql';
export const convertCoreViewTypeToViewType = (
coreViewType: CoreViewType,
): ViewType => {
return coreViewType === CoreViewType.KANBAN
? ViewType.Kanban
: ViewType.Table;
};
@@ -0,0 +1,37 @@
import { type GraphQLView } from '@/views/types/GraphQLView';
import { convertViewKeyToCore } from '@/views/utils/convertViewKeyToCore';
import { convertViewOpenRecordInToCore } from '@/views/utils/convertViewOpenRecordInToCore';
import { convertViewTypeToCore } from '@/views/utils/convertViewTypeToCore';
import { isDefined } from 'twenty-shared/utils';
import { type UpdateViewInput } from '~/generated-metadata/graphql';
export const convertUpdateViewInputToCore = (
view: Partial<GraphQLView>,
): UpdateViewInput => {
const {
key,
openRecordIn,
type,
viewFields: _viewFields,
viewFilters: _viewFilters,
viewFilterGroups: _viewFilterGroups,
viewGroups: _viewGroups,
viewSorts: _viewSorts,
...rest
} = view;
const convertedKey = isDefined(key) ? convertViewKeyToCore(key) : undefined;
const convertedOpenRecordIn = isDefined(openRecordIn)
? convertViewOpenRecordInToCore(openRecordIn)
: undefined;
const convertedType = isDefined(type)
? convertViewTypeToCore(type)
: undefined;
return {
...rest,
...(convertedKey && { key: convertedKey }),
...(convertedOpenRecordIn && { openRecordIn: convertedOpenRecordIn }),
...(convertedType && { type: convertedType }),
};
};
@@ -0,0 +1,26 @@
import { ViewFilterOperand } from 'twenty-shared/types';
import { ViewFilterOperand as CoreViewFilterOperand } from '~/generated/graphql';
const mappingFromCore: Record<CoreViewFilterOperand, ViewFilterOperand> = {
[CoreViewFilterOperand.IS]: ViewFilterOperand.Is,
[CoreViewFilterOperand.IS_NOT_NULL]: ViewFilterOperand.IsNotNull,
[CoreViewFilterOperand.IS_NOT]: ViewFilterOperand.IsNot,
[CoreViewFilterOperand.LESS_THAN_OR_EQUAL]: ViewFilterOperand.LessThanOrEqual,
[CoreViewFilterOperand.GREATER_THAN_OR_EQUAL]:
ViewFilterOperand.GreaterThanOrEqual,
[CoreViewFilterOperand.IS_BEFORE]: ViewFilterOperand.IsBefore,
[CoreViewFilterOperand.IS_AFTER]: ViewFilterOperand.IsAfter,
[CoreViewFilterOperand.CONTAINS]: ViewFilterOperand.Contains,
[CoreViewFilterOperand.DOES_NOT_CONTAIN]: ViewFilterOperand.DoesNotContain,
[CoreViewFilterOperand.IS_EMPTY]: ViewFilterOperand.IsEmpty,
[CoreViewFilterOperand.IS_NOT_EMPTY]: ViewFilterOperand.IsNotEmpty,
[CoreViewFilterOperand.IS_RELATIVE]: ViewFilterOperand.IsRelative,
[CoreViewFilterOperand.IS_IN_PAST]: ViewFilterOperand.IsInPast,
[CoreViewFilterOperand.IS_IN_FUTURE]: ViewFilterOperand.IsInFuture,
[CoreViewFilterOperand.IS_TODAY]: ViewFilterOperand.IsToday,
[CoreViewFilterOperand.VECTOR_SEARCH]: ViewFilterOperand.VectorSearch,
};
export const convertViewFilterOperandFromCore = (
coreOperand: CoreViewFilterOperand,
): ViewFilterOperand => mappingFromCore[coreOperand];
@@ -0,0 +1,28 @@
import { ViewFilterOperand } from 'twenty-shared/types';
import { ViewFilterOperand as CoreViewFilterOperand } from '~/generated-metadata/graphql';
const operandMapping: Record<ViewFilterOperand, CoreViewFilterOperand> = {
[ViewFilterOperand.Is]: CoreViewFilterOperand.IS,
[ViewFilterOperand.IsNotNull]: CoreViewFilterOperand.IS_NOT_NULL,
[ViewFilterOperand.IsNot]: CoreViewFilterOperand.IS_NOT,
[ViewFilterOperand.LessThanOrEqual]: CoreViewFilterOperand.LESS_THAN_OR_EQUAL,
[ViewFilterOperand.GreaterThanOrEqual]:
CoreViewFilterOperand.GREATER_THAN_OR_EQUAL,
[ViewFilterOperand.IsBefore]: CoreViewFilterOperand.IS_BEFORE,
[ViewFilterOperand.IsAfter]: CoreViewFilterOperand.IS_AFTER,
[ViewFilterOperand.Contains]: CoreViewFilterOperand.CONTAINS,
[ViewFilterOperand.DoesNotContain]: CoreViewFilterOperand.DOES_NOT_CONTAIN,
[ViewFilterOperand.IsEmpty]: CoreViewFilterOperand.IS_EMPTY,
[ViewFilterOperand.IsNotEmpty]: CoreViewFilterOperand.IS_NOT_EMPTY,
[ViewFilterOperand.IsRelative]: CoreViewFilterOperand.IS_RELATIVE,
[ViewFilterOperand.IsInPast]: CoreViewFilterOperand.IS_IN_PAST,
[ViewFilterOperand.IsInFuture]: CoreViewFilterOperand.IS_IN_FUTURE,
[ViewFilterOperand.IsToday]: CoreViewFilterOperand.IS_TODAY,
[ViewFilterOperand.VectorSearch]: CoreViewFilterOperand.VECTOR_SEARCH,
};
export const convertViewFilterOperandToCore = (
sharedOperand: ViewFilterOperand,
): CoreViewFilterOperand => {
return operandMapping[sharedOperand];
};
@@ -0,0 +1,7 @@
import { ViewKey } from '~/generated-metadata/graphql';
export const convertViewKeyToCore = (
viewKey: string | null | undefined,
): ViewKey | null => {
return viewKey === 'INDEX' ? ViewKey.INDEX : null;
};
@@ -0,0 +1,9 @@
import { ViewOpenRecordIn } from '~/generated-metadata/graphql';
export const convertViewOpenRecordInToCore = (
viewOpenRecordIn: string,
): ViewOpenRecordIn => {
return viewOpenRecordIn === ViewOpenRecordIn.SIDE_PANEL
? ViewOpenRecordIn.SIDE_PANEL
: ViewOpenRecordIn.RECORD_PAGE;
};
@@ -0,0 +1,9 @@
import { ViewSortDirection } from '~/generated/graphql';
export const convertViewSortDirectionToCore = (
viewSortDirection: string,
): ViewSortDirection => {
return viewSortDirection === 'asc'
? ViewSortDirection.ASC
: ViewSortDirection.DESC;
};
@@ -0,0 +1,5 @@
import { ViewType } from '~/generated-metadata/graphql';
export const convertViewTypeToCore = (viewType: string): ViewType => {
return viewType === 'kanban' ? ViewType.KANBAN : ViewType.TABLE;
};
@@ -85,7 +85,7 @@ export const ViewPickerListContent = () => {
Promise.all(
viewsReordered.map(async (view, index) => {
if (view.position !== index) {
await updateView({ ...view, position: index });
await updateView({ id: view.id, position: index });
}
}),
);
@@ -46,11 +46,6 @@ describe('shouldDisplayRawJsonByDefault', () => {
const defaultValue = '{"key1": "value1", "key2":}';
expect(shouldDisplayRawJsonByDefault(defaultValue)).toBe(true);
});
it('should return true for plain text', () => {
const defaultValue = 'This is just plain text';
expect(shouldDisplayRawJsonByDefault(defaultValue)).toBe(true);
});
});
});
@@ -2,7 +2,7 @@ import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { type ViewFilterOperand as SharedViewFilterOperand } from 'twenty-shared/types';
import { DataSource, type QueryRunner, Repository } from 'typeorm';
import { DataSource, Repository, type QueryRunner } from 'typeorm';
import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
@@ -17,6 +17,7 @@ import { ViewGroup } from 'src/engine/core-modules/view/entities/view-group.enti
import { ViewSort } from 'src/engine/core-modules/view/entities/view-sort.entity';
import { View } from 'src/engine/core-modules/view/entities/view.entity';
import { type ViewFilterGroupLogicalOperator } from 'src/engine/core-modules/view/enums/view-filter-group-logical-operator';
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 { type ViewSortDirection } from 'src/engine/core-modules/view/enums/view-sort-direction';
import { ViewType } from 'src/engine/core-modules/view/enums/view-type.enum';
@@ -29,7 +30,7 @@ import { type ViewGroupWorkspaceEntity } from 'src/modules/view/standard-objects
import { type ViewSortWorkspaceEntity } from 'src/modules/view/standard-objects/view-sort.workspace-entity';
import { type ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
import { convertViewFilterOperandToCoreOperand } from 'src/modules/view/utils/convert-view-filter-operand-to-core-operand.util';
import { transformViewFilterWorkspaceValueToCoreValue } from 'src/modules/view/utils/transform-view-filter-workspace-value-to-core-value';
import { convertViewFilterWorkspaceValueToCoreValue } from 'src/modules/view/utils/convert-view-filter-workspace-value-to-core-value';
@Command({
name: 'migrate:views-to-core',
@@ -257,7 +258,10 @@ export class MigrateViewsToCoreCommand extends ActiveOrSuspendedWorkspacesMigrat
name: viewName,
objectMetadataId: workspaceView.objectMetadataId,
type: workspaceView.type === 'table' ? ViewType.TABLE : ViewType.KANBAN,
key: workspaceView.key,
key:
workspaceView.key === 'INDEX' || workspaceView.key === ViewKey.INDEX
? ViewKey.INDEX
: null,
icon: workspaceView.icon,
position: workspaceView.position,
isCompact: workspaceView.isCompact,
@@ -328,7 +332,7 @@ export class MigrateViewsToCoreCommand extends ActiveOrSuspendedWorkspacesMigrat
operand: convertViewFilterOperandToCoreOperand(
filter.operand as SharedViewFilterOperand,
),
value: transformViewFilterWorkspaceValueToCoreValue(filter.value),
value: convertViewFilterWorkspaceValueToCoreValue(filter.value),
viewFilterGroupId: filter.viewFilterGroupId,
workspaceId,
createdAt: new Date(filter.createdAt),
@@ -0,0 +1,25 @@
import { type MigrationInterface, type QueryRunner } from 'typeorm';
export class MigrateViewKeyFromStringToEnum1755189372929
implements MigrationInterface
{
name = 'MigrateViewKeyFromStringToEnum1755189372929';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "core"."view" DROP COLUMN "key"`);
await queryRunner.query(
`CREATE TYPE "core"."view_key_enum" AS ENUM('INDEX')`,
);
await queryRunner.query(
`ALTER TABLE "core"."view" ADD "key" "core"."view_key_enum"`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "core"."view" DROP COLUMN "key"`);
await queryRunner.query(`DROP TYPE "core"."view_key_enum"`);
await queryRunner.query(
`ALTER TABLE "core"."view" ADD "key" text DEFAULT 'INDEX'`,
);
}
}
@@ -376,8 +376,6 @@ export class ObjectRecordsToGraphqlConnectionHelper {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private formatFieldValue(value: any, fieldType: FieldMetadataType) {
switch (fieldType) {
case FieldMetadataType.RAW_JSON:
return value ? JSON.stringify(value) : value;
case FieldMetadataType.DATE:
case FieldMetadataType.DATE_TIME:
return value instanceof Date ? value.toISOString() : value;
@@ -3,7 +3,6 @@ import { BigIntScalarType } from './big-int.scalar';
import { CursorScalarType } from './cursor.scalar';
import { DateScalarType } from './date.scalar';
import { PositionScalarType } from './position.scalar';
import { RawJSONScalar } from './raw-json.scalar';
import { TimeScalarType } from './time.scalar';
import { TSVectorScalarType } from './ts-vector.scalar';
import { UUIDScalarType } from './uuid.scalar';
@@ -13,7 +12,6 @@ export * from './big-int.scalar';
export * from './cursor.scalar';
export * from './date.scalar';
export * from './position.scalar';
export * from './raw-json.scalar';
export * from './time.scalar';
export * from './ts-vector.scalar';
export * from './uuid.scalar';
@@ -26,6 +24,5 @@ export const scalars = [
UUIDScalarType,
CursorScalarType,
PositionScalarType,
RawJSONScalar,
TSVectorScalarType,
];
@@ -1,81 +0,0 @@
import { GraphQLScalarType } from 'graphql';
import { type Maybe } from 'graphql-yoga';
import { type ObjMap } from 'graphql/jsutils/ObjMap';
import { type ASTNode, Kind, type ValueNode } from 'graphql/language';
import { ValidationError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
const parseLiteral = (
ast: ValueNode,
variables?: Maybe<ObjMap<unknown>>,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
): any => {
switch (ast.kind) {
case Kind.STRING:
case Kind.BOOLEAN:
return ast.value;
case Kind.INT:
case Kind.FLOAT:
return parseFloat(ast.value);
case Kind.OBJECT:
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return parseObject(ast as any, variables);
case Kind.LIST:
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (ast as any).values.map((n: ValueNode) =>
parseLiteral(n, variables),
);
case Kind.NULL:
return null;
case Kind.VARIABLE:
return variables ? variables[ast.name.value] : undefined;
default:
throw new ValidationError(
`JSONStringify cannot represent value: ${JSON.stringify(ast)}`,
);
}
};
const parseObject = (
ast: ASTNode,
variables?: Maybe<ObjMap<unknown>>,
): object => {
const value = Object.create(null);
if ('fields' in ast) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ast.fields?.forEach((field: any) => {
value[field.name.value] = parseLiteral(field.value, variables);
});
}
return value;
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const stringify = (value: any): string => {
return JSON.stringify(value);
};
const parseJSON = (value: string): object => {
try {
return JSON.parse(value);
} catch {
throw new ValidationError(`Value is not valid JSON: ${value}`);
}
};
export const RawJSONScalar = new GraphQLScalarType({
name: 'RawJSONScalar',
description:
'The `RawJSONScalar` scalar type represents JSON values, but stringifies inputs and parses outputs.',
serialize: parseJSON,
parseValue: stringify,
parseLiteral: (ast, variables) => {
if (ast.kind === Kind.STRING) {
return stringify(ast.value);
} else {
return stringify(parseLiteral(ast, variables));
}
},
});
@@ -13,6 +13,7 @@ import {
GraphQLString,
type GraphQLType,
} from 'graphql';
import GraphQLJSON from 'graphql-type-json';
import { FieldMetadataType } from 'twenty-shared/types';
import {
@@ -41,7 +42,6 @@ import {
UUIDScalarType,
} from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { PositionScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars/position.scalar';
import { RawJSONScalar } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars/raw-json.scalar';
import { getNumberFilterType } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-number-filter-type.util';
import { getNumberScalarType } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-number-scalar-type.util';
@@ -87,7 +87,7 @@ export class TypeMapperService {
],
[FieldMetadataType.NUMERIC, BigFloatScalarType],
[FieldMetadataType.POSITION, PositionScalarType],
[FieldMetadataType.RAW_JSON, RawJSONScalar],
[FieldMetadataType.RAW_JSON, GraphQLJSON],
[
FieldMetadataType.ARRAY,
StringArrayScalarType as unknown as GraphQLScalarType,
@@ -1,5 +1,5 @@
import { t } from '@lingui/core/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { isArray, isNonEmptyString } from '@sniptt/guards';
import {
type CountryCallingCode,
parsePhoneNumberWithError,
@@ -8,7 +8,6 @@ import {
getCountryCodesForCallingCode,
isDefined,
isValidCountryCode,
parseJson,
removeUndefinedFields,
} from 'twenty-shared/utils';
@@ -24,7 +23,7 @@ import {
export type PhonesFieldGraphQLInput =
| Partial<
Omit<PhonesMetadata, 'additionalPhones'> & {
additionalPhones: string | null;
additionalPhones: Partial<AdditionalPhoneMetadata>[];
}
>
| null
@@ -195,15 +194,12 @@ export const transformPhonesValue = ({
number: primary.primaryPhoneNumber,
});
const parsedAdditionalPhones = isDefined(additionalPhones)
? parseJson<AdditionalPhoneMetadata[]>(additionalPhones)
: additionalPhones;
const transformedAdditionalPhones = isDefined(parsedAdditionalPhones)
? JSON.stringify(parsedAdditionalPhones.map(validateAndInferPhoneInput))
: parsedAdditionalPhones;
const parsedAdditionalPhones = isArray(additionalPhones)
? additionalPhones
: [];
return removeUndefinedFields({
additionalPhones: transformedAdditionalPhones,
additionalPhones: parsedAdditionalPhones.map(validateAndInferPhoneInput),
primaryPhoneCallingCode,
primaryPhoneCountryCode,
primaryPhoneNumber,
@@ -20,11 +20,10 @@ import { ViewFieldRestApiExceptionFilter } from 'src/engine/core-modules/view/fi
import { ViewFieldService } from 'src/engine/core-modules/view/services/view-field.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { JwtAuthGuard } from 'src/engine/guards/jwt-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
@Controller('rest/metadata/viewFields')
@UseGuards(JwtAuthGuard, WorkspaceAuthGuard)
@UseGuards(WorkspaceAuthGuard)
@UseFilters(ViewFieldRestApiExceptionFilter)
export class ViewFieldController {
constructor(private readonly viewFieldService: ViewFieldService) {}
@@ -20,11 +20,10 @@ import { ViewFilterGroupRestApiExceptionFilter } from 'src/engine/core-modules/v
import { ViewFilterGroupService } from 'src/engine/core-modules/view/services/view-filter-group.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { JwtAuthGuard } from 'src/engine/guards/jwt-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
@Controller('rest/metadata/viewFilterGroups')
@UseGuards(JwtAuthGuard, WorkspaceAuthGuard)
@UseGuards(WorkspaceAuthGuard)
@UseFilters(ViewFilterGroupRestApiExceptionFilter)
export class ViewFilterGroupController {
constructor(
@@ -20,11 +20,10 @@ import { ViewFilterRestApiExceptionFilter } from 'src/engine/core-modules/view/f
import { ViewFilterService } from 'src/engine/core-modules/view/services/view-filter.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { JwtAuthGuard } from 'src/engine/guards/jwt-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
@Controller('rest/metadata/viewFilters')
@UseGuards(JwtAuthGuard, WorkspaceAuthGuard)
@UseGuards(WorkspaceAuthGuard)
@UseFilters(ViewFilterRestApiExceptionFilter)
export class ViewFilterController {
constructor(private readonly viewFilterService: ViewFilterService) {}
@@ -20,11 +20,10 @@ import { ViewGroupRestApiExceptionFilter } from 'src/engine/core-modules/view/fi
import { ViewGroupService } from 'src/engine/core-modules/view/services/view-group.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { JwtAuthGuard } from 'src/engine/guards/jwt-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
@Controller('rest/metadata/viewGroups')
@UseGuards(JwtAuthGuard, WorkspaceAuthGuard)
@UseGuards(WorkspaceAuthGuard)
@UseFilters(ViewGroupRestApiExceptionFilter)
export class ViewGroupController {
constructor(private readonly viewGroupService: ViewGroupService) {}
@@ -20,11 +20,10 @@ import { ViewSortRestApiExceptionFilter } from 'src/engine/core-modules/view/fil
import { ViewSortService } from 'src/engine/core-modules/view/services/view-sort.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { JwtAuthGuard } from 'src/engine/guards/jwt-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
@Controller('rest/metadata/viewSorts')
@UseGuards(JwtAuthGuard, WorkspaceAuthGuard)
@UseGuards(WorkspaceAuthGuard)
@UseFilters(ViewSortRestApiExceptionFilter)
export class ViewSortController {
constructor(private readonly viewSortService: ViewSortService) {}
@@ -20,11 +20,10 @@ import { ViewRestApiExceptionFilter } from 'src/engine/core-modules/view/filters
import { ViewService } from 'src/engine/core-modules/view/services/view.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { JwtAuthGuard } from 'src/engine/guards/jwt-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
@Controller('rest/metadata/views')
@UseGuards(JwtAuthGuard, WorkspaceAuthGuard)
@UseGuards(WorkspaceAuthGuard)
@UseFilters(ViewRestApiExceptionFilter)
export class ViewController {
constructor(private readonly viewService: ViewService) {}
@@ -1,7 +1,8 @@
import { Field, InputType } from '@nestjs/graphql';
import GraphQLJSON from 'graphql-type-json';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { RawJSONScalar } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars/raw-json.scalar';
import { ViewFilterOperand } from 'src/engine/core-modules/view/enums/view-filter-operand';
import { ViewFilterValue } from 'src/engine/core-modules/view/types/view-filter-value.type';
@@ -13,7 +14,7 @@ export class CreateViewFilterInput {
@Field({ nullable: true, defaultValue: ViewFilterOperand.CONTAINS })
operand?: ViewFilterOperand;
@Field(() => RawJSONScalar, { nullable: false })
@Field(() => GraphQLJSON, { nullable: false })
value: ViewFilterValue;
@Field(() => UUIDScalarType, { nullable: true })
@@ -2,6 +2,7 @@ import { Field, InputType } from '@nestjs/graphql';
import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
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';
@@ -16,8 +17,8 @@ export class CreateViewInput {
@Field(() => ViewType, { nullable: true, defaultValue: ViewType.TABLE })
type?: ViewType;
@Field({ nullable: true, defaultValue: 'INDEX' })
key?: string;
@Field(() => ViewKey, { nullable: true })
key?: ViewKey;
@Field({ nullable: false })
icon: string;
@@ -1,11 +1,9 @@
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
import { IDField } from '@ptc-org/nestjs-query-graphql';
import GraphQLJSON from 'graphql-type-json';
import {
RawJSONScalar,
UUIDScalarType,
} from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { ViewFilterOperand } from 'src/engine/core-modules/view/enums/view-filter-operand';
import { ViewFilterValue } from 'src/engine/core-modules/view/types/view-filter-value.type';
@@ -24,7 +22,7 @@ export class ViewFilterDTO {
@Field({ nullable: false, defaultValue: ViewFilterOperand.CONTAINS })
operand: ViewFilterOperand;
@Field(() => RawJSONScalar, { nullable: false })
@Field(() => GraphQLJSON, { nullable: false })
value: ViewFilterValue;
@Field(() => UUIDScalarType, { nullable: true })
@@ -4,11 +4,13 @@ import { IDField } from '@ptc-org/nestjs-query-graphql';
import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
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';
registerEnumType(ViewOpenRecordIn, { name: 'ViewOpenRecordIn' });
registerEnumType(ViewType, { name: 'ViewType' });
registerEnumType(ViewKey, { name: 'ViewKey' });
@ObjectType('CoreView')
export class ViewDTO {
@@ -24,8 +26,8 @@ export class ViewDTO {
@Field(() => ViewType, { nullable: false, defaultValue: ViewType.TABLE })
type: ViewType;
@Field({ nullable: true, defaultValue: 'INDEX' })
key: string;
@Field(() => ViewKey, { nullable: true, defaultValue: ViewKey.INDEX })
key: ViewKey | null;
@Field({ nullable: false })
icon: string;
@@ -20,6 +20,7 @@ import { ViewFilterGroup } from 'src/engine/core-modules/view/entities/view-filt
import { ViewFilter } from 'src/engine/core-modules/view/entities/view-filter.entity';
import { ViewGroup } from 'src/engine/core-modules/view/entities/view-group.entity';
import { ViewSort } from 'src/engine/core-modules/view/entities/view-sort.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 { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
@@ -48,8 +49,13 @@ export class View {
})
type: ViewType;
@Column({ nullable: true, type: 'text', default: 'INDEX' })
key: string;
@Column({
type: 'enum',
enum: Object.values(ViewKey),
nullable: true,
default: null,
})
key: ViewKey | null;
@Column({ nullable: false, type: 'text' })
icon: string;
@@ -0,0 +1,3 @@
export enum ViewKey {
INDEX = 'INDEX',
}
@@ -16,7 +16,17 @@ import { type I18nContext } from 'src/engine/core-modules/i18n/types/i18n-contex
import { generateMessageId } from 'src/engine/core-modules/i18n/utils/generateMessageId';
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 { ViewFieldDTO } from 'src/engine/core-modules/view/dtos/view-field.dto';
import { ViewFilterGroupDTO } from 'src/engine/core-modules/view/dtos/view-filter-group.dto';
import { ViewFilterDTO } from 'src/engine/core-modules/view/dtos/view-filter.dto';
import { ViewGroupDTO } from 'src/engine/core-modules/view/dtos/view-group.dto';
import { ViewSortDTO } from 'src/engine/core-modules/view/dtos/view-sort.dto';
import { ViewDTO } from 'src/engine/core-modules/view/dtos/view.dto';
import { ViewFieldService } from 'src/engine/core-modules/view/services/view-field.service';
import { ViewFilterGroupService } from 'src/engine/core-modules/view/services/view-filter-group.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 { ViewSortService } from 'src/engine/core-modules/view/services/view-sort.service';
import { ViewService } from 'src/engine/core-modules/view/services/view.service';
import { ViewGraphqlApiExceptionFilter } from 'src/engine/core-modules/view/utils/view-graphql-api-exception.filter';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
@@ -30,6 +40,11 @@ import { resolveObjectMetadataStandardOverride } from 'src/engine/metadata-modul
export class ViewResolver {
constructor(
private readonly viewService: ViewService,
private readonly viewFieldService: ViewFieldService,
private readonly viewFilterService: ViewFilterService,
private readonly viewFilterGroupService: ViewFilterGroupService,
private readonly viewGroupService: ViewGroupService,
private readonly viewSortService: ViewSortService,
private readonly i18nService: I18nService,
) {}
@@ -152,4 +167,44 @@ export class ViewResolver {
return isDefined(deletedView);
}
@ResolveField(() => [ViewFieldDTO])
async viewFields(
@Parent() view: ViewDTO,
@AuthWorkspace() workspace: Workspace,
) {
return this.viewFieldService.findByViewId(workspace.id, view.id);
}
@ResolveField(() => [ViewFilterDTO])
async viewFilters(
@Parent() view: ViewDTO,
@AuthWorkspace() workspace: Workspace,
) {
return this.viewFilterService.findByViewId(workspace.id, view.id);
}
@ResolveField(() => [ViewFilterGroupDTO])
async viewFilterGroups(
@Parent() view: ViewDTO,
@AuthWorkspace() workspace: Workspace,
) {
return this.viewFilterGroupService.findByViewId(workspace.id, view.id);
}
@ResolveField(() => [ViewSortDTO])
async viewSorts(
@Parent() view: ViewDTO,
@AuthWorkspace() workspace: Workspace,
) {
return this.viewSortService.findByViewId(workspace.id, view.id);
}
@ResolveField(() => [ViewGroupDTO])
async viewGroups(
@Parent() view: ViewDTO,
@AuthWorkspace() workspace: Workspace,
) {
return this.viewGroupService.findByViewId(workspace.id, view.id);
}
}
@@ -6,6 +6,8 @@ export type RelationFilterValue = {
export type ViewFilterValue =
| string
| string[]
| boolean
| number
| RelationFilterValue
| Record<string, unknown>
| null
@@ -1,7 +1,6 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
import { I18nModule } from 'src/engine/core-modules/i18n/i18n.module';
import { ViewFieldController } from 'src/engine/core-modules/view/controllers/view-field.controller';
import { ViewFilterGroupController } from 'src/engine/core-modules/view/controllers/view-filter-group.controller';
@@ -36,7 +35,6 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/
[View, ViewField, ViewFilter, ViewFilterGroup, ViewGroup, ViewSort],
'core',
),
AuthModule,
I18nModule,
WorkspaceCacheStorageModule,
WorkspaceMetadataCacheModule,
@@ -24,6 +24,18 @@ import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-p
import { PostgresCredentials } from 'src/engine/core-modules/postgres-credentials/postgres-credentials.entity';
import { WorkspaceSSOIdentityProvider } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { ViewFieldDTO } from 'src/engine/core-modules/view/dtos/view-field.dto';
import { ViewFilterGroupDTO } from 'src/engine/core-modules/view/dtos/view-filter-group.dto';
import { ViewFilterDTO } from 'src/engine/core-modules/view/dtos/view-filter.dto';
import { ViewGroupDTO } from 'src/engine/core-modules/view/dtos/view-group.dto';
import { ViewSortDTO } from 'src/engine/core-modules/view/dtos/view-sort.dto';
import { ViewDTO } from 'src/engine/core-modules/view/dtos/view.dto';
import { ViewField } from 'src/engine/core-modules/view/entities/view-field.entity';
import { ViewFilterGroup } from 'src/engine/core-modules/view/entities/view-filter-group.entity';
import { ViewFilter } from 'src/engine/core-modules/view/entities/view-filter.entity';
import { ViewGroup } from 'src/engine/core-modules/view/entities/view-group.entity';
import { ViewSort } from 'src/engine/core-modules/view/entities/view-sort.entity';
import { View } from 'src/engine/core-modules/view/entities/view.entity';
import { Webhook } from 'src/engine/core-modules/webhook/webhook.entity';
import { AgentHandoffEntity } from 'src/engine/metadata-modules/agent/agent-handoff.entity';
import { AgentEntity } from 'src/engine/metadata-modules/agent/agent.entity';
@@ -144,6 +156,33 @@ export class Workspace {
@OneToMany(() => ApiKey, (apiKey) => apiKey.workspace)
apiKeys: Relation<ApiKey[]>;
@Field(() => [ViewDTO], { nullable: true })
@OneToMany(() => View, (view) => view.workspace)
views: Relation<View[]>;
@Field(() => [ViewFieldDTO], { nullable: true })
@OneToMany(() => ViewField, (viewField) => viewField.workspace)
viewFields: Relation<ViewField[]>;
@Field(() => [ViewFilterDTO], { nullable: true })
@OneToMany(() => ViewFilter, (viewFilter) => viewFilter.workspace)
viewFilters: Relation<ViewFilter[]>;
@Field(() => [ViewFilterGroupDTO], { nullable: true })
@OneToMany(
() => ViewFilterGroup,
(viewFilterGroup) => viewFilterGroup.workspace,
)
viewFilterGroups: Relation<ViewFilterGroup[]>;
@Field(() => [ViewGroupDTO], { nullable: true })
@OneToMany(() => ViewGroup, (viewGroup) => viewGroup.workspace)
viewGroups: Relation<ViewGroup[]>;
@Field(() => [ViewSortDTO], { nullable: true })
@OneToMany(() => ViewSort, (viewSort) => viewSort.workspace)
viewSorts: Relation<ViewSort[]>;
@Field()
@Column({ default: 1 })
metadataVersion: number;

Some files were not shown because too many files have changed in this diff Show More