Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4e0943d5d7 | |||
| f473984460 | |||
| 288a120b23 | |||
| 9828f27bbd | |||
| f0f12a0643 | |||
| a747833366 | |||
| 50a94f5659 | |||
| 38f6ca2afe | |||
| 2accf0e0c7 | |||
| f32f1bc1ff | |||
| 698dc9585f | |||
| fbd4ceb2ca | |||
| b1a827e66e | |||
| ac1cfba3b2 | |||
| 914f286a1f | |||
| 1d9a1c69e4 |
@@ -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>;
|
||||
|
||||
-7
@@ -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
-2
@@ -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 ?? '',
|
||||
}),
|
||||
);
|
||||
|
||||
+2
-2
@@ -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
-2
@@ -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,
|
||||
}),
|
||||
);
|
||||
|
||||
+6
-1
@@ -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],
|
||||
|
||||
+43
@@ -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;
|
||||
};
|
||||
+35
-8
@@ -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;
|
||||
|
||||
+2
-2
@@ -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,
|
||||
}),
|
||||
);
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+35
-13
@@ -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(
|
||||
|
||||
+30
-1
@@ -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 (
|
||||
|
||||
+2
-2
@@ -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 ?? '',
|
||||
}),
|
||||
);
|
||||
|
||||
+12
-2
@@ -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);
|
||||
},
|
||||
|
||||
+1
-1
@@ -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,
|
||||
});
|
||||
|
||||
+3
-3
@@ -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`}
|
||||
/>
|
||||
)}
|
||||
|
||||
+6
-4
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
-19
@@ -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);
|
||||
},
|
||||
});
|
||||
+1
-1
@@ -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 ?? '',
|
||||
}),
|
||||
);
|
||||
|
||||
+2
-2
@@ -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 ?? '',
|
||||
}),
|
||||
);
|
||||
|
||||
+12
-45
@@ -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 {
|
||||
|
||||
+2
-2
@@ -16,7 +16,7 @@ import { type ViewFilter } from '@/views/types/ViewFilter';
|
||||
import { convertViewFilterOperandToCore } from '@/views/utils/convertViewFilterOperandToCore';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { isNull } from '@sniptt/guards';
|
||||
import { isDefined, 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,
|
||||
|
||||
+32
-25
@@ -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 {
|
||||
|
||||
+2
-2
@@ -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 ?? '',
|
||||
}),
|
||||
)
|
||||
|
||||
+2
-2
@@ -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 ?? '',
|
||||
}),
|
||||
)
|
||||
|
||||
+2
-2
@@ -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 ?? '',
|
||||
}),
|
||||
)
|
||||
|
||||
+2
-2
@@ -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,
|
||||
}),
|
||||
)
|
||||
|
||||
+1
-1
@@ -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';
|
||||
|
||||
+4
-4
@@ -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 ?? '',
|
||||
}),
|
||||
);
|
||||
|
||||
+144
@@ -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,
|
||||
};
|
||||
};
|
||||
+142
@@ -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,
|
||||
};
|
||||
};
|
||||
+2
-2
@@ -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 }) => {
|
||||
+2
-2
@@ -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 }) => {
|
||||
+21
@@ -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);
|
||||
},
|
||||
});
|
||||
+2
-2
@@ -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);
|
||||
|
||||
+2
-2
@@ -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 '';
|
||||
|
||||
+2
-2
@@ -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,
|
||||
}),
|
||||
);
|
||||
|
||||
+2
-2
@@ -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 ?? '',
|
||||
}),
|
||||
);
|
||||
|
||||
+2
-2
@@ -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,
|
||||
}),
|
||||
);
|
||||
|
||||
+2
-2
@@ -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,
|
||||
}),
|
||||
);
|
||||
|
||||
-8
@@ -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,
|
||||
],
|
||||
|
||||
+13
-32
@@ -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
-11
@@ -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({
|
||||
|
||||
+1
-12
@@ -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;
|
||||
|
||||
-8
@@ -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,
|
||||
|
||||
+1
-8
@@ -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)) {
|
||||
|
||||
-10
@@ -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(`
|
||||
|
||||
-87
@@ -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: {},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+1
-17
@@ -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;
|
||||
|
||||
-3
@@ -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,
|
||||
};
|
||||
|
||||
-4
@@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
+2
-4
@@ -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',
|
||||
|
||||
+8
-18
@@ -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}
|
||||
/>
|
||||
|
||||
+5
-12
@@ -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>
|
||||
}
|
||||
|
||||
+1
-7
@@ -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}
|
||||
|
||||
-7
@@ -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
-8
@@ -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,
|
||||
|
||||
+1
@@ -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);
|
||||
|
||||
+2
@@ -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',
|
||||
}
|
||||
|
||||
+18
-1
@@ -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
|
||||
|
||||
+64
-13
@@ -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();
|
||||
|
||||
|
||||
+2
@@ -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
-1
@@ -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}` },
|
||||
|
||||
+1
-1
@@ -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 {
|
||||
|
||||
-8
@@ -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: {
|
||||
|
||||
-1
@@ -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',
|
||||
|
||||
+23
-55
@@ -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;
|
||||
}, {});
|
||||
|
||||
+1
-1
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
+40
-3
@@ -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;
|
||||
}
|
||||
|
||||
+1
-3
@@ -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(
|
||||
|
||||
-2
@@ -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,
|
||||
|
||||
-5
@@ -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,
|
||||
|
||||
+1
-1
@@ -140,6 +140,6 @@ export class BlocklistItemDeleteMessagesJob {
|
||||
}
|
||||
}
|
||||
|
||||
await this.threadCleanerService.cleanWorkspaceThreads(workspaceId);
|
||||
await this.threadCleanerService.cleanOrphanMessagesAndThreads(workspaceId);
|
||||
}
|
||||
}
|
||||
|
||||
+39
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+5
-1
@@ -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],
|
||||
})
|
||||
|
||||
+112
-7
@@ -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) => {
|
||||
|
||||
+4
-33
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
+1
-2
@@ -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,
|
||||
|
||||
+10
@@ -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
Reference in New Issue
Block a user