Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0091792d78 | |||
| eb46ca4355 | |||
| 9d289b5e10 |
@@ -58,15 +58,17 @@ export const computeContextStoreFilters = ({
|
||||
]);
|
||||
}
|
||||
if (contextStoreTargetedRecordsRule.mode === 'selection') {
|
||||
if (contextStoreTargetedRecordsRule.selectedRecordIds.length === 0) {
|
||||
return { id: { in: [] } };
|
||||
}
|
||||
|
||||
queryFilter = makeAndFilterVariables([
|
||||
recordGqlFilterForAnyFieldFilter,
|
||||
contextStoreTargetedRecordsRule.selectedRecordIds.length > 0
|
||||
? {
|
||||
id: {
|
||||
in: contextStoreTargetedRecordsRule.selectedRecordIds,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
{
|
||||
id: {
|
||||
in: contextStoreTargetedRecordsRule.selectedRecordIds,
|
||||
},
|
||||
},
|
||||
computeRecordGqlOperationFilter({
|
||||
filterValueDependencies,
|
||||
fields: objectMetadataItem?.fields ?? [],
|
||||
|
||||
+15
@@ -1,6 +1,7 @@
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useIncrementalFetchAndMutateRecords } from '@/object-record/hooks/useIncrementalFetchAndMutateRecords';
|
||||
import { useIncrementalUpdateManyRecords } from '@/object-record/hooks/useIncrementalUpdateManyRecords';
|
||||
import { useRefetchFindManyRecords } from '@/object-record/hooks/useRefetchFindManyRecords';
|
||||
import { useUpdateManyRecords } from '@/object-record/hooks/useUpdateManyRecords';
|
||||
import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
@@ -15,6 +16,9 @@ jest.mock('@/object-record/hooks/useRefetchAggregateQueries', () => ({
|
||||
refetchAggregateQueries: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
jest.mock('@/object-record/hooks/useRefetchFindManyRecords', () => ({
|
||||
useRefetchFindManyRecords: jest.fn(),
|
||||
}));
|
||||
jest.mock('@/object-record/hooks/useIncrementalFetchAndMutateRecords');
|
||||
|
||||
const mockUseObjectMetadataItem = jest.mocked(useObjectMetadataItem);
|
||||
@@ -25,6 +29,7 @@ const mockUseUpdateManyRecords = jest.mocked(useUpdateManyRecords);
|
||||
const mockUseIncrementalFetchAndMutateRecords = jest.mocked(
|
||||
useIncrementalFetchAndMutateRecords,
|
||||
);
|
||||
const mockUseRefetchFindManyRecords = jest.mocked(useRefetchFindManyRecords);
|
||||
|
||||
describe('useIncrementalUpdateManyRecords', () => {
|
||||
const mockUpdateManyRecords = jest.fn();
|
||||
@@ -53,6 +58,11 @@ describe('useIncrementalUpdateManyRecords', () => {
|
||||
updateProgress: mockUpdateProgress,
|
||||
cancel: jest.fn(),
|
||||
});
|
||||
|
||||
const mockRefetchFindManyRecords = jest.fn();
|
||||
mockUseRefetchFindManyRecords.mockReturnValue({
|
||||
refetchFindManyRecords: mockRefetchFindManyRecords,
|
||||
});
|
||||
});
|
||||
|
||||
it('should call incrementalFetchAndMutate and execute mutations via useUpdateManyRecords', async () => {
|
||||
@@ -81,6 +91,7 @@ describe('useIncrementalUpdateManyRecords', () => {
|
||||
delayInMsBetweenRequests: 50,
|
||||
skipRegisterObjectOperation: true,
|
||||
skipRefetchAggregateQueries: true,
|
||||
skipOptimisticEffect: true,
|
||||
abortSignal: expect.any(AbortSignal),
|
||||
});
|
||||
expect(mockUpdateProgress).toHaveBeenCalledWith(2, 2);
|
||||
@@ -102,6 +113,10 @@ describe('useIncrementalUpdateManyRecords', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { refetchFindManyRecords } =
|
||||
mockUseRefetchFindManyRecords.mock.results[0].value;
|
||||
expect(refetchFindManyRecords).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should pass abortSignal to updateManyRecords', async () => {
|
||||
|
||||
+8
-1
@@ -1,11 +1,12 @@
|
||||
import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { DEFAULT_QUERY_PAGE_SIZE } from '@/object-record/constants/DefaultQueryPageSize';
|
||||
import { type UseFindManyRecordsParams } from '@/object-record/hooks/useFetchMoreRecordsWithPagination';
|
||||
import { useIncrementalFetchAndMutateRecords } from '@/object-record/hooks/useIncrementalFetchAndMutateRecords';
|
||||
import { useRefetchAggregateQueries } from '@/object-record/hooks/useRefetchAggregateQueries';
|
||||
import { useRefetchFindManyRecords } from '@/object-record/hooks/useRefetchFindManyRecords';
|
||||
import { useUpdateManyRecords } from '@/object-record/hooks/useUpdateManyRecords';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { dispatchObjectRecordOperationBrowserEvent } from '@/browser-event/utils/dispatchObjectRecordOperationBrowserEvent';
|
||||
import { getUpdatedFieldsFromRecordInput } from '@/object-record/utils/getUpdatedFieldsFromRecordInput';
|
||||
|
||||
const DEFAULT_DELAY_BETWEEN_MUTATIONS_MS = 50;
|
||||
@@ -39,6 +40,10 @@ export const useIncrementalUpdateManyRecords = <
|
||||
|
||||
const { refetchAggregateQueries } = useRefetchAggregateQueries();
|
||||
|
||||
const { refetchFindManyRecords } = useRefetchFindManyRecords({
|
||||
objectMetadataNamePlural: objectMetadataItem.namePlural,
|
||||
});
|
||||
|
||||
const {
|
||||
incrementalFetchAndMutate,
|
||||
progress,
|
||||
@@ -68,6 +73,7 @@ export const useIncrementalUpdateManyRecords = <
|
||||
delayInMsBetweenRequests: delayInMsBetweenMutations,
|
||||
skipRegisterObjectOperation: true,
|
||||
skipRefetchAggregateQueries: true,
|
||||
skipOptimisticEffect: true,
|
||||
abortSignal,
|
||||
});
|
||||
|
||||
@@ -78,6 +84,7 @@ export const useIncrementalUpdateManyRecords = <
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
await refetchFindManyRecords();
|
||||
await refetchAggregateQueries({
|
||||
objectMetadataNamePlural: objectMetadataItem.namePlural,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
|
||||
import { capitalize } from 'twenty-shared/utils';
|
||||
|
||||
export const useRefetchFindManyRecords = ({
|
||||
objectMetadataNamePlural,
|
||||
}: {
|
||||
objectMetadataNamePlural: string;
|
||||
}) => {
|
||||
const apolloCoreClient = useApolloCoreClient();
|
||||
|
||||
const refetchFindManyRecords = async () => {
|
||||
const findManyRecordsQueryName = `FindMany${capitalize(
|
||||
objectMetadataNamePlural,
|
||||
)}`;
|
||||
|
||||
await apolloCoreClient.refetchQueries({
|
||||
include: [findManyRecordsQueryName],
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
refetchFindManyRecords,
|
||||
};
|
||||
};
|
||||
+2
@@ -1,6 +1,7 @@
|
||||
import { COMMAND_MENU_DROPDOWN_CLICK_OUTSIDE_ID } from '@/command-menu-item/constants/CommandMenuDropdownClickOutsideId';
|
||||
import { COMMAND_MENU_CLICK_OUTSIDE_ID } from '@/command-menu/constants/CommandMenuClickOutsideId';
|
||||
import { RECORD_BOARD_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-board/constants/RecordBoardClickOutsideListenerId';
|
||||
import { SIDE_PANEL_CLICK_OUTSIDE_ID } from '@/side-panel/constants/SidePanelClickOutsideId';
|
||||
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
|
||||
import { useActiveRecordBoardCard } from '@/object-record/record-board/hooks/useActiveRecordBoardCard';
|
||||
import { useFocusedRecordBoardCard } from '@/object-record/record-board/hooks/useFocusedRecordBoardCard';
|
||||
@@ -37,6 +38,7 @@ export const RecordBoardClickOutsideEffect = () => {
|
||||
PAGE_ACTION_CONTAINER_CLICK_OUTSIDE_ID,
|
||||
RECORD_BOARD_CARD_CLICK_OUTSIDE_ID,
|
||||
LINK_CHIP_CLICK_OUTSIDE_ID,
|
||||
SIDE_PANEL_CLICK_OUTSIDE_ID,
|
||||
],
|
||||
listenerId: RECORD_BOARD_CLICK_OUTSIDE_LISTENER_ID,
|
||||
refs: [],
|
||||
|
||||
+2
@@ -1,6 +1,7 @@
|
||||
import { COMMAND_MENU_DROPDOWN_CLICK_OUTSIDE_ID } from '@/command-menu-item/constants/CommandMenuDropdownClickOutsideId';
|
||||
import { COMMAND_MENU_CLICK_OUTSIDE_ID } from '@/command-menu/constants/CommandMenuClickOutsideId';
|
||||
import { RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/RecordTableClickOutsideListenerId';
|
||||
import { SIDE_PANEL_CLICK_OUTSIDE_ID } from '@/side-panel/constants/SidePanelClickOutsideId';
|
||||
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
|
||||
import { useLeaveTableFocus } from '@/object-record/record-table/hooks/internal/useLeaveTableFocus';
|
||||
import { MODAL_BACKDROP_CLICK_OUTSIDE_ID } from '@/ui/layout/modal/constants/ModalBackdropClickOutsideId';
|
||||
@@ -30,6 +31,7 @@ export const RecordTableBodyFocusClickOutsideEffect = ({
|
||||
COMMAND_MENU_CLICK_OUTSIDE_ID,
|
||||
PAGE_ACTION_CONTAINER_CLICK_OUTSIDE_ID,
|
||||
MODAL_BACKDROP_CLICK_OUTSIDE_ID,
|
||||
SIDE_PANEL_CLICK_OUTSIDE_ID,
|
||||
],
|
||||
listenerId: RECORD_TABLE_CLICK_OUTSIDE_LISTENER_ID,
|
||||
refs: [tableBodyRef],
|
||||
|
||||
+33
-11
@@ -1,15 +1,22 @@
|
||||
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
|
||||
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
|
||||
import { UpdateMultipleRecordsFooter } from '@/object-record/record-update-multiple/components/UpdateMultipleRecordsFooter';
|
||||
import { UpdateMultipleRecordsForm } from '@/object-record/record-update-multiple/components/UpdateMultipleRecordsForm';
|
||||
import { useUpdateMultipleRecordsActions } from '@/object-record/record-update-multiple/hooks/useUpdateMultipleRecordsActions';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
|
||||
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
||||
import { ShowPageContainer } from '@/ui/layout/page/components/ShowPageContainer';
|
||||
import { SidePanelProvider } from '@/ui/layout/side-panel/contexts/SidePanelContext';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useState } from 'react';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const UPDATE_MULTIPLE_RECORDS_CONFIRMATION_MODAL_ID =
|
||||
'update-multiple-records-confirmation';
|
||||
|
||||
const StyledShowPageRightContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -41,23 +48,30 @@ export const UpdateMultipleRecordsContainer = ({
|
||||
contextStoreInstanceId,
|
||||
});
|
||||
|
||||
const contextStoreNumberOfSelectedRecords = useAtomComponentStateValue(
|
||||
contextStoreNumberOfSelectedRecordsComponentState,
|
||||
contextStoreInstanceId,
|
||||
);
|
||||
|
||||
const hasSelectedRecords = contextStoreNumberOfSelectedRecords > 0;
|
||||
|
||||
const { t } = useLingui();
|
||||
const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
|
||||
const { enqueueErrorSnackBar } = useSnackBar();
|
||||
const { openModal } = useModal();
|
||||
const { closeSidePanelMenu } = useSidePanelMenu();
|
||||
|
||||
const [fieldUpdates, setFieldUpdates] = useState<UpdateMultipleRecordsState>(
|
||||
{},
|
||||
);
|
||||
|
||||
const handleUpdate = async () => {
|
||||
const handleUpdateClick = () => {
|
||||
openModal(UPDATE_MULTIPLE_RECORDS_CONFIRMATION_MODAL_ID);
|
||||
};
|
||||
|
||||
const handleConfirmedUpdate = async () => {
|
||||
try {
|
||||
const count = await updateRecords(fieldUpdates);
|
||||
if (count !== undefined) {
|
||||
enqueueSuccessSnackBar({
|
||||
message: t`Successfully updated ${count} records`,
|
||||
});
|
||||
closeSidePanelMenu();
|
||||
}
|
||||
await updateRecords(fieldUpdates);
|
||||
closeSidePanelMenu();
|
||||
} catch (error) {
|
||||
enqueueErrorSnackBar({
|
||||
message:
|
||||
@@ -99,12 +113,20 @@ export const UpdateMultipleRecordsContainer = ({
|
||||
<UpdateMultipleRecordsFooter
|
||||
isUpdating={isUpdating}
|
||||
progress={progress}
|
||||
onUpdate={handleUpdate}
|
||||
onUpdate={handleUpdateClick}
|
||||
onCancel={handleCancel}
|
||||
isUpdateDisabled={!hasChanges}
|
||||
isUpdateDisabled={!hasChanges || !hasSelectedRecords}
|
||||
/>
|
||||
</StyledShowPageRightContainer>
|
||||
</ShowPageContainer>
|
||||
<ConfirmationModal
|
||||
modalInstanceId={UPDATE_MULTIPLE_RECORDS_CONFIRMATION_MODAL_ID}
|
||||
title={t`Update ${contextStoreNumberOfSelectedRecords} records`}
|
||||
subtitle={t`This will modify ${contextStoreNumberOfSelectedRecords} records. This action cannot be undone.`}
|
||||
onConfirmClick={handleConfirmedUpdate}
|
||||
confirmButtonText={t`Update records`}
|
||||
confirmButtonAccent="blue"
|
||||
/>
|
||||
</SidePanelProvider>
|
||||
);
|
||||
};
|
||||
|
||||
+1
-6
@@ -62,13 +62,8 @@ export const useUpdateMultipleRecordsActions = ({
|
||||
filter: graphqlFilter,
|
||||
});
|
||||
|
||||
const updateRecords = async (fieldsToUpdate: Record<string, any>) => {
|
||||
const count = await incrementalUpdateManyRecords(fieldsToUpdate);
|
||||
return count;
|
||||
};
|
||||
|
||||
return {
|
||||
updateRecords,
|
||||
updateRecords: incrementalUpdateManyRecords,
|
||||
isUpdating,
|
||||
progress,
|
||||
cancel,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { SidePanelRouter } from '@/side-panel/components/SidePanelRouter';
|
||||
import { SidePanelWidthEffect } from '@/side-panel/components/SidePanelWidthEffect';
|
||||
import { SIDE_PANEL_CLICK_OUTSIDE_ID } from '@/side-panel/constants/SidePanelClickOutsideId';
|
||||
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
|
||||
import { useSidePanelCloseAnimationCompleteCleanup } from '@/side-panel/hooks/useSidePanelCloseAnimationCompleteCleanup';
|
||||
import {
|
||||
@@ -10,6 +11,7 @@ import { isSidePanelClosingState } from '@/side-panel/states/isSidePanelClosingS
|
||||
import { isSidePanelOpenedState } from '@/side-panel/states/isSidePanelOpenedState';
|
||||
import { tableWidthResizeIsActiveState } from '@/object-record/record-table/states/tableWidthResizeIsActivedState';
|
||||
import { ModalContainerContext } from '@/ui/layout/modal/contexts/ModalContainerContext';
|
||||
import { ParentClickOutsideIdContext } from '@/ui/utilities/pointer-event/contexts/ParentClickOutsideIdContext';
|
||||
import { ResizablePanelGap } from '@/ui/layout/resizable-panel/components/ResizablePanelGap';
|
||||
import { SIDE_PANEL_CONSTRAINTS } from '@/side-panel/constants/SidePanelConstraints';
|
||||
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
||||
@@ -138,11 +140,16 @@ export const SidePanelForDesktop = () => {
|
||||
isResizing={isResizing}
|
||||
onTransitionEnd={handleTransitionEnd}
|
||||
data-side-panel=""
|
||||
data-click-outside-id={SIDE_PANEL_CLICK_OUTSIDE_ID}
|
||||
>
|
||||
<StyledSidePanel>
|
||||
<StyledModalContainer ref={handleModalContainerRef} />
|
||||
<ModalContainerContext.Provider value={{ container: modalContainer }}>
|
||||
{shouldShowContent && <SidePanelRouter />}
|
||||
<ParentClickOutsideIdContext.Provider
|
||||
value={SIDE_PANEL_CLICK_OUTSIDE_ID}
|
||||
>
|
||||
{shouldShowContent && <SidePanelRouter />}
|
||||
</ParentClickOutsideIdContext.Provider>
|
||||
</ModalContainerContext.Provider>
|
||||
</StyledSidePanel>
|
||||
</StyledSidePanelWrapper>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const SIDE_PANEL_CLICK_OUTSIDE_ID = 'side-panel';
|
||||
+7
-1
@@ -10,6 +10,7 @@ import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContaine
|
||||
import { HotkeyEffect } from '@/ui/utilities/hotkey/components/HotkeyEffect';
|
||||
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
|
||||
import { ClickOutsideListenerContext } from '@/ui/utilities/pointer-event/contexts/ClickOutsideListenerContext';
|
||||
import { ParentClickOutsideIdContext } from '@/ui/utilities/pointer-event/contexts/ParentClickOutsideIdContext';
|
||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
@@ -141,6 +142,7 @@ export const DropdownInternalContainer = ({
|
||||
};
|
||||
|
||||
const { excludedClickOutsideId } = useContext(ClickOutsideListenerContext);
|
||||
const parentClickOutsideId = useContext(ParentClickOutsideIdContext);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -162,7 +164,11 @@ export const DropdownInternalContainer = ({
|
||||
isDropdownInModal={isDropdownInModal}
|
||||
>
|
||||
<OverlayContainer>
|
||||
<StyledDropdownInsideContainer id={dropdownId} data-select-disable>
|
||||
<StyledDropdownInsideContainer
|
||||
id={dropdownId}
|
||||
data-select-disable
|
||||
data-click-outside-id={parentClickOutsideId}
|
||||
>
|
||||
{dropdownComponents}
|
||||
</StyledDropdownInsideContainer>
|
||||
</OverlayContainer>
|
||||
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
export const ParentClickOutsideIdContext = createContext<string | undefined>(
|
||||
undefined,
|
||||
);
|
||||
Reference in New Issue
Block a user