mirror of
https://github.com/twentyhq/twenty.git
synced 2024-12-22 19:41:53 +03:00
Merge branch 'main' into feat/new-seeds
This commit is contained in:
commit
59478a08bb
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
|
||||
|
@ -8,19 +8,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 +38,6 @@ export const useDeleteMultipleRecordsAction = ({
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
});
|
||||
|
||||
const { sortedFavorites: favorites } = useFavorites();
|
||||
const { deleteFavorite } = useDeleteFavorite();
|
||||
|
||||
const contextStoreNumberOfSelectedRecords = useRecoilComponentValueV2(
|
||||
contextStoreNumberOfSelectedRecordsComponentState,
|
||||
);
|
||||
@ -76,26 +71,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 +82,7 @@ export const useDeleteMultipleRecordsAction = ({
|
||||
contextStoreNumberOfSelectedRecords < DELETE_MAX_COUNT &&
|
||||
contextStoreNumberOfSelectedRecords > 0;
|
||||
|
||||
const { isInRightDrawer, onActionExecutedCallback } =
|
||||
const { isInRightDrawer, onActionStartedCallback, onActionExecutedCallback } =
|
||||
useContext(ActionMenuContext);
|
||||
|
||||
const registerDeleteMultipleRecordsAction = ({
|
||||
@ -118,7 +95,8 @@ export const useDeleteMultipleRecordsAction = ({
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
key: 'delete-multiple-records',
|
||||
label: 'Delete',
|
||||
label: 'Delete records',
|
||||
shortLabel: 'Delete',
|
||||
position,
|
||||
Icon: IconTrash,
|
||||
accent: 'danger',
|
||||
@ -132,9 +110,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();
|
||||
}
|
||||
|
@ -36,6 +36,7 @@ export const useExportMultipleRecordsAction = ({
|
||||
key: 'export-multiple-records',
|
||||
position,
|
||||
label: displayedExportProgress(progress),
|
||||
shortLabel: 'Export',
|
||||
Icon: IconDatabaseExport,
|
||||
accent: 'default',
|
||||
onClick: () => download(),
|
||||
|
@ -1,5 +1,6 @@
|
||||
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';
|
||||
@ -16,6 +17,7 @@ import {
|
||||
IconHeart,
|
||||
IconHeartOff,
|
||||
IconTrash,
|
||||
IconTrashX,
|
||||
} from 'twenty-ui';
|
||||
|
||||
export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record<
|
||||
@ -70,13 +72,29 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record<
|
||||
],
|
||||
actionHook: useDeleteSingleRecordAction,
|
||||
},
|
||||
destroySingleRecord: {
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
key: 'destroy-single-record',
|
||||
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',
|
||||
label: 'Navigate to previous record',
|
||||
shortLabel: '',
|
||||
position: 3,
|
||||
position: 4,
|
||||
isPinned: true,
|
||||
Icon: IconChevronUp,
|
||||
availableOn: [ActionAvailableOn.SHOW_PAGE],
|
||||
@ -88,7 +106,7 @@ export const DEFAULT_SINGLE_RECORD_ACTIONS_CONFIG_V2: Record<
|
||||
key: 'navigate-to-next-record',
|
||||
label: 'Navigate to next record',
|
||||
shortLabel: '',
|
||||
position: 4,
|
||||
position: 5,
|
||||
isPinned: true,
|
||||
Icon: IconChevronDown,
|
||||
availableOn: [ActionAvailableOn.SHOW_PAGE],
|
||||
|
@ -2,6 +2,7 @@ import { SingleRecordActionHookWithObjectMetadataItem } from '@/action-menu/acti
|
||||
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) {
|
||||
|
@ -3,10 +3,13 @@ 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,73 @@
|
||||
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, onActionExecutedCallback } =
|
||||
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();
|
||||
onActionExecutedCallback?.({ key: 'destroy-single-record' });
|
||||
if (isInRightDrawer) {
|
||||
closeRightDrawer();
|
||||
}
|
||||
}}
|
||||
deleteButtonText={'Permanently Destroy Record'}
|
||||
/>
|
||||
),
|
||||
};
|
||||
};
|
@ -8,11 +8,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 +27,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 === 'delete-multiple-records') {
|
||||
setIsLoadMoreLocked(true);
|
||||
}
|
||||
},
|
||||
onActionExecutedCallback: (action) => {
|
||||
if (action.key === 'delete-multiple-records') {
|
||||
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 }) => void;
|
||||
onActionExecutedCallback?: (action: { key: string }) => void;
|
||||
};
|
||||
|
||||
export const ActionMenuContext = createContext<ActionMenuContextType>({
|
||||
isInRightDrawer: false,
|
||||
onActionStartedCallback: () => {},
|
||||
onActionExecutedCallback: () => {},
|
||||
});
|
||||
|
@ -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,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,7 @@
|
||||
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 +9,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 +82,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 +117,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,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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,7 +1,6 @@
|
||||
import { StyledHeaderDropdownButton } from '@/ui/layout/dropdown/components/StyledHeaderDropdownButton';
|
||||
import styled from '@emotion/styled';
|
||||
import { AppTooltip, Tag, TooltipDelay } from 'twenty-ui';
|
||||
import { formatNumber } from '~/utils/format/number';
|
||||
|
||||
const StyledButton = styled(StyledHeaderDropdownButton)`
|
||||
padding: 0;
|
||||
@ -19,10 +18,7 @@ export const RecordBoardColumnHeaderAggregateDropdownButton = ({
|
||||
return (
|
||||
<div id={dropdownId}>
|
||||
<StyledButton>
|
||||
<Tag
|
||||
text={value ? formatNumber(Number(value)) : '-'}
|
||||
color={'transparent'}
|
||||
/>
|
||||
<Tag text={value ? value.toString() : '-'} color={'transparent'} />
|
||||
<AppTooltip
|
||||
anchorSelect={`#${dropdownId}`}
|
||||
content={tooltip}
|
||||
|
@ -74,7 +74,7 @@ export const useAggregateRecordsForRecordBoardColumn = () => {
|
||||
skip: !isAggregateQueryEnabled,
|
||||
});
|
||||
|
||||
const { value, label } = computeAggregateValueAndLabel({
|
||||
const { value, labelWithFieldName } = computeAggregateValueAndLabel({
|
||||
data,
|
||||
objectMetadataItem,
|
||||
fieldMetadataId: recordIndexKanbanAggregateOperation?.fieldMetadataId,
|
||||
@ -84,6 +84,6 @@ export const useAggregateRecordsForRecordBoardColumn = () => {
|
||||
|
||||
return {
|
||||
aggregateValue: isAggregateQueryEnabled ? value : recordCount,
|
||||
aggregateLabel: isDefined(value) ? label : undefined,
|
||||
aggregateLabel: isDefined(value) ? labelWithFieldName : undefined,
|
||||
};
|
||||
};
|
||||
|
@ -47,8 +47,81 @@ describe('computeAggregateValueAndLabel', () => {
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
value: 2,
|
||||
label: 'Sum of amount',
|
||||
value: '2',
|
||||
label: 'Sum',
|
||||
labelWithFieldName: 'Sum of amount',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle number field as percentage', () => {
|
||||
const mockObjectMetadataWithPercentageField: ObjectMetadataItem = {
|
||||
id: '123',
|
||||
fields: [
|
||||
{
|
||||
id: MOCK_FIELD_ID,
|
||||
name: 'percentage',
|
||||
type: FieldMetadataType.Number,
|
||||
settings: {
|
||||
type: 'percentage',
|
||||
},
|
||||
} as FieldMetadataItem,
|
||||
],
|
||||
} as ObjectMetadataItem;
|
||||
|
||||
const mockData = {
|
||||
percentage: {
|
||||
[AGGREGATE_OPERATIONS.avg]: 0.3,
|
||||
},
|
||||
};
|
||||
|
||||
const result = computeAggregateValueAndLabel({
|
||||
data: mockData,
|
||||
objectMetadataItem: mockObjectMetadataWithPercentageField,
|
||||
fieldMetadataId: MOCK_FIELD_ID,
|
||||
aggregateOperation: AGGREGATE_OPERATIONS.avg,
|
||||
fallbackFieldName: MOCK_KANBAN_FIELD_NAME,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
value: '30%',
|
||||
label: 'Average',
|
||||
labelWithFieldName: 'Average of percentage',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle number field with decimals', () => {
|
||||
const mockObjectMetadataWithDecimalsField: ObjectMetadataItem = {
|
||||
id: '123',
|
||||
fields: [
|
||||
{
|
||||
id: MOCK_FIELD_ID,
|
||||
name: 'decimals',
|
||||
type: FieldMetadataType.Number,
|
||||
settings: {
|
||||
decimals: 2,
|
||||
},
|
||||
} as FieldMetadataItem,
|
||||
],
|
||||
} as ObjectMetadataItem;
|
||||
|
||||
const mockData = {
|
||||
decimals: {
|
||||
[AGGREGATE_OPERATIONS.sum]: 0.009,
|
||||
},
|
||||
};
|
||||
|
||||
const result = computeAggregateValueAndLabel({
|
||||
data: mockData,
|
||||
objectMetadataItem: mockObjectMetadataWithDecimalsField,
|
||||
fieldMetadataId: MOCK_FIELD_ID,
|
||||
aggregateOperation: AGGREGATE_OPERATIONS.sum,
|
||||
fallbackFieldName: MOCK_KANBAN_FIELD_NAME,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
value: '0.01',
|
||||
label: 'Sum',
|
||||
labelWithFieldName: 'Sum of decimals',
|
||||
});
|
||||
});
|
||||
|
||||
@ -86,8 +159,9 @@ describe('computeAggregateValueAndLabel', () => {
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
value: undefined,
|
||||
label: 'Sum of amount',
|
||||
value: '-',
|
||||
label: 'Sum',
|
||||
labelWithFieldName: 'Sum of amount',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -4,6 +4,8 @@ import { getAggregateOperationLabel } from '@/object-record/record-board/record-
|
||||
import { AGGREGATE_OPERATIONS } from '@/object-record/record-table/constants/AggregateOperations';
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { formatAmount } from '~/utils/format/formatAmount';
|
||||
import { formatNumber } from '~/utils/format/number';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const computeAggregateValueAndLabel = ({
|
||||
@ -42,12 +44,33 @@ export const computeAggregateValueAndLabel = ({
|
||||
|
||||
const aggregateValue = data[field.name]?.[aggregateOperation];
|
||||
|
||||
const value =
|
||||
isDefined(aggregateValue) && field.type === FieldMetadataType.Currency
|
||||
? Number(aggregateValue) / 1_000_000
|
||||
: aggregateValue;
|
||||
let value;
|
||||
|
||||
const label =
|
||||
if (aggregateOperation === AGGREGATE_OPERATIONS.count) {
|
||||
value = aggregateValue;
|
||||
} else if (!isDefined(aggregateValue)) {
|
||||
value = '-';
|
||||
} else {
|
||||
value = Number(aggregateValue);
|
||||
|
||||
switch (field.type) {
|
||||
case FieldMetadataType.Currency: {
|
||||
value = formatAmount(value / 1_000_000);
|
||||
break;
|
||||
}
|
||||
|
||||
case FieldMetadataType.Number: {
|
||||
const { decimals, type } = field.settings ?? {};
|
||||
value =
|
||||
type === 'percentage'
|
||||
? `${formatNumber(value * 100, decimals)}%`
|
||||
: formatNumber(value, decimals);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const label = getAggregateOperationLabel(aggregateOperation);
|
||||
const labelWithFieldName =
|
||||
aggregateOperation === AGGREGATE_OPERATIONS.count
|
||||
? `${getAggregateOperationLabel(AGGREGATE_OPERATIONS.count)}`
|
||||
: `${getAggregateOperationLabel(aggregateOperation)} of ${field.name}`;
|
||||
@ -55,5 +78,6 @@ export const computeAggregateValueAndLabel = ({
|
||||
return {
|
||||
value,
|
||||
label,
|
||||
labelWithFieldName,
|
||||
};
|
||||
};
|
||||
|
@ -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 = {
|
||||
|
@ -268,10 +268,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>;
|
||||
}
|
||||
|
||||
|
@ -230,6 +230,7 @@ export const RecordIndexContainer = () => {
|
||||
objectNamePlural={objectNamePlural}
|
||||
viewBarId={recordIndexId}
|
||||
/>
|
||||
<RecordIndexTableContainerEffect />
|
||||
</SpreadsheetImportProvider>
|
||||
<RecordIndexFiltersToContextStoreEffect />
|
||||
{recordIndexViewType === ViewType.Table && (
|
||||
@ -255,7 +256,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,
|
||||
});
|
@ -4,6 +4,6 @@ import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewCompon
|
||||
export const recordIndexRecordGroupHideComponentState =
|
||||
createComponentStateV2<boolean>({
|
||||
key: 'recordIndexRecordGroupHideComponentState',
|
||||
defaultValue: true,
|
||||
defaultValue: false,
|
||||
componentInstanceContext: ViewComponentInstanceContext,
|
||||
});
|
||||
|
@ -13,7 +13,7 @@ import { RecordTableNoRecordGroupBody } from '@/object-record/record-table/recor
|
||||
import { RecordTableNoRecordGroupBodyEffect } from '@/object-record/record-table/record-table-body/components/RecordTableNoRecordGroupBodyEffect';
|
||||
import { RecordTableRecordGroupBodyEffects } from '@/object-record/record-table/record-table-body/components/RecordTableRecordGroupBodyEffects';
|
||||
import { RecordTableRecordGroupsBody } from '@/object-record/record-table/record-table-body/components/RecordTableRecordGroupsBody';
|
||||
import { RecordTableFooter } from '@/object-record/record-table/record-table-footer/components/RecordTableFooter';
|
||||
import { RecordTableAggregateFooter } from '@/object-record/record-table/record-table-footer/components/RecordTableAggregateFooter';
|
||||
import { RecordTableHeader } from '@/object-record/record-table/record-table-header/components/RecordTableHeader';
|
||||
import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState';
|
||||
import { recordTablePendingRecordIdComponentState } from '@/object-record/record-table/states/recordTablePendingRecordIdComponentState';
|
||||
@ -88,7 +88,7 @@ export const RecordTable = () => {
|
||||
<RecordTableEmptyState />
|
||||
) : (
|
||||
<>
|
||||
<StyledTable className="entity-table-cell" ref={tableBodyRef}>
|
||||
<StyledTable ref={tableBodyRef}>
|
||||
<RecordTableHeader />
|
||||
{!hasRecordGroups ? (
|
||||
<RecordTableNoRecordGroupBody />
|
||||
@ -97,7 +97,7 @@ export const RecordTable = () => {
|
||||
)}
|
||||
<RecordTableStickyEffect />
|
||||
{isAggregateQueryEnabled && !hasRecordGroups && (
|
||||
<RecordTableFooter />
|
||||
<RecordTableAggregateFooter endOfTableSticky />
|
||||
)}
|
||||
</StyledTable>
|
||||
<DragSelect
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useCurrentRecordGroupId';
|
||||
import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState';
|
||||
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
|
||||
import { RecordTableFooter } from '@/object-record/record-table/record-table-footer/components/RecordTableFooter';
|
||||
import { RecordTablePendingRecordGroupRow } from '@/object-record/record-table/record-table-row/components/RecordTablePendingRecordGroupRow';
|
||||
import { RecordTableRow } from '@/object-record/record-table/record-table-row/components/RecordTableRow';
|
||||
import { RecordTableRecordGroupSectionAddNew } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupSectionAddNew';
|
||||
@ -9,14 +8,10 @@ import { RecordTableRecordGroupSectionLoadMore } from '@/object-record/record-ta
|
||||
import { isRecordGroupTableSectionToggledComponentState } from '@/object-record/record-table/record-table-section/states/isRecordGroupTableSectionToggledComponentState';
|
||||
import { useRecoilComponentFamilyValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyValueV2';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { useMemo } from 'react';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const RecordTableRecordGroupRows = () => {
|
||||
const isAggregateQueryEnabled = useIsFeatureEnabled(
|
||||
'IS_AGGREGATE_QUERY_ENABLED',
|
||||
);
|
||||
const currentRecordGroupId = useCurrentRecordGroupId();
|
||||
|
||||
const allRecordIds = useRecoilComponentValueV2(
|
||||
@ -63,12 +58,6 @@ export const RecordTableRecordGroupRows = () => {
|
||||
})}
|
||||
<RecordTablePendingRecordGroupRow />
|
||||
<RecordTableRecordGroupSectionAddNew />
|
||||
{isAggregateQueryEnabled && (
|
||||
<RecordTableFooter
|
||||
key={currentRecordGroupId}
|
||||
currentRecordGroupId={currentRecordGroupId}
|
||||
/>
|
||||
)}
|
||||
<RecordTableRecordGroupSectionLoadMore />
|
||||
</>
|
||||
);
|
||||
|
@ -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: {
|
||||
|
@ -15,6 +15,7 @@ export const RecordTableBodyDroppable = ({
|
||||
isDropDisabled,
|
||||
}: RecordTableBodyDroppableProps) => {
|
||||
const [v4Persistable] = useState(v4());
|
||||
const recordTableBodyId = `record-table-body${recordGroupId ? '-' + recordGroupId : ''}`;
|
||||
|
||||
return (
|
||||
<Droppable
|
||||
@ -23,7 +24,7 @@ export const RecordTableBodyDroppable = ({
|
||||
>
|
||||
{(provided) => (
|
||||
<RecordTableBody
|
||||
id="record-table-body"
|
||||
id={recordTableBodyId}
|
||||
ref={provided.innerRef}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...provided.droppableProps}
|
||||
|
@ -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,
|
||||
|
@ -6,12 +6,17 @@ import { RecordTableRecordGroupRows } from '@/object-record/record-table/compone
|
||||
import { RecordTableBodyDroppable } from '@/object-record/record-table/record-table-body/components/RecordTableBodyDroppable';
|
||||
import { RecordTableBodyLoading } from '@/object-record/record-table/record-table-body/components/RecordTableBodyLoading';
|
||||
import { RecordTableBodyRecordGroupDragDropContextProvider } from '@/object-record/record-table/record-table-body/components/RecordTableBodyRecordGroupDragDropContextProvider';
|
||||
import { RecordTableAggregateFooter } from '@/object-record/record-table/record-table-footer/components/RecordTableAggregateFooter';
|
||||
import { RecordTableRecordGroupEmptyRow } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupEmptyRow';
|
||||
import { RecordTableRecordGroupSection } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupSection';
|
||||
import { isRecordTableInitialLoadingComponentState } from '@/object-record/record-table/states/isRecordTableInitialLoadingComponentState';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
|
||||
export const RecordTableRecordGroupsBody = () => {
|
||||
const isAggregateQueryEnabled = useIsFeatureEnabled(
|
||||
'IS_AGGREGATE_QUERY_ENABLED',
|
||||
);
|
||||
const allRecordIds = useRecoilComponentValueV2(
|
||||
recordIndexAllRecordIdsComponentSelector,
|
||||
);
|
||||
@ -29,21 +34,29 @@ export const RecordTableRecordGroupsBody = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<RecordTableBodyRecordGroupDragDropContextProvider>
|
||||
{visibleRecordGroupIds.map((recordGroupId, index) => (
|
||||
<RecordTableRecordGroupBodyContextProvider
|
||||
key={recordGroupId}
|
||||
recordGroupId={recordGroupId}
|
||||
>
|
||||
<RecordGroupContext.Provider value={{ recordGroupId }}>
|
||||
<RecordTableBodyDroppable recordGroupId={recordGroupId}>
|
||||
{index > 0 && <RecordTableRecordGroupEmptyRow />}
|
||||
<RecordTableRecordGroupSection />
|
||||
<RecordTableRecordGroupRows />
|
||||
</RecordTableBodyDroppable>
|
||||
</RecordGroupContext.Provider>
|
||||
</RecordTableRecordGroupBodyContextProvider>
|
||||
))}
|
||||
</RecordTableBodyRecordGroupDragDropContextProvider>
|
||||
<>
|
||||
<RecordTableBodyRecordGroupDragDropContextProvider>
|
||||
{visibleRecordGroupIds.map((recordGroupId, index) => (
|
||||
<RecordTableRecordGroupBodyContextProvider
|
||||
key={recordGroupId}
|
||||
recordGroupId={recordGroupId}
|
||||
>
|
||||
<RecordGroupContext.Provider value={{ recordGroupId }}>
|
||||
<RecordTableBodyDroppable recordGroupId={recordGroupId}>
|
||||
{index > 0 && <RecordTableRecordGroupEmptyRow />}
|
||||
<RecordTableRecordGroupSection />
|
||||
<RecordTableRecordGroupRows />
|
||||
</RecordTableBodyDroppable>
|
||||
{isAggregateQueryEnabled && (
|
||||
<RecordTableAggregateFooter
|
||||
key={recordGroupId}
|
||||
currentRecordGroupId={recordGroupId}
|
||||
/>
|
||||
)}
|
||||
</RecordGroupContext.Provider>
|
||||
</RecordTableRecordGroupBodyContextProvider>
|
||||
))}
|
||||
</RecordTableBodyRecordGroupDragDropContextProvider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,17 +1,16 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { MOBILE_VIEWPORT } from 'twenty-ui';
|
||||
|
||||
import { TABLE_CELL_CHECKBOX_MIN_WIDTH } from '@/object-record/record-table/record-table-cell/components/RecordTableCellCheckbox';
|
||||
import { TABLE_CELL_GRIP_WIDTH } from '@/object-record/record-table/record-table-cell/components/RecordTableCellGrip';
|
||||
import { RecordTableFooterCell } from '@/object-record/record-table/record-table-footer/components/RecordTableFooterCell';
|
||||
import { RecordTableAggregateFooterCell } from '@/object-record/record-table/record-table-footer/components/RecordTableAggregateFooterCell';
|
||||
import { FIRST_TH_WIDTH } from '@/object-record/record-table/record-table-header/components/RecordTableHeader';
|
||||
import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
|
||||
const StyledTableFoot = styled.thead`
|
||||
const StyledTableFoot = styled.thead<{ endOfTableSticky?: boolean }>`
|
||||
cursor: pointer;
|
||||
|
||||
th:nth-of-type(1) {
|
||||
width: 9px;
|
||||
width: ${FIRST_TH_WIDTH};
|
||||
left: 0;
|
||||
border-right-color: ${({ theme }) => theme.background.primary};
|
||||
}
|
||||
@ -59,31 +58,23 @@ const StyledTableFoot = styled.thead`
|
||||
}
|
||||
}
|
||||
|
||||
&.header-sticky {
|
||||
th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 5;
|
||||
}
|
||||
}
|
||||
|
||||
&.header-sticky.first-columns-sticky {
|
||||
th:nth-of-type(1),
|
||||
th:nth-of-type(2),
|
||||
th:nth-of-type(3) {
|
||||
z-index: 10;
|
||||
}
|
||||
tr {
|
||||
position: sticky;
|
||||
z-index: 5;
|
||||
${({ endOfTableSticky }) => endOfTableSticky && `bottom: 0;`}
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledDiv = styled.div`
|
||||
width: calc(${TABLE_CELL_GRIP_WIDTH} + ${TABLE_CELL_CHECKBOX_MIN_WIDTH});
|
||||
const StyledTh = styled.th`
|
||||
background-color: ${({ theme }) => theme.background.primary};
|
||||
`;
|
||||
|
||||
export const RecordTableFooter = ({
|
||||
export const RecordTableAggregateFooter = ({
|
||||
currentRecordGroupId,
|
||||
endOfTableSticky,
|
||||
}: {
|
||||
currentRecordGroupId?: string;
|
||||
endOfTableSticky?: boolean;
|
||||
}) => {
|
||||
const visibleTableColumns = useRecoilComponentValueV2(
|
||||
visibleTableColumnsComponentSelector,
|
||||
@ -93,12 +84,13 @@ export const RecordTableFooter = ({
|
||||
<StyledTableFoot
|
||||
id={`record-table-footer${currentRecordGroupId ? '-' + currentRecordGroupId : ''}`}
|
||||
data-select-disable
|
||||
endOfTableSticky={endOfTableSticky}
|
||||
>
|
||||
<tr>
|
||||
<th />
|
||||
<StyledDiv />
|
||||
<StyledTh />
|
||||
<StyledTh />
|
||||
{visibleTableColumns.map((column, index) => (
|
||||
<RecordTableFooterCell
|
||||
<RecordTableAggregateFooterCell
|
||||
key={`${column.fieldMetadataId}${currentRecordGroupId ? '-' + currentRecordGroupId : ''}`}
|
||||
column={column}
|
||||
currentRecordGroupId={currentRecordGroupId}
|
@ -2,7 +2,7 @@ import styled from '@emotion/styled';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { RecordTableColumnFooterWithDropdown } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnFooterWithDropdown';
|
||||
import { RecordTableColumnFooterWithDropdown } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterWithDropdown';
|
||||
import { tableColumnsComponentState } from '@/object-record/record-table/states/tableColumnsComponentState';
|
||||
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
@ -61,7 +61,7 @@ const StyledColumnFootContainer = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const RecordTableFooterCell = ({
|
||||
export const RecordTableAggregateFooterCell = ({
|
||||
column,
|
||||
isFirstCell = false,
|
||||
currentRecordGroupId,
|
@ -14,7 +14,7 @@ import { useMemo } from 'react';
|
||||
import { Key } from 'ts-key-enum';
|
||||
import { MenuItem } from 'twenty-ui';
|
||||
|
||||
export const RecordTableColumnFooterDropdown = ({
|
||||
export const RecordTableColumnAggregateFooterDropdown = ({
|
||||
column,
|
||||
dropdownId,
|
||||
}: {
|
||||
@ -30,10 +30,6 @@ export const RecordTableColumnFooterDropdown = ({
|
||||
(viewField) => viewField.fieldMetadataId === column.fieldMetadataId,
|
||||
);
|
||||
|
||||
if (!currentViewField) {
|
||||
throw new Error('ViewField not found');
|
||||
}
|
||||
|
||||
useScopedHotkeys(
|
||||
[Key.Escape],
|
||||
() => {
|
||||
@ -56,6 +52,9 @@ export const RecordTableColumnFooterDropdown = ({
|
||||
const handleAggregationChange = (
|
||||
aggregateOperation: AGGREGATE_OPERATIONS,
|
||||
) => {
|
||||
if (!currentViewField) {
|
||||
throw new Error('ViewField not found');
|
||||
}
|
||||
updateViewFieldRecords([
|
||||
{ ...currentViewField, aggregateOperation: aggregateOperation },
|
||||
]);
|
@ -1,12 +1,7 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
AppTooltip,
|
||||
IconChevronDown,
|
||||
isDefined,
|
||||
TooltipDelay,
|
||||
} from 'twenty-ui';
|
||||
import { IconChevronDown, isDefined } from 'twenty-ui';
|
||||
|
||||
const StyledCell = styled.div`
|
||||
align-items: center;
|
||||
@ -37,6 +32,27 @@ const StyledText = styled.span`
|
||||
z-index: 1;
|
||||
`;
|
||||
|
||||
const StyledValueContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex: 1 0 0;
|
||||
gap: 4px;
|
||||
height: 32px;
|
||||
justify-content: flex-end;
|
||||
padding: 8px;
|
||||
`;
|
||||
|
||||
const StyledLabel = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
`;
|
||||
|
||||
const StyledValue = styled.div`
|
||||
color: ${({ theme }) => theme.color.gray60};
|
||||
flex: 1 0 0;
|
||||
`;
|
||||
|
||||
const StyledIcon = styled(IconChevronDown)`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
@ -46,7 +62,7 @@ const StyledIcon = styled(IconChevronDown)`
|
||||
padding-right: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
export const RecordTableColumnFooterAggregateValue = ({
|
||||
export const RecordTableColumnAggregateFooterValue = ({
|
||||
dropdownId,
|
||||
aggregateValue,
|
||||
aggregateLabel,
|
||||
@ -70,20 +86,15 @@ export const RecordTableColumnFooterAggregateValue = ({
|
||||
<StyledCell>
|
||||
{isHovered || isDefined(aggregateValue) || isFirstCell ? (
|
||||
<>
|
||||
<StyledText id={sanitizedId}>
|
||||
{aggregateValue ?? 'Calculate'}
|
||||
</StyledText>
|
||||
<StyledIcon fontWeight={'light'} size={theme.icon.size.sm} />
|
||||
{isDefined(aggregateValue) && isDefined(aggregateLabel) && (
|
||||
<AppTooltip
|
||||
anchorSelect={`#${sanitizedId}`}
|
||||
content={aggregateLabel}
|
||||
noArrow
|
||||
place="top-start"
|
||||
positionStrategy="fixed"
|
||||
delay={TooltipDelay.shortDelay}
|
||||
/>
|
||||
{isDefined(aggregateValue) ? (
|
||||
<StyledValueContainer>
|
||||
<StyledLabel>{aggregateLabel}</StyledLabel>
|
||||
<StyledValue>{aggregateValue}</StyledValue>
|
||||
</StyledValueContainer>
|
||||
) : (
|
||||
<StyledText id={sanitizedId}>Calculate</StyledText>
|
||||
)}
|
||||
<StyledIcon fontWeight={'light'} size={theme.icon.size.sm} />
|
||||
</>
|
||||
) : (
|
||||
<></>
|
@ -1,6 +1,6 @@
|
||||
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { RecordTableColumnFooterAggregateValue } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnFooterAggregateValue';
|
||||
import { RecordTableColumnFooterDropdown } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnFooterDropdown';
|
||||
import { RecordTableColumnAggregateFooterDropdown } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterDropdown';
|
||||
import { RecordTableColumnAggregateFooterValue } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterValue';
|
||||
import { useAggregateRecordsForRecordTableColumnFooter } from '@/object-record/record-table/record-table-footer/hooks/useAggregateRecordsForRecordTableColumnFooter';
|
||||
import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
@ -44,7 +44,7 @@ export const RecordTableColumnFooterWithDropdown = ({
|
||||
onClose={handleDropdownClose}
|
||||
dropdownId={dropdownId}
|
||||
clickableComponent={
|
||||
<RecordTableColumnFooterAggregateValue
|
||||
<RecordTableColumnAggregateFooterValue
|
||||
aggregateLabel={aggregateLabel}
|
||||
aggregateValue={aggregateValue}
|
||||
dropdownId={dropdownId}
|
||||
@ -52,7 +52,7 @@ export const RecordTableColumnFooterWithDropdown = ({
|
||||
/>
|
||||
}
|
||||
dropdownComponents={
|
||||
<RecordTableColumnFooterDropdown
|
||||
<RecordTableColumnAggregateFooterDropdown
|
||||
column={column}
|
||||
dropdownId={dropdownId}
|
||||
/>
|
@ -32,13 +32,10 @@ export const useAggregateRecordsForRecordTableColumnFooter = (
|
||||
recordIndexViewFilterGroups,
|
||||
);
|
||||
|
||||
const viewFieldId = currentViewWithSavedFiltersAndSorts?.viewFields?.find(
|
||||
(viewField) => viewField.fieldMetadataId === fieldMetadataId,
|
||||
)?.id;
|
||||
|
||||
if (!viewFieldId) {
|
||||
throw new Error('ViewField not found');
|
||||
}
|
||||
const viewFieldId =
|
||||
currentViewWithSavedFiltersAndSorts?.viewFields?.find(
|
||||
(viewField) => viewField.fieldMetadataId === fieldMetadataId,
|
||||
)?.id ?? '';
|
||||
|
||||
const aggregateOperationForViewField = useRecoilValue(
|
||||
aggregateOperationForViewFieldState({ viewFieldId: viewFieldId }),
|
||||
|
@ -7,11 +7,13 @@ import { RecordTableHeaderCheckboxColumn } from '@/object-record/record-table/re
|
||||
import { RecordTableHeaderDragDropColumn } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderDragDropColumn';
|
||||
import { RecordTableHeaderLastColumn } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderLastColumn';
|
||||
|
||||
export const FIRST_TH_WIDTH = '9px';
|
||||
|
||||
const StyledTableHead = styled.thead`
|
||||
cursor: pointer;
|
||||
|
||||
th:nth-of-type(1) {
|
||||
width: 9px;
|
||||
width: ${FIRST_TH_WIDTH};
|
||||
left: 0;
|
||||
border-right-color: ${({ theme }) => theme.background.primary};
|
||||
}
|
||||
|
@ -53,7 +53,7 @@ export const RecordTableHeaderPlusButtonContent = () => {
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer withoutScrollWrapper>
|
||||
<DropdownMenuItemsContainer scrollable={false}>
|
||||
<UndecoratedLink
|
||||
fullWidth
|
||||
to={`/settings/objects/${getObjectSlug(objectMetadataItem)}`}
|
||||
|
@ -9,6 +9,7 @@ import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/s
|
||||
import { RecordGroupDefinitionType } from '@/object-record/record-group/types/RecordGroupDefinition';
|
||||
import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState';
|
||||
import { RecordTableTd } from '@/object-record/record-table/record-table-cell/components/RecordTableTd';
|
||||
import { RecordTableRecordGroupStickyEffect } from '@/object-record/record-table/record-table-section/components/RecordTableRecordGroupStickyEffect';
|
||||
import { isRecordGroupTableSectionToggledComponentState } from '@/object-record/record-table/record-table-section/states/isRecordGroupTableSectionToggledComponentState';
|
||||
import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector';
|
||||
import { useRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentFamilyStateV2';
|
||||
@ -107,6 +108,7 @@ export const RecordTableRecordGroupSection = () => {
|
||||
weight="medium"
|
||||
/>
|
||||
<StyledTotalRow>{recordIdsByGroup.length}</StyledTotalRow>
|
||||
<RecordTableRecordGroupStickyEffect />
|
||||
</StyledRecordGroupSection>
|
||||
<StyledEmptyTd colSpan={visibleColumns.length - 1} />
|
||||
<StyledEmptyTd />
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,48 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { useCurrentRecordGroupId } from '@/object-record/record-group/hooks/useCurrentRecordGroupId';
|
||||
import { isRecordTableScrolledLeftComponentState } from '@/object-record/record-table/states/isRecordTableScrolledLeftComponentState';
|
||||
import { scrollWrapperScrollLeftComponentState } from '@/ui/utilities/scroll/states/scrollWrapperScrollLeftComponentState';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
|
||||
|
||||
export const RecordTableRecordGroupStickyEffect = () => {
|
||||
const scrollLeft = useRecoilComponentValueV2(
|
||||
scrollWrapperScrollLeftComponentState,
|
||||
);
|
||||
|
||||
const setIsRecordTableScrolledLeft = useSetRecoilComponentStateV2(
|
||||
isRecordTableScrolledLeftComponentState,
|
||||
);
|
||||
|
||||
const currentRecordGroupId = useCurrentRecordGroupId();
|
||||
|
||||
useEffect(() => {
|
||||
setIsRecordTableScrolledLeft(scrollLeft === 0);
|
||||
if (scrollLeft > 0) {
|
||||
document
|
||||
.getElementById(
|
||||
`record-table-footer${currentRecordGroupId ? '-' + currentRecordGroupId : ''}`,
|
||||
)
|
||||
?.classList.add('first-columns-sticky');
|
||||
document
|
||||
.getElementById(
|
||||
`record-table-body${currentRecordGroupId ? '-' + currentRecordGroupId : ''}`,
|
||||
)
|
||||
?.classList.add('first-columns-sticky');
|
||||
} else {
|
||||
document
|
||||
.getElementById(
|
||||
`record-table-footer${currentRecordGroupId ? '-' + currentRecordGroupId : ''}`,
|
||||
)
|
||||
?.classList.remove('first-columns-sticky');
|
||||
document
|
||||
.getElementById(
|
||||
`record-table-body${currentRecordGroupId ? '-' + currentRecordGroupId : ''}`,
|
||||
)
|
||||
?.classList.remove('first-columns-sticky');
|
||||
}
|
||||
}, [currentRecordGroupId, scrollLeft, setIsRecordTableScrolledLeft]);
|
||||
|
||||
return <></>;
|
||||
};
|
@ -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}
|
||||
|
@ -8,7 +8,6 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/Drop
|
||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useMemo } from 'react';
|
||||
import {
|
||||
ColorSample,
|
||||
IconCheck,
|
||||
@ -21,7 +20,6 @@ import {
|
||||
MenuItem,
|
||||
MenuItemSelectColor,
|
||||
} from 'twenty-ui';
|
||||
import { v4 } from 'uuid';
|
||||
import { computeOptionValueFromLabel } from '~/pages/settings/data-model/utils/compute-option-value-from-label.utils';
|
||||
|
||||
type SettingsDataModelFieldSelectFormOptionRowProps = {
|
||||
@ -81,17 +79,14 @@ export const SettingsDataModelFieldSelectFormOptionRow = ({
|
||||
}: SettingsDataModelFieldSelectFormOptionRowProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const dropdownIds = useMemo(() => {
|
||||
const baseScopeId = `select-field-option-row-${v4()}`;
|
||||
return {
|
||||
color: `${baseScopeId}-color`,
|
||||
actions: `${baseScopeId}-actions`,
|
||||
};
|
||||
}, []);
|
||||
const SELECT_COLOR_DROPDOWN_ID = 'select-color-dropdown';
|
||||
const SELECT_ACTIONS_DROPDOWN_ID = 'select-actions-dropdown';
|
||||
|
||||
const { closeDropdown: closeColorDropdown } = useDropdown(dropdownIds.color);
|
||||
const { closeDropdown: closeColorDropdown } = useDropdown(
|
||||
SELECT_COLOR_DROPDOWN_ID,
|
||||
);
|
||||
const { closeDropdown: closeActionsDropdown } = useDropdown(
|
||||
dropdownIds.actions,
|
||||
SELECT_ACTIONS_DROPDOWN_ID,
|
||||
);
|
||||
|
||||
const handleInputEnter = () => {
|
||||
@ -120,28 +115,26 @@ export const SettingsDataModelFieldSelectFormOptionRow = ({
|
||||
/>
|
||||
</AdvancedSettingsWrapper>
|
||||
<Dropdown
|
||||
dropdownId={dropdownIds.color}
|
||||
dropdownId={SELECT_COLOR_DROPDOWN_ID}
|
||||
dropdownPlacement="bottom-start"
|
||||
dropdownHotkeyScope={{
|
||||
scope: dropdownIds.color,
|
||||
scope: SELECT_COLOR_DROPDOWN_ID,
|
||||
}}
|
||||
clickableComponent={<StyledColorSample colorName={option.color} />}
|
||||
dropdownComponents={
|
||||
<DropdownMenu>
|
||||
<DropdownMenuItemsContainer>
|
||||
{MAIN_COLOR_NAMES.map((colorName) => (
|
||||
<MenuItemSelectColor
|
||||
key={colorName}
|
||||
onClick={() => {
|
||||
onChange({ ...option, color: colorName });
|
||||
closeColorDropdown();
|
||||
}}
|
||||
color={colorName}
|
||||
selected={colorName === option.color}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownMenu>
|
||||
<DropdownMenuItemsContainer>
|
||||
{MAIN_COLOR_NAMES.map((colorName) => (
|
||||
<MenuItemSelectColor
|
||||
key={colorName}
|
||||
onClick={() => {
|
||||
onChange({ ...option, color: colorName });
|
||||
closeColorDropdown();
|
||||
}}
|
||||
color={colorName}
|
||||
selected={colorName === option.color}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
}
|
||||
/>
|
||||
<StyledOptionInput
|
||||
@ -165,10 +158,10 @@ export const SettingsDataModelFieldSelectFormOptionRow = ({
|
||||
autoSelectOnMount={isNewRow}
|
||||
/>
|
||||
<Dropdown
|
||||
dropdownId={dropdownIds.actions}
|
||||
dropdownId={SELECT_ACTIONS_DROPDOWN_ID}
|
||||
dropdownPlacement="right-start"
|
||||
dropdownHotkeyScope={{
|
||||
scope: dropdownIds.actions,
|
||||
scope: SELECT_ACTIONS_DROPDOWN_ID,
|
||||
}}
|
||||
clickableComponent={
|
||||
<StyledLightIconButton accent="tertiary" Icon={IconDotsVertical} />
|
||||
|
@ -1,9 +1,29 @@
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { DEFAULT_PHONE_CALLING_CODE } from '@/object-record/record-field/meta-types/input/components/PhonesFieldInput';
|
||||
import { FieldPhonesValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { getSettingsFieldTypeConfig } from '@/settings/data-model/utils/getSettingsFieldTypeConfig';
|
||||
import {
|
||||
CountryCode,
|
||||
getCountries,
|
||||
getCountryCallingCode,
|
||||
} from 'libphonenumber-js';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
import { stripSimpleQuotesFromString } from '~/utils/string/stripSimpleQuotesFromString';
|
||||
|
||||
const isStrCountryCodeGuard = (str: string): str is CountryCode => {
|
||||
return getCountries().includes(str as CountryCode);
|
||||
};
|
||||
|
||||
export const countryCodeToCallingCode = (countryCode: string): string => {
|
||||
if (!countryCode || !isStrCountryCodeGuard(countryCode)) {
|
||||
return `+${DEFAULT_PHONE_CALLING_CODE}`;
|
||||
}
|
||||
|
||||
const callingCode = getCountryCallingCode(countryCode);
|
||||
|
||||
return callingCode ? `+${callingCode}` : `+${DEFAULT_PHONE_CALLING_CODE}`;
|
||||
};
|
||||
|
||||
export const getPhonesFieldPreviewValue = ({
|
||||
fieldMetadataItem,
|
||||
}: {
|
||||
@ -26,8 +46,16 @@ export const getPhonesFieldPreviewValue = ({
|
||||
fieldMetadataItem.defaultValue?.primaryPhoneCountryCode,
|
||||
)
|
||||
: null;
|
||||
const primaryPhoneCallingCode =
|
||||
fieldMetadataItem.defaultValue?.primaryPhoneCallingCode &&
|
||||
fieldMetadataItem.defaultValue.primaryPhoneCallingCode !== ''
|
||||
? stripSimpleQuotesFromString(
|
||||
fieldMetadataItem.defaultValue?.primaryPhoneCallingCode,
|
||||
)
|
||||
: null;
|
||||
return {
|
||||
...placeholderDefaultValue,
|
||||
primaryPhoneCountryCode,
|
||||
primaryPhoneCallingCode,
|
||||
};
|
||||
};
|
||||
|
@ -16,7 +16,6 @@ import { SettingsSummaryCard } from '@/settings/components/SettingsSummaryCard';
|
||||
import { SettingsDataModelObjectTypeTag } from '@/settings/data-model/objects/components/SettingsDataModelObjectTypeTag';
|
||||
import { getObjectTypeLabel } from '@/settings/data-model/utils/getObjectTypeLabel';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
|
||||
@ -86,21 +85,20 @@ export const SettingsObjectSummaryCard = ({
|
||||
accent="tertiary"
|
||||
/>
|
||||
}
|
||||
dropdownMenuWidth={160}
|
||||
dropdownComponents={
|
||||
<DropdownMenu width="160px">
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItem
|
||||
text="Edit"
|
||||
LeftIcon={IconPencil}
|
||||
onClick={handleEdit}
|
||||
/>
|
||||
<MenuItem
|
||||
text="Deactivate"
|
||||
LeftIcon={IconArchive}
|
||||
onClick={handleDeactivate}
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownMenu>
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItem
|
||||
text="Edit"
|
||||
LeftIcon={IconPencil}
|
||||
onClick={handleEdit}
|
||||
/>
|
||||
<MenuItem
|
||||
text="Deactivate"
|
||||
LeftIcon={IconArchive}
|
||||
onClick={handleDeactivate}
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
}
|
||||
dropdownHotkeyScope={{
|
||||
scope: dropdownId,
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { SettingsSummaryCard } from '@/settings/components/SettingsSummaryCard';
|
||||
import { SettingsIntegrationDatabaseConnectionSyncStatus } from '@/settings/integrations/database-connection/components/SettingsIntegrationDatabaseConnectionSyncStatus';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import styled from '@emotion/styled';
|
||||
import {
|
||||
@ -64,18 +63,16 @@ export const SettingsIntegrationDatabaseConnectionSummaryCard = ({
|
||||
<LightIconButton Icon={IconDotsVertical} accent="tertiary" />
|
||||
}
|
||||
dropdownComponents={
|
||||
<DropdownMenu>
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItem
|
||||
LeftIcon={IconTrash}
|
||||
text="Remove"
|
||||
onClick={onRemove}
|
||||
/>
|
||||
<UndecoratedLink to="./edit">
|
||||
<MenuItem LeftIcon={IconPencil} text="Edit" />
|
||||
</UndecoratedLink>
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownMenu>
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItem
|
||||
LeftIcon={IconTrash}
|
||||
text="Remove"
|
||||
onClick={onRemove}
|
||||
/>
|
||||
<UndecoratedLink to="./edit">
|
||||
<MenuItem LeftIcon={IconPencil} text="Edit" />
|
||||
</UndecoratedLink>
|
||||
</DropdownMenuItemsContainer>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
|
@ -5,6 +5,7 @@ import { RoundedLink, THEME_COMMON } from 'twenty-ui';
|
||||
import { FieldPhonesValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||
import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList';
|
||||
|
||||
import { DEFAULT_PHONE_CALLING_CODE } from '@/object-record/record-field/meta-types/input/components/PhonesFieldInput';
|
||||
import { parsePhoneNumber } from 'libphonenumber-js';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { logError } from '~/utils/logError';
|
||||
@ -36,7 +37,10 @@ export const PhonesDisplay = ({ value, isFocused }: PhonesDisplayProps) => {
|
||||
value?.primaryPhoneNumber
|
||||
? {
|
||||
number: value.primaryPhoneNumber,
|
||||
callingCode: value.primaryPhoneCountryCode,
|
||||
callingCode:
|
||||
value.primaryPhoneCallingCode ||
|
||||
value.primaryPhoneCountryCode ||
|
||||
`+${DEFAULT_PHONE_CALLING_CODE}`,
|
||||
}
|
||||
: null,
|
||||
...parseAdditionalPhones(value?.additionalPhones),
|
||||
@ -50,11 +54,11 @@ export const PhonesDisplay = ({ value, isFocused }: PhonesDisplayProps) => {
|
||||
}),
|
||||
[
|
||||
value?.primaryPhoneNumber,
|
||||
value?.primaryPhoneCallingCode,
|
||||
value?.primaryPhoneCountryCode,
|
||||
value?.additionalPhones,
|
||||
],
|
||||
);
|
||||
|
||||
const parsePhoneNumberOrReturnInvalidValue = (number: string) => {
|
||||
try {
|
||||
return { parsedPhone: parsePhoneNumber(number) };
|
||||
|
@ -165,7 +165,7 @@ export const Select = <Value extends SelectValue>({
|
||||
<DropdownMenuSeparator />
|
||||
)}
|
||||
{!!callToActionButton && (
|
||||
<DropdownMenuItemsContainer hasMaxHeight withoutScrollWrapper>
|
||||
<DropdownMenuItemsContainer hasMaxHeight scrollable={false}>
|
||||
<MenuItem
|
||||
onClick={callToActionButton.onClick}
|
||||
LeftIcon={callToActionButton.Icon}
|
||||
|
@ -38,16 +38,16 @@ export const DropdownMenuItemsContainer = ({
|
||||
children,
|
||||
hasMaxHeight,
|
||||
className,
|
||||
withoutScrollWrapper,
|
||||
scrollable = true,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
hasMaxHeight?: boolean;
|
||||
className?: string;
|
||||
withoutScrollWrapper?: boolean;
|
||||
scrollable?: boolean;
|
||||
}) => {
|
||||
const id = useId();
|
||||
|
||||
return withoutScrollWrapper === true ? (
|
||||
return scrollable !== true ? (
|
||||
<StyledDropdownMenuItemsExternalContainer
|
||||
hasMaxHeight={hasMaxHeight}
|
||||
className={className}
|
||||
|
@ -1,9 +1,11 @@
|
||||
import { RecordActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter';
|
||||
import { RecordAgnosticActionsSetterEffect } from '@/action-menu/actions/record-agnostic-actions/components/RecordAgnosticActionsSetterEffect';
|
||||
import { ActionMenuConfirmationModals } from '@/action-menu/components/ActionMenuConfirmationModals';
|
||||
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
|
||||
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
|
||||
import { AuthModal } from '@/auth/components/AuthModal';
|
||||
import { CommandMenu } from '@/command-menu/components/CommandMenu';
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
|
||||
import { AppErrorBoundary } from '@/error-handler/components/AppErrorBoundary';
|
||||
import { KeyboardShortcutMenu } from '@/keyboard-shortcut-menu/components/KeyboardShortcutMenu';
|
||||
@ -75,6 +77,7 @@ export const DefaultLayout = () => {
|
||||
const theme = useTheme();
|
||||
const windowsWidth = useScreenSize().width;
|
||||
const showAuthModal = useShowAuthModal();
|
||||
const { toggleCommandMenu } = useCommandMenu();
|
||||
|
||||
const isWorkflowEnabled = useIsFeatureEnabled('IS_WORKFLOW_ENABLED');
|
||||
|
||||
@ -96,10 +99,17 @@ export const DefaultLayout = () => {
|
||||
<ActionMenuComponentInstanceContext.Provider
|
||||
value={{ instanceId: 'command-menu' }}
|
||||
>
|
||||
<RecordActionMenuEntriesSetter />
|
||||
{isWorkflowEnabled && <RecordAgnosticActionsSetterEffect />}
|
||||
<ActionMenuConfirmationModals />
|
||||
<CommandMenu />
|
||||
<ActionMenuContext.Provider
|
||||
value={{
|
||||
isInRightDrawer: false,
|
||||
onActionExecutedCallback: toggleCommandMenu,
|
||||
}}
|
||||
>
|
||||
<RecordActionMenuEntriesSetter />
|
||||
{isWorkflowEnabled && <RecordAgnosticActionsSetterEffect />}
|
||||
<ActionMenuConfirmationModals />
|
||||
<CommandMenu />
|
||||
</ActionMenuContext.Provider>
|
||||
</ActionMenuComponentInstanceContext.Provider>
|
||||
</ContextStoreComponentInstanceContext.Provider>
|
||||
<KeyboardShortcutMenu />
|
||||
|
@ -19,7 +19,6 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi
|
||||
import { isWorkflowSubObjectMetadata } from '@/object-metadata/utils/isWorkflowSubObjectMetadata';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { Dropdown } from '../../dropdown/components/Dropdown';
|
||||
import { DropdownMenu } from '../../dropdown/components/DropdownMenu';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
z-index: 1;
|
||||
@ -93,22 +92,20 @@ export const ShowPageAddButton = ({
|
||||
)
|
||||
}
|
||||
dropdownComponents={
|
||||
<DropdownMenu>
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItem
|
||||
onClick={() => handleSelect(CoreObjectNameSingular.Note)}
|
||||
accent="default"
|
||||
LeftIcon={IconNotes}
|
||||
text="Note"
|
||||
/>
|
||||
<MenuItem
|
||||
onClick={() => handleSelect(CoreObjectNameSingular.Task)}
|
||||
accent="default"
|
||||
LeftIcon={IconCheckbox}
|
||||
text="Task"
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownMenu>
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItem
|
||||
onClick={() => handleSelect(CoreObjectNameSingular.Note)}
|
||||
accent="default"
|
||||
LeftIcon={IconNotes}
|
||||
text="Note"
|
||||
/>
|
||||
<MenuItem
|
||||
onClick={() => handleSelect(CoreObjectNameSingular.Task)}
|
||||
accent="default"
|
||||
LeftIcon={IconCheckbox}
|
||||
text="Task"
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
}
|
||||
dropdownHotkeyScope={{
|
||||
scope: PageHotkeyScope.ShowPage,
|
||||
|
@ -7,13 +7,13 @@ import { SummaryCard } from '@/object-record/record-show/components/SummaryCard'
|
||||
import { RecordLayout } from '@/object-record/record-show/types/RecordLayout';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { RightDrawerFooter } from '@/ui/layout/right-drawer/components/RightDrawerFooter';
|
||||
import { ShowPageLeftContainer } from '@/ui/layout/show-page/components/ShowPageLeftContainer';
|
||||
import { SingleTabProps, TabList } from '@/ui/layout/tab/components/TabList';
|
||||
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { RightDrawerFooter } from '@/ui/layout/right-drawer/components/RightDrawerFooter';
|
||||
|
||||
const StyledShowPageRightContainer = styled.div<{ isMobile: boolean }>`
|
||||
display: flex;
|
||||
|
@ -19,6 +19,7 @@ export type ContextProviderName =
|
||||
| 'test'
|
||||
| 'showPageActivityContainer'
|
||||
| 'navigationDrawer'
|
||||
| 'aggregateFooterCell'
|
||||
| 'modalContent';
|
||||
|
||||
const createScrollWrapperContext = (id: string) =>
|
||||
@ -52,6 +53,8 @@ export const ShowPageActivityContainerScrollWrapperContext =
|
||||
export const NavigationDrawerScrollWrapperContext =
|
||||
createScrollWrapperContext('navigationDrawer');
|
||||
export const TestScrollWrapperContext = createScrollWrapperContext('test');
|
||||
export const AggregateFooterCellScrollWrapperContext =
|
||||
createScrollWrapperContext('aggregateFooterCell');
|
||||
export const ModalContentScrollWrapperContext =
|
||||
createScrollWrapperContext('modalContent');
|
||||
|
||||
@ -85,6 +88,8 @@ export const getContextByProviderName = (
|
||||
return ShowPageActivityContainerScrollWrapperContext;
|
||||
case 'navigationDrawer':
|
||||
return NavigationDrawerScrollWrapperContext;
|
||||
case 'aggregateFooterCell':
|
||||
return AggregateFooterCellScrollWrapperContext;
|
||||
case 'modalContent':
|
||||
return ModalContentScrollWrapperContext;
|
||||
default:
|
||||
|
@ -112,15 +112,13 @@ export const UpdateViewButtonGroup = ({
|
||||
/>
|
||||
}
|
||||
dropdownComponents={
|
||||
<>
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItem
|
||||
onClick={handleCreateViewClick}
|
||||
LeftIcon={IconPlus}
|
||||
text="Create view"
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItem
|
||||
onClick={handleCreateViewClick}
|
||||
LeftIcon={IconPlus}
|
||||
text="Create view"
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
|
@ -190,7 +190,7 @@ export const ViewPickerContentCreateMode = () => {
|
||||
)}
|
||||
</DropdownMenuItemsContainer>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer>
|
||||
<DropdownMenuItemsContainer scrollable={false}>
|
||||
<ViewPickerSaveButtonContainer>
|
||||
<ViewPickerCreateButton />
|
||||
</ViewPickerSaveButtonContainer>
|
||||
|
@ -89,7 +89,7 @@ export const ViewPickerContentEditMode = () => {
|
||||
</ViewPickerIconAndNameContainer>
|
||||
</DropdownMenuItemsContainer>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer>
|
||||
<DropdownMenuItemsContainer scrollable={false}>
|
||||
<ViewPickerSaveButtonContainer>
|
||||
<ViewPickerEditButton />
|
||||
</ViewPickerSaveButtonContainer>
|
||||
|
@ -97,7 +97,7 @@ export const ViewPickerListContent = () => {
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
<DropdownMenuSeparator />
|
||||
<StyledBoldDropdownMenuItemsContainer>
|
||||
<StyledBoldDropdownMenuItemsContainer scrollable={false}>
|
||||
<MenuItem
|
||||
onClick={handleAddViewButtonClick}
|
||||
LeftIcon={IconPlus}
|
||||
|
@ -19461,7 +19461,8 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
|
||||
"defaultValue": {
|
||||
"additionalPhones": null,
|
||||
"primaryPhoneNumber": "''",
|
||||
"primaryPhoneCountryCode": "''"
|
||||
"primaryPhoneCountryCode": "''",
|
||||
"primaryPhoneCallingCode": "''"
|
||||
},
|
||||
"options": null,
|
||||
"isLabelSyncedWithName": false,
|
||||
@ -19740,7 +19741,8 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =
|
||||
{
|
||||
"additionalPhones": {},
|
||||
"primaryPhoneNumber": "",
|
||||
"primaryPhoneCountryCode": ""
|
||||
"primaryPhoneCountryCode": "",
|
||||
"primaryPhoneCallingCode": ""
|
||||
}
|
||||
],
|
||||
"options": null,
|
||||
|
@ -22,6 +22,7 @@ export const mockedEmptyPersonData = {
|
||||
xUrl: null,
|
||||
_activityCount: null,
|
||||
company: null,
|
||||
deletedAt: null,
|
||||
__typename: 'Person',
|
||||
};
|
||||
|
||||
@ -43,11 +44,13 @@ export const peopleQueryResult: { people: RecordGqlConnection } = {
|
||||
cursor: 'WzAsICJkYTNjMmM0Yi1kYTAxLTRiODEtOTczNC0yMjYwNjllYjRjZDAiXQ==',
|
||||
node: {
|
||||
__typename: 'Person',
|
||||
deletedAt: null,
|
||||
createdAt: '2024-08-02T09:52:46.814Z',
|
||||
city: 'ASd',
|
||||
phones: {
|
||||
primaryPhoneNumber: '781234562',
|
||||
primaryPhoneCountryCode: '+33',
|
||||
primaryPhoneCountryCode: 'FR',
|
||||
primaryPhoneCallingCode: '+33',
|
||||
},
|
||||
id: 'da3c2c4b-da01-4b81-9734-226069eb4cd0',
|
||||
jobTitle: '',
|
||||
@ -173,11 +176,13 @@ export const peopleQueryResult: { people: RecordGqlConnection } = {
|
||||
cursor: 'WzEsICIyMDIwMjAyMC0xYzBlLTQ5NGMtYTFiNi04NWIxYzZmZWZhYTUiXQ==',
|
||||
node: {
|
||||
__typename: 'Person',
|
||||
deletedAt: null,
|
||||
createdAt: '2024-08-01T09:50:00.000Z',
|
||||
city: 'Seattle',
|
||||
phones: {
|
||||
primaryPhoneNumber: '781234562',
|
||||
primaryPhoneCountryCode: '+33',
|
||||
primaryPhoneCountryCode: 'FR',
|
||||
primaryPhoneCallingCode: '+33',
|
||||
},
|
||||
id: '20202020-1c0e-494c-a1b6-85b1c6fefaa5',
|
||||
jobTitle: '',
|
||||
@ -303,11 +308,13 @@ export const peopleQueryResult: { people: RecordGqlConnection } = {
|
||||
cursor: 'WzIsICIyMDIwMjAyMC1hYzczLTQ3OTctODI0ZS04N2ExZjVhZWE5ZTAiXQ==',
|
||||
node: {
|
||||
__typename: 'Person',
|
||||
deletedAt: null,
|
||||
createdAt: '2024-08-02T09:48:36.193Z',
|
||||
city: 'Los Angeles',
|
||||
phones: {
|
||||
primaryPhoneNumber: '781234576',
|
||||
primaryPhoneCountryCode: '+33',
|
||||
primaryPhoneCountryCode: 'FR',
|
||||
primaryPhoneCallingCode: '+33',
|
||||
},
|
||||
id: '20202020-ac73-4797-824e-87a1f5aea9e0',
|
||||
jobTitle: '',
|
||||
@ -402,11 +409,13 @@ export const peopleQueryResult: { people: RecordGqlConnection } = {
|
||||
cursor: 'WzMsICIyMDIwMjAyMC1mNTE3LTQyZmQtODBhZS0xNDE3M2IzYjcwYWUiXQ==',
|
||||
node: {
|
||||
__typename: 'Person',
|
||||
deletedAt: null,
|
||||
createdAt: '2024-08-02T09:48:36.193Z',
|
||||
city: 'Seattle',
|
||||
phones: {
|
||||
primaryPhoneNumber: '781234545',
|
||||
primaryPhoneCountryCode: '+33',
|
||||
primaryPhoneCountryCode: 'FR',
|
||||
primaryPhoneCallingCode: '+33',
|
||||
},
|
||||
id: '20202020-f517-42fd-80ae-14173b3b70ae',
|
||||
jobTitle: '',
|
||||
@ -501,11 +510,13 @@ export const peopleQueryResult: { people: RecordGqlConnection } = {
|
||||
cursor: 'WzQsICIyMDIwMjAyMC1lZWUxLTQ2OTAtYWQyYy04NjE5ZTViNTZhMmUiXQ==',
|
||||
node: {
|
||||
__typename: 'Person',
|
||||
deletedAt: null,
|
||||
createdAt: '2024-08-02T09:48:36.193Z',
|
||||
city: 'Los Angeles',
|
||||
phones: {
|
||||
primaryPhoneNumber: '781234587',
|
||||
primaryPhoneCountryCode: '+33',
|
||||
primaryPhoneCountryCode: 'FR',
|
||||
primaryPhoneCallingCode: '+33',
|
||||
},
|
||||
id: '20202020-eee1-4690-ad2c-8619e5b56a2e',
|
||||
jobTitle: '',
|
||||
@ -600,11 +611,13 @@ export const peopleQueryResult: { people: RecordGqlConnection } = {
|
||||
cursor: 'WzUsICIyMDIwMjAyMC02Nzg0LTQ0NDktYWZkZi1kYzYyY2I4NzAyZjIiXQ==',
|
||||
node: {
|
||||
__typename: 'Person',
|
||||
deletedAt: null,
|
||||
createdAt: '2024-08-02T09:48:36.193Z',
|
||||
city: 'Seattle',
|
||||
phones: {
|
||||
primaryPhoneNumber: '781234599',
|
||||
primaryPhoneCountryCode: '+33',
|
||||
primaryPhoneCountryCode: 'FR',
|
||||
primaryPhoneCallingCode: '+33',
|
||||
},
|
||||
id: '20202020-6784-4449-afdf-dc62cb8702f2',
|
||||
jobTitle: '',
|
||||
@ -699,11 +712,13 @@ export const peopleQueryResult: { people: RecordGqlConnection } = {
|
||||
cursor: 'WzYsICIyMDIwMjAyMC00OTBmLTQ0NjYtODM5MS03MzNjZmQ2NmEwYzgiXQ==',
|
||||
node: {
|
||||
__typename: 'Person',
|
||||
deletedAt: null,
|
||||
createdAt: '2024-08-02T09:48:36.193Z',
|
||||
city: 'New York',
|
||||
phones: {
|
||||
primaryPhoneNumber: '781234572',
|
||||
primaryPhoneCountryCode: '+33',
|
||||
primaryPhoneCountryCode: 'FR',
|
||||
primaryPhoneCallingCode: '+33',
|
||||
},
|
||||
id: '20202020-490f-4466-8391-733cfd66a0c8',
|
||||
jobTitle: '',
|
||||
@ -798,11 +813,13 @@ export const peopleQueryResult: { people: RecordGqlConnection } = {
|
||||
cursor: 'WzcsICIyMDIwMjAyMC04MGYxLTRkZmYtYjU3MC1hNzQ5NDI1MjhkZTMiXQ==',
|
||||
node: {
|
||||
__typename: 'Person',
|
||||
deletedAt: null,
|
||||
createdAt: '2024-08-02T09:48:36.193Z',
|
||||
city: 'Seattle',
|
||||
phones: {
|
||||
primaryPhoneNumber: '781234582',
|
||||
primaryPhoneCountryCode: '+33',
|
||||
primaryPhoneCountryCode: 'FR',
|
||||
primaryPhoneCallingCode: '+33',
|
||||
},
|
||||
id: '20202020-80f1-4dff-b570-a74942528de3',
|
||||
jobTitle: '',
|
||||
@ -897,11 +914,13 @@ export const peopleQueryResult: { people: RecordGqlConnection } = {
|
||||
cursor: 'WzgsICIyMDIwMjAyMC0zMzhiLTQ2ZGYtODgxMS1mYTA4YzdkMTlkMzUiXQ==',
|
||||
node: {
|
||||
__typename: 'Person',
|
||||
deletedAt: null,
|
||||
createdAt: '2024-08-02T09:48:36.193Z',
|
||||
city: 'New York',
|
||||
phones: {
|
||||
primaryPhoneNumber: '781234569',
|
||||
primaryPhoneCountryCode: '+33',
|
||||
primaryPhoneCountryCode: 'FR',
|
||||
primaryPhoneCallingCode: '+33',
|
||||
},
|
||||
id: '20202020-338b-46df-8811-fa08c7d19d35',
|
||||
jobTitle: '',
|
||||
@ -996,11 +1015,13 @@ export const peopleQueryResult: { people: RecordGqlConnection } = {
|
||||
cursor: 'WzksICIyMDIwMjAyMC02NGFkLTRiMGUtYmJmZC1lOWZkNzk1YjcwMTYiXQ==',
|
||||
node: {
|
||||
__typename: 'Person',
|
||||
deletedAt: null,
|
||||
createdAt: '2024-08-02T09:48:36.193Z',
|
||||
city: 'San Francisco',
|
||||
phones: {
|
||||
primaryPhoneNumber: '781234962',
|
||||
primaryPhoneCountryCode: '+33',
|
||||
primaryPhoneCountryCode: 'FR',
|
||||
primaryPhoneCallingCode: '+33',
|
||||
},
|
||||
id: '20202020-64ad-4b0e-bbfd-e9fd795b7016',
|
||||
jobTitle: '',
|
||||
@ -1095,11 +1116,13 @@ export const peopleQueryResult: { people: RecordGqlConnection } = {
|
||||
cursor: 'WzEwLCAiMjAyMDIwMjAtNWQ1NC00MWI3LWJhMzYtZjBkMjBlMTQxN2FlIl0=',
|
||||
node: {
|
||||
__typename: 'Person',
|
||||
deletedAt: null,
|
||||
createdAt: '2024-08-02T09:48:36.193Z',
|
||||
city: 'New York',
|
||||
phones: {
|
||||
primaryPhoneNumber: '781234502',
|
||||
primaryPhoneCountryCode: '+33',
|
||||
primaryPhoneCountryCode: 'FR',
|
||||
primaryPhoneCallingCode: '+33',
|
||||
},
|
||||
id: '20202020-5d54-41b7-ba36-f0d20e1417ae',
|
||||
jobTitle: '',
|
||||
@ -1194,11 +1217,13 @@ export const peopleQueryResult: { people: RecordGqlConnection } = {
|
||||
cursor: 'WzExLCAiMjAyMDIwMjAtNjIzZC00MWZlLTkyZTctZGQ0NWI3YzU2OGUxIl0=',
|
||||
node: {
|
||||
__typename: 'Person',
|
||||
deletedAt: null,
|
||||
createdAt: '2024-08-02T09:48:36.193Z',
|
||||
city: 'Los Angeles',
|
||||
phones: {
|
||||
primaryPhoneNumber: '781234563',
|
||||
primaryPhoneCountryCode: '+33',
|
||||
primaryPhoneCountryCode: 'FR',
|
||||
primaryPhoneCallingCode: '+33',
|
||||
},
|
||||
id: '20202020-623d-41fe-92e7-dd45b7c568e1',
|
||||
jobTitle: '',
|
||||
@ -1293,11 +1318,13 @@ export const peopleQueryResult: { people: RecordGqlConnection } = {
|
||||
cursor: 'WzEyLCAiMjAyMDIwMjAtMmQ0MC00ZTQ5LThkZjQtOWM2YTA0OTE5MGVmIl0=',
|
||||
node: {
|
||||
__typename: 'Person',
|
||||
deletedAt: null,
|
||||
createdAt: '2024-08-02T09:48:36.193Z',
|
||||
city: 'Seattle',
|
||||
phones: {
|
||||
primaryPhoneNumber: '781234542',
|
||||
primaryPhoneCountryCode: '+33',
|
||||
primaryPhoneCountryCode: 'FR',
|
||||
primaryPhoneCallingCode: '+33',
|
||||
},
|
||||
id: '20202020-2d40-4e49-8df4-9c6a049190ef',
|
||||
jobTitle: '',
|
||||
@ -1392,11 +1419,13 @@ export const peopleQueryResult: { people: RecordGqlConnection } = {
|
||||
cursor: 'WzEzLCAiMjAyMDIwMjAtMmQ0MC00ZTQ5LThkZjQtOWM2YTA0OTE5MGRmIl0=',
|
||||
node: {
|
||||
__typename: 'Person',
|
||||
deletedAt: null,
|
||||
createdAt: '2024-08-02T09:48:36.193Z',
|
||||
city: 'Seattle',
|
||||
phones: {
|
||||
primaryPhoneNumber: '782234562',
|
||||
primaryPhoneCountryCode: '+33',
|
||||
primaryPhoneCountryCode: 'FR',
|
||||
primaryPhoneCallingCode: '+33',
|
||||
},
|
||||
id: '20202020-2d40-4e49-8df4-9c6a049190df',
|
||||
jobTitle: '',
|
||||
@ -1491,11 +1520,13 @@ export const peopleQueryResult: { people: RecordGqlConnection } = {
|
||||
cursor: 'WzE0LCAiMjAyMDIwMjAtMmQ0MC00ZTQ5LThkZjQtOWM2YTA0OTE5MWRlIl0=',
|
||||
node: {
|
||||
__typename: 'Person',
|
||||
deletedAt: null,
|
||||
createdAt: '2024-08-02T09:48:36.193Z',
|
||||
city: 'Seattle',
|
||||
phones: {
|
||||
primaryPhoneNumber: '781274562',
|
||||
primaryPhoneCountryCode: '+33',
|
||||
primaryPhoneCountryCode: 'FR',
|
||||
primaryPhoneCallingCode: '+33',
|
||||
},
|
||||
id: '20202020-2d40-4e49-8df4-9c6a049191de',
|
||||
jobTitle: '',
|
||||
@ -1590,11 +1621,13 @@ export const peopleQueryResult: { people: RecordGqlConnection } = {
|
||||
cursor: 'WzE1LCAiMjAyMDIwMjAtMmQ0MC00ZTQ5LThkZjQtOWM2YTA0OTE5MWRmIl0=',
|
||||
node: {
|
||||
__typename: 'Person',
|
||||
deletedAt: null,
|
||||
createdAt: '2024-08-02T09:48:36.193Z',
|
||||
city: 'Seattle',
|
||||
phones: {
|
||||
primaryPhoneNumber: '781239562',
|
||||
primaryPhoneCountryCode: '+33',
|
||||
primaryPhoneCountryCode: 'FR',
|
||||
primaryPhoneCallingCode: '+33',
|
||||
},
|
||||
id: '20202020-2d40-4e49-8df4-9c6a049191df',
|
||||
jobTitle: '',
|
||||
|
@ -1,5 +1,3 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { Option } from 'nest-commander';
|
||||
import { Repository } from 'typeorm';
|
||||
@ -20,11 +18,8 @@ export type ActiveWorkspacesCommandOptions = BaseCommandOptions & {
|
||||
export abstract class ActiveWorkspacesCommandRunner extends BaseCommandRunner {
|
||||
private workspaceIds: string[] = [];
|
||||
|
||||
protected readonly logger: Logger;
|
||||
|
||||
constructor(protected readonly workspaceRepository: Repository<Workspace>) {
|
||||
super();
|
||||
this.logger = new Logger(this.constructor.name);
|
||||
}
|
||||
|
||||
@Option({
|
||||
|
@ -3,6 +3,7 @@ import { Logger } from '@nestjs/common';
|
||||
import chalk from 'chalk';
|
||||
import { CommandRunner, Option } from 'nest-commander';
|
||||
|
||||
import { CommandLogger } from './logger';
|
||||
export type BaseCommandOptions = {
|
||||
workspaceId?: string;
|
||||
dryRun?: boolean;
|
||||
@ -10,11 +11,13 @@ export type BaseCommandOptions = {
|
||||
};
|
||||
|
||||
export abstract class BaseCommandRunner extends CommandRunner {
|
||||
protected readonly logger: Logger;
|
||||
|
||||
protected logger: CommandLogger | Logger;
|
||||
constructor() {
|
||||
super();
|
||||
this.logger = new Logger(this.constructor.name);
|
||||
this.logger = new CommandLogger({
|
||||
verbose: false,
|
||||
constructorName: this.constructor.name,
|
||||
});
|
||||
}
|
||||
|
||||
@Option({
|
||||
@ -27,10 +30,11 @@ export abstract class BaseCommandRunner extends CommandRunner {
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: '--verbose',
|
||||
flags: '-v, --verbose',
|
||||
description: 'Verbose output',
|
||||
required: false,
|
||||
})
|
||||
parseVerbose() {
|
||||
parseVerbose(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -38,6 +42,13 @@ export abstract class BaseCommandRunner extends CommandRunner {
|
||||
passedParams: string[],
|
||||
options: BaseCommandOptions,
|
||||
): Promise<void> {
|
||||
if (options.verbose) {
|
||||
this.logger = new CommandLogger({
|
||||
verbose: true,
|
||||
constructorName: this.constructor.name,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await this.executeBaseCommand(passedParams, options);
|
||||
} catch (error) {
|
||||
|
@ -52,8 +52,8 @@ import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/worksp
|
||||
UpgradeTo0_32CommandModule,
|
||||
UpgradeTo0_33CommandModule,
|
||||
UpgradeTo0_34CommandModule,
|
||||
FeatureFlagModule,
|
||||
UpgradeTo0_40CommandModule,
|
||||
FeatureFlagModule,
|
||||
],
|
||||
providers: [
|
||||
DataSeedWorkspaceCommand,
|
||||
|
46
packages/twenty-server/src/database/commands/logger.ts
Normal file
46
packages/twenty-server/src/database/commands/logger.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
|
||||
interface CommandLoggerOptions {
|
||||
verbose?: boolean;
|
||||
constructorName: string;
|
||||
}
|
||||
|
||||
export class CommandLogger {
|
||||
private logger: Logger;
|
||||
private verbose: boolean;
|
||||
|
||||
constructor(options: CommandLoggerOptions) {
|
||||
this.logger = new Logger(options.constructorName);
|
||||
this.verbose = options.verbose ?? true;
|
||||
}
|
||||
|
||||
log(message: string, context?: string) {
|
||||
if (this.verbose) {
|
||||
this.logger.log(message, context);
|
||||
}
|
||||
}
|
||||
|
||||
error(message: string, stack?: string, context?: string) {
|
||||
if (this.verbose) {
|
||||
this.logger.error(message, stack, context);
|
||||
}
|
||||
}
|
||||
|
||||
warn(message: string, context?: string) {
|
||||
if (this.verbose) {
|
||||
this.logger.warn(message, context);
|
||||
}
|
||||
}
|
||||
|
||||
debug(message: string, context?: string) {
|
||||
if (this.verbose) {
|
||||
this.logger.debug(message, context);
|
||||
}
|
||||
}
|
||||
|
||||
verboseLog(message: string, context?: string) {
|
||||
if (this.verbose) {
|
||||
this.logger.verbose(message, context);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,144 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { Command } from 'nest-commander';
|
||||
import { Repository } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import {
|
||||
ActiveWorkspacesCommandOptions,
|
||||
ActiveWorkspacesCommandRunner,
|
||||
} from 'src/database/commands/active-workspaces.command';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import {
|
||||
FieldMetadataEntity,
|
||||
FieldMetadataType,
|
||||
} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service';
|
||||
import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util';
|
||||
import {
|
||||
WorkspaceMigrationColumnActionType,
|
||||
WorkspaceMigrationTableAction,
|
||||
WorkspaceMigrationTableActionType,
|
||||
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
|
||||
import { WorkspaceMigrationFactory } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.factory';
|
||||
import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service';
|
||||
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
||||
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service';
|
||||
import { isDefined } from 'src/utils/is-defined';
|
||||
|
||||
@Command({
|
||||
name: 'upgrade-0.40:phone-calling-code-create-column',
|
||||
description: 'Create the callingCode column',
|
||||
})
|
||||
export class PhoneCallingCodeCreateColumnCommand extends ActiveWorkspacesCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(Workspace, 'core')
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
@InjectRepository(FieldMetadataEntity, 'metadata')
|
||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
||||
@InjectRepository(ObjectMetadataEntity, 'metadata')
|
||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||
private readonly workspaceMigrationService: WorkspaceMigrationService,
|
||||
private readonly workspaceMigrationFactory: WorkspaceMigrationFactory,
|
||||
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
|
||||
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
|
||||
) {
|
||||
super(workspaceRepository);
|
||||
}
|
||||
|
||||
async executeActiveWorkspacesCommand(
|
||||
_passedParam: string[],
|
||||
options: ActiveWorkspacesCommandOptions,
|
||||
workspaceIds: string[],
|
||||
): Promise<void> {
|
||||
this.logger.log(
|
||||
'Running command to add calling code and change country code with default one',
|
||||
);
|
||||
|
||||
this.logger.log(`Part 1 - Workspace`);
|
||||
let workspaceIterator = 1;
|
||||
|
||||
for (const workspaceId of workspaceIds) {
|
||||
this.logger.log(
|
||||
`Running command for workspace ${workspaceId} ${workspaceIterator}/${workspaceIds.length}`,
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`P1 Step 1 - let's find all the fieldsMetadata that have the PHONES type, and extract the objectMetadataId`,
|
||||
);
|
||||
|
||||
try {
|
||||
const phonesFieldMetadata = await this.fieldMetadataRepository.find({
|
||||
where: {
|
||||
workspaceId,
|
||||
type: FieldMetadataType.PHONES,
|
||||
},
|
||||
relations: ['object'],
|
||||
});
|
||||
|
||||
for (const phoneFieldMetadata of phonesFieldMetadata) {
|
||||
if (
|
||||
isDefined(phoneFieldMetadata?.name && phoneFieldMetadata.object)
|
||||
) {
|
||||
this.logger.log(
|
||||
`P1 Step 1 - Let's find the "nameSingular" of this objectMetadata: ${phoneFieldMetadata.object.nameSingular || 'not found'}`,
|
||||
);
|
||||
|
||||
if (!phoneFieldMetadata.object?.nameSingular) continue;
|
||||
|
||||
this.logger.log(
|
||||
`P1 Step 1 - Create migration for field ${phoneFieldMetadata.name}`,
|
||||
);
|
||||
|
||||
if (options.dryRun === true) {
|
||||
continue;
|
||||
}
|
||||
await this.workspaceMigrationService.createCustomMigration(
|
||||
generateMigrationName(
|
||||
`create-${phoneFieldMetadata.object.nameSingular}PrimaryPhoneCallingCode-for-field-${phoneFieldMetadata.name}`,
|
||||
),
|
||||
workspaceId,
|
||||
[
|
||||
{
|
||||
name: computeObjectTargetTable(phoneFieldMetadata.object),
|
||||
action: WorkspaceMigrationTableActionType.ALTER,
|
||||
columns: this.workspaceMigrationFactory.createColumnActions(
|
||||
WorkspaceMigrationColumnActionType.CREATE,
|
||||
{
|
||||
id: v4(),
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: `${phoneFieldMetadata.name}PrimaryPhoneCallingCode`,
|
||||
label: `${phoneFieldMetadata.name}PrimaryPhoneCallingCode`,
|
||||
objectMetadataId: phoneFieldMetadata.object.id,
|
||||
workspaceId: workspaceId,
|
||||
isNullable: true,
|
||||
defaultValue: "''",
|
||||
} satisfies Partial<FieldMetadataEntity>,
|
||||
),
|
||||
} satisfies WorkspaceMigrationTableAction,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`P1 Step 1 - RUN migration to create callingCodes for ${workspaceId.slice(0, 5)}`,
|
||||
);
|
||||
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
await this.workspaceMetadataVersionService.incrementMetadataVersion(
|
||||
workspaceId,
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(`Error in workspace ${workspaceId} : ${error}`);
|
||||
}
|
||||
workspaceIterator++;
|
||||
}
|
||||
|
||||
this.logger.log(chalk.green(`Command completed!`));
|
||||
}
|
||||
}
|
@ -0,0 +1,302 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { getCountries, getCountryCallingCode } from 'libphonenumber-js';
|
||||
import { Command } from 'nest-commander';
|
||||
import { Repository } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import {
|
||||
ActiveWorkspacesCommandOptions,
|
||||
ActiveWorkspacesCommandRunner,
|
||||
} from 'src/database/commands/active-workspaces.command';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import {
|
||||
FieldMetadataEntity,
|
||||
FieldMetadataType,
|
||||
} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service';
|
||||
import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util';
|
||||
import {
|
||||
WorkspaceMigrationColumnActionType,
|
||||
WorkspaceMigrationTableAction,
|
||||
WorkspaceMigrationTableActionType,
|
||||
} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity';
|
||||
import { WorkspaceMigrationFactory } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.factory';
|
||||
import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
||||
import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service';
|
||||
import { isDefined } from 'src/utils/is-defined';
|
||||
|
||||
const callingCodeToCountryCode = (callingCode: string): string => {
|
||||
if (!callingCode) {
|
||||
return '';
|
||||
}
|
||||
let callingCodeSanitized = callingCode;
|
||||
|
||||
if (callingCode.startsWith('+')) {
|
||||
callingCodeSanitized = callingCode.slice(1);
|
||||
}
|
||||
|
||||
return (
|
||||
getCountries().find(
|
||||
(countryCode) =>
|
||||
getCountryCallingCode(countryCode) === callingCodeSanitized,
|
||||
) || ''
|
||||
);
|
||||
};
|
||||
|
||||
const isCallingCode = (callingCode: string): boolean => {
|
||||
return callingCodeToCountryCode(callingCode) !== '';
|
||||
};
|
||||
|
||||
@Command({
|
||||
name: 'upgrade-0.40:phone-calling-code-migrate-data',
|
||||
description: 'Add calling code and change country code with default one',
|
||||
})
|
||||
export class PhoneCallingCodeMigrateDataCommand extends ActiveWorkspacesCommandRunner {
|
||||
constructor(
|
||||
@InjectRepository(Workspace, 'core')
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
@InjectRepository(FieldMetadataEntity, 'metadata')
|
||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
||||
@InjectRepository(ObjectMetadataEntity, 'metadata')
|
||||
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
private readonly workspaceMigrationService: WorkspaceMigrationService,
|
||||
private readonly workspaceMigrationFactory: WorkspaceMigrationFactory,
|
||||
private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService,
|
||||
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
|
||||
) {
|
||||
super(workspaceRepository);
|
||||
}
|
||||
|
||||
async executeActiveWorkspacesCommand(
|
||||
_passedParam: string[],
|
||||
options: ActiveWorkspacesCommandOptions,
|
||||
workspaceIds: string[],
|
||||
): Promise<void> {
|
||||
this.logger.log(
|
||||
'Running command to add calling code and change country code with default one',
|
||||
);
|
||||
|
||||
this.logger.log(`Part 1 - Workspace`);
|
||||
|
||||
let workspaceIterator = 1;
|
||||
|
||||
for (const workspaceId of workspaceIds) {
|
||||
this.logger.log(
|
||||
`Running command for workspace ${workspaceId} ${workspaceIterator}/${workspaceIds.length}`,
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`P1 Step 1 - let's find all the fieldsMetadata that have the PHONES type, and extract the objectMetadataId`,
|
||||
);
|
||||
|
||||
try {
|
||||
const phonesFieldMetadata = await this.fieldMetadataRepository.find({
|
||||
where: {
|
||||
workspaceId,
|
||||
type: FieldMetadataType.PHONES,
|
||||
},
|
||||
relations: ['object'],
|
||||
});
|
||||
|
||||
for (const phoneFieldMetadata of phonesFieldMetadata) {
|
||||
if (
|
||||
isDefined(phoneFieldMetadata?.name) &&
|
||||
isDefined(phoneFieldMetadata.object)
|
||||
) {
|
||||
this.logger.log(
|
||||
`P1 Step 1 - Let's find the "nameSingular" of this objectMetadata: ${phoneFieldMetadata.object.nameSingular || 'not found'}`,
|
||||
);
|
||||
|
||||
if (!phoneFieldMetadata.object?.nameSingular) continue;
|
||||
|
||||
this.logger.log(
|
||||
`P1 Step 1 - Create migration for field ${phoneFieldMetadata.name}`,
|
||||
);
|
||||
if (options.dryRun === false) {
|
||||
await this.workspaceMigrationService.createCustomMigration(
|
||||
generateMigrationName(
|
||||
`create-${phoneFieldMetadata.object.nameSingular}PrimaryPhoneCallingCode-for-field-${phoneFieldMetadata.name}`,
|
||||
),
|
||||
workspaceId,
|
||||
[
|
||||
{
|
||||
name: computeObjectTargetTable(phoneFieldMetadata.object),
|
||||
action: WorkspaceMigrationTableActionType.ALTER,
|
||||
columns: this.workspaceMigrationFactory.createColumnActions(
|
||||
WorkspaceMigrationColumnActionType.CREATE,
|
||||
{
|
||||
id: v4(),
|
||||
type: FieldMetadataType.TEXT,
|
||||
name: `${phoneFieldMetadata.name}PrimaryPhoneCallingCode`,
|
||||
label: `${phoneFieldMetadata.name}PrimaryPhoneCallingCode`,
|
||||
objectMetadataId: phoneFieldMetadata.object.id,
|
||||
workspaceId: workspaceId,
|
||||
isNullable: true,
|
||||
defaultValue: "''",
|
||||
},
|
||||
),
|
||||
} satisfies WorkspaceMigrationTableAction,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`P1 Step 1 - RUN migration to create callingCodes for ${workspaceId.slice(0, 5)}`,
|
||||
);
|
||||
await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
await this.workspaceMetadataVersionService.incrementMetadataVersion(
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`P1 Step 2 - Migrations for callingCode must be first. Now can use twentyORMGlobalManager to update countryCode`,
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`P1 Step 3 (same time) - update CountryCode to letters: +33 => FR || +1 => US (if mulitple, first one)`,
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`P1 Step 4 (same time) - update all additioanl phones to add a country code following the same logic`,
|
||||
);
|
||||
|
||||
for (const phoneFieldMetadata of phonesFieldMetadata) {
|
||||
this.logger.log(`P1 Step 2 - for ${phoneFieldMetadata.name}`);
|
||||
if (
|
||||
isDefined(phoneFieldMetadata) &&
|
||||
isDefined(phoneFieldMetadata.name)
|
||||
) {
|
||||
const [objectMetadata] = await this.objectMetadataRepository.find({
|
||||
where: {
|
||||
id: phoneFieldMetadata?.objectMetadataId,
|
||||
},
|
||||
});
|
||||
|
||||
const repository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
objectMetadata.nameSingular,
|
||||
);
|
||||
const records = await repository.find();
|
||||
|
||||
for (const record of records) {
|
||||
if (
|
||||
record?.[phoneFieldMetadata.name]?.primaryPhoneCountryCode &&
|
||||
isCallingCode(
|
||||
record[phoneFieldMetadata.name].primaryPhoneCountryCode,
|
||||
)
|
||||
) {
|
||||
let additionalPhones = null;
|
||||
|
||||
if (record[phoneFieldMetadata.name].additionalPhones) {
|
||||
additionalPhones = record[
|
||||
phoneFieldMetadata.name
|
||||
].additionalPhones.map((phone) => {
|
||||
return {
|
||||
...phone,
|
||||
countryCode: callingCodeToCountryCode(phone.callingCode),
|
||||
};
|
||||
});
|
||||
}
|
||||
if (options.dryRun === false) {
|
||||
await repository.update(record.id, {
|
||||
[`${phoneFieldMetadata.name}PrimaryPhoneCallingCode`]:
|
||||
record[phoneFieldMetadata.name].primaryPhoneCountryCode,
|
||||
[`${phoneFieldMetadata.name}PrimaryPhoneCountryCode`]:
|
||||
callingCodeToCountryCode(
|
||||
record[phoneFieldMetadata.name].primaryPhoneCountryCode,
|
||||
),
|
||||
[`${phoneFieldMetadata.name}AdditionalPhones`]:
|
||||
additionalPhones,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`Error in workspace ${workspaceId} : ${error}`);
|
||||
}
|
||||
workspaceIterator++;
|
||||
}
|
||||
|
||||
this.logger.log(`
|
||||
|
||||
Part 2 - FieldMetadata`);
|
||||
|
||||
workspaceIterator = 1;
|
||||
for (const workspaceId of workspaceIds) {
|
||||
this.logger.log(
|
||||
`Running command for workspace ${workspaceId} ${workspaceIterator}/${workspaceIds.length}`,
|
||||
);
|
||||
|
||||
this.logger.log(
|
||||
`P2 Step 1 - let's find all the fieldsMetadata that have the PHONES type, and extract the objectMetadataId`,
|
||||
);
|
||||
|
||||
try {
|
||||
const phonesFieldMetadata = await this.fieldMetadataRepository.find({
|
||||
where: {
|
||||
workspaceId,
|
||||
type: FieldMetadataType.PHONES,
|
||||
},
|
||||
});
|
||||
|
||||
for (const phoneFieldMetadata of phonesFieldMetadata) {
|
||||
if (
|
||||
!isDefined(phoneFieldMetadata) ||
|
||||
!isDefined(phoneFieldMetadata.defaultValue)
|
||||
)
|
||||
continue;
|
||||
let defaultValue = phoneFieldMetadata.defaultValue;
|
||||
|
||||
// some cases look like it's an array. let's flatten it (not sure the case is supposed to happen but I saw it in my local db)
|
||||
if (Array.isArray(defaultValue) && isDefined(defaultValue[0]))
|
||||
defaultValue = phoneFieldMetadata.defaultValue[0];
|
||||
|
||||
if (!isDefined(defaultValue)) continue;
|
||||
if (typeof defaultValue !== 'object') continue;
|
||||
if (!('primaryPhoneCountryCode' in defaultValue)) continue;
|
||||
if (!defaultValue.primaryPhoneCountryCode) continue;
|
||||
|
||||
const primaryPhoneCountryCode = defaultValue.primaryPhoneCountryCode;
|
||||
|
||||
const countryCode = callingCodeToCountryCode(
|
||||
primaryPhoneCountryCode.replace(/["']/g, ''),
|
||||
);
|
||||
|
||||
if (options.dryRun === false) {
|
||||
await this.fieldMetadataRepository.update(phoneFieldMetadata.id, {
|
||||
defaultValue: {
|
||||
...defaultValue,
|
||||
primaryPhoneCountryCode: countryCode
|
||||
? `'${countryCode}'`
|
||||
: "''",
|
||||
primaryPhoneCallingCode: isCallingCode(
|
||||
primaryPhoneCountryCode.replace(/["']/g, ''),
|
||||
)
|
||||
? primaryPhoneCountryCode
|
||||
: "''",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`Error in workspace ${workspaceId} : ${error}`);
|
||||
}
|
||||
workspaceIterator++;
|
||||
}
|
||||
this.logger.log(chalk.green(`Command completed!`));
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user