feat: replace modal with new design (#4324)

Co-authored-by: Peng Xiao <pengxiao@outlook.com>
This commit is contained in:
Qi
2023-09-13 16:05:19 +08:00
committed by GitHub
parent 49e0172316
commit 0b1ba6bf43
58 changed files with 637 additions and 1404 deletions

View File

@@ -39,7 +39,7 @@
"@mui/material": "^5.14.7", "@mui/material": "^5.14.7",
"@radix-ui/react-select": "^1.2.2", "@radix-ui/react-select": "^1.2.2",
"@react-hookz/web": "^23.1.0", "@react-hookz/web": "^23.1.0",
"@toeverything/components": "^0.0.34", "@toeverything/components": "^0.0.38",
"async-call-rpc": "^6.3.1", "async-call-rpc": "^6.3.1",
"cmdk": "^0.2.0", "cmdk": "^0.2.0",
"css-spring": "^4.1.0", "css-spring": "^4.1.0",

View File

@@ -1,14 +1,9 @@
import { import { Input, toast } from '@affine/component';
Input,
Modal,
ModalCloseButton,
ModalWrapper,
toast,
} from '@affine/component';
import { DebugLogger } from '@affine/debug'; import { DebugLogger } from '@affine/debug';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { HelpIcon } from '@blocksuite/icons'; import { HelpIcon } from '@blocksuite/icons';
import { Button } from '@toeverything/components/button'; import { Button } from '@toeverything/components/button';
import { Modal } from '@toeverything/components/modal';
import { Tooltip } from '@toeverything/components/tooltip'; import { Tooltip } from '@toeverything/components/tooltip';
import type { import type {
LoadDBFileResult, LoadDBFileResult,
@@ -386,16 +381,28 @@ export const CreateWorkspaceModal = ({
/> />
) : null; ) : null;
const onOpenChange = useCallback(
(open: boolean) => {
if (!open) {
onClose();
}
},
[onClose]
);
return ( return (
<Modal open={mode !== false && !!step} onClose={onClose}> <Modal
<ModalWrapper width={560} style={{ padding: '10px' }}> open={mode !== false && !!step}
<div className={style.header}> width={560}
<ModalCloseButton top={6} right={6} onClick={onClose} /> onOpenChange={onOpenChange}
</div> contentOptions={{
{nameWorkspaceNode} style: { padding: '10px' },
{setDBLocationNode} }}
{setSyncingModeNode} >
</ModalWrapper> <div className={style.header}></div>
{nameWorkspaceNode}
{setDBLocationNode}
{setSyncingModeNode}
</Modal> </Modal>
); );
}; };

View File

@@ -1,33 +1,26 @@
import { Modal, ModalWrapper } from '@affine/component';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { CloseIcon } from '@blocksuite/icons'; import {
import { Button, IconButton } from '@toeverything/components/button'; ConfirmModal,
type ConfirmModalProps,
} from '@toeverything/components/modal';
import { useSetAtom } from 'jotai'; import { useSetAtom } from 'jotai';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { authAtom } from '../../../atoms'; import { authAtom } from '../../../atoms';
import { setOnceSignedInEventAtom } from '../../../atoms/event'; import { setOnceSignedInEventAtom } from '../../../atoms/event';
import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-status'; import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-status';
import { ButtonContainer, Content, Header, StyleTips, Title } from './style';
interface EnableAffineCloudModalProps {
open: boolean;
onConfirm: () => void;
onClose: () => void;
}
export const EnableAffineCloudModal = ({ export const EnableAffineCloudModal = ({
onConfirm: propsOnConfirm, onConfirm: propsOnConfirm,
open, ...props
onClose, }: ConfirmModalProps) => {
}: EnableAffineCloudModalProps) => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const loginStatus = useCurrentLoginStatus(); const loginStatus = useCurrentLoginStatus();
const setAuthAtom = useSetAtom(authAtom); const setAuthAtom = useSetAtom(authAtom);
const setOnceSignedInEvent = useSetAtom(setOnceSignedInEventAtom); const setOnceSignedInEvent = useSetAtom(setOnceSignedInEventAtom);
const confirm = useCallback(async () => { const confirm = useCallback(async () => {
return propsOnConfirm(); return propsOnConfirm?.();
}, [propsOnConfirm]); }, [propsOnConfirm]);
const onConfirm = useCallback(() => { const onConfirm = useCallback(() => {
@@ -39,42 +32,29 @@ export const EnableAffineCloudModal = ({
setOnceSignedInEvent(confirm); setOnceSignedInEvent(confirm);
} }
if (loginStatus === 'authenticated') { if (loginStatus === 'authenticated') {
return propsOnConfirm(); return propsOnConfirm?.();
} }
}, [confirm, loginStatus, propsOnConfirm, setAuthAtom, setOnceSignedInEvent]); }, [confirm, loginStatus, propsOnConfirm, setAuthAtom, setOnceSignedInEvent]);
return ( return (
<Modal open={open} onClose={onClose} data-testid="logout-modal"> <ConfirmModal
<ModalWrapper width={480}> width={480}
<Header> title={t['Enable AFFiNE Cloud']()}
<Title>{t['Enable AFFiNE Cloud']()}</Title> description={t['Enable AFFiNE Cloud Description']()}
<IconButton onClick={onClose}> cancelText={t['com.affine.enableAffineCloudModal.button.cancel']()}
<CloseIcon /> onConfirm={onConfirm}
</IconButton> confirmButtonOptions={{
</Header> type: 'primary',
<Content> ['data-testid' as string]: 'confirm-enable-affine-cloud-button',
<StyleTips>{t['Enable AFFiNE Cloud Description']()}</StyleTips> children:
<ButtonContainer> loginStatus === 'authenticated'
<div> ? t['Enable']()
<Button onClick={onClose} block> : t['Sign in and Enable'](),
{t['com.affine.enableAffineCloudModal.button.cancel']()} }}
</Button> contentOptions={{
</div> ['data-testid' as string]: 'enable-cloud-modal',
<div> }}
<Button {...props}
data-testid="confirm-enable-affine-cloud-button" />
type="primary"
block
onClick={onConfirm}
>
{loginStatus === 'authenticated'
? t['Enable']()
: t['Sign in and Enable']()}
</Button>
</div>
</ButtonContainer>
</Content>
</ModalWrapper>
</Modal>
); );
}; };

View File

@@ -1,35 +0,0 @@
import { styled } from '@affine/component';
export const Header = styled('div')({
display: 'flex',
justifyContent: 'space-between',
paddingRight: '20px',
paddingTop: '20px',
paddingLeft: '24px',
alignItems: 'center',
});
export const Content = styled('div')({
padding: '12px 24px 20px 24px',
});
export const Title = styled('div')({
fontSize: 'var(--affine-font-h6)',
lineHeight: '26px',
fontWeight: 600,
});
export const StyleTips = styled('div')(() => {
return {
userSelect: 'none',
marginBottom: '20px',
};
});
export const ButtonContainer = styled('div')(() => {
return {
display: 'flex',
justifyContent: 'flex-end',
gap: '20px',
paddingTop: '20px',
};
});

View File

@@ -1,33 +1,24 @@
import { Input, Modal, ModalCloseButton } from '@affine/component'; import { Input } from '@affine/component';
import type { AffineOfficialWorkspace } from '@affine/env/workspace'; import type { AffineOfficialWorkspace } from '@affine/env/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace';
import { Trans } from '@affine/i18n'; import { Trans } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { Button } from '@toeverything/components/button'; import {
ConfirmModal,
type ConfirmModalProps,
} from '@toeverything/components/modal';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name'; import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
import { useState } from 'react'; import { useState } from 'react';
import { import { StyledInputContent, StyledWorkspaceName } from './style';
StyledButtonContent,
StyledInputContent,
StyledModalHeader,
StyledModalWrapper,
StyledTextContent,
StyledWorkspaceName,
} from './style';
interface WorkspaceDeleteProps { interface WorkspaceDeleteProps extends ConfirmModalProps {
open: boolean;
onClose: () => void;
workspace: AffineOfficialWorkspace; workspace: AffineOfficialWorkspace;
onConfirm: () => void;
} }
export const WorkspaceDeleteModal = ({ export const WorkspaceDeleteModal = ({
open,
onClose,
onConfirm,
workspace, workspace,
...props
}: WorkspaceDeleteProps) => { }: WorkspaceDeleteProps) => {
const [workspaceName] = useBlockSuiteWorkspaceName( const [workspaceName] = useBlockSuiteWorkspaceName(
workspace.blockSuiteWorkspace workspace.blockSuiteWorkspace
@@ -37,65 +28,50 @@ export const WorkspaceDeleteModal = ({
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
return ( return (
<Modal open={open} onClose={onClose}> <ConfirmModal
<StyledModalWrapper> title={`${t['com.affine.workspaceDelete.title']()}?`}
<ModalCloseButton onClick={onClose} /> cancelText={t['com.affine.workspaceDelete.button.cancel']()}
<StyledModalHeader> confirmButtonOptions={{
{t['com.affine.workspaceDelete.title']()}? type: 'error',
</StyledModalHeader> disabled: !allowDelete,
{workspace.flavour === WorkspaceFlavour.LOCAL ? ( ['data-testid' as string]: 'delete-workspace-confirm-button',
<StyledTextContent> children: t['com.affine.workspaceDelete.button.delete'](),
<Trans i18nKey="com.affine.workspaceDelete.description"> }}
Deleting ( {...props}
<StyledWorkspaceName> >
{{ workspace: workspaceName } as any} {workspace.flavour === WorkspaceFlavour.LOCAL ? (
</StyledWorkspaceName> <Trans i18nKey="com.affine.workspaceDelete.description">
) cannot be undone, please proceed with caution. All contents will Deleting (
be lost. <StyledWorkspaceName>
</Trans> {{ workspace: workspaceName } as any}
</StyledTextContent> </StyledWorkspaceName>
) : ( ) cannot be undone, please proceed with caution. All contents will be
<StyledTextContent> lost.
<Trans i18nKey="com.affine.workspaceDelete.description2"> </Trans>
Deleting ( ) : (
<StyledWorkspaceName> <Trans i18nKey="com.affine.workspaceDelete.description2">
{{ workspace: workspaceName } as any} Deleting (
</StyledWorkspaceName> <StyledWorkspaceName>
) will delete both local and cloud data, this operation cannot be {{ workspace: workspaceName } as any}
undone, please proceed with caution. </StyledWorkspaceName>
</Trans> ) will delete both local and cloud data, this operation cannot be
</StyledTextContent> undone, please proceed with caution.
)} </Trans>
<StyledInputContent> )}
<Input <StyledInputContent>
ref={ref => { <Input
if (ref) { ref={ref => {
window.setTimeout(() => ref.focus(), 0); if (ref) {
} window.setTimeout(() => ref.focus(), 0);
}} }
onChange={setDeleteStr} }}
data-testid="delete-workspace-input" onChange={setDeleteStr}
placeholder={t['com.affine.workspaceDelete.placeholder']()} data-testid="delete-workspace-input"
width={315} placeholder={t['com.affine.workspaceDelete.placeholder']()}
height={42} width={315}
/> height={42}
</StyledInputContent> />
<StyledButtonContent> </StyledInputContent>
<Button onClick={onClose} size="large"> </ConfirmModal>
{t['com.affine.workspaceDelete.button.cancel']()}
</Button>
<Button
data-testid="delete-workspace-confirm-button"
disabled={!allowDelete}
onClick={onConfirm}
size="large"
type="error"
style={{ marginLeft: '24px' }}
>
{t['com.affine.workspaceDelete.button.delete']()}
</Button>
</StyledButtonContent>
</StyledModalWrapper>
</Modal>
); );
}; };

View File

@@ -21,20 +21,6 @@ export const StyledModalHeader = styled('div')(() => {
}; };
}); });
// export const StyledModalContent = styled('div')(({ theme }) => {});
export const StyledTextContent = styled('div')(() => {
return {
margin: 'auto',
width: '425px',
fontStyle: 'normal',
fontWeight: '400',
fontSize: '18px',
lineHeight: '26px',
textAlign: 'left',
};
});
export const StyledInputContent = styled('div')(() => { export const StyledInputContent = styled('div')(() => {
return { return {
display: 'flex', display: 'flex',
@@ -45,31 +31,8 @@ export const StyledInputContent = styled('div')(() => {
}; };
}); });
export const StyledButtonContent = styled('div')(() => {
return {
marginBottom: '42px',
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
};
});
export const StyledWorkspaceName = styled('span')(() => { export const StyledWorkspaceName = styled('span')(() => {
return { return {
fontWeight: '600', fontWeight: '600',
}; };
}); });
// export const StyledCancelButton = styled(Button)(({ theme }) => {
// return {
// width: '100px',
// justifyContent: 'center',
// };
// });
// export const StyledDeleteButton = styled(Button)(({ theme }) => {
// return {
// width: '100px',
// justifyContent: 'center',
// };
// });

View File

@@ -1,9 +1,9 @@
import { ConfirmModal } from '@affine/component';
import { SettingRow } from '@affine/component/setting-components'; import { SettingRow } from '@affine/component/setting-components';
import type { AffineOfficialWorkspace } from '@affine/env/workspace'; import type { AffineOfficialWorkspace } from '@affine/env/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace'; import { WorkspaceFlavour } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ArrowRightSmallIcon } from '@blocksuite/icons'; import { ArrowRightSmallIcon } from '@blocksuite/icons';
import { ConfirmModal } from '@toeverything/components/modal';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import type { WorkspaceSettingDetailProps } from '../types'; import type { WorkspaceSettingDetailProps } from '../types';
@@ -33,10 +33,6 @@ export const DeleteLeaveWorkspace = ({
} }
}, [isOwner]); }, [isOwner]);
const onCloseLeaveModal = useCallback(() => {
setShowLeave(false);
}, []);
const onLeaveConfirm = useCallback(() => { const onLeaveConfirm = useCallback(() => {
return onLeaveWorkspace(); return onLeaveWorkspace();
}, [onLeaveWorkspace]); }, [onLeaveWorkspace]);
@@ -71,21 +67,21 @@ export const DeleteLeaveWorkspace = ({
<WorkspaceDeleteModal <WorkspaceDeleteModal
onConfirm={onDeleteConfirm} onConfirm={onDeleteConfirm}
open={showDelete} open={showDelete}
onClose={() => { onOpenChange={setShowDelete}
setShowDelete(false);
}}
workspace={workspace} workspace={workspace}
/> />
) : ( ) : (
<ConfirmModal <ConfirmModal
open={showLeave} open={showLeave}
cancelText={t['com.affine.confirmModal.button.cancel']()}
onConfirm={onLeaveConfirm} onConfirm={onLeaveConfirm}
onCancel={onCloseLeaveModal} onOpenChange={setShowLeave}
onClose={onCloseLeaveModal}
title={`${t['com.affine.deleteLeaveWorkspace.leave']()}?`} title={`${t['com.affine.deleteLeaveWorkspace.leave']()}?`}
content={t['com.affine.deleteLeaveWorkspace.leaveDescription']()} description={t['com.affine.deleteLeaveWorkspace.leaveDescription']()}
confirmType="warning" confirmButtonOptions={{
confirmText={t['Leave']()} type: 'warning',
children: t['Leave'](),
}}
/> />
)} )}
</> </>

View File

@@ -142,9 +142,7 @@ const PublishPanelLocal = ({
{runtimeConfig.enableCloud ? ( {runtimeConfig.enableCloud ? (
<EnableAffineCloudModal <EnableAffineCloudModal
open={open} open={open}
onClose={() => { onOpenChange={setOpen}
setOpen(false);
}}
onConfirm={() => { onConfirm={() => {
onTransferWorkspace( onTransferWorkspace(
WorkspaceFlavour.LOCAL, WorkspaceFlavour.LOCAL,
@@ -155,12 +153,7 @@ const PublishPanelLocal = ({
}} }}
/> />
) : ( ) : (
<TmpDisableAffineCloudModal <TmpDisableAffineCloudModal open={open} onOpenChange={setOpen} />
open={open}
onClose={() => {
setOpen(false);
}}
/>
)} )}
</> </>
); );

View File

@@ -8,12 +8,16 @@ import { guideOnboardingAtom } from '../../atoms/guide';
export const OnboardingModal = memo(function OnboardingModal() { export const OnboardingModal = memo(function OnboardingModal() {
const [open, setOpen] = useAtom(openOnboardingModalAtom); const [open, setOpen] = useAtom(openOnboardingModalAtom);
const [guideOpen, setShowOnboarding] = useAtom(guideOnboardingAtom); const [guideOpen, setShowOnboarding] = useAtom(guideOnboardingAtom);
const onCloseTourModal = useCallback(() => { const onOpenChange = useCallback(
setShowOnboarding(false); (open: boolean) => {
setOpen(false); if (open) return;
}, [setOpen, setShowOnboarding]); setShowOnboarding(false);
setOpen(false);
},
[setOpen, setShowOnboarding]
);
return ( return (
<TourModal open={!open ? guideOpen : open} onClose={onCloseTourModal} /> <TourModal open={!open ? guideOpen : open} onOpenChange={onOpenChange} />
); );
}); });

View File

@@ -1,10 +1,7 @@
import { import { WorkspaceDetailSkeleton } from '@affine/component/setting-components';
SettingModal as SettingModalBase,
type SettingModalProps as SettingModalBaseProps,
WorkspaceDetailSkeleton,
} from '@affine/component/setting-components';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ContactWithUsIcon } from '@blocksuite/icons'; import { ContactWithUsIcon } from '@blocksuite/icons';
import { Modal, type ModalProps } from '@toeverything/components/modal';
import { Suspense, useCallback } from 'react'; import { Suspense, useCallback } from 'react';
import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-status'; import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-status';
@@ -20,7 +17,7 @@ import { WorkspaceSetting } from './workspace-setting';
type ActiveTab = GeneralSettingKeys | 'workspace' | 'account'; type ActiveTab = GeneralSettingKeys | 'workspace' | 'account';
export interface SettingProps { export interface SettingProps extends ModalProps {
activeTab: ActiveTab; activeTab: ActiveTab;
workspaceId: string | null; workspaceId: string | null;
onSettingClick: (params: { onSettingClick: (params: {
@@ -29,15 +26,12 @@ export interface SettingProps {
}) => void; }) => void;
} }
type SettingModalProps = SettingModalBaseProps & SettingProps;
export const SettingModal = ({ export const SettingModal = ({
open,
setOpen,
activeTab = 'appearance', activeTab = 'appearance',
workspaceId = null, workspaceId = null,
onSettingClick, onSettingClick,
}: SettingModalProps) => { ...modalProps
}: SettingProps) => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const loginStatus = useCurrentLoginStatus(); const loginStatus = useCurrentLoginStatus();
@@ -66,7 +60,21 @@ export const SettingModal = ({
}, [onSettingClick]); }, [onSettingClick]);
return ( return (
<SettingModalBase open={open} setOpen={setOpen}> <Modal
width={1080}
height={760}
contentOptions={{
['data-testid' as string]: 'setting-modal',
style: {
maxHeight: '85vh',
maxWidth: '70vw',
padding: 0,
overflow: 'hidden',
display: 'flex',
},
}}
{...modalProps}
>
<SettingSidebar <SettingSidebar
generalSettingList={generalSettingList} generalSettingList={generalSettingList}
onGeneralSettingClick={onGeneralSettingClick} onGeneralSettingClick={onGeneralSettingClick}
@@ -105,6 +113,6 @@ export const SettingModal = ({
</div> </div>
</div> </div>
</div> </div>
</SettingModalBase> </Modal>
); );
}; };

View File

@@ -30,9 +30,7 @@ export const SharePageModal = ({ workspace, page }: SharePageModalProps) => {
{workspace.flavour === WorkspaceFlavour.LOCAL ? ( {workspace.flavour === WorkspaceFlavour.LOCAL ? (
<EnableAffineCloudModal <EnableAffineCloudModal
open={open} open={open}
onClose={() => { onOpenChange={setOpen}
setOpen(false);
}}
onConfirm={() => { onConfirm={() => {
onTransformWorkspace( onTransformWorkspace(
WorkspaceFlavour.LOCAL, WorkspaceFlavour.LOCAL,

View File

@@ -1,79 +1,61 @@
import { Empty, Modal, ModalWrapper } from '@affine/component'; import { Empty } from '@affine/component';
import { Trans } from '@affine/i18n'; import { Trans } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { CloseIcon } from '@blocksuite/icons'; import { Modal, type ModalProps } from '@toeverything/components/modal';
import { IconButton } from '@toeverything/components/button'; import { useCallback } from 'react';
import { import {
Content,
ContentTitle,
Header,
StyleButton, StyleButton,
StyleButtonContainer, StyleButtonContainer,
StyleImage, StyleImage,
StyleTips, StyleTips,
} from './style'; } from './style';
interface TmpDisableAffineCloudModalProps { export const TmpDisableAffineCloudModal = (props: ModalProps) => {
open: boolean;
onClose: () => void;
}
export const TmpDisableAffineCloudModal = ({
open,
onClose,
}: TmpDisableAffineCloudModalProps) => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const onClose = useCallback(() => {
props.onOpenChange?.(false);
}, [props]);
return ( return (
<Modal <Modal
data-testid="disable-affine-cloud-modal" title={t['com.affine.cloudTempDisable.title']()}
open={open} contentOptions={{
onClose={onClose} ['data-testid' as string]: 'disable-affine-cloud-modal',
}}
width={480}
{...props}
> >
<ModalWrapper width={480}> <StyleTips>
<Header> <Trans i18nKey="com.affine.cloudTempDisable.description">
<IconButton onClick={onClose}> We are upgrading the AFFiNE Cloud service and it is temporarily
<CloseIcon /> unavailable on the client side. If you wish to stay updated on the
</IconButton> progress and be notified on availability, you can fill out the
</Header> <a
<Content> href="https://6dxre9ihosp.typeform.com/to/B8IHwuyy"
<ContentTitle> rel="noreferrer"
{t['com.affine.cloudTempDisable.title']()} target="_blank"
</ContentTitle> style={{
<StyleTips> color: 'var(--affine-link-color)',
<Trans i18nKey="com.affine.cloudTempDisable.description"> }}
We are upgrading the AFFiNE Cloud service and it is temporarily >
unavailable on the client side. If you wish to stay updated on the AFFiNE Cloud Signup
progress and be notified on availability, you can fill out the </a>
<a .
href="https://6dxre9ihosp.typeform.com/to/B8IHwuyy" </Trans>
rel="noreferrer" </StyleTips>
target="_blank" <StyleImage>
style={{ <Empty
color: 'var(--affine-link-color)', containerStyle={{
}} width: '200px',
> height: '112px',
AFFiNE Cloud Signup }}
</a> />
. </StyleImage>
</Trans> <StyleButtonContainer>
</StyleTips> <StyleButton type="primary" onClick={onClose}>
<StyleImage> {t['Got it']()}
<Empty </StyleButton>
containerStyle={{ </StyleButtonContainer>
width: '200px',
height: '112px',
}}
/>
</StyleImage>
<StyleButtonContainer>
<StyleButton type="primary" onClick={onClose}>
{t['Got it']()}
</StyleButton>
</StyleButtonContainer>
</Content>
</ModalWrapper>
</Modal> </Modal>
); );
}; };

View File

@@ -16,6 +16,7 @@ export const Content = styled('div')({
export const ContentTitle = styled('h1')(() => { export const ContentTitle = styled('h1')(() => {
return { return {
marginTop: 44,
fontSize: 'var(--affine-font-h6)', fontSize: 'var(--affine-font-h6)',
lineHeight: '28px', lineHeight: '28px',
fontWeight: 600, fontWeight: 600,
@@ -24,8 +25,7 @@ export const ContentTitle = styled('h1')(() => {
export const StyleTips = styled('div')(() => { export const StyleTips = styled('div')(() => {
return { return {
userSelect: 'none', margin: '0 0 20px 0',
margin: '20px 0',
a: { a: {
color: 'var(--affine-primary-color)', color: 'var(--affine-primary-color)',
}, },
@@ -35,7 +35,6 @@ export const StyleTips = styled('div')(() => {
export const StyleButton = styled(Button)(() => { export const StyleButton = styled(Button)(() => {
return { return {
textAlign: 'center', textAlign: 'center',
margin: '20px 0',
borderRadius: '8px', borderRadius: '8px',
backgroundColor: 'var(--affine-primary-color)', backgroundColor: 'var(--affine-primary-color)',
span: { span: {
@@ -46,11 +45,12 @@ export const StyleButton = styled(Button)(() => {
export const StyleButtonContainer = styled('div')(() => { export const StyleButtonContainer = styled('div')(() => {
return { return {
width: '100%', width: '100%',
marginTop: 20,
...displayFlex('flex-end', 'center'), ...displayFlex('flex-end', 'center'),
}; };
}); });
export const StyleImage = styled('div')(() => { export const StyleImage = styled('div')(() => {
return { return {
width: '100%', ...displayFlex('center', 'center'),
}; };
}); });

View File

@@ -1,58 +0,0 @@
import { Modal, ModalWrapper } from '@affine/component';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { CloseIcon } from '@blocksuite/icons';
import { IconButton } from '@toeverything/components/button';
import { Content, ContentTitle, Header, StyleButton, StyleTips } from './style';
export interface TransformWorkspaceToAffineModalProps {
open: boolean;
onClose: () => void;
onConform: () => void;
}
export const TransformWorkspaceToAffineModal = ({
open,
onClose,
onConform,
}: TransformWorkspaceToAffineModalProps) => {
const t = useAFFiNEI18N();
return (
<Modal
open={open}
onClose={onClose}
data-testid="enable-affine-cloud-modal"
>
<ModalWrapper width={560} height={292}>
<Header>
<IconButton onClick={onClose}>
<CloseIcon />
</IconButton>
</Header>
<Content>
<ContentTitle>{t['Enable AFFiNE Cloud']()}?</ContentTitle>
<StyleTips>{t['Enable AFFiNE Cloud Description']()}</StyleTips>
{/* <StyleTips>{t('Retain cached cloud data')}</StyleTips> */}
<div>
<StyleButton
data-testid="confirm-enable-cloud-button"
type="primary"
onClick={onConform}
>
{t['Sign in and Enable']()}
</StyleButton>
<StyleButton
shape="round"
onClick={() => {
onClose();
}}
>
{t['Not now']()}
</StyleButton>
</div>
</Content>
</ModalWrapper>
</Modal>
);
};

View File

@@ -1,41 +0,0 @@
import { styled } from '@affine/component';
import { Button } from '@toeverything/components/button';
export const Header = styled('div')({
height: '44px',
display: 'flex',
flexDirection: 'row-reverse',
paddingRight: '10px',
paddingTop: '10px',
flexShrink: 0,
});
export const Content = styled('div')({
textAlign: 'center',
});
export const ContentTitle = styled('h1')({
fontSize: '20px',
lineHeight: '28px',
fontWeight: 600,
textAlign: 'center',
});
export const StyleTips = styled('div')(() => {
return {
userSelect: 'none',
width: '400px',
margin: 'auto',
marginBottom: '32px',
marginTop: '12px',
};
});
export const StyleButton = styled(Button)(() => {
return {
width: '284px',
display: 'block',
margin: 'auto',
marginTop: '16px',
};
});

View File

@@ -225,9 +225,7 @@ export const PageMenu = ({ rename, pageId }: PageMenuProps) => {
open={openConfirm} open={openConfirm}
title={pageMeta.title} title={pageMeta.title}
onConfirm={handleOnConfirm} onConfirm={handleOnConfirm}
onCancel={() => { onOpenChange={setOpenConfirm}
setOpenConfirm(false);
}}
/> />
</FlexWrapper> </FlexWrapper>
</> </>

View File

@@ -1,5 +1,5 @@
import { Modal, ModalWrapper } from '@affine/component';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { Modal } from '@toeverything/components/modal';
import { Command } from 'cmdk'; import { Command } from 'cmdk';
import { startTransition, Suspense } from 'react'; import { startTransition, Suspense } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
@@ -75,93 +75,86 @@ export const QuickSearchModal = ({
return ( return (
<Modal <Modal
open={open} open={open}
onClose={handleClose} onOpenChange={setOpen}
wrapperPosition={['top', 'center']} width={608}
data-testid="quickSearch" contentOptions={{
> ['data-testid' as string]: 'quickSearch',
<ModalWrapper style: {
width={608}
style={{
maxHeight: '80vh', maxHeight: '80vh',
minHeight: '412px', minHeight: '412px',
top: '80px', top: '80px',
overflow: 'hidden', overflow: 'hidden',
transform: 'translateX(-50%)',
},
}}
>
<Command
shouldFilter={false}
//Handle KeyboardEvent conflicts with blocksuite
onKeyDown={(e: React.KeyboardEvent) => {
if (
e.key === 'ArrowDown' ||
e.key === 'ArrowUp' ||
e.key === 'ArrowLeft' ||
e.key === 'ArrowRight'
) {
e.stopPropagation();
}
}} }}
> >
{/* <NavigationPath <StyledModalHeader>
blockSuiteWorkspace={blockSuiteWorkspace} <SearchInput
onJumpToPage={() => { ref={inputRef}
setOpen(false); onValueChange={value => {
}} setQuery(value);
/> */} }}
<Command onKeyDown={e => {
shouldFilter={false} // Avoid triggering the cmdk onSelect event when the input method is in use
//Handle KeyboardEvent conflicts with blocksuite if (e.nativeEvent.isComposing) {
onKeyDown={(e: React.KeyboardEvent) => { e.stopPropagation();
if ( return;
e.key === 'ArrowDown' || }
e.key === 'ArrowUp' || }}
e.key === 'ArrowLeft' || placeholder={t['Quick search placeholder']()}
e.key === 'ArrowRight' />
) { <StyledShortcut>
e.stopPropagation(); {environment.isBrowser && environment.isMacOs
} ? '⌘ + K'
}} : 'Ctrl + K'}
> </StyledShortcut>
<StyledModalHeader> </StyledModalHeader>
<SearchInput <StyledModalDivider />
ref={inputRef} <Command.List>
onValueChange={value => { <StyledContent>
setQuery(value); <Suspense
}} fallback={
onKeyDown={e => { <StyledNotFound>
// Avoid triggering the cmdk onSelect event when the input method is in use <span>{t['com.affine.loading']()}</span>
if (e.nativeEvent.isComposing) { </StyledNotFound>
e.stopPropagation(); }
return; >
} <Results
}} query={query}
placeholder={t['Quick search placeholder']()} onClose={handleClose}
/> workspace={workspace}
<StyledShortcut> setShowCreatePage={setShowCreatePage}
{environment.isBrowser && environment.isMacOs />
? '⌘ + K' </Suspense>
: 'Ctrl + K'} </StyledContent>
</StyledShortcut> {showCreatePage ? (
</StyledModalHeader> <>
<StyledModalDivider /> <StyledModalDivider />
<Command.List> <StyledModalFooter>
<StyledContent> <Footer
<Suspense
fallback={
<StyledNotFound>
<span>{t['com.affine.loading']()}</span>
</StyledNotFound>
}
>
<Results
query={query} query={query}
onClose={handleClose} onClose={handleClose}
workspace={workspace} blockSuiteWorkspace={blockSuiteWorkspace}
setShowCreatePage={setShowCreatePage}
/> />
</Suspense> </StyledModalFooter>
</StyledContent> </>
{showCreatePage ? ( ) : null}
<> </Command.List>
<StyledModalDivider /> </Command>
<StyledModalFooter>
<Footer
query={query}
onClose={handleClose}
blockSuiteWorkspace={blockSuiteWorkspace}
/>
</StyledModalFooter>
</>
) : null}
</Command.List>
</Command>
</ModalWrapper>
</Modal> </Modal>
); );
}; };

View File

@@ -1,9 +1,7 @@
import { import { MuiClickAwayListener, MuiSlide } from '@affine/component';
ModalCloseButton,
MuiClickAwayListener,
MuiSlide,
} from '@affine/component';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { CloseIcon } from '@blocksuite/icons';
import { IconButton } from '@toeverything/components/button';
import { import {
type ShortcutsInfo, type ShortcutsInfo,
@@ -14,13 +12,7 @@ import {
} from '../../../hooks/affine/use-shortcuts'; } from '../../../hooks/affine/use-shortcuts';
import { KeyboardIcon } from './icons'; import { KeyboardIcon } from './icons';
import * as styles from './style.css'; import * as styles from './style.css';
// import {
// StyledListItem,
// StyledModalHeader,
// StyledShortcutsModal,
// StyledSubTitle,
// StyledTitle,
// } from './style';
type ModalProps = { type ModalProps = {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
@@ -81,12 +73,16 @@ export const ShortcutsModal = ({ open, onClose }: ModalProps) => {
{t['Shortcuts']()} {t['Shortcuts']()}
</div> </div>
<ModalCloseButton <IconButton
top={6} style={{
right={6} position: 'absolute',
right: 6,
top: 6,
}}
onClick={() => { onClick={() => {
onClose(); onClose();
}} }}
icon={<CloseIcon />}
/> />
</div> </div>
<ShortcutsPanel shortcutsInfo={generalShortcutsInfo} /> <ShortcutsPanel shortcutsInfo={generalShortcutsInfo} />

View File

@@ -1,8 +1,8 @@
import { Confirm } from '@affine/component';
import { WorkspaceSubPath } from '@affine/env/workspace'; import { WorkspaceSubPath } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { assertExists } from '@blocksuite/global/utils'; import { assertExists } from '@blocksuite/global/utils';
import { Button } from '@toeverything/components/button'; import { Button } from '@toeverything/components/button';
import { ConfirmModal } from '@toeverything/components/modal';
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta'; import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
import { currentPageIdAtom } from '@toeverything/infra/atom'; import { currentPageIdAtom } from '@toeverything/infra/atom';
import { useAtomValue } from 'jotai'; import { useAtomValue } from 'jotai';
@@ -61,23 +61,21 @@ export const TrashButtonGroup = () => {
{t['com.affine.trashOperation.deletePermanently']()} {t['com.affine.trashOperation.deletePermanently']()}
</Button> </Button>
</div> </div>
<Confirm <ConfirmModal
title={t['com.affine.trashOperation.delete.title']()} title={t['com.affine.trashOperation.delete.title']()}
content={t['com.affine.trashOperation.delete.description']()} cancelText={t['com.affine.confirmModal.button.cancel']()}
confirmText={t['com.affine.trashOperation.delete']()} description={t['com.affine.trashOperation.delete.description']()}
confirmType="error" confirmButtonOptions={{
type: 'error',
children: t['com.affine.trashOperation.delete'](),
}}
open={open} open={open}
onConfirm={useCallback(() => { onConfirm={useCallback(() => {
jumpToSubPath(workspace.id, WorkspaceSubPath.ALL); jumpToSubPath(workspace.id, WorkspaceSubPath.ALL);
blockSuiteWorkspace.removePage(pageId); blockSuiteWorkspace.removePage(pageId);
toast(t['com.affine.toastMessage.permanentlyDeleted']()); toast(t['com.affine.toastMessage.permanentlyDeleted']());
}, [blockSuiteWorkspace, jumpToSubPath, pageId, workspace.id, t])} }, [blockSuiteWorkspace, jumpToSubPath, pageId, workspace.id, t])}
onCancel={() => { onOpenChange={setOpen}
setOpen(false);
}}
onClose={() => {
setOpen(false);
}}
/> />
</div> </div>
); );

View File

@@ -177,6 +177,7 @@ export const WorkspaceListModal = ({
const localWorkspaces = workspaces.filter( const localWorkspaces = workspaces.filter(
({ flavour }) => flavour === WorkspaceFlavour.LOCAL ({ flavour }) => flavour === WorkspaceFlavour.LOCAL
) as (AffineCloudWorkspace | LocalWorkspace)[]; ) as (AffineCloudWorkspace | LocalWorkspace)[];
// FIXME: replace mui popover
return ( return (
<Popover <Popover
sx={{ sx={{
@@ -209,6 +210,7 @@ export const WorkspaceListModal = ({
openModal: true, openModal: true,
})); }));
} }
onClose();
}} }}
data-testid="cloud-signin-button" data-testid="cloud-signin-button"
> >

View File

@@ -1,5 +1,5 @@
import { import {
EditCollectionModel, EditCollectionModal,
useCollectionManager, useCollectionManager,
} from '@affine/component/page-list'; } from '@affine/component/page-list';
import type { Collection } from '@affine/env/filter'; import type { Collection } from '@affine/env/filter';
@@ -46,12 +46,12 @@ export const AddCollectionButton = ({
<PlusIcon /> <PlusIcon />
</IconButton> </IconButton>
<EditCollectionModel <EditCollectionModal
propertiesMeta={workspace.meta.properties} propertiesMeta={workspace.meta.properties}
getPageInfo={getPageInfo} getPageInfo={getPageInfo}
onConfirm={setting.saveCollection} onConfirm={setting.saveCollection}
open={show} open={show}
onClose={() => showUpdateCollection(false)} onOpenChange={showUpdateCollection}
title={t['com.affine.editCollection.saveCollection']()} title={t['com.affine.editCollection.saveCollection']()}
init={defaultCollection} init={defaultCollection}
/> />

View File

@@ -1,6 +1,6 @@
import { MenuItem as CollectionItem } from '@affine/component/app-sidebar'; import { MenuItem as CollectionItem } from '@affine/component/app-sidebar';
import { import {
EditCollectionModel, EditCollectionModal,
useCollectionManager, useCollectionManager,
useSavedCollections, useSavedCollections,
} from '@affine/component/page-list'; } from '@affine/component/page-list';
@@ -203,13 +203,13 @@ const CollectionRenderer = ({
return ( return (
<Collapsible.Root open={!collapsed}> <Collapsible.Root open={!collapsed}>
<EditCollectionModel <EditCollectionModal
propertiesMeta={workspace.meta.properties} propertiesMeta={workspace.meta.properties}
getPageInfo={getPageInfo} getPageInfo={getPageInfo}
init={collection} init={collection}
onConfirm={setting.saveCollection} onConfirm={setting.saveCollection}
open={show} open={show}
onClose={() => showUpdateCollection(false)} onOpenChange={showUpdateCollection}
/> />
<CollectionItem <CollectionItem
data-testid="collection-item" data-testid="collection-item"

View File

@@ -53,14 +53,14 @@ export type RootAppSidebarProps = {
}; };
const RouteMenuLinkItem = React.forwardRef< const RouteMenuLinkItem = React.forwardRef<
HTMLButtonElement, HTMLDivElement,
{ {
currentPath: string; // todo: pass through useRouter? currentPath: string; // todo: pass through useRouter?
path: string; path: string;
icon: ReactElement; icon: ReactElement;
children?: ReactElement; children?: ReactElement;
isDraggedOver?: boolean; isDraggedOver?: boolean;
} & React.HTMLAttributes<HTMLButtonElement> } & React.HTMLAttributes<HTMLDivElement>
>(({ currentPath, path, icon, children, isDraggedOver, ...props }, ref) => { >(({ currentPath, path, icon, children, isDraggedOver, ...props }, ref) => {
// Force active style when a page is dragged over // Force active style when a page is dragged over
const active = isDraggedOver || currentPath === path; const active = isDraggedOver || currentPath === path;

View File

@@ -86,8 +86,8 @@ export const Setting = () => {
activeTab={activeTab} activeTab={activeTab}
workspaceId={workspaceId} workspaceId={workspaceId}
onSettingClick={onSettingClick} onSettingClick={onSettingClick}
setOpen={useCallback( onOpenChange={useCallback(
open => { (open: boolean) => {
setOpenSettingModalAtom(prev => ({ ...prev, open })); setOpenSettingModalAtom(prev => ({ ...prev, open }));
}, },
[setOpenSettingModalAtom] [setOpenSettingModalAtom]
@@ -147,9 +147,7 @@ export function CurrentWorkspaceModals() {
<Suspense> <Suspense>
<TmpDisableAffineCloudModal <TmpDisableAffineCloudModal
open={openDisableCloudAlertModal} open={openDisableCloudAlertModal}
onClose={useCallback(() => { onOpenChange={setOpenDisableCloudAlertModal}
setOpenDisableCloudAlertModal(false);
}, [setOpenDisableCloudAlertModal])}
/> />
</Suspense> </Suspense>
{environment.isDesktop && ( {environment.isDesktop && (

View File

@@ -11,7 +11,7 @@ export default {
} satisfies Meta; } satisfies Meta;
export const Basic: StoryFn = () => { export const Basic: StoryFn = () => {
return <TourModal open={true} onClose={() => {}} />; return <TourModal open={true} />;
}; };
Basic.args = { Basic.args = {
logoSrc: '/imgs/affine-text-logo.png', logoSrc: '/imgs/affine-text-logo.png',

View File

@@ -136,11 +136,11 @@ export const DisableModal: StoryFn = () => {
</StyledDisableButton> </StyledDisableButton>
<PublicLinkDisableModal <PublicLinkDisableModal
open={open} open={open}
onConfirmDisable={() => { onConfirm={() => {
toast('Disabled'); toast('Disabled');
setOpen(false); setOpen(false);
}} }}
onClose={() => setOpen(false)} onOpenChange={setOpen}
/> />
</> </>
); );

View File

@@ -6,7 +6,7 @@ import { Link } from 'react-router-dom';
import * as styles from './index.css'; import * as styles from './index.css';
export interface MenuItemProps extends React.HTMLAttributes<HTMLButtonElement> { export interface MenuItemProps extends React.HTMLAttributes<HTMLDivElement> {
icon?: React.ReactElement; icon?: React.ReactElement;
active?: boolean; active?: boolean;
disabled?: boolean; disabled?: boolean;
@@ -25,7 +25,7 @@ const stopPropagation: React.MouseEventHandler = e => {
e.stopPropagation(); e.stopPropagation();
}; };
export const MenuItem = React.forwardRef<HTMLButtonElement, MenuItemProps>( export const MenuItem = React.forwardRef<HTMLDivElement, MenuItemProps>(
( (
{ {
onClick, onClick,
@@ -42,7 +42,7 @@ export const MenuItem = React.forwardRef<HTMLButtonElement, MenuItemProps>(
) => { ) => {
const collapsible = onCollapsedChange !== undefined; const collapsible = onCollapsedChange !== undefined;
return ( return (
<button <div
ref={ref} ref={ref}
{...props} {...props}
onClick={onClick} onClick={onClick}
@@ -82,22 +82,21 @@ export const MenuItem = React.forwardRef<HTMLButtonElement, MenuItemProps>(
{postfix} {postfix}
</div> </div>
) : null} ) : null}
</button> </div>
); );
} }
); );
MenuItem.displayName = 'MenuItem'; MenuItem.displayName = 'MenuItem';
export const MenuLinkItem = React.forwardRef< export const MenuLinkItem = React.forwardRef<HTMLDivElement, MenuLinkItemProps>(
HTMLButtonElement, ({ to, ...props }, ref) => {
MenuLinkItemProps return (
>(({ to, ...props }, ref) => { <Link to={to} className={styles.linkItemRoot}>
return ( {/* The <a> element rendered by Link does not generate display box due to `display: contents` style */}
<Link to={to} className={styles.linkItemRoot}> {/* Thus ref is passed to MenuItem instead of Link */}
{/* The <a> element rendered by Link does not generate display box due to `display: contents` style */} <MenuItem ref={ref} {...props}></MenuItem>
{/* Thus ref is passed to MenuItem instead of Link */} </Link>
<MenuItem ref={ref} {...props}></MenuItem> );
</Link> }
); );
});
MenuLinkItem.displayName = 'MenuLinkItem'; MenuLinkItem.displayName = 'MenuLinkItem';

View File

@@ -1,5 +1,5 @@
import clsx from 'clsx'; import clsx from 'clsx';
import type { FC } from 'react'; import type { FC, HTMLAttributes } from 'react';
import { Input, type InputProps } from '../../ui/input'; import { Input, type InputProps } from '../../ui/input';
import { authInputWrapper, formHint } from './share.css'; import { authInputWrapper, formHint } from './share.css';
@@ -9,6 +9,7 @@ export type AuthInputProps = InputProps & {
errorHint?: string; errorHint?: string;
withoutHint?: boolean; withoutHint?: boolean;
onEnter?: () => void; onEnter?: () => void;
wrapperProps?: HTMLAttributes<HTMLDivElement>;
}; };
export const AuthInput: FC<AuthInputProps> = ({ export const AuthInput: FC<AuthInputProps> = ({
label, label,
@@ -16,13 +17,15 @@ export const AuthInput: FC<AuthInputProps> = ({
errorHint, errorHint,
withoutHint = false, withoutHint = false,
onEnter, onEnter,
wrapperProps: { className, ...otherWrapperProps } = {},
...inputProps ...inputProps
}) => { }) => {
return ( return (
<div <div
className={clsx(authInputWrapper, { className={clsx(authInputWrapper, className, {
'without-hint': withoutHint, 'without-hint': withoutHint,
})} })}
{...otherWrapperProps}
> >
{label ? <label>{label}</label> : null} {label ? <label>{label}</label> : null}
<Input <Input

View File

@@ -1,6 +1,5 @@
import { Modal, ModalCloseButton, ModalWrapper } from '@affine/component'; import { Modal } from '@toeverything/components/modal';
import type { FC, PropsWithChildren } from 'react'; import type { FC, PropsWithChildren } from 'react';
import { useCallback } from 'react';
export type AuthModalProps = { export type AuthModalProps = {
open: boolean; open: boolean;
@@ -12,32 +11,18 @@ export const AuthModal: FC<PropsWithChildren<AuthModalProps>> = ({
open, open,
setOpen, setOpen,
}) => { }) => {
const handleClose = useCallback(() => {
setOpen(false);
}, [setOpen]);
return ( return (
<Modal <Modal
open={open} open={open}
onClose={handleClose} onOpenChange={setOpen}
wrapperPosition={['center', 'center']} width={400}
data-testid="auth-modal" height={468}
contentOptions={{
['data-testid' as string]: 'auth-modal',
style: { padding: '44px 40px 0' },
}}
> >
<ModalWrapper {children}
width={1080}
height={760}
style={{
height: '468px',
width: '400px',
overflow: 'hidden',
backgroundColor: 'var(--affine-white)',
boxShadow: 'var(--affine-popover-shadow)',
padding: '44px 40px 0',
}}
>
<ModalCloseButton top={20} right={20} onClick={handleClose} />
{children}
</ModalWrapper>
</Modal> </Modal>
); );
}; };

View File

@@ -1,13 +1,14 @@
import { import {
CloseIcon,
ExportToHtmlIcon, ExportToHtmlIcon,
ExportToMarkdownIcon, ExportToMarkdownIcon,
HelpIcon, HelpIcon,
NewIcon, NewIcon,
NotionIcon, NotionIcon,
} from '@blocksuite/icons'; } from '@blocksuite/icons';
import { IconButton } from '@toeverything/components/button';
import { Tooltip } from '@toeverything/components/tooltip'; import { Tooltip } from '@toeverything/components/tooltip';
import { ModalCloseButton } from '../../ui/modal';
import { BlockCard } from '../card/block-card'; import { BlockCard } from '../card/block-card';
import { import {
importPageBodyStyle, importPageBodyStyle,
@@ -27,7 +28,17 @@ export const ImportPage = ({
onClose: () => void; onClose: () => void;
}) => ( }) => (
<div className={importPageContainerStyle}> <div className={importPageContainerStyle}>
<ModalCloseButton top={6} right={6} onClick={onClose} /> <IconButton
style={{
position: 'absolute',
right: 6,
top: 6,
}}
onClick={() => {
onClose();
}}
icon={<CloseIcon />}
/>
<div className={importPageBodyStyle}> <div className={importPageBodyStyle}>
<div className="title">Import</div> <div className="title">Import</div>
<span> <span>

View File

@@ -1,12 +1,10 @@
import { Permission } from '@affine/graphql'; import { Permission } from '@affine/graphql';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { Button } from '@toeverything/components/button'; import { ConfirmModal } from '@toeverything/components/modal';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { Modal, ModalCloseButton, ModalWrapper } from '../../ui/modal';
import { AuthInput } from '..//auth-components'; import { AuthInput } from '..//auth-components';
import { emailRegex } from '..//auth-components/utils'; import { emailRegex } from '..//auth-components/utils';
import * as styles from './styles.css';
export interface InviteModalProps { export interface InviteModalProps {
open: boolean; open: boolean;
@@ -26,10 +24,6 @@ export const InviteModal = ({
const [permission] = useState(Permission.Write); const [permission] = useState(Permission.Write);
const [isValidEmail, setIsValidEmail] = useState(true); const [isValidEmail, setIsValidEmail] = useState(true);
const handleCancel = useCallback(() => {
setOpen(false);
}, [setOpen]);
const handleConfirm = useCallback(() => { const handleConfirm = useCallback(() => {
if (!emailRegex.test(inviteEmail)) { if (!emailRegex.test(inviteEmail)) {
setIsValidEmail(false); setIsValidEmail(false);
@@ -51,47 +45,41 @@ export const InviteModal = ({
}, [open]); }, [open]);
return ( return (
<Modal <ConfirmModal
wrapperPosition={['center', 'center']}
data-testid="invite-modal"
open={open} open={open}
> onOpenChange={setOpen}
<ModalWrapper width={480}
width={480} title={t['Invite Members']()}
height={254} description={t['Invite Members Message']()}
style={{ cancelText={t['com.affine.inviteModal.button.cancel']()}
contentOptions={{
['data-testid' as string]: 'invite-modal',
style: {
padding: '20px 26px', padding: '20px 26px',
},
}}
confirmButtonOptions={{
loading: isMutating,
type: 'primary',
['data-testid' as string]: 'confirm-enable-affine-cloud-button',
children: t['Invite'](),
}}
onConfirm={handleConfirm}
>
{/*TODO: check email & add placeholder*/}
<AuthInput
disabled={isMutating}
placeholder="email@example.com"
value={inviteEmail}
onChange={setInviteEmail}
error={!isValidEmail}
errorHint={isValidEmail ? '' : t['com.affine.auth.sign.email.error']()}
onEnter={handleConfirm}
wrapperProps={{
style: { padding: 0 },
}} }}
> size="large"
<ModalCloseButton top={20} right={20} onClick={handleCancel} /> />
</ConfirmModal>
<div className={styles.inviteModalTitle}>{t['Invite Members']()}</div>
<div className={styles.inviteModalContent}>
{t['Invite Members Message']()}
{/*TODO: check email & add placeholder*/}
<AuthInput
disabled={isMutating}
placeholder="email@example.com"
value={inviteEmail}
onChange={setInviteEmail}
error={!isValidEmail}
errorHint={
isValidEmail ? '' : t['com.affine.auth.sign.email.error']()
}
onEnter={handleConfirm}
style={{ marginTop: 20 }}
size="large"
/>
</div>
<div className={styles.inviteModalButtonContainer}>
<Button style={{ marginRight: 20 }} onClick={handleCancel}>
{t['com.affine.inviteModal.button.cancel']()}
</Button>
<Button type="primary" onClick={handleConfirm} loading={isMutating}>
{t['Invite']()}
</Button>
</div>
</ModalWrapper>
</Modal>
); );
}; };

View File

@@ -10,10 +10,11 @@ import {
} from '@blocksuite/icons'; } from '@blocksuite/icons';
import { IconButton } from '@toeverything/components/button'; import { IconButton } from '@toeverything/components/button';
import { Menu, MenuIcon, MenuItem } from '@toeverything/components/menu'; import { Menu, MenuIcon, MenuItem } from '@toeverything/components/menu';
import { ConfirmModal } from '@toeverything/components/modal';
import { Tooltip } from '@toeverything/components/tooltip'; import { Tooltip } from '@toeverything/components/tooltip';
import { useState } from 'react'; import { useState } from 'react';
import { Confirm, FlexWrapper } from '../../..'; import { FlexWrapper } from '../../..';
import { DisablePublicSharing, MoveToTrash } from './operation-menu-items'; import { DisablePublicSharing, MoveToTrash } from './operation-menu-items';
export interface OperationCellProps { export interface OperationCellProps {
@@ -106,19 +107,12 @@ export const OperationCell = ({
onRemoveToTrash(); onRemoveToTrash();
setOpen(false); setOpen(false);
}} }}
onClose={() => { onOpenChange={setOpen}
setOpen(false);
}}
onCancel={() => {
setOpen(false);
}}
/> />
<DisablePublicSharing.DisablePublicSharingModal <DisablePublicSharing.DisablePublicSharingModal
onConfirmDisable={onDisablePublicSharing} onConfirm={onDisablePublicSharing}
open={openDisableShared} open={openDisableShared}
onClose={() => { onOpenChange={setOpenDisableShared}
setOpenDisableShared(false);
}}
/> />
</> </>
); );
@@ -161,22 +155,20 @@ export const TrashOperationCell = ({
<DeletePermanentlyIcon /> <DeletePermanentlyIcon />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
<Confirm <ConfirmModal
title={`${t['com.affine.trashOperation.deletePermanently']()}?`} title={`${t['com.affine.trashOperation.deletePermanently']()}?`}
content={t['com.affine.trashOperation.deleteDescription']()} description={t['com.affine.trashOperation.deleteDescription']()}
confirmText={t['com.affine.trashOperation.delete']()} cancelText={t['com.affine.confirmModal.button.cancel']()}
confirmType="error" confirmButtonOptions={{
type: 'error',
children: t['com.affine.trashOperation.delete'](),
}}
open={open} open={open}
onOpenChange={setOpen}
onConfirm={() => { onConfirm={() => {
onPermanentlyDeletePage(); onPermanentlyDeletePage();
setOpen(false); setOpen(false);
}} }}
onClose={() => {
setOpen(false);
}}
onCancel={() => {
setOpen(false);
}}
/> />
</FlexWrapper> </FlexWrapper>
); );

View File

@@ -5,9 +5,10 @@ import {
MenuItem, MenuItem,
type MenuItemProps, type MenuItemProps,
} from '@toeverything/components/menu'; } from '@toeverything/components/menu';
import {
import type { ConfirmProps } from '../../..'; ConfirmModal,
import { Confirm } from '../../..'; type ConfirmModalProps,
} from '@toeverything/components/modal';
export const MoveToTrash = (props: MenuItemProps) => { export const MoveToTrash = (props: MenuItemProps) => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
@@ -27,26 +28,29 @@ export const MoveToTrash = (props: MenuItemProps) => {
); );
}; };
const ConfirmModal = ({ const MoveToTrashConfirm = ({
title, title,
...confirmModalProps ...confirmModalProps
}: { }: {
title: string; title: string;
} & ConfirmProps) => { } & ConfirmModalProps) => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
return ( return (
<Confirm <ConfirmModal
title={t['com.affine.moveToTrash.confirmModal.title']()} title={t['com.affine.moveToTrash.confirmModal.title']()}
content={t['com.affine.moveToTrash.confirmModal.description']({ description={t['com.affine.moveToTrash.confirmModal.description']({
title: title || 'Untitled', title: title || 'Untitled',
})} })}
confirmButtonTestId="confirm-delete-page" cancelText={t['com.affine.confirmModal.button.cancel']()}
confirmText={t.Delete()} confirmButtonOptions={{
confirmType="error" ['data-testid' as string]: 'confirm-delete-page',
type: 'error',
children: t.Delete(),
}}
{...confirmModalProps} {...confirmModalProps}
/> />
); );
}; };
MoveToTrash.ConfirmModal = ConfirmModal; MoveToTrash.ConfirmModal = MoveToTrashConfirm;

View File

@@ -5,14 +5,14 @@ import { ViewLayersIcon } from '@blocksuite/icons';
import { Button } from '@toeverything/components/button'; import { Button } from '@toeverything/components/button';
import { Tooltip } from '@toeverything/components/tooltip'; import { Tooltip } from '@toeverything/components/tooltip';
import clsx from 'clsx'; import clsx from 'clsx';
import { useCallback, useState } from 'react'; import { useState } from 'react';
import { import {
type CollectionsAtom, type CollectionsAtom,
useCollectionManager, useCollectionManager,
} from '../use-collection-manager'; } from '../use-collection-manager';
import * as styles from './collection-bar.css'; import * as styles from './collection-bar.css';
import { EditCollectionModel } from './create-collection'; import { EditCollectionModal } from './create-collection';
import { useActions } from './use-action'; import { useActions } from './use-action';
interface CollectionBarProps { interface CollectionBarProps {
@@ -33,21 +33,20 @@ export const CollectionBar = (props: CollectionBarProps) => {
setting, setting,
openEdit: () => setOpen(true), openEdit: () => setOpen(true),
}); });
const onClose = useCallback(() => setOpen(false), []);
return !setting.isDefault ? ( return !setting.isDefault ? (
<tr style={{ userSelect: 'none' }}> <tr style={{ userSelect: 'none' }}>
<td> <td>
<div className={styles.view}> <div className={styles.view}>
<EditCollectionModel <EditCollectionModal
propertiesMeta={propertiesMeta} propertiesMeta={propertiesMeta}
getPageInfo={getPageInfo} getPageInfo={getPageInfo}
init={collection} init={collection}
open={open} open={open}
onClose={onClose} onOpenChange={setOpen}
// eslint-disable-next-line @typescript-eslint/no-misused-promises // eslint-disable-next-line @typescript-eslint/no-misused-promises
onConfirm={setting.updateCollection} onConfirm={setting.updateCollection}
></EditCollectionModel> />
<ViewLayersIcon <ViewLayersIcon
style={{ style={{
height: 20, height: 20,

View File

@@ -14,7 +14,7 @@ import { useCallback, useRef, useState } from 'react';
import { CreateFilterMenu } from '../filter/vars'; import { CreateFilterMenu } from '../filter/vars';
import type { useCollectionManager } from '../use-collection-manager'; import type { useCollectionManager } from '../use-collection-manager';
import * as styles from './collection-list.css'; import * as styles from './collection-list.css';
import { EditCollectionModel } from './create-collection'; import { EditCollectionModal } from './create-collection';
import { useActions } from './use-action'; import { useActions } from './use-action';
const CollectionOption = ({ const CollectionOption = ({
@@ -126,14 +126,16 @@ export const CollectionList = ({
}, },
[setting] [setting]
); );
const closeUpdateCollectionModal = useCallback( const closeUpdateCollectionModal = useCallback((open: boolean) => {
() => setCollection(undefined), if (!open) {
[] setCollection(undefined);
); }
}, []);
const onConfirm = useCallback( const onConfirm = useCallback(
async (view: Collection) => { async (view: Collection) => {
await setting.updateCollection(view); await setting.updateCollection(view);
closeUpdateCollectionModal(); closeUpdateCollectionModal(false);
}, },
[closeUpdateCollectionModal, setting] [closeUpdateCollectionModal, setting]
); );
@@ -214,14 +216,14 @@ export const CollectionList = ({
{t['com.affine.filter']()} {t['com.affine.filter']()}
</Button> </Button>
</Menu> </Menu>
<EditCollectionModel <EditCollectionModal
propertiesMeta={propertiesMeta} propertiesMeta={propertiesMeta}
getPageInfo={getPageInfo} getPageInfo={getPageInfo}
init={collection} init={collection}
open={!!collection} open={!!collection}
onClose={closeUpdateCollectionModal} onOpenChange={closeUpdateCollectionModal}
onConfirm={onConfirm} onConfirm={onConfirm}
></EditCollectionModel> />
</FlexWrapper> </FlexWrapper>
); );
}; };

View File

@@ -10,72 +10,69 @@ import {
} from '@blocksuite/icons'; } from '@blocksuite/icons';
import { uuidv4 } from '@blocksuite/store'; import { uuidv4 } from '@blocksuite/store';
import { Button } from '@toeverything/components/button'; import { Button } from '@toeverything/components/button';
import { Modal } from '@toeverything/components/modal';
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { import { Input, ScrollableContainer } from '../../..';
Input,
Modal,
ModalCloseButton,
ModalWrapper,
ScrollableContainer,
} from '../../..';
import { FilterList } from '../filter'; import { FilterList } from '../filter';
import * as styles from './collection-list.css'; import * as styles from './collection-list.css';
interface EditCollectionModelProps { interface EditCollectionModalProps {
init?: Collection; init?: Collection;
title?: string; title?: string;
open: boolean; open: boolean;
getPageInfo: GetPageInfoById; getPageInfo: GetPageInfoById;
propertiesMeta: PropertiesMeta; propertiesMeta: PropertiesMeta;
onClose: () => void; onOpenChange: (open: boolean) => void;
onConfirm: (view: Collection) => Promise<void>; onConfirm: (view: Collection) => Promise<void>;
} }
export const EditCollectionModel = ({ export const EditCollectionModal = ({
init, init,
onConfirm, onConfirm,
open, open,
onClose, onOpenChange,
getPageInfo, getPageInfo,
propertiesMeta, propertiesMeta,
title, title,
}: EditCollectionModelProps) => { }: EditCollectionModalProps) => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const onConfirmOnCollection = useCallback( const onConfirmOnCollection = useCallback(
(view: Collection) => { (view: Collection) => {
onConfirm(view) onConfirm(view)
.then(() => { .then(() => {
onClose(); onOpenChange(false);
}) })
.catch(err => { .catch(err => {
console.error(err); console.error(err);
}); });
}, },
[onClose, onConfirm] [onConfirm, onOpenChange]
); );
const onCancel = useCallback(() => {
onOpenChange(false);
}, [onOpenChange]);
return ( return (
<Modal open={open} onClose={onClose}> <Modal
<ModalWrapper open={open}
width={600} onOpenChange={onOpenChange}
style={{ width={600}
padding: '40px', contentOptions={{
background: 'var(--affine-background-primary-color)', style: { padding: '40px' },
}} }}
> >
<ModalCloseButton top={12} right={12} onClick={onClose} /> {init ? (
{init ? ( <EditCollection
<EditCollection propertiesMeta={propertiesMeta}
propertiesMeta={propertiesMeta} title={title}
title={title} onConfirmText={t['com.affine.editCollection.save']()}
onConfirmText={t['com.affine.editCollection.save']()} init={init}
init={init} getPageInfo={getPageInfo}
getPageInfo={getPageInfo} onCancel={onCancel}
onCancel={onClose} onConfirm={onConfirmOnCollection}
onConfirm={onConfirmOnCollection} />
/> ) : null}
) : null}
</ModalWrapper>
</Modal> </Modal>
); );
}; };
@@ -301,14 +298,14 @@ export const SaveCollectionButton = ({
> >
{t['com.affine.editCollection.saveCollection']()} {t['com.affine.editCollection.saveCollection']()}
</Button> </Button>
<EditCollectionModel <EditCollectionModal
title={t['com.affine.editCollection.saveCollection']()} title={t['com.affine.editCollection.saveCollection']()}
propertiesMeta={propertiesMeta} propertiesMeta={propertiesMeta}
init={init} init={init}
onConfirm={onConfirm} onConfirm={onConfirm}
open={show} open={show}
getPageInfo={getPageInfo} getPageInfo={getPageInfo}
onClose={() => changeShow(false)} onOpenChange={changeShow}
/> />
</> </>
); );

View File

@@ -1,4 +1,3 @@
export { SettingModal, type SettingModalProps } from './modal';
export { SettingHeader } from './setting-header'; export { SettingHeader } from './setting-header';
export { SettingRow } from './setting-row'; export { SettingRow } from './setting-row';
export * from './storage-progess'; export * from './storage-progess';

View File

@@ -1,43 +0,0 @@
import { Modal, ModalCloseButton, ModalWrapper } from '@affine/component';
import type { PropsWithChildren } from 'react';
import { useCallback } from 'react';
export interface SettingModalProps {
open: boolean;
setOpen: (value: boolean) => void;
}
export const SettingModal = ({
children,
open,
setOpen,
}: PropsWithChildren<SettingModalProps>) => {
const handleClose = useCallback(() => {
setOpen(false);
}, [setOpen]);
return (
<Modal
open={open}
onClose={handleClose}
wrapperPosition={['center', 'center']}
data-testid="setting-modal"
>
<ModalWrapper
width={1080}
height={760}
style={{
maxHeight: '85vh',
maxWidth: '70vw',
overflow: 'hidden',
display: 'flex',
backgroundColor: 'var(--affine-background-overlay-panel-color)',
boxShadow: 'var(--affine-popover-shadow)',
}}
>
<ModalCloseButton top={16} right={20} onClick={handleClose} />
{children}
</ModalWrapper>
</Modal>
);
};

View File

@@ -1,57 +1,24 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { CloseIcon } from '@blocksuite/icons'; import {
import { Button, IconButton } from '@toeverything/components/button'; ConfirmModal,
type ConfirmModalProps,
} from '@toeverything/components/modal';
import { Modal, ModalWrapper } from '../../..'; export const PublicLinkDisableModal = (props: ConfirmModalProps) => {
import { ButtonContainer, Content, Header, StyleTips, Title } from './style';
export type PublicLinkDisableProps = {
open: boolean;
onConfirmDisable: () => void;
onClose: () => void;
};
export const PublicLinkDisableModal = ({
open,
onConfirmDisable,
onClose,
}: PublicLinkDisableProps) => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
return ( return (
<Modal open={open} onClose={onClose}> <ConfirmModal
<ModalWrapper width={480}> width={480}
<Header> title={t['com.affine.publicLinkDisableModal.title']()}
<Title>{t['com.affine.publicLinkDisableModal.title']()}</Title> description={t['com.affine.publicLinkDisableModal.description']()}
<IconButton onClick={onClose}> cancelText={t['com.affine.publicLinkDisableModal.button.cancel']()}
<CloseIcon /> confirmButtonOptions={{
</IconButton> type: 'error',
</Header> ['data-testid' as string]: 'confirm-enable-affine-cloud-button',
<Content> children: t['com.affine.publicLinkDisableModal.button.disable'](),
<StyleTips> }}
{t['com.affine.publicLinkDisableModal.description']()} {...props}
</StyleTips> />
<ButtonContainer>
<div>
<Button onClick={onClose} block>
{t['com.affine.publicLinkDisableModal.button.cancel']()}
</Button>
</div>
<div>
<Button
data-testid="confirm-enable-affine-cloud-button"
type="error"
block
onClick={() => {
onConfirmDisable();
onClose();
}}
>
{t['com.affine.publicLinkDisableModal.button.disable']()}
</Button>
</div>
</ButtonContainer>
</Content>
</ModalWrapper>
</Modal>
); );
}; };

View File

@@ -1,35 +0,0 @@
import { styled } from '../../..';
export const Header = styled('div')({
display: 'flex',
justifyContent: 'space-between',
paddingRight: '20px',
paddingTop: '20px',
paddingLeft: '24px',
alignItems: 'center',
});
export const Content = styled('div')({
padding: '12px 24px 20px 24px',
});
export const Title = styled('div')({
fontSize: 'var(--affine-font-h6)',
lineHeight: '26px',
fontWeight: 600,
});
export const StyleTips = styled('div')(() => {
return {
userSelect: 'none',
marginBottom: '20px',
};
});
export const ButtonContainer = styled('div')(() => {
return {
display: 'flex',
justifyContent: 'flex-end',
gap: '20px',
paddingTop: '20px',
};
});

View File

@@ -208,7 +208,10 @@ export const AffineSharePage = (props: ShareMenuProps) => {
block block
type="danger" type="danger"
className={styles.menuItemStyle} className={styles.menuItemStyle}
onClick={() => setShowDisable(true)} onSelect={e => {
e.preventDefault();
setShowDisable(true);
}}
> >
<div className={styles.disableSharePage}> <div className={styles.disableSharePage}>
{t['Disable Public Link']()} {t['Disable Public Link']()}
@@ -216,10 +219,8 @@ export const AffineSharePage = (props: ShareMenuProps) => {
</MenuItem> </MenuItem>
<PublicLinkDisableModal <PublicLinkDisableModal
open={showDisable} open={showDisable}
onConfirmDisable={onDisablePublic} onConfirm={onDisablePublic}
onClose={() => { onOpenChange={setShowDisable}
setShowDisable(false);
}}
/> />
</> </>
) : null} ) : null}
@@ -231,7 +232,11 @@ export const SharePage = (props: ShareMenuProps) => {
if (props.workspace.flavour === WorkspaceFlavour.LOCAL) { if (props.workspace.flavour === WorkspaceFlavour.LOCAL) {
return <LocalSharePage {...props} />; return <LocalSharePage {...props} />;
} else if (props.workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD) { } else if (props.workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD) {
return <AffineSharePage {...props} />; return (
<>
<AffineSharePage {...props} />
</>
);
} }
throw new Error('Unreachable'); throw new Error('Unreachable');
}; };

View File

@@ -5,7 +5,6 @@ export const modalStyle = style({
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
alignItems: 'center', alignItems: 'center',
position: 'relative',
backgroundColor: 'var(--affine-background-secondary-color)', backgroundColor: 'var(--affine-background-secondary-color)',
borderRadius: '16px', borderRadius: '16px',
overflow: 'hidden', overflow: 'hidden',

View File

@@ -1,9 +1,9 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ArrowLeftSmallIcon, ArrowRightSmallIcon } from '@blocksuite/icons'; import { ArrowLeftSmallIcon, ArrowRightSmallIcon } from '@blocksuite/icons';
import { Modal, type ModalProps } from '@toeverything/components/modal';
import clsx from 'clsx'; import clsx from 'clsx';
import { useState } from 'react'; import { useState } from 'react';
import { Modal, ModalCloseButton, ModalWrapper } from '../..';
import { import {
arrowStyle, arrowStyle,
buttonDisableStyle, buttonDisableStyle,
@@ -25,140 +25,133 @@ import {
videoStyle, videoStyle,
} from './index.css'; } from './index.css';
export interface TourModalProps { export const TourModal = (props: ModalProps) => {
open: boolean;
onClose: () => void;
}
export const TourModal = ({ open, onClose }: TourModalProps) => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const [step, setStep] = useState(-1); const [step, setStep] = useState(-1);
const handleClose = () => {
setStep(-1);
onClose();
};
return ( return (
<Modal <Modal
open={open} width={545}
onClose={handleClose} contentOptions={{
wrapperPosition={['center', 'center']} ['data-testid' as string]: 'onboarding-modal',
hideBackdrop style: {
minHeight: '480px',
padding: 0,
},
}}
overlayOptions={{
style: {
background: 'transparent',
},
}}
closeButtonOptions={{
// @ts-expect-error - fix upstream type
'data-testid': 'onboarding-modal-close-button',
}}
{...props}
> >
<ModalWrapper <div className={modalStyle}>
width={545} <div className={titleContainerStyle}>
style={{ minHeight: '480px' }} {step !== -1 && (
data-testid="onboarding-modal"
>
<ModalCloseButton
top={6}
right={10}
onClick={handleClose}
data-testid="onboarding-modal-close-button"
/>
<div className={modalStyle}>
<div className={titleContainerStyle}>
{step !== -1 && (
<div
className={clsx(titleStyle, {
[slideToRightStyle]: step === 0,
[formSlideToLeftStyle]: step === 1,
})}
>
{t['com.affine.onboarding.title2']()}
</div>
)}
<div <div
className={clsx(titleStyle, { className={clsx(titleStyle, {
[slideToLeftStyle]: step === 1, [slideToRightStyle]: step === 0,
[formSlideToRightStyle]: step === 0, [formSlideToLeftStyle]: step === 1,
})} })}
> >
{t['com.affine.onboarding.title1']()} {t['com.affine.onboarding.title2']()}
</div> </div>
)}
<div
className={clsx(titleStyle, {
[slideToLeftStyle]: step === 1,
[formSlideToRightStyle]: step === 0,
})}
>
{t['com.affine.onboarding.title1']()}
</div> </div>
</div>
<div className={containerStyle}> <div className={containerStyle}>
<div <div
className={clsx(arrowStyle, { [buttonDisableStyle]: step !== 1 })} className={clsx(arrowStyle, { [buttonDisableStyle]: step !== 1 })}
onClick={() => step === 1 && setStep(0)} onClick={() => step === 1 && setStep(0)}
data-testid="onboarding-modal-pre-button" data-testid="onboarding-modal-pre-button"
> >
<ArrowLeftSmallIcon /> <ArrowLeftSmallIcon />
</div> </div>
<div className={videoContainerStyle}> <div className={videoContainerStyle}>
<div className={videoSlideStyle}> <div className={videoSlideStyle}>
{step !== -1 && ( {step !== -1 && (
<video
autoPlay
muted
loop
className={clsx(videoStyle, {
[slideToRightStyle]: step === 0,
[formSlideToLeftStyle]: step === 1,
})}
data-testid="onboarding-modal-editing-video"
>
<source src="/editingVideo.mp4" type="video/mp4" />
<source src="/editingVideo.webm" type="video/webm" />
</video>
)}
<video <video
autoPlay autoPlay
muted muted
loop loop
className={clsx(videoStyle, { className={clsx(videoStyle, {
[slideToLeftStyle]: step === 1, [slideToRightStyle]: step === 0,
[formSlideToRightStyle]: step === 0, [formSlideToLeftStyle]: step === 1,
})} })}
data-testid="onboarding-modal-switch-video" data-testid="onboarding-modal-editing-video"
> >
<source src="/switchVideo.mp4" type="video/mp4" /> <source src="/editingVideo.mp4" type="video/mp4" />
<source src="/switchVideo.webm" type="video/webm" /> <source src="/editingVideo.webm" type="video/webm" />
</video> </video>
</div> )}
</div> <video
<div autoPlay
className={clsx(arrowStyle, { [buttonDisableStyle]: step === 1 })} muted
onClick={() => setStep(1)} loop
data-testid="onboarding-modal-next-button" className={clsx(videoStyle, {
> [slideToLeftStyle]: step === 1,
<ArrowRightSmallIcon /> [formSlideToRightStyle]: step === 0,
})}
data-testid="onboarding-modal-switch-video"
>
<source src="/switchVideo.mp4" type="video/mp4" />
<source src="/switchVideo.webm" type="video/webm" />
</video>
</div> </div>
</div> </div>
<ul className={tabContainerStyle}> <div
<li className={clsx(arrowStyle, { [buttonDisableStyle]: step === 1 })}
className={clsx(tabStyle, { onClick={() => setStep(1)}
[tabActiveStyle]: step !== 1, data-testid="onboarding-modal-next-button"
})} >
onClick={() => setStep(0)} <ArrowRightSmallIcon />
></li>
<li
className={clsx(tabStyle, { [tabActiveStyle]: step === 1 })}
onClick={() => setStep(1)}
></li>
</ul>
<div className={descriptionContainerStyle}>
{step !== -1 && (
<div
className={clsx(descriptionStyle, {
[slideToRightStyle]: step === 0,
[formSlideToLeftStyle]: step === 1,
})}
>
{t['com.affine.onboarding.videoDescription2']()}
</div>
)}
<div
className={clsx(descriptionStyle, {
[slideToLeftStyle]: step === 1,
[formSlideToRightStyle]: step === 0,
})}
>
{t['com.affine.onboarding.videoDescription1']()}
</div>
</div> </div>
</div> </div>
</ModalWrapper> <ul className={tabContainerStyle}>
<li
className={clsx(tabStyle, {
[tabActiveStyle]: step !== 1,
})}
onClick={() => setStep(0)}
></li>
<li
className={clsx(tabStyle, { [tabActiveStyle]: step === 1 })}
onClick={() => setStep(1)}
></li>
</ul>
<div className={descriptionContainerStyle}>
{step !== -1 && (
<div
className={clsx(descriptionStyle, {
[slideToRightStyle]: step === 0,
[formSlideToLeftStyle]: step === 1,
})}
>
{t['com.affine.onboarding.videoDescription2']()}
</div>
)}
<div
className={clsx(descriptionStyle, {
[slideToLeftStyle]: step === 1,
[formSlideToRightStyle]: step === 0,
})}
>
{t['com.affine.onboarding.videoDescription1']()}
</div>
</div>
</div>
</Modal> </Modal>
); );
}; };

View File

@@ -2,12 +2,10 @@ export * from './components/list-skeleton';
export * from './styles'; export * from './styles';
export * from './ui/breadcrumbs'; export * from './ui/breadcrumbs';
export * from './ui/button'; export * from './ui/button';
export * from './ui/confirm';
export * from './ui/empty'; export * from './ui/empty';
export * from './ui/input'; export * from './ui/input';
export * from './ui/layout'; export * from './ui/layout';
export * from './ui/menu'; export * from './ui/menu';
export * from './ui/modal';
export * from './ui/mui'; export * from './ui/mui';
export * from './ui/popper'; export * from './ui/popper';
export * from './ui/scrollbar'; export * from './ui/scrollbar';

View File

@@ -1,110 +0,0 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { Button } from '@toeverything/components/button';
import { useCallback, useMemo } from 'react';
import type { ModalProps } from '../modal';
import { Modal, ModalCloseButton } from '../modal';
import {
StyledColumnButtonWrapper,
StyledConfirmContent,
StyledConfirmTitle,
StyledModalWrapper,
StyledRowButtonWrapper,
} from './styles';
export type ConfirmProps = {
title?: string;
content?: string;
confirmText?: string;
cancelText?: string;
// TODO: Confirm button's color should depend on confirm type
confirmType?: 'primary' | 'warning' | 'error';
buttonDirection?: 'row' | 'column';
onConfirm?: () => void;
onCancel?: () => void;
cancelButtonTestId?: string;
confirmButtonTestId?: string;
} & Omit<ModalProps, 'children'>;
export const Confirm = ({
title,
content,
confirmText,
confirmType,
onConfirm,
onCancel,
buttonDirection = 'row',
cancelText = 'Cancel',
open,
cancelButtonTestId = '',
confirmButtonTestId = '',
}: ConfirmProps) => {
const t = useAFFiNEI18N();
const cancelText_ = useMemo<string>(() => {
return cancelText === 'Cancel'
? t['com.affine.confirmModal.button.cancel']()
: cancelText;
}, [cancelText, t]);
const handleCancel = useCallback(() => {
onCancel?.();
}, [onCancel]);
const handleConfirm = useCallback(() => {
onConfirm?.();
}, [onConfirm]);
return (
<Modal open={open} disablePortal={false}>
<StyledModalWrapper>
<ModalCloseButton onClick={handleCancel} />
<StyledConfirmTitle>{title}</StyledConfirmTitle>
<StyledConfirmContent>{content}</StyledConfirmContent>
{buttonDirection === 'row' ? (
<StyledRowButtonWrapper>
<Button
onClick={handleCancel}
size="large"
style={{ marginRight: '24px' }}
data-testid={cancelButtonTestId}
>
{cancelText_}
</Button>
<Button
type={confirmType}
onClick={handleConfirm}
size="large"
data-testid={confirmButtonTestId}
>
{confirmText}
</Button>
</StyledRowButtonWrapper>
) : (
<StyledColumnButtonWrapper>
<Button
type={confirmType}
onClick={handleConfirm}
style={{ width: '284px', height: '38px', textAlign: 'center' }}
data-testid={confirmButtonTestId}
>
{confirmText}
</Button>
<Button
onClick={handleCancel}
style={{
marginTop: '16px',
width: '284px',
height: '38px',
textAlign: 'center',
}}
data-testid={cancelButtonTestId}
>
{cancelText_}
</Button>
</StyledColumnButtonWrapper>
)}
</StyledModalWrapper>
</Modal>
);
};
export default Confirm;

View File

@@ -1 +0,0 @@
export * from './confirm';

View File

@@ -1,46 +0,0 @@
import { displayFlex, styled } from '../../styles';
import { ModalWrapper } from '../modal';
export const StyledModalWrapper = styled(ModalWrapper)(() => {
return {
minWidth: '460px',
maxWidth: '560px',
maxHeight: '292px',
padding: '44px 84px 32px 84px',
overflow: 'auto',
};
});
export const StyledConfirmTitle = styled('div')(() => {
return {
fontSize: 'var(--affine-font-h6)',
fontWeight: 600,
textAlign: 'center',
lineHeight: '28px',
};
});
export const StyledConfirmContent = styled('div')(() => {
return {
fontSize: 'var(--affine-font-base)',
textAlign: 'center',
marginTop: '12px',
color: 'var(--affine-text-primary-color)',
lineHeight: '26px',
};
});
export const StyledColumnButtonWrapper = styled('div')(() => {
return {
...displayFlex('center', 'center'),
flexDirection: 'column',
marginTop: '32px',
};
});
export const StyledRowButtonWrapper = styled('div')(() => {
return {
...displayFlex('center', 'center'),
flexDirection: 'row',
marginTop: '32px',
};
});

View File

@@ -1,72 +0,0 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { Button, type ButtonType } from '@toeverything/components/button';
import { useCallback } from 'react';
import { Modal, type ModalProps } from './modal';
import { ModalCloseButton } from './modal-close-button';
import { ModalWrapper } from './modal-wrapper';
import {
StyledModalContent,
StyledModalFooter,
StyledModalTitle,
} from './styles';
export interface BaseModalProps
extends Omit<ModalProps, 'onClose' | 'children'> {
title?: string;
content?: string;
confirmText?: string;
confirmType?: ButtonType;
onClose: () => void;
onCancel: () => void;
onConfirm: () => void;
}
export const ConfirmModal = ({
open,
onClose,
confirmText,
confirmType = 'primary',
onCancel,
onConfirm,
title,
content,
}: BaseModalProps) => {
const t = useAFFiNEI18N();
const handleClose = useCallback(() => {
onClose();
}, [onClose]);
return (
<Modal
open={open}
onClose={handleClose}
wrapperPosition={['center', 'center']}
data-testid="auth-modal"
>
<ModalWrapper
width={480}
minHeight={194}
style={{
overflow: 'hidden',
backgroundColor: 'var(--affine-white)',
boxShadow: 'var(--affine-popover-shadow)',
padding: '20px 24px 0',
}}
>
<ModalCloseButton top={22} right={20} onClick={handleClose} />
<StyledModalTitle>{title}</StyledModalTitle>
<StyledModalContent>{content}</StyledModalContent>
<StyledModalFooter>
<Button onClick={onCancel} style={{ marginRight: 20 }}>
{t['com.affine.confirmModal.button.cancel']()}
</Button>
<Button type={confirmType} onClick={onConfirm}>
{confirmText || t['Confirm']()}
</Button>
</StyledModalFooter>
</ModalWrapper>
</Modal>
);
};

View File

@@ -1,8 +0,0 @@
import Modal from './modal';
export * from './confirm-modal';
export * from './modal';
export * from './modal-close-button';
export * from './modal-wrapper';
export default Modal;

View File

@@ -1,39 +0,0 @@
import { CloseIcon } from '@blocksuite/icons';
import {
IconButton,
type IconButtonProps,
} from '@toeverything/components/button';
import type { HTMLAttributes } from 'react';
export type ModalCloseButtonProps = {
top?: number;
right?: number;
absolute?: boolean;
} & Omit<IconButtonProps, 'children'> &
HTMLAttributes<HTMLButtonElement>;
export const ModalCloseButton = ({
absolute = true,
right,
top,
...props
}: ModalCloseButtonProps) => {
return (
<IconButton
style={
absolute
? {
position: 'absolute',
top: top ?? 24,
right: right ?? 40,
zIndex: 1,
}
: {}
}
data-testid="modal-close-button"
{...props}
>
<CloseIcon />
</IconButton>
);
};

View File

@@ -1,22 +0,0 @@
import type { CSSProperties } from 'react';
import { styled } from '../../styles';
export const ModalWrapper = styled('div')<{
width?: CSSProperties['width'];
height?: CSSProperties['height'];
minHeight?: CSSProperties['minHeight'];
}>(({ width, height, minHeight }) => {
return {
width,
height,
minHeight,
backgroundColor: 'var(--affine-background-overlay-panel-color)',
boxShadow: 'var(--affine-shadow-3)',
borderRadius: '12px',
position: 'relative',
maxHeight: 'calc(100vh - 32px)',
};
});
export default ModalWrapper;

View File

@@ -1,59 +0,0 @@
import type { ModalProps as ModalUnstyledOwnProps } from '@mui/base/Modal';
import Fade from '@mui/material/Fade';
import { StyledBackdrop, StyledModal } from './styles';
const Backdrop = ({
open,
...other
}: {
open?: boolean;
className: string;
}) => {
return (
<Fade in={open}>
<StyledBackdrop {...other} />
</Fade>
);
};
export type ModalProps = {
wrapperPosition?: ['top' | 'bottom' | 'center', 'left' | 'right' | 'center'];
} & ModalUnstyledOwnProps;
const transformConfig = {
top: 'flex-start',
bottom: 'flex-end',
center: 'center',
left: 'flex-start',
right: 'flex-end',
};
export const Modal = (props: ModalProps) => {
const {
wrapperPosition = ['center', 'center'],
open,
children,
...otherProps
} = props;
const [vertical, horizontal] = wrapperPosition;
// Fixme: This is a workaround for Mui bug
// Refs: https://github.com/mui/material-ui/issues/33748
if (!open) {
return null;
}
return (
<StyledModal
{...otherProps}
open={open}
slots={{ backdrop: Backdrop }}
alignItems={transformConfig[vertical]}
justifyContent={transformConfig[horizontal]}
disableEnforceFocus
>
<Fade in={open}>{children}</Fade>
</StyledModal>
);
};
export default Modal;

View File

@@ -1,65 +0,0 @@
import { Modal as ModalUnstyled } from '@mui/base/Modal';
import type { CSSProperties } from 'react';
import { styled } from '../../styles';
export const StyledBackdrop = styled('div')(() => {
return {
zIndex: '-1',
position: 'fixed',
right: '0',
bottom: '0',
top: '0',
left: '0',
backgroundColor: 'var(--affine-background-modal-color)',
};
});
export const StyledModal = styled(ModalUnstyled, {
shouldForwardProp: prop => {
return !['justifyContent', 'alignItems'].includes(prop as string);
},
})<{
alignItems: CSSProperties['alignItems'];
justifyContent: CSSProperties['justifyContent'];
}>(({ alignItems, justifyContent }) => {
return {
width: '100vw',
height: '100vh',
display: 'flex',
alignItems,
justifyContent,
position: 'fixed',
left: '0',
top: '0',
zIndex: 'var(--affine-z-index-modal)',
WebkitAppRegion: 'no-drag',
'*': {
WebkitTapHighlightColor: 'transparent',
outline: 'none',
},
};
});
export const StyledModalFooter = styled('div')(() => {
return {
marginTop: 40,
display: 'flex',
justifyContent: 'flex-end',
alignItems: 'center',
};
});
export const StyledModalTitle = styled('div')(() => {
return {
fontWeight: 600,
fontSize: 'var(--affine-font-h-6)',
};
});
export const StyledModalContent = styled('div')(() => {
return {
fontSize: 'var(--affine-font-base)',
lineHeight: '24px',
marginTop: '12px',
};
});

View File

@@ -16,7 +16,7 @@
"dependencies": { "dependencies": {
"@affine/component": "workspace:*", "@affine/component": "workspace:*",
"@affine/sdk": "workspace:*", "@affine/sdk": "workspace:*",
"@toeverything/components": "^0.0.34", "@toeverything/components": "^0.0.38",
"idb": "^7.1.1", "idb": "^7.1.1",
"langchain": "^0.0.138", "langchain": "^0.0.138",
"marked": "^7.0.5", "marked": "^7.0.5",

View File

@@ -18,7 +18,7 @@
"@affine/component": "workspace:*", "@affine/component": "workspace:*",
"@affine/sdk": "workspace:*", "@affine/sdk": "workspace:*",
"@blocksuite/icons": "^2.1.33", "@blocksuite/icons": "^2.1.33",
"@toeverything/components": "^0.0.34" "@toeverything/components": "^0.0.38"
}, },
"devDependencies": { "devDependencies": {
"@affine/plugin-cli": "workspace:*" "@affine/plugin-cli": "workspace:*"

View File

@@ -17,7 +17,7 @@
"@affine/component": "workspace:*", "@affine/component": "workspace:*",
"@affine/sdk": "workspace:*", "@affine/sdk": "workspace:*",
"@blocksuite/icons": "^2.1.33", "@blocksuite/icons": "^2.1.33",
"@toeverything/components": "^0.0.34", "@toeverything/components": "^0.0.38",
"@toeverything/theme": "^0.7.15", "@toeverything/theme": "^0.7.15",
"clsx": "^2.0.0", "clsx": "^2.0.0",
"foxact": "^0.2.20", "foxact": "^0.2.20",

View File

@@ -18,7 +18,7 @@
"@affine/component": "workspace:*", "@affine/component": "workspace:*",
"@affine/sdk": "workspace:*", "@affine/sdk": "workspace:*",
"@blocksuite/icons": "^2.1.33", "@blocksuite/icons": "^2.1.33",
"@toeverything/components": "^0.0.34" "@toeverything/components": "^0.0.38"
}, },
"devDependencies": { "devDependencies": {
"@affine/plugin-cli": "workspace:*", "@affine/plugin-cli": "workspace:*",

View File

@@ -231,7 +231,7 @@ __metadata:
"@affine/component": "workspace:*" "@affine/component": "workspace:*"
"@affine/plugin-cli": "workspace:*" "@affine/plugin-cli": "workspace:*"
"@affine/sdk": "workspace:*" "@affine/sdk": "workspace:*"
"@toeverything/components": ^0.0.34 "@toeverything/components": ^0.0.38
"@types/marked": ^5.0.1 "@types/marked": ^5.0.1
idb: ^7.1.1 idb: ^7.1.1
jotai: ^2.4.1 jotai: ^2.4.1
@@ -282,7 +282,7 @@ __metadata:
"@sentry/webpack-plugin": ^2.7.0 "@sentry/webpack-plugin": ^2.7.0
"@svgr/webpack": ^8.1.0 "@svgr/webpack": ^8.1.0
"@swc/core": ^1.3.81 "@swc/core": ^1.3.81
"@toeverything/components": ^0.0.34 "@toeverything/components": ^0.0.38
"@types/lodash-es": ^4.17.9 "@types/lodash-es": ^4.17.9
"@types/webpack-env": ^1.18.1 "@types/webpack-env": ^1.18.1
async-call-rpc: ^6.3.1 async-call-rpc: ^6.3.1
@@ -458,7 +458,7 @@ __metadata:
"@affine/plugin-cli": "workspace:*" "@affine/plugin-cli": "workspace:*"
"@affine/sdk": "workspace:*" "@affine/sdk": "workspace:*"
"@blocksuite/icons": ^2.1.33 "@blocksuite/icons": ^2.1.33
"@toeverything/components": ^0.0.34 "@toeverything/components": ^0.0.38
languageName: unknown languageName: unknown
linkType: soft linkType: soft
@@ -484,7 +484,7 @@ __metadata:
"@affine/plugin-cli": "workspace:*" "@affine/plugin-cli": "workspace:*"
"@affine/sdk": "workspace:*" "@affine/sdk": "workspace:*"
"@blocksuite/icons": ^2.1.33 "@blocksuite/icons": ^2.1.33
"@toeverything/components": ^0.0.34 "@toeverything/components": ^0.0.38
"@toeverything/theme": ^0.7.15 "@toeverything/theme": ^0.7.15
clsx: ^2.0.0 clsx: ^2.0.0
foxact: ^0.2.20 foxact: ^0.2.20
@@ -580,7 +580,7 @@ __metadata:
"@affine/plugin-cli": "workspace:*" "@affine/plugin-cli": "workspace:*"
"@affine/sdk": "workspace:*" "@affine/sdk": "workspace:*"
"@blocksuite/icons": ^2.1.33 "@blocksuite/icons": ^2.1.33
"@toeverything/components": ^0.0.34 "@toeverything/components": ^0.0.38
jotai: ^2.4.1 jotai: ^2.4.1
react: 18.2.0 react: 18.2.0
react-dom: 18.2.0 react-dom: 18.2.0
@@ -9108,6 +9108,39 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@radix-ui/react-dialog@npm:^1.0.4":
version: 1.0.4
resolution: "@radix-ui/react-dialog@npm:1.0.4"
dependencies:
"@babel/runtime": ^7.13.10
"@radix-ui/primitive": 1.0.1
"@radix-ui/react-compose-refs": 1.0.1
"@radix-ui/react-context": 1.0.1
"@radix-ui/react-dismissable-layer": 1.0.4
"@radix-ui/react-focus-guards": 1.0.1
"@radix-ui/react-focus-scope": 1.0.3
"@radix-ui/react-id": 1.0.1
"@radix-ui/react-portal": 1.0.3
"@radix-ui/react-presence": 1.0.1
"@radix-ui/react-primitive": 1.0.3
"@radix-ui/react-slot": 1.0.2
"@radix-ui/react-use-controllable-state": 1.0.1
aria-hidden: ^1.1.1
react-remove-scroll: 2.5.5
peerDependencies:
"@types/react": "*"
"@types/react-dom": "*"
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
"@types/react-dom":
optional: true
checksum: 01ad549a3685e221628950e6fbec306494170aa3b92cbe00732b1531c16e1cf681138cd4a79d658f4f97d4096676a40d08642090fdea1675d0b7dc78df66d962
languageName: node
linkType: hard
"@radix-ui/react-direction@npm:1.0.0": "@radix-ui/react-direction@npm:1.0.0":
version: 1.0.0 version: 1.0.0
resolution: "@radix-ui/react-direction@npm:1.0.0" resolution: "@radix-ui/react-direction@npm:1.0.0"
@@ -12447,11 +12480,12 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@toeverything/components@npm:^0.0.34": "@toeverything/components@npm:^0.0.38":
version: 0.0.34 version: 0.0.38
resolution: "@toeverything/components@npm:0.0.34" resolution: "@toeverything/components@npm:0.0.38"
dependencies: dependencies:
"@blocksuite/icons": ^2.1.33 "@blocksuite/icons": ^2.1.33
"@radix-ui/react-dialog": ^1.0.4
"@radix-ui/react-dropdown-menu": ^2.0.5 "@radix-ui/react-dropdown-menu": ^2.0.5
"@radix-ui/react-tooltip": ^1.0.6 "@radix-ui/react-tooltip": ^1.0.6
peerDependencies: peerDependencies:
@@ -12459,7 +12493,7 @@ __metadata:
clsx: ^2 clsx: ^2
react: ^18 react: ^18
react-dom: ^18 react-dom: ^18
checksum: 240fadd8101d92eb2da6700372a2cb87516479999943628c7d422d3cfeb3f330a15096eed470d650d207bd1a132e4115e1277b13d59a6f4994827e231e9cf5f3 checksum: 8771f0439be2db0acffa1d6f73a2c49155acf35c5a0538c3391f98f5dfc7961f6e88e48dfc9e5dae2847b3a31a720a58dc79075db024758b74b72bcfe15eadd0
languageName: node languageName: node
linkType: hard linkType: hard