mirror of
https://github.com/twentyhq/twenty.git
synced 2024-12-23 03:51:36 +03:00
Merge branch 'main' into introduce-free-pass
This commit is contained in:
commit
55b692d7a8
8
.github/workflows/ci-demo-check.yml
vendored
8
.github/workflows/ci-demo-check.yml
vendored
@ -1,7 +1,8 @@
|
||||
name: CI demo check
|
||||
name: CI Demo check
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 7,19 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
@ -11,6 +12,9 @@ jobs:
|
||||
test:
|
||||
timeout-minutes: 15
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./packages/twenty-e2e-testing
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@ -27,7 +31,7 @@ jobs:
|
||||
|
||||
- name: Run Playwright tests
|
||||
id: test
|
||||
run: yarn playwright test --grep @demo-only
|
||||
run: yarn playwright test --grep "@demo-only"
|
||||
|
||||
- name: Upload report after tests
|
||||
uses: actions/upload-artifact@v4
|
||||
|
@ -23,6 +23,14 @@ const pagesCoverage = {
|
||||
exclude: ['src/generated/**/*', 'src/modules/**/*', 'src/**/*.ts'],
|
||||
};
|
||||
|
||||
const performanceCoverage = {
|
||||
branches: 35,
|
||||
statements: 60,
|
||||
lines: 60,
|
||||
functions: 45,
|
||||
exclude: ['src/generated/**/*', 'src/modules/**/*', 'src/**/*.ts'],
|
||||
};
|
||||
|
||||
const storybookStoriesFolders = process.env.STORYBOOK_SCOPE;
|
||||
|
||||
module.exports =
|
||||
@ -30,4 +38,6 @@ module.exports =
|
||||
? pagesCoverage
|
||||
: storybookStoriesFolders === 'modules'
|
||||
? modulesCoverage
|
||||
: storybookStoriesFolders === 'performance'
|
||||
? performanceCoverage
|
||||
: globalCoverage;
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { MultipleRecordsActionKeys } from '@/action-menu/actions/record-actions/multiple-records/types/MultipleRecordsActionKeys';
|
||||
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
|
||||
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
|
||||
import {
|
||||
@ -8,19 +9,17 @@ import { contextStoreFiltersComponentState } from '@/context-store/states/contex
|
||||
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters';
|
||||
import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite';
|
||||
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { DEFAULT_QUERY_PAGE_SIZE } from '@/object-record/constants/DefaultQueryPageSize';
|
||||
import { DELETE_MAX_COUNT } from '@/object-record/constants/DeleteMaxCount';
|
||||
import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords';
|
||||
import { useLazyFetchAllRecords } from '@/object-record/hooks/useLazyFetchAllRecords';
|
||||
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
||||
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
|
||||
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useCallback, useContext, useState } from 'react';
|
||||
import { IconTrash, isDefined } from 'twenty-ui';
|
||||
import { useLazyFetchAllRecords } from '@/object-record/hooks/useLazyFetchAllRecords';
|
||||
import { DEFAULT_QUERY_PAGE_SIZE } from '@/object-record/constants/DefaultQueryPageSize';
|
||||
|
||||
export const useDeleteMultipleRecordsAction = ({
|
||||
objectMetadataItem,
|
||||
@ -40,9 +39,6 @@ export const useDeleteMultipleRecordsAction = ({
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
});
|
||||
|
||||
const { sortedFavorites: favorites } = useFavorites();
|
||||
const { deleteFavorite } = useDeleteFavorite();
|
||||
|
||||
const contextStoreNumberOfSelectedRecords = useRecoilComponentValueV2(
|
||||
contextStoreNumberOfSelectedRecordsComponentState,
|
||||
);
|
||||
@ -76,26 +72,8 @@ export const useDeleteMultipleRecordsAction = ({
|
||||
|
||||
resetTableRowSelection();
|
||||
|
||||
for (const recordIdToDelete of recordIdsToDelete) {
|
||||
const foundFavorite = favorites?.find(
|
||||
(favorite) => favorite.recordId === recordIdToDelete,
|
||||
);
|
||||
|
||||
if (foundFavorite !== undefined) {
|
||||
deleteFavorite(foundFavorite.id);
|
||||
}
|
||||
}
|
||||
|
||||
await deleteManyRecords(recordIdsToDelete, {
|
||||
delayInMsBetweenRequests: 50,
|
||||
});
|
||||
}, [
|
||||
deleteFavorite,
|
||||
deleteManyRecords,
|
||||
favorites,
|
||||
fetchAllRecordIds,
|
||||
resetTableRowSelection,
|
||||
]);
|
||||
await deleteManyRecords(recordIdsToDelete);
|
||||
}, [deleteManyRecords, fetchAllRecordIds, resetTableRowSelection]);
|
||||
|
||||
const isRemoteObject = objectMetadataItem.isRemote;
|
||||
|
||||
@ -105,7 +83,7 @@ export const useDeleteMultipleRecordsAction = ({
|
||||
contextStoreNumberOfSelectedRecords < DELETE_MAX_COUNT &&
|
||||
contextStoreNumberOfSelectedRecords > 0;
|
||||
|
||||
const { isInRightDrawer, onActionExecutedCallback } =
|
||||
const { isInRightDrawer, onActionStartedCallback, onActionExecutedCallback } =
|
||||
useContext(ActionMenuContext);
|
||||
|
||||
const registerDeleteMultipleRecordsAction = ({
|
||||
@ -117,8 +95,9 @@ export const useDeleteMultipleRecordsAction = ({
|
||||
addActionMenuEntry({
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
key: 'delete-multiple-records',
|
||||
label: 'Delete',
|
||||
key: MultipleRecordsActionKeys.DELETE,
|
||||
label: 'Delete records',
|
||||
shortLabel: 'Delete',
|
||||
position,
|
||||
Icon: IconTrash,
|
||||
accent: 'danger',
|
||||
@ -132,9 +111,14 @@ export const useDeleteMultipleRecordsAction = ({
|
||||
setIsOpen={setIsDeleteRecordsModalOpen}
|
||||
title={'Delete Records'}
|
||||
subtitle={`Are you sure you want to delete these records? They can be recovered from the Options menu.`}
|
||||
onConfirmClick={() => {
|
||||
handleDeleteClick();
|
||||
onActionExecutedCallback?.();
|
||||
onConfirmClick={async () => {
|
||||
onActionStartedCallback?.({
|
||||
key: 'delete-multiple-records',
|
||||
});
|
||||
await handleDeleteClick();
|
||||
onActionExecutedCallback?.({
|
||||
key: 'delete-multiple-records',
|
||||
});
|
||||
if (isInRightDrawer) {
|
||||
closeRightDrawer();
|
||||
}
|
||||
|
@ -2,6 +2,8 @@ import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { IconDatabaseExport } from 'twenty-ui';
|
||||
|
||||
import { MultipleRecordsActionKeys } from '@/action-menu/actions/record-actions/multiple-records/types/MultipleRecordsActionKeys';
|
||||
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
|
||||
import {
|
||||
ActionMenuEntryScope,
|
||||
ActionMenuEntryType,
|
||||
@ -10,6 +12,7 @@ import {
|
||||
displayedExportProgress,
|
||||
useExportRecords,
|
||||
} from '@/object-record/record-index/export/hooks/useExportRecords';
|
||||
import { useContext } from 'react';
|
||||
|
||||
export const useExportMultipleRecordsAction = ({
|
||||
objectMetadataItem,
|
||||
@ -25,6 +28,9 @@ export const useExportMultipleRecordsAction = ({
|
||||
filename: `${objectMetadataItem.nameSingular}.csv`,
|
||||
});
|
||||
|
||||
const { onActionStartedCallback, onActionExecutedCallback } =
|
||||
useContext(ActionMenuContext);
|
||||
|
||||
const registerExportMultipleRecordsAction = ({
|
||||
position,
|
||||
}: {
|
||||
@ -33,12 +39,21 @@ export const useExportMultipleRecordsAction = ({
|
||||
addActionMenuEntry({
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
key: 'export-multiple-records',
|
||||
key: MultipleRecordsActionKeys.EXPORT,
|
||||
position,
|
||||
label: displayedExportProgress(progress),
|
||||
shortLabel: 'Export',
|
||||
Icon: IconDatabaseExport,
|
||||
accent: 'default',
|
||||
onClick: () => download(),
|
||||
onClick: async () => {
|
||||
await onActionStartedCallback?.({
|
||||
key: MultipleRecordsActionKeys.EXPORT,
|
||||
});
|
||||
await download();
|
||||
await onActionExecutedCallback?.({
|
||||
key: MultipleRecordsActionKeys.EXPORT,
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -0,0 +1,4 @@
|
||||
export enum MultipleRecordsActionKeys {
|
||||
DELETE = 'delete-multiple-records',
|
||||
EXPORT = 'export-multiple-records',
|
||||
}
|
@ -2,6 +2,7 @@ import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { IconDatabaseExport } from 'twenty-ui';
|
||||
|
||||
import { NoSelectionRecordActionKeys } from '@/action-menu/actions/record-actions/no-selection/types/NoSelectionRecordActionsKey';
|
||||
import {
|
||||
ActionMenuEntryScope,
|
||||
ActionMenuEntryType,
|
||||
@ -33,7 +34,7 @@ export const useExportViewNoSelectionRecordAction = ({
|
||||
addActionMenuEntry({
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.Global,
|
||||
key: 'export-view-no-selection',
|
||||
key: NoSelectionRecordActionKeys.EXPORT_VIEW,
|
||||
position,
|
||||
label: displayedExportProgress(progress),
|
||||
Icon: IconDatabaseExport,
|
||||
|
@ -0,0 +1,3 @@
|
||||
export enum NoSelectionRecordActionKeys {
|
||||
EXPORT_VIEW = 'export-view-no-selection',
|
||||
}
|
@ -1,11 +1,13 @@
|
||||
import { getActionConfig } from '@/action-menu/actions/record-actions/single-record/utils/getActionConfig';
|
||||
import { ActionAvailableOn } from '@/action-menu/actions/types/actionAvailableOn';
|
||||
import { ActionAvailableOn } from '@/action-menu/actions/types/ActionAvailableOn';
|
||||
import { wrapActionInCallbacks } from '@/action-menu/actions/utils/wrapActionInCallbacks';
|
||||
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
|
||||
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { useEffect } from 'react';
|
||||
import { useContext, useEffect } from 'react';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
export const ShowPageSingleRecordActionMenuEntrySetterEffect = ({
|
||||
@ -36,6 +38,8 @@ export const ShowPageSingleRecordActionMenuEntrySetterEffect = ({
|
||||
if (!isDefined(selectedRecordId)) {
|
||||
throw new Error('Selected record ID is required');
|
||||
}
|
||||
const { onActionStartedCallback, onActionExecutedCallback } =
|
||||
useContext(ActionMenuContext);
|
||||
|
||||
const actionMenuEntries = Object.values(actionConfig ?? {})
|
||||
.filter((action) =>
|
||||
@ -48,15 +52,21 @@ export const ShowPageSingleRecordActionMenuEntrySetterEffect = ({
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
if (shouldBeRegistered) {
|
||||
return {
|
||||
if (!shouldBeRegistered) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const wrappedAction = wrapActionInCallbacks({
|
||||
action: {
|
||||
...action,
|
||||
onClick,
|
||||
ConfirmationModal,
|
||||
};
|
||||
}
|
||||
},
|
||||
onActionStartedCallback,
|
||||
onActionExecutedCallback,
|
||||
});
|
||||
|
||||
return undefined;
|
||||
return wrappedAction;
|
||||
})
|
||||
.filter(isDefined);
|
||||
|
||||
|
@ -1,11 +1,13 @@
|
||||
import { getActionConfig } from '@/action-menu/actions/record-actions/single-record/utils/getActionConfig';
|
||||
import { ActionAvailableOn } from '@/action-menu/actions/types/actionAvailableOn';
|
||||
import { ActionAvailableOn } from '@/action-menu/actions/types/ActionAvailableOn';
|
||||
import { wrapActionInCallbacks } from '@/action-menu/actions/utils/wrapActionInCallbacks';
|
||||
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
|
||||
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { useEffect } from 'react';
|
||||
import { useContext, useEffect } from 'react';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
export const SingleRecordActionMenuEntrySetterEffect = ({
|
||||
@ -37,6 +39,9 @@ export const SingleRecordActionMenuEntrySetterEffect = ({
|
||||
throw new Error('Selected record ID is required');
|
||||
}
|
||||
|
||||
const { onActionStartedCallback, onActionExecutedCallback } =
|
||||
useContext(ActionMenuContext);
|
||||
|
||||
const actionMenuEntries = Object.values(actionConfig ?? {})
|
||||
.filter((action) =>
|
||||
action.availableOn?.includes(
|
||||
@ -50,15 +55,21 @@ export const SingleRecordActionMenuEntrySetterEffect = ({
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
if (shouldBeRegistered) {
|
||||
return {
|
||||
if (!shouldBeRegistered) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const wrappedAction = wrapActionInCallbacks({
|
||||
action: {
|
||||
...action,
|
||||
onClick,
|
||||
ConfirmationModal,
|
||||
};
|
||||
}
|
||||
},
|
||||
onActionStartedCallback,
|
||||
onActionExecutedCallback,
|
||||
});
|
||||
|
||||
return undefined;
|
||||
return wrappedAction;
|
||||
})
|
||||
.filter(isDefined);
|
||||
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { useAddToFavoritesSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useAddToFavoritesSingleRecordAction';
|
||||
import { useDeleteSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useDeleteSingleRecordAction';
|
||||
import { useRemoveFromFavoritesSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useRemoveFromFavoritesSingleRecordAction';
|
||||
import { ActionAvailableOn } from '@/action-menu/actions/types/actionAvailableOn';
|
||||
import { SingleRecordActionHook } from '@/action-menu/actions/types/singleRecordActionHook';
|
||||
import { SingleRecordActionKeys } from '@/action-menu/actions/record-actions/single-record/types/SingleRecordActionsKey';
|
||||
import { ActionAvailableOn } from '@/action-menu/actions/types/ActionAvailableOn';
|
||||
import { SingleRecordActionHook } from '@/action-menu/actions/types/SingleRecordActionHook';
|
||||
import {
|
||||
ActionMenuEntry,
|
||||
ActionMenuEntryScope,
|
||||
@ -19,7 +20,7 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V1: Record<
|
||||
addToFavoritesSingleRecord: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
key: 'add-to-favorites-single-record',
|
||||
key: SingleRecordActionKeys.ADD_TO_FAVORITES,
|
||||
label: 'Add to favorites',
|
||||
position: 0,
|
||||
Icon: IconHeart,
|
||||
@ -32,7 +33,7 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V1: Record<
|
||||
removeFromFavoritesSingleRecord: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
key: 'remove-from-favorites-single-record',
|
||||
key: SingleRecordActionKeys.REMOVE_FROM_FAVORITES,
|
||||
label: 'Remove from favorites',
|
||||
position: 1,
|
||||
Icon: IconHeartOff,
|
||||
@ -45,7 +46,7 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V1: Record<
|
||||
deleteSingleRecord: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
key: 'delete-single-record',
|
||||
key: SingleRecordActionKeys.DELETE,
|
||||
label: 'Delete',
|
||||
position: 2,
|
||||
Icon: IconTrash,
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { useAddToFavoritesSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useAddToFavoritesSingleRecordAction';
|
||||
import { useDeleteSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useDeleteSingleRecordAction';
|
||||
import { useDestroySingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useDestroySingleRecordAction';
|
||||
import { useNavigateToNextRecordSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useNavigateToNextRecordSingleRecordAction';
|
||||
import { useNavigateToPreviousRecordSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useNavigateToPreviousRecordSingleRecordAction';
|
||||
import { useRemoveFromFavoritesSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useRemoveFromFavoritesSingleRecordAction';
|
||||
import { ActionAvailableOn } from '@/action-menu/actions/types/actionAvailableOn';
|
||||
import { SingleRecordActionHook } from '@/action-menu/actions/types/singleRecordActionHook';
|
||||
import { SingleRecordActionKeys } from '@/action-menu/actions/record-actions/single-record/types/SingleRecordActionsKey';
|
||||
import { ActionAvailableOn } from '@/action-menu/actions/types/ActionAvailableOn';
|
||||
import { SingleRecordActionHook } from '@/action-menu/actions/types/SingleRecordActionHook';
|
||||
import {
|
||||
ActionMenuEntry,
|
||||
ActionMenuEntryScope,
|
||||
@ -16,6 +18,7 @@ import {
|
||||
IconHeart,
|
||||
IconHeartOff,
|
||||
IconTrash,
|
||||
IconTrashX,
|
||||
} from 'twenty-ui';
|
||||
|
||||
export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record<
|
||||
@ -27,7 +30,7 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record<
|
||||
addToFavoritesSingleRecord: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
key: 'add-to-favorites-single-record',
|
||||
key: SingleRecordActionKeys.ADD_TO_FAVORITES,
|
||||
label: 'Add to favorites',
|
||||
shortLabel: 'Add to favorites',
|
||||
position: 0,
|
||||
@ -42,7 +45,7 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record<
|
||||
removeFromFavoritesSingleRecord: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
key: 'remove-from-favorites-single-record',
|
||||
key: SingleRecordActionKeys.REMOVE_FROM_FAVORITES,
|
||||
label: 'Remove from favorites',
|
||||
shortLabel: 'Remove from favorites',
|
||||
isPinned: true,
|
||||
@ -57,7 +60,7 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record<
|
||||
deleteSingleRecord: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
key: 'delete-single-record',
|
||||
key: SingleRecordActionKeys.DELETE,
|
||||
label: 'Delete record',
|
||||
shortLabel: 'Delete',
|
||||
position: 2,
|
||||
@ -70,13 +73,29 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record<
|
||||
],
|
||||
actionHook: useDeleteSingleRecordAction,
|
||||
},
|
||||
destroySingleRecord: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
key: SingleRecordActionKeys.DESTROY,
|
||||
label: 'Permanently destroy record',
|
||||
shortLabel: 'Destroy',
|
||||
position: 3,
|
||||
Icon: IconTrashX,
|
||||
accent: 'danger',
|
||||
isPinned: true,
|
||||
availableOn: [
|
||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionAvailableOn.SHOW_PAGE,
|
||||
],
|
||||
actionHook: useDestroySingleRecordAction,
|
||||
},
|
||||
navigateToPreviousRecord: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
key: 'navigate-to-previous-record',
|
||||
key: SingleRecordActionKeys.NAVIGATE_TO_PREVIOUS_RECORD,
|
||||
label: 'Navigate to previous record',
|
||||
shortLabel: '',
|
||||
position: 3,
|
||||
position: 4,
|
||||
isPinned: true,
|
||||
Icon: IconChevronUp,
|
||||
availableOn: [ActionAvailableOn.SHOW_PAGE],
|
||||
@ -85,10 +104,10 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record<
|
||||
navigateToNextRecord: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
key: 'navigate-to-next-record',
|
||||
key: SingleRecordActionKeys.NAVIGATE_TO_NEXT_RECORD,
|
||||
label: 'Navigate to next record',
|
||||
shortLabel: '',
|
||||
position: 4,
|
||||
position: 5,
|
||||
isPinned: true,
|
||||
Icon: IconChevronDown,
|
||||
availableOn: [ActionAvailableOn.SHOW_PAGE],
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { SingleRecordActionHookWithObjectMetadataItem } from '@/action-menu/actions/types/singleRecordActionHook';
|
||||
import { SingleRecordActionHookWithObjectMetadataItem } from '@/action-menu/actions/types/SingleRecordActionHook';
|
||||
import { useCreateFavorite } from '@/favorites/hooks/useCreateFavorite';
|
||||
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { isNull } from '@sniptt/guards';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
@ -23,7 +24,8 @@ export const useAddToFavoritesSingleRecordAction: SingleRecordActionHookWithObje
|
||||
isDefined(objectMetadataItem) &&
|
||||
isDefined(selectedRecord) &&
|
||||
!objectMetadataItem.isRemote &&
|
||||
!isFavorite;
|
||||
!isFavorite &&
|
||||
isNull(selectedRecord.deletedAt);
|
||||
|
||||
const onClick = () => {
|
||||
if (!shouldBeRegistered) {
|
||||
|
@ -1,12 +1,15 @@
|
||||
import { SingleRecordActionHookWithObjectMetadataItem } from '@/action-menu/actions/types/singleRecordActionHook';
|
||||
import { SingleRecordActionHookWithObjectMetadataItem } from '@/action-menu/actions/types/SingleRecordActionHook';
|
||||
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
|
||||
import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite';
|
||||
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
||||
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
||||
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
|
||||
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
|
||||
import { isNull } from '@sniptt/guards';
|
||||
import { useCallback, useContext, useState } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
export const useDeleteSingleRecordAction: SingleRecordActionHookWithObjectMetadataItem =
|
||||
@ -22,6 +25,8 @@ export const useDeleteSingleRecordAction: SingleRecordActionHookWithObjectMetada
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
});
|
||||
|
||||
const selectedRecord = useRecoilValue(recordStoreFamilyState(recordId));
|
||||
|
||||
const { sortedFavorites: favorites } = useFavorites();
|
||||
const { deleteFavorite } = useDeleteFavorite();
|
||||
|
||||
@ -49,10 +54,10 @@ export const useDeleteSingleRecordAction: SingleRecordActionHookWithObjectMetada
|
||||
|
||||
const isRemoteObject = objectMetadataItem.isRemote;
|
||||
|
||||
const { isInRightDrawer, onActionExecutedCallback } =
|
||||
useContext(ActionMenuContext);
|
||||
const { isInRightDrawer } = useContext(ActionMenuContext);
|
||||
|
||||
const shouldBeRegistered = !isRemoteObject;
|
||||
const shouldBeRegistered =
|
||||
!isRemoteObject && isNull(selectedRecord?.deletedAt);
|
||||
|
||||
const onClick = () => {
|
||||
if (!shouldBeRegistered) {
|
||||
@ -75,7 +80,6 @@ export const useDeleteSingleRecordAction: SingleRecordActionHookWithObjectMetada
|
||||
}
|
||||
onConfirmClick={() => {
|
||||
handleDeleteClick();
|
||||
onActionExecutedCallback?.();
|
||||
if (isInRightDrawer) {
|
||||
closeRightDrawer();
|
||||
}
|
||||
|
@ -0,0 +1,71 @@
|
||||
import { SingleRecordActionHookWithObjectMetadataItem } from '@/action-menu/actions/types/SingleRecordActionHook';
|
||||
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
|
||||
import { useDestroyOneRecord } from '@/object-record/hooks/useDestroyOneRecord';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
||||
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
|
||||
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
|
||||
import { useCallback, useContext, useState } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
export const useDestroySingleRecordAction: SingleRecordActionHookWithObjectMetadataItem =
|
||||
({ recordId, objectMetadataItem }) => {
|
||||
const [isDestroyRecordsModalOpen, setIsDestroyRecordsModalOpen] =
|
||||
useState(false);
|
||||
|
||||
const { resetTableRowSelection } = useRecordTable({
|
||||
recordTableId: objectMetadataItem.namePlural,
|
||||
});
|
||||
|
||||
const { destroyOneRecord } = useDestroyOneRecord({
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
});
|
||||
|
||||
const selectedRecord = useRecoilValue(recordStoreFamilyState(recordId));
|
||||
|
||||
const { closeRightDrawer } = useRightDrawer();
|
||||
|
||||
const handleDeleteClick = useCallback(async () => {
|
||||
resetTableRowSelection();
|
||||
|
||||
await destroyOneRecord(recordId);
|
||||
}, [resetTableRowSelection, destroyOneRecord, recordId]);
|
||||
|
||||
const isRemoteObject = objectMetadataItem.isRemote;
|
||||
|
||||
const { isInRightDrawer } = useContext(ActionMenuContext);
|
||||
|
||||
const shouldBeRegistered =
|
||||
!isRemoteObject && isDefined(selectedRecord?.deletedAt);
|
||||
|
||||
const onClick = () => {
|
||||
if (!shouldBeRegistered) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsDestroyRecordsModalOpen(true);
|
||||
};
|
||||
|
||||
return {
|
||||
shouldBeRegistered,
|
||||
onClick,
|
||||
ConfirmationModal: (
|
||||
<ConfirmationModal
|
||||
isOpen={isDestroyRecordsModalOpen}
|
||||
setIsOpen={setIsDestroyRecordsModalOpen}
|
||||
title={'Permanently Destroy Record'}
|
||||
subtitle={
|
||||
'Are you sure you want to destroy this record? It cannot be recovered anymore.'
|
||||
}
|
||||
onConfirmClick={async () => {
|
||||
await handleDeleteClick();
|
||||
if (isInRightDrawer) {
|
||||
closeRightDrawer();
|
||||
}
|
||||
}}
|
||||
deleteButtonText={'Permanently Destroy Record'}
|
||||
/>
|
||||
),
|
||||
};
|
||||
};
|
@ -1,4 +1,4 @@
|
||||
import { SingleRecordActionHookWithObjectMetadataItem } from '@/action-menu/actions/types/singleRecordActionHook';
|
||||
import { SingleRecordActionHookWithObjectMetadataItem } from '@/action-menu/actions/types/SingleRecordActionHook';
|
||||
import { useRecordShowPagePagination } from '@/object-record/record-show/hooks/useRecordShowPagePagination';
|
||||
|
||||
export const useNavigateToNextRecordSingleRecordAction: SingleRecordActionHookWithObjectMetadataItem =
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { SingleRecordActionHookWithObjectMetadataItem } from '@/action-menu/actions/types/singleRecordActionHook';
|
||||
import { SingleRecordActionHookWithObjectMetadataItem } from '@/action-menu/actions/types/SingleRecordActionHook';
|
||||
import { useRecordShowPagePagination } from '@/object-record/record-show/hooks/useRecordShowPagePagination';
|
||||
|
||||
export const useNavigateToPreviousRecordSingleRecordAction: SingleRecordActionHookWithObjectMetadataItem =
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { SingleRecordActionHookWithObjectMetadataItem } from '@/action-menu/actions/types/singleRecordActionHook';
|
||||
import { SingleRecordActionHookWithObjectMetadataItem } from '@/action-menu/actions/types/SingleRecordActionHook';
|
||||
import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite';
|
||||
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
@ -0,0 +1,8 @@
|
||||
export enum SingleRecordActionKeys {
|
||||
DELETE = 'delete-single-record',
|
||||
DESTROY = 'destroy-single-record',
|
||||
ADD_TO_FAVORITES = 'add-to-favorites-single-record',
|
||||
REMOVE_FROM_FAVORITES = 'remove-from-favorites-single-record',
|
||||
NAVIGATE_TO_NEXT_RECORD = 'navigate-to-next-record-single-record',
|
||||
NAVIGATE_TO_PREVIOUS_RECORD = 'navigate-to-previous-record-single-record',
|
||||
}
|
@ -1,5 +1,10 @@
|
||||
import { useAddToFavoritesSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useAddToFavoritesSingleRecordAction';
|
||||
import { useDeleteSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useDeleteSingleRecordAction';
|
||||
import { useDestroySingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useDestroySingleRecordAction';
|
||||
import { useNavigateToNextRecordSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useNavigateToNextRecordSingleRecordAction';
|
||||
import { useNavigateToPreviousRecordSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useNavigateToPreviousRecordSingleRecordAction';
|
||||
import { useRemoveFromFavoritesSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useRemoveFromFavoritesSingleRecordAction';
|
||||
import { SingleRecordActionKeys } from '@/action-menu/actions/record-actions/single-record/types/SingleRecordActionsKey';
|
||||
import { useActivateDraftWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useActivateDraftWorkflowSingleRecordAction';
|
||||
import { useActivateLastPublishedVersionWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useActivateLastPublishedVersionWorkflowSingleRecordAction';
|
||||
import { useDeactivateWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useDeactivateWorkflowSingleRecordAction';
|
||||
@ -8,8 +13,9 @@ import { useSeeActiveVersionWorkflowSingleRecordAction } from '@/action-menu/act
|
||||
import { useSeeRunsWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useSeeRunsWorkflowSingleRecordAction';
|
||||
import { useSeeVersionsWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useSeeVersionsWorkflowSingleRecordAction';
|
||||
import { useTestWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useTestWorkflowSingleRecordAction';
|
||||
import { ActionAvailableOn } from '@/action-menu/actions/types/actionAvailableOn';
|
||||
import { SingleRecordActionHook } from '@/action-menu/actions/types/singleRecordActionHook';
|
||||
import { WorkflowSingleRecordActionKeys } from '@/action-menu/actions/record-actions/single-record/workflow-actions/types/WorkflowSingleRecordActionsKeys';
|
||||
import { ActionAvailableOn } from '@/action-menu/actions/types/ActionAvailableOn';
|
||||
import { SingleRecordActionHook } from '@/action-menu/actions/types/SingleRecordActionHook';
|
||||
import {
|
||||
ActionMenuEntry,
|
||||
ActionMenuEntryScope,
|
||||
@ -18,12 +24,15 @@ import {
|
||||
import {
|
||||
IconChevronDown,
|
||||
IconChevronUp,
|
||||
IconHeart,
|
||||
IconHeartOff,
|
||||
IconHistory,
|
||||
IconHistoryToggle,
|
||||
IconPlayerPause,
|
||||
IconPlayerPlay,
|
||||
IconPower,
|
||||
IconTrash,
|
||||
IconTrashX,
|
||||
} from 'twenty-ui';
|
||||
|
||||
export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
||||
@ -33,7 +42,7 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
||||
}
|
||||
> = {
|
||||
activateWorkflowDraftSingleRecord: {
|
||||
key: 'activate-workflow-draft-single-record',
|
||||
key: WorkflowSingleRecordActionKeys.ACTIVATE_DRAFT,
|
||||
label: 'Activate Draft',
|
||||
shortLabel: 'Activate Draft',
|
||||
isPinned: true,
|
||||
@ -48,7 +57,7 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
||||
actionHook: useActivateDraftWorkflowSingleRecordAction,
|
||||
},
|
||||
activateWorkflowLastPublishedVersionSingleRecord: {
|
||||
key: 'activate-workflow-last-published-version-single-record',
|
||||
key: WorkflowSingleRecordActionKeys.ACTIVATE_LAST_PUBLISHED,
|
||||
label: 'Activate last published version',
|
||||
shortLabel: 'Activate last version',
|
||||
isPinned: true,
|
||||
@ -63,7 +72,7 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
||||
actionHook: useActivateLastPublishedVersionWorkflowSingleRecordAction,
|
||||
},
|
||||
deactivateWorkflowSingleRecord: {
|
||||
key: 'deactivate-workflow-single-record',
|
||||
key: WorkflowSingleRecordActionKeys.DEACTIVATE,
|
||||
label: 'Deactivate Workflow',
|
||||
shortLabel: 'Deactivate',
|
||||
isPinned: true,
|
||||
@ -78,7 +87,7 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
||||
actionHook: useDeactivateWorkflowSingleRecordAction,
|
||||
},
|
||||
discardWorkflowDraftSingleRecord: {
|
||||
key: 'discard-workflow-draft-single-record',
|
||||
key: WorkflowSingleRecordActionKeys.DISCARD_DRAFT,
|
||||
label: 'Discard Draft',
|
||||
shortLabel: 'Discard Draft',
|
||||
isPinned: true,
|
||||
@ -93,7 +102,7 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
||||
actionHook: useDiscardDraftWorkflowSingleRecordAction,
|
||||
},
|
||||
seeWorkflowActiveVersionSingleRecord: {
|
||||
key: 'see-workflow-active-version-single-record',
|
||||
key: WorkflowSingleRecordActionKeys.SEE_ACTIVE_VERSION,
|
||||
label: 'See active version',
|
||||
shortLabel: 'See active version',
|
||||
isPinned: false,
|
||||
@ -108,7 +117,7 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
||||
actionHook: useSeeActiveVersionWorkflowSingleRecordAction,
|
||||
},
|
||||
seeWorkflowRunsSingleRecord: {
|
||||
key: 'see-workflow-runs-single-record',
|
||||
key: WorkflowSingleRecordActionKeys.SEE_RUNS,
|
||||
label: 'See runs',
|
||||
shortLabel: 'See runs',
|
||||
isPinned: false,
|
||||
@ -123,7 +132,7 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
||||
actionHook: useSeeRunsWorkflowSingleRecordAction,
|
||||
},
|
||||
seeWorkflowVersionsHistorySingleRecord: {
|
||||
key: 'see-workflow-versions-history-single-record',
|
||||
key: WorkflowSingleRecordActionKeys.SEE_VERSIONS,
|
||||
label: 'See versions history',
|
||||
shortLabel: 'See versions',
|
||||
isPinned: false,
|
||||
@ -138,7 +147,7 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
||||
actionHook: useSeeVersionsWorkflowSingleRecordAction,
|
||||
},
|
||||
testWorkflowSingleRecord: {
|
||||
key: 'test-workflow-single-record',
|
||||
key: WorkflowSingleRecordActionKeys.TEST,
|
||||
label: 'Test Workflow',
|
||||
shortLabel: 'Test',
|
||||
isPinned: true,
|
||||
@ -155,7 +164,7 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
||||
navigateToPreviousRecord: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
key: 'navigate-to-previous-record',
|
||||
key: SingleRecordActionKeys.NAVIGATE_TO_PREVIOUS_RECORD,
|
||||
label: 'Navigate to previous workflow',
|
||||
shortLabel: '',
|
||||
position: 9,
|
||||
@ -166,7 +175,7 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
||||
navigateToNextRecord: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
key: 'navigate-to-next-record',
|
||||
key: SingleRecordActionKeys.NAVIGATE_TO_NEXT_RECORD,
|
||||
label: 'Navigate to next workflow',
|
||||
shortLabel: '',
|
||||
position: 10,
|
||||
@ -174,4 +183,66 @@ export const WORKFLOW_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
||||
availableOn: [ActionAvailableOn.SHOW_PAGE],
|
||||
actionHook: useNavigateToNextRecordSingleRecordAction,
|
||||
},
|
||||
addToFavoritesSingleRecord: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
key: SingleRecordActionKeys.ADD_TO_FAVORITES,
|
||||
label: 'Add to favorites',
|
||||
shortLabel: 'Add to favorites',
|
||||
position: 11,
|
||||
isPinned: false,
|
||||
Icon: IconHeart,
|
||||
availableOn: [
|
||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionAvailableOn.SHOW_PAGE,
|
||||
],
|
||||
actionHook: useAddToFavoritesSingleRecordAction,
|
||||
},
|
||||
removeFromFavoritesSingleRecord: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
key: SingleRecordActionKeys.REMOVE_FROM_FAVORITES,
|
||||
label: 'Remove from favorites',
|
||||
shortLabel: 'Remove from favorites',
|
||||
isPinned: false,
|
||||
position: 12,
|
||||
Icon: IconHeartOff,
|
||||
availableOn: [
|
||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionAvailableOn.SHOW_PAGE,
|
||||
],
|
||||
actionHook: useRemoveFromFavoritesSingleRecordAction,
|
||||
},
|
||||
deleteSingleRecord: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
key: SingleRecordActionKeys.DELETE,
|
||||
label: 'Delete record',
|
||||
shortLabel: 'Delete',
|
||||
position: 13,
|
||||
Icon: IconTrash,
|
||||
accent: 'danger',
|
||||
isPinned: false,
|
||||
availableOn: [
|
||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionAvailableOn.SHOW_PAGE,
|
||||
],
|
||||
actionHook: useDeleteSingleRecordAction,
|
||||
},
|
||||
destroySingleRecord: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
key: SingleRecordActionKeys.DESTROY,
|
||||
label: 'Permanently destroy record',
|
||||
shortLabel: 'Destroy',
|
||||
position: 14,
|
||||
Icon: IconTrashX,
|
||||
accent: 'danger',
|
||||
isPinned: false,
|
||||
availableOn: [
|
||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionAvailableOn.SHOW_PAGE,
|
||||
],
|
||||
actionHook: useDestroySingleRecordAction,
|
||||
},
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { SingleRecordActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/singleRecordActionHook';
|
||||
import { SingleRecordActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/SingleRecordActionHook';
|
||||
import { useActivateWorkflowVersion } from '@/workflow/hooks/useActivateWorkflowVersion';
|
||||
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { SingleRecordActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/singleRecordActionHook';
|
||||
import { SingleRecordActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/SingleRecordActionHook';
|
||||
import { useActivateWorkflowVersion } from '@/workflow/hooks/useActivateWorkflowVersion';
|
||||
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { SingleRecordActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/singleRecordActionHook';
|
||||
import { SingleRecordActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/SingleRecordActionHook';
|
||||
import { useDeactivateWorkflowVersion } from '@/workflow/hooks/useDeactivateWorkflowVersion';
|
||||
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { SingleRecordActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/singleRecordActionHook';
|
||||
import { SingleRecordActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/SingleRecordActionHook';
|
||||
import { useDeleteOneWorkflowVersion } from '@/workflow/hooks/useDeleteOneWorkflowVersion';
|
||||
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { SingleRecordActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/singleRecordActionHook';
|
||||
import { SingleRecordActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/SingleRecordActionHook';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useActiveWorkflowVersion } from '@/workflow/hooks/useActiveWorkflowVersion';
|
||||
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { SingleRecordActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/singleRecordActionHook';
|
||||
import { SingleRecordActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/SingleRecordActionHook';
|
||||
import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
|
||||
import { FilterQueryParams } from '@/views/hooks/internal/useViewFromQueryParams';
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { SingleRecordActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/singleRecordActionHook';
|
||||
import { SingleRecordActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/SingleRecordActionHook';
|
||||
import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
|
||||
import { FilterQueryParams } from '@/views/hooks/internal/useViewFromQueryParams';
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { SingleRecordActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/singleRecordActionHook';
|
||||
import { SingleRecordActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/SingleRecordActionHook';
|
||||
import { useRunWorkflowVersion } from '@/workflow/hooks/useRunWorkflowVersion';
|
||||
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
@ -0,0 +1,10 @@
|
||||
export enum WorkflowSingleRecordActionKeys {
|
||||
ACTIVATE_DRAFT = 'activate-draft-workflow-single-record',
|
||||
ACTIVATE_LAST_PUBLISHED = 'activate-last-published-workflow-single-record',
|
||||
DEACTIVATE = 'deactivate-workflow-single-record',
|
||||
DISCARD_DRAFT = 'discard-draft-workflow-single-record',
|
||||
SEE_ACTIVE_VERSION = 'see-active-version-workflow-single-record',
|
||||
SEE_RUNS = 'see-runs-workflow-single-record',
|
||||
SEE_VERSIONS = 'see-versions-workflow-single-record',
|
||||
TEST = 'test-workflow-single-record',
|
||||
}
|
@ -1,10 +1,16 @@
|
||||
import { useAddToFavoritesSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useAddToFavoritesSingleRecordAction';
|
||||
import { useDeleteSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useDeleteSingleRecordAction';
|
||||
import { useDestroySingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useDestroySingleRecordAction';
|
||||
import { useNavigateToNextRecordSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useNavigateToNextRecordSingleRecordAction';
|
||||
import { useNavigateToPreviousRecordSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useNavigateToPreviousRecordSingleRecordAction';
|
||||
import { useSeeExecutionsWorkflowVersionSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-version-actions/hooks/useSeeExecutionsWorkflowVersionSingleRecordAction';
|
||||
import { useRemoveFromFavoritesSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useRemoveFromFavoritesSingleRecordAction';
|
||||
import { SingleRecordActionKeys } from '@/action-menu/actions/record-actions/single-record/types/SingleRecordActionsKey';
|
||||
import { useSeeRunsWorkflowVersionSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-version-actions/hooks/useSeeRunsWorkflowVersionSingleRecordAction';
|
||||
import { useSeeVersionsWorkflowVersionSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-version-actions/hooks/useSeeVersionsWorkflowVersionSingleRecordAction';
|
||||
import { useUseAsDraftWorkflowVersionSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-version-actions/hooks/useUseAsDraftWorkflowVersionSingleRecordAction';
|
||||
import { ActionAvailableOn } from '@/action-menu/actions/types/actionAvailableOn';
|
||||
import { SingleRecordActionHook } from '@/action-menu/actions/types/singleRecordActionHook';
|
||||
import { WorkflowVersionSingleRecordActionKeys } from '@/action-menu/actions/record-actions/single-record/workflow-version-actions/types/WorkflowVersionSingleRecordActionsKeys';
|
||||
import { ActionAvailableOn } from '@/action-menu/actions/types/ActionAvailableOn';
|
||||
import { SingleRecordActionHook } from '@/action-menu/actions/types/SingleRecordActionHook';
|
||||
import {
|
||||
ActionMenuEntry,
|
||||
ActionMenuEntryScope,
|
||||
@ -13,9 +19,13 @@ import {
|
||||
import {
|
||||
IconChevronDown,
|
||||
IconChevronUp,
|
||||
IconHeart,
|
||||
IconHeartOff,
|
||||
IconHistory,
|
||||
IconHistoryToggle,
|
||||
IconPencil,
|
||||
IconTrash,
|
||||
IconTrashX,
|
||||
} from 'twenty-ui';
|
||||
|
||||
export const WORKFLOW_VERSIONS_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
||||
@ -25,7 +35,7 @@ export const WORKFLOW_VERSIONS_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
||||
}
|
||||
> = {
|
||||
useAsDraftWorkflowVersionSingleRecord: {
|
||||
key: 'use-as-draft-workflow-version-single-record',
|
||||
key: WorkflowVersionSingleRecordActionKeys.USE_AS_DRAFT,
|
||||
label: 'Use as draft',
|
||||
position: 1,
|
||||
isPinned: true,
|
||||
@ -38,9 +48,9 @@ export const WORKFLOW_VERSIONS_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
||||
],
|
||||
actionHook: useUseAsDraftWorkflowVersionSingleRecordAction,
|
||||
},
|
||||
seeWorkflowExecutionsSingleRecord: {
|
||||
key: 'see-workflow-executions-single-record',
|
||||
label: 'See executions',
|
||||
seeWorkflowRunsSingleRecord: {
|
||||
key: WorkflowVersionSingleRecordActionKeys.SEE_RUNS,
|
||||
label: 'See runs',
|
||||
position: 2,
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
@ -49,10 +59,10 @@ export const WORKFLOW_VERSIONS_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
||||
ActionAvailableOn.SHOW_PAGE,
|
||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
],
|
||||
actionHook: useSeeExecutionsWorkflowVersionSingleRecordAction,
|
||||
actionHook: useSeeRunsWorkflowVersionSingleRecordAction,
|
||||
},
|
||||
seeWorkflowVersionsHistorySingleRecord: {
|
||||
key: 'see-workflow-versions-history-single-record',
|
||||
key: WorkflowVersionSingleRecordActionKeys.SEE_VERSIONS,
|
||||
label: 'See versions history',
|
||||
position: 3,
|
||||
type: ActionMenuEntryType.Standard,
|
||||
@ -67,10 +77,10 @@ export const WORKFLOW_VERSIONS_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
||||
navigateToPreviousRecord: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
key: 'navigate-to-previous-record',
|
||||
key: SingleRecordActionKeys.NAVIGATE_TO_PREVIOUS_RECORD,
|
||||
label: 'Navigate to previous version',
|
||||
shortLabel: '',
|
||||
position: 9,
|
||||
position: 4,
|
||||
Icon: IconChevronUp,
|
||||
availableOn: [ActionAvailableOn.SHOW_PAGE],
|
||||
actionHook: useNavigateToPreviousRecordSingleRecordAction,
|
||||
@ -78,12 +88,74 @@ export const WORKFLOW_VERSIONS_SINGLE_RECORD_ACTIONS_CONFIG: Record<
|
||||
navigateToNextRecord: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
key: 'navigate-to-next-record',
|
||||
key: SingleRecordActionKeys.NAVIGATE_TO_NEXT_RECORD,
|
||||
label: 'Navigate to next version',
|
||||
shortLabel: '',
|
||||
position: 10,
|
||||
position: 5,
|
||||
Icon: IconChevronDown,
|
||||
availableOn: [ActionAvailableOn.SHOW_PAGE],
|
||||
actionHook: useNavigateToNextRecordSingleRecordAction,
|
||||
},
|
||||
addToFavoritesSingleRecord: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
key: SingleRecordActionKeys.ADD_TO_FAVORITES,
|
||||
label: 'Add to favorites',
|
||||
shortLabel: 'Add to favorites',
|
||||
position: 6,
|
||||
isPinned: false,
|
||||
Icon: IconHeart,
|
||||
availableOn: [
|
||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionAvailableOn.SHOW_PAGE,
|
||||
],
|
||||
actionHook: useAddToFavoritesSingleRecordAction,
|
||||
},
|
||||
removeFromFavoritesSingleRecord: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
key: SingleRecordActionKeys.REMOVE_FROM_FAVORITES,
|
||||
label: 'Remove from favorites',
|
||||
shortLabel: 'Remove from favorites',
|
||||
isPinned: false,
|
||||
position: 7,
|
||||
Icon: IconHeartOff,
|
||||
availableOn: [
|
||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionAvailableOn.SHOW_PAGE,
|
||||
],
|
||||
actionHook: useRemoveFromFavoritesSingleRecordAction,
|
||||
},
|
||||
deleteSingleRecord: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
key: SingleRecordActionKeys.DELETE,
|
||||
label: 'Delete record',
|
||||
shortLabel: 'Delete',
|
||||
position: 8,
|
||||
Icon: IconTrash,
|
||||
accent: 'danger',
|
||||
isPinned: false,
|
||||
availableOn: [
|
||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionAvailableOn.SHOW_PAGE,
|
||||
],
|
||||
actionHook: useDeleteSingleRecordAction,
|
||||
},
|
||||
destroySingleRecord: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
key: SingleRecordActionKeys.DESTROY,
|
||||
label: 'Permanently destroy record',
|
||||
shortLabel: 'Destroy',
|
||||
position: 9,
|
||||
Icon: IconTrashX,
|
||||
accent: 'danger',
|
||||
isPinned: false,
|
||||
availableOn: [
|
||||
ActionAvailableOn.INDEX_PAGE_SINGLE_RECORD_SELECTION,
|
||||
ActionAvailableOn.SHOW_PAGE,
|
||||
],
|
||||
actionHook: useDestroySingleRecordAction,
|
||||
},
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { SingleRecordActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/singleRecordActionHook';
|
||||
import { SingleRecordActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/SingleRecordActionHook';
|
||||
import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { FilterQueryParams } from '@/views/hooks/internal/useViewFromQueryParams';
|
||||
@ -9,7 +9,7 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
export const useSeeExecutionsWorkflowVersionSingleRecordAction: SingleRecordActionHookWithoutObjectMetadataItem =
|
||||
export const useSeeRunsWorkflowVersionSingleRecordAction: SingleRecordActionHookWithoutObjectMetadataItem =
|
||||
({ recordId }) => {
|
||||
const workflowVersion = useRecoilValue(recordStoreFamilyState(recordId));
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useSeeVersionsWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useSeeVersionsWorkflowSingleRecordAction';
|
||||
import { SingleRecordActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/singleRecordActionHook';
|
||||
import { SingleRecordActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/SingleRecordActionHook';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { SingleRecordActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/singleRecordActionHook';
|
||||
import { SingleRecordActionHookWithoutObjectMetadataItem } from '@/action-menu/actions/types/SingleRecordActionHook';
|
||||
import { OverrideWorkflowDraftConfirmationModal } from '@/workflow/components/OverrideWorkflowDraftConfirmationModal';
|
||||
import { useCreateNewWorkflowVersion } from '@/workflow/hooks/useCreateNewWorkflowVersion';
|
||||
import { useWorkflowVersion } from '@/workflow/hooks/useWorkflowVersion';
|
||||
|
@ -0,0 +1,5 @@
|
||||
export enum WorkflowVersionSingleRecordActionKeys {
|
||||
SEE_RUNS = 'see-runs-workflow-version-single-record',
|
||||
SEE_VERSIONS = 'see-versions-workflow-version-single-record',
|
||||
USE_AS_DRAFT = 'use-as-draft-workflow-version-single-record',
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
import { ConfirmationModalProps } from '@/ui/layout/modal/components/ConfirmationModal';
|
||||
|
||||
export type ActionHookResult = {
|
||||
shouldBeRegistered: boolean;
|
||||
onClick: () => Promise<void> | void;
|
||||
ConfirmationModal?: React.ReactElement<ConfirmationModalProps>;
|
||||
};
|
@ -1,4 +1,4 @@
|
||||
import { ActionHookResult } from '@/action-menu/actions/types/actionHookResult';
|
||||
import { ActionHookResult } from '@/action-menu/actions/types/ActionHookResult';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
|
||||
export type SingleRecordActionHook =
|
@ -1,5 +0,0 @@
|
||||
export type ActionHookResult = {
|
||||
shouldBeRegistered: boolean;
|
||||
onClick: () => void;
|
||||
ConfirmationModal?: React.ReactElement;
|
||||
};
|
@ -0,0 +1,40 @@
|
||||
import { ActionMenuEntry } from '@/action-menu/types/ActionMenuEntry';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
export const wrapActionInCallbacks = ({
|
||||
action,
|
||||
onActionStartedCallback,
|
||||
onActionExecutedCallback,
|
||||
}: {
|
||||
action: ActionMenuEntry;
|
||||
onActionStartedCallback?: (action: { key: string }) => Promise<void> | void;
|
||||
onActionExecutedCallback?: (action: { key: string }) => Promise<void> | void;
|
||||
}) => {
|
||||
const onClickWithCallbacks = isDefined(action.ConfirmationModal)
|
||||
? action.onClick
|
||||
: async () => {
|
||||
await onActionStartedCallback?.({ key: action.key });
|
||||
await action.onClick?.();
|
||||
await onActionExecutedCallback?.({ key: action.key });
|
||||
};
|
||||
|
||||
const ConfirmationModalWithCallbacks = isDefined(action.ConfirmationModal)
|
||||
? {
|
||||
...action.ConfirmationModal,
|
||||
props: {
|
||||
...action.ConfirmationModal.props,
|
||||
onConfirmClick: async () => {
|
||||
await onActionStartedCallback?.({ key: action.key });
|
||||
await action.ConfirmationModal?.props.onConfirmClick?.();
|
||||
await onActionExecutedCallback?.({ key: action.key });
|
||||
},
|
||||
},
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
...action,
|
||||
onClick: onClickWithCallbacks,
|
||||
ConfirmationModal: ConfirmationModalWithCallbacks,
|
||||
};
|
||||
};
|
@ -1,4 +1,5 @@
|
||||
import { RecordActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter';
|
||||
import { MultipleRecordsActionKeys } from '@/action-menu/actions/record-actions/multiple-records/types/MultipleRecordsActionKeys';
|
||||
import { RecordAgnosticActionsSetterEffect } from '@/action-menu/actions/record-agnostic-actions/components/RecordAgnosticActionsSetterEffect';
|
||||
import { ActionMenuConfirmationModals } from '@/action-menu/components/ActionMenuConfirmationModals';
|
||||
import { RecordIndexActionMenuBar } from '@/action-menu/components/RecordIndexActionMenuBar';
|
||||
@ -8,11 +9,13 @@ import { RecordIndexActionMenuEffect } from '@/action-menu/components/RecordInde
|
||||
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
|
||||
|
||||
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
|
||||
import { isRecordIndexLoadMoreLockedComponentState } from '@/object-record/record-index/states/isRecordIndexLoadMoreLockedComponentState';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { useIsMobile } from 'twenty-ui';
|
||||
|
||||
export const RecordIndexActionMenu = () => {
|
||||
export const RecordIndexActionMenu = ({ indexId }: { indexId: string }) => {
|
||||
const contextStoreCurrentObjectMetadataId = useRecoilComponentValueV2(
|
||||
contextStoreCurrentObjectMetadataIdComponentState,
|
||||
);
|
||||
@ -25,13 +28,27 @@ export const RecordIndexActionMenu = () => {
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const setIsLoadMoreLocked = useSetRecoilComponentStateV2(
|
||||
isRecordIndexLoadMoreLockedComponentState,
|
||||
indexId,
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{contextStoreCurrentObjectMetadataId && (
|
||||
<ActionMenuContext.Provider
|
||||
value={{
|
||||
isInRightDrawer: false,
|
||||
onActionExecutedCallback: () => {},
|
||||
onActionStartedCallback: (action) => {
|
||||
if (action.key === MultipleRecordsActionKeys.DELETE) {
|
||||
setIsLoadMoreLocked(true);
|
||||
}
|
||||
},
|
||||
onActionExecutedCallback: (action) => {
|
||||
if (action.key === MultipleRecordsActionKeys.DELETE) {
|
||||
setIsLoadMoreLocked(false);
|
||||
}
|
||||
},
|
||||
}}
|
||||
>
|
||||
{isPageHeaderV2Enabled ? (
|
||||
|
@ -27,8 +27,8 @@ export const RecordIndexActionMenuButtons = () => {
|
||||
size="small"
|
||||
variant="secondary"
|
||||
accent="default"
|
||||
title={entry.label}
|
||||
onClick={() => entry.onClick?.()}
|
||||
title={entry.shortLabel}
|
||||
onClick={entry.onClick}
|
||||
ariaLabel={entry.label}
|
||||
/>
|
||||
))}
|
||||
|
@ -1,8 +1,3 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { PositionType } from '../types/PositionType';
|
||||
|
||||
import { actionMenuEntriesComponentSelector } from '@/action-menu/states/actionMenuEntriesComponentSelector';
|
||||
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
|
||||
import { recordIndexActionMenuDropdownPositionComponentState } from '@/action-menu/states/recordIndexActionMenuDropdownPositionComponentState';
|
||||
@ -13,7 +8,10 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/Drop
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { MenuItem } from 'twenty-ui';
|
||||
import { PositionType } from '../types/PositionType';
|
||||
|
||||
type StyledContainerProps = {
|
||||
position: PositionType;
|
||||
|
@ -2,10 +2,12 @@ import { createContext } from 'react';
|
||||
|
||||
type ActionMenuContextType = {
|
||||
isInRightDrawer: boolean;
|
||||
onActionExecutedCallback: () => void;
|
||||
onActionStartedCallback?: (action: { key: string }) => Promise<void> | void;
|
||||
onActionExecutedCallback?: (action: { key: string }) => Promise<void> | void;
|
||||
};
|
||||
|
||||
export const ActionMenuContext = createContext<ActionMenuContextType>({
|
||||
isInRightDrawer: false,
|
||||
onActionStartedCallback: () => {},
|
||||
onActionExecutedCallback: () => {},
|
||||
});
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { ActionAvailableOn } from '@/action-menu/actions/types/actionAvailableOn';
|
||||
import { ActionAvailableOn } from '@/action-menu/actions/types/ActionAvailableOn';
|
||||
import { ConfirmationModalProps } from '@/ui/layout/modal/components/ConfirmationModal';
|
||||
import { MouseEvent, ReactElement } from 'react';
|
||||
import { IconComponent, MenuItemAccent } from 'twenty-ui';
|
||||
|
||||
@ -24,5 +25,5 @@ export type ActionMenuEntry = {
|
||||
accent?: MenuItemAccent;
|
||||
availableOn?: ActionAvailableOn[];
|
||||
onClick?: (event?: MouseEvent<HTMLElement>) => void;
|
||||
ConfirmationModal?: ReactElement;
|
||||
ConfirmationModal?: ReactElement<ConfirmationModalProps>;
|
||||
};
|
||||
|
@ -8,14 +8,11 @@ import { useDebouncedCallback } from 'use-debounce';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity';
|
||||
import { activityBodyFamilyState } from '@/activities/states/activityBodyFamilyState';
|
||||
import { activityTitleHasBeenSetFamilyState } from '@/activities/states/activityTitleHasBeenSetFamilyState';
|
||||
import { canCreateActivityState } from '@/activities/states/canCreateActivityState';
|
||||
import { ActivityEditorHotkeyScope } from '@/activities/types/ActivityEditorHotkeyScope';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { modifyRecordFromCache } from '@/object-record/cache/utils/modifyRecordFromCache';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { BlockEditor } from '@/ui/input/editor/components/BlockEditor';
|
||||
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
|
||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
@ -23,44 +20,32 @@ import { isNonTextWritingKey } from '@/ui/utilities/hotkey/utils/isNonTextWritin
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
import { BLOCK_SCHEMA } from '@/activities/blocks/constants/Schema';
|
||||
import { ActivityRichTextEditorChangeOnActivityIdEffect } from '@/activities/components/ActivityRichTextEditorChangeOnActivityIdEffect';
|
||||
import { useUploadAttachmentFile } from '@/activities/files/hooks/useUploadAttachmentFile';
|
||||
import { Note } from '@/activities/types/Note';
|
||||
import { Task } from '@/activities/types/Task';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { BlockEditor } from '@/ui/input/editor/components/BlockEditor';
|
||||
import '@blocknote/core/fonts/inter.css';
|
||||
import '@blocknote/mantine/style.css';
|
||||
import '@blocknote/react/style.css';
|
||||
|
||||
type RichTextEditorProps = {
|
||||
type ActivityRichTextEditorProps = {
|
||||
activityId: string;
|
||||
fillTitleFromBody: boolean;
|
||||
activityObjectNameSingular:
|
||||
| CoreObjectNameSingular.Task
|
||||
| CoreObjectNameSingular.Note;
|
||||
};
|
||||
|
||||
export const RichTextEditor = ({
|
||||
export const ActivityRichTextEditor = ({
|
||||
activityId,
|
||||
fillTitleFromBody,
|
||||
activityObjectNameSingular,
|
||||
}: RichTextEditorProps) => {
|
||||
}: ActivityRichTextEditorProps) => {
|
||||
const [activityInStore] = useRecoilState(recordStoreFamilyState(activityId));
|
||||
|
||||
const cache = useApolloClient().cache;
|
||||
const activity = activityInStore as Task | Note | null;
|
||||
|
||||
const [activityTitleHasBeenSet, setActivityTitleHasBeenSet] = useRecoilState(
|
||||
activityTitleHasBeenSetFamilyState({
|
||||
activityId: activityId,
|
||||
}),
|
||||
);
|
||||
|
||||
const [activityBody, setActivityBody] = useRecoilState(
|
||||
activityBodyFamilyState({
|
||||
activityId: activityId,
|
||||
}),
|
||||
);
|
||||
|
||||
const { objectMetadataItem: objectMetadataItemActivity } =
|
||||
useObjectMetadataItem({
|
||||
objectNameSingular: activityObjectNameSingular,
|
||||
@ -86,33 +71,6 @@ export const RichTextEditor = ({
|
||||
}
|
||||
}, 300);
|
||||
|
||||
const persistTitleAndBodyDebounced = useDebouncedCallback(
|
||||
(newTitle: string, newBody: string) => {
|
||||
if (isDefined(activity)) {
|
||||
upsertActivity({
|
||||
activity,
|
||||
input: {
|
||||
title: newTitle,
|
||||
body: newBody,
|
||||
},
|
||||
});
|
||||
|
||||
setActivityTitleHasBeenSet(true);
|
||||
}
|
||||
},
|
||||
200,
|
||||
);
|
||||
|
||||
const updateTitleAndBody = useCallback(
|
||||
(newStringifiedBody: string) => {
|
||||
const blockBody = JSON.parse(newStringifiedBody);
|
||||
const newTitleFromBody = blockBody[0]?.content?.[0]?.text;
|
||||
|
||||
persistTitleAndBodyDebounced(newTitleFromBody, newStringifiedBody);
|
||||
},
|
||||
[persistTitleAndBodyDebounced],
|
||||
);
|
||||
|
||||
const [canCreateActivity, setCanCreateActivity] = useRecoilState(
|
||||
canCreateActivityState,
|
||||
);
|
||||
@ -156,24 +114,13 @@ export const RichTextEditor = ({
|
||||
setCanCreateActivity(true);
|
||||
}
|
||||
|
||||
if (!activityTitleHasBeenSet && fillTitleFromBody) {
|
||||
updateTitleAndBody(activityBody);
|
||||
} else {
|
||||
persistBodyDebounced(prepareBody(activityBody));
|
||||
}
|
||||
persistBodyDebounced(prepareBody(activityBody));
|
||||
},
|
||||
[
|
||||
fillTitleFromBody,
|
||||
persistBodyDebounced,
|
||||
activityTitleHasBeenSet,
|
||||
updateTitleAndBody,
|
||||
setCanCreateActivity,
|
||||
canCreateActivity,
|
||||
],
|
||||
[persistBodyDebounced, setCanCreateActivity, canCreateActivity],
|
||||
);
|
||||
|
||||
const handleBodyChange = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
({ set }) =>
|
||||
(newStringifiedBody: string) => {
|
||||
set(recordStoreFamilyState(activityId), (oldActivity) => {
|
||||
return {
|
||||
@ -195,79 +142,28 @@ export const RichTextEditor = ({
|
||||
objectMetadataItem: objectMetadataItemActivity,
|
||||
});
|
||||
|
||||
const activityTitleHasBeenSet = snapshot
|
||||
.getLoadable(
|
||||
activityTitleHasBeenSetFamilyState({
|
||||
activityId: activityId,
|
||||
}),
|
||||
)
|
||||
.getValue();
|
||||
|
||||
const blockBody = JSON.parse(newStringifiedBody);
|
||||
const newTitleFromBody = blockBody[0]?.content?.[0]?.text as string;
|
||||
|
||||
if (!activityTitleHasBeenSet && fillTitleFromBody) {
|
||||
set(recordStoreFamilyState(activityId), (oldActivity) => {
|
||||
return {
|
||||
...oldActivity,
|
||||
id: activityId,
|
||||
title: newTitleFromBody,
|
||||
__typename: 'Activity',
|
||||
};
|
||||
});
|
||||
|
||||
modifyRecordFromCache({
|
||||
recordId: activityId,
|
||||
fieldModifiers: {
|
||||
title: () => {
|
||||
return newTitleFromBody;
|
||||
},
|
||||
},
|
||||
cache,
|
||||
objectMetadataItem: objectMetadataItemActivity,
|
||||
});
|
||||
}
|
||||
|
||||
handlePersistBody(newStringifiedBody);
|
||||
},
|
||||
[
|
||||
activityId,
|
||||
cache,
|
||||
objectMetadataItemActivity,
|
||||
fillTitleFromBody,
|
||||
handlePersistBody,
|
||||
],
|
||||
[activityId, cache, objectMetadataItemActivity, handlePersistBody],
|
||||
);
|
||||
|
||||
const handleBodyChangeDebounced = useDebouncedCallback(handleBodyChange, 500);
|
||||
|
||||
// See https://github.com/twentyhq/twenty/issues/6724 for explanation
|
||||
const setActivityBodyDebouncedToAvoidDragBug = useDebouncedCallback(
|
||||
setActivityBody,
|
||||
100,
|
||||
);
|
||||
|
||||
const handleEditorChange = () => {
|
||||
const newStringifiedBody = JSON.stringify(editor.document) ?? '';
|
||||
|
||||
setActivityBodyDebouncedToAvoidDragBug(newStringifiedBody);
|
||||
|
||||
handleBodyChangeDebounced(newStringifiedBody);
|
||||
};
|
||||
|
||||
const initialBody = useMemo(() => {
|
||||
if (isNonEmptyString(activityBody) && activityBody !== '{}') {
|
||||
return JSON.parse(activityBody);
|
||||
} else if (
|
||||
if (
|
||||
isDefined(activity) &&
|
||||
isNonEmptyString(activity.body) &&
|
||||
activity?.body !== '{}'
|
||||
) {
|
||||
return JSON.parse(activity.body);
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}, [activity, activityBody]);
|
||||
}, [activity]);
|
||||
|
||||
const handleEditorBuiltInUploadFile = async (file: File) => {
|
||||
const { attachementAbsoluteURL } = await handleUploadAttachment(file);
|
||||
@ -367,11 +263,17 @@ export const RichTextEditor = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<BlockEditor
|
||||
onFocus={handleBlockEditorFocus}
|
||||
onBlur={handlerBlockEditorBlur}
|
||||
onChange={handleEditorChange}
|
||||
editor={editor}
|
||||
/>
|
||||
<>
|
||||
<ActivityRichTextEditorChangeOnActivityIdEffect
|
||||
editor={editor}
|
||||
activityId={activityId}
|
||||
/>
|
||||
<BlockEditor
|
||||
onFocus={handleBlockEditorFocus}
|
||||
onBlur={handlerBlockEditorBlur}
|
||||
onChange={handleEditorChange}
|
||||
editor={editor}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
@ -0,0 +1,27 @@
|
||||
import { BLOCK_SCHEMA } from '@/activities/blocks/constants/Schema';
|
||||
import { useReplaceActivityBlockEditorContent } from '@/activities/hooks/useReplaceActivityBlockEditorContent';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
type ActivityRichTextEditorChangeOnActivityIdEffectProps = {
|
||||
activityId: string;
|
||||
editor: typeof BLOCK_SCHEMA.BlockNoteEditor;
|
||||
};
|
||||
|
||||
export const ActivityRichTextEditorChangeOnActivityIdEffect = ({
|
||||
activityId,
|
||||
editor,
|
||||
}: ActivityRichTextEditorChangeOnActivityIdEffectProps) => {
|
||||
const { replaceBlockEditorContent } =
|
||||
useReplaceActivityBlockEditorContent(editor);
|
||||
|
||||
const [currentActivityId, setCurrentActivityId] = useState(activityId);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentActivityId !== activityId) {
|
||||
replaceBlockEditorContent(activityId);
|
||||
setCurrentActivityId(activityId);
|
||||
}
|
||||
}, [activityId, currentActivityId, replaceBlockEditorContent]);
|
||||
|
||||
return <></>;
|
||||
};
|
@ -88,6 +88,7 @@ const mocks = [
|
||||
phones {
|
||||
primaryPhoneNumber
|
||||
primaryPhoneCountryCode
|
||||
primaryPhoneCallingCode
|
||||
additionalPhones
|
||||
}
|
||||
position
|
||||
@ -95,6 +96,7 @@ const mocks = [
|
||||
whatsapp {
|
||||
primaryPhoneNumber
|
||||
primaryPhoneCountryCode
|
||||
primaryPhoneCallingCode
|
||||
additionalPhones
|
||||
}
|
||||
workPreference
|
||||
@ -246,6 +248,7 @@ const mocks = [
|
||||
phones {
|
||||
primaryPhoneNumber
|
||||
primaryPhoneCountryCode
|
||||
primaryPhoneCallingCode
|
||||
additionalPhones
|
||||
}
|
||||
position
|
||||
@ -253,6 +256,7 @@ const mocks = [
|
||||
whatsapp {
|
||||
primaryPhoneNumber
|
||||
primaryPhoneCountryCode
|
||||
primaryPhoneCallingCode
|
||||
additionalPhones
|
||||
}
|
||||
workPreference
|
||||
|
@ -0,0 +1,34 @@
|
||||
import { BLOCK_SCHEMA } from '@/activities/blocks/constants/Schema';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
export const useReplaceActivityBlockEditorContent = (
|
||||
editor: typeof BLOCK_SCHEMA.BlockNoteEditor,
|
||||
) => {
|
||||
const replaceBlockEditorContent = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
(activityId: string) => {
|
||||
if (isDefined(editor)) {
|
||||
const activityInStore = snapshot
|
||||
.getLoadable(recordStoreFamilyState(activityId))
|
||||
.getValue();
|
||||
|
||||
const content = isNonEmptyString(activityInStore?.body)
|
||||
? JSON.parse(activityInStore?.body)
|
||||
: [{ type: 'paragraph', content: '' }];
|
||||
|
||||
if (!isDeeplyEqual(editor.document, content)) {
|
||||
editor.replaceBlocks(editor.document, content);
|
||||
}
|
||||
}
|
||||
},
|
||||
[editor],
|
||||
);
|
||||
|
||||
return {
|
||||
replaceBlockEditorContent,
|
||||
};
|
||||
};
|
@ -0,0 +1,132 @@
|
||||
import { ApolloCache, StoreObject } from '@apollo/client';
|
||||
|
||||
import { sortCachedObjectEdges } from '@/apollo/optimistic-effect/utils/sortCachedObjectEdges';
|
||||
import { triggerUpdateRelationsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRelationsOptimisticEffect';
|
||||
import { CachedObjectRecordQueryVariables } from '@/apollo/types/CachedObjectRecordQueryVariables';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { RecordGqlRefEdge } from '@/object-record/cache/types/RecordGqlRefEdge';
|
||||
import { getEdgeTypename } from '@/object-record/cache/utils/getEdgeTypename';
|
||||
import { isObjectRecordConnectionWithRefs } from '@/object-record/cache/utils/isObjectRecordConnectionWithRefs';
|
||||
import { RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode';
|
||||
import { isRecordMatchingFilter } from '@/object-record/record-filter/utils/isRecordMatchingFilter';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { parseApolloStoreFieldName } from '~/utils/parseApolloStoreFieldName';
|
||||
|
||||
// TODO: add extensive unit tests for this function
|
||||
// That will also serve as documentation
|
||||
export const triggerUpdateRecordOptimisticEffectByBatch = ({
|
||||
cache,
|
||||
objectMetadataItem,
|
||||
currentRecords,
|
||||
updatedRecords,
|
||||
objectMetadataItems,
|
||||
}: {
|
||||
cache: ApolloCache<unknown>;
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
currentRecords: RecordGqlNode[];
|
||||
updatedRecords: RecordGqlNode[];
|
||||
objectMetadataItems: ObjectMetadataItem[];
|
||||
}) => {
|
||||
for (const [index, currentRecord] of currentRecords.entries()) {
|
||||
triggerUpdateRelationsOptimisticEffect({
|
||||
cache,
|
||||
sourceObjectMetadataItem: objectMetadataItem,
|
||||
currentSourceRecord: currentRecord,
|
||||
updatedSourceRecord: updatedRecords[index],
|
||||
objectMetadataItems,
|
||||
});
|
||||
}
|
||||
|
||||
cache.modify<StoreObject>({
|
||||
fields: {
|
||||
[objectMetadataItem.namePlural]: (
|
||||
rootQueryCachedResponse,
|
||||
{ readField, storeFieldName, toReference },
|
||||
) => {
|
||||
const shouldSkip = !isObjectRecordConnectionWithRefs(
|
||||
objectMetadataItem.nameSingular,
|
||||
rootQueryCachedResponse,
|
||||
);
|
||||
|
||||
if (shouldSkip) {
|
||||
return rootQueryCachedResponse;
|
||||
}
|
||||
|
||||
const rootQueryConnection = rootQueryCachedResponse;
|
||||
|
||||
const { fieldVariables: rootQueryVariables } =
|
||||
parseApolloStoreFieldName<CachedObjectRecordQueryVariables>(
|
||||
storeFieldName,
|
||||
);
|
||||
|
||||
const rootQueryCurrentEdges =
|
||||
readField<RecordGqlRefEdge[]>('edges', rootQueryConnection) ?? [];
|
||||
|
||||
let rootQueryNextEdges = [...rootQueryCurrentEdges];
|
||||
|
||||
const rootQueryFilter = rootQueryVariables?.filter;
|
||||
const rootQueryOrderBy = rootQueryVariables?.orderBy;
|
||||
|
||||
for (const updatedRecord of updatedRecords) {
|
||||
const updatedRecordMatchesThisRootQueryFilter =
|
||||
isRecordMatchingFilter({
|
||||
record: updatedRecord,
|
||||
filter: rootQueryFilter ?? {},
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const updatedRecordIndexInRootQueryEdges =
|
||||
rootQueryCurrentEdges.findIndex(
|
||||
(cachedEdge) =>
|
||||
readField('id', cachedEdge.node) === updatedRecord.id,
|
||||
);
|
||||
|
||||
const updatedRecordFoundInRootQueryEdges =
|
||||
updatedRecordIndexInRootQueryEdges > -1;
|
||||
|
||||
const updatedRecordShouldBeAddedToRootQueryEdges =
|
||||
updatedRecordMatchesThisRootQueryFilter &&
|
||||
!updatedRecordFoundInRootQueryEdges;
|
||||
|
||||
const updatedRecordShouldBeRemovedFromRootQueryEdges =
|
||||
!updatedRecordMatchesThisRootQueryFilter &&
|
||||
updatedRecordFoundInRootQueryEdges;
|
||||
|
||||
if (updatedRecordShouldBeAddedToRootQueryEdges) {
|
||||
const updatedRecordNodeReference = toReference(updatedRecord);
|
||||
|
||||
if (isDefined(updatedRecordNodeReference)) {
|
||||
rootQueryNextEdges.push({
|
||||
__typename: getEdgeTypename(objectMetadataItem.nameSingular),
|
||||
node: updatedRecordNodeReference,
|
||||
cursor: '',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (updatedRecordShouldBeRemovedFromRootQueryEdges) {
|
||||
rootQueryNextEdges.splice(updatedRecordIndexInRootQueryEdges, 1);
|
||||
}
|
||||
}
|
||||
|
||||
const rootQueryNextEdgesShouldBeSorted = isDefined(rootQueryOrderBy);
|
||||
|
||||
if (
|
||||
rootQueryNextEdgesShouldBeSorted &&
|
||||
Object.getOwnPropertyNames(rootQueryOrderBy).length > 0
|
||||
) {
|
||||
rootQueryNextEdges = sortCachedObjectEdges({
|
||||
edges: rootQueryNextEdges,
|
||||
orderBy: rootQueryOrderBy,
|
||||
readCacheField: readField,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...rootQueryConnection,
|
||||
edges: rootQueryNextEdges,
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
@ -70,6 +70,7 @@ type CommandGroupConfig = {
|
||||
key?: string;
|
||||
firstHotKey?: string;
|
||||
secondHotKey?: string;
|
||||
shouldCloseCommandMenuOnClick?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
@ -253,6 +254,7 @@ export const CommandMenu = () => {
|
||||
id,
|
||||
label: `${firstName} ${lastName}`,
|
||||
to: `object/person/${id}`,
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
})),
|
||||
[people],
|
||||
);
|
||||
@ -263,6 +265,7 @@ export const CommandMenu = () => {
|
||||
id,
|
||||
label: name ?? '',
|
||||
to: `object/company/${id}`,
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
})),
|
||||
[companies],
|
||||
);
|
||||
@ -273,6 +276,7 @@ export const CommandMenu = () => {
|
||||
id,
|
||||
label: name ?? '',
|
||||
to: `object/opportunity/${id}`,
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
})),
|
||||
[opportunities],
|
||||
);
|
||||
@ -284,6 +288,7 @@ export const CommandMenu = () => {
|
||||
label: note.title ?? '',
|
||||
to: '',
|
||||
onCommandClick: () => openActivityRightDrawer(note.id),
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
})),
|
||||
[notes, openActivityRightDrawer],
|
||||
);
|
||||
@ -295,6 +300,7 @@ export const CommandMenu = () => {
|
||||
label: task.title ?? '',
|
||||
to: '',
|
||||
onCommandClick: () => openActivityRightDrawer(task.id),
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
})),
|
||||
[tasks, openActivityRightDrawer],
|
||||
);
|
||||
@ -307,6 +313,7 @@ export const CommandMenu = () => {
|
||||
id: objectRecord.record.id,
|
||||
label: objectRecord.recordIdentifier.name,
|
||||
to: `object/${objectRecord.objectMetadataItem.nameSingular}/${objectRecord.record.id}`,
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
})),
|
||||
);
|
||||
});
|
||||
@ -488,6 +495,7 @@ export const CommandMenu = () => {
|
||||
onClick: command.onCommandClick,
|
||||
firstHotKey: command.firstHotKey,
|
||||
secondHotKey: command.secondHotKey,
|
||||
shouldCloseCommandMenuOnClick: command.shouldCloseCommandMenuOnClick,
|
||||
}),
|
||||
},
|
||||
{
|
||||
@ -501,6 +509,7 @@ export const CommandMenu = () => {
|
||||
onClick: command.onCommandClick,
|
||||
firstHotKey: command.firstHotKey,
|
||||
secondHotKey: command.secondHotKey,
|
||||
shouldCloseCommandMenuOnClick: command.shouldCloseCommandMenuOnClick,
|
||||
}),
|
||||
},
|
||||
{
|
||||
@ -520,6 +529,7 @@ export const CommandMenu = () => {
|
||||
),
|
||||
firstHotKey: person.firstHotKey,
|
||||
secondHotKey: person.secondHotKey,
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
}),
|
||||
},
|
||||
{
|
||||
@ -540,6 +550,7 @@ export const CommandMenu = () => {
|
||||
),
|
||||
firstHotKey: company.firstHotKey,
|
||||
secondHotKey: company.secondHotKey,
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
}),
|
||||
},
|
||||
{
|
||||
@ -557,6 +568,7 @@ export const CommandMenu = () => {
|
||||
placeholder={opportunity.name ?? ''}
|
||||
/>
|
||||
),
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
}),
|
||||
},
|
||||
{
|
||||
@ -567,6 +579,7 @@ export const CommandMenu = () => {
|
||||
Icon: IconNotes,
|
||||
label: note.title ?? '',
|
||||
onClick: () => openActivityRightDrawer(note.id),
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
}),
|
||||
},
|
||||
{
|
||||
@ -577,6 +590,7 @@ export const CommandMenu = () => {
|
||||
Icon: IconCheckbox,
|
||||
label: task.title ?? '',
|
||||
onClick: () => openActivityRightDrawer(task.id),
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
}),
|
||||
},
|
||||
...Object.entries(customObjectRecordsMap).map(
|
||||
@ -596,6 +610,7 @@ export const CommandMenu = () => {
|
||||
placeholder={objectRecord.recordIdentifier.name ?? ''}
|
||||
/>
|
||||
),
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
}),
|
||||
}),
|
||||
),
|
||||
@ -627,8 +642,17 @@ export const CommandMenu = () => {
|
||||
].find((cmd) => cmd.id === itemId);
|
||||
|
||||
if (isDefined(command)) {
|
||||
const { to, onCommandClick } = command;
|
||||
onItemClick(onCommandClick, to);
|
||||
const {
|
||||
to,
|
||||
onCommandClick,
|
||||
shouldCloseCommandMenuOnClick,
|
||||
} = command;
|
||||
|
||||
onItemClick({
|
||||
shouldCloseCommandMenuOnClick,
|
||||
onClick: onCommandClick,
|
||||
to,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
@ -745,6 +769,9 @@ export const CommandMenu = () => {
|
||||
secondHotKey={
|
||||
workflowRunGlobalCommand.secondHotKey
|
||||
}
|
||||
shouldCloseCommandMenuOnClick={
|
||||
workflowRunGlobalCommand.shouldCloseCommandMenuOnClick
|
||||
}
|
||||
/>
|
||||
</SelectableItem>
|
||||
),
|
||||
@ -765,6 +792,7 @@ export const CommandMenu = () => {
|
||||
key,
|
||||
firstHotKey,
|
||||
secondHotKey,
|
||||
shouldCloseCommandMenuOnClick,
|
||||
} = renderItem(item);
|
||||
return (
|
||||
<SelectableItem itemId={id} key={id}>
|
||||
@ -777,6 +805,9 @@ export const CommandMenu = () => {
|
||||
onClick={onClick}
|
||||
firstHotKey={firstHotKey}
|
||||
secondHotKey={secondHotKey}
|
||||
shouldCloseCommandMenuOnClick={
|
||||
shouldCloseCommandMenuOnClick
|
||||
}
|
||||
/>
|
||||
</SelectableItem>
|
||||
);
|
||||
|
@ -14,6 +14,7 @@ export type CommandMenuItemProps = {
|
||||
Icon?: IconComponent;
|
||||
firstHotKey?: string;
|
||||
secondHotKey?: string;
|
||||
shouldCloseCommandMenuOnClick?: boolean;
|
||||
};
|
||||
|
||||
export const CommandMenuItem = ({
|
||||
@ -24,6 +25,7 @@ export const CommandMenuItem = ({
|
||||
Icon,
|
||||
firstHotKey,
|
||||
secondHotKey,
|
||||
shouldCloseCommandMenuOnClick,
|
||||
}: CommandMenuItemProps) => {
|
||||
const { onItemClick } = useCommandMenu();
|
||||
|
||||
@ -40,7 +42,13 @@ export const CommandMenuItem = ({
|
||||
text={label}
|
||||
firstHotKey={firstHotKey}
|
||||
secondHotKey={secondHotKey}
|
||||
onClick={() => onItemClick(onClick, to)}
|
||||
onClick={() =>
|
||||
onItemClick({
|
||||
shouldCloseCommandMenuOnClick,
|
||||
onClick,
|
||||
to,
|
||||
})
|
||||
}
|
||||
isSelected={isSelectedItemId}
|
||||
/>
|
||||
);
|
||||
|
@ -17,6 +17,7 @@ export const COMMAND_MENU_NAVIGATE_COMMANDS: { [key: string]: Command } = {
|
||||
firstHotKey: 'G',
|
||||
secondHotKey: 'P',
|
||||
Icon: IconUser,
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
},
|
||||
companies: {
|
||||
id: 'go-to-companies',
|
||||
@ -26,6 +27,7 @@ export const COMMAND_MENU_NAVIGATE_COMMANDS: { [key: string]: Command } = {
|
||||
firstHotKey: 'G',
|
||||
secondHotKey: 'C',
|
||||
Icon: IconBuildingSkyscraper,
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
},
|
||||
opportunities: {
|
||||
id: 'go-to-activities',
|
||||
@ -35,6 +37,7 @@ export const COMMAND_MENU_NAVIGATE_COMMANDS: { [key: string]: Command } = {
|
||||
firstHotKey: 'G',
|
||||
secondHotKey: 'O',
|
||||
Icon: IconTargetArrow,
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
},
|
||||
settings: {
|
||||
id: 'go-to-settings',
|
||||
@ -44,6 +47,7 @@ export const COMMAND_MENU_NAVIGATE_COMMANDS: { [key: string]: Command } = {
|
||||
firstHotKey: 'G',
|
||||
secondHotKey: 'S',
|
||||
Icon: IconSettings,
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
},
|
||||
tasks: {
|
||||
id: 'go-to-tasks',
|
||||
@ -53,5 +57,6 @@ export const COMMAND_MENU_NAVIGATE_COMMANDS: { [key: string]: Command } = {
|
||||
firstHotKey: 'G',
|
||||
secondHotKey: 'T',
|
||||
Icon: IconCheckbox,
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
},
|
||||
};
|
||||
|
@ -82,7 +82,11 @@ describe('useCommandMenu', () => {
|
||||
const onClickMock = jest.fn();
|
||||
|
||||
act(() => {
|
||||
result.current.commandMenu.onItemClick(onClickMock, '/test');
|
||||
result.current.commandMenu.onItemClick({
|
||||
shouldCloseCommandMenuOnClick: true,
|
||||
onClick: onClickMock,
|
||||
to: '/test',
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current.isCommandMenuOpened).toBe(true);
|
||||
|
@ -216,8 +216,21 @@ export const useCommandMenu = () => {
|
||||
);
|
||||
|
||||
const onItemClick = useCallback(
|
||||
(onClick?: () => void, to?: string) => {
|
||||
toggleCommandMenu();
|
||||
({
|
||||
shouldCloseCommandMenuOnClick,
|
||||
onClick,
|
||||
to,
|
||||
}: {
|
||||
shouldCloseCommandMenuOnClick?: boolean;
|
||||
onClick?: () => void;
|
||||
to?: string;
|
||||
}) => {
|
||||
if (
|
||||
isDefined(shouldCloseCommandMenuOnClick) &&
|
||||
shouldCloseCommandMenuOnClick
|
||||
) {
|
||||
toggleCommandMenu();
|
||||
}
|
||||
|
||||
if (isDefined(onClick)) {
|
||||
onClick();
|
||||
|
@ -21,4 +21,5 @@ export type Command = {
|
||||
firstHotKey?: string;
|
||||
secondHotKey?: string;
|
||||
onCommandClick?: () => void;
|
||||
shouldCloseCommandMenuOnClick?: boolean;
|
||||
};
|
||||
|
@ -39,6 +39,7 @@ export const useFindManyRecordsSelectedInContextStore = ({
|
||||
const { records, loading, totalCount } = useFindManyRecords({
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
filter: queryFilter,
|
||||
withSoftDeleted: true,
|
||||
orderBy: [
|
||||
{
|
||||
position: 'AscNullsFirst',
|
||||
|
@ -99,6 +99,7 @@ export const FavoriteFolderPicker = ({
|
||||
toggleFolderSelection={toggleFolderSelection}
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
<DropdownMenuSeparator />
|
||||
<FavoriteFolderPickerFooter dropdownId={dropdownId} />
|
||||
</DropdownMenu>
|
||||
);
|
||||
|
@ -4,16 +4,9 @@ import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
import { useNavigationSection } from '@/ui/navigation/navigation-drawer/hooks/useNavigationSection';
|
||||
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||
import { IconPlus, MenuItem } from 'twenty-ui';
|
||||
|
||||
const StyledFooter = styled.div`
|
||||
border-bottom-left-radius: ${({ theme }) => theme.border.radius.md};
|
||||
border-bottom-right-radius: ${({ theme }) => theme.border.radius.md};
|
||||
border-top: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
`;
|
||||
|
||||
export const FavoriteFolderPickerFooter = ({
|
||||
dropdownId,
|
||||
}: {
|
||||
@ -30,20 +23,18 @@ export const FavoriteFolderPickerFooter = ({
|
||||
const { closeDropdown } = useDropdown(dropdownId);
|
||||
|
||||
return (
|
||||
<StyledFooter>
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItem
|
||||
className="add-folder"
|
||||
onClick={() => {
|
||||
setIsNavigationDrawerExpanded(true);
|
||||
openNavigationSection();
|
||||
setIsFavoriteFolderCreating(true);
|
||||
closeDropdown();
|
||||
}}
|
||||
text="Add folder"
|
||||
LeftIcon={() => <IconPlus size={theme.icon.size.md} />}
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
</StyledFooter>
|
||||
<DropdownMenuItemsContainer scrollable={false}>
|
||||
<MenuItem
|
||||
className="add-folder"
|
||||
onClick={() => {
|
||||
setIsNavigationDrawerExpanded(true);
|
||||
openNavigationSection();
|
||||
setIsFavoriteFolderCreating(true);
|
||||
closeDropdown();
|
||||
}}
|
||||
text="Add folder"
|
||||
LeftIcon={() => <IconPlus size={theme.icon.size.md} />}
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
);
|
||||
};
|
||||
|
@ -246,6 +246,7 @@ mutation UpdateOneFavorite(
|
||||
phones {
|
||||
primaryPhoneNumber
|
||||
primaryPhoneCountryCode
|
||||
primaryPhoneCallingCode
|
||||
additionalPhones
|
||||
}
|
||||
position
|
||||
@ -253,6 +254,7 @@ mutation UpdateOneFavorite(
|
||||
whatsapp {
|
||||
primaryPhoneNumber
|
||||
primaryPhoneCountryCode
|
||||
primaryPhoneCallingCode
|
||||
additionalPhones
|
||||
}
|
||||
workPreference
|
||||
@ -532,6 +534,7 @@ export const mocks = [
|
||||
phones {
|
||||
primaryPhoneNumber
|
||||
primaryPhoneCountryCode
|
||||
primaryPhoneCallingCode
|
||||
additionalPhones
|
||||
}
|
||||
position
|
||||
@ -539,6 +542,7 @@ export const mocks = [
|
||||
whatsapp {
|
||||
primaryPhoneNumber
|
||||
primaryPhoneCountryCode
|
||||
primaryPhoneCallingCode
|
||||
additionalPhones
|
||||
}
|
||||
workPreference
|
||||
|
@ -198,6 +198,7 @@ phone
|
||||
{
|
||||
primaryPhoneNumber
|
||||
primaryPhoneCountryCode
|
||||
primaryPhoneCallingCode
|
||||
}
|
||||
linkedinLink
|
||||
{
|
||||
|
@ -48,6 +48,7 @@ describe('mapObjectMetadataToGraphQLQuery', () => {
|
||||
{
|
||||
primaryPhoneNumber
|
||||
primaryPhoneCountryCode
|
||||
primaryPhoneCallingCode
|
||||
}
|
||||
createdAt
|
||||
avatarUrl
|
||||
|
@ -157,6 +157,7 @@ ${mapObjectMetadataToGraphQLQuery({
|
||||
{
|
||||
primaryPhoneNumber
|
||||
primaryPhoneCountryCode
|
||||
primaryPhoneCallingCode
|
||||
additionalPhones
|
||||
}`;
|
||||
}
|
||||
|
@ -30,6 +30,7 @@ export const PERSON_FRAGMENT_WITH_DEPTH_ZERO_RELATIONS = `
|
||||
phones {
|
||||
primaryPhoneNumber
|
||||
primaryPhoneCountryCode
|
||||
primaryPhoneCallingCode
|
||||
additionalPhones
|
||||
}
|
||||
position
|
||||
@ -37,6 +38,7 @@ export const PERSON_FRAGMENT_WITH_DEPTH_ZERO_RELATIONS = `
|
||||
whatsapp {
|
||||
primaryPhoneNumber
|
||||
primaryPhoneCountryCode
|
||||
primaryPhoneCallingCode
|
||||
additionalPhones
|
||||
}
|
||||
workPreference
|
||||
@ -229,6 +231,7 @@ export const PERSON_FRAGMENT_WITH_DEPTH_ONE_RELATIONS = `
|
||||
phones {
|
||||
primaryPhoneNumber
|
||||
primaryPhoneCountryCode
|
||||
primaryPhoneCallingCode
|
||||
additionalPhones
|
||||
}
|
||||
pointOfContactForOpportunities {
|
||||
@ -305,6 +308,7 @@ export const PERSON_FRAGMENT_WITH_DEPTH_ONE_RELATIONS = `
|
||||
whatsapp {
|
||||
primaryPhoneNumber
|
||||
primaryPhoneCountryCode
|
||||
primaryPhoneCallingCode
|
||||
additionalPhones
|
||||
}
|
||||
workPreference
|
||||
|
@ -38,6 +38,7 @@ export const responseData = {
|
||||
},
|
||||
phones: {
|
||||
primaryPhoneCountryCode: '',
|
||||
primaryPhoneCallingCode: '',
|
||||
primaryPhoneNumber: '',
|
||||
},
|
||||
linkedinLink: {
|
||||
|
@ -43,6 +43,7 @@ export const responseData = {
|
||||
},
|
||||
phones: {
|
||||
primaryPhoneCountryCode: '',
|
||||
primaryPhoneCallingCode: '',
|
||||
primaryPhoneNumber: '',
|
||||
},
|
||||
linkedinLink: {
|
||||
|
@ -178,6 +178,7 @@ const mocks: MockedResponse[] = [
|
||||
phones {
|
||||
primaryPhoneNumber
|
||||
primaryPhoneCountryCode
|
||||
primaryPhoneCallingCode
|
||||
additionalPhones
|
||||
}
|
||||
position
|
||||
@ -185,6 +186,7 @@ const mocks: MockedResponse[] = [
|
||||
whatsapp {
|
||||
primaryPhoneNumber
|
||||
primaryPhoneCountryCode
|
||||
primaryPhoneCallingCode
|
||||
additionalPhones
|
||||
}
|
||||
workPreference
|
||||
@ -332,6 +334,7 @@ const mocks: MockedResponse[] = [
|
||||
phones {
|
||||
primaryPhoneNumber
|
||||
primaryPhoneCountryCode
|
||||
primaryPhoneCallingCode
|
||||
additionalPhones
|
||||
}
|
||||
position
|
||||
@ -339,6 +342,7 @@ const mocks: MockedResponse[] = [
|
||||
whatsapp {
|
||||
primaryPhoneNumber
|
||||
primaryPhoneCountryCode
|
||||
primaryPhoneCallingCode
|
||||
additionalPhones
|
||||
}
|
||||
workPreference
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
|
||||
import { triggerUpdateRecordOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffect';
|
||||
import { triggerUpdateRecordOptimisticEffectByBatch } from '@/apollo/optimistic-effect/utils/triggerUpdateRecordOptimisticEffectByBatch';
|
||||
import { apiConfigState } from '@/client-config/states/apiConfigState';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
@ -8,6 +8,7 @@ import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordF
|
||||
import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord';
|
||||
import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache';
|
||||
import { DEFAULT_MUTATION_BATCH_SIZE } from '@/object-record/constants/DefaultMutationBatchSize';
|
||||
import { RecordGqlNode } from '@/object-record/graphql/types/RecordGqlNode';
|
||||
import { useDeleteManyRecordsMutation } from '@/object-record/hooks/useDeleteManyRecordsMutation';
|
||||
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
@ -80,6 +81,9 @@ export const useDeleteManyRecords = ({
|
||||
.map((idToDelete) => getRecordFromCache(idToDelete, apolloClient.cache))
|
||||
.filter(isDefined);
|
||||
|
||||
const cachedRecordsWithConnection: RecordGqlNode[] = [];
|
||||
const optimisticRecordsWithConnection: RecordGqlNode[] = [];
|
||||
|
||||
if (!options?.skipOptimisticEffect) {
|
||||
cachedRecords.forEach((cachedRecord) => {
|
||||
if (!cachedRecord || !cachedRecord.id) {
|
||||
@ -112,20 +116,23 @@ export const useDeleteManyRecords = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
cachedRecordsWithConnection.push(cachedRecordWithConnection);
|
||||
optimisticRecordsWithConnection.push(optimisticRecordWithConnection);
|
||||
|
||||
updateRecordFromCache({
|
||||
objectMetadataItems,
|
||||
objectMetadataItem,
|
||||
cache: apolloClient.cache,
|
||||
record: computedOptimisticRecord,
|
||||
});
|
||||
});
|
||||
|
||||
triggerUpdateRecordOptimisticEffect({
|
||||
cache: apolloClient.cache,
|
||||
objectMetadataItem,
|
||||
currentRecord: cachedRecordWithConnection,
|
||||
updatedRecord: optimisticRecordWithConnection,
|
||||
objectMetadataItems,
|
||||
});
|
||||
triggerUpdateRecordOptimisticEffectByBatch({
|
||||
cache: apolloClient.cache,
|
||||
objectMetadataItem,
|
||||
currentRecords: cachedRecordsWithConnection,
|
||||
updatedRecords: optimisticRecordsWithConnection,
|
||||
objectMetadataItems,
|
||||
});
|
||||
}
|
||||
|
||||
@ -137,6 +144,9 @@ export const useDeleteManyRecords = ({
|
||||
},
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
const cachedRecordsWithConnection: RecordGqlNode[] = [];
|
||||
const optimisticRecordsWithConnection: RecordGqlNode[] = [];
|
||||
|
||||
cachedRecords.forEach((cachedRecord) => {
|
||||
if (isUndefinedOrNull(cachedRecord?.id)) {
|
||||
return;
|
||||
@ -175,16 +185,21 @@ export const useDeleteManyRecords = ({
|
||||
!optimisticRecordWithConnection ||
|
||||
!cachedRecordWithConnection
|
||||
) {
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
|
||||
triggerUpdateRecordOptimisticEffect({
|
||||
cache: apolloClient.cache,
|
||||
objectMetadataItem,
|
||||
currentRecord: optimisticRecordWithConnection,
|
||||
updatedRecord: cachedRecordWithConnection,
|
||||
objectMetadataItems,
|
||||
});
|
||||
cachedRecordsWithConnection.push(cachedRecordWithConnection);
|
||||
optimisticRecordsWithConnection.push(
|
||||
optimisticRecordWithConnection,
|
||||
);
|
||||
});
|
||||
|
||||
triggerUpdateRecordOptimisticEffectByBatch({
|
||||
cache: apolloClient.cache,
|
||||
objectMetadataItem,
|
||||
currentRecords: optimisticRecordsWithConnection,
|
||||
updatedRecords: cachedRecordsWithConnection,
|
||||
objectMetadataItems,
|
||||
});
|
||||
|
||||
throw error;
|
||||
|
@ -20,6 +20,7 @@ export type UseFindManyRecordsParams<T> = ObjectMetadataItemIdentifier &
|
||||
skip?: boolean;
|
||||
recordGqlFields?: RecordGqlOperationGqlRecordFields;
|
||||
fetchPolicy?: WatchQueryFetchPolicy;
|
||||
withSoftDeleted?: boolean;
|
||||
};
|
||||
|
||||
export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
|
||||
@ -33,6 +34,7 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
|
||||
onError,
|
||||
onCompleted,
|
||||
cursorFilter,
|
||||
withSoftDeleted = false,
|
||||
}: UseFindManyRecordsParams<T>) => {
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular,
|
||||
@ -61,11 +63,18 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
|
||||
onCompleted,
|
||||
});
|
||||
|
||||
const withSoftDeleterFilter = {
|
||||
or: [{ deletedAt: { is: 'NULL' } }, { deletedAt: { is: 'NOT_NULL' } }],
|
||||
};
|
||||
|
||||
const { data, loading, error, fetchMore } =
|
||||
useQuery<RecordGqlOperationFindManyResult>(findManyRecordsQuery, {
|
||||
skip: skip || !objectMetadataItem,
|
||||
variables: {
|
||||
filter,
|
||||
filter: {
|
||||
...filter,
|
||||
...(withSoftDeleted ? withSoftDeleterFilter : {}),
|
||||
},
|
||||
orderBy,
|
||||
lastCursor: cursorFilter?.cursor ?? undefined,
|
||||
limit: cursorFilter?.limit ?? limit,
|
||||
|
@ -62,7 +62,7 @@ export const ObjectOptionsDropdownFieldsContent = () => {
|
||||
showDragGrip={true}
|
||||
/>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer withoutScrollWrapper>
|
||||
<DropdownMenuItemsContainer scrollable={false}>
|
||||
<MenuItemNavigate
|
||||
onClick={() => onContentChange('hiddenFields')}
|
||||
LeftIcon={IconEyeOff}
|
||||
|
@ -87,7 +87,7 @@ export const ObjectOptionsDropdownHiddenFieldsContent = () => {
|
||||
closeDropdown();
|
||||
}}
|
||||
>
|
||||
<DropdownMenuItemsContainer withoutScrollWrapper>
|
||||
<DropdownMenuItemsContainer scrollable={false}>
|
||||
<MenuItem LeftIcon={IconSettings} text="Edit Fields" />
|
||||
</DropdownMenuItemsContainer>
|
||||
</UndecoratedLink>
|
||||
|
@ -98,7 +98,7 @@ export const ObjectOptionsDropdownMenuContent = () => {
|
||||
{/** TODO: Should be removed when view settings contains more options */}
|
||||
{viewType === ViewType.Kanban && (
|
||||
<>
|
||||
<DropdownMenuItemsContainer withoutScrollWrapper>
|
||||
<DropdownMenuItemsContainer scrollable={false}>
|
||||
<MenuItem
|
||||
onClick={() => onContentChange('viewSettings')}
|
||||
LeftIcon={IconLayout}
|
||||
@ -109,7 +109,7 @@ export const ObjectOptionsDropdownMenuContent = () => {
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
<DropdownMenuItemsContainer withoutScrollWrapper>
|
||||
<DropdownMenuItemsContainer scrollable={false}>
|
||||
<MenuItem
|
||||
onClick={() => onContentChange('fields')}
|
||||
LeftIcon={IconTag}
|
||||
|
@ -7,6 +7,8 @@ import { GRAY_SCALE } from 'twenty-ui';
|
||||
import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
|
||||
import { isRecordBoardFetchingRecordsByColumnFamilyState } from '@/object-record/record-board/states/isRecordBoardFetchingRecordsByColumnFamilyState';
|
||||
import { recordBoardShouldFetchMoreInColumnComponentFamilyState } from '@/object-record/record-board/states/recordBoardShouldFetchMoreInColumnComponentFamilyState';
|
||||
import { isRecordIndexLoadMoreLockedComponentState } from '@/object-record/record-index/states/isRecordIndexLoadMoreLockedComponentState';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useSetRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentFamilyStateV2';
|
||||
|
||||
const StyledText = styled.div`
|
||||
@ -31,11 +33,23 @@ export const RecordBoardColumnFetchMoreLoader = () => {
|
||||
columnDefinition.id,
|
||||
);
|
||||
|
||||
const isLoadMoreLocked = useRecoilComponentValueV2(
|
||||
isRecordIndexLoadMoreLockedComponentState,
|
||||
);
|
||||
|
||||
const { ref, inView } = useInView();
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoadMoreLocked) {
|
||||
return;
|
||||
}
|
||||
|
||||
setShouldFetchMore(inView);
|
||||
}, [setShouldFetchMore, inView]);
|
||||
}, [setShouldFetchMore, inView, isLoadMoreLocked]);
|
||||
|
||||
if (isLoadMoreLocked) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={ref}>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { FormAddressFieldInput } from '@/object-record/record-field/form-types/components/FormAddressFieldInput';
|
||||
import { FormBooleanFieldInput } from '@/object-record/record-field/form-types/components/FormBooleanFieldInput';
|
||||
import { FormDateTimeFieldInputBase } from '@/object-record/record-field/form-types/components/FormDateTimeFieldInputBase';
|
||||
import { FormDateFieldInput } from '@/object-record/record-field/form-types/components/FormDateFieldInput';
|
||||
import { FormEmailsFieldInput } from '@/object-record/record-field/form-types/components/FormEmailsFieldInput';
|
||||
import { FormFullNameFieldInput } from '@/object-record/record-field/form-types/components/FormFullNameFieldInput';
|
||||
import { FormLinksFieldInput } from '@/object-record/record-field/form-types/components/FormLinksFieldInput';
|
||||
@ -34,6 +34,7 @@ import { isFieldSelect } from '@/object-record/record-field/types/guards/isField
|
||||
import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText';
|
||||
import { isFieldUuid } from '@/object-record/record-field/types/guards/isFieldUuid';
|
||||
import { JsonValue } from 'type-fest';
|
||||
import { FormDateTimeFieldInput } from '@/object-record/record-field/form-types/components/FormDateTimeFieldInput';
|
||||
|
||||
type FormFieldInputProps = {
|
||||
field: FieldDefinition<FieldMetadata>;
|
||||
@ -109,16 +110,14 @@ export const FormFieldInput = ({
|
||||
VariablePicker={VariablePicker}
|
||||
/>
|
||||
) : isFieldDate(field) ? (
|
||||
<FormDateTimeFieldInputBase
|
||||
mode="date"
|
||||
<FormDateFieldInput
|
||||
label={field.label}
|
||||
defaultValue={defaultValue as string | undefined}
|
||||
onPersist={onPersist}
|
||||
VariablePicker={VariablePicker}
|
||||
/>
|
||||
) : isFieldDateTime(field) ? (
|
||||
<FormDateTimeFieldInputBase
|
||||
mode="datetime"
|
||||
<FormDateTimeFieldInput
|
||||
label={field.label}
|
||||
defaultValue={defaultValue as string | undefined}
|
||||
onPersist={onPersist}
|
||||
|
@ -0,0 +1,26 @@
|
||||
import { FormDateTimeFieldInput } from '@/object-record/record-field/form-types/components/FormDateTimeFieldInput';
|
||||
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
|
||||
|
||||
type FormDateFieldInputProps = {
|
||||
label?: string;
|
||||
defaultValue: string | undefined;
|
||||
onPersist: (value: string | null) => void;
|
||||
VariablePicker?: VariablePickerComponent;
|
||||
};
|
||||
|
||||
export const FormDateFieldInput = ({
|
||||
label,
|
||||
defaultValue,
|
||||
onPersist,
|
||||
VariablePicker,
|
||||
}: FormDateFieldInputProps) => {
|
||||
return (
|
||||
<FormDateTimeFieldInput
|
||||
dateOnly
|
||||
label={label}
|
||||
defaultValue={defaultValue}
|
||||
onPersist={onPersist}
|
||||
VariablePicker={VariablePicker}
|
||||
/>
|
||||
);
|
||||
};
|
@ -69,27 +69,26 @@ type DraftValue =
|
||||
value: string;
|
||||
};
|
||||
|
||||
type FormDateTimeFieldInputBaseProps = {
|
||||
mode: 'date' | 'datetime';
|
||||
type FormDateTimeFieldInputProps = {
|
||||
dateOnly?: boolean;
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
defaultValue: string | undefined;
|
||||
onPersist: (value: string | null) => void;
|
||||
VariablePicker?: VariablePickerComponent;
|
||||
};
|
||||
|
||||
export const FormDateTimeFieldInputBase = ({
|
||||
mode,
|
||||
export const FormDateTimeFieldInput = ({
|
||||
dateOnly,
|
||||
label,
|
||||
defaultValue,
|
||||
onPersist,
|
||||
VariablePicker,
|
||||
}: FormDateTimeFieldInputBaseProps) => {
|
||||
}: FormDateTimeFieldInputProps) => {
|
||||
const { timeZone } = useContext(UserContext);
|
||||
|
||||
const inputId = useId();
|
||||
|
||||
const placeholder = mode === 'date' ? 'mm/dd/yyyy' : 'mm/dd/yyyy hh:mm';
|
||||
|
||||
const [draftValue, setDraftValue] = useState<DraftValue>(
|
||||
isStandaloneVariableString(defaultValue)
|
||||
? {
|
||||
@ -116,7 +115,7 @@ export const FormDateTimeFieldInputBase = ({
|
||||
isDefined(draftValueAsDate) && !isStandaloneVariableString(defaultValue)
|
||||
? parseDateToString({
|
||||
date: draftValueAsDate,
|
||||
isDateTimeInput: mode === 'datetime',
|
||||
isDateTimeInput: !dateOnly,
|
||||
userTimezone: timeZone,
|
||||
})
|
||||
: '',
|
||||
@ -143,6 +142,8 @@ export const FormDateTimeFieldInputBase = ({
|
||||
const displayDatePicker =
|
||||
draftValue.type === 'static' && draftValue.mode === 'edit';
|
||||
|
||||
const placeholder = dateOnly ? 'mm/dd/yyyy' : 'mm/dd/yyyy hh:mm';
|
||||
|
||||
useListenClickOutside({
|
||||
refs: [datePickerWrapperRef],
|
||||
listenerId: 'FormDateTimeFieldInputBase',
|
||||
@ -168,7 +169,7 @@ export const FormDateTimeFieldInputBase = ({
|
||||
isDefined(newDate)
|
||||
? parseDateToString({
|
||||
date: newDate,
|
||||
isDateTimeInput: mode === 'datetime',
|
||||
isDateTimeInput: !dateOnly,
|
||||
userTimezone: timeZone,
|
||||
})
|
||||
: '',
|
||||
@ -226,7 +227,7 @@ export const FormDateTimeFieldInputBase = ({
|
||||
isDefined(newDate)
|
||||
? parseDateToString({
|
||||
date: newDate,
|
||||
isDateTimeInput: mode === 'datetime',
|
||||
isDateTimeInput: !dateOnly,
|
||||
userTimezone: timeZone,
|
||||
})
|
||||
: '',
|
||||
@ -262,7 +263,7 @@ export const FormDateTimeFieldInputBase = ({
|
||||
|
||||
const parsedInputDateTime = parseStringToDate({
|
||||
dateAsString: inputDateTimeTrimmed,
|
||||
isDateTimeInput: mode === 'datetime',
|
||||
isDateTimeInput: !dateOnly,
|
||||
userTimezone: timeZone,
|
||||
});
|
||||
|
||||
@ -288,7 +289,7 @@ export const FormDateTimeFieldInputBase = ({
|
||||
setInputDateTime(
|
||||
parseDateToString({
|
||||
date: validatedDate,
|
||||
isDateTimeInput: mode === 'datetime',
|
||||
isDateTimeInput: !dateOnly,
|
||||
userTimezone: timeZone,
|
||||
}),
|
||||
);
|
@ -0,0 +1,370 @@
|
||||
import { MAX_DATE } from '@/ui/input/components/internal/date/constants/MaxDate';
|
||||
import { MIN_DATE } from '@/ui/input/components/internal/date/constants/MinDate';
|
||||
import { parseDateToString } from '@/ui/input/components/internal/date/utils/parseDateToString';
|
||||
import { expect } from '@storybook/jest';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import {
|
||||
fn,
|
||||
userEvent,
|
||||
waitFor,
|
||||
waitForElementToBeRemoved,
|
||||
within,
|
||||
} from '@storybook/test';
|
||||
import { DateTime } from 'luxon';
|
||||
import { FormDateFieldInput } from '../FormDateFieldInput';
|
||||
|
||||
const meta: Meta<typeof FormDateFieldInput> = {
|
||||
title: 'UI/Data/Field/Form/Input/FormDateFieldInput',
|
||||
component: FormDateFieldInput,
|
||||
args: {},
|
||||
argTypes: {},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof FormDateFieldInput>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
label: 'Created At',
|
||||
defaultValue: '2024-12-09T13:20:19.631Z',
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await canvas.findByText('Created At');
|
||||
await canvas.findByDisplayValue('12/09/2024');
|
||||
},
|
||||
};
|
||||
|
||||
export const WithDefaultEmptyValue: Story = {
|
||||
args: {
|
||||
label: 'Created At',
|
||||
defaultValue: undefined,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await canvas.findByText('Created At');
|
||||
await canvas.findByDisplayValue('');
|
||||
await canvas.findByPlaceholderText('mm/dd/yyyy');
|
||||
},
|
||||
};
|
||||
|
||||
export const SetsDateWithInput: Story = {
|
||||
args: {
|
||||
label: 'Created At',
|
||||
defaultValue: undefined,
|
||||
onPersist: fn(),
|
||||
},
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const input = await canvas.findByPlaceholderText('mm/dd/yyyy');
|
||||
|
||||
await userEvent.click(input);
|
||||
|
||||
const dialog = await canvas.findByRole('dialog');
|
||||
expect(dialog).toBeVisible();
|
||||
|
||||
await userEvent.type(input, '12/08/2024{enter}');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(args.onPersist).toHaveBeenCalledWith('2024-12-08T00:00:00.000Z');
|
||||
});
|
||||
|
||||
expect(dialog).toBeVisible();
|
||||
},
|
||||
};
|
||||
|
||||
export const SetsDateWithDatePicker: Story = {
|
||||
args: {
|
||||
label: 'Created At',
|
||||
defaultValue: undefined,
|
||||
onPersist: fn(),
|
||||
},
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const input = await canvas.findByPlaceholderText('mm/dd/yyyy');
|
||||
expect(input).toBeVisible();
|
||||
|
||||
await userEvent.click(input);
|
||||
|
||||
const datePicker = await canvas.findByRole('dialog');
|
||||
expect(datePicker).toBeVisible();
|
||||
|
||||
const dayToChoose = await within(datePicker).findByRole('option', {
|
||||
name: 'Choose Saturday, December 7th, 2024',
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
userEvent.click(dayToChoose),
|
||||
|
||||
waitForElementToBeRemoved(datePicker),
|
||||
waitFor(() => {
|
||||
expect(args.onPersist).toHaveBeenCalledWith(
|
||||
expect.stringMatching(/^2024-12-07/),
|
||||
);
|
||||
}),
|
||||
waitFor(() => {
|
||||
expect(canvas.getByDisplayValue('12/07/2024')).toBeVisible();
|
||||
}),
|
||||
]);
|
||||
},
|
||||
};
|
||||
|
||||
export const ResetsDateByClickingButton: Story = {
|
||||
args: {
|
||||
label: 'Created At',
|
||||
defaultValue: '2024-12-09T13:20:19.631Z',
|
||||
onPersist: fn(),
|
||||
},
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const input = await canvas.findByPlaceholderText('mm/dd/yyyy');
|
||||
expect(input).toBeVisible();
|
||||
|
||||
await userEvent.click(input);
|
||||
|
||||
const datePicker = await canvas.findByRole('dialog');
|
||||
expect(datePicker).toBeVisible();
|
||||
|
||||
const clearButton = await canvas.findByText('Clear');
|
||||
|
||||
await Promise.all([
|
||||
userEvent.click(clearButton),
|
||||
|
||||
waitForElementToBeRemoved(datePicker),
|
||||
waitFor(() => {
|
||||
expect(args.onPersist).toHaveBeenCalledWith(null);
|
||||
}),
|
||||
waitFor(() => {
|
||||
expect(input).toHaveDisplayValue('');
|
||||
}),
|
||||
]);
|
||||
},
|
||||
};
|
||||
|
||||
export const ResetsDateByErasingInputContent: Story = {
|
||||
args: {
|
||||
label: 'Created At',
|
||||
defaultValue: '2024-12-09T13:20:19.631Z',
|
||||
onPersist: fn(),
|
||||
},
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const input = await canvas.findByPlaceholderText('mm/dd/yyyy');
|
||||
expect(input).toBeVisible();
|
||||
|
||||
expect(input).toHaveDisplayValue('12/09/2024');
|
||||
|
||||
await userEvent.clear(input);
|
||||
|
||||
await Promise.all([
|
||||
userEvent.type(input, '{Enter}'),
|
||||
|
||||
waitForElementToBeRemoved(() => canvas.queryByRole('dialog')),
|
||||
waitFor(() => {
|
||||
expect(args.onPersist).toHaveBeenCalledWith(null);
|
||||
}),
|
||||
waitFor(() => {
|
||||
expect(input).toHaveDisplayValue('');
|
||||
}),
|
||||
]);
|
||||
},
|
||||
};
|
||||
|
||||
export const DefaultsToMinValueWhenTypingReallyOldDate: Story = {
|
||||
args: {
|
||||
label: 'Created At',
|
||||
defaultValue: undefined,
|
||||
onPersist: fn(),
|
||||
},
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const input = await canvas.findByPlaceholderText('mm/dd/yyyy');
|
||||
expect(input).toBeVisible();
|
||||
|
||||
await userEvent.click(input);
|
||||
|
||||
const datePicker = await canvas.findByRole('dialog');
|
||||
expect(datePicker).toBeVisible();
|
||||
|
||||
await Promise.all([
|
||||
userEvent.type(input, '02/02/1500{Enter}'),
|
||||
|
||||
waitFor(() => {
|
||||
expect(args.onPersist).toHaveBeenCalledWith(MIN_DATE.toISOString());
|
||||
}),
|
||||
waitFor(() => {
|
||||
expect(input).toHaveDisplayValue(
|
||||
parseDateToString({
|
||||
date: MIN_DATE,
|
||||
isDateTimeInput: false,
|
||||
userTimezone: undefined,
|
||||
}),
|
||||
);
|
||||
}),
|
||||
waitFor(() => {
|
||||
const expectedDate = DateTime.fromJSDate(MIN_DATE)
|
||||
.toLocal()
|
||||
.set({
|
||||
day: MIN_DATE.getUTCDate(),
|
||||
month: MIN_DATE.getUTCMonth() + 1,
|
||||
year: MIN_DATE.getUTCFullYear(),
|
||||
hour: 0,
|
||||
minute: 0,
|
||||
second: 0,
|
||||
millisecond: 0,
|
||||
});
|
||||
|
||||
const selectedDay = within(datePicker).getByRole('option', {
|
||||
selected: true,
|
||||
name: (accessibleName) => {
|
||||
// The name looks like "Choose Sunday, December 31st, 1899"
|
||||
return accessibleName.includes(expectedDate.toFormat('yyyy'));
|
||||
},
|
||||
});
|
||||
expect(selectedDay).toBeVisible();
|
||||
}),
|
||||
]);
|
||||
},
|
||||
};
|
||||
|
||||
export const DefaultsToMaxValueWhenTypingReallyFarDate: Story = {
|
||||
args: {
|
||||
label: 'Created At',
|
||||
defaultValue: undefined,
|
||||
onPersist: fn(),
|
||||
},
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const input = await canvas.findByPlaceholderText('mm/dd/yyyy');
|
||||
expect(input).toBeVisible();
|
||||
|
||||
await userEvent.click(input);
|
||||
|
||||
const datePicker = await canvas.findByRole('dialog');
|
||||
expect(datePicker).toBeVisible();
|
||||
|
||||
await Promise.all([
|
||||
userEvent.type(input, '02/02/2500{Enter}'),
|
||||
|
||||
waitFor(() => {
|
||||
expect(args.onPersist).toHaveBeenCalledWith(MAX_DATE.toISOString());
|
||||
}),
|
||||
waitFor(() => {
|
||||
expect(input).toHaveDisplayValue(
|
||||
parseDateToString({
|
||||
date: MAX_DATE,
|
||||
isDateTimeInput: false,
|
||||
userTimezone: undefined,
|
||||
}),
|
||||
);
|
||||
}),
|
||||
waitFor(() => {
|
||||
const expectedDate = DateTime.fromJSDate(MAX_DATE)
|
||||
.toLocal()
|
||||
.set({
|
||||
day: MAX_DATE.getUTCDate(),
|
||||
month: MAX_DATE.getUTCMonth() + 1,
|
||||
year: MAX_DATE.getUTCFullYear(),
|
||||
hour: 0,
|
||||
minute: 0,
|
||||
second: 0,
|
||||
millisecond: 0,
|
||||
});
|
||||
|
||||
const selectedDay = within(datePicker).getByRole('option', {
|
||||
selected: true,
|
||||
name: (accessibleName) => {
|
||||
// The name looks like "Choose Thursday, December 30th, 2100"
|
||||
return accessibleName.includes(expectedDate.toFormat('yyyy'));
|
||||
},
|
||||
});
|
||||
expect(selectedDay).toBeVisible();
|
||||
}),
|
||||
]);
|
||||
},
|
||||
};
|
||||
|
||||
export const SwitchesToStandaloneVariable: Story = {
|
||||
args: {
|
||||
label: 'Created At',
|
||||
defaultValue: undefined,
|
||||
onPersist: fn(),
|
||||
VariablePicker: ({ onVariableSelect }) => {
|
||||
return (
|
||||
<button
|
||||
onClick={() => {
|
||||
onVariableSelect('{{test}}');
|
||||
}}
|
||||
>
|
||||
Add variable
|
||||
</button>
|
||||
);
|
||||
},
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const addVariableButton = await canvas.findByText('Add variable');
|
||||
await userEvent.click(addVariableButton);
|
||||
|
||||
const variableTag = await canvas.findByText('test');
|
||||
expect(variableTag).toBeVisible();
|
||||
|
||||
const removeVariableButton = canvas.getByTestId(/^remove-icon/);
|
||||
|
||||
await Promise.all([
|
||||
userEvent.click(removeVariableButton),
|
||||
|
||||
waitForElementToBeRemoved(variableTag),
|
||||
waitFor(() => {
|
||||
const input = canvas.getByPlaceholderText('mm/dd/yyyy');
|
||||
expect(input).toBeVisible();
|
||||
}),
|
||||
]);
|
||||
},
|
||||
};
|
||||
|
||||
export const ClickingOutsideDoesNotResetInputState: Story = {
|
||||
args: {
|
||||
label: 'Created At',
|
||||
defaultValue: '2024-12-09T13:20:19.631Z',
|
||||
onPersist: fn(),
|
||||
},
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const defaultValueAsDisplayString = parseDateToString({
|
||||
date: new Date(args.defaultValue!),
|
||||
isDateTimeInput: false,
|
||||
userTimezone: undefined,
|
||||
});
|
||||
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const input = await canvas.findByPlaceholderText('mm/dd/yyyy');
|
||||
expect(input).toBeVisible();
|
||||
expect(input).toHaveDisplayValue(defaultValueAsDisplayString);
|
||||
|
||||
await userEvent.type(input, '{Backspace}{Backspace}');
|
||||
|
||||
const datePicker = await canvas.findByRole('dialog');
|
||||
expect(datePicker).toBeVisible();
|
||||
|
||||
await Promise.all([
|
||||
userEvent.click(canvasElement),
|
||||
|
||||
waitForElementToBeRemoved(datePicker),
|
||||
]);
|
||||
|
||||
expect(args.onPersist).not.toHaveBeenCalled();
|
||||
|
||||
expect(input).toHaveDisplayValue(defaultValueAsDisplayString.slice(0, -2));
|
||||
},
|
||||
};
|
@ -0,0 +1,397 @@
|
||||
import { MAX_DATE } from '@/ui/input/components/internal/date/constants/MaxDate';
|
||||
import { MIN_DATE } from '@/ui/input/components/internal/date/constants/MinDate';
|
||||
import { parseDateToString } from '@/ui/input/components/internal/date/utils/parseDateToString';
|
||||
import { expect } from '@storybook/jest';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import {
|
||||
fn,
|
||||
userEvent,
|
||||
waitFor,
|
||||
waitForElementToBeRemoved,
|
||||
within,
|
||||
} from '@storybook/test';
|
||||
import { DateTime } from 'luxon';
|
||||
import { FormDateTimeFieldInput } from '@/object-record/record-field/form-types/components/FormDateTimeFieldInput';
|
||||
|
||||
const meta: Meta<typeof FormDateTimeFieldInput> = {
|
||||
title: 'UI/Data/Field/Form/Input/FormDateTimeFieldInput',
|
||||
component: FormDateTimeFieldInput,
|
||||
args: {},
|
||||
argTypes: {},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof FormDateTimeFieldInput>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
label: 'Created At',
|
||||
defaultValue: '2024-12-09T13:20:19.631Z',
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await canvas.findByText('Created At');
|
||||
await canvas.findByDisplayValue(/12\/09\/2024 \d{2}:20/);
|
||||
},
|
||||
};
|
||||
|
||||
export const WithDefaultEmptyValue: Story = {
|
||||
args: {
|
||||
label: 'Created At',
|
||||
defaultValue: undefined,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await canvas.findByText('Created At');
|
||||
await canvas.findByDisplayValue('');
|
||||
await canvas.findByPlaceholderText('mm/dd/yyyy hh:mm');
|
||||
},
|
||||
};
|
||||
|
||||
export const SetsDateTimeWithInput: Story = {
|
||||
args: {
|
||||
label: 'Created At',
|
||||
defaultValue: undefined,
|
||||
onPersist: fn(),
|
||||
},
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const input = await canvas.findByPlaceholderText('mm/dd/yyyy hh:mm');
|
||||
|
||||
await userEvent.click(input);
|
||||
|
||||
const dialog = await canvas.findByRole('dialog');
|
||||
expect(dialog).toBeVisible();
|
||||
|
||||
await userEvent.type(input, '12/08/2024 12:10{enter}');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(args.onPersist).toHaveBeenCalledWith(
|
||||
expect.stringMatching(/2024-12-08T\d{2}:10:00.000Z/),
|
||||
);
|
||||
});
|
||||
|
||||
expect(dialog).toBeVisible();
|
||||
},
|
||||
};
|
||||
|
||||
export const DoesNotSetDateWithoutTime: Story = {
|
||||
args: {
|
||||
label: 'Created At',
|
||||
defaultValue: undefined,
|
||||
onPersist: fn(),
|
||||
},
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const input = await canvas.findByPlaceholderText('mm/dd/yyyy hh:mm');
|
||||
|
||||
await userEvent.click(input);
|
||||
|
||||
const dialog = await canvas.findByRole('dialog');
|
||||
expect(dialog).toBeVisible();
|
||||
|
||||
await userEvent.type(input, '12/08/2024{enter}');
|
||||
|
||||
expect(args.onPersist).not.toHaveBeenCalled();
|
||||
expect(dialog).toBeVisible();
|
||||
},
|
||||
};
|
||||
|
||||
export const SetsDateTimeWithDatePicker: Story = {
|
||||
args: {
|
||||
label: 'Created At',
|
||||
defaultValue: undefined,
|
||||
onPersist: fn(),
|
||||
},
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const input = await canvas.findByPlaceholderText('mm/dd/yyyy hh:mm');
|
||||
expect(input).toBeVisible();
|
||||
|
||||
await userEvent.click(input);
|
||||
|
||||
const datePicker = await canvas.findByRole('dialog');
|
||||
expect(datePicker).toBeVisible();
|
||||
|
||||
const dayToChoose = await within(datePicker).findByRole('option', {
|
||||
name: 'Choose Saturday, December 7th, 2024',
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
userEvent.click(dayToChoose),
|
||||
|
||||
waitForElementToBeRemoved(datePicker),
|
||||
waitFor(() => {
|
||||
expect(args.onPersist).toHaveBeenCalledWith(
|
||||
expect.stringMatching(/^2024-12-07/),
|
||||
);
|
||||
}),
|
||||
waitFor(() => {
|
||||
expect(
|
||||
canvas.getByDisplayValue(/12\/07\/2024 \d{2}:\d{2}/),
|
||||
).toBeVisible();
|
||||
}),
|
||||
]);
|
||||
},
|
||||
};
|
||||
|
||||
export const ResetsDateByClickingButton: Story = {
|
||||
args: {
|
||||
label: 'Created At',
|
||||
defaultValue: '2024-12-09T13:20:19.631Z',
|
||||
onPersist: fn(),
|
||||
},
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const input = await canvas.findByPlaceholderText('mm/dd/yyyy hh:mm');
|
||||
expect(input).toBeVisible();
|
||||
|
||||
await userEvent.click(input);
|
||||
|
||||
const datePicker = await canvas.findByRole('dialog');
|
||||
expect(datePicker).toBeVisible();
|
||||
|
||||
const clearButton = await canvas.findByText('Clear');
|
||||
|
||||
await Promise.all([
|
||||
userEvent.click(clearButton),
|
||||
|
||||
waitForElementToBeRemoved(datePicker),
|
||||
waitFor(() => {
|
||||
expect(args.onPersist).toHaveBeenCalledWith(null);
|
||||
}),
|
||||
waitFor(() => {
|
||||
expect(input).toHaveDisplayValue('');
|
||||
}),
|
||||
]);
|
||||
},
|
||||
};
|
||||
|
||||
export const ResetsDateByErasingInputContent: Story = {
|
||||
args: {
|
||||
label: 'Created At',
|
||||
defaultValue: '2024-12-09T13:20:19.631Z',
|
||||
onPersist: fn(),
|
||||
},
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const input = await canvas.findByPlaceholderText('mm/dd/yyyy hh:mm');
|
||||
expect(input).toBeVisible();
|
||||
|
||||
expect(input).toHaveDisplayValue(/12\/09\/2024 \d{2}:\d{2}/);
|
||||
|
||||
await userEvent.clear(input);
|
||||
|
||||
await Promise.all([
|
||||
userEvent.type(input, '{Enter}'),
|
||||
|
||||
waitForElementToBeRemoved(() => canvas.queryByRole('dialog')),
|
||||
waitFor(() => {
|
||||
expect(args.onPersist).toHaveBeenCalledWith(null);
|
||||
}),
|
||||
waitFor(() => {
|
||||
expect(input).toHaveDisplayValue('');
|
||||
}),
|
||||
]);
|
||||
},
|
||||
};
|
||||
|
||||
export const DefaultsToMinValueWhenTypingReallyOldDate: Story = {
|
||||
args: {
|
||||
label: 'Created At',
|
||||
defaultValue: undefined,
|
||||
onPersist: fn(),
|
||||
},
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const input = await canvas.findByPlaceholderText('mm/dd/yyyy hh:mm');
|
||||
expect(input).toBeVisible();
|
||||
|
||||
await userEvent.click(input);
|
||||
|
||||
const datePicker = await canvas.findByRole('dialog');
|
||||
expect(datePicker).toBeVisible();
|
||||
|
||||
await Promise.all([
|
||||
userEvent.type(input, '02/02/1500 10:10{Enter}'),
|
||||
|
||||
waitFor(() => {
|
||||
expect(args.onPersist).toHaveBeenCalledWith(MIN_DATE.toISOString());
|
||||
}),
|
||||
waitFor(() => {
|
||||
expect(input).toHaveDisplayValue(
|
||||
parseDateToString({
|
||||
date: MIN_DATE,
|
||||
isDateTimeInput: true,
|
||||
userTimezone: undefined,
|
||||
}),
|
||||
);
|
||||
}),
|
||||
waitFor(() => {
|
||||
const expectedDate = DateTime.fromJSDate(MIN_DATE)
|
||||
.toLocal()
|
||||
.set({
|
||||
day: MIN_DATE.getUTCDate(),
|
||||
month: MIN_DATE.getUTCMonth() + 1,
|
||||
year: MIN_DATE.getUTCFullYear(),
|
||||
hour: 0,
|
||||
minute: 0,
|
||||
second: 0,
|
||||
millisecond: 0,
|
||||
});
|
||||
|
||||
const selectedDay = within(datePicker).getByRole('option', {
|
||||
selected: true,
|
||||
name: (accessibleName) => {
|
||||
// The name looks like "Choose Sunday, December 31st, 1899"
|
||||
return accessibleName.includes(expectedDate.toFormat('yyyy'));
|
||||
},
|
||||
});
|
||||
expect(selectedDay).toBeVisible();
|
||||
}),
|
||||
]);
|
||||
},
|
||||
};
|
||||
|
||||
export const DefaultsToMaxValueWhenTypingReallyFarDate: Story = {
|
||||
args: {
|
||||
label: 'Created At',
|
||||
defaultValue: undefined,
|
||||
onPersist: fn(),
|
||||
},
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const input = await canvas.findByPlaceholderText('mm/dd/yyyy hh:mm');
|
||||
expect(input).toBeVisible();
|
||||
|
||||
await userEvent.click(input);
|
||||
|
||||
const datePicker = await canvas.findByRole('dialog');
|
||||
expect(datePicker).toBeVisible();
|
||||
|
||||
await Promise.all([
|
||||
userEvent.type(input, '02/02/2500 10:10{Enter}'),
|
||||
|
||||
waitFor(() => {
|
||||
expect(args.onPersist).toHaveBeenCalledWith(MAX_DATE.toISOString());
|
||||
}),
|
||||
waitFor(() => {
|
||||
expect(input).toHaveDisplayValue(
|
||||
parseDateToString({
|
||||
date: MAX_DATE,
|
||||
isDateTimeInput: true,
|
||||
userTimezone: undefined,
|
||||
}),
|
||||
);
|
||||
}),
|
||||
waitFor(() => {
|
||||
const expectedDate = DateTime.fromJSDate(MAX_DATE)
|
||||
.toLocal()
|
||||
.set({
|
||||
day: MAX_DATE.getUTCDate(),
|
||||
month: MAX_DATE.getUTCMonth() + 1,
|
||||
year: MAX_DATE.getUTCFullYear(),
|
||||
hour: 0,
|
||||
minute: 0,
|
||||
second: 0,
|
||||
millisecond: 0,
|
||||
});
|
||||
|
||||
const selectedDay = within(datePicker).getByRole('option', {
|
||||
selected: true,
|
||||
name: (accessibleName) => {
|
||||
// The name looks like "Choose Thursday, December 30th, 2100"
|
||||
return accessibleName.includes(expectedDate.toFormat('yyyy'));
|
||||
},
|
||||
});
|
||||
expect(selectedDay).toBeVisible();
|
||||
}),
|
||||
]);
|
||||
},
|
||||
};
|
||||
|
||||
export const SwitchesToStandaloneVariable: Story = {
|
||||
args: {
|
||||
label: 'Created At',
|
||||
defaultValue: undefined,
|
||||
onPersist: fn(),
|
||||
VariablePicker: ({ onVariableSelect }) => {
|
||||
return (
|
||||
<button
|
||||
onClick={() => {
|
||||
onVariableSelect('{{test}}');
|
||||
}}
|
||||
>
|
||||
Add variable
|
||||
</button>
|
||||
);
|
||||
},
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const addVariableButton = await canvas.findByText('Add variable');
|
||||
await userEvent.click(addVariableButton);
|
||||
|
||||
const variableTag = await canvas.findByText('test');
|
||||
expect(variableTag).toBeVisible();
|
||||
|
||||
const removeVariableButton = canvas.getByTestId(/^remove-icon/);
|
||||
|
||||
await Promise.all([
|
||||
userEvent.click(removeVariableButton),
|
||||
|
||||
waitForElementToBeRemoved(variableTag),
|
||||
waitFor(() => {
|
||||
const input = canvas.getByPlaceholderText('mm/dd/yyyy hh:mm');
|
||||
expect(input).toBeVisible();
|
||||
}),
|
||||
]);
|
||||
},
|
||||
};
|
||||
|
||||
export const ClickingOutsideDoesNotResetInputState: Story = {
|
||||
args: {
|
||||
label: 'Created At',
|
||||
defaultValue: '2024-12-09T13:20:19.631Z',
|
||||
onPersist: fn(),
|
||||
},
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const defaultValueAsDisplayString = parseDateToString({
|
||||
date: new Date(args.defaultValue!),
|
||||
isDateTimeInput: true,
|
||||
userTimezone: undefined,
|
||||
});
|
||||
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const input = await canvas.findByPlaceholderText('mm/dd/yyyy hh:mm');
|
||||
expect(input).toBeVisible();
|
||||
expect(input).toHaveDisplayValue(defaultValueAsDisplayString);
|
||||
|
||||
await userEvent.type(input, '{Backspace}{Backspace}');
|
||||
|
||||
const datePicker = await canvas.findByRole('dialog');
|
||||
expect(datePicker).toBeVisible();
|
||||
|
||||
await Promise.all([
|
||||
userEvent.click(canvasElement),
|
||||
|
||||
waitForElementToBeRemoved(datePicker),
|
||||
]);
|
||||
|
||||
expect(args.onPersist).not.toHaveBeenCalled();
|
||||
|
||||
expect(input).toHaveDisplayValue(defaultValueAsDisplayString.slice(0, -2));
|
||||
},
|
||||
};
|
@ -1,765 +0,0 @@
|
||||
import { MAX_DATE } from '@/ui/input/components/internal/date/constants/MaxDate';
|
||||
import { MIN_DATE } from '@/ui/input/components/internal/date/constants/MinDate';
|
||||
import { parseDateToString } from '@/ui/input/components/internal/date/utils/parseDateToString';
|
||||
import { expect } from '@storybook/jest';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import {
|
||||
fn,
|
||||
userEvent,
|
||||
waitFor,
|
||||
waitForElementToBeRemoved,
|
||||
within,
|
||||
} from '@storybook/test';
|
||||
import { DateTime } from 'luxon';
|
||||
import { FormDateTimeFieldInputBase } from '../FormDateTimeFieldInputBase';
|
||||
|
||||
const meta: Meta<typeof FormDateTimeFieldInputBase> = {
|
||||
title: 'UI/Data/Field/Form/Input/FormDateTimeFieldInputBase',
|
||||
component: FormDateTimeFieldInputBase,
|
||||
args: {},
|
||||
argTypes: {},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof FormDateTimeFieldInputBase>;
|
||||
|
||||
export const DateDefault: Story = {
|
||||
args: {
|
||||
mode: 'date',
|
||||
label: 'Created At',
|
||||
defaultValue: '2024-12-09T13:20:19.631Z',
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await canvas.findByText('Created At');
|
||||
await canvas.findByDisplayValue('12/09/2024');
|
||||
},
|
||||
};
|
||||
|
||||
export const DateWithDefaultEmptyValue: Story = {
|
||||
args: {
|
||||
mode: 'date',
|
||||
label: 'Created At',
|
||||
defaultValue: undefined,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await canvas.findByText('Created At');
|
||||
await canvas.findByDisplayValue('');
|
||||
await canvas.findByPlaceholderText('mm/dd/yyyy');
|
||||
},
|
||||
};
|
||||
|
||||
export const DateSetsDateWithInput: Story = {
|
||||
args: {
|
||||
mode: 'date',
|
||||
label: 'Created At',
|
||||
defaultValue: undefined,
|
||||
onPersist: fn(),
|
||||
},
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const input = await canvas.findByPlaceholderText('mm/dd/yyyy');
|
||||
|
||||
await userEvent.click(input);
|
||||
|
||||
const dialog = await canvas.findByRole('dialog');
|
||||
expect(dialog).toBeVisible();
|
||||
|
||||
await userEvent.type(input, '12/08/2024{enter}');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(args.onPersist).toHaveBeenCalledWith('2024-12-08T00:00:00.000Z');
|
||||
});
|
||||
|
||||
expect(dialog).toBeVisible();
|
||||
},
|
||||
};
|
||||
|
||||
export const DateSetsDateWithDatePicker: Story = {
|
||||
args: {
|
||||
mode: 'date',
|
||||
label: 'Created At',
|
||||
defaultValue: undefined,
|
||||
onPersist: fn(),
|
||||
},
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const input = await canvas.findByPlaceholderText('mm/dd/yyyy');
|
||||
expect(input).toBeVisible();
|
||||
|
||||
await userEvent.click(input);
|
||||
|
||||
const datePicker = await canvas.findByRole('dialog');
|
||||
expect(datePicker).toBeVisible();
|
||||
|
||||
const dayToChoose = await within(datePicker).findByRole('option', {
|
||||
name: 'Choose Saturday, December 7th, 2024',
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
userEvent.click(dayToChoose),
|
||||
|
||||
waitForElementToBeRemoved(datePicker),
|
||||
waitFor(() => {
|
||||
expect(args.onPersist).toHaveBeenCalledWith(
|
||||
expect.stringMatching(/^2024-12-07/),
|
||||
);
|
||||
}),
|
||||
waitFor(() => {
|
||||
expect(canvas.getByDisplayValue('12/07/2024')).toBeVisible();
|
||||
}),
|
||||
]);
|
||||
},
|
||||
};
|
||||
|
||||
export const DateResetsDateByClickingButton: Story = {
|
||||
args: {
|
||||
mode: 'date',
|
||||
label: 'Created At',
|
||||
defaultValue: '2024-12-09T13:20:19.631Z',
|
||||
onPersist: fn(),
|
||||
},
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const input = await canvas.findByPlaceholderText('mm/dd/yyyy');
|
||||
expect(input).toBeVisible();
|
||||
|
||||
await userEvent.click(input);
|
||||
|
||||
const datePicker = await canvas.findByRole('dialog');
|
||||
expect(datePicker).toBeVisible();
|
||||
|
||||
const clearButton = await canvas.findByText('Clear');
|
||||
|
||||
await Promise.all([
|
||||
userEvent.click(clearButton),
|
||||
|
||||
waitForElementToBeRemoved(datePicker),
|
||||
waitFor(() => {
|
||||
expect(args.onPersist).toHaveBeenCalledWith(null);
|
||||
}),
|
||||
waitFor(() => {
|
||||
expect(input).toHaveDisplayValue('');
|
||||
}),
|
||||
]);
|
||||
},
|
||||
};
|
||||
|
||||
export const DateResetsDateByErasingInputContent: Story = {
|
||||
args: {
|
||||
mode: 'date',
|
||||
label: 'Created At',
|
||||
defaultValue: '2024-12-09T13:20:19.631Z',
|
||||
onPersist: fn(),
|
||||
},
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const input = await canvas.findByPlaceholderText('mm/dd/yyyy');
|
||||
expect(input).toBeVisible();
|
||||
|
||||
expect(input).toHaveDisplayValue('12/09/2024');
|
||||
|
||||
await userEvent.clear(input);
|
||||
|
||||
await Promise.all([
|
||||
userEvent.type(input, '{Enter}'),
|
||||
|
||||
waitForElementToBeRemoved(() => canvas.queryByRole('dialog')),
|
||||
waitFor(() => {
|
||||
expect(args.onPersist).toHaveBeenCalledWith(null);
|
||||
}),
|
||||
waitFor(() => {
|
||||
expect(input).toHaveDisplayValue('');
|
||||
}),
|
||||
]);
|
||||
},
|
||||
};
|
||||
|
||||
export const DateDefaultsToMinValueWhenTypingReallyOldDate: Story = {
|
||||
args: {
|
||||
mode: 'date',
|
||||
label: 'Created At',
|
||||
defaultValue: undefined,
|
||||
onPersist: fn(),
|
||||
},
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const input = await canvas.findByPlaceholderText('mm/dd/yyyy');
|
||||
expect(input).toBeVisible();
|
||||
|
||||
await userEvent.click(input);
|
||||
|
||||
const datePicker = await canvas.findByRole('dialog');
|
||||
expect(datePicker).toBeVisible();
|
||||
|
||||
await Promise.all([
|
||||
userEvent.type(input, '02/02/1500{Enter}'),
|
||||
|
||||
waitFor(() => {
|
||||
expect(args.onPersist).toHaveBeenCalledWith(MIN_DATE.toISOString());
|
||||
}),
|
||||
waitFor(() => {
|
||||
expect(input).toHaveDisplayValue(
|
||||
parseDateToString({
|
||||
date: MIN_DATE,
|
||||
isDateTimeInput: false,
|
||||
userTimezone: undefined,
|
||||
}),
|
||||
);
|
||||
}),
|
||||
waitFor(() => {
|
||||
const expectedDate = DateTime.fromJSDate(MIN_DATE)
|
||||
.toLocal()
|
||||
.set({
|
||||
day: MIN_DATE.getUTCDate(),
|
||||
month: MIN_DATE.getUTCMonth() + 1,
|
||||
year: MIN_DATE.getUTCFullYear(),
|
||||
hour: 0,
|
||||
minute: 0,
|
||||
second: 0,
|
||||
millisecond: 0,
|
||||
});
|
||||
|
||||
const selectedDay = within(datePicker).getByRole('option', {
|
||||
selected: true,
|
||||
name: (accessibleName) => {
|
||||
// The name looks like "Choose Sunday, December 31st, 1899"
|
||||
return accessibleName.includes(expectedDate.toFormat('yyyy'));
|
||||
},
|
||||
});
|
||||
expect(selectedDay).toBeVisible();
|
||||
}),
|
||||
]);
|
||||
},
|
||||
};
|
||||
|
||||
export const DateDefaultsToMaxValueWhenTypingReallyFarDate: Story = {
|
||||
args: {
|
||||
mode: 'date',
|
||||
label: 'Created At',
|
||||
defaultValue: undefined,
|
||||
onPersist: fn(),
|
||||
},
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const input = await canvas.findByPlaceholderText('mm/dd/yyyy');
|
||||
expect(input).toBeVisible();
|
||||
|
||||
await userEvent.click(input);
|
||||
|
||||
const datePicker = await canvas.findByRole('dialog');
|
||||
expect(datePicker).toBeVisible();
|
||||
|
||||
await Promise.all([
|
||||
userEvent.type(input, '02/02/2500{Enter}'),
|
||||
|
||||
waitFor(() => {
|
||||
expect(args.onPersist).toHaveBeenCalledWith(MAX_DATE.toISOString());
|
||||
}),
|
||||
waitFor(() => {
|
||||
expect(input).toHaveDisplayValue(
|
||||
parseDateToString({
|
||||
date: MAX_DATE,
|
||||
isDateTimeInput: false,
|
||||
userTimezone: undefined,
|
||||
}),
|
||||
);
|
||||
}),
|
||||
waitFor(() => {
|
||||
const expectedDate = DateTime.fromJSDate(MAX_DATE)
|
||||
.toLocal()
|
||||
.set({
|
||||
day: MAX_DATE.getUTCDate(),
|
||||
month: MAX_DATE.getUTCMonth() + 1,
|
||||
year: MAX_DATE.getUTCFullYear(),
|
||||
hour: 0,
|
||||
minute: 0,
|
||||
second: 0,
|
||||
millisecond: 0,
|
||||
});
|
||||
|
||||
const selectedDay = within(datePicker).getByRole('option', {
|
||||
selected: true,
|
||||
name: (accessibleName) => {
|
||||
// The name looks like "Choose Thursday, December 30th, 2100"
|
||||
return accessibleName.includes(expectedDate.toFormat('yyyy'));
|
||||
},
|
||||
});
|
||||
expect(selectedDay).toBeVisible();
|
||||
}),
|
||||
]);
|
||||
},
|
||||
};
|
||||
|
||||
export const DateSwitchesToStandaloneVariable: Story = {
|
||||
args: {
|
||||
mode: 'date',
|
||||
label: 'Created At',
|
||||
defaultValue: undefined,
|
||||
onPersist: fn(),
|
||||
VariablePicker: ({ onVariableSelect }) => {
|
||||
return (
|
||||
<button
|
||||
onClick={() => {
|
||||
onVariableSelect('{{test}}');
|
||||
}}
|
||||
>
|
||||
Add variable
|
||||
</button>
|
||||
);
|
||||
},
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const addVariableButton = await canvas.findByText('Add variable');
|
||||
await userEvent.click(addVariableButton);
|
||||
|
||||
const variableTag = await canvas.findByText('test');
|
||||
expect(variableTag).toBeVisible();
|
||||
|
||||
const removeVariableButton = canvas.getByTestId(/^remove-icon/);
|
||||
|
||||
await Promise.all([
|
||||
userEvent.click(removeVariableButton),
|
||||
|
||||
waitForElementToBeRemoved(variableTag),
|
||||
waitFor(() => {
|
||||
const input = canvas.getByPlaceholderText('mm/dd/yyyy');
|
||||
expect(input).toBeVisible();
|
||||
}),
|
||||
]);
|
||||
},
|
||||
};
|
||||
|
||||
export const DateClickingOutsideDoesNotResetInputState: Story = {
|
||||
args: {
|
||||
mode: 'date',
|
||||
label: 'Created At',
|
||||
defaultValue: '2024-12-09T13:20:19.631Z',
|
||||
onPersist: fn(),
|
||||
},
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const defaultValueAsDisplayString = parseDateToString({
|
||||
date: new Date(args.defaultValue!),
|
||||
isDateTimeInput: false,
|
||||
userTimezone: undefined,
|
||||
});
|
||||
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const input = await canvas.findByPlaceholderText('mm/dd/yyyy');
|
||||
expect(input).toBeVisible();
|
||||
expect(input).toHaveDisplayValue(defaultValueAsDisplayString);
|
||||
|
||||
await userEvent.type(input, '{Backspace}{Backspace}');
|
||||
|
||||
const datePicker = await canvas.findByRole('dialog');
|
||||
expect(datePicker).toBeVisible();
|
||||
|
||||
await Promise.all([
|
||||
userEvent.click(canvasElement),
|
||||
|
||||
waitForElementToBeRemoved(datePicker),
|
||||
]);
|
||||
|
||||
expect(args.onPersist).not.toHaveBeenCalled();
|
||||
|
||||
expect(input).toHaveDisplayValue(defaultValueAsDisplayString.slice(0, -2));
|
||||
},
|
||||
};
|
||||
|
||||
// ----
|
||||
|
||||
export const DateTimeDefault: Story = {
|
||||
args: {
|
||||
mode: 'datetime',
|
||||
label: 'Created At',
|
||||
defaultValue: '2024-12-09T13:20:19.631Z',
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await canvas.findByText('Created At');
|
||||
await canvas.findByDisplayValue(/12\/09\/2024 \d{2}:20/);
|
||||
},
|
||||
};
|
||||
|
||||
export const DateTimeWithDefaultEmptyValue: Story = {
|
||||
args: {
|
||||
mode: 'datetime',
|
||||
label: 'Created At',
|
||||
defaultValue: undefined,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await canvas.findByText('Created At');
|
||||
await canvas.findByDisplayValue('');
|
||||
await canvas.findByPlaceholderText('mm/dd/yyyy hh:mm');
|
||||
},
|
||||
};
|
||||
|
||||
export const DateTimeSetsDateTimeWithInput: Story = {
|
||||
args: {
|
||||
mode: 'datetime',
|
||||
label: 'Created At',
|
||||
defaultValue: undefined,
|
||||
onPersist: fn(),
|
||||
},
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const input = await canvas.findByPlaceholderText('mm/dd/yyyy hh:mm');
|
||||
|
||||
await userEvent.click(input);
|
||||
|
||||
const dialog = await canvas.findByRole('dialog');
|
||||
expect(dialog).toBeVisible();
|
||||
|
||||
await userEvent.type(input, '12/08/2024 12:10{enter}');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(args.onPersist).toHaveBeenCalledWith(
|
||||
expect.stringMatching(/2024-12-08T\d{2}:10:00.000Z/),
|
||||
);
|
||||
});
|
||||
|
||||
expect(dialog).toBeVisible();
|
||||
},
|
||||
};
|
||||
|
||||
export const DateTimeDoesNotSetDateWithoutTime: Story = {
|
||||
args: {
|
||||
mode: 'datetime',
|
||||
label: 'Created At',
|
||||
defaultValue: undefined,
|
||||
onPersist: fn(),
|
||||
},
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const input = await canvas.findByPlaceholderText('mm/dd/yyyy hh:mm');
|
||||
|
||||
await userEvent.click(input);
|
||||
|
||||
const dialog = await canvas.findByRole('dialog');
|
||||
expect(dialog).toBeVisible();
|
||||
|
||||
await userEvent.type(input, '12/08/2024{enter}');
|
||||
|
||||
expect(args.onPersist).not.toHaveBeenCalled();
|
||||
expect(dialog).toBeVisible();
|
||||
},
|
||||
};
|
||||
|
||||
export const DateTimeSetsDateTimeWithDatePicker: Story = {
|
||||
args: {
|
||||
mode: 'datetime',
|
||||
label: 'Created At',
|
||||
defaultValue: undefined,
|
||||
onPersist: fn(),
|
||||
},
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const input = await canvas.findByPlaceholderText('mm/dd/yyyy hh:mm');
|
||||
expect(input).toBeVisible();
|
||||
|
||||
await userEvent.click(input);
|
||||
|
||||
const datePicker = await canvas.findByRole('dialog');
|
||||
expect(datePicker).toBeVisible();
|
||||
|
||||
const dayToChoose = await within(datePicker).findByRole('option', {
|
||||
name: 'Choose Saturday, December 7th, 2024',
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
userEvent.click(dayToChoose),
|
||||
|
||||
waitForElementToBeRemoved(datePicker),
|
||||
waitFor(() => {
|
||||
expect(args.onPersist).toHaveBeenCalledWith(
|
||||
expect.stringMatching(/^2024-12-07/),
|
||||
);
|
||||
}),
|
||||
waitFor(() => {
|
||||
expect(
|
||||
canvas.getByDisplayValue(/12\/07\/2024 \d{2}:\d{2}/),
|
||||
).toBeVisible();
|
||||
}),
|
||||
]);
|
||||
},
|
||||
};
|
||||
|
||||
export const DateTimeResetsDateByClickingButton: Story = {
|
||||
args: {
|
||||
mode: 'datetime',
|
||||
label: 'Created At',
|
||||
defaultValue: '2024-12-09T13:20:19.631Z',
|
||||
onPersist: fn(),
|
||||
},
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const input = await canvas.findByPlaceholderText('mm/dd/yyyy hh:mm');
|
||||
expect(input).toBeVisible();
|
||||
|
||||
await userEvent.click(input);
|
||||
|
||||
const datePicker = await canvas.findByRole('dialog');
|
||||
expect(datePicker).toBeVisible();
|
||||
|
||||
const clearButton = await canvas.findByText('Clear');
|
||||
|
||||
await Promise.all([
|
||||
userEvent.click(clearButton),
|
||||
|
||||
waitForElementToBeRemoved(datePicker),
|
||||
waitFor(() => {
|
||||
expect(args.onPersist).toHaveBeenCalledWith(null);
|
||||
}),
|
||||
waitFor(() => {
|
||||
expect(input).toHaveDisplayValue('');
|
||||
}),
|
||||
]);
|
||||
},
|
||||
};
|
||||
|
||||
export const DateTimeResetsDateByErasingInputContent: Story = {
|
||||
args: {
|
||||
mode: 'datetime',
|
||||
label: 'Created At',
|
||||
defaultValue: '2024-12-09T13:20:19.631Z',
|
||||
onPersist: fn(),
|
||||
},
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const input = await canvas.findByPlaceholderText('mm/dd/yyyy hh:mm');
|
||||
expect(input).toBeVisible();
|
||||
|
||||
expect(input).toHaveDisplayValue(/12\/09\/2024 \d{2}:\d{2}/);
|
||||
|
||||
await userEvent.clear(input);
|
||||
|
||||
await Promise.all([
|
||||
userEvent.type(input, '{Enter}'),
|
||||
|
||||
waitForElementToBeRemoved(() => canvas.queryByRole('dialog')),
|
||||
waitFor(() => {
|
||||
expect(args.onPersist).toHaveBeenCalledWith(null);
|
||||
}),
|
||||
waitFor(() => {
|
||||
expect(input).toHaveDisplayValue('');
|
||||
}),
|
||||
]);
|
||||
},
|
||||
};
|
||||
|
||||
export const DateTimeDefaultsToMinValueWhenTypingReallyOldDate: Story = {
|
||||
args: {
|
||||
mode: 'datetime',
|
||||
label: 'Created At',
|
||||
defaultValue: undefined,
|
||||
onPersist: fn(),
|
||||
},
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const input = await canvas.findByPlaceholderText('mm/dd/yyyy hh:mm');
|
||||
expect(input).toBeVisible();
|
||||
|
||||
await userEvent.click(input);
|
||||
|
||||
const datePicker = await canvas.findByRole('dialog');
|
||||
expect(datePicker).toBeVisible();
|
||||
|
||||
await Promise.all([
|
||||
userEvent.type(input, '02/02/1500 10:10{Enter}'),
|
||||
|
||||
waitFor(() => {
|
||||
expect(args.onPersist).toHaveBeenCalledWith(MIN_DATE.toISOString());
|
||||
}),
|
||||
waitFor(() => {
|
||||
expect(input).toHaveDisplayValue(
|
||||
parseDateToString({
|
||||
date: MIN_DATE,
|
||||
isDateTimeInput: true,
|
||||
userTimezone: undefined,
|
||||
}),
|
||||
);
|
||||
}),
|
||||
waitFor(() => {
|
||||
const expectedDate = DateTime.fromJSDate(MIN_DATE)
|
||||
.toLocal()
|
||||
.set({
|
||||
day: MIN_DATE.getUTCDate(),
|
||||
month: MIN_DATE.getUTCMonth() + 1,
|
||||
year: MIN_DATE.getUTCFullYear(),
|
||||
hour: 0,
|
||||
minute: 0,
|
||||
second: 0,
|
||||
millisecond: 0,
|
||||
});
|
||||
|
||||
const selectedDay = within(datePicker).getByRole('option', {
|
||||
selected: true,
|
||||
name: (accessibleName) => {
|
||||
// The name looks like "Choose Sunday, December 31st, 1899"
|
||||
return accessibleName.includes(expectedDate.toFormat('yyyy'));
|
||||
},
|
||||
});
|
||||
expect(selectedDay).toBeVisible();
|
||||
}),
|
||||
]);
|
||||
},
|
||||
};
|
||||
|
||||
export const DateTimeDefaultsToMaxValueWhenTypingReallyFarDate: Story = {
|
||||
args: {
|
||||
mode: 'datetime',
|
||||
label: 'Created At',
|
||||
defaultValue: undefined,
|
||||
onPersist: fn(),
|
||||
},
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const input = await canvas.findByPlaceholderText('mm/dd/yyyy hh:mm');
|
||||
expect(input).toBeVisible();
|
||||
|
||||
await userEvent.click(input);
|
||||
|
||||
const datePicker = await canvas.findByRole('dialog');
|
||||
expect(datePicker).toBeVisible();
|
||||
|
||||
await Promise.all([
|
||||
userEvent.type(input, '02/02/2500 10:10{Enter}'),
|
||||
|
||||
waitFor(() => {
|
||||
expect(args.onPersist).toHaveBeenCalledWith(MAX_DATE.toISOString());
|
||||
}),
|
||||
waitFor(() => {
|
||||
expect(input).toHaveDisplayValue(
|
||||
parseDateToString({
|
||||
date: MAX_DATE,
|
||||
isDateTimeInput: true,
|
||||
userTimezone: undefined,
|
||||
}),
|
||||
);
|
||||
}),
|
||||
waitFor(() => {
|
||||
const expectedDate = DateTime.fromJSDate(MAX_DATE)
|
||||
.toLocal()
|
||||
.set({
|
||||
day: MAX_DATE.getUTCDate(),
|
||||
month: MAX_DATE.getUTCMonth() + 1,
|
||||
year: MAX_DATE.getUTCFullYear(),
|
||||
hour: 0,
|
||||
minute: 0,
|
||||
second: 0,
|
||||
millisecond: 0,
|
||||
});
|
||||
|
||||
const selectedDay = within(datePicker).getByRole('option', {
|
||||
selected: true,
|
||||
name: (accessibleName) => {
|
||||
// The name looks like "Choose Thursday, December 30th, 2100"
|
||||
return accessibleName.includes(expectedDate.toFormat('yyyy'));
|
||||
},
|
||||
});
|
||||
expect(selectedDay).toBeVisible();
|
||||
}),
|
||||
]);
|
||||
},
|
||||
};
|
||||
|
||||
export const DateTimeSwitchesToStandaloneVariable: Story = {
|
||||
args: {
|
||||
mode: 'datetime',
|
||||
label: 'Created At',
|
||||
defaultValue: undefined,
|
||||
onPersist: fn(),
|
||||
VariablePicker: ({ onVariableSelect }) => {
|
||||
return (
|
||||
<button
|
||||
onClick={() => {
|
||||
onVariableSelect('{{test}}');
|
||||
}}
|
||||
>
|
||||
Add variable
|
||||
</button>
|
||||
);
|
||||
},
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const addVariableButton = await canvas.findByText('Add variable');
|
||||
await userEvent.click(addVariableButton);
|
||||
|
||||
const variableTag = await canvas.findByText('test');
|
||||
expect(variableTag).toBeVisible();
|
||||
|
||||
const removeVariableButton = canvas.getByTestId(/^remove-icon/);
|
||||
|
||||
await Promise.all([
|
||||
userEvent.click(removeVariableButton),
|
||||
|
||||
waitForElementToBeRemoved(variableTag),
|
||||
waitFor(() => {
|
||||
const input = canvas.getByPlaceholderText('mm/dd/yyyy hh:mm');
|
||||
expect(input).toBeVisible();
|
||||
}),
|
||||
]);
|
||||
},
|
||||
};
|
||||
|
||||
export const DateTimeClickingOutsideDoesNotResetInputState: Story = {
|
||||
args: {
|
||||
mode: 'datetime',
|
||||
label: 'Created At',
|
||||
defaultValue: '2024-12-09T13:20:19.631Z',
|
||||
onPersist: fn(),
|
||||
},
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const defaultValueAsDisplayString = parseDateToString({
|
||||
date: new Date(args.defaultValue!),
|
||||
isDateTimeInput: true,
|
||||
userTimezone: undefined,
|
||||
});
|
||||
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const input = await canvas.findByPlaceholderText('mm/dd/yyyy hh:mm');
|
||||
expect(input).toBeVisible();
|
||||
expect(input).toHaveDisplayValue(defaultValueAsDisplayString);
|
||||
|
||||
await userEvent.type(input, '{Backspace}{Backspace}');
|
||||
|
||||
const datePicker = await canvas.findByRole('dialog');
|
||||
expect(datePicker).toBeVisible();
|
||||
|
||||
await Promise.all([
|
||||
userEvent.click(canvasElement),
|
||||
|
||||
waitForElementToBeRemoved(datePicker),
|
||||
]);
|
||||
|
||||
expect(args.onPersist).not.toHaveBeenCalled();
|
||||
|
||||
expect(input).toHaveDisplayValue(defaultValueAsDisplayString.slice(0, -2));
|
||||
},
|
||||
};
|
@ -39,7 +39,8 @@ const mocks: MockedResponse[] = [
|
||||
input: {
|
||||
phones: {
|
||||
primaryPhoneNumber: '123 456',
|
||||
primaryPhoneCountryCode: '+1',
|
||||
primaryPhoneCountryCode: 'US',
|
||||
primaryPhoneCallingCode: '+1',
|
||||
additionalPhones: [],
|
||||
},
|
||||
},
|
||||
@ -134,7 +135,8 @@ describe('usePersistField', () => {
|
||||
act(() => {
|
||||
result.current.persistField({
|
||||
primaryPhoneNumber: '123 456',
|
||||
primaryPhoneCountryCode: '+1',
|
||||
primaryPhoneCountryCode: 'US',
|
||||
primaryPhoneCallingCode: '+1',
|
||||
additionalPhones: [],
|
||||
});
|
||||
});
|
||||
|
@ -208,6 +208,7 @@ const mocks: MockedResponse[] = [
|
||||
phones {
|
||||
primaryPhoneNumber
|
||||
primaryPhoneCountryCode
|
||||
primaryPhoneCallingCode
|
||||
additionalPhones
|
||||
}
|
||||
position
|
||||
@ -215,6 +216,7 @@ const mocks: MockedResponse[] = [
|
||||
whatsapp {
|
||||
primaryPhoneNumber
|
||||
primaryPhoneCountryCode
|
||||
primaryPhoneCallingCode
|
||||
additionalPhones
|
||||
}
|
||||
workPreference
|
||||
|
@ -9,12 +9,11 @@ import { TEXT_INPUT_STYLE } from 'twenty-ui';
|
||||
import { MultiItemFieldInput } from './MultiItemFieldInput';
|
||||
|
||||
import { createPhonesFromFieldValue } from '@/object-record/record-field/meta-types/input/utils/phonesUtils';
|
||||
import { useCountries } from '@/ui/input/components/internal/hooks/useCountries';
|
||||
import { PhoneCountryPickerDropdownButton } from '@/ui/input/components/internal/phone/components/PhoneCountryPickerDropdownButton';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { stripSimpleQuotesFromString } from '~/utils/string/stripSimpleQuotesFromString';
|
||||
|
||||
export const DEFAULT_PHONE_COUNTRY_CODE = '1';
|
||||
export const DEFAULT_PHONE_CALLING_CODE = '1';
|
||||
|
||||
const StyledCustomPhoneInput = styled(ReactPhoneNumberInput)`
|
||||
font-family: ${({ theme }) => theme.font.family};
|
||||
@ -60,22 +59,22 @@ export const PhonesFieldInput = ({
|
||||
|
||||
const phones = createPhonesFromFieldValue(fieldValue);
|
||||
|
||||
const defaultCallingCode =
|
||||
stripSimpleQuotesFromString(
|
||||
fieldDefinition?.defaultValue?.primaryPhoneCountryCode,
|
||||
) ?? DEFAULT_PHONE_COUNTRY_CODE;
|
||||
// TODO : improve once we store the real country code
|
||||
const defaultCountry = useCountries().find(
|
||||
(obj) => `+${obj.callingCode}` === defaultCallingCode,
|
||||
)?.countryCode;
|
||||
const defaultCountry = stripSimpleQuotesFromString(
|
||||
fieldDefinition?.defaultValue?.primaryPhoneCountryCode,
|
||||
);
|
||||
|
||||
const handlePersistPhones = (
|
||||
updatedPhones: { number: string; callingCode: string }[],
|
||||
updatedPhones: {
|
||||
number: string;
|
||||
countryCode: string;
|
||||
callingCode: string;
|
||||
}[],
|
||||
) => {
|
||||
const [nextPrimaryPhone, ...nextAdditionalPhones] = updatedPhones;
|
||||
persistPhonesField({
|
||||
primaryPhoneNumber: nextPrimaryPhone?.number ?? '',
|
||||
primaryPhoneCountryCode: nextPrimaryPhone?.callingCode ?? '',
|
||||
primaryPhoneCountryCode: nextPrimaryPhone?.countryCode ?? '',
|
||||
primaryPhoneCallingCode: nextPrimaryPhone?.callingCode ?? '',
|
||||
additionalPhones: nextAdditionalPhones,
|
||||
});
|
||||
};
|
||||
@ -96,11 +95,13 @@ export const PhonesFieldInput = ({
|
||||
return {
|
||||
number: phone.nationalNumber,
|
||||
callingCode: `+${phone.countryCallingCode}`,
|
||||
countryCode: phone.country as string,
|
||||
};
|
||||
}
|
||||
return {
|
||||
number: '',
|
||||
callingCode: '',
|
||||
countryCode: '',
|
||||
};
|
||||
}}
|
||||
renderItem={({
|
||||
|
@ -19,7 +19,8 @@ describe('createPhonesFromFieldValue test suite', () => {
|
||||
it('should return an array with primary phone number if it is defined', () => {
|
||||
const fieldValue: FieldPhonesValue = {
|
||||
primaryPhoneNumber: '123456789',
|
||||
primaryPhoneCountryCode: '+1',
|
||||
primaryPhoneCountryCode: 'US',
|
||||
primaryPhoneCallingCode: '+1',
|
||||
additionalPhones: [],
|
||||
};
|
||||
const result = createPhonesFromFieldValue(fieldValue);
|
||||
@ -27,6 +28,24 @@ describe('createPhonesFromFieldValue test suite', () => {
|
||||
{
|
||||
number: '123456789',
|
||||
callingCode: '+1',
|
||||
countryCode: 'US',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return an array with primary phone number if it is defined, even with incorrect callingCode', () => {
|
||||
const fieldValue: FieldPhonesValue = {
|
||||
primaryPhoneNumber: '123456789',
|
||||
primaryPhoneCountryCode: 'US',
|
||||
primaryPhoneCallingCode: '+33',
|
||||
additionalPhones: [],
|
||||
};
|
||||
const result = createPhonesFromFieldValue(fieldValue);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
number: '123456789',
|
||||
callingCode: '+33',
|
||||
countryCode: 'US',
|
||||
},
|
||||
]);
|
||||
});
|
||||
@ -34,10 +53,11 @@ describe('createPhonesFromFieldValue test suite', () => {
|
||||
it('should return an array with both primary and additional phones if they are defined', () => {
|
||||
const fieldValue: FieldPhonesValue = {
|
||||
primaryPhoneNumber: '123456789',
|
||||
primaryPhoneCountryCode: '+1',
|
||||
primaryPhoneCountryCode: 'US',
|
||||
primaryPhoneCallingCode: '+1',
|
||||
additionalPhones: [
|
||||
{ number: '987654321', callingCode: '+44' },
|
||||
{ number: '555555555', callingCode: '+33' },
|
||||
{ number: '987654321', callingCode: '+44', countryCode: 'GB' },
|
||||
{ number: '555555555', callingCode: '+33', countryCode: 'FR' },
|
||||
],
|
||||
};
|
||||
const result = createPhonesFromFieldValue(fieldValue);
|
||||
@ -45,9 +65,10 @@ describe('createPhonesFromFieldValue test suite', () => {
|
||||
{
|
||||
number: '123456789',
|
||||
callingCode: '+1',
|
||||
countryCode: 'US',
|
||||
},
|
||||
{ number: '987654321', callingCode: '+44' },
|
||||
{ number: '555555555', callingCode: '+33' },
|
||||
{ number: '987654321', callingCode: '+44', countryCode: 'GB' },
|
||||
{ number: '555555555', callingCode: '+33', countryCode: 'FR' },
|
||||
]);
|
||||
});
|
||||
|
||||
@ -56,14 +77,14 @@ describe('createPhonesFromFieldValue test suite', () => {
|
||||
primaryPhoneNumber: '',
|
||||
primaryPhoneCountryCode: '',
|
||||
additionalPhones: [
|
||||
{ number: '987654321', callingCode: '+44' },
|
||||
{ number: '555555555', callingCode: '+33' },
|
||||
{ number: '987654321', callingCode: '+44', countryCode: 'GB' },
|
||||
{ number: '555555555', callingCode: '+33', countryCode: 'FR' },
|
||||
],
|
||||
};
|
||||
const result = createPhonesFromFieldValue(fieldValue);
|
||||
expect(result).toEqual([
|
||||
{ number: '987654321', callingCode: '+44' },
|
||||
{ number: '555555555', callingCode: '+33' },
|
||||
{ number: '987654321', callingCode: '+44', countryCode: 'GB' },
|
||||
{ number: '555555555', callingCode: '+33', countryCode: 'FR' },
|
||||
]);
|
||||
});
|
||||
|
||||
@ -72,22 +93,34 @@ describe('createPhonesFromFieldValue test suite', () => {
|
||||
primaryPhoneNumber: ' ',
|
||||
primaryPhoneCountryCode: '',
|
||||
additionalPhones: [
|
||||
{ number: '987654321', callingCode: '+44' },
|
||||
{ number: '555555555', callingCode: '+33' },
|
||||
{ number: '987654321', callingCode: '+44', countryCode: 'GB' },
|
||||
{ number: '555555555', callingCode: '+33', countryCode: 'FR' },
|
||||
],
|
||||
};
|
||||
const result = createPhonesFromFieldValue(fieldValue);
|
||||
expect(result).toEqual([
|
||||
{ number: ' ', callingCode: '' },
|
||||
{ number: '987654321', callingCode: '+44' },
|
||||
{ number: '555555555', callingCode: '+33' },
|
||||
{ number: ' ', callingCode: '', countryCode: '' },
|
||||
{ number: '987654321', callingCode: '+44', countryCode: 'GB' },
|
||||
{ number: '555555555', callingCode: '+33', countryCode: 'FR' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return an empty array if only country code is defined', () => {
|
||||
it('should return an empty array if only country and calling code are defined', () => {
|
||||
const fieldValue: FieldPhonesValue = {
|
||||
primaryPhoneNumber: '',
|
||||
primaryPhoneCountryCode: '+33',
|
||||
primaryPhoneCountryCode: 'FR',
|
||||
primaryPhoneCallingCode: '+33',
|
||||
additionalPhones: [],
|
||||
};
|
||||
const result = createPhonesFromFieldValue(fieldValue);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return an empty array if only calling code is defined', () => {
|
||||
const fieldValue: FieldPhonesValue = {
|
||||
primaryPhoneNumber: '',
|
||||
primaryPhoneCallingCode: '+33',
|
||||
primaryPhoneCountryCode: '',
|
||||
additionalPhones: [],
|
||||
};
|
||||
const result = createPhonesFromFieldValue(fieldValue);
|
||||
|
@ -8,7 +8,10 @@ export const createPhonesFromFieldValue = (fieldValue: FieldPhonesValue) => {
|
||||
fieldValue.primaryPhoneNumber
|
||||
? {
|
||||
number: fieldValue.primaryPhoneNumber,
|
||||
callingCode: fieldValue.primaryPhoneCountryCode,
|
||||
callingCode: fieldValue.primaryPhoneCallingCode
|
||||
? fieldValue.primaryPhoneCallingCode
|
||||
: fieldValue.primaryPhoneCountryCode,
|
||||
countryCode: fieldValue.primaryPhoneCountryCode,
|
||||
}
|
||||
: null,
|
||||
...(fieldValue.additionalPhones ?? []),
|
||||
|
@ -27,6 +27,7 @@ export type FieldDateTimeDraftValue = string;
|
||||
export type FieldPhonesDraftValue = {
|
||||
primaryPhoneNumber: string;
|
||||
primaryPhoneCountryCode: string;
|
||||
primaryPhoneCallingCode: string;
|
||||
additionalPhones?: PhoneRecord[] | null;
|
||||
};
|
||||
export type FieldEmailsDraftValue = {
|
||||
|
@ -265,10 +265,15 @@ export type FieldActorValue = {
|
||||
|
||||
export type FieldArrayValue = string[];
|
||||
|
||||
export type PhoneRecord = { number: string; callingCode: string };
|
||||
export type PhoneRecord = {
|
||||
number: string;
|
||||
callingCode: string;
|
||||
countryCode: string;
|
||||
};
|
||||
|
||||
export type FieldPhonesValue = {
|
||||
primaryPhoneNumber: string;
|
||||
primaryPhoneCountryCode: string;
|
||||
primaryPhoneCallingCode?: string;
|
||||
additionalPhones?: PhoneRecord[] | null;
|
||||
};
|
||||
|
@ -5,8 +5,15 @@ import { FieldPhonesValue } from '../FieldMetadata';
|
||||
export const phonesSchema = z.object({
|
||||
primaryPhoneNumber: z.string(),
|
||||
primaryPhoneCountryCode: z.string(),
|
||||
primaryPhoneCallingCode: z.string(),
|
||||
additionalPhones: z
|
||||
.array(z.object({ number: z.string(), callingCode: z.string() }))
|
||||
.array(
|
||||
z.object({
|
||||
number: z.string(),
|
||||
callingCode: z.string(),
|
||||
countryCode: z.string(),
|
||||
}),
|
||||
)
|
||||
.nullable(),
|
||||
}) satisfies z.ZodType<FieldPhonesValue>;
|
||||
|
||||
|
@ -71,6 +71,9 @@ export const computeDraftValueFromFieldValue = <FieldValue>({
|
||||
primaryPhoneCountryCode: stripSimpleQuotesFromString(
|
||||
fieldDefinition?.defaultValue?.primaryPhoneCountryCode,
|
||||
),
|
||||
primaryPhoneCallingCode: stripSimpleQuotesFromString(
|
||||
fieldDefinition?.defaultValue?.primaryPhoneCallingCode,
|
||||
),
|
||||
} as unknown as FieldInputDraftValue<FieldValue>;
|
||||
}
|
||||
|
||||
|
@ -255,7 +255,9 @@ export const RecordIndexContainer = () => {
|
||||
<RecordIndexBoardDataLoaderEffect recordBoardId={recordIndexId} />
|
||||
</StyledContainerWithPadding>
|
||||
)}
|
||||
{!isPageHeaderV2Enabled && <RecordIndexActionMenu />}
|
||||
{!isPageHeaderV2Enabled && (
|
||||
<RecordIndexActionMenu indexId={recordIndexId} />
|
||||
)}
|
||||
</RecordFieldValueSelectorContextProvider>
|
||||
</StyledContainer>
|
||||
</>
|
||||
|
@ -29,6 +29,8 @@ export const RecordIndexPageHeader = () => {
|
||||
|
||||
const recordIndexViewType = useRecoilValue(recordIndexViewTypeState);
|
||||
|
||||
const { recordIndexId } = useRecordIndexContextOrThrow();
|
||||
|
||||
const numberOfSelectedRecords = useRecoilComponentValueV2(
|
||||
contextStoreNumberOfSelectedRecordsComponentState,
|
||||
);
|
||||
@ -64,7 +66,7 @@ export const RecordIndexPageHeader = () => {
|
||||
|
||||
{isPageHeaderV2Enabled && (
|
||||
<>
|
||||
<RecordIndexActionMenu />
|
||||
<RecordIndexActionMenu indexId={recordIndexId} />
|
||||
<PageHeaderOpenCommandMenuButton />
|
||||
</>
|
||||
)}
|
||||
|
@ -21,6 +21,7 @@ const mockPerson = {
|
||||
whatsapp: {
|
||||
primaryPhoneNumber: '+1',
|
||||
primaryPhoneCountryCode: '234-567-890',
|
||||
primaryPhoneCallingCode: '+33',
|
||||
additionalPhones: [],
|
||||
},
|
||||
linkedinLink: {
|
||||
|
@ -0,0 +1,9 @@
|
||||
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
|
||||
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
|
||||
|
||||
export const isRecordIndexLoadMoreLockedComponentState =
|
||||
createComponentStateV2<boolean>({
|
||||
key: 'isRecordIndexLoadMoreLockedComponentState',
|
||||
componentInstanceContext: ViewComponentInstanceContext,
|
||||
defaultValue: false,
|
||||
});
|
@ -663,7 +663,8 @@ export const mockPerformance = {
|
||||
id: '20202020-2d40-4e49-8df4-9c6a049191df',
|
||||
email: 'lorie.vladim@google.com',
|
||||
phones: {
|
||||
primaryPhoneCountryCode: '+33',
|
||||
primaryPhoneCountryCode: 'FR',
|
||||
primaryPhoneCallingCode: '+33',
|
||||
primaryPhoneNumber: '788901235',
|
||||
},
|
||||
linkedinLink: {
|
||||
|
@ -4,6 +4,7 @@ import { useInView } from 'react-intersection-observer';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { GRAY_SCALE } from 'twenty-ui';
|
||||
|
||||
import { isRecordIndexLoadMoreLockedComponentState } from '@/object-record/record-index/states/isRecordIndexLoadMoreLockedComponentState';
|
||||
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
||||
import { hasRecordTableFetchedAllRecordsComponentStateV2 } from '@/object-record/record-table/states/hasRecordTableFetchedAllRecordsComponentStateV2';
|
||||
import { RecordTableWithWrappersScrollWrapperContext } from '@/ui/utilities/scroll/contexts/ScrollWrapperContexts';
|
||||
@ -22,11 +23,19 @@ const StyledText = styled.div`
|
||||
export const RecordTableBodyFetchMoreLoader = () => {
|
||||
const { setRecordTableLastRowVisible } = useRecordTable();
|
||||
|
||||
const isRecordTableLoadMoreLocked = useRecoilComponentValueV2(
|
||||
isRecordIndexLoadMoreLockedComponentState,
|
||||
);
|
||||
|
||||
const onLastRowVisible = useRecoilCallback(
|
||||
() => async (inView: boolean) => {
|
||||
if (isRecordTableLoadMoreLocked) {
|
||||
return;
|
||||
}
|
||||
|
||||
setRecordTableLastRowVisible(inView);
|
||||
},
|
||||
[setRecordTableLastRowVisible],
|
||||
[setRecordTableLastRowVisible, isRecordTableLoadMoreLocked],
|
||||
);
|
||||
|
||||
const scrollWrapperRef = useContext(
|
||||
@ -37,7 +46,8 @@ export const RecordTableBodyFetchMoreLoader = () => {
|
||||
hasRecordTableFetchedAllRecordsComponentStateV2,
|
||||
);
|
||||
|
||||
const showLoadingMoreRow = !hasRecordTableFetchedAllRecordsComponents;
|
||||
const showLoadingMoreRow =
|
||||
!hasRecordTableFetchedAllRecordsComponents && !isRecordTableLoadMoreLocked;
|
||||
|
||||
const { ref: tbodyRef } = useInView({
|
||||
onChange: onLastRowVisible,
|
||||
|
@ -53,7 +53,7 @@ export const RecordTableHeaderPlusButtonContent = () => {
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer withoutScrollWrapper>
|
||||
<DropdownMenuItemsContainer scrollable={false}>
|
||||
<UndecoratedLink
|
||||
fullWidth
|
||||
to={`/settings/objects/${getObjectSlug(objectMetadataItem)}`}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useCurrentRecordGroupId';
|
||||
import { useLazyLoadRecordIndexTable } from '@/object-record/record-index/hooks/useLazyLoadRecordIndexTable';
|
||||
import { isRecordIndexLoadMoreLockedComponentState } from '@/object-record/record-index/states/isRecordIndexLoadMoreLockedComponentState';
|
||||
import { recordIndexHasFetchedAllRecordsByGroupComponentState } from '@/object-record/record-index/states/recordIndexHasFetchedAllRecordsByGroupComponentState';
|
||||
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
|
||||
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
@ -20,6 +21,10 @@ export const RecordTableRecordGroupSectionLoadMore = () => {
|
||||
currentRecordGroupId,
|
||||
);
|
||||
|
||||
const isLoadMoreLocked = useRecoilComponentValueV2(
|
||||
isRecordIndexLoadMoreLockedComponentState,
|
||||
);
|
||||
|
||||
const recordIds = useRecoilComponentValueV2(
|
||||
recordIndexAllRecordIdsComponentSelector,
|
||||
);
|
||||
@ -28,7 +33,7 @@ export const RecordTableRecordGroupSectionLoadMore = () => {
|
||||
fetchMoreRecords();
|
||||
};
|
||||
|
||||
if (hasFetchedAllRecords) {
|
||||
if (hasFetchedAllRecords || isLoadMoreLocked) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -144,7 +144,7 @@ export const MultiRecordSelect = ({
|
||||
{dropdownPlacement?.includes('end') && (
|
||||
<>
|
||||
{isDefined(onCreate) && (
|
||||
<DropdownMenuItemsContainer>
|
||||
<DropdownMenuItemsContainer scrollable={false}>
|
||||
{createNewButton}
|
||||
</DropdownMenuItemsContainer>
|
||||
)}
|
||||
@ -181,7 +181,7 @@ export const MultiRecordSelect = ({
|
||||
<DropdownMenuSeparator />
|
||||
)}
|
||||
{isDefined(onCreate) && (
|
||||
<DropdownMenuItemsContainer withoutScrollWrapper>
|
||||
<DropdownMenuItemsContainer scrollable={false}>
|
||||
{createNewButton}
|
||||
</DropdownMenuItemsContainer>
|
||||
)}
|
||||
|
@ -69,7 +69,7 @@ export const SingleRecordSelectMenuItemsWithSearch = ({
|
||||
<>
|
||||
{dropdownPlacement?.includes('end') && (
|
||||
<>
|
||||
<DropdownMenuItemsContainer>
|
||||
<DropdownMenuItemsContainer scrollable={false}>
|
||||
{createNewButton}
|
||||
</DropdownMenuItemsContainer>
|
||||
{records.recordsToSelect.length > 0 && <DropdownMenuSeparator />}
|
||||
@ -117,7 +117,7 @@ export const SingleRecordSelectMenuItemsWithSearch = ({
|
||||
<DropdownMenuSeparator />
|
||||
)}
|
||||
{isDefined(onCreate) && (
|
||||
<DropdownMenuItemsContainer>
|
||||
<DropdownMenuItemsContainer scrollable={false}>
|
||||
{createNewButton}
|
||||
</DropdownMenuItemsContainer>
|
||||
)}
|
||||
|
@ -207,6 +207,7 @@ const companyMocks = [
|
||||
phones {
|
||||
primaryPhoneNumber
|
||||
primaryPhoneCountryCode
|
||||
primaryPhoneCallingCode
|
||||
additionalPhones
|
||||
}
|
||||
position
|
||||
@ -214,6 +215,7 @@ const companyMocks = [
|
||||
whatsapp {
|
||||
primaryPhoneNumber
|
||||
primaryPhoneCountryCode
|
||||
primaryPhoneCallingCode
|
||||
additionalPhones
|
||||
}
|
||||
workPreference
|
||||
|
@ -95,6 +95,7 @@ export const generateEmptyFieldValue = (
|
||||
return {
|
||||
primaryPhoneNumber: '',
|
||||
primaryPhoneCountryCode: '',
|
||||
primaryPhoneCallingCode: '',
|
||||
additionalPhones: null,
|
||||
};
|
||||
}
|
||||
|
@ -91,7 +91,9 @@ export const SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS = {
|
||||
exampleValue: {
|
||||
primaryPhoneNumber: '234-567-890',
|
||||
primaryPhoneCountryCode: '+1',
|
||||
additionalPhones: [{ number: '234-567-890', callingCode: '+1' }],
|
||||
additionalPhones: [
|
||||
{ number: '234-567-890', callingCode: '+1', countryCode: 'US' },
|
||||
],
|
||||
},
|
||||
subFields: [
|
||||
'primaryPhoneNumber',
|
||||
@ -102,6 +104,7 @@ export const SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS = {
|
||||
labelBySubField: {
|
||||
primaryPhoneNumber: 'Primary Phone Number',
|
||||
primaryPhoneCountryCode: 'Primary Phone Country Code',
|
||||
primaryPhoneCallingCode: 'Primary Phone Calling Code',
|
||||
additionalPhones: 'Additional Phones',
|
||||
},
|
||||
category: 'Basic',
|
||||
|
@ -3,8 +3,10 @@ import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { phonesSchema as phonesFieldDefaultValueSchema } from '@/object-record/record-field/types/guards/isFieldPhonesValue';
|
||||
import { SettingsOptionCardContentSelect } from '@/settings/components/SettingsOptions/SettingsOptionCardContentSelect';
|
||||
import { countryCodeToCallingCode } from '@/settings/data-model/fields/preview/utils/getPhonesFieldPreviewValue';
|
||||
import { useCountries } from '@/ui/input/components/internal/hooks/useCountries';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { CountryCode } from 'libphonenumber-js';
|
||||
import { IconMap } from 'twenty-ui';
|
||||
import { z } from 'zod';
|
||||
import { applySimpleQuotesToString } from '~/utils/string/applySimpleQuotesToString';
|
||||
@ -27,22 +29,27 @@ export type SettingsDataModelFieldTextFormValues = z.infer<
|
||||
typeof settingsDataModelFieldPhonesFormSchema
|
||||
>;
|
||||
|
||||
export type CountryCodeOrEmpty = CountryCode | '';
|
||||
|
||||
export const SettingsDataModelFieldPhonesForm = ({
|
||||
disabled,
|
||||
fieldMetadataItem,
|
||||
}: SettingsDataModelFieldPhonesFormProps) => {
|
||||
const { control } = useFormContext<SettingsDataModelFieldTextFormValues>();
|
||||
|
||||
const countries = useCountries()
|
||||
.sort((a, b) => a.countryName.localeCompare(b.countryName))
|
||||
.map((country) => ({
|
||||
label: `${country.countryName} (+${country.callingCode})`,
|
||||
value: `+${country.callingCode}`,
|
||||
}));
|
||||
countries.unshift({ label: 'No country', value: '' });
|
||||
const countries = [
|
||||
{ label: 'No country', value: '' },
|
||||
...useCountries()
|
||||
.sort((a, b) => a.countryName.localeCompare(b.countryName))
|
||||
.map((country) => ({
|
||||
label: `${country.countryName} (+${country.callingCode})`,
|
||||
value: country.countryCode as CountryCodeOrEmpty,
|
||||
})),
|
||||
];
|
||||
const defaultDefaultValue = {
|
||||
primaryPhoneNumber: "''",
|
||||
primaryPhoneCountryCode: "''",
|
||||
primaryPhoneCallingCode: "''",
|
||||
additionalPhones: null,
|
||||
};
|
||||
const fieldMetadataItemDefaultValue = fieldMetadataItem?.defaultValue;
|
||||
@ -73,6 +80,9 @@ export const SettingsDataModelFieldPhonesForm = ({
|
||||
...value,
|
||||
primaryPhoneCountryCode:
|
||||
applySimpleQuotesToString(newPhoneCountryCode),
|
||||
primaryPhoneCallingCode: applySimpleQuotesToString(
|
||||
countryCodeToCallingCode(newPhoneCountryCode),
|
||||
),
|
||||
})
|
||||
}
|
||||
disabled={disabled}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user