Modal API Refactoring (#12062)

# Modal API Refactoring

This PR refactors the modal system to use an imperative approach for
setting hotkey scopes, addressing race conditions that occurred with the
previous effect-based implementation.

Fixes #11986
Closes #12087

## Key Changes:

- **New Modal API**: Introduced a `useModal` hook with `openModal`,
`closeModal`, and `toggleModal` functions, similar to the existing
dropdown API
- **Modal Identification**: Added a `modalId` prop to uniquely identify
modals
- **State Management**: Introduced `isModalOpenedComponentState` and
removed individual boolean state atoms (like
`isRemoveSortingModalOpenState`)
- **Modal Constants**: Added consistent modal ID constants (e.g.,
`FavoriteFolderDeleteModalId`, `RecordIndexRemoveSortingModalId`) for
better maintainability
- **Mount Effects**: Created mount effect components (like
`AuthModalMountEffect`) to handle initial modal opening where needed

## Implementation Details:

- Modified `Modal` and `ConfirmationModal` components to accept the new
`modalId` prop
- Added a component-state-based approach using
`ModalComponentInstanceContext` to track modal state
- Introduced imperative modal handlers that properly manage hotkey
scopes
- Components like `ActionModal` and `AttachmentList` now use the new
`useModal` hook for better control over modal state

## Benefits:

- **Race Condition Prevention**: Hotkey scopes are now set imperatively,
eliminating race conditions
- **Consistent API**: Modal and dropdown now share similar patterns,
improving developer experience

## Tests to do before merging:

1. Action Modals (Modal triggered by an action, for example the delete
action)

2. Auth Modal (`AuthModal.tsx` and `AuthModalMountEffect.tsx`)
   - Test that auth modal opens automatically on mount
   - Verify authentication flow works properly

3. Email Verification Sent Modal (in `SignInUp.tsx`)
   - Verify this modal displays correctly

4. Attachment Preview Modal (in `AttachmentList.tsx`)
   - Test opening preview modal by clicking on attachments
   - Verify close, download functionality works
   - Test modal navigation and interactions

5. Favorite Folder Delete Modal (`CurrentWorkspaceMemberFavorites.tsx`)
   - Test deletion confirmation flow
- Check that modal opens when attempting to delete folders with
favorites

6. Record Board Remove Sorting Modal (`RecordBoard.tsx` using
`RecordIndexRemoveSortingModalId`)
- Test that modal appears when trying to drag records with sorting
enabled
   - Verify sorting removal works correctly

7. Record Group Reorder Confirmation Modal
(`RecordGroupReorderConfirmationModal.tsx`)
   - Test group reordering with sorting enabled
   - Verify confirmation modal properly handles sorting removal

8. Confirmation Modal (base component used by several modals)
   - Test all variants with different confirmation options

For each modal, verify:
- Opening/closing behavior
- Hotkey support (Esc to close, Enter to confirm where applicable)
- Click outside behavior
- Proper z-index stacking
- Any modal-specific functionality
This commit is contained in:
Raphaël Bosi
2025-05-16 17:04:22 +02:00
committed by GitHub
parent 8334fe9528
commit 6554947671
94 changed files with 1268 additions and 563 deletions

View File

@ -1,10 +1,14 @@
import { ReactNode, useCallback, useContext, useState } from 'react';
import { ReactNode, useContext } from 'react';
import { createPortal } from 'react-dom';
import { ActionDisplay } from '@/action-menu/actions/display/components/ActionDisplay';
import { ActionConfigContext } from '@/action-menu/contexts/ActionConfigContext';
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
import { useCloseActionMenu } from '@/action-menu/hooks/useCloseActionMenu';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpenedComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { ButtonAccent } from 'twenty-ui/input';
export type ActionModalProps = {
@ -24,11 +28,7 @@ export const ActionModal = ({
confirmButtonAccent = 'danger',
isLoading = false,
}: ActionModalProps) => {
const [isOpen, setIsOpen] = useState(false);
const handleOpen = useCallback(() => {
setIsOpen(true);
}, []);
const { openModal } = useModal();
const { closeActionMenu } = useCloseActionMenu();
@ -38,19 +38,28 @@ export const ActionModal = ({
};
const actionConfig = useContext(ActionConfigContext);
const { actionMenuType } = useContext(ActionMenuContext);
const modalId = `${actionConfig?.key}-action-modal-${actionMenuType}`;
const isModalOpened = useRecoilComponentValueV2(
isModalOpenedComponentState,
modalId,
);
if (!actionConfig) {
return null;
}
const handleClick = () => openModal(modalId);
return (
<>
<ActionDisplay onClick={handleOpen} />
{isOpen &&
<ActionDisplay onClick={handleClick} />
{isModalOpened &&
createPortal(
<ConfirmationModal
isOpen={isOpen}
setIsOpen={setIsOpen}
modalId={modalId}
title={title}
subtitle={subtitle}
onConfirmClick={handleConfirmClick}

View File

@ -4,16 +4,18 @@ import { HttpResponse, graphql } from 'msw';
import { Calendar } from '@/activities/calendar/components/Calendar';
import { getTimelineCalendarEventsFromCompanyId } from '@/activities/calendar/graphql/queries/getTimelineCalendarEventsFromCompanyId';
import { ComponentDecorator } from 'twenty-ui/testing';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { mockedTimelineCalendarEvents } from '~/testing/mock-data/timeline-calendar-events';
import { ComponentDecorator } from 'twenty-ui/testing';
const meta: Meta<typeof Calendar> = {
title: 'Modules/Activities/Calendar/Calendar',
component: Calendar,
decorators: [
I18nFrontDecorator,
ComponentDecorator,
ObjectMetadataItemsDecorator,
SnackBarDecorator,

View File

@ -11,10 +11,11 @@ import { Modal } from '@/ui/layout/modal/components/Modal';
import { useRecoilValue } from 'recoil';
import { ActivityList } from '@/activities/components/ActivityList';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { AttachmentRow } from './AttachmentRow';
import { IconButton } from 'twenty-ui/input';
import { IconDownload, IconX } from 'twenty-ui/display';
import { IconButton } from 'twenty-ui/input';
import { AttachmentRow } from './AttachmentRow';
const DocumentViewer = lazy(() =>
import('@/activities/files/components/DocumentViewer').then((module) => ({
@ -112,6 +113,8 @@ const StyledButtonContainer = styled.div`
gap: ${({ theme }) => theme.spacing(1)};
`;
export const PREVIEW_MODAL_ID = 'preview-modal';
export const AttachmentList = ({
targetableObject,
title,
@ -122,10 +125,13 @@ export const AttachmentList = ({
const [isDraggingFile, setIsDraggingFile] = useState(false);
const [previewedAttachment, setPreviewedAttachment] =
useState<Attachment | null>(null);
const isAttachmentPreviewEnabled = useRecoilValue(
isAttachmentPreviewEnabledState,
);
const { openModal, closeModal } = useModal();
const onUploadFile = async (file: File) => {
await uploadAttachmentFile(file, targetableObject);
};
@ -133,9 +139,11 @@ export const AttachmentList = ({
const handlePreview = (attachment: Attachment) => {
if (!isAttachmentPreviewEnabled) return;
setPreviewedAttachment(attachment);
openModal(PREVIEW_MODAL_ID);
};
const handleClosePreview = () => {
closeModal(PREVIEW_MODAL_ID);
setPreviewedAttachment(null);
};
@ -177,7 +185,12 @@ export const AttachmentList = ({
</StyledContainer>
)}
{previewedAttachment && isAttachmentPreviewEnabled && (
<StyledModal size="large" isClosable onClose={handleClosePreview}>
<StyledModal
modalId={PREVIEW_MODAL_ID}
size="large"
isClosable
onClose={handleClosePreview}
>
<StyledModalHeader>
<StyledHeader>
<StyledModalTitle>{previewedAttachment.name}</StyledModalTitle>

View File

@ -2,14 +2,16 @@ import { Meta, StoryObj } from '@storybook/react';
import { HttpResponse, graphql } from 'msw';
import { EventCardCalendarEvent } from '@/activities/timeline-activities/rows/calendar/components/EventCardCalendarEvent';
import { ComponentDecorator } from 'twenty-ui/testing';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { ComponentDecorator } from 'twenty-ui/testing';
const meta: Meta<typeof EventCardCalendarEvent> = {
title: 'Modules/TimelineActivities/Rows/CalendarEvent/EventCardCalendarEvent',
component: EventCardCalendarEvent,
decorators: [
I18nFrontDecorator,
ComponentDecorator,
ObjectMetadataItemsDecorator,
SnackBarDecorator,

View File

@ -1,3 +1,5 @@
import { AuthModalMountEffect } from '@/auth/components/AuthModalMountEffect';
import { AUTH_MODAL_ID } from '@/auth/constants/AuthModalId';
import { Modal } from '@/ui/layout/modal/components/Modal';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import styled from '@emotion/styled';
@ -13,9 +15,12 @@ type AuthModalProps = {
};
export const AuthModal = ({ children }: AuthModalProps) => (
<Modal padding={'none'} modalVariant="primary">
<ScrollWrapper componentInstanceId="scroll-wrapper-modal-content">
<StyledContent>{children}</StyledContent>
</ScrollWrapper>
</Modal>
<>
<AuthModalMountEffect />
<Modal modalId={AUTH_MODAL_ID} padding={'none'} modalVariant="primary">
<ScrollWrapper componentInstanceId="scroll-wrapper-modal-content">
<StyledContent>{children}</StyledContent>
</ScrollWrapper>
</Modal>
</>
);

View File

@ -0,0 +1,14 @@
import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { useEffect } from 'react';
import { AUTH_MODAL_ID } from '../constants/AuthModalId';
export const AuthModalMountEffect = () => {
const { openModal } = useModal();
useEffect(() => {
openModal(AUTH_MODAL_ID);
}, [openModal]);
return null;
};

View File

@ -0,0 +1 @@
export const AUTH_MODAL_ID = 'auth-modal';

View File

@ -10,7 +10,11 @@ const RenderWithModal = (
args: React.ComponentProps<typeof EmailVerificationSent>,
) => {
return (
<Modal padding="none" modalVariant="primary">
<Modal
modalId={'email-verification-sent-modal'}
padding="none"
modalVariant="primary"
>
<Modal.Content isVerticalCentered isHorizontalCentered>
<EmailVerificationSent email={args.email} isError={args.isError} />
</Modal.Content>

View File

@ -1,6 +1,7 @@
import { FavoriteFolderNavigationDrawerItemDropdown } from '@/favorites/components/FavoriteFolderNavigationDrawerItemDropdown';
import { FavoriteIcon } from '@/favorites/components/FavoriteIcon';
import { FavoritesDroppable } from '@/favorites/components/FavoritesDroppable';
import { FAVORITE_FOLDER_DELETE_MODAL_ID } from '@/favorites/constants/FavoriteFolderDeleteModalId';
import { FavoritesDragContext } from '@/favorites/contexts/FavoritesDragContext';
import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite';
import { useDeleteFavoriteFolder } from '@/favorites/hooks/useDeleteFavoriteFolder';
@ -11,20 +12,22 @@ import { ProcessedFavorite } from '@/favorites/utils/sortFavorites';
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpenedComponentState';
import { NavigationDrawerInput } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerInput';
import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
import { NavigationDrawerItemsCollapsableContainer } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemsCollapsableContainer';
import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem';
import { getNavigationSubItemLeftAdornment } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemLeftAdornment';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { Droppable } from '@hello-pangea/dnd';
import { useContext, useState } from 'react';
import { createPortal } from 'react-dom';
import { useLocation } from 'react-router-dom';
import { useRecoilState } from 'recoil';
import { AnimatedExpandableContainer } from 'twenty-ui/layout';
import { IconFolder, IconFolderOpen, IconHeartOff } from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input';
import { AnimatedExpandableContainer } from 'twenty-ui/layout';
type CurrentWorkspaceMemberFavoritesProps = {
folder: {
folderId: string;
@ -46,7 +49,7 @@ export const CurrentWorkspaceMemberFavorites = ({
const [favoriteFolderName, setFavoriteFolderName] = useState(
folder.folderName,
);
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const { openModal } = useModal();
const [openFavoriteFolderIds, setOpenFavoriteFolderIds] = useRecoilState(
openFavoriteFolderIdsState,
@ -102,9 +105,11 @@ export const CurrentWorkspaceMemberFavorites = ({
setIsFavoriteFolderRenaming(false);
};
const modalId = `${FAVORITE_FOLDER_DELETE_MODAL_ID}-${folder.folderId}`;
const handleFavoriteFolderDelete = async () => {
if (folder.favorites.length > 0) {
setIsDeleteModalOpen(true);
openModal(modalId);
closeFavoriteFolderEditDropdown();
} else {
await deleteFavoriteFolder(folder.folderId);
@ -114,7 +119,6 @@ export const CurrentWorkspaceMemberFavorites = ({
const handleConfirmDelete = async () => {
await deleteFavoriteFolder(folder.folderId);
setIsDeleteModalOpen(false);
};
const rightOptions = (
@ -126,6 +130,11 @@ export const CurrentWorkspaceMemberFavorites = ({
/>
);
const isModalOpened = useRecoilComponentValueV2(
isModalOpenedComponentState,
modalId,
);
return (
<>
<NavigationDrawerItemsCollapsableContainer
@ -207,17 +216,17 @@ export const CurrentWorkspaceMemberFavorites = ({
</AnimatedExpandableContainer>
</NavigationDrawerItemsCollapsableContainer>
{createPortal(
<ConfirmationModal
isOpen={isDeleteModalOpen}
setIsOpen={setIsDeleteModalOpen}
title={`Remove ${folder.favorites.length} ${folder.favorites.length > 1 ? 'favorites' : 'favorite'}?`}
subtitle={`This action will delete this favorite folder ${folder.favorites.length > 1 ? `and all ${folder.favorites.length} favorites` : 'and the favorite'} inside. Do you want to continue?`}
onConfirmClick={handleConfirmDelete}
confirmButtonText="Delete Folder"
/>,
document.body,
)}
{isModalOpened &&
createPortal(
<ConfirmationModal
modalId={modalId}
title={`Remove ${folder.favorites.length} ${folder.favorites.length > 1 ? 'favorites' : 'favorite'}?`}
subtitle={`This action will delete this favorite folder ${folder.favorites.length > 1 ? `and all ${folder.favorites.length} favorites` : 'and the favorite'} inside. Do you want to continue?`}
onConfirmClick={handleConfirmDelete}
confirmButtonText="Delete Folder"
/>,
document.body,
)}
</>
);
};

View File

@ -19,13 +19,13 @@ export const FavoriteFolderNavigationDrawerItemDropdown = ({
closeDropdown,
}: FavoriteFolderNavigationDrawerItemDropdownProps) => {
const handleRename = () => {
onRename();
closeDropdown();
onRename();
};
const handleDelete = () => {
onDelete();
closeDropdown();
onDelete();
};
return (

View File

@ -0,0 +1 @@
export const FAVORITE_FOLDER_DELETE_MODAL_ID = 'favorite-folder-delete-modal';

View File

@ -1,7 +1,7 @@
import styled from '@emotion/styled';
import { DragDropContext, OnDragEndResponder } from '@hello-pangea/dnd'; // Atlassian dnd does not support StrictMode from RN 18, so we use a fork @hello-pangea/dnd https://github.com/atlassian/react-beautiful-dnd/issues/2350
import { useContext, useRef } from 'react';
import { useRecoilCallback, useSetRecoilState } from 'recoil';
import { useRecoilCallback } from 'recoil';
import { getActionMenuIdFromRecordIndexId } from '@/action-menu/utils/getActionMenuIdFromRecordIndexId';
import { RecordBoardHeader } from '@/object-record/record-board/components/RecordBoardHeader';
@ -19,11 +19,12 @@ import { RecordBoardComponentInstanceContext } from '@/object-record/record-boar
import { getDraggedRecordPosition } from '@/object-record/record-board/utils/getDraggedRecordPosition';
import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState';
import { visibleRecordGroupIdsComponentFamilySelector } from '@/object-record/record-group/states/selectors/visibleRecordGroupIdsComponentFamilySelector';
import { RECORD_INDEX_REMOVE_SORTING_MODAL_ID } from '@/object-record/record-index/constants/RecordIndexRemoveSortingModalId';
import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState';
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { isRemoveSortingModalOpenState } from '@/object-record/record-table/states/isRemoveSortingModalOpenState';
import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect';
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
@ -120,9 +121,7 @@ export const RecordBoard = () => {
},
});
const setIsRemoveSortingModalOpen = useSetRecoilState(
isRemoveSortingModalOpenState,
);
const { openModal } = useModal();
const handleDragEnd: OnDragEndResponder = useRecoilCallback(
({ snapshot }) =>
@ -130,7 +129,7 @@ export const RecordBoard = () => {
if (!result.destination) return;
if (currentRecordSorts.length > 0) {
setIsRemoveSortingModalOpen(true);
openModal(RECORD_INDEX_REMOVE_SORTING_MODAL_ID);
return;
}
@ -189,7 +188,7 @@ export const RecordBoard = () => {
recordIndexRecordIdsByGroupFamilyState,
selectFieldMetadataItem,
updateOneRecord,
setIsRemoveSortingModalOpen,
openModal,
currentRecordSorts,
],
);

View File

@ -1,9 +1,8 @@
import { isRecordGroupReorderConfirmationModalVisibleState } from '@/object-record/record-group/states/isRecordGroupReorderConfirmationModalVisibleState';
import { RECORD_GROUP_REORDER_CONFIRMATION_MODAL_ID } from '@/object-record/record-group/constants/RecordGroupReorderConfirmationModalId';
import { recordIndexRecordGroupSortComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupSortComponentState';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { createPortal } from 'react-dom';
import { useRecoilState } from 'recoil';
type RecordGroupReorderConfirmationModalProps = {
onConfirmClick: () => void;
@ -12,23 +11,13 @@ type RecordGroupReorderConfirmationModalProps = {
export const RecordGroupReorderConfirmationModal = ({
onConfirmClick,
}: RecordGroupReorderConfirmationModalProps) => {
const [
isRecordGroupReorderConfirmationModalVisible,
setIsRecordGroupReorderConfirmationModalVisible,
] = useRecoilState(isRecordGroupReorderConfirmationModalVisibleState);
const recordGroupSort = useRecoilComponentValueV2(
recordIndexRecordGroupSortComponentState,
);
if (!isRecordGroupReorderConfirmationModalVisible) {
return null;
}
return createPortal(
<ConfirmationModal
isOpen={isRecordGroupReorderConfirmationModalVisible}
setIsOpen={setIsRecordGroupReorderConfirmationModalVisible}
modalId={RECORD_GROUP_REORDER_CONFIRMATION_MODAL_ID}
title="Group sorting"
subtitle={`Would you like to remove ${recordGroupSort} group sorting?`}
onConfirmClick={onConfirmClick}

View File

@ -0,0 +1,2 @@
export const RECORD_GROUP_REORDER_CONFIRMATION_MODAL_ID =
'record-group-reorder-confirmation-modal';

View File

@ -1,16 +1,16 @@
import { RECORD_GROUP_REORDER_CONFIRMATION_MODAL_ID } from '@/object-record/record-group/constants/RecordGroupReorderConfirmationModalId';
import { useReorderRecordGroups } from '@/object-record/record-group/hooks/useReorderRecordGroups';
import { isRecordGroupReorderConfirmationModalVisibleState } from '@/object-record/record-group/states/isRecordGroupReorderConfirmationModalVisibleState';
import { RecordGroupSort } from '@/object-record/record-group/types/RecordGroupSort';
import { recordIndexRecordGroupSortComponentState } from '@/object-record/record-index/states/recordIndexRecordGroupSortComponentState';
import { recordIndexRecordGroupIsDraggableSortComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexRecordGroupIsDraggableSortComponentSelector';
import { useGoBackToPreviousDropdownFocusId } from '@/ui/layout/dropdown/hooks/useGoBackToPreviousDropdownFocusId';
import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { ViewType } from '@/views/types/ViewType';
import { OnDragEndResponder } from '@hello-pangea/dnd';
import { useState } from 'react';
import { useSetRecoilState } from 'recoil';
type UseRecordGroupReorderConfirmationModalParams = {
recordIndexId: string;
@ -26,9 +26,7 @@ export const useRecordGroupReorderConfirmationModal = ({
const { goBackToPreviousDropdownFocusId } =
useGoBackToPreviousDropdownFocusId();
const setIsRecordGroupReorderConfirmationModalVisible = useSetRecoilState(
isRecordGroupReorderConfirmationModalVisibleState,
);
const { openModal } = useModal();
const [pendingDragEndHandlerParams, setPendingDragEndHandlerParams] =
useState<Parameters<OnDragEndResponder> | null>(null);
@ -58,7 +56,7 @@ export const useRecordGroupReorderConfirmationModal = ({
const handleDragEndWithModal: OnDragEndResponder = (result, provided) => {
if (!isDragableSortRecordGroup) {
setIsRecordGroupReorderConfirmationModalVisible(true);
openModal(RECORD_GROUP_REORDER_CONFIRMATION_MODAL_ID);
setActiveDropdownFocusIdAndMemorizePrevious(null);
setPendingDragEndHandlerParams([result, provided]);
} else {

View File

@ -1,6 +0,0 @@
import { atom } from 'recoil';
export const isRecordGroupReorderConfirmationModalVisibleState = atom<boolean>({
key: 'isRecordGroupReorderConfirmationModalVisibleState',
default: false,
});

View File

@ -9,7 +9,10 @@ import { RecordBoardBodyEscapeHotkeyEffect } from '@/object-record/record-board/
import { RecordBoardHotkeyEffect } from '@/object-record/record-board/components/RecordBoardHotkeyEffect';
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
import { RecordIndexRemoveSortingModal } from '@/object-record/record-index/components/RecordIndexRemoveSortingModal';
import { RECORD_INDEX_REMOVE_SORTING_MODAL_ID } from '@/object-record/record-index/constants/RecordIndexRemoveSortingModalId';
import { recordIndexKanbanFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexKanbanFieldMetadataIdState';
import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpenedComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
type RecordIndexBoardContainerProps = {
recordBoardId: string;
viewBarId: string;
@ -39,6 +42,11 @@ export const RecordIndexBoardContainer = ({
shouldMatchRootQueryFilter: true,
});
const isRecordIndexRemoveSortingModalOpened = useRecoilComponentValueV2(
isModalOpenedComponentState,
RECORD_INDEX_REMOVE_SORTING_MODAL_ID,
);
if (!selectFieldMetadataItem) {
return;
}
@ -55,7 +63,9 @@ export const RecordIndexBoardContainer = ({
}}
>
<RecordBoard />
<RecordIndexRemoveSortingModal />
{isRecordIndexRemoveSortingModalOpened && (
<RecordIndexRemoveSortingModal />
)}
<RecordBoardHotkeyEffect />
<RecordBoardBodyEscapeHotkeyEffect />
</RecordBoardContext.Provider>

View File

@ -1,8 +1,6 @@
import { useRecoilState } from 'recoil';
import { RECORD_INDEX_REMOVE_SORTING_MODAL_ID } from '@/object-record/record-index/constants/RecordIndexRemoveSortingModalId';
import { useRemoveRecordSort } from '@/object-record/record-sort/hooks/useRemoveRecordSort';
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
import { isRemoveSortingModalOpenState } from '@/object-record/record-table/states/isRemoveSortingModalOpenState';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
@ -14,8 +12,6 @@ export const RecordIndexRemoveSortingModal = () => {
const fieldMetadataIds = currentRecordSorts.map(
(viewSort) => viewSort.fieldMetadataId,
);
const [isRemoveSortingModalOpen, setIsRemoveSortingModalOpen] =
useRecoilState(isRemoveSortingModalOpenState);
const { removeRecordSort } = useRemoveRecordSort();
@ -26,15 +22,12 @@ export const RecordIndexRemoveSortingModal = () => {
};
return (
<>
<ConfirmationModal
isOpen={isRemoveSortingModalOpen}
setIsOpen={setIsRemoveSortingModalOpen}
title={'Remove sorting?'}
subtitle={'This is required to enable manual row reordering.'}
onConfirmClick={handleRemoveClick}
confirmButtonText={'Remove Sorting'}
/>
</>
<ConfirmationModal
modalId={RECORD_INDEX_REMOVE_SORTING_MODAL_ID}
title={'Remove sorting?'}
subtitle={'This is required to enable manual row reordering.'}
onConfirmClick={handleRemoveClick}
confirmButtonText={'Remove Sorting'}
/>
);
};

View File

@ -1,8 +1,11 @@
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { RecordUpdateHookParams } from '@/object-record/record-field/contexts/FieldContext';
import { RecordIndexRemoveSortingModal } from '@/object-record/record-index/components/RecordIndexRemoveSortingModal';
import { RECORD_INDEX_REMOVE_SORTING_MODAL_ID } from '@/object-record/record-index/constants/RecordIndexRemoveSortingModalId';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { RecordTableWithWrappers } from '@/object-record/record-table/components/RecordTableWithWrappers';
import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpenedComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
type RecordIndexTableContainerProps = {
recordTableId: string;
@ -15,6 +18,11 @@ export const RecordIndexTableContainer = ({
}: RecordIndexTableContainerProps) => {
const { objectNameSingular } = useRecordIndexContextOrThrow();
const isRecordIndexRemoveSortingModalOpened = useRecoilComponentValueV2(
isModalOpenedComponentState,
RECORD_INDEX_REMOVE_SORTING_MODAL_ID,
);
const { updateOneRecord } = useUpdateOneRecord({
objectNameSingular,
});
@ -34,7 +42,9 @@ export const RecordIndexTableContainer = ({
viewBarId={viewBarId}
updateRecordMutation={updateEntity}
/>
<RecordIndexRemoveSortingModal />
{isRecordIndexRemoveSortingModalOpened && (
<RecordIndexRemoveSortingModal />
)}
</>
);
};

View File

@ -0,0 +1,2 @@
export const RECORD_INDEX_REMOVE_SORTING_MODAL_ID =
'record-index-remove-sorting-modal';

View File

@ -1,7 +1,7 @@
import { css } from '@emotion/react';
import styled from '@emotion/styled';
import { motion } from 'framer-motion';
import { useCallback, useContext, useState } from 'react';
import { useCallback, useContext } from 'react';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
@ -34,6 +34,7 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/Drop
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { createPortal } from 'react-dom';
import {
IconChevronDown,
@ -87,6 +88,8 @@ const StyledClickableZone = styled.div`
const MotionIconChevronDown = motion.create(IconChevronDown);
const DELETE_RELATION_MODAL_ID = 'delete-relation-modal';
type RecordDetailRelationRecordsListItemProps = {
isExpanded: boolean;
onClick: (relationRecordId: string) => void;
@ -100,8 +103,7 @@ export const RecordDetailRelationRecordsListItem = ({
}: RecordDetailRelationRecordsListItemProps) => {
const { fieldDefinition } = useContext(FieldContext);
const [isDeleteRelationModalOpen, setIsDeleteRelationModalOpen] =
useState(false);
const { openModal } = useModal();
const {
relationFieldMetadataId,
@ -173,13 +175,12 @@ export const RecordDetailRelationRecordsListItem = ({
};
const handleDelete = async () => {
setIsDeleteRelationModalOpen(true);
closeDropdown();
openModal(DELETE_RELATION_MODAL_ID);
};
const handleConfirmDelete = async () => {
await deleteOneRelationRecord(relationRecord.id);
setIsDeleteRelationModalOpen(false);
};
const useUpdateOneObjectRecordMutation: RecordUpdateHook = () => {
@ -306,8 +307,7 @@ export const RecordDetailRelationRecordsListItem = ({
</AnimatedEaseInOut>
{createPortal(
<ConfirmationModal
isOpen={isDeleteRelationModalOpen}
setIsOpen={setIsDeleteRelationModalOpen}
modalId={DELETE_RELATION_MODAL_ID}
title={`Delete Related ${relationObjectTypeName}`}
subtitle={
<>

View File

@ -1,14 +1,15 @@
import { DragDropContext, DropResult } from '@hello-pangea/dnd';
import { ReactNode } from 'react';
import { useRecoilCallback, useSetRecoilState } from 'recoil';
import { useRecoilCallback } from 'recoil';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { getDraggedRecordPosition } from '@/object-record/record-board/utils/getDraggedRecordPosition';
import { RECORD_INDEX_REMOVE_SORTING_MODAL_ID } from '@/object-record/record-index/constants/RecordIndexRemoveSortingModalId';
import { recordIndexAllRecordIdsComponentSelector } from '@/object-record/record-index/states/selectors/recordIndexAllRecordIdsComponentSelector';
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { isRemoveSortingModalOpenState } from '@/object-record/record-table/states/isRemoveSortingModalOpenState';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
@ -33,15 +34,13 @@ export const RecordTableBodyDragDropContextProvider = ({
currentRecordSortsComponentState,
);
const setIsRemoveSortingModalOpen = useSetRecoilState(
isRemoveSortingModalOpenState,
);
const { openModal } = useModal();
const handleDragEnd = useRecoilCallback(
({ snapshot }) =>
(result: DropResult) => {
if (currentRecordSorts.length > 0) {
setIsRemoveSortingModalOpen(true);
openModal(RECORD_INDEX_REMOVE_SORTING_MODAL_ID);
return;
}
@ -100,10 +99,10 @@ export const RecordTableBodyDragDropContextProvider = ({
});
},
[
currentRecordSorts.length,
recordIndexAllRecordIdsSelector,
setIsRemoveSortingModalOpen,
updateOneRow,
currentRecordSorts,
openModal,
],
);

View File

@ -1,15 +1,16 @@
import { DragDropContext, DropResult } from '@hello-pangea/dnd';
import { ReactNode } from 'react';
import { useRecoilCallback, useSetRecoilState } from 'recoil';
import { useRecoilCallback } from 'recoil';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { getDraggedRecordPosition } from '@/object-record/record-board/utils/getDraggedRecordPosition';
import { recordGroupDefinitionFamilyState } from '@/object-record/record-group/states/recordGroupDefinitionFamilyState';
import { RECORD_INDEX_REMOVE_SORTING_MODAL_ID } from '@/object-record/record-index/constants/RecordIndexRemoveSortingModalId';
import { recordIndexRecordIdsByGroupComponentFamilyState } from '@/object-record/record-index/states/recordIndexRecordIdsByGroupComponentFamilyState';
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { useRecordTableContextOrThrow } from '@/object-record/record-table/contexts/RecordTableContext';
import { isRemoveSortingModalOpenState } from '@/object-record/record-table/states/isRemoveSortingModalOpenState';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue';
import { isDefined } from 'twenty-shared/utils';
@ -26,9 +27,7 @@ export const RecordTableBodyRecordGroupDragDropContextProvider = ({
objectNameSingular,
});
const setIsRemoveSortingModalOpen = useSetRecoilState(
isRemoveSortingModalOpenState,
);
const { openModal } = useModal();
const recordIdsByGroupFamilyState = useRecoilComponentCallbackStateV2(
recordIndexRecordIdsByGroupComponentFamilyState,
@ -73,7 +72,7 @@ export const RecordTableBodyRecordGroupDragDropContextProvider = ({
}
if (currentRecordSorts.length > 0) {
setIsRemoveSortingModalOpen(true);
openModal(RECORD_INDEX_REMOVE_SORTING_MODAL_ID);
return;
}
@ -130,11 +129,11 @@ export const RecordTableBodyRecordGroupDragDropContextProvider = ({
});
},
[
currentRecordSortsCallbackState,
objectMetadataItem.fields,
recordIdsByGroupFamilyState,
updateOneRow,
setIsRemoveSortingModalOpen,
currentRecordSortsCallbackState,
openModal,
],
);

View File

@ -1,5 +0,0 @@
import { createState } from 'twenty-ui/utilities';
export const isRemoveSortingModalOpenState = createState<boolean>({
key: 'isRemoveSortingModalOpenState',
defaultValue: false,
});

View File

@ -1,5 +1,3 @@
import { useState } from 'react';
import { ConnectedAccount } from '@/accounts/types/ConnectedAccount';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useDestroyOneRecord } from '@/object-record/hooks/useDestroyOneRecord';
@ -9,6 +7,7 @@ import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { Trans, useLingui } from '@lingui/react/macro';
import {
IconCalendarEvent,
@ -25,13 +24,14 @@ type SettingsAccountsRowDropdownMenuProps = {
account: ConnectedAccount;
};
const DELETE_ACCOUNT_MODAL_ID = 'delete-account-modal';
export const SettingsAccountsRowDropdownMenu = ({
account,
}: SettingsAccountsRowDropdownMenuProps) => {
const dropdownId = `settings-account-row-${account.id}`;
const { t } = useLingui();
const [isDeleteAccountModalOpen, setIsDeleteAccountModalOpen] =
useState(false);
const { openModal } = useModal();
const navigate = useNavigateSettings();
const { closeDropdown } = useDropdown(dropdownId);
@ -43,7 +43,6 @@ export const SettingsAccountsRowDropdownMenu = ({
const deleteAccount = async () => {
await destroyOneRecord(account.id);
setIsDeleteAccountModalOpen(false);
};
return (
@ -89,16 +88,15 @@ export const SettingsAccountsRowDropdownMenu = ({
LeftIcon={IconTrash}
text={t`Remove account`}
onClick={() => {
setIsDeleteAccountModalOpen(true);
closeDropdown();
openModal(DELETE_ACCOUNT_MODAL_ID);
}}
/>
</DropdownMenuItemsContainer>
}
/>
<ConfirmationModal
isOpen={isDeleteAccountModalOpen}
setIsOpen={setIsDeleteAccountModalOpen}
modalId={DELETE_ACCOUNT_MODAL_ID}
title={t`Data deletion`}
subtitle={
<Trans>

View File

@ -1,18 +1,18 @@
import { useState } from 'react';
import { useRecoilValue } from 'recoil';
import { useAuth } from '@/auth/hooks/useAuth';
import { currentUserState } from '@/auth/states/currentUserState';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { useLingui } from '@lingui/react/macro';
import { useDeleteUserAccountMutation } from '~/generated/graphql';
import { Button } from 'twenty-ui/input';
import { H2Title } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { useDeleteUserAccountMutation } from '~/generated/graphql';
const DELETE_ACCOUNT_MODAL_ID = 'delete-account-modal';
export const DeleteAccount = () => {
const { t } = useLingui();
const [isDeleteAccountModalOpen, setIsDeleteAccountModalOpen] =
useState(false);
const { openModal } = useModal();
const [deleteUserAccount] = useDeleteUserAccountMutation();
const currentUser = useRecoilValue(currentUserState);
@ -33,7 +33,7 @@ export const DeleteAccount = () => {
<Button
accent="danger"
onClick={() => setIsDeleteAccountModalOpen(true)}
onClick={() => openModal(DELETE_ACCOUNT_MODAL_ID)}
variant="secondary"
title={t`Delete account`}
/>
@ -41,8 +41,7 @@ export const DeleteAccount = () => {
<ConfirmationModal
confirmationValue={userEmail}
confirmationPlaceholder={userEmail ?? ''}
isOpen={isDeleteAccountModalOpen}
setIsOpen={setIsDeleteAccountModalOpen}
modalId={DELETE_ACCOUNT_MODAL_ID}
title={t`Account Deletion`}
subtitle={
<>

View File

@ -1,22 +1,22 @@
import { Trans, useLingui } from '@lingui/react/macro';
import { useState } from 'react';
import { useRecoilValue } from 'recoil';
import { useAuth } from '@/auth/hooks/useAuth';
import { currentUserState } from '@/auth/states/currentUserState';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { useDeleteCurrentWorkspaceMutation } from '~/generated/graphql';
import { Button } from 'twenty-ui/input';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { H2Title, IconTrash } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { useDeleteCurrentWorkspaceMutation } from '~/generated/graphql';
const DELETE_WORKSPACE_MODAL_ID = 'delete-workspace-modal';
export const DeleteWorkspace = () => {
const [isDeleteWorkSpaceModalOpen, setIsDeleteWorkSpaceModalOpen] =
useState(false);
const [deleteCurrentWorkspace] = useDeleteCurrentWorkspaceMutation();
const currentUser = useRecoilValue(currentUserState);
const userEmail = currentUser?.email;
const { t } = useLingui();
const { openModal } = useModal();
const { signOut } = useAuth();
@ -36,14 +36,13 @@ export const DeleteWorkspace = () => {
variant="secondary"
title={t`Delete workspace`}
Icon={IconTrash}
onClick={() => setIsDeleteWorkSpaceModalOpen(true)}
onClick={() => openModal(DELETE_WORKSPACE_MODAL_ID)}
/>
<ConfirmationModal
modalId={DELETE_WORKSPACE_MODAL_ID}
confirmationPlaceholder={userEmail}
confirmationValue={userEmail}
isOpen={isDeleteWorkSpaceModalOpen}
setIsOpen={setIsDeleteWorkSpaceModalOpen}
title={t`Workspace Deletion`}
subtitle={
<Trans>

View File

@ -1,5 +1,5 @@
import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersStates';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersStates';
import { useUpdateWorkspaceMemberRole } from '@/settings/roles/hooks/useUpdateWorkspaceMemberRole';
import { SettingsRoleAssignmentConfirmationModal } from '@/settings/roles/role-assignment/components/SettingsRoleAssignmentConfirmationModal';
import { SettingsRoleAssignmentTableHeader } from '@/settings/roles/role-assignment/components/SettingsRoleAssignmentTableHeader';
@ -11,18 +11,14 @@ import { SettingsPath } from '@/types/SettingsPath';
import { TextInput } from '@/ui/input/components/TextInput';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpenedComponentState';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { useState } from 'react';
import { useRecoilValue } from 'recoil';
import {
Role,
SearchRecord,
WorkspaceMember,
} from '~/generated-metadata/graphql';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
import { SettingsRoleAssignmentTableRow } from './SettingsRoleAssignmentTableRow';
import {
AppTooltip,
H2Title,
@ -32,6 +28,14 @@ import {
} from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
import {
Role,
SearchRecord,
WorkspaceMember,
} from '~/generated-metadata/graphql';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
import { ROLE_ASSIGNMENT_CONFIRMATION_MODAL_ID } from '../constants/RoleAssignmentConfirmationModalId';
import { SettingsRoleAssignmentTableRow } from './SettingsRoleAssignmentTableRow';
const StyledAssignToMemberContainer = styled.div`
display: flex;
@ -84,8 +88,7 @@ export const SettingsRoleAssignment = ({
updateWorkspaceMemberRoleDraftState,
} = useUpdateWorkspaceMemberRole(roleId);
const [confirmationModalIsOpen, setConfirmationModalIsOpen] =
useState<boolean>(false);
const { openModal, closeModal } = useModal();
const [selectedWorkspaceMember, setSelectedWorkspaceMember] =
useState<SettingsRoleAssignmentConfirmationModalSelectedWorkspaceMember | null>(
null,
@ -135,7 +138,6 @@ export const SettingsRoleAssignment = ({
);
const handleModalClose = () => {
setConfirmationModalIsOpen(false);
setSelectedWorkspaceMember(null);
};
@ -152,12 +154,17 @@ export const SettingsRoleAssignment = ({
role: existingRole,
avatarUrl: workspaceMemberSearchRecord.imageUrl,
});
setConfirmationModalIsOpen(true);
openModal(ROLE_ASSIGNMENT_CONFIRMATION_MODAL_ID);
closeDropdown();
};
const isModalOpened = useRecoilComponentValueV2(
isModalOpenedComponentState,
ROLE_ASSIGNMENT_CONFIRMATION_MODAL_ID,
);
const handleConfirm = async () => {
if (!selectedWorkspaceMember || !confirmationModalIsOpen) return;
if (!selectedWorkspaceMember || !isModalOpened) return;
if (!isCreateMode) {
await addWorkspaceMemberToRoleAndUpdateState({
@ -188,6 +195,7 @@ export const SettingsRoleAssignment = ({
const handleRoleClick = (roleId: string) => {
navigateSettings(SettingsPath.RoleDetail, { roleId });
handleModalClose();
closeModal(ROLE_ASSIGNMENT_CONFIRMATION_MODAL_ID);
};
const handleSearchChange = (text: string) => {
@ -267,10 +275,9 @@ export const SettingsRoleAssignment = ({
</StyledAssignToMemberContainer>
</Section>
{confirmationModalIsOpen && selectedWorkspaceMember && (
{selectedWorkspaceMember && (
<SettingsRoleAssignmentConfirmationModal
selectedWorkspaceMember={selectedWorkspaceMember}
isOpen={true}
onClose={handleModalClose}
onConfirm={handleConfirm}
onRoleClick={handleRoleClick}

View File

@ -1,11 +1,11 @@
import { SettingsRoleAssignmentConfirmationModalSubtitle } from '@/settings/roles/role-assignment/components/SettingsRoleAssignmentConfirmationModalSubtitle';
import { ROLE_ASSIGNMENT_CONFIRMATION_MODAL_ID } from '@/settings/roles/role-assignment/constants/RoleAssignmentConfirmationModalId';
import { SettingsRoleAssignmentConfirmationModalSelectedWorkspaceMember } from '@/settings/roles/role-assignment/types/SettingsRoleAssignmentConfirmationModalSelectedWorkspaceMember';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { t } from '@lingui/core/macro';
type SettingsRoleAssignmentConfirmationModalProps = {
selectedWorkspaceMember: SettingsRoleAssignmentConfirmationModalSelectedWorkspaceMember;
isOpen: boolean;
onClose: () => void;
onConfirm: () => void;
onRoleClick: (roleId: string) => void;
@ -13,7 +13,6 @@ type SettingsRoleAssignmentConfirmationModalProps = {
export const SettingsRoleAssignmentConfirmationModal = ({
selectedWorkspaceMember,
isOpen,
onClose,
onConfirm,
onRoleClick,
@ -24,8 +23,7 @@ export const SettingsRoleAssignmentConfirmationModal = ({
return (
<ConfirmationModal
isOpen={isOpen}
setIsOpen={onClose}
modalId={ROLE_ASSIGNMENT_CONFIRMATION_MODAL_ID}
title={title}
subtitle={
<SettingsRoleAssignmentConfirmationModalSubtitle
@ -33,6 +31,7 @@ export const SettingsRoleAssignmentConfirmationModal = ({
onRoleClick={onRoleClick}
/>
}
onClose={onClose}
onConfirmClick={onConfirm}
confirmButtonText={t`Confirm`}
confirmButtonAccent="danger"

View File

@ -0,0 +1,2 @@
export const ROLE_ASSIGNMENT_CONFIRMATION_MODAL_ID =
'role-assignment-confirmation-modal';

View File

@ -5,14 +5,16 @@ import { ServerlessFunctionFormValues } from '@/settings/serverless-functions/ho
import { SettingsServerlessFunctionHotkeyScope } from '@/settings/serverless-functions/types/SettingsServerlessFunctionHotKeyScope';
import { SettingsPath } from '@/types/SettingsPath';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useState } from 'react';
import { Key } from 'ts-key-enum';
import { H2Title } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
import { useHotkeyScopeOnMount } from '~/hooks/useHotkeyScopeOnMount';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
import { Button } from 'twenty-ui/input';
import { H2Title } from 'twenty-ui/display';
import { Section } from 'twenty-ui/layout';
const DELETE_FUNCTION_MODAL_ID = 'delete-function-modal';
export const SettingsServerlessFunctionSettingsTab = ({
formValues,
@ -26,8 +28,7 @@ export const SettingsServerlessFunctionSettingsTab = ({
onCodeChange: (filePath: string, value: string) => void;
}) => {
const navigate = useNavigateSettings();
const [isDeleteFunctionModalOpen, setIsDeleteFunctionModalOpen] =
useState(false);
const { openModal } = useModal();
const { deleteOneServerlessFunction } = useDeleteOneServerlessFunction();
const deleteFunction = async () => {
@ -42,7 +43,7 @@ export const SettingsServerlessFunctionSettingsTab = ({
useScopedHotkeys(
[Key.Delete],
() => {
setIsDeleteFunctionModalOpen(true);
openModal(DELETE_FUNCTION_MODAL_ID);
},
SettingsServerlessFunctionHotkeyScope.ServerlessFunctionSettingsTab,
);
@ -68,7 +69,7 @@ export const SettingsServerlessFunctionSettingsTab = ({
<H2Title title="Danger zone" description="Delete this function" />
<Button
accent="danger"
onClick={() => setIsDeleteFunctionModalOpen(true)}
onClick={() => openModal(DELETE_FUNCTION_MODAL_ID)}
variant="secondary"
size="small"
title="Delete function"
@ -77,8 +78,7 @@ export const SettingsServerlessFunctionSettingsTab = ({
<ConfirmationModal
confirmationValue={formValues.name}
confirmationPlaceholder={formValues.name}
isOpen={isDeleteFunctionModalOpen}
setIsOpen={setIsDeleteFunctionModalOpen}
modalId={DELETE_FUNCTION_MODAL_ID}
title="Function Deletion"
subtitle={
<>

View File

@ -1,7 +1,7 @@
import { defaultSpreadsheetImportProps } from '@/spreadsheet-import/provider/components/SpreadsheetImport';
import {
SpreadsheetImportDialogOptions,
SpreadsheetImportFields
SpreadsheetImportDialogOptions,
SpreadsheetImportFields
} from '@/spreadsheet-import/types';
import { SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetColumns';
import { FieldMetadataType } from 'twenty-shared/types';
@ -131,7 +131,6 @@ export const mockRsiValues = mockComponentBehaviourForTypes({
onSubmit: async () => {
return;
},
isOpen: true,
onClose: () => {
return;
},

View File

@ -19,11 +19,13 @@ const StyledCloseButtonContainer = styled.div`
top: 0;
`;
type ModalCloseButtonProps = {
onClose: () => void;
type SpreadSheetImportModalCloseButtonProps = {
onClose?: () => void;
};
export const ModalCloseButton = ({ onClose }: ModalCloseButtonProps) => {
export const SpreadSheetImportModalCloseButton = ({
onClose,
}: SpreadSheetImportModalCloseButtonProps) => {
const { initialStepState } = useSpreadsheetImportInternal();
const { initialStep } = useSpreadsheetImportInitialStep(
@ -40,7 +42,7 @@ export const ModalCloseButton = ({ onClose }: ModalCloseButtonProps) => {
const handleClose = () => {
if (activeStep === -1) {
onClose();
onClose?.();
return;
}
enqueueDialog({

View File

@ -3,8 +3,8 @@ import styled from '@emotion/styled';
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
import { Modal } from '@/ui/layout/modal/components/Modal';
import { ModalCloseButton } from './ModalCloseButton';
import { MOBILE_VIEWPORT } from 'twenty-ui/theme';
import { SpreadSheetImportModalCloseButton } from './SpreadSheetImportModalCloseButton';
const StyledModal = styled(Modal)`
height: 61%;
@ -27,29 +27,30 @@ const StyledRtlLtr = styled.div`
flex-direction: column;
`;
type ModalWrapperProps = {
type SpreadSheetImportModalWrapperProps = {
children: React.ReactNode;
isOpen: boolean;
onClose: () => void;
modalId: string;
onClose?: () => void;
};
export const ModalWrapper = ({
export const SpreadSheetImportModalWrapper = ({
modalId,
children,
isOpen,
onClose,
}: ModalWrapperProps) => {
}: SpreadSheetImportModalWrapperProps) => {
const { rtl } = useSpreadsheetImportInternal();
return (
<>
{isOpen && (
<StyledModal size="large">
<StyledRtlLtr dir={rtl ? 'rtl' : 'ltr'}>
<ModalCloseButton onClose={onClose} />
{children}
</StyledRtlLtr>
</StyledModal>
)}
</>
<StyledModal
size="large"
modalId={modalId}
isClosable={true}
onClose={onClose}
>
<StyledRtlLtr dir={rtl ? 'rtl' : 'ltr'}>
<SpreadSheetImportModalCloseButton onClose={onClose} />
{children}
</StyledRtlLtr>
</StyledModal>
);
};

View File

@ -0,0 +1 @@
export const SPREADSHEET_IMPORT_MODAL_ID = 'spreadsheet-import';

View File

@ -17,7 +17,6 @@ type SpreadsheetKey = 'spreadsheet_key';
export const mockedSpreadsheetOptions: SpreadsheetImportDialogOptions<SpreadsheetKey> =
{
isOpen: true,
onClose: () => {},
fields: [],
uploadStepHook: async () => [],

View File

@ -1,14 +1,18 @@
import { useSetRecoilState } from 'recoil';
import { SPREADSHEET_IMPORT_MODAL_ID } from '@/spreadsheet-import/constants/SpreadsheetImportModalId';
import { spreadsheetImportDialogState } from '@/spreadsheet-import/states/spreadsheetImportDialogState';
import { SpreadsheetImportDialogOptions } from '@/spreadsheet-import/types';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
export const useOpenSpreadsheetImportDialog = <T extends string>() => {
const setSpreadSheetImport = useSetRecoilState(spreadsheetImportDialogState);
const { openModal } = useModal();
const openSpreadsheetImportDialog = (
options: Omit<SpreadsheetImportDialogOptions<T>, 'isOpen' | 'onClose'>,
) => {
openModal(SPREADSHEET_IMPORT_MODAL_ID);
setSpreadSheetImport({
isOpen: true,
options,

View File

@ -1,5 +1,6 @@
import { ModalWrapper } from '@/spreadsheet-import/components/ModalWrapper';
import { ReactSpreadsheetImportContextProvider } from '@/spreadsheet-import/components/ReactSpreadsheetImportContextProvider';
import { SpreadSheetImportModalWrapper } from '@/spreadsheet-import/components/SpreadSheetImportModalWrapper';
import { SPREADSHEET_IMPORT_MODAL_ID } from '@/spreadsheet-import/constants/SpreadsheetImportModalId';
import { SpreadsheetImportStepperContainer } from '@/spreadsheet-import/steps/components/SpreadsheetImportStepperContainer';
import { SpreadsheetImportDialogOptions as SpreadsheetImportProps } from '@/spreadsheet-import/types';
@ -31,9 +32,12 @@ export const SpreadsheetImport = <T extends string>(
return (
<ReactSpreadsheetImportContextProvider values={mergedProps}>
<ModalWrapper isOpen={mergedProps.isOpen} onClose={mergedProps.onClose}>
<SpreadSheetImportModalWrapper
modalId={SPREADSHEET_IMPORT_MODAL_ID}
onClose={mergedProps.onClose}
>
<SpreadsheetImportStepperContainer />
</ModalWrapper>
</SpreadSheetImportModalWrapper>
</ReactSpreadsheetImportContextProvider>
);
};

View File

@ -3,7 +3,9 @@ import { useRecoilState, useSetRecoilState } from 'recoil';
import { spreadsheetImportDialogState } from '@/spreadsheet-import/states/spreadsheetImportDialogState';
import { SPREADSHEET_IMPORT_MODAL_ID } from '@/spreadsheet-import/constants/SpreadsheetImportModalId';
import { matchColumnsState } from '@/spreadsheet-import/steps/components/MatchColumnsStep/components/states/initialComputedColumnsState';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { SpreadsheetImport } from './SpreadsheetImport';
type SpreadsheetImportProviderProps = React.PropsWithChildren;
@ -17,12 +19,16 @@ export const SpreadsheetImportProvider = (
const setMatchColumnsState = useSetRecoilState(matchColumnsState);
const { closeModal } = useModal();
const handleClose = () => {
setSpreadsheetImportDialog({
isOpen: false,
options: null,
});
closeModal(SPREADSHEET_IMPORT_MODAL_ID);
setMatchColumnsState([]);
};
@ -31,7 +37,6 @@ export const SpreadsheetImportProvider = (
{props.children}
{spreadsheetImportDialog.isOpen && spreadsheetImportDialog.options && (
<SpreadsheetImport
isOpen={true}
onClose={handleClose}
// eslint-disable-next-line react/jsx-props-no-spreading
{...spreadsheetImportDialog.options}

View File

@ -1,8 +1,8 @@
import { Meta } from '@storybook/react';
import { mockRsiValues } from '@/spreadsheet-import/__mocks__/mockRsiValues';
import { ModalWrapper } from '@/spreadsheet-import/components/ModalWrapper';
import { ReactSpreadsheetImportContextProvider } from '@/spreadsheet-import/components/ReactSpreadsheetImportContextProvider';
import { SpreadSheetImportModalWrapper } from '@/spreadsheet-import/components/SpreadSheetImportModalWrapper';
import { MatchColumnsStep } from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep';
import { SpreadsheetImportStep } from '@/spreadsheet-import/steps/types/SpreadsheetImportStep';
import { DialogManagerScope } from '@/ui/feedback/dialog-manager/scopes/DialogManagerScope';
@ -64,7 +64,10 @@ const mockData = [
export const Default = () => (
<DialogManagerScope dialogManagerScopeId="dialog-manager">
<ReactSpreadsheetImportContextProvider values={mockRsiValues}>
<ModalWrapper isOpen={true} onClose={() => null}>
<SpreadSheetImportModalWrapper
modalId="match-columns-step"
onClose={() => null}
>
<MatchColumnsStep
headerValues={mockData[0] as string[]}
data={mockData.slice(1)}
@ -75,7 +78,7 @@ export const Default = () => (
nextStep={() => null}
onError={() => null}
/>
</ModalWrapper>
</SpreadSheetImportModalWrapper>
</ReactSpreadsheetImportContextProvider>
</DialogManagerScope>
);

View File

@ -4,11 +4,13 @@ import {
headerSelectionTableFields,
mockRsiValues,
} from '@/spreadsheet-import/__mocks__/mockRsiValues';
import { ModalWrapper } from '@/spreadsheet-import/components/ModalWrapper';
import { ReactSpreadsheetImportContextProvider } from '@/spreadsheet-import/components/ReactSpreadsheetImportContextProvider';
import { SpreadSheetImportModalWrapper } from '@/spreadsheet-import/components/SpreadSheetImportModalWrapper';
import { SelectHeaderStep } from '@/spreadsheet-import/steps/components/SelectHeaderStep/SelectHeaderStep';
import { SpreadsheetImportStepType } from '@/spreadsheet-import/steps/types/SpreadsheetImportStepType';
import { DialogManagerScope } from '@/ui/feedback/dialog-manager/scopes/DialogManagerScope';
import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpenedComponentState';
import { RecoilRoot } from 'recoil';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
const meta: Meta<typeof SelectHeaderStep> = {
@ -17,15 +19,30 @@ const meta: Meta<typeof SelectHeaderStep> = {
parameters: {
layout: 'fullscreen',
},
decorators: [I18nFrontDecorator],
decorators: [
(Story) => (
<RecoilRoot
initializeState={({ set }) => {
set(
isModalOpenedComponentState.atomFamily({
instanceId: 'select-header-step',
}),
true,
);
}}
>
<Story />
</RecoilRoot>
),
I18nFrontDecorator,
],
};
export default meta;
export const Default = () => (
<DialogManagerScope dialogManagerScopeId="dialog-manager">
<ReactSpreadsheetImportContextProvider values={mockRsiValues}>
<ModalWrapper isOpen={true} onClose={() => null}>
<SpreadSheetImportModalWrapper modalId="select-header-step">
<SelectHeaderStep
importedRows={headerSelectionTableFields}
setCurrentStepState={() => null}
@ -38,7 +55,7 @@ export const Default = () => (
data: headerSelectionTableFields,
}}
/>
</ModalWrapper>
</SpreadSheetImportModalWrapper>
</ReactSpreadsheetImportContextProvider>
</DialogManagerScope>
);

View File

@ -1,11 +1,13 @@
import { Meta } from '@storybook/react';
import { mockRsiValues } from '@/spreadsheet-import/__mocks__/mockRsiValues';
import { ModalWrapper } from '@/spreadsheet-import/components/ModalWrapper';
import { ReactSpreadsheetImportContextProvider } from '@/spreadsheet-import/components/ReactSpreadsheetImportContextProvider';
import { SpreadSheetImportModalWrapper } from '@/spreadsheet-import/components/SpreadSheetImportModalWrapper';
import { SelectSheetStep } from '@/spreadsheet-import/steps/components/SelectSheetStep/SelectSheetStep';
import { SpreadsheetImportStepType } from '@/spreadsheet-import/steps/types/SpreadsheetImportStepType';
import { DialogManagerScope } from '@/ui/feedback/dialog-manager/scopes/DialogManagerScope';
import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpenedComponentState';
import { RecoilRoot } from 'recoil';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
const meta: Meta<typeof SelectSheetStep> = {
@ -14,7 +16,23 @@ const meta: Meta<typeof SelectSheetStep> = {
parameters: {
layout: 'fullscreen',
},
decorators: [I18nFrontDecorator],
decorators: [
(Story) => (
<RecoilRoot
initializeState={({ set }) => {
set(
isModalOpenedComponentState.atomFamily({
instanceId: 'select-sheet-step',
}),
true,
);
}}
>
<Story />
</RecoilRoot>
),
I18nFrontDecorator,
],
};
export default meta;
@ -24,7 +42,10 @@ const sheetNames = ['Sheet1', 'Sheet2', 'Sheet3'];
export const Default = () => (
<DialogManagerScope dialogManagerScopeId="dialog-manager">
<ReactSpreadsheetImportContextProvider values={mockRsiValues}>
<ModalWrapper isOpen={true} onClose={() => null}>
<SpreadSheetImportModalWrapper
modalId="select-sheet-step"
onClose={() => null}
>
<SelectSheetStep
sheetNames={sheetNames}
setCurrentStepState={() => {}}
@ -55,7 +76,7 @@ export const Default = () => (
onError={() => null}
onBack={() => Promise.resolve()}
/>
</ModalWrapper>
</SpreadSheetImportModalWrapper>
</ReactSpreadsheetImportContextProvider>
</DialogManagerScope>
);

View File

@ -1,11 +1,13 @@
import { Meta } from '@storybook/react';
import { mockRsiValues } from '@/spreadsheet-import/__mocks__/mockRsiValues';
import { ModalWrapper } from '@/spreadsheet-import/components/ModalWrapper';
import { ReactSpreadsheetImportContextProvider } from '@/spreadsheet-import/components/ReactSpreadsheetImportContextProvider';
import { SpreadSheetImportModalWrapper } from '@/spreadsheet-import/components/SpreadSheetImportModalWrapper';
import { UploadStep } from '@/spreadsheet-import/steps/components/UploadStep/UploadStep';
import { SpreadsheetImportStepType } from '@/spreadsheet-import/steps/types/SpreadsheetImportStepType';
import { DialogManagerScope } from '@/ui/feedback/dialog-manager/scopes/DialogManagerScope';
import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpenedComponentState';
import { RecoilRoot } from 'recoil';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
@ -15,7 +17,24 @@ const meta: Meta<typeof UploadStep> = {
parameters: {
layout: 'fullscreen',
},
decorators: [SnackBarDecorator, I18nFrontDecorator],
decorators: [
(Story) => (
<RecoilRoot
initializeState={({ set }) => {
set(
isModalOpenedComponentState.atomFamily({
instanceId: 'upload-step',
}),
true,
);
}}
>
<Story />
</RecoilRoot>
),
SnackBarDecorator,
I18nFrontDecorator,
],
};
export default meta;
@ -23,7 +42,7 @@ export default meta;
export const Default = () => (
<DialogManagerScope dialogManagerScopeId="dialog-manager">
<ReactSpreadsheetImportContextProvider values={mockRsiValues}>
<ModalWrapper isOpen={true} onClose={() => null}>
<SpreadSheetImportModalWrapper modalId="upload-step" onClose={() => null}>
<UploadStep
setUploadedFile={() => null}
setCurrentStepState={() => null}
@ -32,7 +51,7 @@ export const Default = () => (
setPreviousStepState={() => null}
currentStepState={{ type: SpreadsheetImportStepType.upload }}
/>
</ModalWrapper>
</SpreadSheetImportModalWrapper>
</ReactSpreadsheetImportContextProvider>
</DialogManagerScope>
);

View File

@ -5,18 +5,37 @@ import {
importedColums,
mockRsiValues,
} from '@/spreadsheet-import/__mocks__/mockRsiValues';
import { ModalWrapper } from '@/spreadsheet-import/components/ModalWrapper';
import { ReactSpreadsheetImportContextProvider } from '@/spreadsheet-import/components/ReactSpreadsheetImportContextProvider';
import { SpreadSheetImportModalWrapper } from '@/spreadsheet-import/components/SpreadSheetImportModalWrapper';
import { ValidationStep } from '@/spreadsheet-import/steps/components/ValidationStep/ValidationStep';
import { DialogManagerScope } from '@/ui/feedback/dialog-manager/scopes/DialogManagerScope';
import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpenedComponentState';
import { RecoilRoot } from 'recoil';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
const meta: Meta<typeof ValidationStep> = {
title: 'Modules/SpreadsheetImport/ValidationStep',
component: ValidationStep,
parameters: {
layout: 'fullscreen',
},
decorators: [I18nFrontDecorator],
decorators: [
(Story) => (
<RecoilRoot
initializeState={({ set }) => {
set(
isModalOpenedComponentState.atomFamily({
instanceId: 'validation-step',
}),
true,
);
}}
>
<Story />
</RecoilRoot>
),
I18nFrontDecorator,
],
};
export default meta;
@ -26,7 +45,10 @@ const file = new File([''], 'file.csv');
export const Default = () => (
<DialogManagerScope dialogManagerScopeId="dialog-manager">
<ReactSpreadsheetImportContextProvider values={mockRsiValues}>
<ModalWrapper isOpen={true} onClose={() => null}>
<SpreadSheetImportModalWrapper
modalId="validation-step"
onClose={() => null}
>
<ValidationStep
initialData={editableTableInitialData}
file={file}
@ -34,7 +56,7 @@ export const Default = () => (
onBack={() => Promise.resolve()}
setCurrentStepState={() => null}
/>
</ModalWrapper>
</SpreadSheetImportModalWrapper>
</ReactSpreadsheetImportContextProvider>
</DialogManagerScope>
);

View File

@ -9,8 +9,6 @@ import { SpreadsheetImportTableHook } from '@/spreadsheet-import/types/Spreadshe
import { SpreadsheetImportStep } from '../steps/types/SpreadsheetImportStep';
export type SpreadsheetImportDialogOptions<FieldNames extends string> = {
// Is modal visible.
isOpen: boolean;
// callback when RSI is closed before final submit
onClose: () => void;
// Field description for requested data

View File

@ -6,17 +6,18 @@ import { useDebouncedCallback } from 'use-debounce';
import { TextInput } from '@/ui/input/components/TextInput';
import { Modal, ModalVariants } from '@/ui/layout/modal/components/Modal';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { useLingui } from '@lingui/react/macro';
import { Button, ButtonAccent } from 'twenty-ui/input';
import { H1Title, H1TitleFontColor } from 'twenty-ui/display';
import { Button, ButtonAccent } from 'twenty-ui/input';
import { Section, SectionAlignment, SectionFontColor } from 'twenty-ui/layout';
export type ConfirmationModalProps = {
isOpen: boolean;
modalId: string;
title: string;
loading?: boolean;
subtitle: ReactNode;
setIsOpen: (val: boolean) => void;
onClose?: () => void;
onConfirmClick: () => void;
confirmButtonText?: string;
confirmationPlaceholder?: string;
@ -58,12 +59,12 @@ export const StyledConfirmationButton = styled(StyledCenteredButton)`
`;
export const ConfirmationModal = ({
isOpen = false,
modalId,
title,
loading,
subtitle,
setIsOpen,
onConfirmClick,
onClose,
confirmButtonText = 'Confirm',
confirmationValue,
confirmationPlaceholder,
@ -88,10 +89,16 @@ export const ConfirmationModal = ({
250,
);
const handleConfirmClick = () => {
onConfirmClick();
const { closeModal } = useModal();
setIsOpen(false);
const handleConfirmClick = () => {
closeModal(modalId);
onConfirmClick();
};
const handleCancelClick = () => {
closeModal(modalId);
onClose?.();
};
const handleEnter = () => {
@ -103,63 +110,59 @@ export const ConfirmationModal = ({
return (
<AnimatePresence mode="wait">
<LayoutGroup>
{isOpen && (
<StyledConfirmationModal
onClose={() => {
if (isOpen) {
setIsOpen(false);
}
}}
onEnter={handleEnter}
isClosable={true}
padding="large"
modalVariant={modalVariant}
className="confirmation-modal"
<StyledConfirmationModal
modalId={modalId}
onClose={() => {
onClose?.();
}}
onEnter={handleEnter}
isClosable={true}
padding="large"
modalVariant={modalVariant}
className="confirmation-modal"
>
<StyledCenteredTitle>
<H1Title title={title} fontColor={H1TitleFontColor.Primary} />
</StyledCenteredTitle>
<StyledSection
alignment={SectionAlignment.Center}
fontColor={SectionFontColor.Primary}
>
<StyledCenteredTitle>
<H1Title title={title} fontColor={H1TitleFontColor.Primary} />
</StyledCenteredTitle>
<StyledSection
alignment={SectionAlignment.Center}
fontColor={SectionFontColor.Primary}
>
{subtitle}
</StyledSection>
{confirmationValue && (
<Section>
<TextInput
dataTestId="confirmation-modal-input"
value={inputConfirmationValue}
onChange={handleInputConfimrationValueChange}
placeholder={confirmationPlaceholder}
fullWidth
disableHotkeys
key={'input-' + confirmationValue}
/>
</Section>
)}
<StyledCenteredButton
onClick={() => {
setIsOpen(false);
}}
variant="secondary"
title={t`Cancel`}
fullWidth
/>
{subtitle}
</StyledSection>
{confirmationValue && (
<Section>
<TextInput
dataTestId="confirmation-modal-input"
value={inputConfirmationValue}
onChange={handleInputConfimrationValueChange}
placeholder={confirmationPlaceholder}
fullWidth
disableHotkeys
key={'input-' + confirmationValue}
/>
</Section>
)}
<StyledCenteredButton
onClick={handleCancelClick}
variant="secondary"
title={t`Cancel`}
fullWidth
dataTestId="confirmation-modal-cancel-button"
/>
{AdditionalButtons}
{AdditionalButtons}
<StyledCenteredButton
onClick={handleConfirmClick}
variant="secondary"
accent={confirmButtonAccent}
title={confirmButtonText}
disabled={!isValidValue || loading}
fullWidth
dataTestId="confirmation-modal-confirm-button"
/>
</StyledConfirmationModal>
)}
<StyledCenteredButton
onClick={handleConfirmClick}
variant="secondary"
accent={confirmButtonAccent}
title={confirmButtonText}
disabled={!isValidValue || loading}
fullWidth
dataTestId="confirmation-modal-confirm-button"
/>
</StyledConfirmationModal>
</LayoutGroup>
</AnimatePresence>
);

View File

@ -1,18 +1,16 @@
import { RootStackingContextZIndices } from '@/ui/layout/constants/RootStackingContextZIndices';
import { ModalHotkeysAndClickOutsideEffect } from '@/ui/layout/modal/components/ModalHotkeysAndClickOutsideEffect';
import { ModalHotkeyScope } from '@/ui/layout/modal/components/types/ModalHotkeyScope';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import {
ClickOutsideMode,
useListenClickOutside,
} from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { ModalComponentInstanceContext } from '@/ui/layout/modal/contexts/ModalComponentInstanceContext';
import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpenedComponentState';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { css, useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { motion } from 'framer-motion';
import React, { useEffect, useRef } from 'react';
import { Key } from 'ts-key-enum';
import React, { useRef } from 'react';
const StyledModalDiv = styled(motion.div)<{
size?: ModalSize;
padding?: ModalPadding;
@ -169,6 +167,7 @@ export type ModalPadding = 'none' | 'small' | 'medium' | 'large';
export type ModalVariants = 'primary' | 'secondary' | 'tertiary';
export type ModalProps = React.PropsWithChildren & {
modalId: string;
size?: ModalSize;
padding?: ModalPadding;
className?: string;
@ -176,7 +175,7 @@ export type ModalProps = React.PropsWithChildren & {
onEnter?: () => void;
modalVariant?: ModalVariants;
} & (
| { isClosable: true; onClose: () => void }
| { isClosable: true; onClose?: () => void }
| { isClosable?: false; onClose?: never }
);
@ -187,11 +186,11 @@ const modalAnimation = {
};
export const Modal = ({
modalId,
children,
size = 'medium',
padding = 'medium',
className,
hotkeyScope = ModalHotkeyScope.Default,
onEnter,
isClosable = false,
onClose,
@ -200,80 +199,65 @@ export const Modal = ({
const isMobile = useIsMobile();
const modalRef = useRef<HTMLDivElement>(null);
const {
goBackToPreviousHotkeyScope,
setHotkeyScopeAndMemorizePreviousScope,
} = usePreviousHotkeyScope();
useEffect(() => {
setHotkeyScopeAndMemorizePreviousScope(hotkeyScope);
return () => {
goBackToPreviousHotkeyScope();
};
}, [
hotkeyScope,
setHotkeyScopeAndMemorizePreviousScope,
goBackToPreviousHotkeyScope,
]);
useScopedHotkeys(
[Key.Enter],
() => {
onEnter?.();
},
hotkeyScope,
);
useScopedHotkeys(
[Key.Escape],
() => {
if (isClosable && onClose !== undefined) {
onClose();
}
},
hotkeyScope,
);
useListenClickOutside({
refs: [modalRef],
listenerId: 'MODAL_CLICK_OUTSIDE_LISTENER_ID',
callback: () => {
if (isClosable && onClose !== undefined) {
onClose();
}
},
mode: ClickOutsideMode.comparePixels,
});
const theme = useTheme();
const stopEventPropagation = (e: React.MouseEvent) => {
e.stopPropagation();
};
const theme = useTheme();
const isModalOpened = useRecoilComponentValueV2(
isModalOpenedComponentState,
modalId,
);
const { closeModal } = useModal();
const handleClose = () => {
onClose?.();
closeModal(modalId);
};
return (
<StyledBackDrop
className="modal-backdrop"
onMouseDown={stopEventPropagation}
modalVariant={modalVariant}
>
<StyledModalDiv
ref={modalRef}
size={size}
padding={padding}
initial="hidden"
animate="visible"
exit="exit"
layout
modalVariant={modalVariant}
variants={modalAnimation}
transition={{ duration: theme.animation.duration.normal }}
className={className}
isMobile={isMobile}
>
{children}
</StyledModalDiv>
</StyledBackDrop>
<>
{isModalOpened && (
<ModalComponentInstanceContext.Provider
value={{
instanceId: modalId,
}}
>
<ModalHotkeysAndClickOutsideEffect
modalId={modalId}
modalRef={modalRef}
onEnter={onEnter}
isClosable={isClosable}
onClose={handleClose}
/>
<StyledBackDrop
data-testid="modal-backdrop"
className="modal-backdrop"
onMouseDown={stopEventPropagation}
modalVariant={modalVariant}
>
<StyledModalDiv
ref={modalRef}
size={size}
padding={padding}
initial="hidden"
animate="visible"
exit="exit"
layout
modalVariant={modalVariant}
variants={modalAnimation}
transition={{ duration: theme.animation.duration.normal }}
className={className}
isMobile={isMobile}
>
{children}
</StyledModalDiv>
</StyledBackDrop>
</ModalComponentInstanceContext.Provider>
)}
</>
);
};

View File

@ -0,0 +1,54 @@
import { ModalHotkeyScope } from '@/ui/layout/modal/components/types/ModalHotkeyScope';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import {
ClickOutsideMode,
useListenClickOutside,
} from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { Key } from 'ts-key-enum';
type ModalHotkeysAndClickOutsideEffectProps = {
modalRef: React.RefObject<HTMLDivElement>;
onEnter?: () => void;
isClosable?: boolean;
onClose?: () => void;
modalId: string;
};
export const ModalHotkeysAndClickOutsideEffect = ({
modalRef,
onEnter,
isClosable = false,
onClose,
modalId,
}: ModalHotkeysAndClickOutsideEffectProps) => {
useScopedHotkeys(
[Key.Enter],
() => {
onEnter?.();
},
ModalHotkeyScope.ModalFocus,
);
useScopedHotkeys(
[Key.Escape],
() => {
if (isClosable && onClose !== undefined) {
onClose();
}
},
ModalHotkeyScope.ModalFocus,
);
useListenClickOutside({
refs: [modalRef],
listenerId: `MODAL_CLICK_OUTSIDE_LISTENER_ID_${modalId}`,
callback: () => {
if (isClosable && onClose !== undefined) {
onClose();
}
},
mode: ClickOutsideMode.compareHTMLRef,
});
return null;
};

View File

@ -1,26 +1,58 @@
import { Meta, StoryObj } from '@storybook/react';
import { expect, fn, userEvent, waitFor, within } from '@storybook/test';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { ConfirmationModal } from '../ConfirmationModal';
import { ModalHotkeyScope } from '@/ui/layout/modal/components/types/ModalHotkeyScope';
import { currentHotkeyScopeState } from '@/ui/utilities/hotkey/states/internal/currentHotkeyScopeState';
import { internalHotkeysEnabledScopesState } from '@/ui/utilities/hotkey/states/internal/internalHotkeysEnabledScopesState';
import { ComponentDecorator } from 'twenty-ui/testing';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { RootDecorator } from '~/testing/decorators/RootDecorator';
import { isModalOpenedComponentState } from '../../states/isModalOpenedComponentState';
import { ConfirmationModal } from '../ConfirmationModal';
const initializeState = ({ set }: { set: (atom: any, value: any) => void }) => {
set(
isModalOpenedComponentState.atomFamily({
instanceId: 'confirmation-modal',
}),
true,
);
set(currentHotkeyScopeState, {
scope: ModalHotkeyScope.ModalFocus,
customScopes: {
commandMenu: true,
goto: false,
keyboardShortcutMenu: false,
},
});
set(internalHotkeysEnabledScopesState, [ModalHotkeyScope.ModalFocus]);
};
const meta: Meta<typeof ConfirmationModal> = {
title: 'UI/Layout/Modal/ConfirmationModal',
component: ConfirmationModal,
decorators: [ComponentDecorator, I18nFrontDecorator],
decorators: [RootDecorator, ComponentDecorator, I18nFrontDecorator],
parameters: {
initializeState,
disableHotkeyInitialization: true,
},
};
export default meta;
type Story = StoryObj<typeof ConfirmationModal>;
const closeMock = fn();
const confirmMock = fn();
export const Default: Story = {
args: {
isOpen: true,
modalId: 'confirmation-modal',
title: 'Pariatur labore.',
subtitle: 'Velit dolore aliquip laborum occaecat fugiat.',
confirmButtonText: 'Delete',
},
decorators: [ComponentDecorator],
};
export const InputConfirmation: Story = {
@ -29,5 +61,120 @@ export const InputConfirmation: Story = {
confirmationPlaceholder: 'email@test.dev',
...Default.args,
},
decorators: Default.decorators,
};
export const CloseOnEscape: Story = {
args: {
modalId: 'confirmation-modal',
title: 'Escape Key Test',
subtitle: 'This modal should close when pressing the Escape key.',
confirmButtonText: 'Confirm',
onClose: closeMock,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByText('Escape Key Test');
closeMock.mockClear();
await userEvent.keyboard('{Escape}');
await waitFor(() => {
expect(closeMock).toHaveBeenCalledTimes(1);
});
},
};
export const CloseOnClickOutside: Story = {
args: {
modalId: 'confirmation-modal',
title: 'Click Outside Test',
subtitle: 'This modal should close when clicking outside of it.',
confirmButtonText: 'Confirm',
onClose: closeMock,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByText('Click Outside Test');
const backdrop = await canvas.findByTestId('modal-backdrop');
await userEvent.click(backdrop);
await waitFor(() => {
expect(closeMock).toHaveBeenCalledTimes(1);
});
},
};
export const ConfirmWithEnterKey: Story = {
args: {
modalId: 'confirmation-modal',
title: 'Enter Key Test',
subtitle: 'This modal should confirm when pressing the Enter key.',
confirmButtonText: 'Confirm',
onConfirmClick: confirmMock,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByText('Enter Key Test');
await userEvent.keyboard('{Enter}');
await waitFor(() => {
expect(confirmMock).toHaveBeenCalledTimes(1);
});
},
};
export const CancelButtonClick: Story = {
args: {
modalId: 'confirmation-modal',
title: 'Cancel Button Test',
subtitle: 'Clicking the cancel button should close the modal',
confirmButtonText: 'Confirm',
onClose: closeMock,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByText('Cancel Button Test');
const cancelButton = await canvas.findByRole('button', {
name: /Cancel/,
});
await userEvent.click(cancelButton);
await waitFor(() => {
expect(closeMock).toHaveBeenCalledTimes(1);
});
},
};
export const ConfirmButtonClick: Story = {
args: {
modalId: 'confirmation-modal',
title: 'Confirm Button Test',
subtitle: 'Clicking the confirm button should trigger the confirm action',
confirmButtonText: 'Confirm',
onConfirmClick: confirmMock,
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByText('Confirm Button Test');
const confirmButton = await canvas.findByRole('button', {
name: /Confirm/,
});
await userEvent.click(confirmButton);
await waitFor(() => {
expect(confirmMock).toHaveBeenCalledTimes(1);
});
},
};

View File

@ -1,18 +1,53 @@
import { Meta, StoryObj } from '@storybook/react';
import { expect, fn, userEvent, waitFor, within } from '@storybook/test';
import { Modal } from '../Modal';
import { ModalHotkeyScope } from '@/ui/layout/modal/components/types/ModalHotkeyScope';
import { currentHotkeyScopeState } from '@/ui/utilities/hotkey/states/internal/currentHotkeyScopeState';
import { internalHotkeysEnabledScopesState } from '@/ui/utilities/hotkey/states/internal/internalHotkeysEnabledScopesState';
import { ComponentDecorator } from 'twenty-ui/testing';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { RootDecorator } from '~/testing/decorators/RootDecorator';
import { isModalOpenedComponentState } from '../../states/isModalOpenedComponentState';
import { Modal } from '../Modal';
const initializeState = ({ set }: { set: (atom: any, value: any) => void }) => {
set(
isModalOpenedComponentState.atomFamily({
instanceId: 'modal-id',
}),
true,
);
set(currentHotkeyScopeState, {
scope: ModalHotkeyScope.ModalFocus,
customScopes: {
commandMenu: true,
goto: false,
keyboardShortcutMenu: false,
},
});
set(internalHotkeysEnabledScopesState, [ModalHotkeyScope.ModalFocus]);
};
const meta: Meta<typeof Modal> = {
title: 'UI/Layout/Modal/Modal',
component: Modal,
decorators: [I18nFrontDecorator, RootDecorator, ComponentDecorator],
parameters: {
initializeState,
disableHotkeyInitialization: true,
},
};
export default meta;
type Story = StoryObj<typeof Modal>;
const closeMock = fn();
export const Default: Story = {
args: {
modalId: 'modal-id',
size: 'medium',
padding: 'medium',
children: (
@ -29,8 +64,63 @@ export const Default: Story = {
</>
),
},
decorators: [ComponentDecorator],
argTypes: {
children: { control: false },
};
export const CloseClosableModalOnClickOutside: Story = {
args: {
modalId: 'modal-id',
size: 'medium',
padding: 'medium',
isClosable: true,
onClose: closeMock,
children: (
<>
<Modal.Header>Click Outside Test</Modal.Header>
<Modal.Content>
This modal should close when clicking outside of it.
</Modal.Content>
</>
),
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByText('Click Outside Test');
const backdrop = document.querySelector('.modal-backdrop') as HTMLElement;
await userEvent.click(backdrop);
await waitFor(() => {
expect(closeMock).toHaveBeenCalledTimes(1);
});
},
};
export const CloseClosableModalOnEscape: Story = {
args: {
modalId: 'modal-id',
size: 'medium',
padding: 'medium',
isClosable: true,
onClose: closeMock,
children: (
<>
<Modal.Header>Escape Key Test</Modal.Header>
<Modal.Content>
This modal should close when pressing the Escape key.
</Modal.Content>
</>
),
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByText('Escape Key Test');
await userEvent.keyboard('{Escape}');
await waitFor(() => {
expect(closeMock).toHaveBeenCalledTimes(1);
});
},
};

View File

@ -1,3 +1,3 @@
export enum ModalHotkeyScope {
Default = 'default',
ModalFocus = 'modal-focus',
}

View File

@ -0,0 +1,3 @@
import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext';
export const ModalComponentInstanceContext = createComponentInstanceContext();

View File

@ -0,0 +1,181 @@
import { renderHook } from '@testing-library/react';
import { RecoilRoot, useRecoilValue } from 'recoil';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpenedComponentState';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { act } from 'react';
jest.mock('@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope');
const mockSetHotkeyScopeAndMemorizePreviousScope = jest.fn();
const mockGoBackToPreviousHotkeyScope = jest.fn();
const modalId = 'test-modal-id';
const customHotkeyScope: HotkeyScope = {
scope: 'test-scope',
customScopes: {
goto: true,
commandMenu: true,
},
};
describe('useModal', () => {
beforeEach(() => {
jest.clearAllMocks();
(usePreviousHotkeyScope as jest.Mock).mockReturnValue({
setHotkeyScopeAndMemorizePreviousScope:
mockSetHotkeyScopeAndMemorizePreviousScope,
goBackToPreviousHotkeyScope: mockGoBackToPreviousHotkeyScope,
});
});
it('should open a modal', () => {
const { result } = renderHook(
() => {
const modal = useModal();
const isModalOpened = useRecoilValue(
isModalOpenedComponentState.atomFamily({ instanceId: modalId }),
);
return { modal, isModalOpened };
},
{
wrapper: RecoilRoot,
},
);
act(() => {
result.current.modal.openModal(modalId);
});
expect(result.current.isModalOpened).toBe(true);
});
it('should open a modal with custom hotkey scope', () => {
const { result } = renderHook(
() => {
const modal = useModal();
const isModalOpened = useRecoilValue(
isModalOpenedComponentState.atomFamily({ instanceId: modalId }),
);
return { modal, isModalOpened };
},
{
wrapper: RecoilRoot,
},
);
act(() => {
result.current.modal.openModal(modalId, customHotkeyScope);
});
expect(result.current.isModalOpened).toBe(true);
expect(mockSetHotkeyScopeAndMemorizePreviousScope).toHaveBeenCalledWith(
customHotkeyScope.scope,
customHotkeyScope.customScopes,
);
});
it('should close a modal', () => {
const { result } = renderHook(
() => {
const modal = useModal();
const isModalOpened = useRecoilValue(
isModalOpenedComponentState.atomFamily({ instanceId: modalId }),
);
return { modal, isModalOpened };
},
{
wrapper: RecoilRoot,
},
);
act(() => {
result.current.modal.openModal(modalId);
});
expect(result.current.isModalOpened).toBe(true);
act(() => {
result.current.modal.closeModal(modalId);
});
expect(result.current.isModalOpened).toBe(false);
expect(mockGoBackToPreviousHotkeyScope).toHaveBeenCalled();
});
it('should toggle a modal (open when closed)', () => {
const { result } = renderHook(
() => {
const modal = useModal();
const isModalOpened = useRecoilValue(
isModalOpenedComponentState.atomFamily({ instanceId: modalId }),
);
return { modal, isModalOpened };
},
{
wrapper: RecoilRoot,
},
);
expect(result.current.isModalOpened).toBe(false);
act(() => {
result.current.modal.toggleModal(modalId);
});
expect(result.current.isModalOpened).toBe(true);
});
it('should toggle a modal (close when open)', () => {
const { result } = renderHook(
() => {
const modal = useModal();
const isModalOpened = useRecoilValue(
isModalOpenedComponentState.atomFamily({ instanceId: modalId }),
);
return { modal, isModalOpened };
},
{
wrapper: RecoilRoot,
},
);
act(() => {
result.current.modal.openModal(modalId);
});
expect(result.current.isModalOpened).toBe(true);
act(() => {
result.current.modal.toggleModal(modalId);
});
expect(result.current.isModalOpened).toBe(false);
expect(mockGoBackToPreviousHotkeyScope).toHaveBeenCalled();
});
it('should toggle a modal with custom hotkey scope', () => {
const { result } = renderHook(
() => {
const modal = useModal();
const isModalOpened = useRecoilValue(
isModalOpenedComponentState.atomFamily({ instanceId: modalId }),
);
return { modal, isModalOpened };
},
{
wrapper: RecoilRoot,
},
);
act(() => {
result.current.modal.toggleModal(modalId, customHotkeyScope);
});
expect(result.current.isModalOpened).toBe(true);
expect(mockSetHotkeyScopeAndMemorizePreviousScope).toHaveBeenCalledWith(
customHotkeyScope.scope,
customHotkeyScope.customScopes,
);
});
});

View File

@ -0,0 +1,93 @@
import { useRecoilCallback } from 'recoil';
import { ModalHotkeyScope } from '@/ui/layout/modal/components/types/ModalHotkeyScope';
import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpenedComponentState';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { isDefined } from 'twenty-shared/utils';
export const useModal = () => {
const {
setHotkeyScopeAndMemorizePreviousScope,
goBackToPreviousHotkeyScope,
} = usePreviousHotkeyScope('modal');
const closeModal = useRecoilCallback(
({ set, snapshot }) =>
(modalId: string) => {
const isModalOpen = snapshot
.getLoadable(
isModalOpenedComponentState.atomFamily({ instanceId: modalId }),
)
.getValue();
if (isModalOpen) {
goBackToPreviousHotkeyScope();
set(
isModalOpenedComponentState.atomFamily({ instanceId: modalId }),
false,
);
}
},
[goBackToPreviousHotkeyScope],
);
const openModal = useRecoilCallback(
({ set, snapshot }) =>
(modalId: string, customHotkeyScope?: HotkeyScope) => {
const isModalOpened = snapshot
.getLoadable(
isModalOpenedComponentState.atomFamily({ instanceId: modalId }),
)
.getValue();
if (isModalOpened) {
return;
}
set(
isModalOpenedComponentState.atomFamily({ instanceId: modalId }),
true,
);
if (isDefined(customHotkeyScope)) {
setHotkeyScopeAndMemorizePreviousScope(
customHotkeyScope.scope,
customHotkeyScope.customScopes,
);
} else {
setHotkeyScopeAndMemorizePreviousScope(ModalHotkeyScope.ModalFocus, {
goto: false,
commandMenu: false,
commandMenuOpen: false,
keyboardShortcutMenu: false,
});
}
},
[setHotkeyScopeAndMemorizePreviousScope],
);
const toggleModal = useRecoilCallback(
({ snapshot }) =>
(modalId: string, customHotkeyScope?: HotkeyScope) => {
const isModalOpen = snapshot
.getLoadable(
isModalOpenedComponentState.atomFamily({ instanceId: modalId }),
)
.getValue();
if (isModalOpen) {
closeModal(modalId);
} else {
openModal(modalId, customHotkeyScope);
}
},
[closeModal, openModal],
);
return {
closeModal,
openModal,
toggleModal,
};
};

View File

@ -0,0 +1,8 @@
import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2';
import { ModalComponentInstanceContext } from '../contexts/ModalComponentInstanceContext';
export const isModalOpenedComponentState = createComponentStateV2<boolean>({
key: 'isModalOpenedComponentState',
defaultValue: false,
componentInstanceContext: ModalComponentInstanceContext,
});

View File

@ -4,12 +4,14 @@ import {
ConfirmationModal,
StyledCenteredButton,
} from '@/ui/layout/modal/components/ConfirmationModal';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { useCreateDraftFromWorkflowVersion } from '@/workflow/hooks/useCreateDraftFromWorkflowVersion';
import { openOverrideWorkflowDraftConfirmationModalState } from '@/workflow/states/openOverrideWorkflowDraftConfirmationModalState';
import { useRecoilState } from 'recoil';
import { useNavigateApp } from '~/hooks/useNavigateApp';
import { getAppPath } from '~/utils/navigation/getAppPath';
const OVERRIDE_WORKFLOW_DRAFT_CONFIRMATION_MODAL_ID =
'override-workflow-draft-confirmation-modal';
export const OverrideWorkflowDraftConfirmationModal = ({
workflowId,
workflowVersionIdToCopy,
@ -17,10 +19,7 @@ export const OverrideWorkflowDraftConfirmationModal = ({
workflowId: string;
workflowVersionIdToCopy: string;
}) => {
const [
openOverrideWorkflowDraftConfirmationModal,
setOpenOverrideWorkflowDraftConfirmationModal,
] = useRecoilState(openOverrideWorkflowDraftConfirmationModalState);
const { closeModal } = useModal();
const { createDraftFromWorkflowVersion } =
useCreateDraftFromWorkflowVersion();
@ -42,8 +41,7 @@ export const OverrideWorkflowDraftConfirmationModal = ({
return (
<>
<ConfirmationModal
isOpen={openOverrideWorkflowDraftConfirmationModal}
setIsOpen={setOpenOverrideWorkflowDraftConfirmationModal}
modalId={OVERRIDE_WORKFLOW_DRAFT_CONFIRMATION_MODAL_ID}
title="A draft already exists"
subtitle="A draft already exists for this workflow. Are you sure you want to erase it?"
onConfirmClick={handleOverrideDraft}
@ -55,7 +53,7 @@ export const OverrideWorkflowDraftConfirmationModal = ({
objectRecordId: workflowId,
})}
onClick={() => {
setOpenOverrideWorkflowDraftConfirmationModal(false);
closeModal(OVERRIDE_WORKFLOW_DRAFT_CONFIRMATION_MODAL_ID);
}}
variant="secondary"
title="Go to Draft"