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
@@ -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