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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
+7
-14
@@ -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;
|
||||
|
||||
+8
-8
@@ -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}
|
||||
|
||||
+1
-1
@@ -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`}
|
||||
|
||||
+1
-1
@@ -259,7 +259,7 @@ export const CurrentWorkspaceMemberFavorites = ({
|
||||
{isModalOpened &&
|
||||
createPortal(
|
||||
<ConfirmationModal
|
||||
modalId={modalId}
|
||||
modalInstanceId={modalId}
|
||||
title={
|
||||
folder.favorites.length > 1
|
||||
? t`Remove ${favoriteCount} favorites?`
|
||||
|
||||
+1
-1
@@ -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?`
|
||||
|
||||
+1
-1
@@ -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>
|
||||
|
||||
+1
-1
@@ -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}
|
||||
|
||||
+1
-1
@@ -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}
|
||||
|
||||
+1
-1
@@ -137,7 +137,7 @@ export const SettingsAccountsRowDropdownMenu = ({
|
||||
}
|
||||
/>
|
||||
<ConfirmationModal
|
||||
modalId={deleteAccountModalId}
|
||||
modalInstanceId={deleteAccountModalId}
|
||||
title={t`Data deletion`}
|
||||
subtitle={
|
||||
<Trans>
|
||||
|
||||
+3
-3
@@ -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}
|
||||
|
||||
+2
-2
@@ -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}
|
||||
/>
|
||||
|
||||
+3
-3
@@ -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}
|
||||
|
||||
+1
-1
@@ -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`}
|
||||
|
||||
+1
-1
@@ -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>
|
||||
|
||||
+1
-1
@@ -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`}
|
||||
|
||||
+1
-1
@@ -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 ? (
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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 ? (
|
||||
|
||||
+17
-12
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+3
-6
@@ -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}
|
||||
|
||||
+1
-1
@@ -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
-10
@@ -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`} />
|
||||
</>
|
||||
);
|
||||
|
||||
+3
-8
@@ -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}
|
||||
|
||||
+3
-3
@@ -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}
|
||||
|
||||
+3
-11
@@ -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}
|
||||
|
||||
+3
-10
@@ -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>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
+8
-18
@@ -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} />
|
||||
</>
|
||||
);
|
||||
|
||||
+3
-9
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+53
-49
@@ -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}
|
||||
|
||||
+1
-1
@@ -66,7 +66,7 @@ export const Default = () => (
|
||||
>
|
||||
<ReactSpreadsheetImportContextProvider values={mockRsiValues}>
|
||||
<SpreadSheetImportModalWrapper
|
||||
modalId="match-columns-step"
|
||||
modalInstanceId="match-columns-step"
|
||||
onClose={() => null}
|
||||
>
|
||||
<MatchColumnsStep
|
||||
|
||||
+1
-1
@@ -43,7 +43,7 @@ export const Default = () => (
|
||||
>
|
||||
<ReactSpreadsheetImportContextProvider values={mockRsiValues}>
|
||||
<SpreadSheetImportModalWrapper
|
||||
modalId="select-header-step"
|
||||
modalInstanceId="select-header-step"
|
||||
onClose={() => null}
|
||||
>
|
||||
<SelectHeaderStep
|
||||
|
||||
+1
-1
@@ -43,7 +43,7 @@ export const Default = () => (
|
||||
>
|
||||
<ReactSpreadsheetImportContextProvider values={mockRsiValues}>
|
||||
<SpreadSheetImportModalWrapper
|
||||
modalId="select-sheet-step"
|
||||
modalInstanceId="select-sheet-step"
|
||||
onClose={() => null}
|
||||
>
|
||||
<SelectSheetStep
|
||||
|
||||
+4
-1
@@ -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}
|
||||
|
||||
+1
-1
@@ -46,7 +46,7 @@ export const Default = () => (
|
||||
>
|
||||
<ReactSpreadsheetImportContextProvider values={mockRsiValues}>
|
||||
<SpreadSheetImportModalWrapper
|
||||
modalId="validation-step"
|
||||
modalInstanceId="validation-step"
|
||||
onClose={() => null}
|
||||
>
|
||||
<ValidationStep
|
||||
|
||||
+5
-5
@@ -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,
|
||||
)}
|
||||
</>
|
||||
|
||||
+23
-20
@@ -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;
|
||||
+5
-5
@@ -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>
|
||||
);
|
||||
};
|
||||
+6
-6
@@ -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',
|
||||
|
||||
+20
-20
@@ -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 }
|
||||
);
|
||||
+1
-1
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+1
-1
@@ -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}
|
||||
|
||||
+1
-1
@@ -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}
|
||||
|
||||
+2
-2
@@ -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>
|
||||
|
||||
+1
-1
@@ -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`}
|
||||
|
||||
+2
-2
@@ -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}
|
||||
|
||||
@@ -15,6 +15,8 @@ import {
|
||||
import checker from 'vite-plugin-checker';
|
||||
import svgr from 'vite-plugin-svgr';
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
|
||||
import { createWywProfilingPlugin } from 'twenty-shared/vite';
|
||||
type Checkers = Parameters<typeof checker>[0];
|
||||
|
||||
export default defineConfig(({ command, mode }) => {
|
||||
@@ -107,9 +109,9 @@ export default defineConfig(({ command, mode }) => {
|
||||
configPath: path.resolve(__dirname, './lingui.config.ts'),
|
||||
}),
|
||||
checker(checkers),
|
||||
{
|
||||
...wyw({
|
||||
include: ['**/*.{ts,tsx}'],
|
||||
createWywProfilingPlugin(
|
||||
wyw({
|
||||
include: [path.resolve(__dirname, 'src') + '/**/*.{ts,tsx}'],
|
||||
exclude: [
|
||||
'**/generated-metadata/**',
|
||||
'**/testing/mock-data/generated/**',
|
||||
@@ -135,8 +137,7 @@ export default defineConfig(({ command, mode }) => {
|
||||
plugins: ['@babel/plugin-transform-export-namespace-from'],
|
||||
},
|
||||
}),
|
||||
enforce: 'pre',
|
||||
},
|
||||
),
|
||||
visualizer({
|
||||
open: true,
|
||||
gzipSize: true,
|
||||
|
||||
@@ -95,6 +95,11 @@
|
||||
"import": "./dist/utils.mjs",
|
||||
"require": "./dist/utils.cjs"
|
||||
},
|
||||
"./vite": {
|
||||
"types": "./dist/vite/index.d.ts",
|
||||
"import": "./dist/vite.mjs",
|
||||
"require": "./dist/vite.cjs"
|
||||
},
|
||||
"./workflow": {
|
||||
"types": "./dist/workflow/index.d.ts",
|
||||
"import": "./dist/workflow.mjs",
|
||||
@@ -118,6 +123,7 @@
|
||||
"translations",
|
||||
"types",
|
||||
"utils",
|
||||
"vite",
|
||||
"workflow",
|
||||
"workspace"
|
||||
],
|
||||
@@ -153,6 +159,9 @@
|
||||
"utils": [
|
||||
"dist/utils/index.d.ts"
|
||||
],
|
||||
"vite": [
|
||||
"dist/vite/index.d.ts"
|
||||
],
|
||||
"workflow": [
|
||||
"dist/workflow/index.d.ts"
|
||||
],
|
||||
|
||||
@@ -40,6 +40,8 @@
|
||||
"{projectRoot}/types/dist",
|
||||
"{projectRoot}/utils/package.json",
|
||||
"{projectRoot}/utils/dist",
|
||||
"{projectRoot}/vite/package.json",
|
||||
"{projectRoot}/vite/dist",
|
||||
"{projectRoot}/workflow/package.json",
|
||||
"{projectRoot}/workflow/dist",
|
||||
"{projectRoot}/workspace/package.json",
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
/* eslint-disable no-console */
|
||||
import { type Plugin } from 'vite';
|
||||
|
||||
const LINARIA_IMPORT_RE = /@linaria/;
|
||||
|
||||
type WywProfilingOptions = {
|
||||
slowThresholdMs?: number;
|
||||
topSlowFilesCount?: number;
|
||||
progressIntervalFiles?: number;
|
||||
};
|
||||
|
||||
export const createWywProfilingPlugin = (
|
||||
wywPlugin: Plugin,
|
||||
options?: WywProfilingOptions,
|
||||
): Plugin => {
|
||||
const slowThresholdMs = options?.slowThresholdMs ?? 50;
|
||||
const topSlowFilesCount = options?.topSlowFilesCount ?? 10;
|
||||
const progressIntervalFiles = options?.progressIntervalFiles ?? 50;
|
||||
|
||||
let totalMs = 0;
|
||||
let fileCount = 0;
|
||||
let skippedCount = 0;
|
||||
const slowFiles: { id: string; ms: number }[] = [];
|
||||
const originalTransform = wywPlugin.transform;
|
||||
|
||||
console.log(
|
||||
`[linaria/wyw] CSS pre-build profiling enabled (slow threshold: ${slowThresholdMs}ms)`,
|
||||
);
|
||||
|
||||
return {
|
||||
...wywPlugin,
|
||||
enforce: 'pre' as const,
|
||||
transform(code: string, id: string, ...rest: unknown[]) {
|
||||
if (!LINARIA_IMPORT_RE.test(code)) {
|
||||
skippedCount++;
|
||||
return null;
|
||||
}
|
||||
|
||||
const start = performance.now();
|
||||
const result = (originalTransform as Function).call(
|
||||
this,
|
||||
code,
|
||||
id,
|
||||
...rest,
|
||||
);
|
||||
|
||||
const handleTiming = (elapsed: number) => {
|
||||
totalMs += elapsed;
|
||||
fileCount++;
|
||||
|
||||
if (elapsed > slowThresholdMs) {
|
||||
slowFiles.push({ id, ms: elapsed });
|
||||
}
|
||||
|
||||
if (fileCount % progressIntervalFiles === 0) {
|
||||
console.log(
|
||||
`[linaria/wyw] CSS pre-build progress: ${fileCount} transformed, ${skippedCount} skipped, ${totalMs.toFixed(0)}ms total`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (result && typeof result === 'object' && 'then' in result) {
|
||||
return (result as Promise<unknown>).then((res) => {
|
||||
handleTiming(performance.now() - start);
|
||||
return res;
|
||||
});
|
||||
}
|
||||
|
||||
handleTiming(performance.now() - start);
|
||||
return result;
|
||||
},
|
||||
buildEnd() {
|
||||
console.log('\n[linaria/wyw] ===== CSS PRE-BUILD TIMING SUMMARY =====');
|
||||
console.log(`[linaria/wyw] Files transformed: ${fileCount}`);
|
||||
console.log(`[linaria/wyw] Files skipped (no @linaria): ${skippedCount}`);
|
||||
console.log(`[linaria/wyw] Transform time: ${totalMs.toFixed(0)}ms`);
|
||||
console.log(
|
||||
`[linaria/wyw] Avg per transformed file: ${fileCount > 0 ? (totalMs / fileCount).toFixed(1) : 0}ms`,
|
||||
);
|
||||
|
||||
if (slowFiles.length > 0) {
|
||||
console.log(
|
||||
`[linaria/wyw] Slow CSS pre-build files (>${slowThresholdMs}ms):`,
|
||||
);
|
||||
slowFiles
|
||||
.sort((a, b) => b.ms - a.ms)
|
||||
.slice(0, topSlowFilesCount)
|
||||
.forEach((slowFile) =>
|
||||
console.log(
|
||||
`[linaria/wyw] ${slowFile.ms.toFixed(0)}ms ${slowFile.id.replace(process.cwd(), '')}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
console.log('[linaria/wyw] ==========================================\n');
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
/*
|
||||
* _____ _
|
||||
*|_ _|_ _____ _ __ | |_ _ _
|
||||
* | | \ \ /\ / / _ \ '_ \| __| | | | Auto-generated file
|
||||
* | | \ V V / __/ | | | |_| |_| | Any edits to this will be overridden
|
||||
* |_| \_/\_/ \___|_| |_|\__|\__, |
|
||||
* |___/
|
||||
*/
|
||||
|
||||
export { createWywProfilingPlugin } from './createWywProfilingPlugin';
|
||||
@@ -14,6 +14,7 @@ export default [
|
||||
{
|
||||
ignores: [
|
||||
'**/node_modules/**',
|
||||
'**/generated/**',
|
||||
],
|
||||
},
|
||||
|
||||
|
||||
@@ -67,10 +67,7 @@ export const AvatarOrIcon = ({
|
||||
|
||||
if (isIconInverted || isDefined(IconBackgroundColor)) {
|
||||
return (
|
||||
<StyledAvatarOrIconWrapper
|
||||
isClickable={isClickable}
|
||||
onClick={onClick}
|
||||
>
|
||||
<StyledAvatarOrIconWrapper isClickable={isClickable} onClick={onClick}>
|
||||
<StyledIconWithBackgroundContainer
|
||||
backgroundColor={
|
||||
IconBackgroundColor ?? theme.background.invertedSecondary
|
||||
@@ -87,10 +84,7 @@ export const AvatarOrIcon = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledAvatarOrIconWrapper
|
||||
isClickable={isClickable}
|
||||
onClick={onClick}
|
||||
>
|
||||
<StyledAvatarOrIconWrapper isClickable={isClickable} onClick={onClick}>
|
||||
<Icon
|
||||
size={theme.icon.size.sm}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
|
||||
@@ -140,8 +140,8 @@ const StyledContainer = styled.div<
|
||||
`;
|
||||
|
||||
const StyledRightComponentDivider = styled.div`
|
||||
border-left: 1px solid ${themeCssVariables.border.color.light};
|
||||
align-self: stretch;
|
||||
border-left: 1px solid ${themeCssVariables.border.color.light};
|
||||
`;
|
||||
|
||||
const renderRightComponent = (
|
||||
@@ -155,7 +155,7 @@ const renderRightComponent = (
|
||||
const rendered =
|
||||
typeof rightComponent === 'function' ? rightComponent() : rightComponent;
|
||||
|
||||
if (rightComponentDivider) {
|
||||
if (rightComponentDivider === true) {
|
||||
return (
|
||||
<>
|
||||
<StyledRightComponentDivider />
|
||||
|
||||
@@ -13,4 +13,3 @@ export * from './layout';
|
||||
export * from './navigation';
|
||||
export * from './theme';
|
||||
export * from './utilities';
|
||||
|
||||
|
||||
@@ -41,10 +41,12 @@ const StyledJsonListBase = styled.ul<{
|
||||
padding: 0;
|
||||
display: grid;
|
||||
row-gap: ${themeCssVariables.spacing[2]};
|
||||
padding-left: ${({ depth }) => (depth > 0 ? themeCssVariables.spacing[8] : '0')};
|
||||
padding-left: ${({ depth }) =>
|
||||
depth > 0 ? themeCssVariables.spacing[8] : '0'};
|
||||
|
||||
> :first-of-type {
|
||||
margin-top: ${({ depth }) => (depth > 0 ? themeCssVariables.spacing[2] : '0')};
|
||||
margin-top: ${({ depth }) =>
|
||||
depth > 0 ? themeCssVariables.spacing[2] : '0'};
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -39,6 +39,18 @@ export { Card } from './card/components/Card';
|
||||
export { CardContent } from './card/components/CardContent';
|
||||
export { CardFooter } from './card/components/CardFooter';
|
||||
export { CardHeader } from './card/components/CardHeader';
|
||||
export { Modal } from './modal/components/Modal';
|
||||
export { ModalBackdrop } from './modal/components/ModalBackdrop';
|
||||
export type { ModalContentProps } from './modal/components/ModalContent';
|
||||
export { ModalContent } from './modal/components/ModalContent';
|
||||
export type { ModalFooterProps } from './modal/components/ModalFooter';
|
||||
export { ModalFooter } from './modal/components/ModalFooter';
|
||||
export type { ModalHeaderProps } from './modal/components/ModalHeader';
|
||||
export { ModalHeader } from './modal/components/ModalHeader';
|
||||
export type { ModalOverlay } from './modal/types/ModalOverlay';
|
||||
export type { ModalPadding } from './modal/types/ModalPadding';
|
||||
export type { ModalProps } from './modal/types/ModalProps';
|
||||
export type { ModalSize } from './modal/types/ModalSize';
|
||||
export {
|
||||
SectionAlignment,
|
||||
SectionFontColor,
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
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 '@ui/theme-constants';
|
||||
import { ThemeContext } from '@ui/theme';
|
||||
|
||||
import { type ModalOverlay } from '../types/ModalOverlay';
|
||||
import { type ModalPadding } from '../types/ModalPadding';
|
||||
import { type ModalProps } from '../types/ModalProps';
|
||||
import { type ModalSize } from '../types/ModalSize';
|
||||
import { ModalBackdrop } from './ModalBackdrop';
|
||||
|
||||
const DEFAULT_MODAL_Z_INDEX = 40;
|
||||
const DEFAULT_BACKDROP_Z_INDEX = 39;
|
||||
|
||||
const StyledModalDiv = styled.div<{
|
||||
size?: ModalSize;
|
||||
padding?: ModalPadding;
|
||||
isMobile: boolean;
|
||||
overlay: ModalOverlay;
|
||||
gap?: number;
|
||||
smallBorderRadius?: boolean;
|
||||
narrowWidth?: boolean;
|
||||
autoHeight?: boolean;
|
||||
modalZIndex: number;
|
||||
}>`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: ${({ overlay }) =>
|
||||
overlay === 'dark'
|
||||
? themeCssVariables.boxShadow.superHeavy
|
||||
: overlay === 'transparent'
|
||||
? 'none'
|
||||
: themeCssVariables.boxShadow.strong};
|
||||
background: ${({ overlay }) =>
|
||||
overlay === 'transparent'
|
||||
? 'transparent'
|
||||
: themeCssVariables.background.primary};
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
border-radius: ${({ isMobile, overlay, smallBorderRadius }) => {
|
||||
if (isMobile === true || overlay === 'transparent') return '0';
|
||||
if (smallBorderRadius === true) return themeCssVariables.spacing[1];
|
||||
return themeCssVariables.border.radius.md;
|
||||
}};
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
z-index: ${({ modalZIndex }) => modalZIndex};
|
||||
|
||||
gap: ${({ gap }) =>
|
||||
gap !== undefined ? `var(--t-spacing-${gap})` : 'unset'};
|
||||
|
||||
width: ${({ isMobile, size, narrowWidth }) => {
|
||||
if (narrowWidth === true)
|
||||
return `calc(400px - ${themeCssVariables.spacing[32]})`;
|
||||
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, autoHeight }) => {
|
||||
if (autoHeight === true) return 'auto';
|
||||
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 AnimatedModalDiv = motion.create(StyledModalDiv);
|
||||
const AnimatedBackdrop = motion.create(ModalBackdrop);
|
||||
|
||||
const modalAnimation = {
|
||||
hidden: { opacity: 0 },
|
||||
visible: { opacity: 1 },
|
||||
exit: { opacity: 0 },
|
||||
};
|
||||
|
||||
export const Modal = ({
|
||||
isOpen,
|
||||
children,
|
||||
size = 'medium',
|
||||
padding = 'medium',
|
||||
overlay = 'dark',
|
||||
isMobile = false,
|
||||
isInContainer = false,
|
||||
container,
|
||||
gap,
|
||||
smallBorderRadius,
|
||||
narrowWidth,
|
||||
autoHeight,
|
||||
modalZIndex = DEFAULT_MODAL_Z_INDEX,
|
||||
backdropZIndex = DEFAULT_BACKDROP_Z_INDEX,
|
||||
backdropTestId = 'modal-backdrop',
|
||||
backdropClickOutsideId,
|
||||
preventClickOutside,
|
||||
onBackdropMouseDown,
|
||||
modalRef: externalRef,
|
||||
}: ModalProps) => {
|
||||
const internalRef = useRef<HTMLDivElement>(null);
|
||||
const resolvedRef = externalRef ?? internalRef;
|
||||
const { theme } = useContext(ThemeContext);
|
||||
|
||||
const handleBackdropMouseDown = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
onBackdropMouseDown?.(e);
|
||||
};
|
||||
|
||||
const content = (
|
||||
<AnimatePresence mode="wait">
|
||||
{isOpen && (
|
||||
<AnimatedBackdrop
|
||||
data-testid={backdropTestId}
|
||||
data-click-outside-id={backdropClickOutsideId}
|
||||
onMouseDown={handleBackdropMouseDown}
|
||||
overlay={overlay}
|
||||
backdropZIndex={backdropZIndex}
|
||||
isInContainer={isInContainer}
|
||||
>
|
||||
<AnimatedModalDiv
|
||||
ref={resolvedRef}
|
||||
size={size}
|
||||
padding={padding}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
exit="exit"
|
||||
layout
|
||||
overlay={overlay}
|
||||
variants={modalAnimation}
|
||||
transition={{ duration: theme.animation.duration.normal }}
|
||||
isMobile={isMobile}
|
||||
gap={gap}
|
||||
smallBorderRadius={smallBorderRadius}
|
||||
narrowWidth={narrowWidth}
|
||||
autoHeight={autoHeight}
|
||||
modalZIndex={modalZIndex}
|
||||
data-globally-prevent-click-outside={preventClickOutside}
|
||||
>
|
||||
{children}
|
||||
</AnimatedModalDiv>
|
||||
</AnimatedBackdrop>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
|
||||
if (isDefined(container)) {
|
||||
return createPortal(content, container);
|
||||
}
|
||||
|
||||
return content;
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { themeCssVariables } from '@ui/theme-constants';
|
||||
|
||||
import { type ModalOverlay } from '../types/ModalOverlay';
|
||||
|
||||
const StyledModalBackdrop = styled.div<{
|
||||
overlay: ModalOverlay;
|
||||
backdropZIndex: number;
|
||||
isInContainer?: boolean;
|
||||
}>`
|
||||
align-items: center;
|
||||
background: ${({ overlay, isInContainer }) =>
|
||||
isInContainer || overlay === 'light'
|
||||
? themeCssVariables.background.overlayTertiary
|
||||
: themeCssVariables.background.overlayPrimary};
|
||||
display: flex;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
left: 0;
|
||||
pointer-events: auto;
|
||||
position: ${({ isInContainer }) => (isInContainer ? 'absolute' : 'fixed')};
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: ${({ backdropZIndex }) => backdropZIndex};
|
||||
user-select: none;
|
||||
`;
|
||||
|
||||
export const ModalBackdrop = StyledModalBackdrop;
|
||||
@@ -0,0 +1,59 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import React from 'react';
|
||||
import { themeCssVariables } from '@ui/theme-constants';
|
||||
|
||||
const StyledContent = styled.div<{
|
||||
isVerticallyCentered?: boolean;
|
||||
isHorizontallyCentered?: boolean;
|
||||
noPadding?: boolean;
|
||||
overflowHidden?: boolean;
|
||||
gap?: number;
|
||||
contentPadding?: number;
|
||||
}>`
|
||||
align-items: ${({ isVerticallyCentered }) =>
|
||||
isVerticallyCentered ? 'center' : 'stretch'};
|
||||
display: flex;
|
||||
flex: 1 1 0%;
|
||||
flex-direction: column;
|
||||
gap: ${({ gap }) =>
|
||||
gap !== undefined ? `var(--t-spacing-${gap})` : 'unset'};
|
||||
justify-content: ${({ isHorizontallyCentered }) =>
|
||||
isHorizontallyCentered ? 'center' : 'flex-start'};
|
||||
overflow: ${({ overflowHidden }) => (overflowHidden ? 'hidden' : 'visible')};
|
||||
padding: ${({ noPadding, contentPadding }) => {
|
||||
if (noPadding === true) return '0';
|
||||
if (contentPadding !== undefined)
|
||||
return `var(--t-spacing-${contentPadding})`;
|
||||
return themeCssVariables.spacing[10];
|
||||
}};
|
||||
`;
|
||||
|
||||
export type ModalContentProps = React.PropsWithChildren & {
|
||||
isVerticallyCentered?: boolean;
|
||||
isHorizontallyCentered?: boolean;
|
||||
noPadding?: boolean;
|
||||
overflowHidden?: boolean;
|
||||
gap?: number;
|
||||
contentPadding?: number;
|
||||
};
|
||||
|
||||
export const ModalContent = ({
|
||||
children,
|
||||
isVerticallyCentered,
|
||||
isHorizontallyCentered,
|
||||
noPadding,
|
||||
overflowHidden,
|
||||
gap,
|
||||
contentPadding,
|
||||
}: ModalContentProps) => (
|
||||
<StyledContent
|
||||
isVerticallyCentered={isVerticallyCentered}
|
||||
isHorizontallyCentered={isHorizontallyCentered}
|
||||
noPadding={noPadding}
|
||||
overflowHidden={overflowHidden}
|
||||
gap={gap}
|
||||
contentPadding={contentPadding}
|
||||
>
|
||||
{children}
|
||||
</StyledContent>
|
||||
);
|
||||
@@ -0,0 +1,43 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import React from 'react';
|
||||
import { themeCssVariables } from '@ui/theme-constants';
|
||||
|
||||
const StyledFooter = styled.div<{
|
||||
autoHeight?: boolean;
|
||||
centered?: boolean;
|
||||
smallPadding?: boolean;
|
||||
}>`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
height: ${({ autoHeight }) => (autoHeight ? 'auto' : '60px')};
|
||||
justify-content: ${({ centered }) => (centered ? 'center' : 'flex-end')};
|
||||
overflow: hidden;
|
||||
padding: ${({ smallPadding }) =>
|
||||
smallPadding ? themeCssVariables.spacing[3] : themeCssVariables.spacing[5]};
|
||||
`;
|
||||
|
||||
export type ModalFooterProps = React.PropsWithChildren & {
|
||||
autoHeight?: boolean;
|
||||
centered?: boolean;
|
||||
smallPadding?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const ModalFooter = ({
|
||||
children,
|
||||
autoHeight,
|
||||
centered,
|
||||
smallPadding,
|
||||
className,
|
||||
}: ModalFooterProps) => (
|
||||
<StyledFooter
|
||||
autoHeight={autoHeight}
|
||||
centered={centered}
|
||||
smallPadding={smallPadding}
|
||||
className={className}
|
||||
>
|
||||
{children}
|
||||
</StyledFooter>
|
||||
);
|
||||
@@ -0,0 +1,65 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import React from 'react';
|
||||
import { MOBILE_VIEWPORT, themeCssVariables } from '@ui/theme-constants';
|
||||
|
||||
const StyledHeader = styled.div<{
|
||||
noPadding?: boolean;
|
||||
autoHeight?: boolean;
|
||||
hasBorderBottom?: boolean;
|
||||
paddingHorizontal?: number;
|
||||
backgroundColor?: string;
|
||||
}>`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-shrink: 0;
|
||||
height: ${({ autoHeight }) => (autoHeight ? 'auto' : '60px')};
|
||||
overflow: hidden;
|
||||
padding: ${({ noPadding, paddingHorizontal }) => {
|
||||
if (paddingHorizontal !== undefined)
|
||||
return `0 var(--t-spacing-${paddingHorizontal})`;
|
||||
if (noPadding === true) return '0';
|
||||
return themeCssVariables.spacing[5];
|
||||
}};
|
||||
background-color: ${({ backgroundColor }) => backgroundColor ?? 'unset'};
|
||||
border-bottom: ${({ hasBorderBottom }) =>
|
||||
hasBorderBottom
|
||||
? `1px solid ${themeCssVariables.border.color.medium}`
|
||||
: 'none'};
|
||||
@media (max-width: ${MOBILE_VIEWPORT}px) {
|
||||
${({ paddingHorizontal }) =>
|
||||
paddingHorizontal !== undefined
|
||||
? `padding-left: ${themeCssVariables.spacing[4]}; padding-right: ${themeCssVariables.spacing[4]};`
|
||||
: ''}
|
||||
}
|
||||
`;
|
||||
|
||||
export type ModalHeaderProps = React.PropsWithChildren & {
|
||||
noPadding?: boolean;
|
||||
autoHeight?: boolean;
|
||||
hasBorderBottom?: boolean;
|
||||
paddingHorizontal?: number;
|
||||
backgroundColor?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const ModalHeader = ({
|
||||
children,
|
||||
noPadding,
|
||||
autoHeight,
|
||||
hasBorderBottom,
|
||||
paddingHorizontal,
|
||||
backgroundColor,
|
||||
className,
|
||||
}: ModalHeaderProps) => (
|
||||
<StyledHeader
|
||||
noPadding={noPadding}
|
||||
autoHeight={autoHeight}
|
||||
hasBorderBottom={hasBorderBottom}
|
||||
paddingHorizontal={paddingHorizontal}
|
||||
backgroundColor={backgroundColor}
|
||||
className={className}
|
||||
>
|
||||
{children}
|
||||
</StyledHeader>
|
||||
);
|
||||
@@ -0,0 +1,244 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
||||
import { useState } from 'react';
|
||||
import { H1Title, H1TitleFontColor, H2Title, IconX } from '@ui/display';
|
||||
import { Button, IconButton } from '@ui/input';
|
||||
import { Section, SectionAlignment, SectionFontColor } from '@ui/layout';
|
||||
import { ComponentDecorator } from '@ui/testing';
|
||||
import { themeCssVariables } from '@ui/theme-constants';
|
||||
|
||||
import { Modal } from '../Modal';
|
||||
import { ModalContent } from '../ModalContent';
|
||||
import { ModalFooter } from '../ModalFooter';
|
||||
import { ModalHeader } from '../ModalHeader';
|
||||
|
||||
const StyledCenteredTitle = styled.div`
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
const StyledSection = styled(Section)`
|
||||
margin-bottom: ${themeCssVariables.spacing[6]};
|
||||
`;
|
||||
|
||||
const meta: Meta<typeof Modal> = {
|
||||
title: 'UI/Layout/Modal/Modal',
|
||||
component: Modal,
|
||||
decorators: [ComponentDecorator],
|
||||
argTypes: {
|
||||
size: {
|
||||
control: 'select',
|
||||
options: ['small', 'medium', 'large', 'extraLarge'],
|
||||
},
|
||||
padding: {
|
||||
control: 'select',
|
||||
options: ['none', 'small', 'medium', 'large'],
|
||||
},
|
||||
overlay: {
|
||||
control: 'select',
|
||||
options: ['light', 'dark', 'transparent'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Modal>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
isOpen: true,
|
||||
size: 'medium',
|
||||
padding: 'none',
|
||||
overlay: 'dark',
|
||||
},
|
||||
render: ({ isOpen, size, padding, overlay }) => (
|
||||
<Modal isOpen={isOpen} size={size} padding={padding} overlay={overlay}>
|
||||
<ModalHeader>
|
||||
<H2Title
|
||||
title="Edit workspace"
|
||||
description="Update your workspace settings"
|
||||
/>
|
||||
</ModalHeader>
|
||||
<ModalContent>
|
||||
<Section>
|
||||
Workspace name and subdomain can be changed from the settings panel.
|
||||
These changes will be reflected across all members.
|
||||
</Section>
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<Button title="Cancel" variant="secondary" />
|
||||
<Button title="Save" variant="primary" accent="blue" />
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
),
|
||||
};
|
||||
|
||||
export const Confirmation: Story = {
|
||||
args: {
|
||||
isOpen: true,
|
||||
padding: 'large',
|
||||
overlay: 'dark',
|
||||
smallBorderRadius: true,
|
||||
narrowWidth: true,
|
||||
autoHeight: true,
|
||||
gap: 2,
|
||||
},
|
||||
render: ({
|
||||
isOpen,
|
||||
padding,
|
||||
overlay,
|
||||
smallBorderRadius,
|
||||
narrowWidth,
|
||||
autoHeight,
|
||||
gap,
|
||||
}) => (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
padding={padding}
|
||||
overlay={overlay}
|
||||
smallBorderRadius={smallBorderRadius}
|
||||
narrowWidth={narrowWidth}
|
||||
autoHeight={autoHeight}
|
||||
gap={gap}
|
||||
>
|
||||
<StyledCenteredTitle>
|
||||
<H1Title title="Delete record?" fontColor={H1TitleFontColor.Primary} />
|
||||
</StyledCenteredTitle>
|
||||
<StyledSection
|
||||
alignment={SectionAlignment.Center}
|
||||
fontColor={SectionFontColor.Primary}
|
||||
>
|
||||
This action cannot be undone. The record and all of its data will be
|
||||
permanently removed.
|
||||
</StyledSection>
|
||||
<Button title="Cancel" variant="secondary" fullWidth justify="center" />
|
||||
<Button
|
||||
title="Delete"
|
||||
variant="secondary"
|
||||
accent="danger"
|
||||
fullWidth
|
||||
justify="center"
|
||||
/>
|
||||
</Modal>
|
||||
),
|
||||
};
|
||||
|
||||
export const Small: Story = {
|
||||
args: {
|
||||
isOpen: true,
|
||||
size: 'small',
|
||||
padding: 'none',
|
||||
overlay: 'dark',
|
||||
},
|
||||
render: ({ isOpen, size, padding, overlay }) => (
|
||||
<Modal isOpen={isOpen} size={size} padding={padding} overlay={overlay}>
|
||||
<ModalHeader>
|
||||
<H2Title title="Archive item" />
|
||||
</ModalHeader>
|
||||
<ModalContent>
|
||||
<Section>Are you sure you want to archive this item?</Section>
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<Button title="No" variant="secondary" />
|
||||
<Button title="Yes, archive" variant="primary" accent="blue" />
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
),
|
||||
};
|
||||
|
||||
export const ExtraLarge: Story = {
|
||||
args: {
|
||||
isOpen: true,
|
||||
size: 'extraLarge',
|
||||
padding: 'none',
|
||||
overlay: 'dark',
|
||||
},
|
||||
render: ({ isOpen, size, padding, overlay }) => (
|
||||
<Modal isOpen={isOpen} size={size} padding={padding} overlay={overlay}>
|
||||
<ModalHeader>
|
||||
<H2Title
|
||||
title="Import contacts"
|
||||
description="Upload a CSV file to import your contacts"
|
||||
/>
|
||||
</ModalHeader>
|
||||
<ModalContent>
|
||||
<Section>
|
||||
The file should include columns for name, email, phone, and company.
|
||||
Drag and drop your CSV file here, or click to browse.
|
||||
</Section>
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<Button title="Cancel" variant="secondary" />
|
||||
<Button title="Upload & import" variant="primary" accent="blue" />
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
),
|
||||
};
|
||||
|
||||
export const Closed: Story = {
|
||||
args: {
|
||||
isOpen: false,
|
||||
size: 'medium',
|
||||
padding: 'medium',
|
||||
overlay: 'dark',
|
||||
},
|
||||
render: ({ isOpen, size, padding, overlay }) => (
|
||||
<Modal isOpen={isOpen} size={size} padding={padding} overlay={overlay}>
|
||||
<ModalContent>This should not be visible.</ModalContent>
|
||||
</Modal>
|
||||
),
|
||||
};
|
||||
|
||||
const InteractiveModal = () => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
title="Open Modal"
|
||||
variant="primary"
|
||||
accent="blue"
|
||||
onClick={() => setIsOpen(true)}
|
||||
/>
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size="medium"
|
||||
padding="none"
|
||||
overlay="dark"
|
||||
onBackdropMouseDown={() => setIsOpen(false)}
|
||||
>
|
||||
<ModalHeader>
|
||||
<H2Title title="Create record" />
|
||||
<IconButton
|
||||
Icon={IconX}
|
||||
variant="tertiary"
|
||||
size="small"
|
||||
onClick={() => setIsOpen(false)}
|
||||
/>
|
||||
</ModalHeader>
|
||||
<ModalContent>
|
||||
<Section>
|
||||
Fill in the details below to create a new record. All fields are
|
||||
optional.
|
||||
</Section>
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<Button
|
||||
title="Cancel"
|
||||
variant="secondary"
|
||||
onClick={() => setIsOpen(false)}
|
||||
/>
|
||||
<Button
|
||||
title="Create"
|
||||
variant="primary"
|
||||
accent="blue"
|
||||
onClick={() => setIsOpen(false)}
|
||||
/>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const Interactive: Story = {
|
||||
render: () => <InteractiveModal />,
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
export { Modal } from './components/Modal';
|
||||
export { ModalBackdrop } from './components/ModalBackdrop';
|
||||
export { ModalContent } from './components/ModalContent';
|
||||
export type { ModalContentProps } from './components/ModalContent';
|
||||
export { ModalFooter } from './components/ModalFooter';
|
||||
export type { ModalFooterProps } from './components/ModalFooter';
|
||||
export { ModalHeader } from './components/ModalHeader';
|
||||
export type { ModalHeaderProps } from './components/ModalHeader';
|
||||
export type { ModalOverlay } from './types/ModalOverlay';
|
||||
export type { ModalPadding } from './types/ModalPadding';
|
||||
export type { ModalProps } from './types/ModalProps';
|
||||
export type { ModalSize } from './types/ModalSize';
|
||||
@@ -0,0 +1 @@
|
||||
export type ModalOverlay = 'light' | 'dark' | 'transparent';
|
||||
@@ -0,0 +1 @@
|
||||
export type ModalPadding = 'none' | 'small' | 'medium' | 'large';
|
||||
@@ -0,0 +1,26 @@
|
||||
import type React from 'react';
|
||||
|
||||
import { type ModalOverlay } from './ModalOverlay';
|
||||
import { type ModalPadding } from './ModalPadding';
|
||||
import { type ModalSize } from './ModalSize';
|
||||
|
||||
export type ModalProps = React.PropsWithChildren & {
|
||||
isOpen: boolean;
|
||||
size?: ModalSize;
|
||||
padding?: ModalPadding;
|
||||
overlay?: ModalOverlay;
|
||||
isMobile?: boolean;
|
||||
isInContainer?: boolean;
|
||||
container?: HTMLElement | null;
|
||||
gap?: number;
|
||||
smallBorderRadius?: boolean;
|
||||
narrowWidth?: boolean;
|
||||
autoHeight?: boolean;
|
||||
modalZIndex?: number;
|
||||
backdropZIndex?: number;
|
||||
backdropTestId?: string;
|
||||
backdropClickOutsideId?: string;
|
||||
preventClickOutside?: boolean;
|
||||
onBackdropMouseDown?: (e: React.MouseEvent) => void;
|
||||
modalRef?: React.RefObject<HTMLDivElement>;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export type ModalSize = 'small' | 'medium' | 'large' | 'extraLarge';
|
||||
File diff suppressed because it is too large
Load Diff
+1024
-991
File diff suppressed because one or more lines are too long
+1018
-991
File diff suppressed because one or more lines are too long
@@ -1,6 +1,7 @@
|
||||
import react from '@vitejs/plugin-react-swc';
|
||||
import wyw from '@wyw-in-js/vite';
|
||||
import * as path from 'path';
|
||||
import { createWywProfilingPlugin } from 'twenty-shared/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
import checker from 'vite-plugin-checker';
|
||||
import dts, { type PluginOptions } from 'vite-plugin-dts';
|
||||
@@ -81,15 +82,14 @@ export default defineConfig(({ command }) => {
|
||||
svgr(),
|
||||
dts(dtsConfig),
|
||||
checker(checkersConfig),
|
||||
{
|
||||
...wyw({
|
||||
createWywProfilingPlugin(
|
||||
wyw({
|
||||
include: [path.resolve(__dirname, 'src') + '/**/*.{ts,tsx}'],
|
||||
babelOptions: {
|
||||
presets: ['@babel/preset-typescript', '@babel/preset-react'],
|
||||
},
|
||||
}),
|
||||
enforce: 'pre',
|
||||
},
|
||||
),
|
||||
],
|
||||
build: {
|
||||
cssCodeSplit: false,
|
||||
|
||||
Reference in New Issue
Block a user