Refactor modal (#18377)

## Summary

- Move Modal UI components (`Modal`, `ModalContent`, `ModalHeader`,
`ModalFooter`, `ModalBackdrop`) from `twenty-front` to `twenty-ui` as
stateless, reusable components
- Create `ModalStatefulWrapper` in `twenty-front` that connects Jotai
state (`isModalOpenedComponentState`) to the stateless `Modal` via an
`isOpen` prop
- Rename `modalVariant` prop to `overlay` with clearer values: `'dark'`
(default), `'light'` (in-container), `'transparent'` (invisible panel).
Remove unused `'medium'` overlay
- Rename `modalId` to `modalInstanceId` across the entire modal zone
(~30 consumer files)
- Extract `ModalProps` to its own file in
`twenty-ui/types/ModalProps.ts`; extract `ModalStatefulWrapperProps` to
its own file using `Pick<ModalProps, ...>` for shared props
- Extract `ModalBackdrop` to its own file and export from `twenty-ui`;
use it in `UserOrMetadataLoader` instead of a local styled component
- Use `ModalFooter` in `StepNavigationButton` and `ModalHeader` in
`SpreadsheetImportStepperContainer` instead of duplicated `styled.div`
definitions
- Remove unused `onClose` prop from stateless `Modal`; fix `typeof
document` guard in `ModalStatefulWrapper`
- Split shared types into individual files: `ModalSize.ts`,
`ModalPadding.ts`, `ModalOverlay.ts`
- Extract wyw profiling instrumentation from `vite.config.ts` into
reusable `createWywProfilingPlugin` with parametrized threshold and
improved logging
- Delete old `Modal.tsx`, `Modal.styles.ts`, `ModalContent.tsx`,
`ModalHeader.tsx`, `ModalFooter.tsx` from `twenty-front`
- Add comprehensive Storybook stories in `twenty-ui` covering Default,
Confirmation, Small, ExtraLarge, Closed, and Interactive variants
This commit is contained in:
Charles Bochet
2026-03-04 13:22:31 +01:00
committed by GitHub
parent 995793c0ac
commit 3b2bf39565
99 changed files with 4283 additions and 3688 deletions
@@ -1,9 +1,10 @@
import { styled } from '@linaria/react';
import { useShowAuthModal } from '@/ui/layout/hooks/useShowAuthModal';
import { Modal } from '@/ui/layout/modal/components/Modal';
import { RootStackingContextZIndices } from '@/ui/layout/constants/RootStackingContextZIndices';
import { NAVIGATION_DRAWER_CONSTRAINTS } from '@/ui/layout/resizable-panel/constants/NavigationDrawerConstraints';
import { MOBILE_VIEWPORT, themeCssVariables } from 'twenty-ui/theme-constants';
import { ModalBackdrop } from 'twenty-ui/layout';
import { LeftPanelSkeletonLoader } from '~/loading/components/LeftPanelSkeletonLoader';
import { RightPanelSkeletonLoader } from '~/loading/components/RightPanelSkeletonLoader';
@@ -29,7 +30,12 @@ export const UserOrMetadataLoader = () => {
return (
<StyledContainer>
{showAuthModal && <Modal.Backdrop modalVariant="primary" />}
{showAuthModal && (
<ModalBackdrop
overlay="dark"
backdropZIndex={RootStackingContextZIndices.RootModalBackDrop}
/>
)}
<LeftPanelSkeletonLoader />
<RightPanelSkeletonLoader />
</StyledContainer>
@@ -65,7 +65,7 @@ export const ActionModal = ({
<ActionDisplay onClick={handleClick} />
{isModalOpened && (
<ConfirmationModal
modalId={modalId}
modalInstanceId={modalId}
title={title}
subtitle={subtitle}
onConfirmClick={handleConfirmClick}
@@ -9,7 +9,8 @@ import { type Attachment } from '@/activities/files/types/Attachment';
import { downloadFile } from '@/activities/files/utils/downloadFile';
import { type ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { isAttachmentPreviewEnabledState } from '@/client-config/states/isAttachmentPreviewEnabledState';
import { Modal } from '@/ui/layout/modal/components/Modal';
import { ModalStatefulWrapper } from '@/ui/layout/modal/components/ModalStatefulWrapper';
import { ModalContent, ModalHeader } from 'twenty-ui/layout';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
@@ -104,20 +105,6 @@ const StyledModalTitle = styled.span`
color: ${themeCssVariables.font.color.primary};
`;
const StyledModalHeader = styled(Modal.Header)`
height: auto;
padding: 0;
`;
const StyledModalContent = styled(Modal.Content)`
padding: 0;
`;
const StyledModal = styled(Modal)`
gap: ${themeCssVariables.spacing[2]};
padding: ${themeCssVariables.spacing[3]};
`;
const StyledButtonContainer = styled.div`
display: flex;
flex-direction: row;
@@ -231,14 +218,16 @@ export const AttachmentList = ({
{previewedAttachment &&
isAttachmentPreviewEnabled &&
createPortal(
<StyledModal
modalId={PREVIEW_MODAL_ID}
<ModalStatefulWrapper
modalInstanceId={PREVIEW_MODAL_ID}
size="large"
isClosable
onClose={handleClosePreview}
ignoreContainer
renderInDocumentBody
gap={2}
padding="small"
>
<StyledModalHeader>
<ModalHeader noPadding autoHeight>
<StyledHeader>
<StyledModalTitle>{previewedAttachment.name}</StyledModalTitle>
<StyledButtonContainer>
@@ -256,11 +245,11 @@ export const AttachmentList = ({
/>
</StyledButtonContainer>
</StyledHeader>
</StyledModalHeader>
</ModalHeader>
<ScrollWrapper
componentInstanceId={`preview-modal-${previewedAttachment.id}`}
>
<StyledModalContent>
<ModalContent noPadding>
<Suspense
fallback={
<StyledLoadingContainer>
@@ -275,9 +264,9 @@ export const AttachmentList = ({
documentUrl={getAttachmentUrl(previewedAttachment)}
/>
</Suspense>
</StyledModalContent>
</ModalContent>
</ScrollWrapper>
</StyledModal>,
</ModalStatefulWrapper>,
document.body,
)}
</>
@@ -1,7 +1,7 @@
import { AuthModalMountEffect } from '@/auth/components/AuthModalMountEffect';
import { AUTH_MODAL_ID } from '@/auth/constants/AuthModalId';
import { getAuthModalConfig } from '@/auth/utils/getAuthModalConfig';
import { Modal } from '@/ui/layout/modal/components/Modal';
import { ModalStatefulWrapper } from '@/ui/layout/modal/components/ModalStatefulWrapper';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { styled } from '@linaria/react';
import React from 'react';
@@ -23,11 +23,11 @@ export const AuthModal = ({ children }: AuthModalProps) => {
return (
<>
<AuthModalMountEffect />
<Modal
modalId={AUTH_MODAL_ID}
<ModalStatefulWrapper
modalInstanceId={AUTH_MODAL_ID}
padding="none"
size={config.size}
modalVariant={config.variant}
overlay={config.overlay}
>
{config.showScrollWrapper ? (
<ScrollWrapper componentInstanceId="scroll-wrapper-modal-content">
@@ -36,7 +36,7 @@ export const AuthModal = ({ children }: AuthModalProps) => {
) : (
<>{children}</>
)}
</Modal>
</ModalStatefulWrapper>
</>
);
};
@@ -8,7 +8,7 @@ import { useVerifyLogin } from '@/auth/hooks/useVerifyLogin';
import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState';
import { useIsCurrentLocationOnAWorkspace } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspace';
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
import { Modal } from '@/ui/layout/modal/components/Modal';
import { ModalContent } from 'twenty-ui/layout';
import { useLingui } from '@lingui/react/macro';
import { useEffect, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
@@ -126,9 +126,9 @@ export const VerifyEmailEffect = () => {
if (isError) {
return (
<Modal.Content isVerticalCentered isHorizontalCentered>
<ModalContent isVerticallyCentered isHorizontallyCentered>
<EmailVerificationSent email={email} isError={true} />
</Modal.Content>
</ModalContent>
);
}
@@ -5,14 +5,14 @@ import { MemoryRouter, Route, Routes } from 'react-router-dom';
// Mock component that just renders the error state of VerifyEmailEffect directly
// (since normal VerifyEmailEffect has async logic that's hard to test in Storybook)
import { EmailVerificationSent } from '@/auth/sign-in-up/components/EmailVerificationSent';
import { Modal } from '@/ui/layout/modal/components/Modal';
import { ModalContent } from 'twenty-ui/layout';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
const VerifyEmailEffectErrorState = ({ email = 'user@example.com' }) => {
return (
<Modal.Content isVerticalCentered isHorizontalCentered>
<ModalContent isVerticallyCentered isHorizontallyCentered>
<EmailVerificationSent email={email} isError={true} />
</Modal.Content>
</ModalContent>
);
};
@@ -29,7 +29,7 @@ const meta: Meta<typeof VerifyEmailEffectErrorState> = {
],
parameters: {
codeSection: {
docs: 'IMPORTANT: When rendering EmailVerificationSent from VerifyEmailEffect, always wrap it with Modal.Content to maintain consistent styling.',
docs: 'IMPORTANT: When rendering EmailVerificationSent from VerifyEmailEffect, always wrap it with ModalContent to maintain consistent styling.',
},
},
};
@@ -1,12 +1,9 @@
import {
type ModalSize,
type ModalVariants,
} from '@/ui/layout/modal/components/Modal';
import { type ModalOverlay, type ModalSize } from 'twenty-ui/layout';
import { AppPath } from 'twenty-shared/types';
type AuthModalConfigType = {
size: ModalSize;
variant: ModalVariants;
overlay: ModalOverlay;
showScrollWrapper: boolean;
};
@@ -16,12 +13,12 @@ export const AUTH_MODAL_CONFIG: {
} = {
default: {
size: 'medium',
variant: 'primary',
overlay: 'dark',
showScrollWrapper: true,
},
[AppPath.BookCall]: {
size: 'extraLarge',
variant: 'transparent',
overlay: 'transparent',
showScrollWrapper: false,
},
};
@@ -1,24 +1,17 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { EmailVerificationSent } from '@/auth/sign-in-up/components/EmailVerificationSent';
import { Modal } from '@/ui/layout/modal/components/Modal';
import { ModalContent } from 'twenty-ui/layout';
import { ComponentDecorator } from 'twenty-ui/testing';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
// Wrap the component in Modal.Content to reflect how it's used in the app
const RenderWithModal = (
const RenderWithModalContent = (
args: React.ComponentProps<typeof EmailVerificationSent>,
) => {
return (
<Modal
modalId="email-verification-sent-modal"
padding="none"
modalVariant="primary"
>
<Modal.Content isVerticalCentered isHorizontalCentered>
<EmailVerificationSent email={args.email} isError={args.isError} />
</Modal.Content>
</Modal>
<ModalContent isVerticallyCentered isHorizontallyCentered>
<EmailVerificationSent email={args.email} isError={args.isError} />
</ModalContent>
);
};
@@ -28,10 +21,10 @@ const meta: Meta<typeof EmailVerificationSent> = {
decorators: [ComponentDecorator, SnackBarDecorator],
parameters: {
codeSection: {
docs: 'This component should always be wrapped with Modal.Content in the app.\n\nCorrect usage:\n```tsx\n<Modal.Content isVerticalCentered isHorizontalCentered>\n <EmailVerificationSent email={email} />\n</Modal.Content>\n```\n',
docs: 'This component should always be wrapped with ModalContent in the app.\n\nCorrect usage:\n```tsx\n<ModalContent isVerticallyCentered isHorizontallyCentered>\n <EmailVerificationSent email={email} />\n</ModalContent>\n```\n',
},
},
render: RenderWithModal,
render: RenderWithModalContent,
};
export default meta;
@@ -507,7 +507,7 @@ export const SettingsBillingSubscriptionInfo = ({
)}
</StyledSwitchButtonContainer>
<ConfirmationModal
modalId={SWITCH_BILLING_INTERVAL_TO_YEARLY_MODAL_ID}
modalInstanceId={SWITCH_BILLING_INTERVAL_TO_YEARLY_MODAL_ID}
title={t`Change to Yearly?`}
subtitle={confirmationModalSwitchToYearlyMessage()}
onConfirmClick={switchInterval}
@@ -516,7 +516,7 @@ export const SettingsBillingSubscriptionInfo = ({
loading={isSwitchingInterval}
/>
<ConfirmationModal
modalId={SWITCH_BILLING_INTERVAL_TO_MONTHLY_MODAL_ID}
modalInstanceId={SWITCH_BILLING_INTERVAL_TO_MONTHLY_MODAL_ID}
title={t`Change to Monthly?`}
subtitle={confirmationModalSwitchToMonthlyMessage()}
onConfirmClick={switchInterval}
@@ -525,7 +525,7 @@ export const SettingsBillingSubscriptionInfo = ({
loading={isSwitchingInterval}
/>
<ConfirmationModal
modalId={CANCEL_SWITCH_BILLING_INTERVAL_MODAL_ID}
modalInstanceId={CANCEL_SWITCH_BILLING_INTERVAL_MODAL_ID}
title={t`Cancel interval switching?`}
subtitle={confirmationModalCancelIntervalSwitchingMessage()}
onConfirmClick={cancelIntervalSwitching}
@@ -534,7 +534,7 @@ export const SettingsBillingSubscriptionInfo = ({
loading={isCancellingIntervalSwitch}
/>
<ConfirmationModal
modalId={SWITCH_BILLING_PLAN_TO_ENTERPRISE_MODAL_ID}
modalInstanceId={SWITCH_BILLING_PLAN_TO_ENTERPRISE_MODAL_ID}
title={t`Change to Organization Plan?`}
subtitle={confirmationModalSwitchToOrganizationMessage()}
onConfirmClick={switchPlan}
@@ -543,7 +543,7 @@ export const SettingsBillingSubscriptionInfo = ({
loading={isSwitchingPlan}
/>
<ConfirmationModal
modalId={SWITCH_BILLING_PLAN_TO_PRO_MODAL_ID}
modalInstanceId={SWITCH_BILLING_PLAN_TO_PRO_MODAL_ID}
title={t`Change to Pro Plan?`}
subtitle={confirmationModalSwitchToProMessage()}
onConfirmClick={switchPlan}
@@ -552,7 +552,7 @@ export const SettingsBillingSubscriptionInfo = ({
loading={isSwitchingPlan}
/>
<ConfirmationModal
modalId={CANCEL_SWITCH_BILLING_PLAN_MODAL_ID}
modalInstanceId={CANCEL_SWITCH_BILLING_PLAN_MODAL_ID}
title={t`Cancel plan switching?`}
subtitle={confirmationModalCancelPlanSwitchingMessage()}
onConfirmClick={cancelPlanSwitching}
@@ -561,7 +561,7 @@ export const SettingsBillingSubscriptionInfo = ({
loading={isCancellingPlanSwitch}
/>
<ConfirmationModal
modalId={END_TRIAL_PERIOD_MODAL_ID}
modalInstanceId={END_TRIAL_PERIOD_MODAL_ID}
title={t`Start Your Subscription`}
subtitle={t`We will activate your paid plan. Do you want to proceed?`}
onConfirmClick={endTrialPeriod}
@@ -570,7 +570,7 @@ export const SettingsBillingSubscriptionInfo = ({
loading={isEndTrialPeriodLoading}
/>
<ConfirmationModal
modalId={CANCEL_SWITCH_METERED_PRICE_MODAL_ID}
modalInstanceId={CANCEL_SWITCH_METERED_PRICE_MODAL_ID}
title={t`Cancel metered tier switching?`}
subtitle={t`You have scheduled a metered tier change. Do you want to cancel it?`}
onConfirmClick={cancelMeteredSwitching}
@@ -183,7 +183,7 @@ export const MeteredPriceSelector = ({
)}
</StyledRow>
<ConfirmationModal
modalId={confirmModalId}
modalInstanceId={confirmModalId}
title={isUpgrade() ? t`Confirm upgrade` : t`Confirm downgrade`}
subtitle={t`Confirm changing your current credit plan.`}
confirmButtonText={isUpgrade() ? t`Upgrade` : t`Downgrade`}
@@ -259,7 +259,7 @@ export const CurrentWorkspaceMemberFavorites = ({
{isModalOpened &&
createPortal(
<ConfirmationModal
modalId={modalId}
modalInstanceId={modalId}
title={
folder.favorites.length > 1
? t`Remove ${favoriteCount} favorites?`
@@ -317,7 +317,7 @@ export const CurrentWorkspaceMemberNavigationMenuItems = ({
{isModalOpened &&
createPortal(
<ConfirmationModal
modalId={modalId}
modalInstanceId={modalId}
title={
folder.navigationMenuItems.length > 1
? t`Remove ${navigationMenuItemCount} navigation menu items?`
@@ -278,7 +278,7 @@ export const RecordDetailRelationRecordsListItem = ({
</AnimatedEaseInOut>
{createPortal(
<ConfirmationModal
modalId={getDeleteRelationModalId(relationRecord.id)}
modalInstanceId={getDeleteRelationModalId(relationRecord.id)}
title={t`Delete Related ${relationObjectTypeName}`}
subtitle={
<Trans>
@@ -21,7 +21,7 @@ export const RecordGroupReorderConfirmationModal = ({
<>
{createPortal(
<ConfirmationModal
modalId={RECORD_GROUP_REORDER_CONFIRMATION_MODAL_ID}
modalInstanceId={RECORD_GROUP_REORDER_CONFIRMATION_MODAL_ID}
title={t`Group sorting`}
subtitle={t`Would you like to remove ${recordIndexRecordGroupSort} group sorting?`}
onConfirmClick={onConfirmClick}
@@ -26,7 +26,7 @@ export const RecordIndexRemoveSortingModal = () => {
return (
<ConfirmationModal
modalId={RECORD_INDEX_REMOVE_SORTING_MODAL_ID}
modalInstanceId={RECORD_INDEX_REMOVE_SORTING_MODAL_ID}
title={t`Remove sorting?`}
subtitle={t`This is required to enable manual row reordering.`}
onConfirmClick={handleRemoveClick}
@@ -137,7 +137,7 @@ export const SettingsAccountsRowDropdownMenu = ({
}
/>
<ConfirmationModal
modalId={deleteAccountModalId}
modalInstanceId={deleteAccountModalId}
title={t`Data deletion`}
subtitle={
<Trans>
@@ -2,14 +2,14 @@ import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModa
import { plural, t } from '@lingui/core/macro';
type SettingsAdminDeleteJobsConfirmationModalProps = {
modalId: string;
modalInstanceId: string;
jobCount: number;
onConfirm: () => void;
onClose?: () => void;
};
export const SettingsAdminDeleteJobsConfirmationModal = ({
modalId,
modalInstanceId,
jobCount,
onConfirm,
onClose,
@@ -26,7 +26,7 @@ export const SettingsAdminDeleteJobsConfirmationModal = ({
return (
<ConfirmationModal
modalId={modalId}
modalInstanceId={modalInstanceId}
title={title}
subtitle={subtitle}
onConfirmClick={onConfirm}
@@ -399,12 +399,12 @@ export const SettingsAdminQueueJobsTable = ({
)}
<SettingsAdminRetryJobsConfirmationModal
modalId={RETRY_MODAL_ID}
modalInstanceId={RETRY_MODAL_ID}
jobCount={selectedCount > 0 ? selectedCount : failedJobs.length}
onConfirm={confirmRetrySelected}
/>
<SettingsAdminDeleteJobsConfirmationModal
modalId={DELETE_MODAL_ID}
modalInstanceId={DELETE_MODAL_ID}
jobCount={selectedCount}
onConfirm={confirmDeleteSelected}
/>
@@ -2,14 +2,14 @@ import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModa
import { plural, t } from '@lingui/core/macro';
type SettingsAdminRetryJobsConfirmationModalProps = {
modalId: string;
modalInstanceId: string;
jobCount: number;
onConfirm: () => void;
onClose?: () => void;
};
export const SettingsAdminRetryJobsConfirmationModal = ({
modalId,
modalInstanceId,
jobCount,
onConfirm,
onClose,
@@ -26,7 +26,7 @@ export const SettingsAdminRetryJobsConfirmationModal = ({
return (
<ConfirmationModal
modalId={modalId}
modalInstanceId={modalInstanceId}
title={title}
subtitle={subtitle}
onConfirmClick={onConfirm}
@@ -139,7 +139,7 @@ export const ObjectSettings = ({
</StyledFormSection>
)}
<ConfirmationModal
modalId={DELETE_OBJECT_MODAL_ID}
modalInstanceId={DELETE_OBJECT_MODAL_ID}
title={t`Delete ${objectLabel} object?`}
subtitle={t`This will permanently delete the object and all its records. Type "yes" to confirm.`}
confirmButtonText={t`Delete`}
@@ -198,7 +198,7 @@ export const SettingsDevelopersWebhookForm = ({
<ConfirmationModal
confirmationPlaceholder={t`yes`}
confirmationValue={t`yes`}
modalId={DELETE_WEBHOOK_MODAL_ID}
modalInstanceId={DELETE_WEBHOOK_MODAL_ID}
title={t`Delete webhook`}
subtitle={
<Trans>
@@ -150,7 +150,7 @@ export const MemberPermissionsTab = ({
{pendingRole && (
<ConfirmationModal
modalId={CONFIRM_ROLE_CHANGE_MODAL_ID}
modalInstanceId={CONFIRM_ROLE_CHANGE_MODAL_ID}
title={t`Confirm role update`}
subtitle={t`Are you sure you want to update the role of this user from "${oldRoleLabel}" to "${newRoleLabel}"?`}
onConfirmClick={handleConfirmRoleChange}
@@ -86,7 +86,7 @@ export const DeleteAccount = () => {
<ConfirmationModal
confirmationValue={userEmail}
confirmationPlaceholder={userEmail ?? ''}
modalId={LEAVE_WORKSPACE_MODAL_ID}
modalInstanceId={LEAVE_WORKSPACE_MODAL_ID}
title={t`Leave workspace`}
subtitle={
<>
@@ -109,7 +109,7 @@ export const DeleteAccount = () => {
<ConfirmationModal
confirmationValue={userEmail}
confirmationPlaceholder={userEmail ?? ''}
modalId={DELETE_ACCOUNT_MODAL_ID}
modalInstanceId={DELETE_ACCOUNT_MODAL_ID}
title={t`Account Deletion`}
subtitle={
<>
@@ -43,7 +43,7 @@ export const DeleteWorkspace = () => {
/>
<ConfirmationModal
modalId={DELETE_WORKSPACE_MODAL_ID}
modalInstanceId={DELETE_WORKSPACE_MODAL_ID}
confirmationPlaceholder={userEmail}
confirmationValue={userEmail}
title={t`Workspace Deletion`}
@@ -25,7 +25,7 @@ export const SettingsRoleAssignmentConfirmationModal = ({
return (
<ConfirmationModal
modalId={ROLE_ASSIGNMENT_CONFIRMATION_MODAL_ID}
modalInstanceId={ROLE_ASSIGNMENT_CONFIRMATION_MODAL_ID}
title={title}
subtitle={
selectedRoleTarget.role ? (
@@ -26,7 +26,7 @@ export const SettingsRoleSettingsDeleteRoleConfirmationModal = ({
return (
<ConfirmationModal
modalId={ROLE_SETTINGS_DELETE_ROLE_CONFIRMATION_MODAL_ID}
modalInstanceId={ROLE_SETTINGS_DELETE_ROLE_CONFIRMATION_MODAL_ID}
title={t`Delete Role Permanently`}
subtitle={
<SettingsRoleSettingsDeleteRoleConfirmationModalSubtitle
@@ -98,7 +98,7 @@ export const DeleteTwoFactorAuthentication = () => {
<ConfirmationModal
confirmationValue={userEmail}
confirmationPlaceholder={userEmail ?? ''}
modalId={DELETE_TWO_FACTOR_AUTHENTICATION_MODAL_ID}
modalInstanceId={DELETE_TWO_FACTOR_AUTHENTICATION_MODAL_ID}
title={t`2FA Method Reset`}
subtitle={
isTwoFactorAuthenticationEnforced ? (
@@ -2,14 +2,16 @@ import { styled } from '@linaria/react';
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
import { Modal } from '@/ui/layout/modal/components/Modal';
import { ModalStatefulWrapper } from '@/ui/layout/modal/components/ModalStatefulWrapper';
import { MOBILE_VIEWPORT } from 'twenty-ui/theme-constants';
import { SpreadSheetImportModalCloseButton } from './SpreadSheetImportModalCloseButton';
const StyledModal = styled(Modal)`
const StyledInnerContainer = styled.div`
display: flex;
flex: 1;
flex-direction: column;
min-height: 600px;
min-width: 800px;
padding: 0;
position: relative;
@media (max-width: ${MOBILE_VIEWPORT}px) {
min-width: auto;
@@ -27,29 +29,32 @@ const StyledRtlLtr = styled.div`
type SpreadSheetImportModalWrapperProps = {
children: React.ReactNode;
modalId: string;
modalInstanceId: string;
onClose: () => void;
};
export const SpreadSheetImportModalWrapper = ({
modalId,
modalInstanceId,
children,
onClose,
}: SpreadSheetImportModalWrapperProps) => {
const { rtl } = useSpreadsheetImportInternal();
return (
<StyledModal
<ModalStatefulWrapper
size="extraLarge"
modalId={modalId}
padding="none"
modalInstanceId={modalInstanceId}
isClosable={true}
onClose={onClose}
shouldCloseModalOnClickOutsideOrEscape={false}
>
<StyledRtlLtr dir={rtl ? 'rtl' : 'ltr'}>
<SpreadSheetImportModalCloseButton onClose={onClose} />
{children}
</StyledRtlLtr>
</StyledModal>
<StyledInnerContainer>
<StyledRtlLtr dir={rtl ? 'rtl' : 'ltr'}>
<SpreadSheetImportModalCloseButton onClose={onClose} />
{children}
</StyledRtlLtr>
</StyledInnerContainer>
</ModalStatefulWrapper>
);
};
@@ -1,19 +1,16 @@
import { styled } from '@linaria/react';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { Modal } from '@/ui/layout/modal/components/Modal';
import { t } from '@lingui/core/macro';
import { CircularProgressBar } from 'twenty-ui/feedback';
import { MainButton } from 'twenty-ui/input';
import { ModalFooter } from 'twenty-ui/layout';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
const StyledFooter = styled(Modal.Footer)`
const StyledFooter = styled(ModalFooter)`
border-top: 1px solid ${themeCssVariables.border.color.medium};
box-shadow: ${themeCssVariables.boxShadow.strong};
gap: 10px;
justify-content: space-between;
padding: ${themeCssVariables.spacing[4]};
height: auto;
`;
type StepNavigationButtonProps = {
@@ -34,7 +31,7 @@ export const StepNavigationButton = ({
isContinueDisabled = false,
}: StepNavigationButtonProps) => {
return (
<StyledFooter>
<StyledFooter autoHeight>
{!isUndefinedOrNull(onBack) && (
<MainButton
Icon={isLoading ? CircularProgressBar : undefined}
@@ -70,7 +70,7 @@ export const SpreadsheetImport = (props: SpreadsheetImportProps) => {
return (
<ReactSpreadsheetImportContextProvider values={mergedProps}>
<SpreadSheetImportModalWrapper
modalId={SPREADSHEET_IMPORT_MODAL_ID}
modalInstanceId={SPREADSHEET_IMPORT_MODAL_ID}
onClose={confirmOnClose}
>
<SpreadsheetImportStepperContainer />
@@ -3,19 +3,12 @@ import { StepNavigationButton } from '@/spreadsheet-import/components/StepNaviga
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
import { spreadsheetImportCreatedRecordsProgressState } from '@/spreadsheet-import/states/spreadsheetImportCreatedRecordsProgressState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { Modal } from '@/ui/layout/modal/components/Modal';
import { ModalContent } from 'twenty-ui/layout';
import { styled } from '@linaria/react';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { t } from '@lingui/core/macro';
import { Loader } from 'twenty-ui/feedback';
const StyledContent = styled(Modal.Content)`
align-items: center;
display: flex;
justify-content: center;
padding: 0px;
`;
const StyledHeader = styled.span`
color: ${themeCssVariables.font.color.primary};
font-size: ${themeCssVariables.font.size.md};
@@ -50,11 +43,11 @@ export const ImportDataStep = ({
return (
<>
<StyledContent>
<ModalContent noPadding isVerticallyCentered isHorizontallyCentered>
<StyledHeader>{t`Importing Data ...`}</StyledHeader>
<StyledDescription>{t`${formattedCreatedRecordsProgress} out of ${formattedRecordsToImportCount} records imported.`}</StyledDescription>
<Loader />
</StyledContent>
</ModalContent>
<StepNavigationButton onBack={onClose} backTitle={t`Cancel`} />
</>
);
@@ -14,7 +14,7 @@ import { setIgnoreColumn } from '@/spreadsheet-import/utils/setIgnoreColumn';
import { setSubColumn } from '@/spreadsheet-import/utils/setSubColumn';
import { useDialogManager } from '@/ui/feedback/dialog-manager/hooks/useDialogManager';
import { Modal } from '@/ui/layout/modal/components/Modal';
import { ModalContent } from 'twenty-ui/layout';
import { DO_NOT_IMPORT_OPTION_KEY } from '@/spreadsheet-import/constants/DoNotImportOptionKey';
import { ColumnGrid } from '@/spreadsheet-import/steps/components/MatchColumnsStep/components/ColumnGrid';
@@ -33,11 +33,6 @@ import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { Trans, useLingui } from '@lingui/react/macro';
import { themeCssVariables } from 'twenty-ui/theme-constants';
const StyledContent = styled(Modal.Content)`
align-items: center;
padding: 0px;
`;
const StyledColumnsContainer = styled.div`
align-items: center;
display: flex;
@@ -276,7 +271,7 @@ export const MatchColumnsStep = ({
return (
<>
<StyledContent>
<ModalContent noPadding isVerticallyCentered>
<ScrollWrapper componentInstanceId="scroll-wrapper-modal-content">
<ColumnGrid
columns={columns}
@@ -304,7 +299,7 @@ export const MatchColumnsStep = ({
)}
/>
</ScrollWrapper>
</StyledContent>
</ModalContent>
<StepNavigationButton
onContinue={handleOnContinue}
isLoading={isLoading}
@@ -6,7 +6,7 @@ import { Heading } from '@/spreadsheet-import/components/Heading';
import { StepNavigationButton } from '@/spreadsheet-import/components/StepNavigationButton';
import { type ImportedRow } from '@/spreadsheet-import/types';
import { Modal } from '@/ui/layout/modal/components/Modal';
import { ModalContent } from 'twenty-ui/layout';
import { useComputeColumnSuggestionsAndAutoMatch } from '@/spreadsheet-import/hooks/useComputeColumnSuggestionsAndAutoMatch';
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
@@ -104,7 +104,7 @@ export const SelectHeaderStep = ({
return (
<>
<Modal.Content>
<ModalContent>
<StyledHeading title={t`Select header row`} />
<StyledTableContainer>
<SelectHeaderTable
@@ -113,7 +113,7 @@ export const SelectHeaderStep = ({
setSelectedRowIndexes={setSelectedRowIndexes}
/>
</StyledTableContainer>
</Modal.Content>
</ModalContent>
<StepNavigationButton
onContinue={handleOnContinue}
onBack={onBack}
@@ -9,20 +9,12 @@ import { SpreadsheetImportStepType } from '@/spreadsheet-import/steps/types/Spre
import { exceedsMaxRecords } from '@/spreadsheet-import/utils/exceedsMaxRecords';
import { mapWorkbook } from '@/spreadsheet-import/utils/mapWorkbook';
import { Modal } from '@/ui/layout/modal/components/Modal';
import { ModalContent } from 'twenty-ui/layout';
import { useLingui } from '@lingui/react/macro';
import { Radio, RadioGroup } from 'twenty-ui/input';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { type WorkBook } from 'xlsx-ugnis';
const StyledContent = styled(Modal.Content)`
align-items: center;
display: flex;
flex-direction: column;
justify-content: center;
gap: ${themeCssVariables.spacing[8]};
`;
const StyledHeading = styled(Heading)`
display: flex;
`;
@@ -111,7 +103,7 @@ export const SelectSheetStep = ({
return (
<>
<StyledContent>
<ModalContent isVerticallyCentered isHorizontallyCentered gap={8}>
<StyledHeading title={t`Select the sheet to use`} />
<StyledRadioContainer>
<RadioGroup onValueChange={(value) => setValue(value)} value={value}>
@@ -124,7 +116,7 @@ export const SelectSheetStep = ({
))}
</RadioGroup>
</StyledRadioContainer>
</StyledContent>
</ModalContent>
<StepNavigationButton
onContinue={() => handleOnContinue(value)}
onBack={onBack}
@@ -1,10 +1,9 @@
import { styled } from '@linaria/react';
import { useCallback, useContext, useState } from 'react';
import { ThemeContext } from 'twenty-ui/theme';
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { Modal } from '@/ui/layout/modal/components/Modal';
import { ModalContent } from 'twenty-ui/layout';
import { ImportDataStep } from '@/spreadsheet-import/steps/components/ImportDataStep';
import { type SpreadsheetImportStep } from '@/spreadsheet-import/steps/types/SpreadsheetImportStep';
@@ -16,12 +15,6 @@ import { SelectSheetStep } from './SelectSheetStep/SelectSheetStep';
import { UploadStep } from './UploadStep/UploadStep';
import { ValidationStep } from './ValidationStep/ValidationStep';
const StyledProgressBarContainer = styled(Modal.Content)`
align-items: center;
display: flex;
justify-content: center;
`;
type SpreadsheetImportStepperProps = {
nextStep: () => void;
prevStep: () => void;
@@ -137,13 +130,13 @@ export const SpreadsheetImportStepper = ({
case SpreadsheetImportStepType.loading:
default:
return (
<StyledProgressBarContainer>
<ModalContent isVerticallyCentered isHorizontallyCentered>
<CircularProgressBar
size={80}
barWidth={8}
barColor={theme.font.color.primary}
/>
</StyledProgressBarContainer>
</ModalContent>
);
}
};
@@ -1,5 +1,3 @@
import { styled } from '@linaria/react';
import { useSpreadsheetImportInitialStep } from '@/spreadsheet-import/hooks/useSpreadsheetImportInitialStep';
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
@@ -7,24 +5,12 @@ import { StepBar } from '@/ui/navigation/step-bar/components/StepBar';
import { useStepBar } from '@/ui/navigation/step-bar/hooks/useStepBar';
import { spreadsheetImportDialogState } from '@/spreadsheet-import/states/spreadsheetImportDialogState';
import { Modal } from '@/ui/layout/modal/components/Modal';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useLingui } from '@lingui/react/macro';
import { MOBILE_VIEWPORT, themeCssVariables } from 'twenty-ui/theme-constants';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { ModalHeader } from 'twenty-ui/layout';
import { SpreadsheetImportStepper } from './SpreadsheetImportStepper';
const StyledHeader = styled(Modal.Header)`
background-color: ${themeCssVariables.background.secondary};
border-bottom: 1px solid ${themeCssVariables.border.color.medium};
padding: 0px ${themeCssVariables.spacing[30]};
height: 60px;
flex-shrink: 0;
@media (max-width: ${MOBILE_VIEWPORT}px) {
padding-left: ${themeCssVariables.spacing[4]};
padding-right: ${themeCssVariables.spacing[4]};
}
`;
export const SpreadsheetImportStepperContainer = () => {
const { t } = useLingui();
@@ -50,7 +36,11 @@ export const SpreadsheetImportStepperContainer = () => {
return (
<>
<StyledHeader>
<ModalHeader
hasBorderBottom
paddingHorizontal={30}
backgroundColor={themeCssVariables.background.secondary}
>
{spreadsheetImportDialog.isStepBarVisible && (
<StepBar activeStep={activeStep}>
{steps.map((key) => (
@@ -62,7 +52,7 @@ export const SpreadsheetImportStepperContainer = () => {
))}
</StepBar>
)}
</StyledHeader>
</ModalHeader>
<SpreadsheetImportStepper nextStep={nextStep} prevStep={prevStep} />
</>
);
@@ -1,10 +1,8 @@
import { t } from '@lingui/core/macro';
import { styled } from '@linaria/react';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { useCallback, useState } from 'react';
import { type WorkBook } from 'xlsx-ugnis';
import { Modal } from '@/ui/layout/modal/components/Modal';
import { ModalContent } from 'twenty-ui/layout';
import { useComputeColumnSuggestionsAndAutoMatch } from '@/spreadsheet-import/hooks/useComputeColumnSuggestionsAndAutoMatch';
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
@@ -14,10 +12,6 @@ import { exceedsMaxRecords } from '@/spreadsheet-import/utils/exceedsMaxRecords'
import { mapWorkbook } from '@/spreadsheet-import/utils/mapWorkbook';
import { DropZone } from './components/DropZone';
const StyledContent = styled(Modal.Content)`
padding: ${themeCssVariables.spacing[6]};
`;
type UploadStepProps = {
setUploadedFile: (file: File) => void;
setCurrentStepState: (data: any) => void;
@@ -118,8 +112,8 @@ export const UploadStep = ({
);
return (
<StyledContent>
<ModalContent contentPadding={6}>
<DropZone onContinue={handleOnContinue} isLoading={isLoading} />
</StyledContent>
</ModalContent>
);
};
@@ -12,7 +12,7 @@ import { type SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetC
import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType';
import { addErrorsAndRunHooks } from '@/spreadsheet-import/utils/dataMutations';
import { useDialogManager } from '@/ui/feedback/dialog-manager/hooks/useDialogManager';
import { Modal } from '@/ui/layout/modal/components/Modal';
import { ModalContent } from 'twenty-ui/layout';
import { styled } from '@linaria/react';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { Trans, useLingui } from '@lingui/react/macro';
@@ -31,8 +31,10 @@ import { Button, Toggle } from 'twenty-ui/input';
import { generateColumns } from './components/columns';
import { type ImportedStructuredRowMetadata } from './types';
const StyledContent = styled(Modal.Content)`
padding: 0px;
const StyledContentWrapper = styled.div`
display: flex;
flex: 1 1 0%;
flex-direction: column;
position: relative;
`;
@@ -282,53 +284,55 @@ export const ValidationStep = ({
return (
<>
<StyledContent>
{filterByErrors && tableData.length === 0 ? (
<StyledNoRowsWithErrorsContainer>
<Trans>No rows with errors</Trans>
</StyledNoRowsWithErrorsContainer>
) : (
<StyledScrollContainer>
<SpreadsheetImportTable
headerRowHeight={32}
rowKeyGetter={rowKeyGetter}
rows={tableData}
onRowsChange={updateRow}
columns={columns}
selectedRows={selectedRows}
onSelectedRowsChange={setSelectedRows as any} // TODO: replace 'any'
components={{
noRowsFallback: (
<StyledNoRowsContainer>
{filterByErrors
? t`No data containing errors`
: t`No data found`}
</StyledNoRowsContainer>
),
}}
<ModalContent noPadding>
<StyledContentWrapper>
{filterByErrors && tableData.length === 0 ? (
<StyledNoRowsWithErrorsContainer>
<Trans>No rows with errors</Trans>
</StyledNoRowsWithErrorsContainer>
) : (
<StyledScrollContainer>
<SpreadsheetImportTable
headerRowHeight={32}
rowKeyGetter={rowKeyGetter}
rows={tableData}
onRowsChange={updateRow}
columns={columns}
selectedRows={selectedRows}
onSelectedRowsChange={setSelectedRows as any} // TODO: replace 'any'
components={{
noRowsFallback: (
<StyledNoRowsContainer>
{filterByErrors
? t`No data containing errors`
: t`No data found`}
</StyledNoRowsContainer>
),
}}
/>
</StyledScrollContainer>
)}
<StyledToolbar>
<StyledErrorToggle>
<Toggle
value={filterByErrors}
onChange={() => setFilterByErrors(!filterByErrors)}
toggleSize="small"
/>
<StyledErrorToggleDescription>
<Trans>Show only rows with errors</Trans>
</StyledErrorToggleDescription>
</StyledErrorToggle>
<StyledButton
Icon={IconTrash}
title={t`Remove`}
accent="default"
onClick={deleteSelectedRows}
disabled={selectedRows.size === 0}
/>
</StyledScrollContainer>
)}
<StyledToolbar>
<StyledErrorToggle>
<Toggle
value={filterByErrors}
onChange={() => setFilterByErrors(!filterByErrors)}
toggleSize="small"
/>
<StyledErrorToggleDescription>
<Trans>Show only rows with errors</Trans>
</StyledErrorToggleDescription>
</StyledErrorToggle>
<StyledButton
Icon={IconTrash}
title={t`Remove`}
accent="default"
onClick={deleteSelectedRows}
disabled={selectedRows.size === 0}
/>
</StyledToolbar>
</StyledContent>
</StyledToolbar>
</StyledContentWrapper>
</ModalContent>
<StepNavigationButton
onContinue={onContinue}
onBack={onBack}
@@ -66,7 +66,7 @@ export const Default = () => (
>
<ReactSpreadsheetImportContextProvider values={mockRsiValues}>
<SpreadSheetImportModalWrapper
modalId="match-columns-step"
modalInstanceId="match-columns-step"
onClose={() => null}
>
<MatchColumnsStep
@@ -43,7 +43,7 @@ export const Default = () => (
>
<ReactSpreadsheetImportContextProvider values={mockRsiValues}>
<SpreadSheetImportModalWrapper
modalId="select-header-step"
modalInstanceId="select-header-step"
onClose={() => null}
>
<SelectHeaderStep
@@ -43,7 +43,7 @@ export const Default = () => (
>
<ReactSpreadsheetImportContextProvider values={mockRsiValues}>
<SpreadSheetImportModalWrapper
modalId="select-sheet-step"
modalInstanceId="select-sheet-step"
onClose={() => null}
>
<SelectSheetStep
@@ -46,7 +46,10 @@ export const Default = () => (
value={{ instanceId: 'dialog-manager' }}
>
<ReactSpreadsheetImportContextProvider values={mockRsiValues}>
<SpreadSheetImportModalWrapper modalId="upload-step" onClose={() => null}>
<SpreadSheetImportModalWrapper
modalInstanceId="upload-step"
onClose={() => null}
>
<UploadStep
setUploadedFile={() => null}
setCurrentStepState={() => null}
@@ -46,7 +46,7 @@ export const Default = () => (
>
<ReactSpreadsheetImportContextProvider values={mockRsiValues}>
<SpreadSheetImportModalWrapper
modalId="validation-step"
modalInstanceId="validation-step"
onClose={() => null}
>
<ValidationStep
@@ -1,6 +1,6 @@
import { downloadFile } from '@/activities/files/utils/downloadFile';
import { filePreviewState } from '@/ui/field/display/states/filePreviewState';
import { Modal } from '@/ui/layout/modal/components/Modal';
import { ModalStatefulWrapper } from '@/ui/layout/modal/components/ModalStatefulWrapper';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
@@ -100,12 +100,12 @@ export const GlobalFilePreviewModal = (): JSX.Element | null => {
return (
<>
{createPortal(
<Modal
modalId={GLOBAL_FILE_PREVIEW_MODAL_ID}
<ModalStatefulWrapper
modalInstanceId={GLOBAL_FILE_PREVIEW_MODAL_ID}
size="large"
isClosable
onClose={handleClose}
ignoreContainer
renderInDocumentBody
>
<StyledModalHeader>
<StyledHeader>
@@ -141,7 +141,7 @@ export const GlobalFilePreviewModal = (): JSX.Element | null => {
</Suspense>
</StyledModalContent>
</ScrollWrapper>
</Modal>,
</ModalStatefulWrapper>,
document.body,
)}
</>
@@ -4,17 +4,22 @@ import { useDebouncedCallback } from 'use-debounce';
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
import { Modal, type ModalVariants } from '@/ui/layout/modal/components/Modal';
import { ModalStatefulWrapper } from '@/ui/layout/modal/components/ModalStatefulWrapper';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react/macro';
import { H1Title, H1TitleFontColor } from 'twenty-ui/display';
import { Button, type ButtonAccent } from 'twenty-ui/input';
import { Section, SectionAlignment, SectionFontColor } from 'twenty-ui/layout';
import {
Section,
SectionAlignment,
SectionFontColor,
type ModalOverlay,
} from 'twenty-ui/layout';
import { themeCssVariables } from 'twenty-ui/theme-constants';
export type ConfirmationModalProps = {
modalId: string;
modalInstanceId: string;
title: string;
loading?: boolean;
subtitle: ReactNode;
@@ -25,18 +30,11 @@ export type ConfirmationModalProps = {
confirmationValue?: string;
confirmButtonAccent?: ButtonAccent;
AdditionalButtons?: React.ReactNode;
modalVariant?: ModalVariants;
overlay?: ModalOverlay;
};
const StyledConfirmationModal = styled(Modal)`
border-radius: ${themeCssVariables.spacing[1]};
width: calc(400px - ${themeCssVariables.spacing[32]});
height: auto;
`;
export const StyledCenteredButton = styled(Button)`
box-sizing: border-box;
justify-content: center;
margin-top: ${themeCssVariables.spacing[2]};
`;
@@ -62,7 +60,7 @@ export const StyledConfirmationButton = styled(StyledCenteredButton)`
const defaultConfirmButtonText = msg`Confirm`;
export const ConfirmationModal = ({
modalId,
modalInstanceId,
title,
loading,
subtitle,
@@ -73,7 +71,7 @@ export const ConfirmationModal = ({
confirmationPlaceholder,
confirmButtonAccent = 'danger',
AdditionalButtons,
modalVariant = 'primary',
overlay = 'dark',
}: ConfirmationModalProps) => {
const { i18n, t } = useLingui();
const translatedConfirmButtonText =
@@ -97,12 +95,12 @@ export const ConfirmationModal = ({
const { closeModal } = useModal();
const handleConfirmClick = () => {
closeModal(modalId);
closeModal(modalInstanceId);
onConfirmClick();
};
const handleCancelClick = () => {
closeModal(modalId);
closeModal(modalInstanceId);
onClose?.();
};
@@ -113,17 +111,20 @@ export const ConfirmationModal = ({
};
return (
<StyledConfirmationModal
modalId={modalId}
<ModalStatefulWrapper
modalInstanceId={modalInstanceId}
onClose={() => {
onClose?.();
}}
onEnter={handleEnter}
isClosable={true}
padding="large"
modalVariant={modalVariant}
overlay={overlay}
dataGloballyPreventClickOutside
ignoreContainer
renderInDocumentBody
smallBorderRadius
narrowWidth
autoHeight
>
<StyledCenteredTitle>
<H1Title title={title} fontColor={H1TitleFontColor.Primary} />
@@ -153,6 +154,7 @@ export const ConfirmationModal = ({
variant="secondary"
title={t`Cancel`}
fullWidth
justify="center"
dataTestId="confirmation-modal-cancel-button"
/>
@@ -165,8 +167,9 @@ export const ConfirmationModal = ({
title={translatedConfirmButtonText}
disabled={!isValidValue || loading}
fullWidth
justify="center"
dataTestId="confirmation-modal-confirm-button"
/>
</StyledConfirmationModal>
</ModalStatefulWrapper>
);
};
@@ -1,322 +0,0 @@
import { RootStackingContextZIndices } from '@/ui/layout/constants/RootStackingContextZIndices';
import { ModalHotkeysAndClickOutsideEffect } from '@/ui/layout/modal/components/ModalHotkeysAndClickOutsideEffect';
import { ModalComponentInstanceContext } from '@/ui/layout/modal/contexts/ModalComponentInstanceContext';
import { useModalContainer } from '@/ui/layout/modal/contexts/ModalContainerContext';
import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpenedComponentState';
import { MODAL_BACKDROP_CLICK_OUTSIDE_ID } from '@/ui/layout/modal/constants/ModalBackdropClickOutsideId';
import { MODAL_CLICK_OUTSIDE_LISTENER_EXCLUDED_ID } from '@/ui/layout/modal/constants/ModalClickOutsideListenerExcludedClassName';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { ClickOutsideListenerContext } from '@/ui/utilities/pointer-event/contexts/ClickOutsideListenerContext';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { styled } from '@linaria/react';
import { AnimatePresence, motion } from 'framer-motion';
import React, { useContext, useRef } from 'react';
import { createPortal } from 'react-dom';
import { isDefined } from 'twenty-shared/utils';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { ThemeContext } from 'twenty-ui/theme';
const StyledModalDivBase = styled.div<{
size?: ModalSize;
padding?: ModalPadding;
isMobile: boolean;
modalVariant: ModalVariants;
}>`
display: flex;
flex-direction: column;
box-shadow: ${({ modalVariant }) =>
modalVariant === 'primary'
? themeCssVariables.boxShadow.superHeavy
: modalVariant === 'transparent'
? 'none'
: themeCssVariables.boxShadow.strong};
background: ${({ modalVariant }) =>
modalVariant === 'transparent'
? 'transparent'
: themeCssVariables.background.primary};
color: ${themeCssVariables.font.color.primary};
border-radius: ${({ isMobile, modalVariant }) => {
if (isMobile || modalVariant === 'transparent') return `0`;
return themeCssVariables.border.radius.md;
}};
overflow-x: hidden;
overflow-y: auto;
z-index: ${RootStackingContextZIndices.RootModal}; // should be higher than Backdrop's z-index
width: ${({ isMobile, size }) => {
if (isMobile)
return themeCssVariables.modal.size.fullscreen.width ?? 'auto';
switch (size) {
case 'small':
return themeCssVariables.modal.size.sm.width ?? 'auto';
case 'medium':
return themeCssVariables.modal.size.md.width ?? 'auto';
case 'large':
return themeCssVariables.modal.size.lg.width ?? 'auto';
case 'extraLarge':
return themeCssVariables.modal.size.xl.width ?? 'auto';
default:
return 'auto';
}
}};
padding: ${({ padding }) => {
switch (padding) {
case 'none':
return themeCssVariables.spacing[0];
case 'small':
return themeCssVariables.spacing[2];
case 'medium':
return themeCssVariables.spacing[4];
case 'large':
return themeCssVariables.spacing[6];
default:
return 'auto';
}
}};
height: ${({ isMobile, size }) => {
if (isMobile)
return themeCssVariables.modal.size.fullscreen.height ?? 'auto';
switch (size) {
case 'extraLarge':
return themeCssVariables.modal.size.xl.height ?? 'auto';
default:
return 'auto';
}
}};
max-height: ${({ isMobile }) => (isMobile ? 'none' : '90dvh')};
`;
const StyledModalDiv = motion.create(StyledModalDivBase);
const StyledHeader = styled.div`
align-items: center;
display: flex;
flex-direction: row;
height: 60px;
overflow: hidden;
padding: ${themeCssVariables.spacing[5]};
`;
const StyledContent = styled.div<{
isVerticalCentered?: boolean;
isHorizontalCentered?: boolean;
}>`
display: flex;
flex: 1;
flex: 1 1 0%;
flex-direction: column;
padding: ${themeCssVariables.spacing[10]};
align-items: ${({ isVerticalCentered }) =>
isVerticalCentered ? 'center' : 'stretch'};
justify-content: ${({ isHorizontalCentered }) =>
isHorizontalCentered ? 'center' : 'flex-start'};
`;
const StyledFooter = styled.div`
align-items: center;
display: flex;
flex-direction: row;
height: 60px;
overflow: hidden;
padding: ${themeCssVariables.spacing[5]};
`;
const StyledBackDropBase = styled.div<{
modalVariant: ModalVariants;
isInContainer?: boolean;
}>`
align-items: center;
background: ${({ modalVariant, isInContainer }) =>
isInContainer
? themeCssVariables.background.overlayTertiary
: modalVariant === 'primary' || modalVariant === 'transparent'
? themeCssVariables.background.overlayPrimary
: modalVariant === 'secondary'
? themeCssVariables.background.overlaySecondary
: themeCssVariables.background.overlayTertiary};
display: flex;
height: 100%;
justify-content: center;
left: 0;
pointer-events: auto;
position: ${({ isInContainer }) => (isInContainer ? 'absolute' : 'fixed')};
top: 0;
width: 100%;
z-index: ${RootStackingContextZIndices.RootModalBackDrop};
user-select: none;
`;
const StyledBackDrop = motion.create(StyledBackDropBase);
type ModalHeaderProps = React.PropsWithChildren & {
className?: string;
};
const ModalHeader = ({ children, className }: ModalHeaderProps) => (
<StyledHeader className={className}>{children}</StyledHeader>
);
type ModalContentProps = React.PropsWithChildren & {
className?: string;
isVerticalCentered?: boolean;
isHorizontalCentered?: boolean;
};
const ModalContent = ({
children,
className,
isVerticalCentered,
isHorizontalCentered,
}: ModalContentProps) => (
<StyledContent
className={className}
isVerticalCentered={isVerticalCentered}
isHorizontalCentered={isHorizontalCentered}
>
{children}
</StyledContent>
);
type ModalFooterProps = React.PropsWithChildren & {
className?: string;
};
const ModalFooter = ({ children, className }: ModalFooterProps) => (
<StyledFooter className={className}>{children}</StyledFooter>
);
export type ModalSize = 'small' | 'medium' | 'large' | 'extraLarge';
export type ModalPadding = 'none' | 'small' | 'medium' | 'large';
export type ModalVariants =
| 'primary'
| 'secondary'
| 'tertiary'
| 'transparent';
export type ModalProps = React.PropsWithChildren & {
modalId: string;
size?: ModalSize;
padding?: ModalPadding;
className?: string;
onEnter?: () => void;
modalVariant?: ModalVariants;
dataGloballyPreventClickOutside?: boolean;
shouldCloseModalOnClickOutsideOrEscape?: boolean;
ignoreContainer?: boolean;
} & (
| { isClosable: true; onClose?: () => void }
| { isClosable?: false; onClose?: never }
);
const modalAnimation = {
hidden: { opacity: 0 },
visible: { opacity: 1 },
exit: { opacity: 0 },
};
export const Modal = ({
modalId,
children,
size = 'medium',
padding = 'medium',
className,
onEnter,
isClosable = false,
onClose,
modalVariant = 'primary',
dataGloballyPreventClickOutside = false,
shouldCloseModalOnClickOutsideOrEscape = true,
ignoreContainer = false,
}: ModalProps) => {
const isMobile = useIsMobile();
const modalRef = useRef<HTMLDivElement>(null);
const { container } = useModalContainer();
const effectiveContainer = ignoreContainer
? isDefined(document)
? document.body
: null
: container;
const isInContainer = isDefined(container) && !ignoreContainer;
const { theme } = useContext(ThemeContext);
const stopEventPropagation = (e: React.MouseEvent) => {
e.stopPropagation();
};
const isModalOpened = useAtomComponentStateValue(
isModalOpenedComponentState,
modalId,
);
const { closeModal } = useModal();
const handleClose = () => {
onClose?.();
if (shouldCloseModalOnClickOutsideOrEscape) closeModal(modalId);
};
const modalContent = (
<AnimatePresence mode="wait">
{isModalOpened && (
<ModalComponentInstanceContext.Provider
value={{
instanceId: modalId,
}}
>
<ClickOutsideListenerContext.Provider
value={{
excludedClickOutsideId: MODAL_CLICK_OUTSIDE_LISTENER_EXCLUDED_ID,
}}
>
<ModalHotkeysAndClickOutsideEffect
modalId={modalId}
modalRef={modalRef}
onEnter={onEnter}
isClosable={isClosable}
onClose={handleClose}
/>
<StyledBackDrop
data-testid="modal-backdrop"
data-click-outside-id={MODAL_BACKDROP_CLICK_OUTSIDE_ID}
onMouseDown={stopEventPropagation}
modalVariant={modalVariant}
isInContainer={isInContainer}
>
<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}
data-globally-prevent-click-outside={
dataGloballyPreventClickOutside
}
>
{children}
</StyledModalDiv>
</StyledBackDrop>
</ClickOutsideListenerContext.Provider>
</ModalComponentInstanceContext.Provider>
)}
</AnimatePresence>
);
if (isDefined(effectiveContainer)) {
return createPortal(modalContent, effectiveContainer);
}
return modalContent;
};
Modal.Header = ModalHeader;
Modal.Content = ModalContent;
Modal.Footer = ModalFooter;
Modal.Backdrop = StyledBackDrop;
@@ -9,7 +9,7 @@ type ModalHotkeysAndClickOutsideEffectProps = {
onEnter?: () => void;
isClosable?: boolean;
onClose?: () => void;
modalId: string;
modalInstanceId: string;
};
export const ModalHotkeysAndClickOutsideEffect = ({
@@ -17,14 +17,14 @@ export const ModalHotkeysAndClickOutsideEffect = ({
onEnter,
isClosable = false,
onClose,
modalId,
modalInstanceId,
}: ModalHotkeysAndClickOutsideEffectProps) => {
useHotkeysOnFocusedElement({
keys: [Key.Enter],
callback: () => {
onEnter?.();
},
focusId: modalId,
focusId: modalInstanceId,
dependencies: [onEnter],
});
@@ -35,7 +35,7 @@ export const ModalHotkeysAndClickOutsideEffect = ({
onClose();
}
},
focusId: modalId,
focusId: modalInstanceId,
dependencies: [isClosable, onClose],
});
@@ -45,7 +45,7 @@ export const ModalHotkeysAndClickOutsideEffect = ({
MODAL_CLICK_OUTSIDE_LISTENER_EXCLUDED_ID,
DIALOG_CLICK_OUTSIDE_ID,
],
listenerId: `MODAL_CLICK_OUTSIDE_LISTENER_ID_${modalId}`,
listenerId: `MODAL_CLICK_OUTSIDE_LISTENER_ID_${modalInstanceId}`,
callback: () => {
if (isClosable && onClose !== undefined) {
onClose();
@@ -0,0 +1,96 @@
import { ModalHotkeysAndClickOutsideEffect } from '@/ui/layout/modal/components/ModalHotkeysAndClickOutsideEffect';
import { MODAL_BACKDROP_CLICK_OUTSIDE_ID } from '@/ui/layout/modal/constants/ModalBackdropClickOutsideId';
import { MODAL_CLICK_OUTSIDE_LISTENER_EXCLUDED_ID } from '@/ui/layout/modal/constants/ModalClickOutsideListenerExcludedClassName';
import { RootStackingContextZIndices } from '@/ui/layout/constants/RootStackingContextZIndices';
import { ModalComponentInstanceContext } from '@/ui/layout/modal/contexts/ModalComponentInstanceContext';
import { useModalContainer } from '@/ui/layout/modal/contexts/ModalContainerContext';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpenedComponentState';
import { type ModalStatefulWrapperProps } from '@/ui/layout/modal/types/ModalStatefulWrapperProps';
import { ClickOutsideListenerContext } from '@/ui/utilities/pointer-event/contexts/ClickOutsideListenerContext';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useRef } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { Modal } from 'twenty-ui/layout';
export const ModalStatefulWrapper = ({
modalInstanceId,
children,
size = 'medium',
padding = 'medium',
onEnter,
isClosable = false,
onClose,
overlay = 'dark',
dataGloballyPreventClickOutside = false,
shouldCloseModalOnClickOutsideOrEscape = true,
renderInDocumentBody = false,
gap,
smallBorderRadius,
narrowWidth,
autoHeight,
}: ModalStatefulWrapperProps) => {
const isMobile = useIsMobile();
const modalRef = useRef<HTMLDivElement>(null);
const { container } = useModalContainer();
const effectiveContainer = renderInDocumentBody ? document.body : container;
const isInContainer = isDefined(container) && !renderInDocumentBody;
const isModalOpened = useAtomComponentStateValue(
isModalOpenedComponentState,
modalInstanceId,
);
const { closeModal } = useModal();
const handleClose = () => {
onClose?.();
if (shouldCloseModalOnClickOutsideOrEscape) {
closeModal(modalInstanceId);
}
};
return (
<ModalComponentInstanceContext.Provider
value={{ instanceId: modalInstanceId }}
>
<ClickOutsideListenerContext.Provider
value={{
excludedClickOutsideId: MODAL_CLICK_OUTSIDE_LISTENER_EXCLUDED_ID,
}}
>
{isModalOpened && (
<ModalHotkeysAndClickOutsideEffect
modalInstanceId={modalInstanceId}
modalRef={modalRef}
onEnter={onEnter}
isClosable={isClosable}
onClose={handleClose}
/>
)}
<Modal
isOpen={isModalOpened}
size={size}
padding={padding}
overlay={isInContainer ? 'light' : overlay}
isMobile={isMobile}
isInContainer={isInContainer}
container={effectiveContainer}
gap={gap}
smallBorderRadius={smallBorderRadius}
narrowWidth={narrowWidth}
autoHeight={autoHeight}
modalZIndex={RootStackingContextZIndices.RootModal}
backdropZIndex={RootStackingContextZIndices.RootModalBackDrop}
backdropClickOutsideId={MODAL_BACKDROP_CLICK_OUTSIDE_ID}
preventClickOutside={dataGloballyPreventClickOutside}
modalRef={modalRef}
>
{children}
</Modal>
</ClickOutsideListenerContext.Provider>
</ModalComponentInstanceContext.Provider>
);
};
@@ -54,7 +54,7 @@ const confirmMock = fn();
export const Default: Story = {
args: {
modalId: 'confirmation-modal',
modalInstanceId: 'confirmation-modal',
title: 'Pariatur labore.',
subtitle: 'Velit dolore aliquip laborum occaecat fugiat.',
confirmButtonText: 'Delete',
@@ -72,7 +72,7 @@ export const InputConfirmation: Story = {
export const CloseOnEscape: Story = {
args: {
modalId: 'confirmation-modal',
modalInstanceId: 'confirmation-modal',
title: 'Escape Key Test',
subtitle: 'This modal should close when pressing the Escape key.',
confirmButtonText: 'Confirm',
@@ -95,7 +95,7 @@ export const CloseOnEscape: Story = {
export const CloseOnClickOutside: Story = {
args: {
modalId: 'confirmation-modal',
modalInstanceId: 'confirmation-modal',
title: 'Click Outside Test',
subtitle: 'This modal should close when clicking outside of it.',
confirmButtonText: 'Confirm',
@@ -121,7 +121,7 @@ export const CloseOnClickOutside: Story = {
export const ConfirmWithEnterKey: Story = {
args: {
modalId: 'confirmation-modal',
modalInstanceId: 'confirmation-modal',
title: 'Enter Key Test',
subtitle: 'This modal should confirm when pressing the Enter key.',
confirmButtonText: 'Confirm',
@@ -142,7 +142,7 @@ export const ConfirmWithEnterKey: Story = {
export const CancelButtonClick: Story = {
args: {
modalId: 'confirmation-modal',
modalInstanceId: 'confirmation-modal',
title: 'Cancel Button Test',
subtitle: 'Clicking the cancel button should close the modal',
confirmButtonText: 'Confirm',
@@ -166,7 +166,7 @@ export const CancelButtonClick: Story = {
export const ConfirmButtonClick: Story = {
args: {
modalId: 'confirmation-modal',
modalInstanceId: 'confirmation-modal',
title: 'Confirm Button Test',
subtitle: 'Clicking the confirm button should trigger the confirm action',
confirmButtonText: 'Confirm',
@@ -5,11 +5,12 @@ import {
} from '@storybook/react-vite';
import { expect, fn, userEvent, waitFor, within } from 'storybook/test';
import { Modal } from '@/ui/layout/modal/components/Modal';
import { ModalStatefulWrapper } from '@/ui/layout/modal/components/ModalStatefulWrapper';
import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpenedComponentState';
import { focusStackState } from '@/ui/utilities/focus/states/focusStackState';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { ModalContent, ModalFooter, ModalHeader } from 'twenty-ui/layout';
import { ComponentDecorator } from 'twenty-ui/testing';
import { RootDecorator } from '~/testing/decorators/RootDecorator';
import { sleep } from '~/utils/sleep';
@@ -37,9 +38,9 @@ const JotaiInitDecorator: Decorator = (Story) => {
return <Story />;
};
const meta: Meta<typeof Modal> = {
title: 'UI/Layout/Modal/Modal',
component: Modal,
const meta: Meta<typeof ModalStatefulWrapper> = {
title: 'UI/Layout/Modal/ModalStatefulWrapper',
component: ModalStatefulWrapper,
decorators: [JotaiInitDecorator, RootDecorator, ComponentDecorator],
parameters: {
disableHotkeyInitialization: true,
@@ -47,26 +48,26 @@ const meta: Meta<typeof Modal> = {
};
export default meta;
type Story = StoryObj<typeof Modal>;
type Story = StoryObj<typeof ModalStatefulWrapper>;
const closeMock = fn();
export const Default: Story = {
args: {
modalId: 'modal-id',
modalInstanceId: 'modal-id',
size: 'medium',
padding: 'medium',
children: (
<>
<Modal.Header>Stay in touch</Modal.Header>
<Modal.Content>
<ModalHeader>Stay in touch</ModalHeader>
<ModalContent>
This is a dummy newletter form so don't bother trying to test it. Not
that I expect you to, anyways. :)
</Modal.Content>
<Modal.Footer>
</ModalContent>
<ModalFooter>
By using Twenty, you're opting for the finest CRM experience you'll
ever encounter.
</Modal.Footer>
</ModalFooter>
</>
),
},
@@ -74,17 +75,17 @@ export const Default: Story = {
export const CloseClosableModalOnClickOutside: Story = {
args: {
modalId: 'modal-id',
modalInstanceId: 'modal-id',
size: 'medium',
padding: 'medium',
isClosable: true,
onClose: closeMock,
children: (
<>
<Modal.Header>Click Outside Test</Modal.Header>
<Modal.Content>
<ModalHeader>Click Outside Test</ModalHeader>
<ModalContent>
This modal should close when clicking outside of it.
</Modal.Content>
</ModalContent>
</>
),
},
@@ -94,7 +95,6 @@ export const CloseClosableModalOnClickOutside: Story = {
await canvas.findByText('Click Outside Test');
const backdrop = await canvas.findByTestId('modal-backdrop');
// We need to wait for the outside click listener to be registered
await sleep(100);
await userEvent.click(backdrop);
@@ -106,17 +106,17 @@ export const CloseClosableModalOnClickOutside: Story = {
export const CloseClosableModalOnEscape: Story = {
args: {
modalId: 'modal-id',
modalInstanceId: 'modal-id',
size: 'medium',
padding: 'medium',
isClosable: true,
onClose: closeMock,
children: (
<>
<Modal.Header>Escape Key Test</Modal.Header>
<Modal.Content>
<ModalHeader>Escape Key Test</ModalHeader>
<ModalContent>
This modal should close when pressing the Escape key.
</Modal.Content>
</ModalContent>
</>
),
},
@@ -14,9 +14,11 @@ export const useModal = () => {
const store = useStore();
const closeModal = useCallback(
(modalId: string) => {
(modalInstanceId: string) => {
const isModalOpen = store.get(
isModalOpenedComponentState.atomFamily({ instanceId: modalId }),
isModalOpenedComponentState.atomFamily({
instanceId: modalInstanceId,
}),
);
if (!isModalOpen) {
@@ -24,11 +26,13 @@ export const useModal = () => {
}
removeFocusItemFromFocusStackById({
focusId: modalId,
focusId: modalInstanceId,
});
store.set(
isModalOpenedComponentState.atomFamily({ instanceId: modalId }),
isModalOpenedComponentState.atomFamily({
instanceId: modalInstanceId,
}),
false,
);
},
@@ -36,9 +40,11 @@ export const useModal = () => {
);
const openModal = useCallback(
(modalId: string) => {
(modalInstanceId: string) => {
const isModalOpened = store.get(
isModalOpenedComponentState.atomFamily({ instanceId: modalId }),
isModalOpenedComponentState.atomFamily({
instanceId: modalInstanceId,
}),
);
if (isModalOpened) {
@@ -46,15 +52,17 @@ export const useModal = () => {
}
store.set(
isModalOpenedComponentState.atomFamily({ instanceId: modalId }),
isModalOpenedComponentState.atomFamily({
instanceId: modalInstanceId,
}),
true,
);
pushFocusItemToFocusStack({
focusId: modalId,
focusId: modalInstanceId,
component: {
type: FocusComponentType.MODAL,
instanceId: modalId,
instanceId: modalInstanceId,
},
globalHotkeysConfig: {
enableGlobalHotkeysWithModifiers: false,
@@ -66,15 +74,17 @@ export const useModal = () => {
);
const toggleModal = useCallback(
(modalId: string) => {
(modalInstanceId: string) => {
const isModalOpen = store.get(
isModalOpenedComponentState.atomFamily({ instanceId: modalId }),
isModalOpenedComponentState.atomFamily({
instanceId: modalInstanceId,
}),
);
if (isModalOpen) {
closeModal(modalId);
closeModal(modalInstanceId);
} else {
openModal(modalId);
openModal(modalInstanceId);
}
},
[store, closeModal, openModal],
@@ -0,0 +1,23 @@
import type React from 'react';
import { type ModalProps } from 'twenty-ui/layout';
export type ModalStatefulWrapperProps = Pick<
ModalProps,
| 'size'
| 'padding'
| 'overlay'
| 'gap'
| 'smallBorderRadius'
| 'narrowWidth'
| 'autoHeight'
> &
React.PropsWithChildren & {
modalInstanceId: string;
onEnter?: () => void;
dataGloballyPreventClickOutside?: boolean;
shouldCloseModalOnClickOutsideOrEscape?: boolean;
renderInDocumentBody?: boolean;
} & (
| { isClosable: true; onClose?: () => void }
| { isClosable?: false; onClose?: never }
);
@@ -42,7 +42,7 @@ export const OverrideWorkflowDraftConfirmationModal = ({
return (
<>
<ConfirmationModal
modalId={OVERRIDE_WORKFLOW_DRAFT_CONFIRMATION_MODAL_ID}
modalInstanceId={OVERRIDE_WORKFLOW_DRAFT_CONFIRMATION_MODAL_ID}
title={t`A draft already exists`}
subtitle={t`A draft already exists for this workflow. Are you sure you want to erase it?`}
onConfirmClick={handleOverrideDraft}
@@ -11,7 +11,7 @@ import { useCaptcha } from '@/client-config/hooks/useCaptcha';
import { useRedirect } from '@/domain-manager/hooks/useRedirect';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { TextInput } from '@/ui/input/components/TextInput';
import { Modal } from '@/ui/layout/modal/components/Modal';
import { ModalContent } from 'twenty-ui/layout';
import { ApolloError } from '@apollo/client';
import { styled } from '@linaria/react';
import { zodResolver } from '@hookform/resolvers/zod';
@@ -206,7 +206,7 @@ export const PasswordReset = () => {
return (
isTokenValid && (
<Modal.Content isVerticalCentered isHorizontalCentered>
<ModalContent isVerticallyCentered isHorizontallyCentered>
<StyledMainContainer>
<AnimatedEaseIn>
<Logo
@@ -291,7 +291,7 @@ export const PasswordReset = () => {
)}
</StyledContentContainer>
</StyledMainContainer>
</Modal.Content>
</ModalContent>
)
);
};
@@ -29,7 +29,7 @@ import { SignInUpTwoFactorAuthenticationProvision } from '@/auth/sign-in-up/comp
import { SignInUpTOTPVerification } from '@/auth/sign-in-up/components/internal/SignInUpTwoFactorAuthenticationVerification';
import { useWorkspaceFromInviteHash } from '@/auth/sign-in-up/hooks/useWorkspaceFromInviteHash';
import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState';
import { Modal } from '@/ui/layout/modal/components/Modal';
import { ModalContent } from 'twenty-ui/layout';
import { useLingui } from '@lingui/react/macro';
import { useSearchParams } from 'react-router-dom';
import { isDefined } from 'twenty-shared/utils';
@@ -61,7 +61,7 @@ const StandardContent = ({
onClickOnLogo: () => void;
}) => {
return (
<Modal.Content isVerticalCentered isHorizontalCentered>
<ModalContent isVerticallyCentered isHorizontallyCentered>
<AnimatedEaseIn>
<Logo
secondaryLogo={workspacePublicData?.logo}
@@ -77,7 +77,7 @@ const StandardContent = ({
SignInUpStep.TwoFactorAuthenticationVerification,
SignInUpStep.WorkspaceSelection,
].includes(signInUpStep) && <FooterNote />}
</Modal.Content>
</ModalContent>
);
};
@@ -205,9 +205,9 @@ export const SignInUp = () => {
if (signInUpStep === SignInUpStep.EmailVerification) {
return (
<Modal.Content isVerticalCentered isHorizontalCentered>
<ModalContent isVerticallyCentered isHorizontallyCentered>
<EmailVerificationSent email={searchParams.get('email')} />
</Modal.Content>
</ModalContent>
);
}
@@ -1,12 +1,11 @@
import Cal from '@calcom/embed-react';
import { styled } from '@linaria/react';
import { useContext } from 'react';
import { Link } from 'react-router-dom';
import { currentUserState } from '@/auth/states/currentUserState';
import { calendarBookingPageIdState } from '@/client-config/states/calendarBookingPageIdState';
import { useSetNextOnboardingStatus } from '@/onboarding/hooks/useSetNextOnboardingStatus';
import { Modal } from '@/ui/layout/modal/components/Modal';
import { ModalContent, ModalFooter } from 'twenty-ui/layout';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { useLingui } from '@lingui/react/macro';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
@@ -14,24 +13,12 @@ import { AppPath } from 'twenty-shared/types';
import { IconChevronLeft, IconChevronRightPipe } from 'twenty-ui/display';
import { LightButton } from 'twenty-ui/input';
import { ThemeContext } from 'twenty-ui/theme';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { useIsMobile } from 'twenty-ui/utilities';
import {
OnboardingStatus,
useSkipBookOnboardingStepMutation,
} from '~/generated-metadata/graphql';
const StyledModalFooter = styled(Modal.Footer)`
height: auto;
justify-content: center;
padding: ${themeCssVariables.spacing[3]};
`;
const StyledModalContent = styled(Modal.Content)`
overflow: hidden;
padding: 0;
`;
export const BookCall = () => {
const { t } = useLingui();
const { theme } = useContext(ThemeContext);
@@ -51,7 +38,12 @@ export const BookCall = () => {
return (
<>
<StyledModalContent isHorizontalCentered isVerticalCentered>
<ModalContent
noPadding
overflowHidden
isHorizontallyCentered
isVerticallyCentered
>
<ScrollWrapper
componentInstanceId="scroll-wrapper-modal-content"
autoHeight={!isMobile}
@@ -66,8 +58,8 @@ export const BookCall = () => {
}}
/>
</ScrollWrapper>
</StyledModalContent>
<StyledModalFooter>
</ModalContent>
<ModalFooter autoHeight centered smallPadding>
{isPlanRequired ? (
<Link to={AppPath.PlanRequired}>
<LightButton Icon={IconChevronLeft} title={t`Back`} />
@@ -79,7 +71,7 @@ export const BookCall = () => {
onClick={handleCompleteOnboarding}
/>
)}
</StyledModalFooter>
</ModalFooter>
</>
);
};
@@ -1,7 +1,7 @@
import { SubTitle } from '@/auth/components/SubTitle';
import { Title } from '@/auth/components/Title';
import { useSetNextOnboardingStatus } from '@/onboarding/hooks/useSetNextOnboardingStatus';
import { Modal } from '@/ui/layout/modal/components/Modal';
import { ModalContent } from 'twenty-ui/layout';
import { styled } from '@linaria/react';
import { Trans, useLingui } from '@lingui/react/macro';
import { Link } from 'react-router-dom';
@@ -17,10 +17,6 @@ const StyledCoverImage = styled.img`
width: 320px;
`;
const StyledModalContent = styled(Modal.Content)`
gap: ${themeCssVariables.spacing[8]};
`;
const StyledTitleContainer = styled.div`
align-items: center;
display: flex;
@@ -51,7 +47,7 @@ export const BookCallDecision = () => {
};
return (
<StyledModalContent isVerticalCentered isHorizontalCentered>
<ModalContent gap={8} isVerticallyCentered isHorizontallyCentered>
<StyledTitleContainer>
<Title noMarginTop>
<Trans>Book your onboarding</Trans>
@@ -70,6 +66,6 @@ export const BookCallDecision = () => {
</StyledLink>
<LightButton title={t`Finish`} onClick={handleFinish} />
</StyledButtonContainer>
</StyledModalContent>
</ModalContent>
);
};
@@ -1,4 +1,4 @@
import { Modal } from '@/ui/layout/modal/components/Modal';
import { ModalContent } from 'twenty-ui/layout';
import { styled } from '@linaria/react';
import { isDefined } from 'twenty-shared/utils';
import { ChooseYourPlanContent } from '~/pages/onboarding/internal/ChooseYourPlanContent';
@@ -14,12 +14,12 @@ export const ChooseYourPlan = () => {
const { isPlansLoaded } = usePlans();
const billing = useAtomStateValue(billingState);
return (
<Modal.Content isVerticalCentered>
<ModalContent isVerticallyCentered>
{isDefined(billing) && isPlansLoaded ? (
<ChooseYourPlanContent billing={billing} />
) : (
<StyledChooseYourPlanPlaceholder />
)}
</Modal.Content>
</ModalContent>
);
};
@@ -18,7 +18,7 @@ import { WorkspaceMemberPictureUploader } from '@/settings/workspace-member/comp
import { PageFocusId } from '@/types/PageFocusId';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { TextInput } from '@/ui/input/components/TextInput';
import { Modal } from '@/ui/layout/modal/components/Modal';
import { ModalContent } from 'twenty-ui/layout';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { ApolloError } from '@apollo/client';
import { i18n } from '@lingui/core';
@@ -172,7 +172,7 @@ export const CreateProfile = () => {
});
return (
<Modal.Content isVerticalCentered isHorizontalCentered>
<ModalContent isVerticallyCentered isHorizontallyCentered>
<Title noMarginTop>
<Trans>Create profile</Trans>
</Title>
@@ -251,6 +251,6 @@ export const CreateProfile = () => {
fullWidth
/>
</StyledButtonContainer>
</Modal.Content>
</ModalContent>
);
};
@@ -14,7 +14,7 @@ import { useSetNextOnboardingStatus } from '@/onboarding/hooks/useSetNextOnboard
import { WorkspaceLogoUploader } from '@/settings/workspace/components/WorkspaceLogoUploader';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { TextInput } from '@/ui/input/components/TextInput';
import { Modal } from '@/ui/layout/modal/components/Modal';
import { ModalContent } from 'twenty-ui/layout';
import { useLoadCurrentUser } from '@/users/hooks/useLoadCurrentUser';
import { ApolloError } from '@apollo/client';
import { Trans, useLingui } from '@lingui/react/macro';
@@ -152,7 +152,7 @@ export const CreateWorkspace = () => {
};
return (
<Modal.Content isVerticalCentered isHorizontalCentered>
<ModalContent isVerticallyCentered isHorizontallyCentered>
{pendingCreationLoaderStep !== PendingCreationLoaderStep.None && (
<>
<Logo
@@ -241,6 +241,6 @@ export const CreateWorkspace = () => {
</StyledButtonContainer>
</>
)}
</Modal.Content>
</ModalContent>
);
};
@@ -6,7 +6,7 @@ import { useSetNextOnboardingStatus } from '@/onboarding/hooks/useSetNextOnboard
import { PageFocusId } from '@/types/PageFocusId';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { TextInput } from '@/ui/input/components/TextInput';
import { Modal } from '@/ui/layout/modal/components/Modal';
import { ModalContent } from 'twenty-ui/layout';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { styled } from '@linaria/react';
import { zodResolver } from '@hookform/resolvers/zod';
@@ -169,7 +169,7 @@ export const InviteTeam = () => {
});
return (
<Modal.Content isVerticalCentered isHorizontalCentered>
<ModalContent isVerticallyCentered isHorizontallyCentered>
<Title>
<Trans>Invite your team</Trans>
</Title>
@@ -229,6 +229,6 @@ export const InviteTeam = () => {
<Trans>Skip</Trans>
</ClickToActionLink>
</StyledActionSkipLinkContainer>
</Modal.Content>
</ModalContent>
);
};
@@ -2,7 +2,7 @@ import { SubTitle } from '@/auth/components/SubTitle';
import { Title } from '@/auth/components/Title';
import { currentUserState } from '@/auth/states/currentUserState';
import { OnboardingModalCircularIcon } from '@/onboarding/components/OnboardingModalCircularIcon';
import { Modal } from '@/ui/layout/modal/components/Modal';
import { ModalContent } from 'twenty-ui/layout';
import { useSubscriptionStatus } from '@/workspace/hooks/useSubscriptionStatus';
import { styled } from '@linaria/react';
import { t } from '@lingui/core/macro';
@@ -13,15 +13,10 @@ import { isDefined } from 'twenty-shared/utils';
import { IconCheck } from 'twenty-ui/display';
import { Loader } from 'twenty-ui/feedback';
import { MainButton } from 'twenty-ui/input';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { AnimatedEaseIn } from 'twenty-ui/utilities';
import { useGetCurrentUserLazyQuery } from '~/generated-metadata/graphql';
import { useNavigateApp } from '~/hooks/useNavigateApp';
const StyledModalContent = styled(Modal.Content)`
gap: ${themeCssVariables.spacing[8]};
`;
const StyledTitleContainer = styled.div`
display: flex;
flex-direction: column;
@@ -67,7 +62,7 @@ export const PaymentSuccess = () => {
};
return (
<StyledModalContent isVerticalCentered isHorizontalCentered>
<ModalContent gap={8} isVerticallyCentered isHorizontallyCentered>
<AnimatedEaseIn>
<OnboardingModalCircularIcon Icon={IconCheck} />
</AnimatedEaseIn>
@@ -82,6 +77,6 @@ export const PaymentSuccess = () => {
Icon={() => (isLoading ? <Loader /> : null)}
disabled={isLoading}
/>
</StyledModalContent>
</ModalContent>
);
};
@@ -15,7 +15,7 @@ import { isMicrosoftCalendarEnabledState } from '@/client-config/states/isMicros
import { isMicrosoftMessagingEnabledState } from '@/client-config/states/isMicrosoftMessagingEnabledState';
import { useTriggerApisOAuth } from '@/settings/accounts/hooks/useTriggerApiOAuth';
import { PageFocusId } from '@/types/PageFocusId';
import { Modal } from '@/ui/layout/modal/components/Modal';
import { ModalContent } from 'twenty-ui/layout';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { t } from '@lingui/core/macro';
import { AppPath, ConnectedAccountProvider } from 'twenty-shared/types';
@@ -116,7 +116,7 @@ export const SyncEmails = () => {
});
return (
<Modal.Content isVerticalCentered isHorizontalCentered>
<ModalContent isVerticallyCentered isHorizontallyCentered>
<Title noMarginTop>{t`Emails and Calendar`}</Title>
<SubTitle>
{t`Sync your Emails and Calendar with Twenty. Choose your privacy settings.`}
@@ -175,6 +175,6 @@ export const SyncEmails = () => {
{t`Continue without sync`}
</ClickToActionLink>
</StyledActionLinkContainer>
</Modal.Content>
</ModalContent>
);
};
@@ -195,7 +195,7 @@ export const SettingsAdminConfigVariableDetails = () => {
</SubMenuTopBarContainer>
<ConfirmationModal
modalId={RESET_VARIABLE_MODAL_ID}
modalInstanceId={RESET_VARIABLE_MODAL_ID}
title={t`Reset variable`}
subtitle={t`This will revert the database value to environment/default value. The database override will be removed and the system will use the environment settings.`}
onConfirmClick={handleConfirmReset}
@@ -583,7 +583,7 @@ export const SettingsSkillForm = ({ mode }: { mode: 'create' | 'edit' }) => {
</SettingsPageContainer>
<ConfirmationModal
modalId={DELETE_SKILL_MODAL_ID}
modalInstanceId={DELETE_SKILL_MODAL_ID}
title={t`Delete Skill`}
subtitle={t`Are you sure you want to delete this skill? This action cannot be undone.`}
onConfirmClick={handleDelete}
@@ -46,7 +46,7 @@ export const SettingsAgentDeleteConfirmationModal = ({
<ConfirmationModal
confirmationValue={agentName}
confirmationPlaceholder={agentName}
modalId={DELETE_AGENT_MODAL_ID}
modalInstanceId={DELETE_AGENT_MODAL_ID}
title={t`Delete Agent`}
subtitle={
<Trans>
@@ -198,7 +198,7 @@ export const SettingsAgentEvalsTab = ({
</Section>
<ConfirmationModal
modalId={DELETE_EVAL_INPUT_MODAL_ID}
modalInstanceId={DELETE_EVAL_INPUT_MODAL_ID}
title={t`Delete Evaluation Input`}
subtitle={t`Are you sure you want to delete this evaluation input?`}
onConfirmClick={handleDeleteInput}
@@ -592,7 +592,7 @@ export const SettingsApplicationRegistrationDetails = () => {
<ConfirmationModal
confirmationPlaceholder={confirmationValue}
confirmationValue={confirmationValue}
modalId={ROTATE_SECRET_MODAL_ID}
modalInstanceId={ROTATE_SECRET_MODAL_ID}
title={t`Rotate client secret`}
subtitle={
<Trans>
@@ -609,7 +609,7 @@ export const SettingsApplicationRegistrationDetails = () => {
<ConfirmationModal
confirmationPlaceholder={confirmationValue}
confirmationValue={confirmationValue}
modalId={DELETE_REGISTRATION_MODAL_ID}
modalInstanceId={DELETE_REGISTRATION_MODAL_ID}
title={t`Delete app`}
subtitle={
<Trans>
@@ -110,7 +110,7 @@ export const SettingsApplicationDetailAboutTab = ({
<ConfirmationModal
confirmationPlaceholder={confirmationValue}
confirmationValue={confirmationValue}
modalId={UNINSTALL_APPLICATION_MODAL_ID}
modalInstanceId={UNINSTALL_APPLICATION_MODAL_ID}
title={t`Uninstall Application?`}
subtitle={
<Trans>
@@ -429,7 +429,7 @@ export const SettingsObjectFieldEdit = () => {
</FormProvider>
{fieldMetadataItem?.isCustom && (
<ConfirmationModal
modalId={DELETE_FIELD_MODAL_ID}
modalInstanceId={DELETE_FIELD_MODAL_ID}
title={t`Delete ${fieldLabel} field?`}
subtitle={t`This will permanently delete the field and all its data from ${objectLabel}. Type "yes" to confirm.`}
confirmButtonText={t`Delete`}
@@ -324,7 +324,7 @@ export const SettingsDevelopersApiKeyDetail = () => {
<ConfirmationModal
confirmationPlaceholder={confirmationValue}
confirmationValue={confirmationValue}
modalId={DELETE_API_KEY_MODAL_ID}
modalInstanceId={DELETE_API_KEY_MODAL_ID}
title={t`Delete API key`}
subtitle={
<Trans>
@@ -340,7 +340,7 @@ export const SettingsDevelopersApiKeyDetail = () => {
<ConfirmationModal
confirmationPlaceholder={confirmationValue}
confirmationValue={confirmationValue}
modalId={REGENERATE_API_KEY_MODAL_ID}
modalInstanceId={REGENERATE_API_KEY_MODAL_ID}
title={t`Regenerate an API key`}
subtitle={
<Trans>
@@ -237,7 +237,7 @@ export const SettingsDomain = () => {
</FormProvider>
</form>
<ConfirmationModal
modalId={SUBDOMAIN_CHANGE_CONFIRMATION_MODAL_ID}
modalInstanceId={SUBDOMAIN_CHANGE_CONFIRMATION_MODAL_ID}
title={t`Change subdomain?`}
subtitle={t`You're about to change your workspace subdomain. This action will log out all users.`}
onConfirmClick={() => {
@@ -219,7 +219,7 @@ export const SettingsWorkspaceMember = () => {
</SettingsPageContainer>
<ConfirmationModal
modalId={DELETE_MEMBER_MODAL_ID}
modalInstanceId={DELETE_MEMBER_MODAL_ID}
title={t`Remove member from workspace`}
subtitle={t`This action cannot be undone. This will permanently remove this member from this workspace and remove them from all their assignments.`}
onConfirmClick={handleDeleteMember}