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,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;