feat!: affine cloud support (#3813)

Co-authored-by: Hongtao Lye <codert.sn@gmail.com>
Co-authored-by: liuyi <forehalo@gmail.com>
Co-authored-by: LongYinan <lynweklm@gmail.com>
Co-authored-by: X1a0t <405028157@qq.com>
Co-authored-by: JimmFly <yangjinfei001@gmail.com>
Co-authored-by: Peng Xiao <pengxiao@outlook.com>
Co-authored-by: xiaodong zuo <53252747+zuoxiaodong0815@users.noreply.github.com>
Co-authored-by: DarkSky <25152247+darkskygit@users.noreply.github.com>
Co-authored-by: Qi <474021214@qq.com>
Co-authored-by: danielchim <kahungchim@gmail.com>
This commit is contained in:
Alex Yang
2023-08-29 05:07:05 -05:00
committed by GitHub
parent d0145c6f38
commit 2f6c4e3696
414 changed files with 19469 additions and 7591 deletions

View File

@@ -0,0 +1,14 @@
import clsx from 'clsx';
import type { FC, HTMLAttributes, PropsWithChildren } from 'react';
import { authContent } from './share.css';
export const AuthContent: FC<
PropsWithChildren & HTMLAttributes<HTMLDivElement>
> = ({ children, className, ...otherProps }) => {
return (
<div className={clsx(authContent, className)} {...otherProps}>
{children}
</div>
);
};

View File

@@ -0,0 +1,49 @@
import clsx from 'clsx';
import type { FC } from 'react';
import { Input, type InputProps } from '../../ui/input';
import { authInputWrapper, formHint } from './share.css';
export type AuthInputProps = InputProps & {
label?: string;
error?: boolean;
errorHint?: string;
withoutHint?: boolean;
onEnter?: () => void;
};
export const AuthInput: FC<AuthInputProps> = ({
label,
error,
errorHint,
withoutHint = false,
onEnter,
...inputProps
}) => {
return (
<div
className={clsx(authInputWrapper, {
'without-hint': withoutHint,
})}
>
{label ? <label>{label}</label> : null}
<Input
size="extraLarge"
status={error ? 'error' : 'default'}
onKeyDown={e => {
if (e.key === 'Enter') {
onEnter?.();
}
}}
{...inputProps}
/>
{error && errorHint && !withoutHint ? (
<div
className={clsx(formHint, {
error: error,
})}
>
{errorHint}
</div>
) : null}
</div>
);
};

View File

@@ -0,0 +1,32 @@
import type { FC, PropsWithChildren, ReactNode } from 'react';
import { Empty } from '../../ui/empty';
import { Wrapper } from '../../ui/layout';
import { Logo } from './logo';
import { authPageContainer } from './share.css';
export const AuthPageContainer: FC<
PropsWithChildren<{ title?: ReactNode; subtitle?: ReactNode }>
> = ({ children, title, subtitle }) => {
return (
<div className={authPageContainer}>
<Wrapper
style={{
position: 'absolute',
top: 25,
left: 20,
}}
>
<Logo />
</Wrapper>
<div className="wrapper">
<div className="content">
<p className="title">{title}</p>
<p className="subtitle">{subtitle}</p>
{children}
</div>
<Empty />
</div>
</div>
);
};

View File

@@ -0,0 +1,24 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ArrowLeftSmallIcon } from '@blocksuite/icons';
import { Button, type ButtonProps } from '@toeverything/components/button';
import { type FC } from 'react';
export const BackButton: FC<ButtonProps> = props => {
const t = useAFFiNEI18N();
return (
<Button
type="plain"
style={{
marginTop: 12,
marginLeft: -5,
paddingLeft: 0,
paddingRight: 5,
color: 'var(--affine-text-secondary-color)',
}}
icon={<ArrowLeftSmallIcon />}
{...props}
>
{t['Back Home']()}
</Button>
);
};

View File

@@ -0,0 +1,89 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { Button } from '@toeverything/components/button';
import type { FC } from 'react';
import { useCallback, useState } from 'react';
import { AuthInput } from './auth-input';
import { AuthPageContainer } from './auth-page-container';
import { emailRegex } from './utils';
type User = {
id: string;
name: string;
email: string;
image: string;
};
export const ChangeEmailPage: FC<{
user: User;
onChangeEmail: (email: string) => Promise<boolean>;
onOpenAffine: () => void;
}> = ({ onChangeEmail: propsOnChangeEmail, onOpenAffine }) => {
const t = useAFFiNEI18N();
const [hasSetUp, setHasSetUp] = useState(false);
const [email, setEmail] = useState('');
const [isValidEmail, setIsValidEmail] = useState(true);
const [loading, setLoading] = useState(false);
const onContinue = useCallback(
() =>
void (async () => {
if (!emailRegex.test(email)) {
setIsValidEmail(false);
return;
}
setIsValidEmail(true);
setLoading(true);
const setup = await propsOnChangeEmail(email);
setLoading(false);
setHasSetUp(setup);
})(),
[email, propsOnChangeEmail]
);
const onEmailChange = useCallback((value: string) => {
setEmail(value);
}, []);
return (
<AuthPageContainer
title={
hasSetUp
? t['com.affine.auth.change.email.page.success.title']()
: t['com.affine.auth.change.email.page.title']()
}
subtitle={
hasSetUp
? t['com.affine.auth.change.email.page.success.subtitle']()
: t['com.affine.auth.change.email.page.subtitle']()
}
>
{hasSetUp ? (
<Button type="primary" size="large" onClick={onOpenAffine}>
{t['com.affine.auth.open.affine']()}
</Button>
) : (
<>
<AuthInput
width={320}
label={t['com.affine.settings.email']()}
placeholder={t['com.affine.auth.sign.email.placeholder']()}
value={email}
onChange={onEmailChange}
error={!isValidEmail}
errorHint={
isValidEmail ? '' : t['com.affine.auth.sign.email.error']()
}
onEnter={onContinue}
/>
<Button
type="primary"
size="large"
onClick={onContinue}
loading={loading}
>
{t['com.affine.auth.set.email.save']()}
</Button>
</>
)}
</AuthPageContainer>
);
};

View File

@@ -0,0 +1,58 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { Button } from '@toeverything/components/button';
import type { FC } from 'react';
import { useCallback, useState } from 'react';
import { AuthPageContainer } from './auth-page-container';
import { SetPassword } from './set-password';
type User = {
id: string;
name: string;
email: string;
image: string;
};
export const ChangePasswordPage: FC<{
user: User;
onSetPassword: (password: string) => void;
onOpenAffine: () => void;
}> = ({ user: { email }, onSetPassword: propsOnSetPassword, onOpenAffine }) => {
const t = useAFFiNEI18N();
const [hasSetUp, setHasSetUp] = useState(false);
const onSetPassword = useCallback(
(passWord: string) => {
propsOnSetPassword(passWord);
setHasSetUp(true);
},
[propsOnSetPassword]
);
return (
<AuthPageContainer
title={
hasSetUp
? t['com.affine.auth.set.password.page.success']()
: t['com.affine.auth.reset.password.page.title']()
}
subtitle={
hasSetUp ? (
t['com.affine.auth.sign.up.success.subtitle']()
) : (
<>
{t['com.affine.auth.page.sent.email.subtitle']()}
<a href={`mailto:${email}`}>{email}</a>
</>
)
}
>
{hasSetUp ? (
<Button type="primary" size="large" onClick={onOpenAffine}>
{t['com.affine.auth.open.affine']()}
</Button>
) : (
<SetPassword onSetPassword={onSetPassword} />
)}
</AuthPageContainer>
);
};

View File

@@ -0,0 +1,14 @@
export * from './auth-content';
export * from './auth-input';
export * from './auth-page-container';
export * from './back-button';
export * from './change-email-page';
export * from './change-password-page';
export * from './modal';
export * from './modal-header';
export * from './password-input';
export * from './resend-button';
export * from './set-password-page';
export * from './sign-in-page-container';
export * from './sign-in-success-page';
export * from './sign-up-page';

View File

@@ -0,0 +1,18 @@
export const Logo = () => {
return (
<svg
width="149"
height="48"
viewBox="0 0 149 48"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M41.2519 35.7795C40.6446 34.7245 39.6331 32.973 38.6338 31.243C38.3306 30.718 38.0295 30.1962 37.7406 29.6957C37.1466 28.6663 36.6056 27.7277 36.2197 27.0578C33.5423 22.4283 28.2713 13.2624 25.6378 8.72996C24.8263 7.46664 22.9461 7.56672 22.248 8.87498C21.4559 10.2486 20.5842 11.7591 19.6635 13.3553C19.3715 13.8619 19.0735 14.3776 18.7724 14.8995C14.9365 21.546 10.4473 29.326 7.24932 34.8675C7.08601 35.1637 6.78183 35.6467 6.62974 35.9664C6.36333 36.5434 6.41539 37.2624 6.75121 37.7955C7.1299 38.4266 7.83828 38.7728 8.56198 38.7371C9.43267 38.7371 11.2792 38.7361 13.6667 38.7371C14.2332 38.7371 14.8303 38.7371 15.4519 38.7371C23.7535 38.7371 36.4351 38.7422 39.5473 38.7371C41.059 38.7402 42.0073 37.0816 41.254 35.7785L41.2519 35.7795ZM23.382 29.8929L21.854 27.2447C21.5855 26.779 21.9213 26.1969 22.4582 26.1969H25.5143C26.0522 26.1969 26.388 26.779 26.1186 27.2447L24.5905 29.8929C24.3221 30.3586 23.6504 30.3586 23.381 29.8929H23.382ZM20.822 24.9611C20.6954 24.6384 20.5862 24.3106 20.4964 23.9776L25.5521 24.9611H20.822ZM23.1309 31.9283C22.9155 32.1999 22.6858 32.4583 22.4429 32.7034L20.7659 27.8309L23.1299 31.9283H23.1309ZM28.0079 26.444C28.3499 26.4951 28.6888 26.5655 29.0215 26.6534L25.6429 30.5424L28.0079 26.444ZM20.1851 22.318C20.131 21.8258 20.1075 21.3284 20.1095 20.83L26.7861 24.0889L20.184 22.3191L20.1851 22.318ZM19.393 27.1967L21.1609 33.7992C20.7618 34.0923 20.3433 34.362 19.9105 34.6091L19.3919 27.1967H19.393ZM30.6139 27.214C31.0671 27.4132 31.5091 27.642 31.9398 27.8932L25.7807 32.0498L30.6139 27.214ZM20.2626 18.7283C20.3851 17.8234 20.574 16.9318 20.8026 16.077L30.9456 24.8988L20.2637 18.7283H20.2626ZM18.015 35.5293C17.1708 35.8755 16.3053 36.1574 15.4509 36.3861L18.015 23.1882V35.5293ZM33.6822 29.0758C34.4038 29.6345 35.0816 30.2442 35.7073 30.8692L23.0002 35.2464L33.6822 29.0758ZM24.4303 11.1167C26.3003 14.3633 29.0154 19.0714 31.6019 23.5528L21.9448 13.8905C22.5123 12.908 23.0502 11.9756 23.5463 11.1146C23.7433 10.7746 24.2333 10.7746 24.4303 11.1146V11.1167ZM9.34693 35.7213C9.87873 34.8031 10.6096 33.5439 10.7719 33.2539C12.3111 30.5863 14.3842 26.9945 16.5257 23.2831L12.9889 36.4883C11.7222 36.4883 10.63 36.4883 9.7889 36.4883C9.39592 36.4883 9.14993 36.0624 9.34693 35.7223V35.7213ZM38.1867 36.4903C35.2378 36.4903 29.5125 36.4903 23.828 36.4903L37.0271 32.9516C37.7192 34.1506 38.2734 35.1106 38.6287 35.7254C38.8257 36.0654 38.5797 36.4903 38.1877 36.4903H38.1867Z" />
<path d="M60.656 11.908C60.5365 11.4259 60.1037 11.0879 59.6077 11.0879H57.9326C57.4366 11.0879 57.0038 11.4259 56.8844 11.908L51.205 34.8244C51.0366 35.5056 51.551 36.1643 52.2533 36.1643H52.831C53.3332 36.1643 53.7691 35.8181 53.8824 35.3289L55.0919 30.1296C55.2052 29.6404 55.6411 29.2942 56.1433 29.2942H61.396C61.8982 29.2942 62.334 29.6404 62.4473 30.1296L63.6569 35.3289C63.7702 35.8181 64.2061 36.1643 64.7083 36.1643H65.286C65.9872 36.1643 66.5027 35.5056 66.3343 34.8244L60.6549 11.908H60.656ZM60.3344 26.8891H57.2059C56.5108 26.8891 55.9963 26.2416 56.1545 25.5645L58.245 15.4978C58.3746 14.9412 59.1667 14.9412 59.2963 15.4978L61.3868 25.5645C61.544 26.2416 61.0306 26.8891 60.3354 26.8891H60.3344Z" />
<path d="M101.52 22.0453H88.56C87.9639 22.0453 87.4801 21.5613 87.4801 20.9648V15.5439C87.4801 14.4103 88.3987 13.4911 89.5318 13.4911H95.945C96.5411 13.4911 97.0249 13.007 97.0249 12.4106V12.1665C97.0249 11.5701 96.5411 11.086 95.945 11.086H88.7672C86.5012 11.086 84.6649 12.9243 84.6649 15.1905V22.0433H73.1266C72.5304 22.0433 72.0466 21.5592 72.0466 20.9628V15.5419C72.0466 14.4082 72.9653 13.4891 74.0983 13.4891H80.5116C81.1077 13.4891 81.5915 13.005 81.5915 12.4086V12.1645C81.5915 11.5681 81.1077 11.084 80.5116 11.084H73.3338C71.0677 11.084 69.2314 12.9223 69.2314 15.1885V35.0799C69.2314 35.6763 69.7153 36.1604 70.3114 36.1604H70.9677C71.5638 36.1604 72.0476 35.6763 72.0476 35.0799V25.5269C72.0476 24.9305 72.5315 24.4464 73.1276 24.4464H84.6659V35.0799C84.6659 35.6763 85.1498 36.1604 85.7459 36.1604H86.4022C86.9983 36.1604 87.4821 35.6763 87.4821 35.0799V25.5269C87.4821 24.9305 87.966 24.4464 88.5621 24.4464H100.827C101.96 24.4464 102.879 25.3655 102.879 26.4991V35.0922C102.879 35.6886 103.363 36.1727 103.959 36.1727H104.546C105.142 36.1727 105.626 35.6886 105.626 35.0922V26.1468C105.626 23.8796 103.788 22.0423 101.523 22.0423L101.52 22.0453Z" />
<path d="M122.781 11.0879H122.19C121.595 11.0879 121.112 11.57 121.11 12.1644L121.045 32.7288L115.155 11.8743C115.023 11.4086 114.6 11.0879 114.116 11.0879H111.549C110.953 11.0879 110.469 11.572 110.469 12.1684V35.0849C110.469 35.6813 110.953 36.1654 111.549 36.1654H112.14C112.735 36.1654 113.218 35.6833 113.22 35.089L113.285 14.5245L119.175 35.379C119.306 35.8447 119.73 36.1654 120.214 36.1654H122.781C123.377 36.1654 123.861 35.6813 123.861 35.0849V12.1674C123.861 11.571 123.377 11.0869 122.781 11.0869V11.0879Z" />
<path d="M132.074 13.492H138.179C138.775 13.492 139.259 13.0079 139.259 12.4115V12.1674C139.259 11.571 138.775 11.0869 138.179 11.0869H131.345C129.079 11.0869 127.243 12.9252 127.243 15.1914V32.0578C127.243 34.325 129.08 36.1623 131.345 36.1623H138.179C138.775 36.1623 139.259 35.6782 139.259 35.0818V34.8377C139.259 34.2413 138.775 33.7572 138.179 33.7572H132.074C130.941 33.7572 130.023 32.8381 130.023 31.7044V25.1846C130.023 24.5882 130.506 24.1041 131.103 24.1041H137.835C138.431 24.1041 138.915 23.62 138.915 23.0236V22.7795C138.915 22.1831 138.431 21.699 137.835 21.699H131.103C130.506 21.699 130.023 21.2149 130.023 20.6185V15.5407C130.023 14.4071 130.941 13.4879 132.074 13.4879V13.492Z" />
<path d="M103.979 14.1446C103.499 13.6646 102.681 13.8842 102.506 14.5398L101.713 17.5025C101.537 18.1572 102.136 18.7567 102.792 18.582L105.753 17.7885C106.408 17.6128 106.626 16.7938 106.148 16.3148L103.981 14.1466L103.979 14.1446Z" />
</svg>
);
};

View File

@@ -0,0 +1,18 @@
import { Logo1Icon } from '@blocksuite/icons';
import type { FC } from 'react';
import { modalHeaderWrapper } from './share.css';
export const ModalHeader: FC<{
title: string;
subTitle: string;
}> = ({ title, subTitle }) => {
return (
<div className={modalHeaderWrapper}>
<p>
<Logo1Icon className="logo" />
{title}
</p>
<p>{subTitle}</p>
</div>
);
};

View File

@@ -0,0 +1,43 @@
import { Modal, ModalCloseButton, ModalWrapper } from '@affine/component';
import type { FC, PropsWithChildren } from 'react';
import { useCallback } from 'react';
export type AuthModalProps = {
open: boolean;
setOpen: (value: boolean) => void;
};
export const AuthModal: FC<PropsWithChildren<AuthModalProps>> = ({
children,
open,
setOpen,
}) => {
const handleClose = useCallback(() => {
setOpen(false);
}, [setOpen]);
return (
<Modal
open={open}
onClose={handleClose}
wrapperPosition={['center', 'center']}
data-testid="auth-modal"
>
<ModalWrapper
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>
);
};

View File

@@ -0,0 +1,30 @@
export const ErrorIcon = () => {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g id="InformationFill_Duotone">
<g id="Icon (Stroke)">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M1.33398 8.00065C1.33398 4.31875 4.31875 1.33398 8.00065 1.33398C11.6826 1.33398 14.6673 4.31875 14.6673 8.00065C14.6673 11.6826 11.6826 14.6673 8.00065 14.6673C4.31875 14.6673 1.33398 11.6826 1.33398 8.00065ZM7.33398 5.33398C7.33398 4.96579 7.63246 4.66732 8.00065 4.66732H8.00732C8.37551 4.66732 8.67398 4.96579 8.67398 5.33398C8.67398 5.70217 8.37551 6.00065 8.00732 6.00065H8.00065C7.63246 6.00065 7.33398 5.70217 7.33398 5.33398ZM8.00065 6.66732C8.36884 6.66732 8.66732 6.96579 8.66732 7.33398V10.6673C8.66732 11.0355 8.36884 11.334 8.00065 11.334C7.63246 11.334 7.33398 11.0355 7.33398 10.6673V7.33398C7.33398 6.96579 7.63246 6.66732 8.00065 6.66732Z"
fill="#EB4335"
/>
<path
d="M8.66732 7.33398C8.66732 6.96579 8.36884 6.66732 8.00065 6.66732C7.63246 6.66732 7.33398 6.96579 7.33398 7.33398V10.6673C7.33398 11.0355 7.63246 11.334 8.00065 11.334C8.36884 11.334 8.66732 11.0355 8.66732 10.6673V7.33398Z"
fill="white"
/>
<path
d="M8.00065 4.66732C7.63246 4.66732 7.33398 4.96579 7.33398 5.33398C7.33398 5.70217 7.63246 6.00065 8.00065 6.00065H8.00732C8.37551 6.00065 8.67398 5.70217 8.67398 5.33398C8.67398 4.96579 8.37551 4.66732 8.00732 4.66732H8.00065Z"
fill="white"
/>
</g>
</g>
</svg>
);
};

View File

@@ -0,0 +1,100 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { passwordStrength } from 'check-password-strength';
import { type FC, useEffect } from 'react';
import { useCallback, useState } from 'react';
import { Input, type InputProps } from '../../../ui/input';
import { ErrorIcon } from './error';
import { SuccessIcon } from './success';
import { Tag } from './tag';
export type Status = 'weak' | 'medium' | 'strong' | 'maximum';
export const PasswordInput: FC<
InputProps & {
onPass: (password: string) => void;
onPrevent: () => void;
}
> = ({ onPass, onPrevent, ...inputProps }) => {
const t = useAFFiNEI18N();
const [status, setStatus] = useState<Status | null>(null);
const [confirmStatus, setConfirmStatus] = useState<
'success' | 'error' | null
>(null);
const [password, setPassWord] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const onPasswordChange = useCallback((value: string) => {
setPassWord(value);
if (!value) {
return setStatus(null);
}
if (value.length > 20) {
return setStatus('maximum');
}
switch (passwordStrength(value).id) {
case 0:
case 1:
setStatus('weak');
break;
case 2:
setStatus('medium');
break;
case 3:
setStatus('strong');
break;
}
}, []);
const onConfirmPasswordChange = useCallback((value: string) => {
setConfirmPassword(value);
}, []);
useEffect(() => {
if (password === confirmPassword) {
setConfirmStatus('success');
} else {
setConfirmStatus('error');
}
}, [confirmPassword, password]);
useEffect(() => {
if (confirmStatus === 'success' && password.length > 7) {
onPass(password);
} else {
onPrevent();
}
}, [confirmStatus, onPass, onPrevent, password]);
return (
<>
<Input
type="password"
size="extraLarge"
style={{ marginBottom: 20 }}
placeholder={t['com.affine.auth.set.password.placeholder']()}
onChange={onPasswordChange}
endFix={status ? <Tag status={status} /> : null}
{...inputProps}
/>
<Input
type="password"
size="extraLarge"
placeholder={t['com.affine.auth.set.password.placeholder.confirm']()}
onChange={onConfirmPasswordChange}
endFix={
confirmStatus ? (
confirmStatus === 'success' ? (
<SuccessIcon />
) : (
<ErrorIcon />
)
) : null
}
{...inputProps}
/>
</>
);
};

View File

@@ -0,0 +1,28 @@
import { style } from '@vanilla-extract/css';
export const tag = style({
padding: '0 15px',
height: 20,
lineHeight: '20px',
borderRadius: 10,
fontSize: 'var(--affine-font-xs)',
selectors: {
'&.weak': {
backgroundColor: 'var(--affine-tag-red)',
color: 'var(--affine-error-color)',
},
'&.medium': {
backgroundColor: 'var(--affine-tag-orange)',
color: 'var(--affine-warning-color)',
},
'&.strong': {
backgroundColor: 'var(--affine-tag-green)',
color: 'var(--affine-success-color)',
},
'&.maximum': {
backgroundColor: 'var(--affine-tag-red)',
color: 'var(--affine-error-color)',
},
},
});

View File

@@ -0,0 +1,28 @@
export const SuccessIcon = () => {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g id="SingleSelect">
<path
id="Ellipse 2102 (Stroke)"
fillRule="evenodd"
clipRule="evenodd"
d="M1.5 8C1.5 4.41015 4.41015 1.5 8 1.5C11.5899 1.5 14.5 4.41015 14.5 8C14.5 11.5899 11.5899 14.5 8 14.5C4.41015 14.5 1.5 11.5899 1.5 8Z"
fill="#10CB86"
/>
<path
id="Icon (Stroke)"
fillRule="evenodd"
clipRule="evenodd"
d="M11.0052 5.63143C11.2087 5.81802 11.2225 6.13431 11.0359 6.33787L7.36923 10.3379C7.27708 10.4384 7.14786 10.4969 7.01151 10.4999C6.87517 10.5028 6.74353 10.45 6.6471 10.3536L4.98043 8.68689C4.78517 8.49163 4.78517 8.17505 4.98043 7.97978C5.17569 7.78452 5.49228 7.78452 5.68754 7.97978L6.98495 9.27719L10.2987 5.66214C10.4853 5.45858 10.8016 5.44483 11.0052 5.63143Z"
fill="white"
/>
</g>
</svg>
);
};

View File

@@ -0,0 +1,28 @@
import clsx from 'clsx';
import { type FC, useMemo } from 'react';
import type { Status } from './index';
import { tag } from './style.css';
export const Tag: FC<{ status: Status }> = ({ status }) => {
const textMap = useMemo<{ [K in Status]: string }>(() => {
return {
weak: 'Weak',
medium: 'Medium',
strong: 'Strong',
maximum: 'Maximum',
};
}, []);
return (
<div
className={clsx(tag, {
weak: status === 'weak',
medium: status === 'medium',
strong: status === 'strong',
maximum: status === 'maximum',
})}
>
{textMap[status]}
</div>
);
};

View File

@@ -0,0 +1,76 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { Button } from '@toeverything/components/button';
import { type FC, useCallback, useEffect, useState } from 'react';
import { resendButtonWrapper } from './share.css';
const formatTime = (time: number): string => {
const minutes = Math.floor(time / 60);
const seconds = time % 60;
const formattedMinutes = minutes.toString().padStart(2, '0');
const formattedSeconds = seconds.toString().padStart(2, '0');
return `${formattedMinutes}:${formattedSeconds}`;
};
const CountDown: FC<{
seconds: number;
onEnd?: () => void;
}> = ({ seconds, onEnd }) => {
const [timeLeft, setTimeLeft] = useState(seconds);
useEffect(() => {
if (timeLeft === 0) {
onEnd?.();
return;
}
const intervalId = setInterval(() => {
setTimeLeft(timeLeft - 1);
if (timeLeft - 1 === 0) {
clearInterval(intervalId);
}
}, 1000);
return () => clearInterval(intervalId);
}, [onEnd, timeLeft]);
return (
<div style={{ width: 45, textAlign: 'center' }}>{formatTime(timeLeft)}</div>
);
};
export const ResendButton: FC<{
onClick: () => void;
countDownSeconds?: number;
}> = ({ onClick, countDownSeconds = 60 }) => {
const t = useAFFiNEI18N();
const [canResend, setCanResend] = useState(false);
const onButtonClick = useCallback(() => {
onClick();
setCanResend(false);
}, [onClick]);
const onCountDownEnd = useCallback(() => {
setCanResend(true);
}, [setCanResend]);
return (
<div className={resendButtonWrapper}>
{canResend ? (
<Button type="plain" size="large" onClick={onButtonClick}>
{t['com.affine.auth.sign.auth.code.resend.hint']()}
</Button>
) : (
<>
<span className="resend-code-hint">
{t['com.affine.auth.sign.auth.code.on.resend.hint']()}
</span>
<CountDown seconds={countDownSeconds} onEnd={onCountDownEnd} />
</>
)}
</div>
);
};

View File

@@ -0,0 +1,60 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { Button } from '@toeverything/components/button';
import type { FC } from 'react';
import { useCallback, useState } from 'react';
import { AuthPageContainer } from './auth-page-container';
import { SetPassword } from './set-password';
type User = {
id: string;
name: string;
email: string;
image: string;
};
export const SetPasswordPage: FC<{
user: User;
onSetPassword: (password: string) => void;
onOpenAffine: () => void;
}> = ({ user: { email }, onSetPassword: propsOnSetPassword, onOpenAffine }) => {
const t = useAFFiNEI18N();
const [hasSetUp, setHasSetUp] = useState(false);
const onSetPassword = useCallback(
(passWord: string) => {
propsOnSetPassword(passWord);
setHasSetUp(true);
},
[propsOnSetPassword]
);
return (
<AuthPageContainer
title={
hasSetUp
? t['com.affine.auth.sign.up.success.title']()
: t['com.affine.auth.page.sent.email.title']()
}
subtitle={
hasSetUp ? (
t['com.affine.auth.sign.up.success.subtitle']()
) : (
<>
{t['com.affine.auth.page.sent.email.subtitle']()}
<a href={`mailto:${email}`}>{email}</a>
</>
)
}
>
<h1>This is set page</h1>
{hasSetUp ? (
<Button type="primary" size="large" onClick={onOpenAffine}>
{t['com.affine.auth.open.affine']()}
</Button>
) : (
<SetPassword onSetPassword={onSetPassword} />
)}
</AuthPageContainer>
);
};

View File

@@ -0,0 +1,50 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { Button } from '@toeverything/components/button';
import { type FC, useCallback, useRef, useState } from 'react';
import { Wrapper } from '../../ui/layout';
import { PasswordInput } from './password-input';
export const SetPassword: FC<{
showLater?: boolean;
onLater?: () => void;
onSetPassword: (password: string) => void;
}> = ({ onLater, onSetPassword, showLater = false }) => {
const t = useAFFiNEI18N();
const [passwordPass, setPasswordPass] = useState(false);
const passwordRef = useRef('');
return (
<>
<Wrapper marginTop={30} marginBottom={42}>
<PasswordInput
width={320}
onPass={useCallback(password => {
setPasswordPass(true);
passwordRef.current = password;
}, [])}
onPrevent={useCallback(() => {
setPasswordPass(false);
}, [])}
/>
</Wrapper>
<Button
type="primary"
size="large"
disabled={!passwordPass}
style={{ marginRight: 20 }}
onClick={useCallback(() => {
onSetPassword(passwordRef.current);
}, [onSetPassword])}
>
{t['com.affine.auth.set.password.save']()}
</Button>
{showLater ? (
<Button type="plain" size="large" onClick={onLater}>
{t['com.affine.auth.later']()}
</Button>
) : null}
</>
);
};

View File

@@ -0,0 +1,180 @@
import { globalStyle, keyframes, style } from '@vanilla-extract/css';
export const modalHeaderWrapper = style({});
globalStyle(`${modalHeaderWrapper} .logo`, {
fontSize: 'var(--affine-font-h-3)',
fontWeight: 600,
color: 'var(--affine-blue)',
marginRight: '6px',
verticalAlign: 'middle',
});
globalStyle(`${modalHeaderWrapper} > p:first-of-type`, {
fontSize: 'var(--affine-font-h-5)',
fontWeight: 600,
marginBottom: '4px',
lineHeight: '28px',
display: 'flex',
alignItems: 'center',
});
globalStyle(`${modalHeaderWrapper} > p:last-of-type`, {
fontSize: 'var(--affine-font-h-4)',
fontWeight: 600,
lineHeight: '28px',
});
export const authInputWrapper = style({
paddingBottom: '30px',
position: 'relative',
selectors: {
'&.without-hint': {
paddingBottom: '20px',
},
},
});
globalStyle(`${authInputWrapper} label`, {
display: 'block',
color: 'var(--light-text-color-text-secondary-color, #8E8D91)',
marginBottom: '4px',
fontSize: 'var(--affine-font-sm)',
fontWeight: 600,
lineHeight: '22px',
});
export const formHint = style({
fontSize: 'var(--affine-font-sm)',
position: 'absolute',
bottom: '4px',
left: 0,
lineHeight: '22px',
selectors: {
'&.error': {
color: 'var(--affine-error-color)',
},
'&.warning': {
color: 'var(--affine-warning-color)',
},
},
});
const rotate = keyframes({
'0%': { transform: 'rotate(0deg)' },
'50%': { transform: 'rotate(180deg)' },
'100%': { transform: 'rotate(360deg)' },
});
export const loading = style({
width: '15px',
height: '15px',
position: 'relative',
borderRadius: '50%',
overflow: 'hidden',
backgroundColor: 'var(--affine-border-color)',
selectors: {
'&::after': {
content: '""',
width: '12px',
height: '12px',
position: 'absolute',
left: '0',
right: '0',
top: '0',
bottom: '0',
margin: 'auto',
backgroundColor: '#fff',
zIndex: 2,
borderRadius: '50%',
},
'&::before': {
content: '""',
width: '20px',
height: '20px',
backgroundColor: 'var(--affine-blue)',
position: 'absolute',
left: '50%',
bottom: '50%',
zIndex: '1',
transformOrigin: 'left bottom',
animation: `${rotate} 1.5s infinite linear`,
},
},
});
export const authContent = style({
fontSize: 'var(--affine-font-base)',
lineHeight: 'var(--affine-font-h-3)',
marginTop: '30px',
});
globalStyle(`${authContent} a`, {
color: 'var(--affine-link-color)',
});
export const authCodeContainer = style({
paddingBottom: '40px',
position: 'relative',
});
export const authCodeWrapper = style({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
});
export const authCodeErrorMessage = style({
color: 'var(--affine-error-color)',
fontSize: 'var(--affine-font-sm)',
textAlign: 'center',
lineHeight: '1.5',
position: 'absolute',
left: 0,
right: 0,
bottom: 5,
margin: 'auto',
});
export const resendButtonWrapper = style({
height: 32,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
marginTop: 30,
});
globalStyle(`${resendButtonWrapper} .resend-code-hint`, {
fontWeight: 600,
fontSize: 'var(--affine-font-sm)',
marginRight: 8,
});
export const authPageContainer = style({
height: '100vh',
width: '100vw',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
fontSize: 'var(--affine-font-base)',
});
globalStyle(`${authPageContainer} .wrapper`, {
display: 'flex',
alignItems: 'center',
});
globalStyle(`${authPageContainer} .content`, {
maxWidth: '700px',
minWidth: '550px',
});
globalStyle(`${authPageContainer} .title`, {
fontSize: 'var(--affine-font-title)',
fontWeight: 600,
marginBottom: '28px',
});
globalStyle(`${authPageContainer} .subtitle`, {
marginBottom: '28px',
});
globalStyle(`${authPageContainer} a`, {
color: 'var(--affine-link-color)',
});
export const signInPageContainer = style({
width: '400px',
margin: '205px auto 0',
});

View File

@@ -0,0 +1,6 @@
import type { PropsWithChildren } from 'react';
import { signInPageContainer } from './share.css';
export const SignInPageContainer = ({ children }: PropsWithChildren) => {
return <div className={signInPageContainer}>{children}</div>;
};

View File

@@ -0,0 +1,21 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { Button } from '@toeverything/components/button';
import type { FC } from 'react';
import { AuthPageContainer } from './auth-page-container';
export const SignInSuccessPage: FC<{
onOpenAffine: () => void;
}> = ({ onOpenAffine }) => {
const t = useAFFiNEI18N();
return (
<AuthPageContainer
title={t['com.affine.auth.signed.success.title']()}
subtitle={t['com.affine.auth.signed.success.subtitle']()}
>
<Button type="primary" size="large" onClick={onOpenAffine}>
{t['com.affine.auth.open.affine']()}
</Button>
</AuthPageContainer>
);
};

View File

@@ -0,0 +1,65 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { Button } from '@toeverything/components/button';
import type { FC } from 'react';
import { useCallback, useState } from 'react';
import { AuthPageContainer } from './auth-page-container';
import { SetPassword } from './set-password';
type User = {
id: string;
name: string;
email: string;
image: string;
};
export const SignUpPage: FC<{
user: User;
onSetPassword: (password: string) => void;
onOpenAffine: () => void;
}> = ({ user: { email }, onSetPassword: propsOnSetPassword, onOpenAffine }) => {
const t = useAFFiNEI18N();
const [hasSetUp, setHasSetUp] = useState(false);
const onSetPassword = useCallback(
(passWord: string) => {
propsOnSetPassword(passWord);
setHasSetUp(true);
},
[propsOnSetPassword]
);
const onLater = useCallback(() => {
setHasSetUp(true);
}, []);
return (
<AuthPageContainer
title={
hasSetUp
? t['com.affine.auth.sign.up.success.title']()
: t['com.affine.auth.page.sent.email.title']()
}
subtitle={
hasSetUp ? (
t['com.affine.auth.sign.up.success.subtitle']()
) : (
<>
{t['com.affine.auth.page.sent.email.subtitle']()}
<a href={`mailto:${email}`}>{email}</a>
</>
)
}
>
{hasSetUp ? (
<Button type="primary" size="large" onClick={onOpenAffine}>
{t['com.affine.auth.open.affine']()}
</Button>
) : (
<SetPassword
onSetPassword={onSetPassword}
onLater={onLater}
showLater={true}
/>
)}
</AuthPageContainer>
);
};

View File

@@ -0,0 +1,2 @@
export const emailRegex =
/^(?:(?:[^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@(?:(?:\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|((?:[a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;

View File

@@ -12,6 +12,7 @@ import {
StyledSettingLink,
StyledWorkspaceInfo,
StyledWorkspaceTitle,
StyledWorkspaceTitleArea,
} from './styles';
export interface WorkspaceTypeProps {
@@ -70,25 +71,29 @@ export const WorkspaceCard = ({
<WorkspaceAvatar size={28} workspace={workspace} />
<StyledWorkspaceInfo>
<StyledWorkspaceTitle>{name}</StyledWorkspaceTitle>
<WorkspaceType flavour={meta.flavour} />
<StyledWorkspaceTitleArea style={{ display: 'flex' }}>
<StyledWorkspaceTitle>{name}</StyledWorkspaceTitle>
<StyledSettingLink
className="setting-entry"
onClick={e => {
e.stopPropagation();
onSettingClick(meta.id);
}}
withoutHoverStyle={true}
>
<SettingsIcon style={{ margin: '0px' }} />
</StyledSettingLink>
</StyledWorkspaceTitleArea>
{/* {meta.flavour === WorkspaceFlavour.LOCAL && (
<p title={t['Available Offline']()}>
<LocalDataIcon />
<span>{t['Available Offline']()}</span>
<WorkspaceType flavour={meta.flavour} />
</p>
)} */}
<WorkspaceType flavour={meta.flavour} />
</StyledWorkspaceInfo>
<StyledSettingLink
className="setting-entry"
data-testid="workspace-card-setting-button"
onClick={e => {
e.stopPropagation();
onSettingClick(meta.id);
}}
>
<SettingsIcon />
</StyledSettingLink>
</StyledCard>
);
};

View File

@@ -104,3 +104,10 @@ export const StyledWorkspaceType = styled('p')(() => {
fontSize: 10,
};
});
export const StyledWorkspaceTitleArea = styled('div')(() => {
return {
display: 'flex',
justifyContent: 'space-between',
};
});

View File

@@ -0,0 +1,51 @@
import { AuthPageContainer } from '@affine/component/auth-components';
import { type GetInviteInfoQuery } from '@affine/graphql';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { Avatar } from '@toeverything/components/avatar';
import { Button } from '@toeverything/components/button';
import { FlexWrapper } from '../../ui/layout';
import * as styles from './styles.css';
export const AcceptInvitePage = ({
onOpenWorkspace,
inviteInfo,
}: {
onOpenWorkspace: () => void;
inviteInfo: GetInviteInfoQuery['getInviteInfo'];
}) => {
const t = useAFFiNEI18N();
return (
<AuthPageContainer
title={t['Successfully joined!']()}
subtitle={
<FlexWrapper alignItems="center">
<Avatar
url={inviteInfo.user.avatarUrl || ''}
name={inviteInfo.user.name}
size={20}
// FIXME: fix it in @toeverything/components/avatar
imageProps={{
style: {
objectFit: 'cover',
},
}}
/>
<span className={styles.inviteName}>{inviteInfo.user.name}</span>
{t['invited you to join']()}
<Avatar
url={`data:image/png;base64,${inviteInfo.workspace.avatar}`}
name={inviteInfo.workspace.name}
size={20}
style={{ marginLeft: 4 }}
colorfulFallback={true}
/>
<span className={styles.inviteName}>{inviteInfo.workspace.name}</span>
</FlexWrapper>
}
>
<Button type="primary" size="large" onClick={onOpenWorkspace}>
{t['Visit Workspace']()}
</Button>
</AuthPageContainer>
);
};

View File

@@ -0,0 +1,2 @@
export * from './accept-invite-page';
export * from './invite-modal';

View File

@@ -0,0 +1,147 @@
import { Permission } from '@affine/graphql';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { Button } from '@toeverything/components/button';
import { useCallback, useEffect, useState } from 'react';
import { Modal, ModalCloseButton, ModalWrapper } from '../../ui/modal';
import { AuthInput } from '..//auth-components';
import { emailRegex } from '..//auth-components/utils';
import * as styles from './styles.css';
export interface InviteModalProps {
open: boolean;
setOpen: (value: boolean) => void;
onConfirm: (params: { email: string; permission: Permission }) => void;
isMutating: boolean;
}
const PermissionMenu = ({
currentPermission,
onChange,
}: {
currentPermission: Permission;
onChange: (permission: Permission) => void;
}) => {
console.log('currentPermission', currentPermission);
console.log('onChange', onChange);
return null;
// return (
// <Menu
// trigger="click"
// content={
// <>
// {Object.entries(Permission).map(([permission]) => {
// return (
// <MenuItem
// key={permission}
// onClick={() => {
// onChange(permission as Permission);
// }}
// >
// {permission}
// </MenuItem>
// );
// })}
// </>
// }
// >
// <MenuTrigger
// type="plain"
// style={{
// marginRight: -10,
// height: '100%',
// }}
// >
// {currentPermission}
// </MenuTrigger>
// </Menu>
// );
};
export const InviteModal = ({
open,
setOpen,
onConfirm,
isMutating,
}: InviteModalProps) => {
const t = useAFFiNEI18N();
const [inviteEmail, setInviteEmail] = useState('');
const [permission, setPermission] = useState(Permission.Write);
const [isValidEmail, setIsValidEmail] = useState(true);
const handleCancel = useCallback(() => {
setOpen(false);
}, [setOpen]);
const handleConfirm = useCallback(() => {
if (!emailRegex.test(inviteEmail)) {
setIsValidEmail(false);
return;
}
setIsValidEmail(true);
onConfirm({
email: inviteEmail,
permission,
});
}, [inviteEmail, onConfirm, permission]);
useEffect(() => {
if (!open) {
setInviteEmail('');
setIsValidEmail(true);
}
}, [open]);
return (
<Modal
wrapperPosition={['center', 'center']}
data-testid="invite-modal"
open={open}
>
<ModalWrapper
width={480}
height={254}
style={{
padding: '20px 26px',
}}
>
<ModalCloseButton top={20} right={20} onClick={handleCancel} />
<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"
endFix={
<PermissionMenu
currentPermission={permission}
onChange={setPermission}
/>
}
/>
</div>
<div className={styles.inviteModalButtonContainer}>
<Button style={{ marginRight: 20 }} onClick={handleCancel}>
{t['Cancel']()}
</Button>
<Button type="primary" onClick={handleConfirm} loading={isMutating}>
{t['Invite']()}
</Button>
</div>
</ModalWrapper>
</Modal>
);
};

View File

@@ -0,0 +1,23 @@
import { style } from '@vanilla-extract/css';
export const inviteModalTitle = style({
fontWeight: '600',
fontSize: 'var(--affine-font-h-6)',
marginBottom: '20px',
});
export const inviteModalContent = style({
marginBottom: '10px',
});
export const inviteModalButtonContainer = style({
display: 'flex',
justifyContent: 'flex-end',
// marginTop: 10,
});
export const inviteName = style({
marginLeft: '4px',
marginRight: '10px',
color: 'var(--affine-black)',
});

View File

@@ -233,6 +233,7 @@ function NotificationCard(props: NotificationCardProps): ReactElement {
data-index={index}
data-front={isFront}
data-expanded={expand}
data-testid="affine-notification"
onMouseEnter={() => {
setExpand(true);
}}

View File

@@ -16,13 +16,22 @@ import { Menu, MenuItem } from '../../..';
import { getContentParser } from './get-content-parser';
import type { CommonMenuItemProps } from './types';
type ExportMenuItemProps = {
iconSize?: number;
gap?: string;
fontSize?: string;
};
const MenuItemStyle = {
padding: '4px 12px',
};
export const ExportToPdfMenuItem = ({
onSelect,
}: CommonMenuItemProps<{ type: 'pdf' }>) => {
style = MenuItemStyle,
iconSize,
gap,
fontSize,
}: CommonMenuItemProps<{ type: 'pdf' }> & ExportMenuItemProps) => {
const t = useAFFiNEI18N();
const { currentEditor } = globalThis;
const setPushNotification = useSetAtom(pushNotificationAtom);
@@ -82,7 +91,10 @@ export const ExportToPdfMenuItem = ({
data-testid="export-to-pdf"
onClick={onClickDownloadPDF}
icon={<ExportToPdfIcon />}
style={MenuItemStyle}
style={style}
iconSize={iconSize}
gap={gap}
fontSize={fontSize}
>
{t['Export to PDF']()}
</MenuItem>
@@ -91,7 +103,11 @@ export const ExportToPdfMenuItem = ({
export const ExportToHtmlMenuItem = ({
onSelect,
}: CommonMenuItemProps<{ type: 'html' }>) => {
style = MenuItemStyle,
iconSize,
gap,
fontSize,
}: CommonMenuItemProps<{ type: 'html' }> & ExportMenuItemProps) => {
const t = useAFFiNEI18N();
const { currentEditor } = globalThis;
const setPushNotification = useSetAtom(pushNotificationAtom);
@@ -126,7 +142,10 @@ export const ExportToHtmlMenuItem = ({
data-testid="export-to-html"
onClick={onClickExportHtml}
icon={<ExportToHtmlIcon />}
style={MenuItemStyle}
style={style}
iconSize={iconSize}
gap={gap}
fontSize={fontSize}
>
{t['Export to HTML']()}
</MenuItem>
@@ -136,7 +155,11 @@ export const ExportToHtmlMenuItem = ({
export const ExportToPngMenuItem = ({
onSelect,
}: CommonMenuItemProps<{ type: 'png' }>) => {
style = MenuItemStyle,
iconSize,
gap,
fontSize,
}: CommonMenuItemProps<{ type: 'png' }> & ExportMenuItemProps) => {
const t = useAFFiNEI18N();
const { currentEditor } = globalThis;
const setPushNotification = useSetAtom(pushNotificationAtom);
@@ -173,7 +196,10 @@ export const ExportToPngMenuItem = ({
data-testid="export-to-png"
onClick={onClickDownloadPNG}
icon={<ExportToPngIcon />}
style={MenuItemStyle}
style={style}
iconSize={iconSize}
gap={gap}
fontSize={fontSize}
>
{t['Export to PNG']()}
</MenuItem>
@@ -183,7 +209,11 @@ export const ExportToPngMenuItem = ({
export const ExportToMarkdownMenuItem = ({
onSelect,
}: CommonMenuItemProps<{ type: 'markdown' }>) => {
style = MenuItemStyle,
iconSize,
gap,
fontSize,
}: CommonMenuItemProps<{ type: 'markdown' }> & ExportMenuItemProps) => {
const t = useAFFiNEI18N();
const { currentEditor } = globalThis;
const setPushNotification = useSetAtom(pushNotificationAtom);
@@ -218,7 +248,10 @@ export const ExportToMarkdownMenuItem = ({
data-testid="export-to-markdown"
onClick={onClickExportMarkdown}
icon={<ExportToMarkdownIcon />}
style={MenuItemStyle}
style={style}
iconSize={iconSize}
gap={gap}
fontSize={fontSize}
>
{t['Export to Markdown']()}
</MenuItem>
@@ -253,7 +286,7 @@ export const Export = ({
data-testid="export-menu"
icon={<ExportIcon />}
endIcon={<ArrowRightSmallIcon />}
style={{ padding: '4px 12px' }}
style={MenuItemStyle}
onClick={e => {
e.stopPropagation();
onItemClick?.();

View File

@@ -3,14 +3,15 @@ import type { CSSProperties, PropsWithChildren, ReactNode } from 'react';
import { settingRow } from './share.css';
type SettingRowProps = {
export type SettingRowProps = PropsWithChildren<{
name: ReactNode;
desc: ReactNode;
style?: CSSProperties;
onClick?: () => void;
spreadCol?: boolean;
'data-testid'?: string;
};
disabled?: boolean;
}>;
export const SettingRow = ({
name,
@@ -19,12 +20,14 @@ export const SettingRow = ({
onClick,
style,
spreadCol = true,
disabled = false,
...props
}: PropsWithChildren<SettingRowProps>) => {
return (
<div
className={clsx(settingRow, {
'two-col': spreadCol,
disabled,
})}
style={style}
onClick={onClick}

View File

@@ -53,6 +53,18 @@ export const settingRow = style({
'&:last-of-type': {
marginBottom: '0',
},
'&.disabled': {
position: 'relative',
},
'&.disabled::after': {
content: '',
position: 'absolute',
left: 0,
top: 0,
width: '100%',
height: '100%',
backgroundColor: 'rgba(255,255,255,0.5)',
},
},
});

View File

@@ -1,13 +1,9 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { Button } from '@toeverything/components/button';
import { CloseIcon } from '@blocksuite/icons';
import { Button, IconButton } from '@toeverything/components/button';
import { Modal, ModalCloseButton } from '../../..';
import {
StyledButtonContent,
StyledModalHeader,
StyledModalWrapper,
StyledTextContent,
} from './style';
import { Modal, ModalWrapper } from '../../..';
import { ButtonContainer, Content, Header, StyleTips, Title } from './style';
export type PublicLinkDisableProps = {
open: boolean;
@@ -23,29 +19,37 @@ export const PublicLinkDisableModal = ({
const t = useAFFiNEI18N();
return (
<Modal open={open} onClose={onClose}>
<StyledModalWrapper>
<ModalCloseButton onClick={onClose} top={12} right={12} />
<StyledModalHeader>{t['Disable Public Link ?']()}</StyledModalHeader>
<StyledTextContent>
{t['Disable Public Link Description']()}
</StyledTextContent>
<StyledButtonContent>
<Button onClick={onClose}>{t['Cancel']()}</Button>
<Button
type="error"
data-testid="disable-public-link-confirm-button"
onClick={() => {
onConfirmDisable();
onClose();
}}
style={{ marginLeft: '24px' }}
>
{t['Disable']()}
</Button>
</StyledButtonContent>
</StyledModalWrapper>
<ModalWrapper width={480}>
<Header>
<Title>{t['Disable Public Link']()}</Title>
<IconButton onClick={onClose}>
<CloseIcon />
</IconButton>
</Header>
<Content>
<StyleTips>{t['Disable Public Link Description']()}</StyleTips>
<ButtonContainer>
<div>
<Button onClick={onClose} block>
{t['Cancel']()}
</Button>
</div>
<div>
<Button
data-testid="confirm-enable-affine-cloud-button"
type="error"
block
onClick={() => {
onConfirmDisable();
onClose();
}}
>
{t['Disable']()}
</Button>
</div>
</ButtonContainer>
</Content>
</ModalWrapper>
</Modal>
);
};

View File

@@ -1,42 +1,35 @@
import { styled } from '../../..';
export const StyledModalWrapper = styled('div')(() => {
return {
position: 'relative',
padding: '0px',
width: '560px',
background: 'var(--affine-white)',
borderRadius: '12px',
// height: '312px',
};
export const Header = styled('div')({
display: 'flex',
justifyContent: 'space-between',
paddingRight: '20px',
paddingTop: '20px',
paddingLeft: '24px',
alignItems: 'center',
});
export const StyledModalHeader = styled('div')(() => {
return {
margin: '44px 0px 12px 0px',
width: '560px',
fontWeight: '600',
fontSize: 'var(--affine-font-h6)',
textAlign: 'center',
};
export const Content = styled('div')({
padding: '12px 24px 20px 24px',
});
export const StyledTextContent = styled('div')(() => {
return {
margin: 'auto',
width: '560px',
padding: '0px 84px',
fontWeight: '400',
fontSize: 'var(--affine-font-base)',
textAlign: 'center',
};
export const Title = styled('div')({
fontSize: 'var(--affine-font-h6)',
lineHeight: '26px',
fontWeight: 600,
});
export const StyledButtonContent = styled('div')(() => {
export const StyleTips = styled('div')(() => {
return {
userSelect: 'none',
marginBottom: '20px',
};
});
export const ButtonContainer = styled('div')(() => {
return {
margin: '32px 0',
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
justifyContent: 'flex-end',
gap: '20px',
paddingTop: '20px',
};
});

View File

@@ -1,27 +0,0 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import {
ExportToHtmlMenuItem,
ExportToMarkdownMenuItem,
ExportToPdfMenuItem,
ExportToPngMenuItem,
} from '../page-list/operation-menu-items/export';
import { actionsStyle, descriptionStyle, menuItemStyle } from './index.css';
// import type { ShareMenuProps } from './share-menu';
export const Export = () => {
const t = useAFFiNEI18N();
return (
<div className={menuItemStyle}>
<div className={descriptionStyle}>
{t['Export Shared Pages Description']()}
</div>
<div className={actionsStyle}>
<ExportToPdfMenuItem />
<ExportToHtmlMenuItem />
<ExportToPngMenuItem />
<ExportToMarkdownMenuItem />
</div>
</div>
);
};

View File

@@ -1,16 +1,5 @@
import { style } from '@vanilla-extract/css';
export const tabStyle = style({
display: 'flex',
flex: '1',
width: '100%',
padding: '0 10px',
margin: '0',
justifyContent: 'center',
alignItems: 'center',
position: 'relative',
});
export const menuItemStyle = style({
padding: '4px 18px',
paddingBottom: '16px',
@@ -20,9 +9,9 @@ export const menuItemStyle = style({
export const descriptionStyle = style({
wordWrap: 'break-word',
// wordBreak: 'break-all',
fontSize: '16px',
marginTop: '16px',
marginBottom: '16px',
fontSize: 'var(--affine-font-xs)',
lineHeight: '20px',
color: 'var(--affine-text-secondary-color)',
});
export const buttonStyle = style({
@@ -42,13 +31,103 @@ export const containerStyle = style({
display: 'flex',
width: '100%',
flexDirection: 'column',
gap: '8px',
});
export const indicatorContainerStyle = style({
position: 'relative',
});
export const inputButtonRowStyle = style({
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
marginTop: '16px',
});
export const titleContainerStyle = style({
display: 'flex',
alignItems: 'center',
gap: '4px',
fontSize: 'var(--affine-font-sm)',
fontWeight: 600,
lineHeight: '22px',
padding: '0 4px',
});
export const subTitleStyle = style({
fontSize: 'var(--affine-font-sm)',
fontWeight: 500,
lineHeight: '22px',
});
export const columnContainerStyle = style({
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
padding: '0 4px',
width: '100%',
gap: '12px',
});
export const rowContainerStyle = style({
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
gap: '12px',
padding: '4px',
});
export const radioButtonGroup = style({
display: 'flex',
justifyContent: 'flex-end',
padding: '2px',
minWidth: '154px',
maxWidth: '250px',
});
export const radioButton = style({
color: 'var(--affine-text-secondary-color)',
selectors: {
'&[data-state="checked"]': {
color: 'var(--affine-text-primary-color)',
},
},
});
export const spanStyle = style({
padding: '4px 20px',
});
export const disableSharePage = style({
color: 'var(--affine-error-color)',
});
export const localSharePage = style({
padding: '12px 8px',
display: 'flex',
alignItems: 'center',
borderRadius: '8px',
backgroundColor: 'var(--affine-background-secondary-color)',
minHeight: '108px',
position: 'relative',
});
export const cloudSvgContainer = style({
width: '100%',
height: '100%',
minWidth: '185px',
});
export const cloudSvgStyle = style({
width: '193px',
height: '108px',
position: 'absolute',
bottom: '0',
right: '8px',
});
export const shareIconStyle = style({
fontSize: '16px',
color: 'var(--affine-icon-color)',
display: 'flex',
alignItems: 'center',
});

View File

@@ -0,0 +1,3 @@
import { atom } from 'jotai/vanilla';
export const enableShareMenuAtom = atom(false);

View File

@@ -1,4 +1,3 @@
export * from './disable-public-link';
export * from './share-menu';
export * from './share-workspace';
export * from './styles';

View File

@@ -0,0 +1,48 @@
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import {
ExportToHtmlMenuItem,
ExportToMarkdownMenuItem,
ExportToPdfMenuItem,
ExportToPngMenuItem,
} from '../page-list/operation-menu-items/export';
import * as styles from './index.css';
// import type { ShareMenuProps } from './share-menu';
export const ShareExport = () => {
const t = useAFFiNEI18N();
return (
<>
<div className={styles.titleContainerStyle} style={{ fontWeight: '500' }}>
{t['com.affine.share-menu.ShareViaExport']()}
</div>
<div>
<ExportToPdfMenuItem
style={{ padding: '4px' }}
iconSize={16}
gap={'4px'}
/>
<ExportToHtmlMenuItem
style={{ padding: '4px' }}
iconSize={16}
gap={'4px'}
/>
<ExportToPngMenuItem
style={{ padding: '4px' }}
iconSize={16}
gap={'4px'}
/>
<ExportToMarkdownMenuItem
style={{ padding: '4px' }}
iconSize={16}
gap={'4px'}
/>
</div>
<div className={styles.columnContainerStyle}>
<div className={styles.descriptionStyle}>
{t['com.affine.share-menu.ShareViaExportDescription']()}
</div>
</div>
</>
);
};

View File

@@ -1,139 +1,66 @@
import type {
AffineCloudWorkspace,
AffineOfficialWorkspace,
AffinePublicWorkspace,
LocalWorkspace,
} from '@affine/env/workspace';
import { ExportIcon, PublishIcon, ShareIcon } from '@blocksuite/icons';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { Page } from '@blocksuite/store';
import { Button } from '@toeverything/components/button';
import { useBlockSuiteWorkspacePageIsPublic } from '@toeverything/hooks/use-block-suite-workspace-page-is-public';
import { type ReactElement, useRef } from 'react';
import { useCallback, useState } from 'react';
import { Divider } from '@toeverything/components/divider';
import { useAtom } from 'jotai';
import { Menu } from '../../ui/menu/menu';
import { Export } from './export';
import { containerStyle, indicatorContainerStyle, tabStyle } from './index.css';
import * as styles from './index.css';
import { enableShareMenuAtom } from './index.jotai';
import { ShareExport } from './share-export';
import { SharePage } from './share-page';
import { ShareWorkspace } from './share-workspace';
import { StyledIndicator, TabItem } from './styles';
type SharePanel = 'SharePage' | 'Export' | 'ShareWorkspace';
type ShareMenuComponent<T> = (props: T) => ReactElement;
const MenuItems: Record<SharePanel, ShareMenuComponent<ShareMenuProps>> = {
SharePage: SharePage,
Export: Export,
ShareWorkspace: ShareWorkspace,
};
const tabIcons = {
SharePage: <ShareIcon />,
Export: <ExportIcon />,
ShareWorkspace: <PublishIcon />,
};
export interface ShareMenuProps<
Workspace extends AffineCloudWorkspace | LocalWorkspace =
Workspace extends AffineOfficialWorkspace =
| AffineCloudWorkspace
| LocalWorkspace,
| LocalWorkspace
| AffinePublicWorkspace,
> {
workspace: Workspace;
currentPage: Page;
onEnableAffineCloud: (workspace: LocalWorkspace) => void;
onOpenWorkspaceSettings: (workspace: Workspace) => void;
togglePagePublic: (page: Page, isPublic: boolean) => Promise<void>;
toggleWorkspacePublish: (
workspace: Workspace,
publish: boolean
) => Promise<void>;
}
function assertInstanceOf<T, U extends T>(
obj: T,
type: new (...args: any[]) => U
): asserts obj is U {
if (!(obj instanceof type)) {
throw new Error('Object is not instance of type');
}
useIsSharedPage: (
workspaceId: string,
pageId: string
) => [isSharePage: boolean, setIsSharePage: (enable: boolean) => void];
onEnableAffineCloud: () => void;
togglePagePublic: () => Promise<void>;
}
export const ShareMenu = (props: ShareMenuProps) => {
const [activeItem, setActiveItem] = useState<SharePanel>('SharePage');
const [isPublic] = useBlockSuiteWorkspacePageIsPublic(props.currentPage);
const [open, setOpen] = useState(false);
const containerRef = useRef<HTMLDivElement | null>(null);
const indicatorRef = useRef<HTMLDivElement | null>(null);
const startTransaction = useCallback(() => {
if (indicatorRef.current && containerRef.current) {
const indicator = indicatorRef.current;
const activeTabElement = containerRef.current.querySelector(
`[data-tab-key="${activeItem}"]`
);
assertInstanceOf(activeTabElement, HTMLElement);
requestAnimationFrame(() => {
indicator.style.left = `${activeTabElement.offsetLeft}px`;
indicator.style.width = `${activeTabElement.offsetWidth}px`;
});
}
}, [activeItem]);
const handleMenuChange = useCallback(
(selectedItem: SharePanel) => {
setActiveItem(selectedItem);
startTransaction();
},
[setActiveItem, startTransaction]
const { useIsSharedPage } = props;
const isSharedPage = useIsSharedPage(
props.workspace.id,
props.currentPage.id
);
const ActiveComponent = MenuItems[activeItem];
interface ShareMenuProps {
activeItem: SharePanel;
onChangeTab: (selectedItem: SharePanel) => void;
}
const ShareMenu = ({ activeItem, onChangeTab }: ShareMenuProps) => {
const handleButtonClick = (itemName: SharePanel) => {
onChangeTab(itemName);
setActiveItem(itemName);
};
return (
<div className={tabStyle} ref={containerRef}>
{Object.keys(MenuItems).map(item => (
<TabItem
isActive={activeItem === item}
key={item}
data-tab-key={item}
onClick={() => handleButtonClick(item as SharePanel)}
>
{tabIcons[item as SharePanel]}
{isPublic ? (item === 'SharePage' ? 'SharedPage' : item) : item}
</TabItem>
))}
const [open, setOpen] = useAtom(enableShareMenuAtom);
const t = useAFFiNEI18N();
const content = (
<div className={styles.containerStyle}>
<SharePage {...props} />
<div className={styles.columnContainerStyle}>
<Divider dividerColor="var(--affine-border-color)" size="thinner" />
</div>
);
};
const Share = (
<>
<ShareMenu activeItem={activeItem} onChangeTab={handleMenuChange} />
<div className={indicatorContainerStyle}>
<StyledIndicator
ref={(ref: HTMLDivElement | null) => {
indicatorRef.current = ref;
startTransaction();
}}
/>
</div>
<div className={containerStyle}>
<ActiveComponent {...props} />
</div>
</>
<ShareExport />
</div>
);
return (
<Menu
content={Share}
menuStyles={{
padding: '12px',
background: 'var(--affine-background-overlay-panel-color)',
transform: 'translateX(-10px)',
}}
content={content}
visible={open}
placement="bottom"
trigger={['click']}
width={439}
width={410}
disablePortal={true}
onClickAway={() => {
setOpen(false);
@@ -144,9 +71,19 @@ export const ShareMenu = (props: ShareMenuProps) => {
onClick={() => {
setOpen(!open);
}}
type={isPublic ? 'primary' : undefined}
type={'plain'}
>
<div>{isPublic ? 'Shared' : 'Share'}</div>
<div
style={{
color: isSharedPage
? 'var(--affine-link-color)'
: 'var(--affine-text-primary-color)',
}}
>
{isSharedPage
? t['com.affine.share-menu.sharedButton']()
: t['com.affine.share-menu.shareButton']()}
</div>
</Button>
</Menu>
);

View File

@@ -1,49 +1,90 @@
import type { LocalWorkspace } from '@affine/env/workspace';
import {
Menu,
MenuItem,
MenuTrigger,
RadioButton,
RadioButtonGroup,
Switch,
} from '@affine/component';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { Trans } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ArrowRightSmallIcon, WebIcon } from '@blocksuite/icons';
import { Button } from '@toeverything/components/button';
import { useBlockSuiteWorkspacePageIsPublic } from '@toeverything/hooks/use-block-suite-workspace-page-is-public';
import { useState } from 'react';
import { useCallback, useMemo } from 'react';
import Input from '../../ui/input';
import { toast } from '../../ui/toast';
import { PublicLinkDisableModal } from './disable-public-link';
import {
descriptionStyle,
inputButtonRowStyle,
menuItemStyle,
} from './index.css';
import * as styles from './index.css';
import type { ShareMenuProps } from './share-menu';
import { StyledDisableButton, StyledInput, StyledLinkSpan } from './styles';
const CloudSvg = () => (
<svg
className={styles.cloudSvgStyle}
width="193"
height="108"
viewBox="0 0 193 108"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clipPath="url(#clip0_12864_5906)">
<g opacity="0.1">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M100.707 32.803C82.4182 32.803 67.5923 47.6289 67.5923 65.9176C67.5923 68.4215 67.8692 70.8539 68.3917 73.1882C69.0964 76.3367 67.1162 79.4606 63.9681 80.1667C52.6616 82.7029 44.2173 92.8102 44.2173 104.876C44.2173 118.861 55.5547 130.199 69.5402 130.199H139.665C157.954 130.199 172.78 115.373 172.78 97.0842C172.78 78.7956 157.954 63.9696 139.665 63.9696C139.444 63.9696 139.223 63.9718 139.002 63.9762C136.18 64.0314 133.721 62.0614 133.16 59.2949C130.095 44.179 116.723 32.803 100.707 32.803ZM55.9048 65.9176C55.9048 41.1741 75.9634 21.1155 100.707 21.1155C120.758 21.1155 137.723 34.2825 143.444 52.4393C166.419 54.3582 184.467 73.6135 184.467 97.0842C184.467 121.828 164.409 141.886 139.665 141.886H69.5402C49.0999 141.886 32.5298 125.316 32.5298 104.876C32.5298 89.163 42.3171 75.7471 56.1241 70.3739C55.979 68.9069 55.9048 67.4202 55.9048 65.9176Z"
fill="var(--affine-icon-color)"
/>
</g>
</g>
<defs>
<clipPath id="clip0_12864_5906">
<rect width="193" height="108" fill="white" />
</clipPath>
</defs>
</svg>
);
export const LocalSharePage = (props: ShareMenuProps) => {
const t = useAFFiNEI18N();
return (
<div className={menuItemStyle}>
<div className={descriptionStyle}>{t['Shared Pages Description']()}</div>
<Button
type="primary"
data-testid="share-menu-enable-affine-cloud-button"
onClick={() => {
props.onEnableAffineCloud(props.workspace as LocalWorkspace);
}}
>
{t['Enable AFFiNE Cloud']()}
</Button>
</div>
<>
<div className={styles.titleContainerStyle}>
<div className={styles.shareIconStyle}>
<WebIcon />
</div>
{t['com.affine.share-menu.SharePage']()}
</div>
<div className={styles.localSharePage}>
<div className={styles.columnContainerStyle} style={{ gap: '16px' }}>
<div className={styles.descriptionStyle}>
{t['com.affine.share-menu.EnableCloudDescription']()}
</div>
<div>
<Button onClick={props.onEnableAffineCloud} type="primary">
{t['Enable AFFiNE Cloud']()}
</Button>
</div>
</div>
<div className={styles.cloudSvgContainer}></div>
<CloudSvg />
</div>
</>
);
};
export const AffineSharePage = (props: ShareMenuProps) => {
const [isPublic, setIsPublic] = useBlockSuiteWorkspacePageIsPublic(
props.currentPage
);
const {
workspace: { id: workspaceId },
currentPage: { id: pageId },
} = props;
const [isPublic, setIsPublic] = props.useIsSharedPage(workspaceId, pageId);
const [showDisable, setShowDisable] = useState(false);
const t = useAFFiNEI18N();
const sharingUrl = useMemo(() => {
return `${prefixUrl}public-workspace/${props.workspace.id}/${props.currentPage.id}`;
}, [props.workspace.id, props.currentPage.id]);
return `${runtimeConfig.serverUrlPrefix}/share/${workspaceId}/${pageId}`;
}, [workspaceId, pageId]);
const onClickCreateLink = useCallback(() => {
setIsPublic(true);
}, [setIsPublic]);
@@ -65,51 +106,126 @@ export const AffineSharePage = (props: ShareMenuProps) => {
}, [setIsPublic]);
return (
<div className={menuItemStyle}>
<div className={descriptionStyle}>
{t['Create Shared Link Description']()}
<>
<div className={styles.titleContainerStyle}>
<div className={styles.shareIconStyle}>
<WebIcon />
</div>
{t['com.affine.share-menu.SharePage']()}
</div>
<div className={inputButtonRowStyle}>
<StyledInput
type="text"
<div className={styles.titleContainerStyle} style={{ fontWeight: '500' }}>
{t['com.affine.share-menu.ShareWithLink']()}
</div>
<div className={styles.columnContainerStyle}>
<div className={styles.descriptionStyle}>
{t['com.affine.share-menu.ShareWithLinkDescription']()}
</div>
</div>
<div className={styles.rowContainerStyle}>
<Input
inputStyle={{
color: 'var(--affine-text-secondary-color)',
fontSize: 'var(--affine-font-xs)',
lineHeight: '20px',
}}
value={isPublic ? sharingUrl : `${runtimeConfig.serverUrlPrefix}/...`}
readOnly
value={isPublic ? sharingUrl : 'https://app.affine.pro/xxxx'}
/>
{!isPublic && (
{isPublic ? (
<Button onClick={onClickCopyLink} style={{ padding: '4px 12px' }}>
{t.Copy()}
</Button>
) : (
<Button
data-testid="affine-share-create-link"
onClick={onClickCreateLink}
type="primary"
style={{ padding: '4px 12px' }}
>
{t['Create']()}
</Button>
)}
{isPublic && (
<Button
data-testid="affine-share-copy-link"
onClick={onClickCopyLink}
>
{t['Copy Link']()}
{t.Create()}
</Button>
)}
</div>
<div className={descriptionStyle}>
<Trans i18nKey="Shared Pages In Public Workspace Description">
The entire Workspace is published on the web and can be edited via
<StyledLinkSpan
onClick={() => {
props.onOpenWorkspaceSettings(props.workspace);
}}
>
Workspace Settings
</StyledLinkSpan>
.
</Trans>
</div>
{isPublic && (
{runtimeConfig.enableEnhanceShareMode ? (
<div className={styles.rowContainerStyle}>
<div className={styles.subTitleStyle}>
{t['com.affine.share-menu.ShareMode']()}
</div>
<div>
<RadioButtonGroup
className={styles.radioButtonGroup}
defaultValue={'page'}
onValueChange={() => {}}
>
<RadioButton
className={styles.radioButton}
value={'page'}
spanStyle={styles.spanStyle}
>
{t['Page']()}
</RadioButton>
<RadioButton
className={styles.radioButton}
value={'edgeless'}
spanStyle={styles.spanStyle}
>
{t['Edgeless']()}
</RadioButton>
</RadioButtonGroup>
</div>
</div>
) : null}
{isPublic ? (
<>
<StyledDisableButton onClick={() => setShowDisable(true)}>
{t['Disable Public Link']()}
</StyledDisableButton>
{runtimeConfig.enableEnhanceShareMode && (
<>
<div className={styles.rowContainerStyle}>
<div className={styles.subTitleStyle}>Link expires</div>
<div>
<Menu
content={<MenuItem>Never</MenuItem>}
placement="bottom-end"
trigger="click"
>
<MenuTrigger
style={{
display: 'flex',
justifyContent: 'space-between',
padding: '4px 6px 4px 10px',
}}
>
Never
</MenuTrigger>
</Menu>
</div>
</div>
<div className={styles.rowContainerStyle}>
<div className={styles.subTitleStyle}>
{'Show "Created with AFFiNE"'}
</div>
<div>
<Switch />
</div>
</div>
<div className={styles.rowContainerStyle}>
<div className={styles.subTitleStyle}>
Search engine indexing
</div>
<div>
<Switch />
</div>
</div>
</>
)}
<div
className={styles.rowContainerStyle}
onClick={() => setShowDisable(true)}
style={{ cursor: 'pointer' }}
>
<div className={styles.disableSharePage}>
{t['Disable Public Link']()}
</div>
<ArrowRightSmallIcon />
</div>
<PublicLinkDisableModal
open={showDisable}
onConfirmDisable={onDisablePublic}
@@ -118,8 +234,8 @@ export const AffineSharePage = (props: ShareMenuProps) => {
}}
/>
</>
)}
</div>
) : null}
</>
);
};

View File

@@ -1,67 +0,0 @@
import type {
AffineCloudWorkspace,
LocalWorkspace,
} from '@affine/env/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { Button } from '@toeverything/components/button';
import { descriptionStyle, menuItemStyle } from './index.css';
import type { ShareMenuProps } from './share-menu';
const ShareLocalWorkspace = (props: ShareMenuProps<LocalWorkspace>) => {
const t = useAFFiNEI18N();
return (
<div className={menuItemStyle}>
<div className={descriptionStyle}>
{t['Share Menu Public Workspace Description1']()}
</div>
<Button
data-testid="share-menu-enable-affine-cloud-button"
onClick={() => {
props.onOpenWorkspaceSettings(props.workspace);
}}
>
{t['Open Workspace Settings']()}
</Button>
</div>
);
};
const ShareAffineWorkspace = (props: ShareMenuProps<AffineCloudWorkspace>) => {
// fixme: regression
const isPublicWorkspace = false;
const t = useAFFiNEI18N();
return (
<div className={menuItemStyle}>
<div className={descriptionStyle}>
{isPublicWorkspace
? t['Share Menu Public Workspace Description2']()
: t['Share Menu Public Workspace Description1']()}
</div>
<Button
data-testid="share-menu-publish-to-web-button"
onClick={() => {
props.onOpenWorkspaceSettings(props.workspace);
}}
>
{t['Open Workspace Settings']()}
</Button>
</div>
);
};
export const ShareWorkspace = (props: ShareMenuProps) => {
if (props.workspace.flavour === WorkspaceFlavour.LOCAL) {
return (
<ShareLocalWorkspace {...(props as ShareMenuProps<LocalWorkspace>)} />
);
} else if (props.workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD) {
return (
<ShareAffineWorkspace
{...(props as ShareMenuProps<AffineCloudWorkspace>)}
/>
);
}
throw new Error('Unreachable');
};

View File

@@ -1,20 +1,14 @@
import { clsx } from 'clsx';
import type {
HTMLAttributes,
PropsWithChildren,
ReactElement,
ReactNode,
} from 'react';
import type { HTMLAttributes, PropsWithChildren, ReactElement } from 'react';
import { AppSidebarFallback } from '../app-sidebar';
import { appStyle, mainContainerStyle, toolStyle } from './index.css';
export interface WorkspaceRootProps {
export type WorkspaceRootProps = PropsWithChildren<{
resizing?: boolean;
useNoisyBackground?: boolean;
useBlurBackground?: boolean;
children: ReactNode;
}
}>;
export const AppContainer = ({
resizing,
@@ -53,7 +47,7 @@ export const MainContainer = ({
{...props}
className={clsx(mainContainerStyle, 'main-container', className)}
data-is-macos={environment.isDesktop && environment.isMacOs}
data-show-padding={padding}
data-show-padding={!!padding}
>
{children}
</div>