Compare commits

...

16 Commits

Author SHA1 Message Date
Charles Bochet 4e0943d5d7 Fix view update 2025-09-12 23:46:48 +02:00
Charles Bochet f473984460 Fix on view filters (#14462)
Fixes https://github.com/twentyhq/twenty/issues/14058
2025-09-12 22:48:18 +02:00
Charles Bochet 288a120b23 Fix missing relation column on tasks / notes table (#14402)
Closes https://github.com/twentyhq/twenty/issues/13484



https://github.com/user-attachments/assets/4800b25b-956d-4681-beeb-23263041fccc
2025-09-12 22:47:58 +02:00
Charles Bochet 9828f27bbd Fix View picker dropdown placement (#14457)
Fixes https://github.com/twentyhq/twenty/issues/14445

## What

In the current `MenuItemWithOptionDropdown` component (which is a
MenuItem with the 3 dots dropdown), we used to have a position: static
on the 3 dots hovered.
This enabled the dropdown to not move when we scroll the MenuItems.

However, this is not playing well with the dropdown autoplacement.
I'm removing it as I think the right solution would actually to prevent
the scroll when the dropdown is open but this is non straight forward
and not really a big issue. I'm fine with the dropdown being "fixed" to
the scrollable content, it also makes sense

Before:


https://github.com/user-attachments/assets/8efec8fa-430a-408e-b549-fd7433b7a38d

After:


https://github.com/user-attachments/assets/b8ee4375-0944-4008-9ab6-f7f9f1d67e9c



## Testing

I was considering adding a story here but this is hard to test: hover +
scroll behavior are not well supported, I don't think it worth the
investment especially as I think the vision is to block the scroll

This componenent is used in ViewPicker and MultiItemsInput (ex.
PhonesFieldInput). I have check that both were still working well
2025-09-12 22:47:51 +02:00
Marie f0f12a0643 Limit rest api relations depth to 1 (#14453)
Until we tackle [Implement relations depth 2 for rest
api](https://github.com/twentyhq/twenty/issues/14452), let's remove the
depth 2 query parameter which is not working (times out all the time).
2025-09-12 22:47:42 +02:00
Marie a747833366 Fix Relation display (many side) (#14411)
Fixes https://github.com/twentyhq/twenty/issues/14280.

Imitated impementation of MultiSelectFieldDisplay.
2025-09-12 22:47:30 +02:00
Charles Bochet 50a94f5659 1.5.3 tag 2025-09-10 16:23:00 +02:00
Charles Bochet 38f6ca2afe Implement ViewGroups optimistic rendering (#14388)
# Context

We have recently migrated from workspace.views to core.views. While
doing it, we've lost the optimistic rendering on views in frontend. This
is an issue for viewField and viewGroups that are persisted without
having an intermediate storing layer (viewFilters, viewSorts,
viewFilterGroups are not directly persisted when we do the changes,
therefore we have an underlying layer to keep them and the optimistic
was implemented there already).

ViewFields have been treated in a previous PR, this PR focus on
ViewGroups

## What
Optimistic on ViewGroups

## Other 
+ fix a bug in the sequencing to persist viewFilter + viewFilterGroups
2025-09-10 16:10:25 +02:00
Charles Bochet 2accf0e0c7 Fix view advanced filters broken (#14387)
## Context

We recently migrated from workspaceSchema.views to core.views. While
doing it we've migrated views using 1-5-migrate-views-to-core command
but we forgot to migrate the subFieldName

Closes: https://github.com/twentyhq/twenty/issues/14369
2025-09-10 16:10:15 +02:00
Abdul Rahman f32f1bc1ff fix: agents query runs even when AI feature flag is disabled (#14372) 2025-09-10 16:09:49 +02:00
Charles Bochet 698dc9585f Rename prefetchViewStates in coreViewStates (#14373)
View system used to rely on workspaceSchema views and was loaded in what
we called the "prefetch". We have recently migrated views to
core.coreViews and the states should now be named after coreViews

Fixes: https://github.com/twentyhq/twenty/issues/14349
2025-09-10 16:08:15 +02:00
Etienne fbd4ceb2ca Upsert - fixes (#14358)
- add more explicit error message on upsert conflicts
- increase MAX_RECORDS_QUERY constant
- add soft deleted records when returning upserted records




fixes https://github.com/twentyhq/private-issues/issues/300
2025-09-10 16:06:56 +02:00
Thomas Trompette b1a827e66e Remove is branch enabled feature flag (#14357)
As title
2025-09-10 16:06:33 +02:00
Charles Bochet ac1cfba3b2 DevXP improvements on new views (#14330) 2025-09-10 16:06:06 +02:00
Charles Bochet 914f286a1f Messaging cleaning fixes (#14345) 2025-09-08 00:26:07 +02:00
Charles Bochet 1d9a1c69e4 Improve Messaging Gmail experience (#14342)
In this PR, I'm solving several issues:
1) We were not checking if the currentWorkspaceMember was owning the
message in the thread. It kind of worked before because the case of
shared threads (with shared threads visibility restriction) was not
happening that often. It seems that the bug has always been there
2) Re-implement orphan messages and threads deletion on messageChannel
deletion. We used to brutally look for all orphans, we disabled it last
week because it was too heavy on db. I've re-implemented it more
carefully and "surgically"
3) Gmail sync was not handling folder synced correctly. It was
leveraging labelIds which it shouldn't do (this is a AND AND parameter)
in full sync
4) Added a command to clean orphan message threads manually if needed.
Usually this is done when you remove a messageChannel, or change
blocklist rules but it can be useful to have it to debug
2025-09-07 21:16:28 +02:00
134 changed files with 1602 additions and 1103 deletions
@@ -1003,7 +1003,6 @@ export enum FeatureFlagKey {
IS_RELATION_CONNECT_ENABLED = 'IS_RELATION_CONNECT_ENABLED',
IS_STRIPE_INTEGRATION_ENABLED = 'IS_STRIPE_INTEGRATION_ENABLED',
IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED',
IS_WORKFLOW_BRANCH_ENABLED = 'IS_WORKFLOW_BRANCH_ENABLED',
IS_WORKFLOW_ITERATOR_ENABLED = 'IS_WORKFLOW_ITERATOR_ENABLED',
IS_WORKSPACE_MIGRATION_V2_ENABLED = 'IS_WORKSPACE_MIGRATION_V2_ENABLED'
}
@@ -3509,9 +3508,7 @@ export type UpdateViewInput = {
isCompact?: InputMaybe<Scalars['Boolean']>;
kanbanAggregateOperation?: InputMaybe<AggregateOperations>;
kanbanAggregateOperationFieldMetadataId?: InputMaybe<Scalars['UUID']>;
key?: InputMaybe<ViewKey>;
name?: InputMaybe<Scalars['String']>;
objectMetadataId?: InputMaybe<Scalars['UUID']>;
openRecordIn?: InputMaybe<ViewOpenRecordIn>;
position?: InputMaybe<Scalars['Float']>;
type?: InputMaybe<ViewType>;
@@ -967,7 +967,6 @@ export enum FeatureFlagKey {
IS_RELATION_CONNECT_ENABLED = 'IS_RELATION_CONNECT_ENABLED',
IS_STRIPE_INTEGRATION_ENABLED = 'IS_STRIPE_INTEGRATION_ENABLED',
IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED',
IS_WORKFLOW_BRANCH_ENABLED = 'IS_WORKFLOW_BRANCH_ENABLED',
IS_WORKFLOW_ITERATOR_ENABLED = 'IS_WORKFLOW_ITERATOR_ENABLED',
IS_WORKSPACE_MIGRATION_V2_ENABLED = 'IS_WORKSPACE_MIGRATION_V2_ENABLED'
}
@@ -3347,9 +3346,7 @@ export type UpdateViewInput = {
isCompact?: InputMaybe<Scalars['Boolean']>;
kanbanAggregateOperation?: InputMaybe<AggregateOperations>;
kanbanAggregateOperationFieldMetadataId?: InputMaybe<Scalars['UUID']>;
key?: InputMaybe<ViewKey>;
name?: InputMaybe<Scalars['String']>;
objectMetadataId?: InputMaybe<Scalars['UUID']>;
openRecordIn?: InputMaybe<ViewOpenRecordIn>;
position?: InputMaybe<Scalars['Float']>;
type?: InputMaybe<ViewType>;
@@ -12,12 +12,10 @@ import { DATABASE_TRIGGER_TYPES } from '@/workflow/workflow-trigger/constants/Da
import { OTHER_TRIGGER_TYPES } from '@/workflow/workflow-trigger/constants/OtherTriggerTypes';
import { useUpdateWorkflowVersionTrigger } from '@/workflow/workflow-trigger/hooks/useUpdateWorkflowVersionTrigger';
import { getTriggerDefaultDefinition } from '@/workflow/workflow-trigger/utils/getTriggerDefaultDefinition';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useTheme } from '@emotion/react';
import { TRIGGER_STEP_ID } from 'twenty-shared/workflow';
import { useIcons } from 'twenty-ui/display';
import { MenuItemCommand } from 'twenty-ui/navigation';
import { FeatureFlagKey } from '~/generated/graphql';
export const CommandMenuWorkflowSelectTriggerTypeContent = ({
workflow,
@@ -35,10 +33,6 @@ export const CommandMenuWorkflowSelectTriggerTypeContent = ({
);
const { openWorkflowEditStepInCommandMenu } = useWorkflowCommandMenu();
const isWorkflowBranchEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_WORKFLOW_BRANCH_ENABLED,
);
const handleTriggerTypeClick = ({
type,
defaultLabel,
@@ -54,7 +48,6 @@ export const CommandMenuWorkflowSelectTriggerTypeContent = ({
defaultLabel,
type,
activeNonSystemObjectMetadataItems,
steps: !isWorkflowBranchEnabled ? workflow.currentVersion.steps : [],
}),
);
@@ -2,9 +2,9 @@ import { MainContextStoreProviderEffect } from '@/context-store/components/MainC
import { useIsSettingsPage } from '@/navigation/hooks/useIsSettingsPage';
import { useLastVisitedView } from '@/navigation/hooks/useLastVisitedView';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { prefetchIndexViewIdFromObjectMetadataItemFamilySelector } from '@/prefetch/states/selector/prefetchIndexViewIdFromObjectMetadataItemFamilySelector';
import { AppPath } from '@/types/AppPath';
import { useShowAuthModal } from '@/ui/layout/hooks/useShowAuthModal';
import { coreIndexViewIdFromObjectMetadataItemFamilySelector } from '@/views/states/selectors/coreIndexViewIdFromObjectMetadataItemFamilySelector';
import { useLocation, useParams, useSearchParams } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
@@ -60,7 +60,7 @@ export const MainContextStoreProvider = () => {
);
const indexViewId = useRecoilValue(
prefetchIndexViewIdFromObjectMetadataItemFamilySelector({
coreIndexViewIdFromObjectMetadataItemFamilySelector({
objectMetadataItemId: objectMetadataItem?.id ?? '',
}),
);
@@ -6,8 +6,8 @@ import { getViewType } from '@/context-store/utils/getViewType';
import { useSetLastVisitedObjectMetadataId } from '@/navigation/hooks/useSetLastVisitedObjectMetadataId';
import { useSetLastVisitedViewForObjectMetadataNamePlural } from '@/navigation/hooks/useSetLastVisitedViewForObjectMetadataNamePlural';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { prefetchViewFromViewIdFamilySelector } from '@/prefetch/states/selector/prefetchViewFromViewIdFamilySelector';
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
import { coreViewFromViewIdFamilySelector } from '@/views/states/selectors/coreViewFromViewIdFamilySelector';
import { useEffect } from 'react';
import { useRecoilValue } from 'recoil';
@@ -53,7 +53,7 @@ export const MainContextStoreProviderEffect = ({
);
const view = useRecoilValue(
prefetchViewFromViewIdFamilySelector({
coreViewFromViewIdFamilySelector({
viewId: viewId ?? '',
}),
);
@@ -2,13 +2,13 @@ import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainCo
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { lastVisitedViewPerObjectMetadataItemState } from '@/navigation/states/lastVisitedViewPerObjectMetadataItemState';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { prefetchViewsFromObjectMetadataItemFamilySelector } from '@/prefetch/states/selector/prefetchViewsFromObjectMetadataItemFamilySelector';
import { AppPath } from '@/types/AppPath';
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
import { NavigationDrawerItemsCollapsableContainer } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemsCollapsableContainer';
import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem';
import { getNavigationSubItemLeftAdornment } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemLeftAdornment';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { coreViewsFromObjectMetadataItemFamilySelector } from '@/views/states/selectors/coreViewsFromObjectMetadataItemFamilySelector';
import { useLocation } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
import { useIcons } from 'twenty-ui/display';
@@ -23,7 +23,7 @@ export const NavigationDrawerItemForObjectMetadataItem = ({
objectMetadataItem,
}: NavigationDrawerItemForObjectMetadataItemProps) => {
const views = useRecoilValue(
prefetchViewsFromObjectMetadataItemFamilySelector({
coreViewsFromObjectMetadataItemFamilySelector({
objectMetadataItemId: objectMetadataItem.id,
}),
);
@@ -1,4 +1,5 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { isActiveFieldMetadataItem } from '@/object-metadata/utils/isActiveFieldMetadataItem';
import { useMemo } from 'react';
export const useActiveFieldMetadataItems = ({
@@ -10,7 +11,11 @@ export const useActiveFieldMetadataItems = ({
() =>
objectMetadataItem
? objectMetadataItem.readableFields.filter(
({ isActive, isSystem }) => isActive && !isSystem,
({ isActive, isSystem, name }) =>
isActiveFieldMetadataItem({
objectNameSingular: objectMetadataItem.nameSingular,
fieldMetadata: { isActive, isSystem, name },
}),
)
: [],
[objectMetadataItem],
@@ -0,0 +1,43 @@
import { isActiveFieldMetadataItem } from '@/object-metadata/utils/isActiveFieldMetadataItem';
describe('isActiveFieldMetadataItem', () => {
it('should return false for inactive fields', () => {
const res = isActiveFieldMetadataItem({
fieldMetadata: { isActive: false, isSystem: false, name: 'fieldName' },
objectNameSingular: 'objectNameSingular',
});
expect(res).toBe(false);
});
it('should return true for active fields', () => {
const res = isActiveFieldMetadataItem({
fieldMetadata: { isActive: true, isSystem: false, name: 'fieldName' },
objectNameSingular: 'objectNameSingular',
});
expect(res).toBe(true);
});
it('should return false for system fields', () => {
const res = isActiveFieldMetadataItem({
fieldMetadata: { isActive: true, isSystem: true, name: 'fieldName' },
objectNameSingular: 'objectNameSingular',
});
expect(res).toBe(false);
});
it('should return true for note targets', () => {
const res = isActiveFieldMetadataItem({
fieldMetadata: { isActive: true, isSystem: false, name: 'noteTargets' },
objectNameSingular: 'note',
});
expect(res).toBe(true);
});
it('should return true for task targets', () => {
const res = isActiveFieldMetadataItem({
fieldMetadata: { isActive: true, isSystem: false, name: 'taskTargets' },
objectNameSingular: 'task',
});
expect(res).toBe(true);
});
});
@@ -0,0 +1,31 @@
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { type FieldMetadataItem } from '../types/FieldMetadataItem';
type IsFieldMetadataAvailableForViewFieldArgs = {
objectNameSingular: string;
fieldMetadata: Pick<FieldMetadataItem, 'name' | 'isSystem' | 'isActive'>;
};
export const isActiveFieldMetadataItem = ({
objectNameSingular,
fieldMetadata,
}: IsFieldMetadataAvailableForViewFieldArgs) => {
if (fieldMetadata.isActive === false) {
return false;
}
if (
(objectNameSingular === CoreObjectNameSingular.Note &&
fieldMetadata.name === 'noteTargets') ||
(objectNameSingular === CoreObjectNameSingular.Task &&
fieldMetadata.name === 'taskTargets')
) {
return true;
}
if (fieldMetadata.isSystem === true) {
return false;
}
return true;
};
@@ -4,12 +4,14 @@ import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { useLoadRecordIndexStates } from '@/object-record/record-index/hooks/useLoadRecordIndexStates';
import { recordIndexViewTypeState } from '@/object-record/record-index/states/recordIndexViewTypeState';
import { prefetchViewFromViewIdFamilySelector } from '@/prefetch/states/selector/prefetchViewFromViewIdFamilySelector';
import { usePersistViewGroupRecords } from '@/views/hooks/internal/usePersistViewGroupRecords';
import { useUpdateCurrentView } from '@/views/hooks/useUpdateCurrentView';
import { coreViewsState } from '@/views/states/coreViewState';
import { type GraphQLView } from '@/views/types/GraphQLView';
import { type ViewGroup } from '@/views/types/ViewGroup';
import { ViewType, viewTypeIconMapping } from '@/views/types/ViewType';
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
import { convertViewTypeToCore } from '@/views/utils/convertViewTypeToCore';
import { useGetAvailableFieldsForKanban } from '@/views/view-picker/hooks/useGetAvailableFieldsForKanban';
import { useCallback } from 'react';
import { useRecoilCallback, useSetRecoilState } from 'recoil';
@@ -63,7 +65,7 @@ export const useSetViewTypeFromLayoutOptionsMenu = () => {
);
const setAndPersistViewType = useRecoilCallback(
({ snapshot }) =>
({ snapshot, set }) =>
async (viewType: ViewType) => {
const currentViewId = snapshot
.getLoadable(
@@ -73,18 +75,24 @@ export const useSetViewTypeFromLayoutOptionsMenu = () => {
)
.getValue();
const existingCoreViews = snapshot
.getLoadable(coreViewsState)
.getValue();
if (!isDefined(currentViewId)) {
throw new Error('No view id found');
}
const currentView = snapshot
.getLoadable(
prefetchViewFromViewIdFamilySelector({ viewId: currentViewId }),
)
.getValue();
if (!isDefined(currentView)) {
const currentCoreView = existingCoreViews.find(
(coreView) => coreView.id === currentViewId,
);
if (!isDefined(currentCoreView)) {
throw new Error('No current view found');
}
const currentView = convertCoreViewToView(currentCoreView);
const updateCurrentViewParams: Partial<GraphQLView> = {};
updateCurrentViewParams.type = viewType;
@@ -105,6 +113,15 @@ export const useSetViewTypeFromLayoutOptionsMenu = () => {
);
}
setRecordIndexViewType(viewType);
set(coreViewsState, [
...existingCoreViews.filter(
(coreView) => coreView.id !== currentView.id,
),
{
...currentCoreView,
type: convertViewTypeToCore(viewType),
},
]);
if (shouldChangeIcon(currentView.icon, currentView.type)) {
updateCurrentViewParams.icon =
@@ -114,6 +131,16 @@ export const useSetViewTypeFromLayoutOptionsMenu = () => {
}
case ViewType.Table:
setRecordIndexViewType(viewType);
set(coreViewsState, [
...existingCoreViews.filter(
(coreView) => coreView.id !== currentView.id,
),
{
...currentCoreView,
type: convertViewTypeToCore(viewType),
},
]);
if (shouldChangeIcon(currentView.icon, currentView.type)) {
updateCurrentViewParams.icon =
viewTypeIconMapping(viewType).displayName;
@@ -19,11 +19,11 @@ import { getRecordFieldCardRelationPickerDropdownId } from '@/object-record/reco
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { prefetchIndexViewIdFromObjectMetadataItemFamilySelector } from '@/prefetch/states/selector/prefetchIndexViewIdFromObjectMetadataItemFamilySelector';
import { AppPath } from '@/types/AppPath';
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { coreIndexViewIdFromObjectMetadataItemFamilySelector } from '@/views/states/selectors/coreIndexViewIdFromObjectMetadataItemFamilySelector';
import { useLingui } from '@lingui/react/macro';
import { ViewFilterOperand } from 'twenty-shared/types';
import { RelationType } from '~/generated-metadata/graphql';
@@ -82,7 +82,7 @@ export const RecordDetailRelationSection = ({
);
const indexViewId = useRecoilValue(
prefetchIndexViewIdFromObjectMetadataItemFamilySelector({
coreIndexViewIdFromObjectMetadataItemFamilySelector({
objectMetadataItemId: relationObjectMetadataItem.id,
}),
);
@@ -26,7 +26,7 @@ export const useChangeRecordFieldVisibility = (
const { saveViewFields } = useSaveCurrentViewFields();
const changeRecordFieldVisibility = ({
const changeRecordFieldVisibility = async ({
fieldMetadataId,
isVisible,
}: {
@@ -56,7 +56,7 @@ export const useChangeRecordFieldVisibility = (
upsertRecordField(recordFieldToUpsert);
saveViewFields([mapRecordFieldToViewField(recordFieldToUpsert)]);
await saveViewFields([mapRecordFieldToViewField(recordFieldToUpsert)]);
} else {
updateRecordField(fieldMetadataId, {
isVisible: shouldShowFieldMetadataItem,
@@ -20,7 +20,7 @@ export const useMoveRecordField = (recordTableId?: string) => {
// because otherwise it will just do nothing while moving left and right of non visible record fields
const moveRecordField = useRecoilCallback(
({ snapshot }) =>
({
async ({
direction,
fieldMetadataItemIdToMove,
}: {
@@ -73,7 +73,7 @@ export const useMoveRecordField = (recordTableId?: string) => {
position: currentRecordFieldNewPosition,
});
saveViewFields([
await saveViewFields([
mapRecordFieldToViewField({
...targetRecordField,
position: targetRecordFieldNewPosition,
@@ -1,4 +1,5 @@
import { flattenedReadableFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedReadableFieldMetadataItemIdsSelector';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { isActiveFieldMetadataItem } from '@/object-metadata/utils/isActiveFieldMetadataItem';
import { RecordFieldsComponentInstanceContext } from '@/object-record/record-field/states/context/RecordFieldsComponentInstanceContext';
import { currentRecordFieldsComponentState } from '@/object-record/record-field/states/currentRecordFieldsComponentState';
import { createComponentSelector } from '@/ui/utilities/state/component-state/utils/createComponentSelector';
@@ -16,20 +17,41 @@ export const visibleRecordFieldsComponentSelector = createComponentSelector({
}),
);
const readableFieldMetadataItems = get(
flattenedReadableFieldMetadataItemsSelector,
);
const objectMetadataItems = get(objectMetadataItemsState);
const filteredVisibleAndReadableRecordFields = currentRecordFields.filter(
(recordFieldToFilter) =>
recordFieldToFilter.isVisible === true &&
readableFieldMetadataItems.some(
(fieldMetadataItemToFilter) =>
fieldMetadataItemToFilter.id ===
recordFieldToFilter.fieldMetadataItemId &&
fieldMetadataItemToFilter.isActive === true &&
fieldMetadataItemToFilter.isSystem !== true,
),
(recordFieldToFilter) => {
if (!recordFieldToFilter.isVisible) {
return false;
}
const objectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.fields.some(
(fieldMetadataItem) =>
fieldMetadataItem.id ===
recordFieldToFilter.fieldMetadataItemId,
),
);
if (!objectMetadataItem) {
return false;
}
const fieldMetadataItem = objectMetadataItem.fields.find(
(fieldMetadataItem) =>
fieldMetadataItem.id === recordFieldToFilter.fieldMetadataItemId,
);
if (!fieldMetadataItem) {
return false;
}
return isActiveFieldMetadataItem({
objectNameSingular: objectMetadataItem.nameSingular,
fieldMetadata: fieldMetadataItem,
});
},
);
return [...filteredVisibleAndReadableRecordFields].sort(
@@ -8,9 +8,20 @@ import { useFieldFocus } from '@/object-record/record-field/ui/hooks/useFieldFoc
import { useRelationFromManyFieldDisplay } from '@/object-record/record-field/ui/meta-types/hooks/useRelationFromManyFieldDisplay';
import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList';
import styled from '@emotion/styled';
import { useContext } from 'react';
import { isDefined } from 'twenty-shared/utils';
const StyledContainer = styled.div`
align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
justify-content: flex-start;
max-width: 100%;
overflow: hidden;
width: 100%;
`;
export const RelationFromManyFieldDisplay = () => {
const { fieldValue, fieldDefinition, generateRecordChipData } =
useRelationFromManyFieldDisplay();
@@ -51,7 +62,7 @@ export const RelationFromManyFieldDisplay = () => {
const relationFieldName = fieldName === 'noteTargets' ? 'note' : 'task';
return (
return isFocused ? (
<ExpandableList isChipCountDisplayed={isFocused}>
{fieldValue
.map((record) => {
@@ -69,6 +80,24 @@ export const RelationFromManyFieldDisplay = () => {
})
.filter(isDefined)}
</ExpandableList>
) : (
<StyledContainer>
{fieldValue
.map((record) => {
if (!isDefined(record) || !isDefined(record[relationFieldName])) {
return undefined;
}
return (
<RecordChip
key={record.id}
objectNameSingular={objectNameSingular}
record={record[relationFieldName]}
forceDisableClick={disableChipClick}
/>
);
})
.filter(isDefined)}
</StyledContainer>
);
} else if (isRelationFromActivityTargets) {
return (
@@ -1,8 +1,8 @@
import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow';
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { useLoadRecordIndexStates } from '@/object-record/record-index/hooks/useLoadRecordIndexStates';
import { prefetchViewFromViewIdFamilySelector } from '@/prefetch/states/selector/prefetchViewFromViewIdFamilySelector';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { coreViewFromViewIdFamilySelector } from '@/views/states/selectors/coreViewFromViewIdFamilySelector';
import { useEffect, useState } from 'react';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
@@ -18,7 +18,7 @@ export const RecordIndexLoadBaseOnContextStoreEffect = () => {
);
const view = useRecoilValue(
prefetchViewFromViewIdFamilySelector({
coreViewFromViewIdFamilySelector({
viewId: contextStoreCurrentViewId ?? '',
}),
);
@@ -110,7 +110,12 @@ export const useHandleRecordGroupField = () => {
}
if (viewGroupsToDelete.length > 0) {
await deleteViewGroupRecords(viewGroupsToDelete);
await deleteViewGroupRecords(
viewGroupsToDelete.map((group) => ({
id: group.id,
viewId: view.id,
})),
);
}
},
[
@@ -144,7 +149,12 @@ export const useHandleRecordGroupField = () => {
return;
}
await deleteViewGroupRecords(view.viewGroups);
await deleteViewGroupRecords(
view.viewGroups.map((group) => ({
id: group.id,
viewId: view.id,
})),
);
setRecordGroupsFromViewGroups(view.id, [], objectMetadataItem);
},
@@ -17,7 +17,7 @@ export const useMoveTableColumn = ({ recordTableId }: useRecordTableProps) => {
async (direction: 'left' | 'right', fieldMetadataItemId: string) => {
unfocusRecordTableCell();
moveRecordField({
await moveRecordField({
direction: direction === 'left' ? 'before' : 'after',
fieldMetadataItemIdToMove: fieldMetadataItemId,
});
@@ -98,9 +98,9 @@ export const RecordTableColumnHeadDropdownMenu = ({
moveTableColumn('right', recordField.fieldMetadataItemId);
};
const handleColumnVisibility = () => {
const handleColumnVisibility = async () => {
closeDropdownAndToggleScroll();
changeRecordFieldVisibility({
await changeRecordFieldVisibility({
fieldMetadataId: recordField.fieldMetadataItemId,
isVisible: false,
});
@@ -170,7 +170,7 @@ export const RecordTableColumnHeadDropdownMenu = ({
{canHide && (
<MenuItem
LeftIcon={IconEyeOff}
onClick={handleColumnVisibility}
onClick={async () => await handleColumnVisibility()}
text={t`Hide`}
/>
)}
@@ -31,9 +31,11 @@ export const RecordTableHeaderPlusButtonContent = () => {
useChangeRecordFieldVisibility(recordTableId);
const handleAddColumn = useCallback(
(column: Pick<ColumnDefinition<FieldMetadata>, 'fieldMetadataId'>) => {
async (
column: Pick<ColumnDefinition<FieldMetadata>, 'fieldMetadataId'>,
) => {
closeDropdown();
changeRecordFieldVisibility({ ...column, isVisible: true });
await changeRecordFieldVisibility({ ...column, isVisible: true });
},
[changeRecordFieldVisibility, closeDropdown],
);
@@ -60,8 +62,8 @@ export const RecordTableHeaderPlusButtonContent = () => {
{availableFieldMetadataItemsToShow.map((fieldMetadataItem) => (
<MenuItem
key={fieldMetadataItem.id}
onClick={() =>
handleAddColumn({
onClick={async () =>
await handleAddColumn({
fieldMetadataId: fieldMetadataItem.id,
})
}
@@ -1,19 +0,0 @@
import { coreViewsState } from '@/views/states/coreViewState';
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
import { selectorFamily } from 'recoil';
export const prefetchViewIdsFromObjectMetadataItemFamilySelector =
selectorFamily<string[], { objectMetadataItemId: string }>({
key: 'prefetchViewIdsFromObjectMetadataItemFamilySelector',
get:
({ objectMetadataItemId }) =>
({ get }) => {
const coreViews = get(coreViewsState);
const views = coreViews.map(convertCoreViewToView);
return views
.filter((view) => view.objectMetadataId === objectMetadataItemId)
.map((view) => view.id);
},
});
@@ -54,7 +54,7 @@ export const SettingsRoleAssignment = ({
const { addApiKeyToRoleAndUpdateState, updateApiKeyRoleDraftState } =
useUpdateApiKeyRole(roleId);
const { data: agentsData } = useFindManyAgentsQuery();
const { data: agentsData } = useFindManyAgentsQuery({ skip: !isAiEnabled });
const { data: apiKeysData } = useGetApiKeysQuery();
const { openModal, closeModal } = useModal();
@@ -1,11 +1,11 @@
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { anyFieldFilterValueComponentState } from '@/object-record/record-filter/states/anyFieldFilterValueComponentState';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { prefetchViewFromViewIdFamilySelector } from '@/prefetch/states/selector/prefetchViewFromViewIdFamilySelector';
import { useRecoilComponentFamilyState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import { hasInitializedAnyFieldFilterComponentFamilyState } from '@/views/states/hasInitializedAnyFieldFilterComponentFamilyState';
import { coreViewFromViewIdFamilySelector } from '@/views/states/selectors/coreViewFromViewIdFamilySelector';
import { useEffect } from 'react';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
@@ -18,7 +18,7 @@ export const ViewBarAnyFieldFilterEffect = () => {
const { objectMetadataItem } = useRecordIndexContextOrThrow();
const currentView = useRecoilValue(
prefetchViewFromViewIdFamilySelector({
coreViewFromViewIdFamilySelector({
viewId: currentViewId ?? '',
}),
);
@@ -1,11 +1,11 @@
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { currentRecordFieldsComponentState } from '@/object-record/record-field/states/currentRecordFieldsComponentState';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { prefetchViewFromViewIdFamilySelector } from '@/prefetch/states/selector/prefetchViewFromViewIdFamilySelector';
import { useRecoilComponentFamilyState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import { hasInitializedCurrentRecordFieldsComponentFamilyState } from '@/views/states/hasInitializedCurrentRecordFieldsComponentFamilyState';
import { coreViewFromViewIdFamilySelector } from '@/views/states/selectors/coreViewFromViewIdFamilySelector';
import { mapViewFieldToRecordField } from '@/views/utils/mapViewFieldToRecordField';
import { useEffect } from 'react';
import { useRecoilValue } from 'recoil';
@@ -19,7 +19,7 @@ export const ViewBarRecordFieldEffect = () => {
const { objectMetadataItem } = useRecordIndexContextOrThrow();
const currentView = useRecoilValue(
prefetchViewFromViewIdFamilySelector({
coreViewFromViewIdFamilySelector({
viewId: currentViewId ?? '',
}),
);
@@ -1,12 +1,12 @@
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { prefetchViewFromViewIdFamilySelector } from '@/prefetch/states/selector/prefetchViewFromViewIdFamilySelector';
import { useRecoilComponentFamilyState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import { useMapViewFiltersToFilters } from '@/views/hooks/useMapViewFiltersToFilters';
import { hasInitializedCurrentRecordFiltersComponentFamilyState } from '@/views/states/hasInitializedCurrentRecordFiltersComponentFamilyState';
import { coreViewFromViewIdFamilySelector } from '@/views/states/selectors/coreViewFromViewIdFamilySelector';
import { useEffect } from 'react';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
@@ -19,7 +19,7 @@ export const ViewBarRecordFilterEffect = () => {
const { objectMetadataItem } = useRecordIndexContextOrThrow();
const currentView = useRecoilValue(
prefetchViewFromViewIdFamilySelector({
coreViewFromViewIdFamilySelector({
viewId: currentViewId ?? '',
}),
);
@@ -1,11 +1,11 @@
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { prefetchViewFromViewIdFamilySelector } from '@/prefetch/states/selector/prefetchViewFromViewIdFamilySelector';
import { useRecoilComponentFamilyState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import { hasInitializedCurrentRecordFilterGroupsComponentFamilyState } from '@/views/states/hasInitializedCurrentRecordFilterGroupsComponentFamilyState';
import { coreViewFromViewIdFamilySelector } from '@/views/states/selectors/coreViewFromViewIdFamilySelector';
import { mapViewFilterGroupsToRecordFilterGroups } from '@/views/utils/mapViewFilterGroupsToRecordFilterGroups';
import { useEffect } from 'react';
import { useRecoilValue } from 'recoil';
@@ -19,7 +19,7 @@ export const ViewBarRecordFilterGroupEffect = () => {
const { objectMetadataItem } = useRecordIndexContextOrThrow();
const currentView = useRecoilValue(
prefetchViewFromViewIdFamilySelector({
coreViewFromViewIdFamilySelector({
viewId: currentViewId ?? '',
}),
);
@@ -1,11 +1,11 @@
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
import { prefetchViewFromViewIdFamilySelector } from '@/prefetch/states/selector/prefetchViewFromViewIdFamilySelector';
import { useRecoilComponentFamilyState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import { hasInitializedCurrentRecordSortsComponentFamilyState } from '@/views/states/hasInitializedCurrentRecordSortsComponentFamilyState';
import { coreViewFromViewIdFamilySelector } from '@/views/states/selectors/coreViewFromViewIdFamilySelector';
import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts';
import { useEffect } from 'react';
import { useRecoilValue } from 'recoil';
@@ -19,7 +19,7 @@ export const ViewBarRecordSortEffect = () => {
const { objectMetadataItem } = useRecordIndexContextOrThrow();
const currentView = useRecoilValue(
prefetchViewFromViewIdFamilySelector({
coreViewFromViewIdFamilySelector({
viewId: currentViewId ?? '',
}),
);
@@ -1,35 +1,21 @@
import { useCallback } from 'react';
import { v4 } from 'uuid';
import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect';
import { triggerUpdateRecordOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { CREATE_CORE_VIEW_FIELD } from '@/views/graphql/mutations/createCoreViewField';
import { UPDATE_CORE_VIEW_FIELD } from '@/views/graphql/mutations/updateCoreViewField';
import { useTriggerViewFieldOptimisticEffect } from '@/views/optimistic-effects/hooks/useTriggerViewFieldOptimisticEffect';
import { type GraphQLView } from '@/views/types/GraphQLView';
import { type ViewField } from '@/views/types/ViewField';
import { useApolloClient } from '@apollo/client';
import { isNull } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
import { type CoreViewField } from '~/generated/graphql';
export const usePersistViewFieldRecords = () => {
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular: CoreObjectNameSingular.ViewField,
});
const getRecordFromCache = useGetRecordFromCache({
objectNameSingular: CoreObjectNameSingular.ViewField,
});
const { objectMetadataItems } = useObjectMetadataItems();
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const apolloClient = useApolloClient();
const { triggerViewFieldOptimisticEffect } =
useTriggerViewFieldOptimisticEffect();
const createCoreViewFieldRecords = useCallback(
(
viewFieldsToCreate: Omit<ViewField, 'definition'>[],
@@ -50,28 +36,19 @@ export const usePersistViewFieldRecords = () => {
size: viewField.size,
} satisfies Partial<CoreViewField>,
},
update: (cache, { data }) => {
update: (_cache, { data }) => {
const record = data?.['createCoreViewField'];
if (!record) return;
triggerCreateRecordsOptimisticEffect({
cache,
objectMetadataItem,
recordsToCreate: [record],
objectMetadataItems,
objectPermissionsByObjectMetadataId,
triggerViewFieldOptimisticEffect({
createdViewFields: [record],
});
},
}),
),
);
},
[
apolloClient,
objectMetadataItem,
objectMetadataItems,
objectPermissionsByObjectMetadataId,
],
[apolloClient, triggerViewFieldOptimisticEffect],
);
const updateCoreViewFieldRecords = useCallback(
@@ -91,29 +68,19 @@ export const usePersistViewFieldRecords = () => {
aggregateOperation: viewField.aggregateOperation,
} satisfies Partial<CoreViewField>,
},
update: (cache, { data }) => {
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,
triggerViewFieldOptimisticEffect({
updatedViewFields: [record],
});
},
}),
),
);
},
[apolloClient, getRecordFromCache, objectMetadataItem, objectMetadataItems],
[apolloClient, triggerViewFieldOptimisticEffect],
);
return {
@@ -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, parseJson } from 'twenty-shared/utils';
import { isDefined } 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: parseJson(viewFilter.value),
value: viewFilter.value,
operand: convertViewFilterOperandToCore(viewFilter.operand),
positionInViewFilterGroup: viewFilter.positionInViewFilterGroup,
viewFilterGroupId: viewFilter.viewFilterGroupId,
@@ -3,8 +3,10 @@ import { useCallback } from 'react';
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 { useTriggerViewGroupOptimisticEffect } from '@/views/optimistic-effects/hooks/useTriggerViewGroupOptimisticEffect';
import { type ViewGroup } from '@/views/types/ViewGroup';
import { useApolloClient } from '@apollo/client';
import { type CoreViewGroup } from '~/generated/graphql';
type CreateViewGroupRecordsArgs = {
viewGroupsToCreate: ViewGroup[];
@@ -14,6 +16,9 @@ type CreateViewGroupRecordsArgs = {
export const usePersistViewGroupRecords = () => {
const apolloClient = useApolloClient();
const { triggerViewGroupOptimisticEffect } =
useTriggerViewGroupOptimisticEffect();
const createCoreViewGroupRecords = useCallback(
({ viewGroupsToCreate, viewId }: CreateViewGroupRecordsArgs) => {
if (viewGroupsToCreate.length === 0) return;
@@ -32,11 +37,19 @@ export const usePersistViewGroupRecords = () => {
position: viewGroup.position,
},
},
update: (_cache, { data }) => {
const record = data?.['createCoreViewGroup'];
if (!record) return;
triggerViewGroupOptimisticEffect({
createdViewGroups: [record],
});
},
}),
),
);
},
[apolloClient],
[apolloClient, triggerViewGroupOptimisticEffect],
);
const updateCoreViewGroupRecords = useCallback(
@@ -44,7 +57,7 @@ export const usePersistViewGroupRecords = () => {
if (!viewGroupsToUpdate.length) return;
const mutationPromises = viewGroupsToUpdate.map((viewGroup) =>
apolloClient.mutate<{ updateCoreViewGroup: ViewGroup }>({
apolloClient.mutate<{ updateCoreViewGroup: CoreViewGroup }>({
mutation: UPDATE_CORE_VIEW_GROUP,
variables: {
id: viewGroup.id,
@@ -55,35 +68,24 @@ export const usePersistViewGroupRecords = () => {
},
// Avoid cache being updated with stale data
fetchPolicy: 'no-cache',
update: (_cache, { data }) => {
const record = data?.['updateCoreViewGroup'];
if (!record) return;
triggerViewGroupOptimisticEffect({
updatedViewGroups: [record],
});
},
}),
);
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,
},
});
});
return Promise.all(mutationPromises);
},
[apolloClient],
[apolloClient, triggerViewGroupOptimisticEffect],
);
const deleteCoreViewGroupRecords = useCallback(
async (viewGroupsToDelete: ViewGroup[]) => {
async (viewGroupsToDelete: Pick<CoreViewGroup, 'id' | 'viewId'>[]) => {
if (!viewGroupsToDelete.length) return;
return Promise.all(
@@ -93,11 +95,16 @@ export const usePersistViewGroupRecords = () => {
variables: {
id: viewGroup.id,
},
update: () => {
triggerViewGroupOptimisticEffect({
deletedViewGroups: [viewGroup],
});
},
}),
),
);
},
[apolloClient],
[apolloClient, triggerViewGroupOptimisticEffect],
);
return {
@@ -1,8 +1,8 @@
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { anyFieldFilterValueComponentState } from '@/object-record/record-filter/states/anyFieldFilterValueComponentState';
import { prefetchViewFromViewIdFamilySelector } from '@/prefetch/states/selector/prefetchViewFromViewIdFamilySelector';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import { coreViewFromViewIdFamilySelector } from '@/views/states/selectors/coreViewFromViewIdFamilySelector';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
@@ -20,7 +20,7 @@ export const useApplyCurrentViewAnyFieldFilterToAnyFieldFilter = () => {
() => {
const currentView = snapshot
.getLoadable(
prefetchViewFromViewIdFamilySelector({
coreViewFromViewIdFamilySelector({
viewId: currentViewId ?? '',
}),
)
@@ -1,9 +1,9 @@
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { currentRecordFilterGroupsComponentState } from '@/object-record/record-filter-group/states/currentRecordFilterGroupsComponentState';
import { prefetchViewFromViewIdFamilySelector } from '@/prefetch/states/selector/prefetchViewFromViewIdFamilySelector';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import { coreViewFromViewIdFamilySelector } from '@/views/states/selectors/coreViewFromViewIdFamilySelector';
import { mapViewFilterGroupsToRecordFilterGroups } from '@/views/utils/mapViewFilterGroupsToRecordFilterGroups';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
@@ -28,7 +28,7 @@ export const useApplyCurrentViewFilterGroupsToCurrentRecordFilterGroups =
() => {
const currentView = snapshot
.getLoadable(
prefetchViewFromViewIdFamilySelector({
coreViewFromViewIdFamilySelector({
viewId: currentViewId ?? '',
}),
)
@@ -1,8 +1,8 @@
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { prefetchViewFromViewIdFamilySelector } from '@/prefetch/states/selector/prefetchViewFromViewIdFamilySelector';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import { coreViewFromViewIdFamilySelector } from '@/views/states/selectors/coreViewFromViewIdFamilySelector';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { useMapViewFiltersToFilters } from './useMapViewFiltersToFilters';
@@ -23,7 +23,7 @@ export const useApplyCurrentViewFiltersToCurrentRecordFilters = () => {
() => {
const currentView = snapshot
.getLoadable(
prefetchViewFromViewIdFamilySelector({
coreViewFromViewIdFamilySelector({
viewId: currentViewId ?? '',
}),
)
@@ -1,8 +1,8 @@
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
import { prefetchViewFromViewIdFamilySelector } from '@/prefetch/states/selector/prefetchViewFromViewIdFamilySelector';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import { coreViewFromViewIdFamilySelector } from '@/views/states/selectors/coreViewFromViewIdFamilySelector';
import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
@@ -13,7 +13,7 @@ export const useApplyCurrentViewSortsToCurrentRecordSorts = () => {
);
const currentView = useRecoilValue(
prefetchViewFromViewIdFamilySelector({
coreViewFromViewIdFamilySelector({
viewId: currentViewId ?? '',
}),
);
@@ -4,7 +4,6 @@ import { anyFieldFilterValueComponentState } from '@/object-record/record-filter
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
import { prefetchViewFromViewIdFamilySelector } from '@/prefetch/states/selector/prefetchViewFromViewIdFamilySelector';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
@@ -15,6 +14,7 @@ import { usePersistViewGroupRecords } from '@/views/hooks/internal/usePersistVie
import { usePersistViewSortRecords } from '@/views/hooks/internal/usePersistViewSortRecords';
import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId';
import { isPersistingViewFieldsState } from '@/views/states/isPersistingViewFieldsState';
import { coreViewFromViewIdFamilySelector } from '@/views/states/selectors/coreViewFromViewIdFamilySelector';
import { type GraphQLView } from '@/views/types/GraphQLView';
import { type ViewGroup } from '@/views/types/ViewGroup';
import { type ViewSort } from '@/views/types/ViewSort';
@@ -97,7 +97,7 @@ export const useCreateViewFromCurrentView = (viewBarComponentId?: string) => {
const sourceView = snapshot
.getLoadable(
prefetchViewFromViewIdFamilySelector({
coreViewFromViewIdFamilySelector({
viewId: currentViewId,
}),
)
@@ -1,5 +1,5 @@
import { prefetchViewFromViewIdFamilySelector } from '@/prefetch/states/selector/prefetchViewFromViewIdFamilySelector';
import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId';
import { coreViewFromViewIdFamilySelector } from '@/views/states/selectors/coreViewFromViewIdFamilySelector';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { useDeleteCoreViewMutation } from '~/generated/graphql';
@@ -14,7 +14,7 @@ export const useDeleteView = () => {
async (viewId: string) => {
const currentView = snapshot
.getLoadable(
prefetchViewFromViewIdFamilySelector({
coreViewFromViewIdFamilySelector({
viewId,
}),
)
@@ -1,6 +1,6 @@
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { prefetchViewFromViewIdFamilySelector } from '@/prefetch/states/selector/prefetchViewFromViewIdFamilySelector';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { coreViewFromViewIdFamilySelector } from '@/views/states/selectors/coreViewFromViewIdFamilySelector';
import { useRecoilValue } from 'recoil';
@@ -10,7 +10,7 @@ export const useGetCurrentViewOnly = () => {
);
const currentView = useRecoilValue(
prefetchViewFromViewIdFamilySelector({
coreViewFromViewIdFamilySelector({
viewId: currentViewId ?? '',
}),
);
@@ -1,4 +1,4 @@
import { prefetchViewFromViewIdFamilySelector } from '@/prefetch/states/selector/prefetchViewFromViewIdFamilySelector';
import { coreViewFromViewIdFamilySelector } from '@/views/states/selectors/coreViewFromViewIdFamilySelector';
import { useRecoilCallback } from 'recoil';
export const useGetViewFromPrefetchState = () => {
@@ -7,7 +7,7 @@ export const useGetViewFromPrefetchState = () => {
(viewId: string) => {
const view = snapshot
.getLoadable(
prefetchViewFromViewIdFamilySelector({
coreViewFromViewIdFamilySelector({
viewId: viewId,
}),
)
@@ -3,7 +3,7 @@ import { currentRecordFieldsComponentState } from '@/object-record/record-field/
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 { coreViewsByObjectMetadataIdFamilySelector } from '@/views/states/selectors/coreViewsByObjectMetadataIdFamilySelector';
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
import { getFilterableFieldsWithVectorSearch } from '@/views/utils/getFilterableFieldsWithVectorSearch';
@@ -16,8 +16,8 @@ export const useSaveCurrentViewFiltersAndSorts = () => {
const saveCurrentViewFilterAndSorts = async () => {
await saveRecordSortsToViewSorts();
await saveRecordFiltersToViewFilters();
await saveRecordFilterGroupsToViewFilterGroups();
await saveRecordFiltersToViewFilters();
await saveAnyFieldFilterToView();
};
@@ -1,9 +1,9 @@
import { useRecoilCallback } from 'recoil';
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { prefetchViewFromViewIdFamilySelector } from '@/prefetch/states/selector/prefetchViewFromViewIdFamilySelector';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId';
import { coreViewFromViewIdFamilySelector } from '@/views/states/selectors/coreViewFromViewIdFamilySelector';
import { type GraphQLView } from '@/views/types/GraphQLView';
import { convertUpdateViewInputToCore } from '@/views/utils/convertUpdateViewInputToCore';
import { isDefined } from 'twenty-shared/utils';
@@ -28,7 +28,7 @@ export const useUpdateCurrentView = () => {
const currentView = snapshot
.getLoadable(
prefetchViewFromViewIdFamilySelector({
coreViewFromViewIdFamilySelector({
viewId: currentViewId ?? '',
}),
)
@@ -2,6 +2,7 @@ import { useRecordIndexContextOrThrow } from '@/object-record/record-index/conte
import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId';
import { type GraphQLView } from '@/views/types/GraphQLView';
import { convertUpdateViewInputToCore } from '@/views/utils/convertUpdateViewInputToCore';
import { view } from 'framer-motion';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { useUpdateCoreViewMutation } from '~/generated/graphql';
@@ -1,5 +1,5 @@
import { prefetchIndexViewIdFromObjectMetadataItemFamilySelector } from '@/prefetch/states/selector/prefetchIndexViewIdFromObjectMetadataItemFamilySelector';
import { prefetchViewFromViewIdFamilySelector } from '@/prefetch/states/selector/prefetchViewFromViewIdFamilySelector';
import { coreIndexViewIdFromObjectMetadataItemFamilySelector } from '@/views/states/selectors/coreIndexViewIdFromObjectMetadataItemFamilySelector';
import { coreViewFromViewIdFamilySelector } from '@/views/states/selectors/coreViewFromViewIdFamilySelector';
import { useRecoilValue } from 'recoil';
export const useViewOrDefaultViewFromPrefetchedViews = ({
@@ -8,13 +8,13 @@ export const useViewOrDefaultViewFromPrefetchedViews = ({
objectMetadataItemId: string;
}) => {
const indexViewId = useRecoilValue(
prefetchIndexViewIdFromObjectMetadataItemFamilySelector({
coreIndexViewIdFromObjectMetadataItemFamilySelector({
objectMetadataItemId,
}),
);
const indexView = useRecoilValue(
prefetchViewFromViewIdFamilySelector({
coreViewFromViewIdFamilySelector({
viewId: indexViewId ?? '',
}),
);
@@ -0,0 +1,144 @@
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
import { coreViewsState } from '@/views/states/coreViewState';
import { type CoreViewWithRelations } from '@/views/types/CoreViewWithRelations';
import { useApolloClient } from '@apollo/client';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { type CoreViewField } from '~/generated/graphql';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
export const useTriggerViewFieldOptimisticEffect = () => {
const apolloClient = useApolloClient();
const cache = apolloClient.cache;
const triggerViewFieldOptimisticEffect = useRecoilCallback(
({ set, snapshot }) =>
({
createdViewFields = [],
updatedViewFields = [],
deletedViewFields = [],
}: {
createdViewFields?: CoreViewField[];
updatedViewFields?: CoreViewField[];
deletedViewFields?: Pick<CoreViewField, 'id' | 'viewId'>[];
}) => {
const coreViews = getSnapshotValue(snapshot, coreViewsState);
let newCoreViews = [...coreViews];
createdViewFields.forEach((createdViewField) => {
cache.modify<CoreViewWithRelations>({
id: cache.identify({
__typename: 'CoreView',
id: createdViewField.viewId,
}),
fields: {
viewFields: (existingViewFields, { toReference }) => [
...(existingViewFields ?? []),
toReference(createdViewField),
],
},
});
const toBeModifiedCoreView = newCoreViews.find(
(coreView) => coreView.id === createdViewField.viewId,
);
if (isDefined(toBeModifiedCoreView)) {
newCoreViews = [
...newCoreViews.filter(
(coreView) => coreView.id !== createdViewField.viewId,
),
{
...toBeModifiedCoreView,
viewFields: [
...toBeModifiedCoreView.viewFields,
createdViewField,
],
},
];
}
});
updatedViewFields.forEach((updatedViewField) => {
cache.modify<CoreViewWithRelations>({
id: cache.identify({
__typename: 'CoreView',
id: updatedViewField.viewId,
}),
fields: {
viewFields: (existingViewFields, { readField, toReference }) =>
existingViewFields.map((viewField) => {
const viewFieldId = readField<string>('id', viewField);
if (viewFieldId === updatedViewField.id) {
return toReference(updatedViewField);
}
return viewField;
}),
},
});
const toBeModifiedCoreView = newCoreViews.find(
(coreView) => coreView.id === updatedViewField.viewId,
);
if (isDefined(toBeModifiedCoreView)) {
newCoreViews = [
...newCoreViews.filter(
(coreView) => coreView.id !== updatedViewField.viewId,
),
{
...toBeModifiedCoreView,
viewFields: [
...toBeModifiedCoreView.viewFields.filter(
(viewField) => viewField.id !== updatedViewField.id,
),
updatedViewField,
],
},
];
}
});
deletedViewFields.forEach(
(deletedViewField: Pick<CoreViewField, 'id' | 'viewId'>) => {
cache.modify<CoreViewWithRelations>({
id: cache.identify({
__typename: 'CoreView',
id: deletedViewField.viewId,
}),
fields: {
viewFields: (existingViewFields, { readField }) =>
existingViewFields.filter(
(viewField) =>
readField('id', viewField) !== deletedViewField.id,
),
},
});
const toBeModifiedCoreView = newCoreViews.find(
(coreView) => coreView.id === deletedViewField.viewId,
);
if (isDefined(toBeModifiedCoreView)) {
newCoreViews = [
...newCoreViews.filter(
(coreView) => coreView.id !== deletedViewField.viewId,
),
{
...toBeModifiedCoreView,
viewFields: toBeModifiedCoreView.viewFields.filter(
(viewField) => viewField.id !== deletedViewField.id,
),
},
];
}
},
);
if (!isDeeplyEqual(coreViews, newCoreViews)) {
set(coreViewsState, newCoreViews);
}
},
[cache],
);
return {
triggerViewFieldOptimisticEffect,
};
};
@@ -0,0 +1,142 @@
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
import { coreViewsState } from '@/views/states/coreViewState';
import { type CoreViewWithRelations } from '@/views/types/CoreViewWithRelations';
import { useApolloClient } from '@apollo/client';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { type CoreViewGroup } from '~/generated/graphql';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
export const useTriggerViewGroupOptimisticEffect = () => {
const apolloClient = useApolloClient();
const cache = apolloClient.cache;
const triggerViewGroupOptimisticEffect = useRecoilCallback(
({ set, snapshot }) =>
({
createdViewGroups = [],
updatedViewGroups = [],
deletedViewGroups = [],
}: {
createdViewGroups?: CoreViewGroup[];
updatedViewGroups?: CoreViewGroup[];
deletedViewGroups?: Pick<CoreViewGroup, 'id' | 'viewId'>[];
}) => {
const coreViews = getSnapshotValue(snapshot, coreViewsState);
let newCoreViews = [...coreViews];
createdViewGroups.forEach((createdViewGroup) => {
cache.modify<CoreViewWithRelations>({
id: cache.identify({
__typename: 'CoreView',
id: createdViewGroup.viewId,
}),
fields: {
viewGroups: (existingViewGroups, { toReference }) => [
...(existingViewGroups ?? []),
toReference(createdViewGroup),
],
},
});
const toBeModifiedCoreView = newCoreViews.find(
(coreView) => coreView.id === createdViewGroup.viewId,
);
if (isDefined(toBeModifiedCoreView)) {
newCoreViews = [
...newCoreViews.filter(
(coreView) => coreView.id !== createdViewGroup.viewId,
),
{
...toBeModifiedCoreView,
viewGroups: [
...toBeModifiedCoreView.viewGroups,
createdViewGroup,
],
},
];
}
});
updatedViewGroups.forEach((updatedViewGroup) => {
cache.modify<CoreViewWithRelations>({
id: cache.identify({
__typename: 'CoreView',
id: updatedViewGroup.viewId,
}),
fields: {
viewGroups: (existingViewGroups, { readField, toReference }) =>
existingViewGroups.map((viewGroup) => {
const viewGroupId = readField<string>('id', viewGroup);
if (viewGroupId === updatedViewGroup.id) {
return toReference(updatedViewGroup);
}
return viewGroup;
}),
},
});
const toBeModifiedCoreView = newCoreViews.find(
(coreView) => coreView.id === updatedViewGroup.viewId,
);
if (isDefined(toBeModifiedCoreView)) {
newCoreViews = [
...newCoreViews.filter(
(coreView) => coreView.id !== updatedViewGroup.viewId,
),
{
...toBeModifiedCoreView,
viewGroups: [
...toBeModifiedCoreView.viewGroups.filter(
(viewGroup) => viewGroup.id !== updatedViewGroup.id,
),
updatedViewGroup,
],
},
];
}
});
deletedViewGroups.forEach((deletedViewGroup) => {
cache.modify<CoreViewWithRelations>({
id: cache.identify({
__typename: 'CoreView',
id: deletedViewGroup.viewId,
}),
fields: {
viewGroups: (existingViewGroups, { readField }) =>
existingViewGroups.filter(
(viewGroup) =>
readField('id', viewGroup) !== deletedViewGroup.id,
),
},
});
const toBeModifiedCoreView = newCoreViews.find(
(coreView) => coreView.id === deletedViewGroup.viewId,
);
if (isDefined(toBeModifiedCoreView)) {
newCoreViews = [
...newCoreViews.filter(
(coreView) => coreView.id !== deletedViewGroup.viewId,
),
{
...toBeModifiedCoreView,
viewGroups: toBeModifiedCoreView.viewGroups.filter(
(viewGroup) => viewGroup.id !== deletedViewGroup.id,
),
},
];
}
});
if (!isDeeplyEqual(coreViews, newCoreViews)) {
set(coreViewsState, newCoreViews);
}
},
[cache],
);
return {
triggerViewGroupOptimisticEffect,
};
};
@@ -3,9 +3,9 @@ import { ViewKey } from '@/views/types/ViewKey';
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
import { selectorFamily } from 'recoil';
export const prefetchIndexViewIdFromObjectMetadataItemFamilySelector =
export const coreIndexViewIdFromObjectMetadataItemFamilySelector =
selectorFamily<string | undefined, { objectMetadataItemId: string }>({
key: 'prefetchIndexViewIdFromObjectMetadataItemFamilySelector',
key: 'coreIndexViewIdFromObjectMetadataItemFamilySelector',
get:
({ objectMetadataItemId }) =>
({ get }) => {
@@ -3,11 +3,11 @@ import { type View } from '@/views/types/View';
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
import { selectorFamily } from 'recoil';
export const prefetchViewFromViewIdFamilySelector = selectorFamily<
export const coreViewFromViewIdFamilySelector = selectorFamily<
View | undefined,
{ viewId: string }
>({
key: 'prefetchViewFromViewIdFamilySelector',
key: 'coreViewFromViewIdFamilySelector',
get:
({ viewId }) =>
({ get }) => {
@@ -0,0 +1,21 @@
import { coreViewsState } from '@/views/states/coreViewState';
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
import { selectorFamily } from 'recoil';
export const coreViewIdsFromObjectMetadataItemFamilySelector = selectorFamily<
string[],
{ objectMetadataItemId: string }
>({
key: 'coreViewIdsFromObjectMetadataItemFamilySelector',
get:
({ objectMetadataItemId }) =>
({ get }) => {
const coreViews = get(coreViewsState);
const views = coreViews.map(convertCoreViewToView);
return views
.filter((view) => view.objectMetadataId === objectMetadataItemId)
.map((view) => view.id);
},
});
@@ -1,8 +1,8 @@
import { coreViewsState } from '@/views/states/coreViewState';
import { selector } from 'recoil';
export const prefetchViewLengthSelector = selector<number>({
key: 'prefetchViewLengthSelector',
export const coreViewLengthFamilySelector = selector<number>({
key: 'coreViewLengthFamilySelector',
get: ({ get }) => {
const coreViews = get(coreViewsState);
@@ -3,11 +3,11 @@ import { type View } from '@/views/types/View';
import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView';
import { selectorFamily } from 'recoil';
export const prefetchViewsFromObjectMetadataItemFamilySelector = selectorFamily<
export const coreViewsFromObjectMetadataItemFamilySelector = selectorFamily<
View[],
{ objectMetadataItemId: string }
>({
key: 'prefetchViewsFromObjectMetadataItemFamilySelector',
key: 'coreViewsFromObjectMetadataItemFamilySelector',
get:
({ objectMetadataItemId }) =>
({ get }) => {
@@ -3,36 +3,37 @@ 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';
import { type UpdateViewInput } from '~/generated/graphql';
export const convertUpdateViewInputToCore = (
view: Partial<GraphQLView & { __typename?: string }>,
): UpdateViewInput => {
const {
key,
openRecordIn,
type,
viewFields: _viewFields,
viewFilters: _viewFilters,
viewFilterGroups: _viewFilterGroups,
viewGroups: _viewGroups,
viewSorts: _viewSorts,
kanbanFieldMetadataId: _kanbanFieldMetadataId,
id: _id,
__typename: _typename,
...rest
} = view;
const convertedKey = isDefined(key) ? convertViewKeyToCore(key) : undefined;
const convertedOpenRecordIn = isDefined(openRecordIn)
? convertViewOpenRecordInToCore(openRecordIn)
const convertedKey = isDefined(view.key)
? convertViewKeyToCore(view.key)
: undefined;
const convertedType = isDefined(type)
? convertViewTypeToCore(type)
const convertedOpenRecordIn = isDefined(view.openRecordIn)
? convertViewOpenRecordInToCore(view.openRecordIn)
: undefined;
const convertedType = isDefined(view.type)
? convertViewTypeToCore(view.type)
: undefined;
return {
...rest,
id: view.id,
...(view.name && { name: view.name }),
...(view.icon && { icon: view.icon }),
...(view.position && { position: view.position }),
...(view.isCompact && { isCompact: view.isCompact }),
...(view.kanbanAggregateOperation && {
kanbanAggregateOperation: view.kanbanAggregateOperation,
}),
...(view.kanbanAggregateOperationFieldMetadataId && {
kanbanAggregateOperationFieldMetadataId:
view.kanbanAggregateOperationFieldMetadataId,
}),
...(view.anyFieldFilterValue && {
anyFieldFilterValue: view.anyFieldFilterValue,
}),
...(convertedKey && { key: convertedKey }),
...(convertedOpenRecordIn && { openRecordIn: convertedOpenRecordIn }),
...(convertedType && { type: convertedType }),
@@ -4,6 +4,7 @@ import { type RecordFilter } from '@/object-record/record-filter/types/RecordFil
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';
import { parseJson } from 'twenty-shared/utils';
export const getRecordFilterLabelValue = ({
recordFilter,
@@ -30,7 +31,7 @@ export const getRecordFilterLabelValue = ({
}
}
if (recordFilter.type === 'SELECT' || recordFilter.type === 'MULTI_SELECT') {
const valueArray = JSON.parse(recordFilter.value);
const valueArray = parseJson<string[]>(recordFilter.value);
if (!Array.isArray(valueArray)) {
return '';
@@ -1,10 +1,10 @@
import { useEffect } from 'react';
import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow';
import { prefetchViewsFromObjectMetadataItemFamilySelector } from '@/prefetch/states/selector/prefetchViewsFromObjectMetadataItemFamilySelector';
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import { coreViewsFromObjectMetadataItemFamilySelector } from '@/views/states/selectors/coreViewsFromObjectMetadataItemFamilySelector';
import { viewTypeIconMapping } from '@/views/types/ViewType';
import { useGetAvailableFieldsForKanban } from '@/views/view-picker/hooks/useGetAvailableFieldsForKanban';
import { useViewPickerMode } from '@/views/view-picker/hooks/useViewPickerMode';
@@ -48,7 +48,7 @@ export const ViewPickerContentEffect = () => {
const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow();
const viewsOnCurrentObject = useRecoilValue(
prefetchViewsFromObjectMetadataItemFamilySelector({
coreViewsFromObjectMetadataItemFamilySelector({
objectMetadataItemId: objectMetadataItem.id,
}),
);
@@ -1,8 +1,8 @@
import { FavoriteFolderPicker } from '@/favorites/favorite-folder-picker/components/FavoriteFolderPicker';
import { FavoriteFolderPickerEffect } from '@/favorites/favorite-folder-picker/components/FavoriteFolderPickerEffect';
import { FavoriteFolderPickerInstanceContext } from '@/favorites/favorite-folder-picker/states/context/FavoriteFolderPickerInstanceContext';
import { prefetchViewFromViewIdFamilySelector } from '@/prefetch/states/selector/prefetchViewFromViewIdFamilySelector';
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
import { coreViewFromViewIdFamilySelector } from '@/views/states/selectors/coreViewFromViewIdFamilySelector';
import { VIEW_PICKER_DROPDOWN_ID } from '@/views/view-picker/constants/ViewPickerDropdownId';
import { viewPickerReferenceViewIdComponentState } from '@/views/view-picker/states/viewPickerReferenceViewIdComponentState';
import { useRecoilValue } from 'recoil';
@@ -13,7 +13,7 @@ export const ViewPickerFavoriteFoldersDropdown = () => {
);
const view = useRecoilValue(
prefetchViewFromViewIdFamilySelector({
coreViewFromViewIdFamilySelector({
viewId: viewPickerReferenceViewId ?? '',
}),
);
@@ -3,7 +3,6 @@ import { type DropResult } from '@hello-pangea/dnd';
import { type MouseEvent, useCallback } from 'react';
import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow';
import { prefetchViewsFromObjectMetadataItemFamilySelector } from '@/prefetch/states/selector/prefetchViewsFromObjectMetadataItemFamilySelector';
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableList';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
@@ -15,6 +14,7 @@ import { useChangeView } from '@/views/hooks/useChangeView';
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
import { useOpenCreateViewDropdown } from '@/views/hooks/useOpenCreateViewDropown';
import { useUpdateView } from '@/views/hooks/useUpdateView';
import { coreViewsFromObjectMetadataItemFamilySelector } from '@/views/states/selectors/coreViewsFromObjectMetadataItemFamilySelector';
import { ViewPickerOptionDropdown } from '@/views/view-picker/components/ViewPickerOptionDropdown';
import { VIEW_PICKER_DROPDOWN_ID } from '@/views/view-picker/constants/ViewPickerDropdownId';
import { useViewPickerMode } from '@/views/view-picker/hooks/useViewPickerMode';
@@ -35,7 +35,7 @@ export const ViewPickerListContent = () => {
const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow();
const viewsOnCurrentObject = useRecoilValue(
prefetchViewsFromObjectMetadataItemFamilySelector({
coreViewsFromObjectMetadataItemFamilySelector({
objectMetadataItemId: objectMetadataItem.id,
}),
);
@@ -1,12 +1,12 @@
import { useRecoilCallback, useRecoilValue } from 'recoil';
import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow';
import { prefetchViewsFromObjectMetadataItemFamilySelector } from '@/prefetch/states/selector/prefetchViewsFromObjectMetadataItemFamilySelector';
import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackState';
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
import { useChangeView } from '@/views/hooks/useChangeView';
import { useDeleteView } from '@/views/hooks/useDeleteView';
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
import { coreViewsFromObjectMetadataItemFamilySelector } from '@/views/states/selectors/coreViewsFromObjectMetadataItemFamilySelector';
import { useCloseAndResetViewPicker } from '@/views/view-picker/hooks/useCloseAndResetViewPicker';
import { viewPickerIsDirtyComponentState } from '@/views/view-picker/states/viewPickerIsDirtyComponentState';
import { viewPickerIsPersistingComponentState } from '@/views/view-picker/states/viewPickerIsPersistingComponentState';
@@ -34,7 +34,7 @@ export const useDeleteViewFromCurrentState = (viewBarInstanceId?: string) => {
const { objectMetadataItem } = useContextStoreObjectMetadataItemOrThrow();
const viewsOnCurrentObject = useRecoilValue(
prefetchViewsFromObjectMetadataItemFamilySelector({
coreViewsFromObjectMetadataItemFamilySelector({
objectMetadataItemId: objectMetadataItem.id,
}),
);
@@ -14,11 +14,9 @@ import { workflowRunDiagramAutomaticallyOpenedStepsComponentState } from '@/work
import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/states/workflowSelectedNodeComponentState';
import { generateWorkflowRunDiagram } from '@/workflow/workflow-diagram/utils/generateWorkflowRunDiagram';
import { getWorkflowNodeIconKey } from '@/workflow/workflow-diagram/utils/getWorkflowNodeIconKey';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { useIcons } from 'twenty-ui/display';
import { FeatureFlagKey } from '~/generated/graphql';
export const useRunWorkflowRunOpeningInCommandMenuSideEffects = () => {
const apolloCoreClient = useApolloCoreClient();
@@ -27,10 +25,6 @@ export const useRunWorkflowRunOpeningInCommandMenuSideEffects = () => {
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const isWorkflowBranchEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_WORKFLOW_BRANCH_ENABLED,
);
const runWorkflowRunOpeningInCommandMenuSideEffects = useRecoilCallback(
({ snapshot, set }) =>
({
@@ -62,7 +56,6 @@ export const useRunWorkflowRunOpeningInCommandMenuSideEffects = () => {
steps: workflowRunRecord.state.flow.steps,
stepInfos: workflowRunRecord.state.stepInfos,
trigger: workflowRunRecord.state.flow.trigger,
isWorkflowBranchEnabled,
});
if (!isDefined(stepToOpenByDefault)) {
@@ -132,7 +125,6 @@ export const useRunWorkflowRunOpeningInCommandMenuSideEffects = () => {
[
apolloCoreClient.cache,
objectPermissionsByObjectMetadataId,
isWorkflowBranchEnabled,
openWorkflowRunViewStepInCommandMenu,
getIcon,
],
@@ -5,7 +5,6 @@ import { useRecoilComponentCallbackState } from '@/ui/utilities/state/component-
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
import { WorkflowDiagramCustomMarkers } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramCustomMarkers';
import { WorkflowDiagramRightClickCommandMenu } from '@/workflow/workflow-diagram/components/WorkflowDiagramRightClickCommandMenu';
import { useRightDrawerState } from '@/workflow/workflow-diagram/hooks/useRightDrawerState';
import { workflowDiagramComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramComponentState';
@@ -19,9 +18,10 @@ import {
type WorkflowDiagramNode,
type WorkflowDiagramNodeType,
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { getOrganizedDiagram } from '@/workflow/workflow-diagram/utils/getOrganizedDiagram';
import { WorkflowDiagramConnection } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramConnection';
import { WorkflowDiagramCustomMarkers } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramCustomMarkers';
import { useEdgeState } from '@/workflow/workflow-diagram/workflow-edges/hooks/useEdgeState';
import { workflowInsertStepIdsComponentState } from '@/workflow/workflow-steps/states/workflowInsertStepIdsComponentState';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import {
@@ -53,9 +53,6 @@ import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { Tag, type TagColor } from 'twenty-ui/components';
import { THEME_COMMON } from 'twenty-ui/theme';
import { FeatureFlagKey } from '~/generated/graphql';
import { useEdgeState } from '@/workflow/workflow-diagram/workflow-edges/hooks/useEdgeState';
import { WorkflowDiagramConnection } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramConnection';
const StyledResetReactflowStyles = styled.div`
height: 100%;
@@ -181,23 +178,15 @@ export const WorkflowDiagramCanvasBase = ({
const { setEdgeHovered, clearEdgeHover } = useEdgeState();
const isWorkflowBranchEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_WORKFLOW_BRANCH_ENABLED,
);
const [workflowDiagramFlowInitialized, setWorkflowDiagramFlowInitialized] =
useState<boolean>(false);
const { nodes, edges } = useMemo(() => {
if (isDefined(workflowDiagram)) {
if (isWorkflowBranchEnabled) {
return workflowDiagram;
}
return getOrganizedDiagram(workflowDiagram);
return workflowDiagram;
}
return { nodes: [], edges: [] };
}, [workflowDiagram, isWorkflowBranchEnabled]);
}, [workflowDiagram]);
const { rightDrawerState } = useRightDrawerState();
const { isInRightDrawer } = useContext(ActionMenuContext);
@@ -407,10 +396,6 @@ export const WorkflowDiagramCanvasBase = ({
WorkflowDiagramNode,
WorkflowDiagramEdge
> = async ({ nodes, edges }) => {
if (!isWorkflowBranchEnabled) {
return false;
}
if (nodes.length === 0 && edges.length > 0) {
return true;
}
@@ -421,7 +406,7 @@ export const WorkflowDiagramCanvasBase = ({
const onDelete: OnDelete<WorkflowDiagramNode, WorkflowDiagramEdge> = async ({
edges,
}) => {
if (!isWorkflowBranchEnabled || !isDefined(onDeleteEdge)) {
if (!isDefined(onDeleteEdge)) {
return;
}
@@ -479,23 +464,19 @@ export const WorkflowDiagramCanvasBase = ({
onEdgeMouseLeave={onEdgeMouseLeave}
onNodesChange={handleNodesChanges}
onEdgesChange={handleEdgesChange}
onConnect={isWorkflowBranchEnabled ? onConnect : undefined}
onNodeDragStop={isWorkflowBranchEnabled ? onNodeDragStop : undefined}
onConnect={onConnect}
onNodeDragStop={onNodeDragStop}
onBeforeDelete={onBeforeDelete}
onDelete={onDelete}
selectNodesOnDrag={false}
proOptions={{ hideAttribution: true }}
multiSelectionKeyCode={null}
nodesFocusable={false}
nodesDraggable={isWorkflowBranchEnabled ? nodesDraggable : false}
edgesFocusable={
isWorkflowBranchEnabled ? isDefined(onDeleteEdge) : false
}
nodesDraggable={nodesDraggable}
edgesFocusable={isDefined(onDeleteEdge)}
panOnDrag={workflowDiagramPanOnDrag}
onPaneContextMenu={
isWorkflowBranchEnabled ? onPaneContextMenu : undefined
}
nodesConnectable={isWorkflowBranchEnabled ? nodesConnectable : false}
onPaneContextMenu={onPaneContextMenu}
nodesConnectable={nodesConnectable}
paneClickDistance={10} // Fix small unwanted user dragging does not select node
preventScrolling={false}
connectionLineComponent={WorkflowDiagramConnection}
@@ -506,7 +487,7 @@ export const WorkflowDiagramCanvasBase = ({
{children}
</ReactFlow>
{isDefined(handlePaneContextMenu) && isWorkflowBranchEnabled && (
{isDefined(handlePaneContextMenu) && (
<WorkflowDiagramRightClickCommandMenu />
)}
@@ -1,26 +1,16 @@
import { useEdgeState } from '@/workflow/workflow-diagram/workflow-edges/hooks/useEdgeState';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import {
type OnSelectionChangeParams,
useOnSelectionChange,
} from '@xyflow/react';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { FeatureFlagKey } from '~/generated/graphql';
export const WorkflowDiagramCanvasEditableEffect = () => {
const { setEdgeSelected, clearEdgeSelected } = useEdgeState();
const isWorkflowBranchEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_WORKFLOW_BRANCH_ENABLED,
);
const handleSelectedEdges = useCallback(
({ edges }: OnSelectionChangeParams) => {
if (!isWorkflowBranchEnabled) {
return;
}
const selectedEdge = edges?.[0];
if (!isDefined(selectedEdge)) {
@@ -34,7 +24,7 @@ export const WorkflowDiagramCanvasEditableEffect = () => {
target: selectedEdge.target,
});
},
[isWorkflowBranchEnabled, setEdgeSelected, clearEdgeSelected],
[setEdgeSelected, clearEdgeSelected],
);
useOnSelectionChange({
@@ -12,11 +12,9 @@ import { workflowDiagramComponentState } from '@/workflow/workflow-diagram/state
import { getWorkflowVersionDiagram } from '@/workflow/workflow-diagram/utils/getWorkflowVersionDiagram';
import { mergeWorkflowDiagrams } from '@/workflow/workflow-diagram/utils/mergeWorkflowDiagrams';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useEffect } from 'react';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { FeatureFlagKey } from '~/generated/graphql';
export const WorkflowDiagramEffect = () => {
const workflowVisualizerWorkflowId = useRecoilComponentValue(
@@ -40,10 +38,6 @@ export const WorkflowDiagramEffect = () => {
workflowLastCreatedStepIdComponentState,
);
const isWorkflowBranchEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_WORKFLOW_BRANCH_ENABLED,
);
const computeAndMergeNewWorkflowDiagram = useRecoilCallback(
({ snapshot, set }) => {
return (currentVersion: WorkflowVersion) => {
@@ -54,7 +48,6 @@ export const WorkflowDiagramEffect = () => {
const nextWorkflowDiagram = getWorkflowVersionDiagram({
workflowVersion: currentVersion,
isWorkflowBranchEnabled,
isEditable: true,
});
@@ -88,11 +81,7 @@ export const WorkflowDiagramEffect = () => {
set(workflowDiagramState, mergedWorkflowDiagram);
};
},
[
workflowDiagramState,
isWorkflowBranchEnabled,
workflowLastCreatedStepIdState,
],
[workflowDiagramState, workflowLastCreatedStepIdState],
);
const currentVersion = workflowWithCurrentVersion?.currentVersion;
@@ -18,12 +18,10 @@ import { workflowSelectedNodeComponentState } from '@/workflow/workflow-diagram/
import { generateWorkflowRunDiagram } from '@/workflow/workflow-diagram/utils/generateWorkflowRunDiagram';
import { getWorkflowNodeIconKey } from '@/workflow/workflow-diagram/utils/getWorkflowNodeIconKey';
import { selectWorkflowDiagramNode } from '@/workflow/workflow-diagram/utils/selectWorkflowDiagramNode';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useContext, useEffect } from 'react';
import { useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { useIcons } from 'twenty-ui/display';
import { FeatureFlagKey } from '~/generated/graphql';
export const WorkflowRunVisualizerEffect = ({
workflowRunId,
@@ -71,10 +69,6 @@ export const WorkflowRunVisualizerEffect = ({
const { isInRightDrawer } = useContext(ActionMenuContext);
const isWorkflowBranchEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_WORKFLOW_BRANCH_ENABLED,
);
useEffect(() => {
setWorkflowRunId(workflowRunId);
}, [setWorkflowRunId, workflowRunId]);
@@ -133,7 +127,6 @@ export const WorkflowRunVisualizerEffect = ({
trigger: workflowRunState.flow.trigger,
steps: workflowRunState.flow.steps,
stepInfos: workflowRunState.stepInfos,
isWorkflowBranchEnabled,
});
if (workflowDiagramStatus !== 'done') {
@@ -204,7 +197,6 @@ export const WorkflowRunVisualizerEffect = ({
[
flowState,
getIcon,
isWorkflowBranchEnabled,
openWorkflowRunViewStepInCommandMenu,
workflowDiagramState,
workflowDiagramStatusState,
@@ -6,10 +6,8 @@ import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/wo
import { workflowVisualizerWorkflowVersionIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowVersionIdComponentState';
import { workflowDiagramComponentState } from '@/workflow/workflow-diagram/states/workflowDiagramComponentState';
import { getWorkflowVersionDiagram } from '@/workflow/workflow-diagram/utils/getWorkflowVersionDiagram';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useEffect } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { FeatureFlagKey } from '~/generated/graphql';
export const WorkflowVersionVisualizerEffect = ({
workflowVersionId,
@@ -31,10 +29,6 @@ export const WorkflowVersionVisualizerEffect = ({
const { populateStepsOutputSchema } = useStepsOutputSchema();
const isWorkflowBranchEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_WORKFLOW_BRANCH_ENABLED,
);
useEffect(() => {
if (!isDefined(workflowVersion)) {
setFlow(undefined);
@@ -66,12 +60,11 @@ export const WorkflowVersionVisualizerEffect = ({
const nextWorkflowDiagram = getWorkflowVersionDiagram({
workflowVersion,
isWorkflowBranchEnabled,
isEditable: false,
});
setWorkflowDiagram(nextWorkflowDiagram);
}, [isWorkflowBranchEnabled, setWorkflowDiagram, workflowVersion]);
}, [setWorkflowDiagram, workflowVersion]);
useEffect(() => {
if (!isDefined(workflowVersion)) {
@@ -104,8 +104,6 @@ describe('generateWorkflowRunDiagram', () => {
trigger,
steps,
stepInfos,
isWorkflowBranchEnabled: true,
});
expect(result).toMatchInlineSnapshot(`
@@ -337,8 +335,6 @@ describe('generateWorkflowRunDiagram', () => {
trigger,
steps,
stepInfos,
isWorkflowBranchEnabled: true,
});
expect(result).toMatchInlineSnapshot(`
@@ -570,8 +566,6 @@ describe('generateWorkflowRunDiagram', () => {
trigger,
steps,
stepInfos,
isWorkflowBranchEnabled: true,
});
expect(result).toMatchInlineSnapshot(`
@@ -822,8 +816,6 @@ describe('generateWorkflowRunDiagram', () => {
trigger,
steps,
stepInfos,
isWorkflowBranchEnabled: true,
});
expect(result).toMatchInlineSnapshot(`
@@ -1055,8 +1047,6 @@ describe('generateWorkflowRunDiagram', () => {
trigger,
steps,
stepInfos,
isWorkflowBranchEnabled: true,
});
expect(result).toMatchInlineSnapshot(`
@@ -46,7 +46,6 @@ describe('transformFilterNodesAsEdges', () => {
nodes: diagram.nodes,
edges: diagram.edges,
defaultFilterEdgeType: 'filter--editable',
isWorkflowBranchEnabled: true,
});
expect(result.nodes).toEqual(diagram.nodes);
@@ -113,7 +112,6 @@ describe('transformFilterNodesAsEdges', () => {
nodes: diagram.nodes,
edges: diagram.edges,
defaultFilterEdgeType: 'filter--editable',
isWorkflowBranchEnabled: true,
});
// Should only have nodes A and C
@@ -259,7 +257,6 @@ describe('transformFilterNodesAsEdges', () => {
nodes: diagram.nodes,
edges: diagram.edges,
defaultFilterEdgeType: 'filter--editable',
isWorkflowBranchEnabled: true,
});
// Should only have nodes A, C, and D
@@ -352,7 +349,6 @@ describe('transformFilterNodesAsEdges', () => {
nodes: diagram.nodes,
edges: diagram.edges,
defaultFilterEdgeType: 'filter--editable',
isWorkflowBranchEnabled: true,
});
// Should only have node A (filter node B is removed)
@@ -435,7 +431,6 @@ describe('transformFilterNodesAsEdges', () => {
nodes: diagram.nodes,
edges: diagram.edges,
defaultFilterEdgeType: 'filter--editable',
isWorkflowBranchEnabled: true,
});
// Should have trigger and C nodes
@@ -485,86 +480,4 @@ describe('transformFilterNodesAsEdges', () => {
},
]);
});
it('should set selectable and deletable to false when isWorkflowBranchEnabled is false', () => {
const diagram: WorkflowDiagram = {
nodes: [
{
id: 'A',
data: {
nodeType: 'action',
actionType: 'CODE',
name: 'Step A',
hasNextStepIds: true,
position: { x: 0, y: 0 },
stepId: 'A',
},
position: { x: 0, y: 0 },
},
{
id: 'B',
data: {
nodeType: 'action',
actionType: 'FILTER',
name: 'Filter B',
hasNextStepIds: true,
position: { x: 0, y: 150 },
stepId: 'B',
},
position: { x: 0, y: 150 },
},
{
id: 'C',
data: {
nodeType: 'action',
actionType: 'SEND_EMAIL',
name: 'Step C',
hasNextStepIds: false,
position: { x: 0, y: 300 },
stepId: 'C',
},
position: { x: 0, y: 300 },
},
],
edges: [
{
id: 'A-B',
source: 'A',
target: 'B',
data: { edgeType: 'default' },
},
{
id: 'B-C',
source: 'B',
target: 'C',
data: { edgeType: 'default' },
},
],
};
const result = transformFilterNodesAsEdges({
nodes: diagram.nodes,
edges: diagram.edges,
defaultFilterEdgeType: 'filter--editable',
isWorkflowBranchEnabled: false,
});
// Should have one edge with filter data
expect(result.edges).toHaveLength(1);
expect(result.edges[0]).toEqual({
id: 'A-C-filter-B',
type: 'filter--editable',
source: 'A',
target: 'C',
selectable: false,
deletable: false,
data: {
edgeType: 'filter',
stepId: 'B',
name: 'Filter B',
runStatus: undefined,
filterSettings: {},
},
});
});
});
@@ -4,7 +4,6 @@ import {
} from '@/workflow/types/Workflow';
import { FIRST_NODE_POSITION } from '@/workflow/workflow-diagram/constants/FirstNodePosition';
import { VERTICAL_DISTANCE_BETWEEN_TWO_NODES } from '@/workflow/workflow-diagram/constants/VerticalDistanceBetweenTwoNodes';
import { WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION } from '@/workflow/workflow-diagram/workflow-edges/constants/WorkflowVisualizerEdgeDefaultConfiguration';
import {
type WorkflowDiagram,
type WorkflowDiagramEdge,
@@ -13,9 +12,9 @@ import {
type WorkflowDiagramStepNodeData,
} from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { getWorkflowDiagramTriggerNode } from '@/workflow/workflow-diagram/utils/getWorkflowDiagramTriggerNode';
import { WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION } from '@/workflow/workflow-diagram/workflow-edges/constants/WorkflowVisualizerEdgeDefaultConfiguration';
import { WORKFLOW_DIAGRAM_EMPTY_TRIGGER_NODE_DEFINITION } from '@/workflow/workflow-diagram/constants/WorkflowDiagramEmptyTriggerNodeDefinition';
import { getRootStepIds } from '@/workflow/workflow-trigger/utils/getRootStepIds';
import { isDefined } from 'twenty-shared/utils';
import { TRIGGER_STEP_ID } from 'twenty-shared/workflow';
import { v4 } from 'uuid';
@@ -24,12 +23,10 @@ export const generateWorkflowDiagram = ({
trigger,
steps,
defaultEdgeType,
isWorkflowBranchEnabled = false,
}: {
trigger: WorkflowTrigger | undefined;
steps: Array<WorkflowStep>;
defaultEdgeType: WorkflowDiagramEdgeType;
isWorkflowBranchEnabled?: boolean;
}): WorkflowDiagram => {
const nodes: Array<WorkflowDiagramNode> = [];
const edges: Array<WorkflowDiagramEdge> = [];
@@ -38,19 +35,6 @@ export const generateWorkflowDiagram = ({
nodes.push(getWorkflowDiagramTriggerNode({ trigger }));
} else {
nodes.push(WORKFLOW_DIAGRAM_EMPTY_TRIGGER_NODE_DEFINITION);
const triggerNextStepIds =
isDefined(steps) && !isWorkflowBranchEnabled ? getRootStepIds(steps) : [];
triggerNextStepIds.forEach((stepId) => {
edges.push({
...WORKFLOW_VISUALIZER_EDGE_DEFAULT_CONFIGURATION,
type: 'blank',
id: v4(),
source: 'trigger',
target: stepId,
});
});
}
let levelYPos = FIRST_NODE_POSITION.y;
@@ -19,12 +19,10 @@ export const generateWorkflowRunDiagram = ({
trigger,
steps,
stepInfos,
isWorkflowBranchEnabled,
}: {
trigger: WorkflowTrigger;
steps: Array<WorkflowStep>;
stepInfos: WorkflowRunStepInfos | undefined;
isWorkflowBranchEnabled: boolean;
}): {
diagram: WorkflowRunDiagram;
stepToOpenByDefault:
@@ -107,7 +105,6 @@ export const generateWorkflowRunDiagram = ({
nodes: workflowRunDiagramNodes,
edges: workflowRunDiagramEdges,
defaultFilterEdgeType: 'filter--run',
isWorkflowBranchEnabled,
}),
stepToOpenByDefault,
};
@@ -22,11 +22,9 @@ const getEdgeTypeToCreateByDefault = ({
export const getWorkflowVersionDiagram = ({
workflowVersion,
isWorkflowBranchEnabled,
isEditable,
}: {
workflowVersion: WorkflowVersion | undefined;
isWorkflowBranchEnabled?: boolean;
isEditable: boolean;
}): WorkflowDiagram => {
if (!isDefined(workflowVersion)) {
@@ -39,13 +37,11 @@ export const getWorkflowVersionDiagram = ({
defaultEdgeType: getEdgeTypeToCreateByDefault({
isEditable,
}),
isWorkflowBranchEnabled,
});
return transformFilterNodesAsEdges({
nodes: diagram.nodes,
edges: diagram.edges,
defaultFilterEdgeType: isEditable ? 'filter--editable' : 'filter--readonly',
isWorkflowBranchEnabled: isWorkflowBranchEnabled === true,
});
};
@@ -12,12 +12,10 @@ export const transformFilterNodesAsEdges = <
nodes,
edges,
defaultFilterEdgeType,
isWorkflowBranchEnabled,
}: {
nodes: T[];
edges: U[];
defaultFilterEdgeType: WorkflowDiagramEdgeType;
isWorkflowBranchEnabled: boolean;
}): { nodes: T[]; edges: U[] } => {
const filterNodes = nodes.filter(
(node) =>
@@ -55,8 +53,8 @@ export const transformFilterNodesAsEdges = <
type: defaultFilterEdgeType,
id: `${incomingEdge.source}-${outgoingEdge.target}-filter-${filterNode.id}`,
target: outgoingEdge.target,
selectable: isWorkflowBranchEnabled === true,
deletable: isWorkflowBranchEnabled === true,
selectable: true,
deletable: true,
data: {
...incomingEdge.data,
edgeType: 'filter',
@@ -3,17 +3,17 @@ import { commandMenuNavigationStackState } from '@/command-menu/states/commandMe
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
import { workflowVisualizerWorkflowIdComponentState } from '@/workflow/states/workflowVisualizerWorkflowIdComponentState';
import { useOpenWorkflowEditFilterInCommandMenu } from '@/workflow/workflow-diagram/hooks/useOpenWorkflowEditFilterInCommandMenu';
import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation';
import { type WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { WorkflowDiagramBaseEdge } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramBaseEdge';
import { WorkflowDiagramEdgeButtonGroup } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeButtonGroup';
import { WorkflowDiagramEdgeV2Container } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeV2Container';
import { WorkflowDiagramEdgeV2VisibilityContainer } from '@/workflow/workflow-diagram/workflow-edges/components/WorkflowDiagramEdgeV2VisibilityContainer';
import { WORKFLOW_DIAGRAM_EDGE_OPTIONS_CLICK_OUTSIDE_ID } from '@/workflow/workflow-diagram/workflow-edges/constants/WorkflowDiagramEdgeOptionsClickOutsideId';
import { useOpenWorkflowEditFilterInCommandMenu } from '@/workflow/workflow-diagram/hooks/useOpenWorkflowEditFilterInCommandMenu';
import { useStartNodeCreation } from '@/workflow/workflow-diagram/hooks/useStartNodeCreation';
import { type WorkflowDiagramEdge } from '@/workflow/workflow-diagram/types/WorkflowDiagram';
import { useEdgeState } from '@/workflow/workflow-diagram/workflow-edges/hooks/useEdgeState';
import { useCreateStep } from '@/workflow/workflow-steps/hooks/useCreateStep';
import { useDeleteEdge } from '@/workflow/workflow-steps/hooks/useDeleteEdge';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import {
EdgeLabelRenderer,
type EdgeProps,
@@ -23,8 +23,6 @@ import { type MouseEvent, useContext } from 'react';
import { useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { IconFilter, IconPlus, IconTrash } from 'twenty-ui/display';
import { FeatureFlagKey } from '~/generated/graphql';
import { useEdgeState } from '@/workflow/workflow-diagram/workflow-edges/hooks/useEdgeState';
type WorkflowDiagramDefaultEdgeEditableProps = EdgeProps<WorkflowDiagramEdge>;
@@ -38,10 +36,6 @@ export const WorkflowDiagramDefaultEdgeEditable = ({
markerStart,
markerEnd,
}: WorkflowDiagramDefaultEdgeEditableProps) => {
const isWorkflowBranchEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_WORKFLOW_BRANCH_ENABLED,
);
const { isInRightDrawer } = useContext(ActionMenuContext);
const { isEdgeHovered } = useEdgeState();
@@ -142,14 +136,10 @@ export const WorkflowDiagramDefaultEdgeEditable = ({
Icon: IconPlus,
onClick: handleNodeButtonClick,
},
...(isWorkflowBranchEnabled
? [
{
Icon: IconTrash,
onClick: handleDeleteBranch,
},
]
: []),
{
Icon: IconTrash,
onClick: handleDeleteBranch,
},
]}
selected={nodeCreationStarted}
/>
@@ -28,7 +28,6 @@ import { useDeleteEdge } from '@/workflow/workflow-steps/hooks/useDeleteEdge';
import { useDeleteStep } from '@/workflow/workflow-steps/hooks/useDeleteStep';
import { WorkflowStepFilterCounter } from '@/workflow/workflow-steps/workflow-actions/filter-action/components/WorkflowStepFilterCounter';
import { useFilterCounter } from '@/workflow/workflow-steps/workflow-actions/filter-action/hooks/useFilterCounter';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { css, useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
@@ -50,7 +49,6 @@ import {
} from 'twenty-ui/display';
import { IconButtonGroup } from 'twenty-ui/input';
import { MenuItem } from 'twenty-ui/navigation';
import { FeatureFlagKey } from '~/generated/graphql';
type WorkflowDiagramFilterEdgeEditableProps = EdgeProps<WorkflowDiagramEdge>;
@@ -98,9 +96,6 @@ export const WorkflowDiagramFilterEdgeEditable = ({
const { t } = useLingui();
const theme = useTheme();
const isWorkflowBranchEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_WORKFLOW_BRANCH_ENABLED,
);
const [edgePath, labelX, labelY] = getBezierPath({
sourceX,
@@ -274,13 +269,11 @@ export const WorkflowDiagramFilterEdgeEditable = ({
LeftIcon={IconPlus}
onClick={handleAddNodeButtonClick}
/>
{isWorkflowBranchEnabled && (
<MenuItem
text={t`Delete branch`}
LeftIcon={IconTrash}
onClick={handleDeleteBranchClick}
/>
)}
<MenuItem
text={t`Delete branch`}
LeftIcon={IconTrash}
onClick={handleDeleteBranchClick}
/>
</DropdownMenuItemsContainer>
</DropdownContent>
}
@@ -2,11 +2,9 @@ import type { WorkflowRunStepStatus } from '@/workflow/types/Workflow';
import { NODE_HANDLE_HEIGHT_PX } from '@/workflow/workflow-diagram/constants/NodeHandleHeightPx';
import { NODE_HANDLE_WIDTH_PX } from '@/workflow/workflow-diagram/constants/NodeHandleWidthPx';
import { getWorkflowDiagramColors } from '@/workflow/workflow-diagram/utils/getWorkflowDiagramColors';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { css } from '@emotion/react';
import styled from '@emotion/styled';
import { Handle, Position, type HandleProps } from '@xyflow/react';
import { FeatureFlagKey } from '~/generated/graphql';
type WorkflowDiagramHandleSourceProps = {
selected: boolean;
@@ -89,15 +87,11 @@ export const WorkflowDiagramHandleSource = ({
readOnly = false,
runStatus,
}: WorkflowDiagramHandleSourceProps) => {
const isWorkflowBranchEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_WORKFLOW_BRANCH_ENABLED,
);
return (
<StyledHandle
type="source"
position={Position.Bottom}
disableHoverEffect={!isWorkflowBranchEnabled || readOnly}
disableHoverEffect={readOnly}
selected={selected}
hovered={hovered}
runStatus={runStatus}
@@ -28,7 +28,6 @@ describe('getTriggerDefaultDefinition', () => {
eventName: `${generatedMockObjectMetadataItems[0].nameSingular}.created`,
outputSchema: {},
},
nextStepIds: [],
position: {
x: 0,
y: 0,
@@ -50,7 +49,6 @@ describe('getTriggerDefaultDefinition', () => {
eventName: `${generatedMockObjectMetadataItems[0].nameSingular}.updated`,
outputSchema: {},
},
nextStepIds: [],
position: {
x: 0,
y: 0,
@@ -72,7 +70,6 @@ describe('getTriggerDefaultDefinition', () => {
eventName: `${generatedMockObjectMetadataItems[0].nameSingular}.deleted`,
outputSchema: {},
},
nextStepIds: [],
position: {
x: 0,
y: 0,
@@ -94,7 +91,6 @@ describe('getTriggerDefaultDefinition', () => {
eventName: `${generatedMockObjectMetadataItems[0].nameSingular}.created`,
outputSchema: {},
},
nextStepIds: [],
position: {
x: 0,
y: 0,
@@ -118,7 +114,6 @@ describe('getTriggerDefaultDefinition', () => {
icon: COMMAND_MENU_DEFAULT_ICON,
isPinned: false,
},
nextStepIds: [],
position: {
x: 0,
y: 0,
@@ -141,7 +136,6 @@ describe('getTriggerDefaultDefinition', () => {
schedule: { day: 1, hour: 0, minute: 0 },
outputSchema: {},
},
nextStepIds: [],
position: {
x: 0,
y: 0,
@@ -164,7 +158,6 @@ describe('getTriggerDefaultDefinition', () => {
httpMethod: 'GET',
authentication: null,
},
nextStepIds: [],
position: {
x: 0,
y: 0,
@@ -1,25 +1,21 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import {
type WorkflowAction,
type WorkflowTrigger,
type WorkflowTriggerType,
} from '@/workflow/types/Workflow';
import { DATABASE_TRIGGER_TYPES } from '@/workflow/workflow-trigger/constants/DatabaseTriggerTypes';
import { getManualTriggerDefaultSettings } from '@/workflow/workflow-trigger/utils/getManualTriggerDefaultSettings';
import { getRootStepIds } from '@/workflow/workflow-trigger/utils/getRootStepIds';
import { assertUnreachable, isDefined } from 'twenty-shared/utils';
import { assertUnreachable } from 'twenty-shared/utils';
// TODO: This needs to be migrated to the server
export const getTriggerDefaultDefinition = ({
defaultLabel,
type,
activeNonSystemObjectMetadataItems,
steps,
}: {
defaultLabel: string;
type: WorkflowTriggerType;
activeNonSystemObjectMetadataItems: ObjectMetadataItem[];
steps?: WorkflowAction[] | null;
}): WorkflowTrigger => {
if (activeNonSystemObjectMetadataItems.length === 0) {
throw new Error(
@@ -27,12 +23,9 @@ export const getTriggerDefaultDefinition = ({
);
}
const nextStepIds = isDefined(steps) ? getRootStepIds(steps) : [];
const baseTriggerDefinition = {
name: defaultLabel,
position: { x: 0, y: 0 },
nextStepIds,
};
switch (type) {
@@ -8,6 +8,7 @@ import { WorkspaceCleanerModule } from 'src/engine/workspace-manager/workspace-c
import { WorkspaceHealthCommandModule } from 'src/engine/workspace-manager/workspace-health/commands/workspace-health-command.module';
import { WorkspaceMigrationRunnerCommandsModule } from 'src/engine/workspace-manager/workspace-migration-runner/commands/workspace-migration-runner-commands.module';
import { WorkspaceSyncMetadataCommandsModule } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/workspace-sync-metadata-commands.module';
import { MessagingMessageCleanerModule } from 'src/modules/messaging/message-cleaner/messaging-message-cleaner.module';
@Module({
imports: [
@@ -16,6 +17,7 @@ import { WorkspaceSyncMetadataCommandsModule } from 'src/engine/workspace-manage
DatabaseCommandModule,
WorkspaceCleanerModule,
WorkspaceHealthCommandModule,
MessagingMessageCleanerModule,
WorkspaceMigrationRunnerCommandsModule,
ObjectMetadataModule,
FieldMetadataModule,
@@ -562,6 +562,7 @@ export class MigrateViewsToCoreCommand extends ActiveOrSuspendedWorkspacesMigrat
createdAt: new Date(filter.createdAt),
updatedAt: new Date(filter.updatedAt),
deletedAt: filter.deletedAt ? new Date(filter.deletedAt) : null,
subFieldName: filter.subFieldName,
};
const repository = queryRunner.manager.getRepository(ViewFilterEntity);
@@ -19,4 +19,6 @@ export enum GraphqlQueryRunnerExceptionCode {
RELATION_TARGET_OBJECT_METADATA_NOT_FOUND = 'RELATION_TARGET_OBJECT_METADATA_NOT_FOUND',
NOT_IMPLEMENTED = 'NOT_IMPLEMENTED',
INVALID_POST_HOOK_PAYLOAD = 'INVALID_POST_HOOK_PAYLOAD',
UPSERT_MULTIPLE_MATCHING_RECORDS_CONFLICT = 'UPSERT_MULTIPLE_MATCHING_RECORDS_CONFLICT',
UPSERT_MAX_RECORDS_EXCEEDED = 'UPSERT_MAX_RECORDS_EXCEEDED',
}
@@ -1,3 +1,4 @@
import { FieldMetadataType } from 'twenty-shared/types';
import { capitalize } from 'twenty-shared/utils';
import {
@@ -51,7 +52,10 @@ export class GraphqlQueryOrderFieldParser {
Object.assign(acc, compositeOrder);
} else {
acc[`"${objectNameSingular}"."${key}"`] =
const orderByCasting =
this.getOptionalOrderByCasting(fieldMetadata);
acc[`"${objectNameSingular}"."${key}"${orderByCasting}`] =
this.convertOrderByToFindOptionsOrder(
value as OrderByDirection,
isForwardPagination,
@@ -65,6 +69,19 @@ export class GraphqlQueryOrderFieldParser {
);
}
private getOptionalOrderByCasting(
fieldMetadata: Pick<FieldMetadataEntity, 'type'>,
): string {
if (
fieldMetadata.type === FieldMetadataType.SELECT ||
fieldMetadata.type === FieldMetadataType.MULTI_SELECT
) {
return '::text';
}
return '';
}
private parseCompositeFieldForOrder(
fieldMetadata: FieldMetadataEntity,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -1,5 +1,6 @@
import { Injectable } from '@nestjs/common';
import { t } from '@lingui/core/macro';
import { QUERY_MAX_RECORDS } from 'twenty-shared/constants';
import { capitalize, isDefined } from 'twenty-shared/utils';
import {
@@ -41,6 +42,16 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
async resolve(
executionArgs: GraphqlQueryResolverExecutionArgs<CreateManyResolverArgs>,
): Promise<ObjectRecord[]> {
if (executionArgs.args.data.length > QUERY_MAX_RECORDS) {
throw new GraphqlQueryRunnerException(
`Maximum number of records to upsert is ${QUERY_MAX_RECORDS}.`,
GraphqlQueryRunnerExceptionCode.UPSERT_MAX_RECORDS_EXCEEDED,
{
userFriendlyMessage: t`Maximum number of records to upsert is ${QUERY_MAX_RECORDS}.`,
},
);
}
const { objectMetadataItemWithFieldMaps, objectMetadataMaps } =
executionArgs.options;
@@ -284,12 +295,36 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
const recordsToInsert: Partial<ObjectRecord>[] = [];
for (const record of records) {
let existingRecord: PartialObjectRecordWithId | null = null;
const matchingRecordId = this.getMatchingRecordId(
record,
conflictingFields,
existingRecords,
);
for (const field of conflictingFields) {
if (isDefined(matchingRecordId)) {
recordsToUpdate.push({ ...record, id: matchingRecordId });
} else {
recordsToInsert.push(record);
}
}
return { recordsToUpdate, recordsToInsert };
}
private getMatchingRecordId(
record: Partial<ObjectRecord>,
conflictingFields: {
baseField: string;
fullPath: string;
column: string;
}[],
existingRecords: PartialObjectRecordWithId[],
): string | undefined {
const matchingRecordIds = conflictingFields.reduce<string[]>(
(acc, field) => {
const requestFieldValue = this.getValueFromPath(record, field.fullPath);
const existingRec = existingRecords.find((existingRecord) => {
const matchingRecord = existingRecords.find((existingRecord) => {
const existingFieldValue = this.getValueFromPath(
existingRecord,
field.fullPath,
@@ -301,20 +336,35 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
);
});
if (existingRec) {
existingRecord = { ...record, id: existingRec.id };
break;
if (isDefined(matchingRecord)) {
acc.push(matchingRecord.id);
}
}
if (existingRecord) {
recordsToUpdate.push({ ...record, id: existingRecord.id });
} else {
recordsToInsert.push(record);
}
return acc;
},
[],
);
if ([...new Set(matchingRecordIds)].length > 1) {
const conflictingFieldsValues = conflictingFields
.map((field) => {
const value = this.getValueFromPath(record, field.fullPath);
return isDefined(value) ? `${field.fullPath}: ${value}` : undefined;
})
.filter(isDefined)
.join(', ');
throw new GraphqlQueryRunnerException(
`Multiple records found with the same unique field values for ${conflictingFieldsValues}. Cannot determine which record to update.`,
GraphqlQueryRunnerExceptionCode.UPSERT_MULTIPLE_MATCHING_RECORDS_CONFLICT,
{
userFriendlyMessage: t`Multiple records found with the same unique field values for ${conflictingFieldsValues}. Cannot determine which record to update.`,
},
);
}
return { recordsToUpdate, recordsToInsert };
return matchingRecordIds[0];
}
private async processRecordsToUpdate({
@@ -400,6 +450,7 @@ export class GraphqlQueryCreateManyResolverService extends GraphqlQueryBaseResol
.where({
id: In(objectRecords.generatedMaps.map((record) => record.id)),
})
.withDeleted()
.take(QUERY_MAX_RECORDS)
.getMany();
@@ -24,6 +24,8 @@ export const graphqlQueryRunnerExceptionHandler = (
case GraphqlQueryRunnerExceptionCode.FIELD_NOT_FOUND:
case GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT:
case GraphqlQueryRunnerExceptionCode.NOT_IMPLEMENTED:
case GraphqlQueryRunnerExceptionCode.UPSERT_MULTIPLE_MATCHING_RECORDS_CONFLICT:
case GraphqlQueryRunnerExceptionCode.UPSERT_MAX_RECORDS_EXCEEDED:
throw new UserInputError(error);
case GraphqlQueryRunnerExceptionCode.RECORD_NOT_FOUND:
throw new NotFoundError(error);
@@ -1,7 +1,7 @@
import { computeDepth } from 'src/engine/api/rest/core/query-builder/utils/compute-depth.utils';
describe('computeDepth', () => {
[0, 1, 2].forEach((depth) => {
[0, 1].forEach((depth) => {
it('should compute depth from query', () => {
const request: any = {
query: { depth: `${depth}` },
@@ -2,7 +2,7 @@ import { BadRequestException } from '@nestjs/common';
import { type Request } from 'express';
const ALLOWED_DEPTH_VALUES = [0, 1, 2];
const ALLOWED_DEPTH_VALUES = [0, 1];
export const computeDepth = (request: Request): number | undefined => {
if (!request.query.depth) {
@@ -6,7 +6,7 @@ export const MAX_DEPTH = 2;
export type Depth = 0 | 1 | 2;
const ALLOWED_DEPTH_VALUES: Depth[] = [0, 1, MAX_DEPTH];
const ALLOWED_DEPTH_VALUES: Depth[] = [0, 1];
@Injectable()
export class DepthInputFactory {
@@ -22,14 +22,6 @@ export const PUBLIC_FEATURE_FLAGS: PublicFeatureFlag[] = [
'https://twenty.com/images/lab/is-imap-smtp-caldav-enabled.png',
},
},
{
key: FeatureFlagKey.IS_WORKFLOW_BRANCH_ENABLED,
metadata: {
label: 'Workflow Branches',
description: 'Create multiple branches on your workflows',
imagePath: 'https://twenty.com/images/lab/is-workflow-branch-enabled.png',
},
},
{
key: FeatureFlagKey.IS_MESSAGE_FOLDER_CONTROL_ENABLED,
metadata: {
@@ -7,7 +7,6 @@ export enum FeatureFlagKey {
IS_AI_ENABLED = 'IS_AI_ENABLED',
IS_IMAP_SMTP_CALDAV_ENABLED = 'IS_IMAP_SMTP_CALDAV_ENABLED',
IS_MORPH_RELATION_ENABLED = 'IS_MORPH_RELATION_ENABLED',
IS_WORKFLOW_BRANCH_ENABLED = 'IS_WORKFLOW_BRANCH_ENABLED',
IS_RELATION_CONNECT_ENABLED = 'IS_RELATION_CONNECT_ENABLED',
IS_CORE_VIEW_SYNCING_ENABLED = 'IS_CORE_VIEW_SYNCING_ENABLED',
IS_CORE_VIEW_ENABLED = 'IS_CORE_VIEW_ENABLED',
@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
import { In, Not } from 'typeorm';
import { In } from 'typeorm';
import { type TimelineThread } from 'src/engine/core-modules/messaging/dtos/timeline-thread.dto';
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
@@ -94,6 +94,7 @@ export class TimelineMessagingService {
await this.twentyORMManager.getRepository<MessageParticipantWorkspaceEntity>(
'messageParticipant',
);
const threadParticipants = await messageParticipantRepository
.createQueryBuilder()
.select('messageParticipant')
@@ -183,32 +184,11 @@ export class TimelineMessagingService {
'messageThread',
);
const threadsWithoutWorkspaceMember = await messageThreadRepository.find({
select: {
id: true,
},
where: {
id: In(messageThreadIds),
messages: {
messageChannelMessageAssociations: {
messageChannel: {
connectedAccount: {
accountOwnerId: Not(workspaceMemberId),
},
},
},
},
},
});
const threadIdsWithoutWorkspaceMember = threadsWithoutWorkspaceMember.map(
(thread) => thread.id,
);
const threadVisibility = await messageThreadRepository
.createQueryBuilder()
.select('messageThread.id', 'id')
.addSelect('messageChannel.visibility', 'visibility')
.addSelect('connectedAccount.accountOwnerId', 'accountOwnerId')
.leftJoin('messageThread.messages', 'message')
.leftJoin(
'message.messageChannelMessageAssociations',
@@ -218,46 +198,34 @@ export class TimelineMessagingService {
'messageChannelMessageAssociation.messageChannel',
'messageChannel',
)
.leftJoin('messageChannel.connectedAccount', 'connectedAccount')
.where('messageThread.id = ANY(:messageThreadIds)', {
messageThreadIds: threadIdsWithoutWorkspaceMember,
messageThreadIds: messageThreadIds,
})
.getRawMany();
const visibilityValues = Object.values(MessageChannelVisibility);
const threadVisibilityByThreadIdForWhichWorkspaceMemberIsNotOwner:
| {
[key: string]: MessageChannelVisibility;
}
| undefined = threadVisibility?.reduce(
(threadVisibilityAcc, threadVisibility) => {
threadVisibilityAcc[threadVisibility.id] =
visibilityValues[
Math.max(
visibilityValues.indexOf(threadVisibility.visibility),
visibilityValues.indexOf(
threadVisibilityAcc[threadVisibility.id] ??
MessageChannelVisibility.METADATA,
),
)
];
return threadVisibilityAcc;
},
{},
);
const threadVisibilityByThreadId: {
[key: string]: MessageChannelVisibility;
} = messageThreadIds.reduce((threadVisibilityAcc, messageThreadId) => {
// If the workspace member is not the owner of the thread, use the visibility value from the query
// @ts-expect-error legacy noImplicitAny
threadVisibilityAcc[messageThreadId] =
threadIdsWithoutWorkspaceMember.includes(messageThreadId)
? (threadVisibilityByThreadIdForWhichWorkspaceMemberIsNotOwner?.[
messageThreadId
] ?? MessageChannelVisibility.METADATA)
: MessageChannelVisibility.SHARE_EVERYTHING;
} = threadVisibility.reduce((threadVisibilityAcc, threadVisibility) => {
if (threadVisibility.accountOwnerId === workspaceMemberId) {
threadVisibilityAcc[threadVisibility.id] =
MessageChannelVisibility.SHARE_EVERYTHING;
return threadVisibilityAcc;
}
threadVisibilityAcc[threadVisibility.id] =
visibilityValues[
Math.max(
visibilityValues.indexOf(threadVisibility.visibility),
visibilityValues.indexOf(
threadVisibilityAcc[threadVisibility.id] ??
MessageChannelVisibility.METADATA,
),
)
];
return threadVisibilityAcc;
}, {});
@@ -63,7 +63,7 @@ describe('computeParameters', () => {
required: false,
schema: {
type: 'integer',
enum: [0, 1, 2],
enum: [0, 1],
default: 1,
},
});
@@ -53,7 +53,7 @@ export const computeDepthParameters = (): OpenAPIV3_1.ParameterObject => {
required: false,
schema: {
type: 'integer',
enum: [0, 1, 2],
enum: [0, 1],
default: 1,
},
};
@@ -1,6 +1,43 @@
import { InputType, PartialType } from '@nestjs/graphql';
import { Field, InputType } from '@nestjs/graphql';
import { CreateViewInput } from './create-view.input';
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 { ViewOpenRecordIn } from 'src/engine/core-modules/view/enums/view-open-record-in';
import { ViewType } from 'src/engine/core-modules/view/enums/view-type.enum';
// TODO: this should be refactored like for view-field.input.ts
// This is a temporary fix as we were extending the CreateViewInput class which was adding default values for the non filled fields
@InputType()
export class UpdateViewInput extends PartialType(CreateViewInput) {}
export class UpdateViewInput {
@Field(() => UUIDScalarType, { nullable: true })
id: string;
@Field({ nullable: true })
name?: string;
@Field(() => ViewType, { nullable: true })
type?: ViewType;
@Field({ nullable: true })
icon?: string;
@Field({ nullable: true })
position?: number;
@Field({ nullable: true })
isCompact?: boolean;
@Field(() => ViewOpenRecordIn, {
nullable: true,
})
openRecordIn?: ViewOpenRecordIn;
@Field(() => AggregateOperations, { nullable: true })
kanbanAggregateOperation?: AggregateOperations;
@Field(() => UUIDScalarType, { nullable: true })
kanbanAggregateOperationFieldMetadataId?: string;
@Field({ nullable: true })
anyFieldFilterValue?: string;
}
@@ -103,9 +103,7 @@ export class FieldMetadataRelatedRecordsService {
);
if (!existingViewGroup) {
throw new Error(
`View group not found for option "${oldOption.value}" during update.`,
);
continue;
}
this.viewGroupService.update(
@@ -130,7 +130,6 @@ describe('WorkspaceEntityManager', () => {
IS_AI_ENABLED: false,
IS_IMAP_SMTP_CALDAV_ENABLED: false,
IS_MORPH_RELATION_ENABLED: false,
IS_WORKFLOW_BRANCH_ENABLED: false,
IS_RELATION_CONNECT_ENABLED: false,
IS_CORE_VIEW_SYNCING_ENABLED: false,
IS_CORE_VIEW_ENABLED: false,
@@ -158,7 +157,6 @@ describe('WorkspaceEntityManager', () => {
IS_AI_ENABLED: false,
IS_IMAP_SMTP_CALDAV_ENABLED: false,
IS_MORPH_RELATION_ENABLED: false,
IS_WORKFLOW_BRANCH_ENABLED: false,
IS_RELATION_CONNECT_ENABLED: false,
IS_CORE_VIEW_SYNCING_ENABLED: false,
IS_CORE_VIEW_ENABLED: false,
@@ -40,11 +40,6 @@ export const seedFeatureFlags = async (
workspaceId: workspaceId,
value: true,
},
{
key: FeatureFlagKey.IS_WORKFLOW_BRANCH_ENABLED,
workspaceId: workspaceId,
value: false,
},
{
key: FeatureFlagKey.IS_IMAP_SMTP_CALDAV_ENABLED,
workspaceId: workspaceId,
@@ -140,6 +140,6 @@ export class BlocklistItemDeleteMessagesJob {
}
}
await this.threadCleanerService.cleanWorkspaceThreads(workspaceId);
await this.threadCleanerService.cleanOrphanMessagesAndThreads(workspaceId);
}
}
@@ -0,0 +1,39 @@
import { InjectRepository } from '@nestjs/typeorm';
import { Command } from 'nest-commander';
import { Repository } from 'typeorm';
import {
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
type RunOnWorkspaceArgs,
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { MessagingMessageCleanerService } from 'src/modules/messaging/message-cleaner/services/messaging-message-cleaner.service';
@Command({
name: 'messaging:message-cleaner-remove-orphans',
description: 'Remove orphan message and threads from messaging',
})
export class MessagingMessageCleanerRemoveOrphansCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
constructor(
@InjectRepository(Workspace)
protected readonly workspaceRepository: Repository<Workspace>,
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly messagingMessageCleanerService: MessagingMessageCleanerService,
) {
super(workspaceRepository, twentyORMGlobalManager);
}
override async runOnWorkspace({
workspaceId,
}: RunOnWorkspaceArgs): Promise<void> {
try {
await this.messagingMessageCleanerService.cleanOrphanMessagesAndThreads(
workspaceId,
);
} catch (error) {
this.logger.error('Error while deleting workflowRun', error);
}
}
}
@@ -23,6 +23,8 @@ export class MessagingConnectedAccountDeletionCleanupJob {
async handle(
data: MessagingConnectedAccountDeletionCleanupJobData,
): Promise<void> {
await this.messageCleanerService.cleanWorkspaceThreads(data.workspaceId);
await this.messageCleanerService.cleanOrphanMessagesAndThreads(
data.workspaceId,
);
}
}
@@ -1,15 +1,19 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { MessagingMessageCleanerRemoveOrphansCommand } from 'src/modules/messaging/message-cleaner/commands/messaging-message-clearner-remove-orphans.command';
import { MessagingConnectedAccountDeletionCleanupJob } from 'src/modules/messaging/message-cleaner/jobs/messaging-connected-account-deletion-cleanup.job';
import { MessagingMessageCleanerConnectedAccountListener } from 'src/modules/messaging/message-cleaner/listeners/messaging-message-cleaner-connected-account.listener';
import { MessagingMessageCleanerService } from 'src/modules/messaging/message-cleaner/services/messaging-message-cleaner.service';
@Module({
imports: [],
imports: [TypeOrmModule.forFeature([Workspace])],
providers: [
MessagingMessageCleanerService,
MessagingConnectedAccountDeletionCleanupJob,
MessagingMessageCleanerConnectedAccountListener,
MessagingMessageCleanerRemoveOrphansCommand,
],
exports: [MessagingMessageCleanerService],
})
@@ -1,9 +1,11 @@
import { Injectable, Logger } from '@nestjs/common';
import { IsNull } from 'typeorm';
import chunk from 'lodash.chunk';
import { In, IsNull } from 'typeorm';
import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager';
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { MessageChannelMessageAssociationWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel-message-association.workspace-entity';
import { type MessageThreadWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-thread.workspace-entity';
import { type MessageWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message.workspace-entity';
import { deleteUsingPagination } from 'src/modules/messaging/message-cleaner/utils/delete-using-pagination.util';
@@ -11,20 +13,123 @@ import { deleteUsingPagination } from 'src/modules/messaging/message-cleaner/uti
@Injectable()
export class MessagingMessageCleanerService {
private readonly logger = new Logger(MessagingMessageCleanerService.name);
constructor(private readonly twentyORMManager: TwentyORMManager) {}
constructor(
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {}
async deleteMessagesChannelMessageAssociationsAndRelatedOrphans({
workspaceId,
messageExternalIds,
messageChannelId,
}: {
workspaceId: string;
messageExternalIds: string[];
messageChannelId: string;
}) {
const messageRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<MessageWorkspaceEntity>(
workspaceId,
'message',
);
const messageChannelMessageAssociationRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<MessageChannelMessageAssociationWorkspaceEntity>(
workspaceId,
'messageChannelMessageAssociation',
);
public async cleanWorkspaceThreads(workspaceId: string) {
const messageThreadRepository =
await this.twentyORMManager.getRepository<MessageThreadWorkspaceEntity>(
await this.twentyORMGlobalManager.getRepositoryForWorkspace<MessageThreadWorkspaceEntity>(
workspaceId,
'messageThread',
);
const messageExternalIdsChunks = chunk(messageExternalIds, 500);
for (const messageExternalIdsChunk of messageExternalIdsChunks) {
const messageChannelMessageAssociationsToDelete =
await messageChannelMessageAssociationRepository.find({
where: {
messageExternalId: In(messageExternalIdsChunk),
messageChannelId,
},
});
if (messageChannelMessageAssociationsToDelete.length <= 0) {
continue;
}
await messageChannelMessageAssociationRepository.delete(
messageChannelMessageAssociationsToDelete.map(({ id }) => id),
);
this.logger.log(
`WorkspaceId: ${workspaceId} Deleting ${messageChannelMessageAssociationsToDelete.length} message channel message associations`,
);
const orphanMessages = await messageRepository.find({
where: {
id: In(
messageChannelMessageAssociationsToDelete.map(
({ messageId }) => messageId,
),
),
messageChannelMessageAssociations: {
id: IsNull(),
},
},
});
if (orphanMessages.length <= 0) {
continue;
}
this.logger.log(
`WorkspaceId: ${workspaceId} Deleting ${orphanMessages.length} orphan messages`,
);
await messageRepository.delete(orphanMessages.map(({ id }) => id));
const orphanMessageThreads = await messageThreadRepository.find({
where: {
id: In(orphanMessages.map(({ messageThreadId }) => messageThreadId)),
messages: {
id: IsNull(),
},
},
});
if (orphanMessageThreads.length <= 0) {
continue;
}
this.logger.log(
`WorkspaceId: ${workspaceId} Deleting ${orphanMessageThreads.length} orphan message threads`,
);
await messageThreadRepository.delete(
orphanMessageThreads.map(({ id }) => id),
);
}
}
public async cleanOrphanMessagesAndThreads(workspaceId: string) {
const messageThreadRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<MessageThreadWorkspaceEntity>(
workspaceId,
'messageThread',
);
const messageRepository =
await this.twentyORMManager.getRepository<MessageWorkspaceEntity>(
await this.twentyORMGlobalManager.getRepositoryForWorkspace<MessageWorkspaceEntity>(
workspaceId,
'message',
);
const workspaceDataSource = await this.twentyORMManager.getDatasource();
const workspaceDataSource =
await this.twentyORMGlobalManager.getDataSourceForWorkspace({
workspaceId,
});
await workspaceDataSource.transaction(
async (transactionManager: WorkspaceEntityManager) => {
@@ -1,18 +1,14 @@
import { Injectable, Logger } from '@nestjs/common';
import { gmail_v1 } from 'googleapis';
import { isDefined } from 'twenty-shared/utils';
import {
MessageFolder,
MessageFolderDriver,
} from 'src/modules/messaging/message-folder-manager/interfaces/message-folder-driver.interface';
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
import { MESSAGING_GMAIL_EXCLUDED_CATEGORIES } from 'src/modules/messaging/message-import-manager/drivers/gmail/constants/messaging-gmail-excluded-categories';
import { MESSAGING_GMAIL_DEFAULT_NOT_SYNCED_LABELS } from 'src/modules/messaging/message-import-manager/drivers/gmail/constants/messaging-gmail-default-not-synced-labels';
import { GmailClientProvider } from 'src/modules/messaging/message-import-manager/drivers/gmail/providers/gmail-client.provider';
import { GmailHandleErrorService } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-handle-error.service';
import { computeGmailCategoryLabelId } from 'src/modules/messaging/message-import-manager/drivers/gmail/utils/compute-gmail-category-label-id.util';
@Injectable()
export class GmailGetAllFoldersService implements MessageFolderDriver {
@@ -23,24 +19,8 @@ export class GmailGetAllFoldersService implements MessageFolderDriver {
private readonly gmailHandleErrorService: GmailHandleErrorService,
) {}
private isExcludedCategoryFolder(labelId: string): boolean {
const excludedCategoryIds = MESSAGING_GMAIL_EXCLUDED_CATEGORIES.map(
(category) => computeGmailCategoryLabelId(category),
);
return excludedCategoryIds.includes(labelId);
}
private isIncludedFolder(label: gmail_v1.Schema$Label): boolean {
if (!isDefined(label.id)) {
return false;
}
const isTargetSystemFolder =
label.type === 'system' && (label.id === 'INBOX' || label.id === 'SENT');
const isUserFolder = label.type === 'user';
return isTargetSystemFolder || isUserFolder;
private isSyncedByDefault(labelId: string): boolean {
return !MESSAGING_GMAIL_DEFAULT_NOT_SYNCED_LABELS.includes(labelId);
}
async getAllMessageFolders(
@@ -74,21 +54,12 @@ export class GmailGetAllFoldersService implements MessageFolderDriver {
continue;
}
if (this.isExcludedCategoryFolder(label.id)) {
continue;
}
if (!this.isIncludedFolder(label)) {
continue;
}
const isSentFolder = label.id === 'SENT';
const isSyncedByDefault = label.id === 'INBOX' || label.id === 'SENT';
folders.push({
externalId: label.id,
name: label.name,
isSynced: isSyncedByDefault,
isSynced: this.isSyncedByDefault(label.id),
isSentFolder,
});
}
@@ -69,6 +69,7 @@ export class SyncMessageFoldersService {
messageFolderRepository,
});
// TODO: we should delete folders that are not in the list anymore
for (const folder of folders) {
const existingFolder = this.findExistingFolderInMap(
existingFolderMap,
@@ -80,8 +81,6 @@ export class SyncMessageFoldersService {
existingFolder.id,
{
name: folder.name,
isSynced: folder.isSynced,
isSentFolder: folder.isSentFolder,
externalId: folder.externalId,
},
manager,
@@ -0,0 +1,10 @@
export const MESSAGING_GMAIL_DEFAULT_NOT_SYNCED_LABELS = [
'CATEGORY_PROMOTIONS',
'CATEGORY_SOCIAL',
'CATEGORY_FORUMS',
'CATEGORY_UPDATES',
'TRASH',
'SPAM',
'DRAFT',
'CHAT',
];

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