Merge branch 'canary' into stable

This commit is contained in:
李华桥
2023-11-23 21:31:42 +08:00
357 changed files with 16759 additions and 12832 deletions
+14 -19
View File
@@ -12,7 +12,6 @@
"@blocksuite/editor": "*",
"@blocksuite/global": "*",
"@blocksuite/icons": "2.1.34",
"@blocksuite/lit": "*",
"@blocksuite/store": "*"
},
"dependencies": {
@@ -22,14 +21,11 @@
"@affine/workspace": "workspace:*",
"@dnd-kit/core": "^6.0.8",
"@dnd-kit/modifiers": "^6.0.1",
"@dnd-kit/sortable": "^7.0.2",
"@dnd-kit/sortable": "^8.0.0",
"@emotion/cache": "^11.11.0",
"@emotion/react": "^11.11.1",
"@emotion/server": "^11.11.0",
"@emotion/styled": "^11.11.0",
"@mui/base": "5.0.0-beta.19",
"@mui/icons-material": "^5.14.14",
"@mui/material": "^5.14.14",
"@popperjs/core": "^2.11.8",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-collapsible": "^1.0.3",
@@ -47,15 +43,14 @@
"clsx": "^2.0.0",
"dayjs": "^1.11.10",
"foxact": "^0.2.20",
"jotai": "^2.4.3",
"jotai-effect": "^0.2.2",
"jotai-scope": "^0.4.0",
"jotai": "^2.5.1",
"jotai-effect": "^0.2.3",
"jotai-scope": "^0.4.1",
"lit": "^3.0.2",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"lottie-react": "^2.4.0",
"lottie-web": "^5.12.2",
"nanoid": "^5.0.1",
"nanoid": "^5.0.3",
"next-themes": "^0.2.1",
"react": "18.2.0",
"react-datepicker": "^4.20.0",
@@ -69,12 +64,12 @@
"uuid": "^9.0.1"
},
"devDependencies": {
"@blocksuite/blocks": "0.0.0-20231116023037-31273bb7-nightly",
"@blocksuite/editor": "0.0.0-20231116023037-31273bb7-nightly",
"@blocksuite/global": "0.0.0-20231116023037-31273bb7-nightly",
"@blocksuite/icons": "2.1.35",
"@blocksuite/lit": "0.0.0-20231116023037-31273bb7-nightly",
"@blocksuite/store": "0.0.0-20231116023037-31273bb7-nightly",
"@blocksuite/blocks": "0.0.0-20231122113751-6bf81eb3-nightly",
"@blocksuite/editor": "0.0.0-20231122113751-6bf81eb3-nightly",
"@blocksuite/global": "0.0.0-20231122113751-6bf81eb3-nightly",
"@blocksuite/icons": "2.1.36",
"@blocksuite/lit": "0.0.0-20231122113751-6bf81eb3-nightly",
"@blocksuite/store": "0.0.0-20231122113751-6bf81eb3-nightly",
"@storybook/jest": "^0.2.3",
"@storybook/testing-library": "^0.2.2",
"@testing-library/react": "^14.0.0",
@@ -85,10 +80,10 @@
"@types/react-dom": "^18.2.13",
"@vanilla-extract/css": "^1.13.0",
"fake-indexeddb": "^5.0.0",
"typescript": "^5.2.2",
"typescript": "^5.3.2",
"vite": "^4.4.11",
"vitest": "0.34.6",
"yjs": "^13.6.8"
"yjs": "^13.6.10"
},
"version": "0.10.2"
"version": "0.10.3-canary.2"
}
@@ -1,47 +0,0 @@
import { Trans } from '@affine/i18n';
import { CloseIcon, Logo1Icon } from '@blocksuite/icons';
import {
downloadCloseButtonStyle,
downloadMessageStyle,
downloadTipContainerStyle,
downloadTipIconStyle,
downloadTipStyle,
linkStyle,
} from './index.css';
export const DownloadTips = ({ onClose }: { onClose: () => void }) => {
return (
<div
className={downloadTipContainerStyle}
data-testid="download-client-tip"
>
<div className={downloadTipStyle}>
<Logo1Icon className={downloadTipIconStyle} />
<div className={downloadMessageStyle}>
<Trans i18nKey="com.affine.banner.content">
This demo is limited.
<a
className={linkStyle}
href="https://affine.pro/download"
target="_blank"
rel="noreferrer"
>
Download the AFFiNE Client
</a>
for the latest features and Performance.
</Trans>
</div>
</div>
<div
className={downloadCloseButtonStyle}
onClick={onClose}
data-testid="download-client-tip-close-button"
>
<CloseIcon className={downloadTipIconStyle} />
</div>
</div>
);
};
export default DownloadTips;
@@ -1,13 +1,4 @@
import { keyframes, style } from '@vanilla-extract/css';
const slideDown = keyframes({
'0%': {
height: '0px',
},
'100%': {
height: '44px',
},
});
import { style } from '@vanilla-extract/css';
export const browserWarningStyle = style({
backgroundColor: 'var(--affine-background-warning-color)',
@@ -36,52 +27,31 @@ export const closeIconStyle = style({
position: 'relative',
zIndex: 1,
});
export const downloadTipContainerStyle = style({
backgroundColor: 'var(--affine-primary-color)',
color: 'var(--affine-white)',
export const tipsContainer = style({
backgroundColor: 'var(--affine-background-error-color)',
color: 'var(--affine-error-color)',
width: '100%',
height: '44px',
fontSize: 'var(--affine-font-base)',
fontSize: 'var(--affine-font-sm)',
fontWeight: '700',
display: 'flex',
justifyContent: 'center',
justifyContent: 'space-between',
alignItems: 'center',
position: 'relative',
animation: `${slideDown} .3s ease-in-out forwards`,
padding: '12px 16px',
position: 'sticky',
gap: '16px',
containerType: 'inline-size',
});
export const downloadTipStyle = style({
export const tipsMessage = style({
color: 'var(--affine-error-color)',
flexGrow: 1,
flexShrink: 1,
});
export const tipsRightItem = style({
display: 'flex',
justifyContent: 'center',
flexShrink: 0,
justifyContent: 'space-between',
alignItems: 'center',
});
export const downloadTipIconStyle = style({
color: 'var(--affine-white)',
width: '24px',
height: '24px',
fontSize: '24px',
position: 'relative',
zIndex: 1,
});
export const downloadCloseButtonStyle = style({
color: 'var(--affine-white)',
cursor: 'pointer',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
position: 'absolute',
right: '24px',
});
export const downloadMessageStyle = style({
color: 'var(--affine-white)',
marginLeft: '8px',
});
export const linkStyle = style({
color: 'var(--affine-white)',
textDecoration: 'underline',
':hover': {
textDecoration: 'underline',
},
':visited': {
color: 'var(--affine-white)',
textDecoration: 'underline',
},
gap: '16px',
});
@@ -1,2 +1,2 @@
export * from './browser-warning';
export * from './download-client';
export * from './local-demo-tips';
@@ -0,0 +1,54 @@
import { CloseIcon } from '@blocksuite/icons';
import { Button, IconButton } from '@toeverything/components/button';
import { useCallback } from 'react';
import * as styles from './index.css';
type LocalDemoTipsProps = {
isLoggedIn: boolean;
onLogin: () => void;
onEnableCloud: () => void;
onClose: () => void;
};
export const LocalDemoTips = ({
onClose,
isLoggedIn,
onLogin,
onEnableCloud,
}: LocalDemoTipsProps) => {
const content = isLoggedIn
? 'This is a local demo workspace, and the data is stored locally. We recommend enabling AFFiNE Cloud.'
: 'This is a local demo workspace, and the data is stored locally in the browser. We recommend Enabling AFFiNE Cloud or downloading the client for a better experience.';
const buttonLabel = isLoggedIn
? 'Enable AFFiNE Cloud'
: 'Sign in with AFFiNE Cloud';
const handleClick = useCallback(() => {
if (isLoggedIn) {
return onEnableCloud();
}
return onLogin();
}, [isLoggedIn, onEnableCloud, onLogin]);
return (
<div className={styles.tipsContainer} data-testid="local-demo-tips">
<div className={styles.tipsMessage}>{content}</div>
<div className={styles.tipsRightItem}>
<div>
<Button onClick={handleClick}>{buttonLabel}</Button>
</div>
<IconButton
onClick={onClose}
data-testid="local-demo-tips-close-button"
>
<CloseIcon />
</IconButton>
</div>
</div>
);
};
export default LocalDemoTips;
@@ -0,0 +1,22 @@
import { style } from '@vanilla-extract/css';
export {
closeIcon,
ellipsisTextOverflow,
halo,
icon,
particles,
root,
} from '../app-updater-button/index.css';
export const rootPadding = style({
padding: '0 24px',
});
export const label = style({
display: 'flex',
alignItems: 'center',
width: '100%',
height: '100%',
fontSize: 'var(--affine-font-sm)',
whiteSpace: 'nowrap',
});
@@ -0,0 +1,53 @@
import { CloseIcon, DownloadIcon } from '@blocksuite/icons';
import clsx from 'clsx';
import { useCallback, useState } from 'react';
import * as styles from './index.css';
// Although it is called an input, it is actually a button.
export function AppDownloadButton({
className,
style,
}: {
className?: string;
style?: React.CSSProperties;
}) {
const [show, setShow] = useState(true);
const handleClose = useCallback(() => {
setShow(false);
}, []);
// TODO: unify this type of literal value.
const handleClick = useCallback(() => {
const url = `https://affine.pro/download?channel=stable`;
open(url, '_blank');
}, []);
if (!show) {
return null;
}
return (
<button
style={style}
className={clsx([styles.root, styles.rootPadding, className])}
onClick={handleClick}
>
<div className={clsx([styles.label])}>
<DownloadIcon className={styles.icon} />
<span className={styles.ellipsisTextOverflow}>Download App</span>
</div>
<div
className={styles.closeIcon}
onClick={e => {
e.stopPropagation();
handleClose();
}}
>
<CloseIcon />
</div>
<div className={styles.particles} aria-hidden="true"></div>
<span className={styles.halo} aria-hidden="true"></span>
</button>
);
}
@@ -1,11 +1,11 @@
import { Skeleton } from '@mui/material';
import { assignInlineVars } from '@vanilla-extract/dynamic';
import clsx from 'clsx';
import { useAtom, useAtomValue } from 'jotai';
import debounce from 'lodash/debounce';
import { debounce } from 'lodash-es';
import type { PropsWithChildren, ReactElement } from 'react';
import { useEffect, useRef, useState } from 'react';
import { Skeleton } from '../../ui/skeleton';
import { fallbackHeaderStyle, fallbackStyle } from './fallback.css';
import {
floatingMaxWidth,
@@ -159,6 +159,7 @@ export const AppSidebarFallback = (): ReactElement | null => {
};
export * from './add-page-button';
export * from './app-download-button';
export * from './app-updater-button';
export * from './category-divider';
export * from './index.css';
@@ -1,7 +1,6 @@
import { EditorContainer } from '@blocksuite/editor';
import { assertExists } from '@blocksuite/global/utils';
import type { Page } from '@blocksuite/store';
import { Skeleton } from '@mui/material';
import clsx from 'clsx';
import { use } from 'foxact/use';
import type { CSSProperties, ReactElement } from 'react';
@@ -17,11 +16,12 @@ import {
import type { FallbackProps } from 'react-error-boundary';
import { ErrorBoundary } from 'react-error-boundary';
import { Skeleton } from '../../ui/skeleton';
import {
blockSuiteEditorHeaderStyle,
blockSuiteEditorStyle,
} from './index.css';
import { getPresets } from './preset';
import { editorPresets } from './preset';
interface BlockElement extends Element {
path: string[];
@@ -104,12 +104,10 @@ const BlockSuiteEditorImpl = ({
if (editor.page !== page) {
editor.page = page;
editor.pagePreset = editorPresets.pageModePreset;
editor.edgelessPreset = editorPresets.edgelessModePreset;
}
const presets = getPresets();
editor.pagePreset = presets.pageModePreset;
editor.edgelessPreset = presets.edgelessModePreset;
useLayoutEffect(() => {
if (editor) {
const disposes: (() => void)[] = [];
@@ -17,7 +17,7 @@ class CustomAttachmentService extends AttachmentService {
}
}
export function getPresets() {
function getPresets() {
const pageModePreset = PagePreset.map(preset => {
if (preset.schema.model.flavour === 'affine:attachment') {
return {
@@ -42,3 +42,5 @@ export function getPresets() {
edgelessModePreset,
};
}
export const editorPresets = getPresets();
@@ -2,7 +2,6 @@ import { WorkspaceFlavour } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
import { CollaborationIcon, SettingsIcon } from '@blocksuite/icons';
import { Skeleton } from '@mui/material';
import { Avatar } from '@toeverything/components/avatar';
import { Divider } from '@toeverything/components/divider';
import { Tooltip } from '@toeverything/components/tooltip';
@@ -12,6 +11,7 @@ import { getBlockSuiteWorkspaceAtom } from '@toeverything/infra/__internal__/wor
import { useAtomValue } from 'jotai/react';
import { useCallback } from 'react';
import { Skeleton } from '../../../ui/skeleton';
import {
StyledCard,
StyledIconContainer,
@@ -1,13 +0,0 @@
import { Skeleton } from '@mui/material';
import { memo } from 'react';
export const ListSkeleton = memo(function ListItemSkeleton() {
return (
<>
<Skeleton animation="wave" height={40} />
<Skeleton animation="wave" height={40} />
<Skeleton animation="wave" height={40} />
<Skeleton animation="wave" height={40} />
</>
);
});
@@ -60,9 +60,9 @@ export const notificationStyle = style({
opacity: 1,
height: 'var(--front-toast-height)',
vars: {
'--scale': 'var(--toasts-before)* 0.05 + 1',
'--scale': 'calc(1 - var(--toasts-before)* 0.05)',
'--y':
'translateY(calc(var(--lift-amount) * var(--toasts-before))) scale(calc(-1 * var(--scale)))',
'translateY(calc(var(--lift-amount) * var(--toasts-before))) scale(var(--scale))',
},
},
'&[data-mounted=true][data-expanded=true]': {
@@ -6,7 +6,7 @@ import {
type MenuItemProps,
} from '@toeverything/components/menu';
import { PublicLinkDisableModal } from '../../share-menu';
import { PublicLinkDisableModal } from '../../disable-public-link';
export const DisablePublicSharing = (props: MenuItemProps) => {
const t = useAFFiNEI18N();
@@ -64,14 +64,6 @@ export const innerBackdrop = style({
},
});
const range = (start: number, end: number) => {
const result = [];
for (let i = start; i < end; i++) {
result.push(i);
}
return result;
};
export const tag = style({
height: '20px',
display: 'flex',
@@ -94,14 +86,6 @@ export const tagSticky = style([
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
left: 0,
selectors: range(0, 20).reduce((selectors, i) => {
return {
...selectors,
[`&:nth-last-child(${i + 1})`]: {
right: `${i * 48}px`,
},
};
}, {}),
},
]);
@@ -18,6 +18,7 @@ interface TagItemProps {
tag: Tag;
idx: number;
mode: 'sticky' | 'list-item';
style?: React.CSSProperties;
}
// hack: map var(--affine-tag-xxx) colors to var(--affine-palette-line-xxx)
@@ -37,13 +38,14 @@ const tagColorMap = (color: string) => {
return mapping[color] || color;
};
const TagItem = ({ tag, idx, mode }: TagItemProps) => {
const TagItem = ({ tag, idx, mode, style }: TagItemProps) => {
return (
<div
data-testid="page-tag"
className={mode === 'sticky' ? styles.tagSticky : styles.tagListItem}
data-idx={idx}
title={tag.value}
style={style}
>
<div
className={styles.tagIndicator}
@@ -101,8 +103,30 @@ export const PageTags = ({
const tagsNormal = useMemo(() => {
const nTags = maxItems ? tags.slice(0, maxItems) : tags;
// sort tags by length
nTags.sort((a, b) => a.value.length - b.value.length);
const tagRightCharLength = nTags.reduceRight<number[]>(
(acc, tag) => {
const curr = acc[0] + Math.min(tag.value.length, 10);
return [curr, ...acc];
},
[0]
);
tagRightCharLength.shift();
return nTags.map((tag, idx) => (
<TagItem key={tag.id} tag={tag} idx={idx} mode="sticky" />
<TagItem
key={tag.id}
tag={tag}
idx={idx}
mode="sticky"
style={{
right: `calc(${tagRightCharLength[idx]}em)`,
}}
/>
));
}, [maxItems, tags]);
return (
@@ -192,9 +192,9 @@ export const pageGroupsAtom = atom(get => {
sorter.key === 'createDate' || sorter.key === 'updatedDate'
? sorter.key
: // default sort
!sorter.key
? DEFAULT_SORT_KEY
: undefined;
!sorter.key
? DEFAULT_SORT_KEY
: undefined;
}
return pagesToPageGroups(sorter.pages, groupBy);
});
@@ -1,4 +1,3 @@
import { useMediaQuery, useTheme } from '@mui/material';
import clsx from 'clsx';
import {
type BaseSyntheticEvent,
@@ -8,12 +7,6 @@ import {
import * as styles from './page-list.css';
export const useIsSmallDevices = () => {
const theme = useTheme();
const isSmallDevices = useMediaQuery(theme.breakpoints.down(900));
return isSmallDevices;
};
export function isToday(date: Date): boolean {
const today = new Date();
return (
@@ -92,10 +92,10 @@ export const RulesMode = ({
values={{
highlight: t['com.affine.editCollection.rules.tips.highlight'](),
}}
>
Pages that meet the rules will be added to the current collection{' '}
<span className={styles.rulesTitleHighlight}>highlight</span>.
</Trans>
components={{
2: <span className={styles.rulesTitleHighlight} />,
}}
/>
</div>
<div className={styles.rulesContainer}>
<div className={styles.rulesContainerLeft}>
@@ -1,5 +1,4 @@
import { Skeleton } from '@mui/material';
import { Skeleton } from '../../ui/skeleton';
import { SettingHeader } from './setting-header';
import { SettingRow } from './setting-row';
import { SettingWrapper } from './wrapper';
@@ -1,12 +1,11 @@
import { Skeleton } from '@mui/material';
import { FlexWrapper } from '../../ui/layout';
import { Skeleton } from '../../ui/skeleton';
export const WorkspaceListItemSkeleton = () => {
return (
<FlexWrapper
alignItems="center"
style={{ padding: '0 8px', height: 30, marginBottom: 4 }}
style={{ padding: '0 24px', height: 30, marginBottom: 4 }}
>
<Skeleton
variant="circular"
@@ -14,7 +13,12 @@ export const WorkspaceListItemSkeleton = () => {
height={14}
style={{ marginRight: 10 }}
/>
<Skeleton variant="rectangular" height={16} style={{ flexGrow: 1 }} />
<Skeleton
variant="rectangular"
height={16}
width={0}
style={{ flexGrow: 1 }}
/>
</FlexWrapper>
);
};
@@ -1,3 +0,0 @@
export * from './disable-public-link';
export * from './share-menu';
export * from './styles';
@@ -1,91 +0,0 @@
import { Button } from '@toeverything/components/button';
import { displayFlex, styled } from '../..';
export const TabItem = styled('li')<{ isActive?: boolean }>(({ isActive }) => {
{
return {
...displayFlex('center', 'center'),
flex: '1',
height: '30px',
color: 'var(--affine-text-primary-color)',
opacity: isActive ? 1 : 0.2,
fontWeight: '500',
fontSize: 'var(--affine-font-base)',
lineHeight: 'var(--affine-line-height)',
cursor: 'pointer',
transition: 'all 0.15s ease',
padding: '0 10px',
marginBottom: '4px',
borderRadius: '4px',
position: 'relative',
':hover': {
background: 'var(--affine-hover-color)',
opacity: 1,
color: isActive
? 'var(--affine-t/ext-primary-color)'
: 'var(--affine-text-secondary-color)',
svg: {
fill: isActive
? 'var(--affine-text-primary-color)'
: 'var(--affine-text-secondary-color)',
},
},
svg: {
fontSize: '20px',
marginRight: '12px',
},
':after': {
content: '""',
position: 'absolute',
bottom: '-6px',
left: '0',
width: '100%',
height: '2px',
background: 'var(--affine-text-primary-color)',
opacity: 0.2,
},
};
}
});
export const StyledIndicator = styled('div')(() => {
return {
height: '2px',
background: 'var(--affine-text-primary-color)',
position: 'absolute',
left: '0',
transition: 'left .3s, width .3s',
};
});
export const StyledInput = styled('input')(() => {
return {
padding: '4px 8px',
height: '28px',
color: 'var(--affine-placeholder-color)',
border: `1px solid ${'var(--affine-placeholder-color)'}`,
cursor: 'default',
overflow: 'hidden',
userSelect: 'text',
borderRadius: '4px',
flexGrow: 1,
marginRight: '10px',
};
});
export const StyledDisableButton = styled(Button)(() => {
return {
color: '#FF631F',
height: '32px',
border: 'none',
marginTop: '16px',
borderRadius: '8px',
padding: '0',
};
});
export const StyledLinkSpan = styled('span')(() => {
return {
marginLeft: '4px',
color: 'var(--affine-primary-color)',
fontWeight: '500',
cursor: 'pointer',
};
});
@@ -2,8 +2,6 @@ import { lightCssVariables } from '@toeverything/theme';
import type { ComplexStyleRule } from '@vanilla-extract/css';
import { globalStyle, style } from '@vanilla-extract/css';
import { breakpoints } from '../../styles/mui-theme';
export const appStyle = style({
width: '100%',
position: 'relative',
@@ -134,10 +132,10 @@ export const toolStyle = style({
flexDirection: 'column',
gap: '12px',
'@media': {
[breakpoints.down('md', true)]: {
'screen and (max-width: 960px)': {
right: 'calc((100vw - 640px) * 3 / 19 + 14px)',
},
[breakpoints.down('sm', true)]: {
'screen and (max-width: 640px)': {
right: '5px',
bottom: '5px',
},
@@ -149,10 +147,10 @@ export const toolStyle = style({
'&[data-in-trash-page="true"]': {
bottom: '70px',
'@media': {
[breakpoints.down('md', true)]: {
'screen and (max-width: 960px)': {
bottom: '80px',
},
[breakpoints.down('sm', true)]: {
'screen and (max-width: 640px)': {
bottom: '85px',
},
print: {
+1 -7
View File
@@ -1,6 +1,4 @@
export * from './components/list-skeleton';
export * from './styles';
export * from './ui/breadcrumbs';
export * from './ui/button';
export * from './ui/checkbox';
export * from './ui/empty';
@@ -8,12 +6,8 @@ export * from './ui/input';
export * from './ui/layout';
export * from './ui/lottie/collections-icon';
export * from './ui/lottie/delete-icon';
export * from './ui/menu';
export * from './ui/mui';
export * from './ui/popper';
export * from './ui/scrollbar';
export * from './ui/shared/container';
export * from './ui/skeleton';
export * from './ui/switch';
export * from './ui/table';
export * from './ui/toast';
export * from './ui/tree-view';
@@ -1,3 +1,2 @@
export * from './helper';
export * from './mui-theme';
export * from './mui-theme-provider';
export * from './styled';
@@ -1,3 +0,0 @@
import { alpha, css, keyframes, styled } from '@mui/material/styles';
export { alpha, css, keyframes, styled };
@@ -1,86 +0,0 @@
import type {
Breakpoint,
BreakpointsOptions,
ThemeOptions,
} from '@mui/material';
export const muiThemes = {
breakpoints: {
values: {
xs: 0,
sm: 640,
md: 960,
lg: 1280,
xl: 1920,
},
},
} satisfies ThemeOptions;
// Ported from mui
// See https://github.com/mui/material-ui/blob/eba90da5359ff9c58b02800dfe468dc6c0b95bd2/packages/mui-system/src/createTheme/createBreakpoints.js
// License under MIT
function createBreakpoints(breakpoints: BreakpointsOptions): Readonly<
Omit<BreakpointsOptions, 'up' | 'down'> & {
up: (key: Breakpoint | number, pure?: boolean) => string;
down: (key: Breakpoint | number, pure?: boolean) => string;
}
> {
const {
// The breakpoint **start** at this value.
// For instance with the first breakpoint xs: [xs, sm).
values = {
xs: 0, // phone
sm: 600, // tablet
md: 900, // small laptop
lg: 1200, // desktop
xl: 1536, // large screen
},
unit = 'px',
step = 5,
...other
} = breakpoints;
const keys = Object.keys(values) as ['xs', 'sm', 'md', 'lg', 'xl'];
function up(key: Breakpoint | number, pure = false) {
const value = typeof key === 'number' ? key : values[key];
const original = `(min-width:${value}${unit})`;
if (pure) {
return original;
}
return `@media ${original}`;
}
function down(key: Breakpoint | number, pure = false) {
const value = typeof key === 'number' ? key : values[key];
const original = `(max-width:${value - step / 100}${unit})`;
if (pure) {
return original;
}
return `@media ${original}`;
}
return {
keys,
values,
up,
down,
unit,
// between,
// only,
// not,
...other,
};
}
/**
* @example
* ```ts
* export const iconButtonStyle = style({
* [breakpoints.up('sm')]: {
* padding: '6px'
* },
* });
* ```
*/
export const breakpoints = createBreakpoints(muiThemes.breakpoints);
@@ -0,0 +1,3 @@
import styled from '@emotion/styled';
export { styled };
@@ -1,14 +0,0 @@
import type { BreadcrumbsProps } from '@mui/material/Breadcrumbs';
import MuiBreadcrumbs from '@mui/material/Breadcrumbs';
import type { ComponentType } from 'react';
import { styled } from '../../styles';
const StyledMuiBreadcrumbs = styled(MuiBreadcrumbs)(() => {
return {
color: 'var(--affine-text-primary-color)',
};
});
export const Breadcrumbs: ComponentType<BreadcrumbsProps> =
StyledMuiBreadcrumbs;
@@ -1,60 +0,0 @@
import { styled } from '../../styles';
import type { ButtonProps } from './interface';
import { getButtonColors } from './utils';
export const LoadingContainer = styled('div')<Pick<ButtonProps, 'type'>>(({
theme,
type = 'default',
}) => {
const { color } = getButtonColors(theme, type, false);
return `
margin: 0px auto;
width: 38px;
text-align: center;
.load {
width: 8px;
height: 8px;
background-color: ${color};
border-radius: 100%;
display: inline-block;
-webkit-animation: bouncedelay 1.4s infinite ease-in-out;
animation: bouncedelay 1.4s infinite ease-in-out;
/* Prevent first frame from flickering when animation starts */
-webkit-animation-fill-mode: both;
animation-fill-mode: both;
}
.load1 {
-webkit-animation-delay: -0.32s;
animation-delay: -0.32s;
}
.load2 {
-webkit-animation-delay: -0.16s;
animation-delay: -0.16s;
}
@-webkit-keyframes bouncedelay {
0%, 80%, 100% { -webkit-transform: scale(0) }
40% { -webkit-transform: scale(1.0) }
}
@keyframes bouncedelay {
0%, 80%, 100% {
transform: scale(0);
-webkit-transform: scale(0);
} 40% {
transform: scale(1.0);
-webkit-transform: scale(1.0);
}
}
`;
});
export const Loading = ({ type }: Pick<ButtonProps, 'type'>) => {
return (
<LoadingContainer type={type} className="load-container">
<div className="load load1"></div>
<div className="load load2"></div>
<div className="load"></div>
</LoadingContainer>
);
};
@@ -1,9 +1,6 @@
import type { Theme } from '@mui/material';
import type { ButtonProps } from './interface';
export const getButtonColors = (
_theme: Theme,
type: ButtonProps['type'],
disabled: boolean,
extend?: {
@@ -51,8 +51,8 @@ export const Checkbox = ({
const icon = indeterminate
? icons.indeterminate
: checked
? icons.checked
: icons.unchecked;
? icons.checked
: icons.unchecked;
return (
<div
@@ -1,6 +0,0 @@
/**
* @deprecated
* Use @toeverything/components/menu instead, this component only used in bookmark plugin, since it support set anchor as Range
*/
export * from './menu-item';
export * from './pure-menu';
@@ -1,44 +0,0 @@
import type { HTMLAttributes, PropsWithChildren, ReactElement } from 'react';
import { forwardRef } from 'react';
import {
StyledContent,
StyledEndIconWrapper,
StyledMenuItem,
StyledStartIconWrapper,
} from './styles';
export type IconMenuProps = PropsWithChildren<{
icon?: ReactElement;
endIcon?: ReactElement;
iconSize?: number;
disabled?: boolean;
active?: boolean;
disableHover?: boolean;
userFocused?: boolean;
gap?: string;
fontSize?: string;
}> &
HTMLAttributes<HTMLButtonElement>;
export const MenuItem = forwardRef<HTMLButtonElement, IconMenuProps>(
({ endIcon, icon, children, gap, fontSize, iconSize, ...props }, ref) => {
return (
<StyledMenuItem ref={ref} {...props}>
{icon && (
<StyledStartIconWrapper iconSize={iconSize} gap={gap}>
{icon}
</StyledStartIconWrapper>
)}
<StyledContent fontSize={fontSize}>{children}</StyledContent>
{endIcon && (
<StyledEndIconWrapper iconSize={iconSize} gap={gap}>
{endIcon}
</StyledEndIconWrapper>
)}
</StyledMenuItem>
);
}
);
MenuItem.displayName = 'MenuItem';
export default MenuItem;
@@ -1,24 +0,0 @@
import type { CSSProperties } from 'react';
import type { PurePopperProps } from '../popper';
import { PurePopper } from '../popper';
import { StyledMenuWrapper } from './styles';
export type PureMenuProps = PurePopperProps & {
width?: CSSProperties['width'];
height?: CSSProperties['height'];
};
export const PureMenu = ({
children,
placement,
width,
...otherProps
}: PureMenuProps) => {
return (
<PurePopper placement={placement} {...otherProps}>
<StyledMenuWrapper width={width} placement={placement}>
{children}
</StyledMenuWrapper>
</PurePopper>
);
};
@@ -1,115 +0,0 @@
import type { CSSProperties } from 'react';
import { displayFlex, styled, textEllipsis } from '../../styles';
import StyledPopperContainer from '../shared/container';
export const StyledMenuWrapper = styled(StyledPopperContainer, {
shouldForwardProp: propName =>
!['width', 'height'].includes(propName as string),
})<{
width?: CSSProperties['width'];
height?: CSSProperties['height'];
}>(({ width, height }) => {
return {
width,
height,
minWidth: '200px',
background: 'var(--affine-white)',
padding: '8px 4px',
fontSize: '14px',
backgroundColor: 'var(--affine-white)',
boxShadow: 'var(--affine-menu-shadow)',
userSelect: 'none',
};
});
export const StyledStartIconWrapper = styled('div')<{
gap?: CSSProperties['gap'];
iconSize?: CSSProperties['fontSize'];
}>(({ gap, iconSize }) => {
return {
display: 'flex',
marginRight: gap ? gap : '12px',
fontSize: iconSize ? iconSize : '20px',
color: 'var(--affine-icon-color)',
};
});
export const StyledEndIconWrapper = styled('div')<{
gap?: CSSProperties['gap'];
iconSize?: CSSProperties['fontSize'];
}>(({ gap, iconSize }) => {
return {
display: 'flex',
marginLeft: gap ? gap : '12px',
fontSize: iconSize ? iconSize : '20px',
color: 'var(--affine-icon-color)',
};
});
export const StyledContent = styled('div')<{
fontSize?: CSSProperties['fontSize'];
}>(({ fontSize }) => {
return {
textAlign: 'left',
flexGrow: 1,
fontSize: fontSize ? fontSize : 'var(--affine-font-base)',
...textEllipsis(1),
};
});
export const StyledMenuItem = styled('button')<{
isDir?: boolean;
disabled?: boolean;
active?: boolean;
disableHover?: boolean;
userFocused?: boolean;
}>(({
isDir = false,
disabled = false,
active = false,
disableHover = false,
userFocused = false,
}) => {
return {
width: '100%',
borderRadius: '5px',
padding: '0 14px',
fontSize: 'var(--affine-font-sm)',
height: '32px',
...displayFlex('flex-start', 'center'),
cursor: isDir ? 'pointer' : '',
position: 'relative',
backgroundColor: 'transparent',
color: disabled
? 'var(--affine-text-disable-color)'
: 'var(--affine-text-primary-color)',
svg: {
color: disabled
? 'var(--affine-text-disable-color)'
: 'var(--affine-icon-color)',
},
...(disabled
? {
cursor: 'not-allowed',
pointerEvents: 'none',
}
: {}),
':hover':
disabled || disableHover
? {}
: {
backgroundColor: 'var(--affine-hover-color)',
},
...(userFocused && !disabled
? {
backgroundColor: 'var(--affine-hover-color)',
}
: {}),
...(active && !disabled
? {
backgroundColor: 'var(--affine-hover-color)',
}
: {}),
};
});
-19
View File
@@ -1,19 +0,0 @@
import { ClickAwayListener as MuiClickAwayListener } from '@mui/base/ClickAwayListener';
import MuiAvatar from '@mui/material/Avatar';
import MuiBreadcrumbs from '@mui/material/Breadcrumbs';
import MuiCollapse from '@mui/material/Collapse';
import MuiFade from '@mui/material/Fade';
import MuiGrow from '@mui/material/Grow';
import MuiSkeleton from '@mui/material/Skeleton';
import MuiSlide from '@mui/material/Slide';
export {
MuiAvatar,
MuiBreadcrumbs,
MuiClickAwayListener,
MuiCollapse,
MuiFade,
MuiGrow,
MuiSkeleton,
MuiSlide,
};
@@ -1,3 +0,0 @@
export * from './interface';
export * from './popper';
export * from './pure-popper';
@@ -1,64 +0,0 @@
import {
type PopperPlacementType,
type PopperProps as PopperUnstyledProps,
} from '@mui/base/Popper';
import type { CSSProperties, ReactElement, ReactNode, Ref } from 'react';
export type VirtualElement = {
getBoundingClientRect: () => ClientRect | DOMRect;
contextElement?: Element;
};
export type PopperHandler = {
setVisible: (visible: boolean) => void;
};
export type PopperArrowProps = {
placement?: PopperPlacementType;
};
export type PopperProps = {
// Popover content
content?: ReactNode;
// Popover trigger
children: ReactElement;
// Whether the default is implicit
defaultVisible?: boolean;
// Used to manually control the visibility of the Popover
visible?: boolean;
// TODO: support focus
trigger?: 'hover' | 'click' | 'focus' | ('click' | 'hover' | 'focus')[];
// How long does it take for the mouse to display the Popover, in milliseconds
pointerEnterDelay?: number;
// How long does it take to hide the Popover after the mouse moves out, in milliseconds
pointerLeaveDelay?: number;
// Callback fired when the component closed or open
onVisibleChange?: (visible: boolean) => void;
// Popover container style
popoverStyle?: CSSProperties;
// Popover container class name
popoverClassName?: string;
// Anchor class name
anchorClassName?: string;
// Popover z-index
zIndex?: number;
offset?: [number, number];
showArrow?: boolean;
popperHandlerRef?: Ref<PopperHandler>;
onClickAway?: () => void;
triggerContainerStyle?: CSSProperties;
} & Omit<PopperUnstyledProps, 'open' | 'content'>;
@@ -1,97 +0,0 @@
import type { CSSProperties } from 'react';
import { forwardRef } from 'react';
import { styled } from '../../styles';
import type { PopperArrowProps } from './interface';
export const PopperArrow = forwardRef<HTMLElement, PopperArrowProps>(
function PopperArrow({ placement }, ref) {
return <StyledArrow placement={placement} ref={ref} />;
}
);
const getArrowStyle = (
placement: PopperArrowProps['placement'] = 'bottom',
backgroundColor: CSSProperties['backgroundColor']
) => {
if (placement.indexOf('bottom') === 0) {
return {
top: 0,
left: 0,
marginTop: '-0.9em',
width: '3em',
height: '1em',
'&::before': {
borderWidth: '0 1em 1em 1em',
borderColor: `transparent transparent ${backgroundColor} transparent`,
},
};
}
if (placement.indexOf('top') === 0) {
return {
bottom: 0,
left: 0,
marginBottom: '-0.9em',
width: '3em',
height: '1em',
'&::before': {
borderWidth: '1em 1em 0 1em',
borderColor: `${backgroundColor} transparent transparent transparent`,
},
};
}
if (placement.indexOf('left') === 0) {
return {
right: 0,
marginRight: '-0.9em',
height: '3em',
width: '1em',
'&::before': {
borderWidth: '1em 0 1em 1em',
borderColor: `transparent transparent transparent ${backgroundColor}`,
},
};
}
if (placement.indexOf('right') === 0) {
return {
left: 0,
marginLeft: '-0.9em',
height: '3em',
width: '1em',
'&::before': {
borderWidth: '1em 1em 1em 0',
borderColor: `transparent ${backgroundColor} transparent transparent`,
},
};
}
return {
display: 'none',
};
};
const StyledArrow = styled('span')<{
placement?: PopperArrowProps['placement'];
}>(({ placement }) => {
return {
position: 'absolute',
fontSize: '7px',
width: '3em',
'::before': {
content: '""',
margin: 'auto',
display: 'block',
width: 0,
height: 0,
borderStyle: 'solid',
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
},
...getArrowStyle(placement, 'var(--affine-tooltip)'),
};
});
@@ -1,300 +0,0 @@
import { ClickAwayListener } from '@mui/base/ClickAwayListener';
import { Popper as PopperUnstyled } from '@mui/base/Popper';
import Grow from '@mui/material/Grow';
import type { CSSProperties, PointerEvent } from 'react';
import {
cloneElement,
useEffect,
useImperativeHandle,
useMemo,
useRef,
useState,
} from 'react';
import { styled } from '../../styles';
import type { PopperProps, VirtualElement } from './interface';
export const Popper = ({
children,
content,
anchorEl: propsAnchorEl,
placement = 'top-start',
defaultVisible = false,
visible: propsVisible,
trigger = 'hover',
pointerEnterDelay = 500,
pointerLeaveDelay = 100,
onVisibleChange,
popoverStyle,
popoverClassName,
anchorClassName,
zIndex,
offset = [0, 5],
showArrow = false,
popperHandlerRef,
onClick,
onClickAway,
onPointerEnter,
onPointerLeave,
triggerContainerStyle = {},
...popperProps
}: PopperProps) => {
const [anchorEl, setAnchorEl] = useState<VirtualElement>();
const [visible, setVisible] = useState(defaultVisible);
//const [arrowRef, setArrowRef] = useState<HTMLElement>();
const arrowRef = null;
const pointerLeaveTimer = useRef<number>();
const pointerEnterTimer = useRef<number>();
const visibleControlledByParent = typeof propsVisible !== 'undefined';
const isAnchorCustom = typeof propsAnchorEl !== 'undefined';
const hasHoverTrigger = useMemo(() => {
return (
trigger === 'hover' ||
(Array.isArray(trigger) && trigger.includes('hover'))
);
}, [trigger]);
const hasClickTrigger = useMemo(() => {
return (
trigger === 'click' ||
(Array.isArray(trigger) && trigger.includes('click'))
);
}, [trigger]);
const onPointerEnterHandler = (e: PointerEvent<HTMLDivElement>) => {
onPointerEnter?.(e);
if (!hasHoverTrigger || visibleControlledByParent) {
return;
}
window.clearTimeout(pointerLeaveTimer.current);
pointerEnterTimer.current = window.window.setTimeout(() => {
setVisible(true);
}, pointerEnterDelay);
};
const onPointerLeaveHandler = (e: PointerEvent<HTMLDivElement>) => {
onPointerLeave?.(e);
if (!hasHoverTrigger || visibleControlledByParent) {
return;
}
window.clearTimeout(pointerEnterTimer.current);
pointerLeaveTimer.current = window.window.setTimeout(() => {
setVisible(false);
}, pointerLeaveDelay);
};
useEffect(() => {
onVisibleChange?.(visible);
}, [visible, onVisibleChange]);
useImperativeHandle(popperHandlerRef, () => {
return {
setVisible: (visible: boolean) => {
!visibleControlledByParent && setVisible(visible);
},
};
});
const mergedClass = [anchorClassName, children.props.className]
.filter(Boolean)
.join(' ');
return (
<ClickAwayListener
onClickAway={() => {
if (visibleControlledByParent) {
onClickAway?.();
} else {
setVisible(false);
}
}}
>
<Container style={triggerContainerStyle}>
{cloneElement(children, {
ref: (dom: HTMLDivElement) => setAnchorEl(dom),
onClick: (e: MouseEvent) => {
children.props.onClick?.(e);
if (!hasClickTrigger || visibleControlledByParent) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
onClick?.(e);
return;
}
setVisible(!visible);
},
onPointerEnter: onPointerEnterHandler,
onPointerLeave: onPointerLeaveHandler,
...(mergedClass
? {
className: mergedClass,
}
: {}),
})}
{content && (
<BasicStyledPopper
open={visibleControlledByParent ? propsVisible : visible}
zIndex={zIndex}
anchorEl={isAnchorCustom ? propsAnchorEl : anchorEl}
placement={placement}
transition
modifiers={[
{
name: 'offset',
options: {
offset,
},
},
{
name: 'arrow',
enabled: showArrow,
options: {
element: arrowRef,
},
},
]}
{...popperProps}
>
{({ TransitionProps }) => (
<Grow {...TransitionProps}>
<div
onPointerEnter={onPointerEnterHandler}
onPointerLeave={onPointerLeaveHandler}
style={popoverStyle}
className={popoverClassName}
onClick={() => {
if (hasClickTrigger && !visibleControlledByParent) {
setVisible(false);
}
}}
>
{showArrow ? (
placement.indexOf('bottom') === 0 ? (
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="11"
height="6"
viewBox="0 0 11 6"
fill="none"
>
<path
d="M6.38889 0.45C5.94444 -0.15 5.05555 -0.150001 4.61111 0.449999L0.499999 6L10.5 6L6.38889 0.45Z"
style={{ fill: 'var(--affine-tooltip)' }}
/>
</svg>
{content}
</div>
) : placement.indexOf('top') === 0 ? (
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
{content}
<svg
xmlns="http://www.w3.org/2000/svg"
width="11"
height="6"
viewBox="0 0 11 6"
fill="none"
>
<path
d="M4.61111 5.55C5.05556 6.15 5.94445 6.15 6.38889 5.55L10.5 -4.76837e-07H0.5L4.61111 5.55Z"
style={{ fill: 'var(--affine-tooltip)' }}
/>
</svg>
</div>
) : placement.indexOf('left') === 0 ? (
<>
{content}
<svg
xmlns="http://www.w3.org/2000/svg"
width="6"
height="10"
viewBox="0 0 6 10"
fill="none"
>
<path
d="M5.55 5.88889C6.15 5.44444 6.15 4.55555 5.55 4.11111L-4.76837e-07 0L-4.76837e-07 10L5.55 5.88889Z"
style={{ fill: 'var(--affine-tooltip)' }}
/>
</svg>
</>
) : placement.indexOf('right') === 0 ? (
<>
<svg
xmlns="http://www.w3.org/2000/svg"
width="6"
height="10"
viewBox="0 0 6 10"
style={{ fill: 'var(--affine-tooltip)' }}
>
<path
d="M0.45 4.11111C-0.15 4.55556 -0.15 5.44445 0.45 5.88889L6 10V0L0.45 4.11111Z"
style={{ fill: 'var(--affine-tooltip)' }}
/>
</svg>
{content}
</>
) : (
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
{content}
<svg
xmlns="http://www.w3.org/2000/svg"
width="11"
height="6"
viewBox="0 0 11 6"
fill="none"
>
<path
d="M4.61111 5.55C5.05556 6.15 5.94445 6.15 6.38889 5.55L10.5 -4.76837e-07H0.5L4.61111 5.55Z"
style={{ fill: 'var(--affine-tooltip)' }}
/>
</svg>
</div>
)
) : (
content
)}
</div>
</Grow>
)}
</BasicStyledPopper>
)}
</Container>
</ClickAwayListener>
);
};
// The children of ClickAwayListener must be a DOM Node to judge whether the click is outside, use node.contains
const Container = styled('div')({
display: 'contents',
});
export const BasicStyledPopper = styled(PopperUnstyled, {
shouldForwardProp: (propName: string) =>
!['zIndex'].some(name => name === propName),
})<{
zIndex?: CSSProperties['zIndex'];
}>(({ zIndex }) => {
return {
zIndex: zIndex ?? 'var(--affine-z-index-popover)',
};
});
@@ -1,66 +0,0 @@
import type { PopperProps as PopperUnstyledProps } from '@mui/base/Popper';
import Grow from '@mui/material/Grow';
import type { CSSProperties, PropsWithChildren } from 'react';
import { useState } from 'react';
import { PopperArrow } from './popover-arrow';
import { BasicStyledPopper } from './popper';
import { PopperWrapper } from './styles';
export type PurePopperProps = {
zIndex?: CSSProperties['zIndex'];
offset?: [number, number];
showArrow?: boolean;
} & PopperUnstyledProps &
PropsWithChildren;
export const PurePopper = (props: PurePopperProps) => {
const {
children,
zIndex,
offset,
showArrow = false,
modifiers = [],
placement,
...otherProps
} = props;
const [arrowRef, setArrowRef] = useState<HTMLElement | null>();
return (
<BasicStyledPopper
zIndex={zIndex}
transition
modifiers={[
{
name: 'offset',
options: {
offset,
},
},
{
name: 'arrow',
enabled: showArrow,
options: {
element: arrowRef,
},
},
...modifiers,
]}
placement={placement}
{...otherProps}
>
{({ TransitionProps }) => (
<Grow {...TransitionProps}>
<PopperWrapper>
{showArrow && (
<PopperArrow placement={placement} ref={setArrowRef} />
)}
{children}
</PopperWrapper>
</Grow>
)}
</BasicStyledPopper>
);
};
@@ -1,7 +0,0 @@
import { styled } from '../../styles';
export const PopperWrapper = styled('div')(() => {
return {
position: 'relative',
};
});
@@ -1,57 +0,0 @@
import type { PopperPlacementType } from '@mui/material';
import { styled } from '../../styles';
export type PopperDirection =
| 'none'
| 'left-top'
| 'left-bottom'
| 'right-top'
| 'right-bottom';
const getBorderRadius = (direction: PopperDirection, radius = '0') => {
const map: Record<PopperDirection, string> = {
none: `${radius}`,
'left-top': `0 ${radius} ${radius} ${radius}`,
'left-bottom': `${radius} ${radius} ${radius} 0`,
'right-top': `${radius} 0 ${radius} ${radius}`,
'right-bottom': `${radius} ${radius} 0 ${radius}`,
};
return map[direction];
};
export const placementToContainerDirection: Record<
PopperPlacementType,
PopperDirection
> = {
top: 'none',
'top-start': 'left-bottom',
'top-end': 'right-bottom',
right: 'none',
'right-start': 'left-top',
'right-end': 'left-bottom',
bottom: 'none',
'bottom-start': 'none',
'bottom-end': 'none',
left: 'none',
'left-start': 'right-top',
'left-end': 'right-bottom',
auto: 'none',
'auto-start': 'none',
'auto-end': 'none',
};
export const StyledPopperContainer = styled('div')<{
placement?: PopperPlacementType;
}>(({ placement = 'top' }) => {
const direction = placementToContainerDirection[placement];
const borderRadius = getBorderRadius(
direction,
'var(--affine-popover-radius)'
);
return {
borderRadius,
};
});
export default StyledPopperContainer;
@@ -0,0 +1,94 @@
import { keyframes, style } from '@vanilla-extract/css';
import type { PickStringFromUnion, SkeletonProps } from './types';
// variables
const bg = 'var(--affine-placeholder-color)';
const highlight = 'rgba(255, 255, 255, 0.4)';
const defaultHeight = '32px';
const pulseKeyframes = keyframes({
'0%': { opacity: 1 },
'50%': { opacity: 0.5 },
'100%': { opacity: 1 },
});
const waveKeyframes = keyframes({
'0%': { transform: 'translateX(-100%)' },
'50%': { transform: 'translateX(100%)' },
'100%': { transform: 'translateX(100%)' },
});
export const root = style({
display: 'block',
width: '100%',
height: defaultHeight,
flexShrink: 0,
/**
* paint background in ::before,
* so that we can use opacity to control the color
**/
position: 'relative',
'::before': {
content: '',
position: 'absolute',
borderRadius: 'inherit',
inset: 0,
opacity: 0.3,
backgroundColor: bg,
},
});
export const variant: Record<string, string> = {
circular: style({
width: defaultHeight,
borderRadius: '50%',
}),
rectangular: style({
borderRadius: '0px',
}),
rounded: style({
borderRadius: '8px',
}),
text: style({
borderRadius: '4px',
height: '1.2em',
marginTop: '0.2em',
marginBottom: '0.2em',
}),
};
export const animation: Record<
PickStringFromUnion<SkeletonProps['animation']>,
string
> = {
pulse: style({
animation: `${pulseKeyframes} 2s ease-in-out 0.5s infinite`,
}),
wave: style({
position: 'relative',
overflow: 'hidden',
/* Fix bug in Safari https://bugs.webkit.org/show_bug.cgi?id=68196 */
WebkitMaskImage: '-webkit-radial-gradient(white, black)',
'::after': {
animation: `${waveKeyframes} 2s linear 0.5s infinite`,
background: `linear-gradient(
90deg,
transparent,
${highlight},
transparent
)`,
content: '',
position: 'absolute',
transform:
'translateX(-100%)' /* Avoid flash during server-side hydration */,
bottom: 0,
left: 0,
right: 0,
top: 0,
},
}),
};
@@ -0,0 +1,2 @@
export * from './skeleton';
export * from './types';
@@ -0,0 +1,49 @@
import clsx from 'clsx';
import * as styles from './index.css';
import type { SkeletonProps } from './types';
function getSize(size: number | string) {
return typeof size === 'number' || /^\d+$/.test(size) ? `${size}px` : size;
}
/**
*
* @returns
*/
export const Skeleton = ({
animation = 'pulse',
variant = 'text',
children,
width: _width,
height: _height,
style: _style,
className: _className,
...props
}: SkeletonProps) => {
const width = _width !== undefined ? getSize(_width) : undefined;
const height = _height !== undefined ? getSize(_height) : undefined;
const style = {
width,
height,
...(_style || {}),
};
return (
<div
className={clsx(
_className,
styles.root,
styles.variant[variant],
animation && styles.animation[animation]
)}
style={style}
{...props}
>
{children}
</div>
);
};
@@ -0,0 +1,33 @@
import type { HTMLAttributes, PropsWithChildren } from 'react';
export interface SkeletonProps
extends PropsWithChildren,
HTMLAttributes<HTMLElement> {
/**
* The animation. If `false` the animation effect is disabled.
*/
animation?: 'pulse' | 'wave' | false;
/**
* The type of content that will be rendered.
* @default `'text'`
*/
variant?: 'circular' | 'rectangular' | 'rounded' | 'text' | string;
/**
* Width of the skeleton. Useful when the skeleton is inside an inline element with no width of its own.
*/
width?: number | string;
/**
* Height of the skeleton. Useful when you don't want to adapt the skeleton to a text element but for instance a card.
*/
height?: number | string;
/**
* Wrapper component. If not provided, the default element is a div.
*/
wrapper?: string;
}
export type PickStringFromUnion<T> = T extends string ? T : never;
@@ -1,10 +1,3 @@
// import Table from '@mui/material/Table';
// import TableBody from '@mui/material/TableBody';
// import TableCell from '@mui/material/TableCell';
// import TableHead from '@mui/material/TableHead';
// import TableRow from '@mui/material/TableRow';
//
export * from './interface';
export * from './table';
export * from './table-body';
@@ -30,15 +30,15 @@ const htmlToElement = <T extends ChildNode>(html: string | TemplateResult) => {
const createToastContainer = (portal?: HTMLElement) => {
portal = portal || document.body;
const styles = css`
position: absolute;
width: 100%;
position: fixed;
z-index: 9999;
top: 16px;
left: 16px;
right: 16px;
bottom: 78px;
left: 50%;
transform: translateX(-50%);
pointer-events: none;
display: flex;
flex-direction: column-reverse;
flex-direction: column;
align-items: center;
`;
const template = html`<div
@@ -55,6 +55,65 @@ export type ToastOptions = {
portal?: HTMLElement;
};
const animateToastOut = (toastElement: HTMLDivElement) => {
toastElement.style.opacity = '0';
setTimeout(() => toastElement.remove(), 300); // Match transition duration
};
const createAndShowNewToast = (
message: string,
duration: number,
portal?: HTMLElement
) => {
if (!ToastContainer || (portal && !portal.contains(ToastContainer))) {
ToastContainer = createToastContainer(portal);
}
const toastStyles = css`
position: absolute;
bottom: 0;
max-width: 480px;
text-align: center;
font-family: var(--affine-font-family);
font-size: var(--affine-font-sm);
padding: 10px 16px;
margin: 0;
color: var(--affine-white);
background: var(--affine-tooltip);
box-shadow: var(--affine-float-button-shadow);
border-radius: 8px;
opacity: 0;
transform: translateY(100%);
transition:
transform 0.3s cubic-bezier(0.25, 0.1, 0.25, 1),
opacity 0.3s ease;
`;
const toastTemplate = html`<div
style="${toastStyles}"
data-testid="affine-toast"
>
${message}
</div>`;
const toastElement = htmlToElement<HTMLDivElement>(toastTemplate);
// message is not trusted
toastElement.textContent = message;
ToastContainer.appendChild(toastElement);
logger.debug(`toast with message: "${message}"`);
window.dispatchEvent(
new CustomEvent('affine-toast:emit', { detail: message })
);
setTimeout(() => {
toastElement.style.opacity = '1';
toastElement.style.transform = 'translateY(0)';
}, 100);
setTimeout(() => {
animateToastOut(toastElement);
}, duration);
};
/**
* @example
* ```ts
@@ -63,80 +122,21 @@ export type ToastOptions = {
*/
export const toast = (
message: string,
{ duration = 2500, portal }: ToastOptions = {
duration: 2500,
}
{ duration = 3000, portal }: ToastOptions = {}
) => {
if (!ToastContainer || (portal && !portal.contains(ToastContainer))) {
ToastContainer = createToastContainer(portal);
if (ToastContainer && ToastContainer.children.length >= 2) {
// If there are already two toasts, remove the oldest one immediately
const oldestToast = ToastContainer.children[0] as HTMLDivElement;
oldestToast.remove();
}
const styles = css`
max-width: 480px;
text-align: center;
font-family: var(--affine-font-family);
font-size: var(--affine-font-sm);
padding: 6px 12px;
margin: 10px 0 0 0;
color: var(--affine-white);
background: var(--affine-tooltip);
box-shadow: var(--affine-float-button-shadow);
border-radius: 10px;
transition: all 230ms cubic-bezier(0.21, 1.02, 0.73, 1);
opacity: 0;
`;
// If there is one toast already, start its disappearing animation
if (ToastContainer && ToastContainer.children.length === 1) {
const currentToast = ToastContainer.children[0] as HTMLDivElement;
animateToastOut(currentToast);
}
const template = html`<div
style="${styles}"
data-testid="affine-toast"
></div>`;
const element = htmlToElement<HTMLDivElement>(template);
// message is not trusted
element.textContent = message;
ToastContainer.appendChild(element);
logger.debug(`toast with message: "${message}"`);
window.dispatchEvent(
new CustomEvent('affine-toast:emit', { detail: message })
);
const fadeIn = [
{
opacity: 0,
},
{ opacity: 1 },
];
const options = {
duration: 230,
easing: 'cubic-bezier(0.21, 1.02, 0.73, 1)',
fill: 'forwards' as const,
} satisfies KeyframeAnimationOptions;
element.animate(fadeIn, options);
setTimeout(() => {
const animation = element.animate(
// fade out
fadeIn.reverse(),
options
);
animation.finished
.then(() => {
element.style.maxHeight = '0';
element.style.margin = '0';
element.style.padding = '0';
// wait for transition
// ToastContainer = null;
element.addEventListener('transitionend', () => {
element.remove();
});
})
.catch(err => {
console.error(err);
});
}, duration);
return element;
createAndShowNewToast(message, duration, portal);
};
export default toast;
@@ -1,32 +0,0 @@
import { useState } from 'react';
import type { TreeNodeProps } from '../types';
export const useCollapsed = ({
initialCollapsedIds = [],
disableCollapse = false,
}: {
disableCollapse?: boolean;
initialCollapsedIds?: string[];
}) => {
// TODO: should record collapsedIds in localStorage
const [collapsedIds, setCollapsedIds] =
useState<string[]>(initialCollapsedIds);
const setCollapsed: TreeNodeProps['setCollapsed'] = (id, collapsed) => {
if (disableCollapse) {
return;
}
if (collapsed) {
setCollapsedIds(ids => [...ids, id]);
} else {
setCollapsedIds(ids => ids.filter(i => i !== id));
}
};
return {
collapsedIds,
setCollapsed,
};
};
export default useCollapsed;
@@ -1,63 +0,0 @@
import { useEffect, useState } from 'react';
import type { TreeViewProps } from '../types';
import { flattenIds } from '../utils';
export const useSelectWithKeyboard = <RenderProps>({
data,
enableKeyboardSelection,
onSelect,
}: Pick<
TreeViewProps<RenderProps>,
'data' | 'enableKeyboardSelection' | 'onSelect'
>) => {
const [selectedId, setSelectedId] = useState<string>();
// TODO: should record collapsedIds in localStorage
useEffect(() => {
if (!enableKeyboardSelection) {
return;
}
const flattenedIds = flattenIds<RenderProps>(data);
const handleDirectionKeyDown = (e: KeyboardEvent) => {
if (e.key !== 'ArrowDown' && e.key !== 'ArrowUp') {
return;
}
if (selectedId === undefined) {
setSelectedId(flattenedIds[0]);
return;
}
let selectedIndex = flattenedIds.indexOf(selectedId);
if (e.key === 'ArrowDown') {
selectedIndex < flattenedIds.length - 1 && selectedIndex++;
}
if (e.key === 'ArrowUp') {
selectedIndex > 0 && selectedIndex--;
}
setSelectedId(flattenedIds[selectedIndex]);
};
const handleEnterKeyDown = (e: KeyboardEvent) => {
if (e.key !== 'Enter') {
return;
}
selectedId && onSelect?.(selectedId);
};
document.addEventListener('keydown', handleDirectionKeyDown);
document.addEventListener('keydown', handleEnterKeyDown);
return () => {
document.removeEventListener('keydown', handleDirectionKeyDown);
document.removeEventListener('keydown', handleEnterKeyDown);
};
}, [data, enableKeyboardSelection, onSelect, selectedId]);
return {
selectedId,
};
};
export default useSelectWithKeyboard;
@@ -1,3 +0,0 @@
export * from './tree-node';
export * from './tree-view';
export * from './types';
@@ -1,44 +0,0 @@
import MuiCollapse from '@mui/material/Collapse';
import { lightTheme } from '@toeverything/theme';
import type { CSSProperties } from 'react';
import { alpha, styled } from '../../styles';
export const StyledCollapse = styled(MuiCollapse)<{
indent?: CSSProperties['paddingLeft'];
}>(({ indent = 12 }) => {
return {
paddingLeft: indent,
};
});
export const StyledTreeNodeWrapper = styled('div')(() => {
return {
position: 'relative',
};
});
export const StyledTreeNodeContainer = styled('div')<{ isDragging?: boolean }>(
({ isDragging = false }) => {
return {
background: isDragging ? 'var(--affine-hover-color)' : '',
};
}
);
export const StyledNodeLine = styled('div')<{
isOver: boolean;
isTop?: boolean;
}>(({ isOver, isTop = false }) => {
return {
position: 'absolute',
left: '0',
...(isTop ? { top: '-1px' } : { bottom: '-1px' }),
width: '100%',
paddingTop: '2x',
borderTop: '2px solid',
borderColor: isOver ? 'var(--affine-primary-color)' : 'transparent',
boxShadow: isOver
? `0px 0px 8px ${alpha(lightTheme.primaryColor, 0.35)}`
: 'none',
zIndex: 1,
};
});
@@ -1,88 +0,0 @@
import { useDroppable } from '@dnd-kit/core';
import { StyledNodeLine } from './styles';
import type { NodeLIneProps, TreeNodeItemProps } from './types';
export const NodeLine = <RenderProps,>({
node,
allowDrop = true,
isTop = false,
}: NodeLIneProps<RenderProps>) => {
const { isOver, setNodeRef } = useDroppable({
id: `${node.id}-${isTop ? 'top' : 'bottom'}-line`,
disabled: !allowDrop,
data: {
node,
position: {
topLine: isTop,
bottomLine: !isTop,
internal: false,
},
},
});
return (
<StyledNodeLine
ref={setNodeRef}
isOver={isOver && allowDrop}
isTop={isTop}
/>
);
};
export const TreeNodeItemWithDnd = <RenderProps,>({
node,
allowDrop,
setCollapsed,
...otherProps
}: TreeNodeItemProps<RenderProps>) => {
const { onAdd, onDelete } = otherProps;
const { isOver, setNodeRef } = useDroppable({
id: node.id,
disabled: !allowDrop,
data: {
node,
position: {
topLine: false,
bottomLine: false,
internal: true,
},
},
});
return (
<div ref={setNodeRef}>
<TreeNodeItem
onAdd={onAdd}
onDelete={onDelete}
node={node}
allowDrop={allowDrop}
setCollapsed={setCollapsed}
isOver={isOver}
{...otherProps}
/>
</div>
);
};
export const TreeNodeItem = <RenderProps,>({
node,
collapsed,
setCollapsed,
selectedId,
isOver = false,
onAdd,
onDelete,
disableCollapse,
allowDrop = true,
}: TreeNodeItemProps<RenderProps>) => {
return node.render?.(node, {
isOver: isOver && allowDrop,
onAdd: () => onAdd?.(node.id),
onDelete: () => onDelete?.(node.id),
collapsed,
setCollapsed,
isSelected: selectedId === node.id,
disableCollapse,
});
};
@@ -1,106 +0,0 @@
import { useDraggable } from '@dnd-kit/core';
import { useMemo } from 'react';
import {
StyledCollapse,
StyledTreeNodeContainer,
StyledTreeNodeWrapper,
} from './styles';
import { NodeLine, TreeNodeItem, TreeNodeItemWithDnd } from './tree-node-inner';
import type { TreeNodeProps } from './types';
export const TreeNodeWithDnd = <RenderProps,>(
props: TreeNodeProps<RenderProps>
) => {
const { draggingId, node, allowDrop } = props;
const { attributes, listeners, setNodeRef } = useDraggable({
id: props.node.id,
});
const isDragging = useMemo(
() => draggingId === node.id,
[draggingId, node.id]
);
return (
<StyledTreeNodeContainer
ref={setNodeRef}
isDragging={isDragging}
{...listeners}
{...attributes}
>
<TreeNode
{...props}
allowDrop={allowDrop === false ? allowDrop : !isDragging}
/>
</StyledTreeNodeContainer>
);
};
export const TreeNode = <RenderProps,>({
node,
index,
allowDrop = true,
...otherProps
}: TreeNodeProps<RenderProps>) => {
const { indent, enableDnd, collapsedIds } = otherProps;
const collapsed = collapsedIds.includes(node.id);
const { renderTopLine = true, renderBottomLine = true } = node;
return (
<>
<StyledTreeNodeWrapper>
{enableDnd && renderTopLine && index === 0 && (
<NodeLine
node={node}
{...otherProps}
allowDrop={allowDrop}
isTop={true}
/>
)}
{enableDnd ? (
<TreeNodeItemWithDnd
node={node}
index={index}
allowDrop={allowDrop}
collapsed={collapsed}
{...otherProps}
/>
) : (
<TreeNodeItem
node={node}
index={index}
allowDrop={allowDrop}
collapsed={collapsed}
{...otherProps}
/>
)}
{enableDnd &&
renderBottomLine &&
(!node.children?.length || collapsed) && (
<NodeLine node={node} {...otherProps} allowDrop={allowDrop} />
)}
</StyledTreeNodeWrapper>
<StyledCollapse in={!collapsed} indent={indent}>
{node.children &&
node.children.map((childNode, index) =>
enableDnd ? (
<TreeNodeWithDnd
key={childNode.id}
node={childNode}
index={index}
{...otherProps}
allowDrop={allowDrop}
/>
) : (
<TreeNode
key={childNode.id}
node={childNode}
index={index}
allowDrop={false}
{...otherProps}
/>
)
)}
</StyledCollapse>
</>
);
};
@@ -1,126 +0,0 @@
import type { DragEndEvent } from '@dnd-kit/core';
import {
closestCenter,
DndContext,
DragOverlay,
PointerSensor,
useSensor,
useSensors,
} from '@dnd-kit/core';
import { useCallback, useState } from 'react';
import useCollapsed from './hooks/use-collapsed';
import useSelectWithKeyboard from './hooks/use-select-with-keyboard';
import { TreeNode, TreeNodeWithDnd } from './tree-node';
import type { Node, TreeViewProps } from './types';
import { findNode } from './utils';
export const TreeView = <RenderProps,>({
data,
enableKeyboardSelection,
onSelect,
enableDnd = true,
disableCollapse,
onDrop,
...otherProps
}: TreeViewProps<RenderProps>) => {
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 8,
},
})
);
const { selectedId } = useSelectWithKeyboard({
data,
onSelect,
enableKeyboardSelection,
});
const { collapsedIds, setCollapsed } = useCollapsed({ disableCollapse });
const [draggingId, setDraggingId] = useState<string>();
const onDragEnd = useCallback(
(e: DragEndEvent) => {
const { active, over } = e;
const position = over?.data.current?.position;
const dropId = over?.data.current?.node.id;
setDraggingId(undefined);
if (!over || !active || !position) {
return;
}
onDrop?.(active.id as string, dropId, position);
},
[onDrop]
);
const onDragMove = useCallback((e: DragEndEvent) => {
setDraggingId(e.active.id as string);
}, []);
if (enableDnd) {
const treeNodes = data.map((node, index) => (
<TreeNodeWithDnd
key={node.id}
index={index}
collapsedIds={collapsedIds}
setCollapsed={setCollapsed}
node={node}
selectedId={selectedId}
enableDnd={enableDnd}
disableCollapse={disableCollapse}
draggingId={draggingId}
{...otherProps}
/>
));
const draggingNode = (function () {
let draggingNode: Node<RenderProps> | undefined;
if (draggingId) {
draggingNode = findNode(draggingId, data);
}
if (draggingNode) {
return (
<TreeNode
node={draggingNode}
index={0}
allowDrop={false}
collapsedIds={collapsedIds}
setCollapsed={() => {}}
{...otherProps}
/>
);
}
return null;
})();
return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragMove={onDragMove}
onDragEnd={onDragEnd}
>
{treeNodes}
<DragOverlay>{draggingNode}</DragOverlay>
</DndContext>
);
}
return (
<>
{data.map((node, index) => (
<TreeNode
key={node.id}
index={index}
collapsedIds={collapsedIds}
setCollapsed={setCollapsed}
node={node}
selectedId={selectedId}
enableDnd={enableDnd}
disableCollapse={disableCollapse}
{...otherProps}
/>
))}
</>
);
};
export default TreeView;
@@ -1,72 +0,0 @@
import type { CSSProperties, ReactNode } from 'react';
export type DropPosition = {
topLine: boolean;
bottomLine: boolean;
internal: boolean;
};
export type OnDrop = (
dragId: string,
dropId: string,
position: DropPosition
) => void;
export type Node<RenderProps = unknown> = {
id: string;
children?: Node<RenderProps>[];
render: (
node: Node<RenderProps>,
eventsAndStatus: {
isOver: boolean;
onAdd: () => void;
onDelete: () => void;
collapsed: boolean;
setCollapsed: (id: string, collapsed: boolean) => void;
isSelected: boolean;
disableCollapse?: ReactNode;
},
renderProps?: RenderProps
) => ReactNode;
renderTopLine?: boolean;
renderBottomLine?: boolean;
};
type CommonProps = {
enableDnd?: boolean;
enableKeyboardSelection?: boolean;
indent?: CSSProperties['paddingLeft'];
onAdd?: (parentId: string) => void;
onDelete?: (deleteId: string) => void;
onDrop?: OnDrop;
// Only trigger when the enableKeyboardSelection is true
onSelect?: (id: string) => void;
disableCollapse?: ReactNode;
};
export type TreeNodeProps<RenderProps = unknown> = {
node: Node<RenderProps>;
index: number;
collapsedIds: string[];
setCollapsed: (id: string, collapsed: boolean) => void;
allowDrop?: boolean;
selectedId?: string;
draggingId?: string;
} & CommonProps;
export type TreeNodeItemProps<RenderProps = unknown> = {
collapsed: boolean;
setCollapsed: (id: string, collapsed: boolean) => void;
isOver?: boolean;
} & TreeNodeProps<RenderProps>;
export type TreeViewProps<RenderProps = unknown> = {
data: Node<RenderProps>[];
initialCollapsedIds?: string[];
disableCollapse?: boolean;
} & CommonProps;
export type NodeLIneProps<RenderProps = unknown> = {
allowDrop: boolean;
isTop?: boolean;
} & Pick<TreeNodeProps<RenderProps>, 'node'>;
@@ -1,37 +0,0 @@
import type { Node } from './types';
export function flattenIds<RenderProps>(arr: Node<RenderProps>[]): string[] {
const result: string[] = [];
function flatten(arr: Node<RenderProps>[]) {
for (let i = 0, len = arr.length; i < len; i++) {
const item = arr[i];
result.push(item.id);
if (Array.isArray(item.children)) {
flatten(item.children);
}
}
}
flatten(arr);
return result;
}
export function findNode<RenderProps>(
id: string,
nodes: Node<RenderProps>[]
): Node<RenderProps> | undefined {
for (let i = 0, len = nodes.length; i < len; i++) {
const node = nodes[i];
if (node.id === id) {
return node;
}
if (node.children) {
const result = findNode(id, node.children);
if (result) {
return result;
}
}
}
return undefined;
}
@@ -20,12 +20,6 @@ export const productionCacheGroups = {
priority: Number.MAX_SAFE_INTEGER,
chunks: 'async' as const,
},
mui: {
name: `npm-mui`,
test: testPackageName(/[\\/]node_modules[\\/](mui|@mui)[\\/]/),
priority: 200,
enforce: true,
},
blocksuite: {
name: `npm-blocksuite`,
test: testPackageName(/[\\/]node_modules[\\/](@blocksuite)[\\/]/),
@@ -186,9 +186,6 @@ export const createConfiguration: (
'global',
'dist'
),
'@blocksuite/lit': blocksuiteBaseDir
? join(blocksuiteBaseDir, 'packages', 'lit', 'src')
: join(workspaceRoot, 'node_modules', '@blocksuite', 'lit', 'dist'),
'@blocksuite/store/providers/broadcast-channel': blocksuiteBaseDir
? join(
blocksuiteBaseDir,
@@ -129,8 +129,8 @@ export function getRuntimeConfig(buildFlags: BuildFlags): RuntimeConfig {
enableCaptcha: process.env.ENABLE_CAPTCHA
? process.env.ENABLE_CAPTCHA === 'true'
: buildFlags.mode === 'development'
? false
: currentBuildPreset.enableCaptcha,
? false
: currentBuildPreset.enableCaptcha,
enableEnhanceShareMode: process.env.ENABLE_ENHANCE_SHARE_MODE
? process.env.ENABLE_ENHANCE_SHARE_MODE === 'true'
: currentBuildPreset.enableEnhanceShareMode,
@@ -140,8 +140,8 @@ export function getRuntimeConfig(buildFlags: BuildFlags): RuntimeConfig {
enablePayment: process.env.ENABLE_PAYMENT
? process.env.ENABLE_PAYMENT !== 'false'
: buildFlags.mode === 'development'
? true
: currentBuildPreset.enablePayment,
? true
: currentBuildPreset.enablePayment,
};
if (buildFlags.mode === 'development') {
@@ -19,26 +19,8 @@ export default async function (cli_env: any, _: any) {
const config = createConfiguration(flags, runtimeConfig);
return merge(config, {
entry: {
'polyfill/intl-segmenter': {
import: resolve(rootPath, 'src/polyfill/intl-segmenter.ts'),
},
'polyfill/ses': {
import: resolve(rootPath, 'src/polyfill/ses.ts'),
},
plugin: {
dependOn: ['polyfill/intl-segmenter', 'polyfill/ses'],
import: resolve(rootPath, 'src/bootstrap/register-plugins.ts'),
},
app: {
chunkLoading: 'import',
dependOn: ['polyfill/intl-segmenter', 'polyfill/ses', 'plugin'],
import: resolve(rootPath, 'src/index.tsx'),
},
'_plugin/index.test': {
chunkLoading: 'import',
dependOn: ['polyfill/intl-segmenter', 'polyfill/ses', 'plugin'],
import: resolve(rootPath, 'src/_plugin/index.test.tsx'),
},
app: resolve(rootPath, 'src/index.tsx'),
'_plugin/index.test': resolve(rootPath, 'src/_plugin/index.test.tsx'),
},
plugins: [
new HTMLPlugin({
@@ -46,7 +28,7 @@ export default async function (cli_env: any, _: any) {
inject: 'body',
scriptLoading: 'module',
minify: false,
chunks: ['app', 'plugin', 'polyfill/intl-segmenter', 'polyfill/ses'],
chunks: ['app'],
filename: 'index.html',
templateParameters: {
GIT_SHORT_SHA: gitShortHash(),
@@ -59,12 +41,7 @@ export default async function (cli_env: any, _: any) {
scriptLoading: 'module',
minify: false,
publicPath: getPublicPath(flags),
chunks: [
'_plugin/index.test',
'plugin',
'polyfill/intl-segmenter',
'polyfill/ses',
],
chunks: ['_plugin/index.test'],
filename: '_plugin/index.html',
templateParameters: {
GIT_SHORT_SHA: gitShortHash(),
+32 -14
View File
@@ -2,7 +2,7 @@
"name": "@affine/core",
"type": "module",
"private": true,
"version": "0.10.2",
"version": "0.10.3-canary.2",
"scripts": {
"build": "yarn -T run build-core",
"dev": "yarn -T run dev-core",
@@ -17,6 +17,7 @@
},
"dependencies": {
"@affine-test/fixtures": "workspace:*",
"@affine/cmdk": "workspace:*",
"@affine/component": "workspace:*",
"@affine/debug": "workspace:*",
"@affine/env": "workspace:*",
@@ -24,49 +25,62 @@
"@affine/i18n": "workspace:*",
"@affine/templates": "workspace:*",
"@affine/workspace": "workspace:*",
"@blocksuite/block-std": "0.0.0-20231116023037-31273bb7-nightly",
"@blocksuite/blocks": "0.0.0-20231116023037-31273bb7-nightly",
"@blocksuite/editor": "0.0.0-20231116023037-31273bb7-nightly",
"@blocksuite/global": "0.0.0-20231116023037-31273bb7-nightly",
"@blocksuite/icons": "2.1.35",
"@blocksuite/lit": "0.0.0-20231116023037-31273bb7-nightly",
"@blocksuite/store": "0.0.0-20231116023037-31273bb7-nightly",
"@blocksuite/block-std": "0.0.0-20231122113751-6bf81eb3-nightly",
"@blocksuite/blocks": "0.0.0-20231122113751-6bf81eb3-nightly",
"@blocksuite/editor": "0.0.0-20231122113751-6bf81eb3-nightly",
"@blocksuite/global": "0.0.0-20231122113751-6bf81eb3-nightly",
"@blocksuite/icons": "2.1.36",
"@blocksuite/lit": "0.0.0-20231122113751-6bf81eb3-nightly",
"@blocksuite/store": "0.0.0-20231122113751-6bf81eb3-nightly",
"@blocksuite/virgo": "0.0.0-20231122113751-6bf81eb3-nightly",
"@dnd-kit/core": "^6.0.8",
"@dnd-kit/sortable": "^7.0.2",
"@dnd-kit/sortable": "^8.0.0",
"@emotion/cache": "^11.11.0",
"@emotion/react": "^11.11.1",
"@emotion/server": "^11.11.0",
"@emotion/styled": "^11.11.0",
"@marsidev/react-turnstile": "^0.3.1",
"@mui/material": "^5.14.14",
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.4",
"@radix-ui/react-scroll-area": "^1.0.5",
"@radix-ui/react-select": "^2.0.0",
"@react-hookz/web": "^23.1.0",
"@toeverything/components": "^0.0.46",
"@toeverything/theme": "^0.7.20",
"@vanilla-extract/css": "^1.13.0",
"@vanilla-extract/dynamic": "^2.0.3",
"async-call-rpc": "^6.3.1",
"bytes": "^3.1.2",
"clsx": "^2.0.0",
"css-spring": "^4.1.0",
"cssnano": "^6.0.1",
"dayjs": "^1.11.10",
"foxact": "^0.2.20",
"graphql": "^16.8.1",
"idb": "^7.1.1",
"intl-segmenter-polyfill-rs": "^0.1.6",
"jotai": "^2.4.3",
"jotai": "^2.5.1",
"jotai-devtools": "^0.7.0",
"lit": "^3.0.2",
"lottie-web": "^5.12.2",
"mini-css-extract-plugin": "^2.7.6",
"next-auth": "^4.23.2",
"nanoid": "^5.0.3",
"next-auth": "^4.24.5",
"next-themes": "^0.2.1",
"postcss-loader": "^7.3.3",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-error-boundary": "^4.0.11",
"react-is": "18.2.0",
"react-resizable-panels": "^0.0.55",
"react-resizable-panels": "^0.0.61",
"react-router-dom": "^6.16.0",
"rxjs": "^7.8.1",
"ses": "^0.18.8",
"swr": "2.2.4",
"uuid": "^9.0.1",
"valtio": "^1.11.2",
"y-protocols": "^1.0.6",
"yjs": "^13.6.8",
"yjs": "^13.6.10",
"zod": "^3.22.4"
},
"devDependencies": {
@@ -76,12 +90,15 @@
"@sentry/webpack-plugin": "^2.8.0",
"@svgr/webpack": "^8.1.0",
"@swc/core": "^1.3.93",
"@testing-library/react": "^14.0.0",
"@types/bytes": "^3.1.3",
"@types/lodash-es": "^4.17.9",
"@types/uuid": "^9.0.5",
"@types/webpack-env": "^1.18.2",
"copy-webpack-plugin": "^11.0.0",
"css-loader": "^6.8.1",
"express": "^4.18.2",
"fake-indexeddb": "^5.0.0",
"html-webpack-plugin": "^5.5.3",
"lodash-es": "^4.17.21",
"mime-types": "^2.1.35",
@@ -91,6 +108,7 @@
"swc-loader": "^0.2.3",
"swc-plugin-coverage-instrument": "^0.0.20",
"thread-loader": "^4.0.2",
"vitest": "0.34.6",
"webpack": "^5.89.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.1",
@@ -1,3 +1,6 @@
import '../polyfill/ses-lockdown';
import '../polyfill/intl-segmenter';
import { assertExists } from '@blocksuite/global/utils';
import {
getCurrentStore,
@@ -3,7 +3,6 @@ import {
DEFAULT_WORKSPACE_NAME,
PageNotFoundError,
} from '@affine/env/constant';
import type { LocalIndexedDBDownloadProvider } from '@affine/env/workspace';
import type { WorkspaceAdapter } from '@affine/env/workspace';
import {
LoadPriority,
@@ -14,11 +13,7 @@ import {
CRUD,
saveWorkspaceToLocalStorage,
} from '@affine/workspace/local/crud';
import {
getOrCreateWorkspace,
globalBlockSuiteSchema,
} from '@affine/workspace/manager';
import { createIndexedDBDownloadProvider } from '@affine/workspace/providers';
import { getOrCreateWorkspace } from '@affine/workspace/manager';
import { getBlockSuiteWorkspaceAtom } from '@toeverything/infra/__internal__/workspace';
import { getCurrentStore } from '@toeverything/infra/atom';
import { initEmptyPage } from '@toeverything/infra/blocksuite';
@@ -49,7 +44,6 @@ export const LocalAdapter: WorkspaceAdapter<WorkspaceFlavour.LOCAL> = {
blockSuiteWorkspace.meta.setName(DEFAULT_WORKSPACE_NAME);
if (runtimeConfig.enablePreloading) {
buildShowcaseWorkspace(blockSuiteWorkspace, {
schema: globalBlockSuiteSchema,
store: getCurrentStore(),
atoms: {
pageMode: setPageModeAtom,
@@ -66,15 +60,6 @@ export const LocalAdapter: WorkspaceAdapter<WorkspaceFlavour.LOCAL> = {
logger.error('init page with empty failed', error);
});
}
const provider = createIndexedDBDownloadProvider(
blockSuiteWorkspace.id,
blockSuiteWorkspace.doc,
{
awareness: blockSuiteWorkspace.awarenessStore.awareness,
}
) as LocalIndexedDBDownloadProvider;
provider.sync();
provider.whenReady.catch(console.error);
saveWorkspaceToLocalStorage(blockSuiteWorkspace.id);
logger.debug('create first workspace');
return [blockSuiteWorkspace.id];
+41 -26
View File
@@ -130,7 +130,9 @@ export const pageCollectionBaseAtom =
await userSetting.loaded;
const view = userSetting.view;
if (view) {
const collections: DeprecatedCollection[] = [...view.values()];
const collections: Omit<DeprecatedCollection, 'workspaceId'>[] = [
...view.values(),
];
//delete collections
view.clear();
return collections.map(v => {
@@ -147,36 +149,49 @@ export const pageCollectionBaseAtom =
return new Observable<BaseCollectionsDataType>(subscriber => {
const group = new DisposableGroup();
currentWorkspacePromise.then(async currentWorkspace => {
const workspaceSetting = getWorkspaceSetting(currentWorkspace);
migrateCollectionsFromIdbData(currentWorkspace).then(collections => {
if (collections.length) {
workspaceSetting.addCollection(...collections);
}
});
migrateCollectionsFromUserData(currentWorkspace).then(collections => {
if (collections.length) {
workspaceSetting.addCollection(...collections);
}
});
subscriber.next({
loading: false,
collections: workspaceSetting.collections,
});
if (group.disposed) {
return;
}
const fn = () => {
currentWorkspacePromise
.then(async currentWorkspace => {
const workspaceSetting = getWorkspaceSetting(currentWorkspace);
migrateCollectionsFromIdbData(currentWorkspace)
.then(collections => {
if (collections.length) {
workspaceSetting.addCollection(...collections);
}
})
.catch(error => {
console.error(error);
});
migrateCollectionsFromUserData(currentWorkspace)
.then(collections => {
if (collections.length) {
workspaceSetting.addCollection(...collections);
}
})
.catch(error => {
console.error(error);
});
subscriber.next({
loading: false,
collections: workspaceSetting.collections,
});
};
workspaceSetting.collectionsYArray.observe(fn);
group.add(() => {
workspaceSetting.collectionsYArray.unobserve(fn);
if (group.disposed) {
return;
}
const fn = () => {
subscriber.next({
loading: false,
collections: workspaceSetting.collections,
});
};
workspaceSetting.collectionsYArray.observe(fn);
group.add(() => {
workspaceSetting.collectionsYArray.unobserve(fn);
});
})
.catch(error => {
subscriber.error(error);
});
});
return () => {
group.dispose();
};
+1 -1
View File
@@ -40,7 +40,7 @@ export const authAtom = atom<AuthAtom>({
export const openDisableCloudAlertModalAtom = atom(false);
type PageMode = 'page' | 'edgeless';
export type PageMode = 'page' | 'edgeless';
type PageLocalSetting = {
mode: PageMode;
};
+1 -116
View File
@@ -1,129 +1,18 @@
import { setupGlobal } from '@affine/env/global';
import type { WorkspaceAdapter } from '@affine/env/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace';
import type { RootWorkspaceMetadata } from '@affine/workspace/atom';
import type { WorkspaceFlavour } from '@affine/env/workspace';
import {
type RootWorkspaceMetadataV2,
rootWorkspacesMetadataAtom,
workspaceAdaptersAtom,
} from '@affine/workspace/atom';
import {
getOrCreateWorkspace,
globalBlockSuiteSchema,
} from '@affine/workspace/manager';
import { assertExists } from '@blocksuite/global/utils';
import {
migrateLocalBlobStorage,
migrateWorkspace,
WorkspaceVersion,
} from '@toeverything/infra/blocksuite';
import { downloadBinary, overwriteBinary } from '@toeverything/y-indexeddb';
import type { createStore } from 'jotai/vanilla';
import { nanoid } from 'nanoid';
import { applyUpdate, Doc as YDoc, encodeStateAsUpdate } from 'yjs';
import { WorkspaceAdapters } from '../adapters/workspace';
import { performanceLogger } from '../shared';
const performanceSetupLogger = performanceLogger.namespace('setup');
async function tryMigration() {
const value = localStorage.getItem('jotai-workspaces');
if (value) {
try {
const metadata = JSON.parse(value) as RootWorkspaceMetadata[];
const promises: Promise<void>[] = [];
const newMetadata = [...metadata];
metadata.forEach(oldMeta => {
if (oldMeta.flavour === WorkspaceFlavour.LOCAL) {
let doc: YDoc;
const options = {
getCurrentRootDoc: async () => {
doc = new YDoc({
guid: oldMeta.id,
});
const downloadWorkspace = async (doc: YDoc): Promise<void> => {
const binary = await downloadBinary(doc.guid);
if (binary) {
applyUpdate(doc, binary);
}
await Promise.all(
[...doc.subdocs.values()].map(subdoc =>
downloadWorkspace(subdoc)
)
);
};
await downloadWorkspace(doc);
return doc;
},
createWorkspace: async () =>
getOrCreateWorkspace(nanoid(), WorkspaceFlavour.LOCAL),
getSchema: () => globalBlockSuiteSchema,
};
promises.push(
migrateWorkspace(
'version' in oldMeta ? oldMeta.version : undefined,
options
).then(async status => {
if (typeof status !== 'boolean') {
const adapter = WorkspaceAdapters[oldMeta.flavour];
const oldWorkspace = await adapter.CRUD.get(oldMeta.id);
const newId = await adapter.CRUD.create(status);
assertExists(
oldWorkspace,
'workspace should exist after migrate'
);
await adapter.CRUD.delete(oldWorkspace.blockSuiteWorkspace);
const index = newMetadata.findIndex(
meta => meta.id === oldMeta.id
);
newMetadata[index] = {
...oldMeta,
id: newId,
version: WorkspaceVersion.Surface,
};
await migrateLocalBlobStorage(status.id, newId);
console.log('workspace migrated', oldMeta.id, newId);
} else if (status) {
const index = newMetadata.findIndex(
meta => meta.id === oldMeta.id
);
newMetadata[index] = {
...oldMeta,
version: WorkspaceVersion.Surface,
};
const overWrite = async (doc: YDoc): Promise<void> => {
await overwriteBinary(doc.guid, encodeStateAsUpdate(doc));
return Promise.all(
[...doc.subdocs.values()].map(subdoc => overWrite(subdoc))
).then();
};
await overWrite(doc);
console.log('workspace migrated', oldMeta.id);
}
})
);
}
});
await Promise.all(promises)
.then(() => {
console.log('migration done');
})
.catch(e => {
console.error('migration failed', e);
})
.finally(() => {
localStorage.setItem('jotai-workspaces', JSON.stringify(newMetadata));
window.dispatchEvent(new CustomEvent('migration-done'));
window.$migrationDone = true;
});
} catch (e) {
console.error('error when migrating data', e);
}
}
}
export function createFirstAppData(store: ReturnType<typeof createStore>) {
const createFirst = (): RootWorkspaceMetadataV2[] => {
const Plugins = Object.values(WorkspaceAdapters).sort(
@@ -136,7 +25,6 @@ export function createFirstAppData(store: ReturnType<typeof createStore>) {
<RootWorkspaceMetadataV2>{
id,
flavour: Plugin.flavour,
version: WorkspaceVersion.DatabaseV3,
}
);
}).filter((ids): ids is RootWorkspaceMetadataV2 => !!ids);
@@ -163,9 +51,6 @@ export async function setup(store: ReturnType<typeof createStore>) {
performanceSetupLogger.info('setup global');
setupGlobal();
performanceSetupLogger.info('try migration');
await tryMigration();
performanceSetupLogger.info('get root workspace meta');
// do not read `rootWorkspacesMetadataAtom` before migration
await store.get(rootWorkspacesMetadataAtom);
@@ -15,7 +15,7 @@ export const errorDetailStyle = style({
});
export const errorTitle = style({
fontSize: '36px',
fontSize: '32px',
lineHeight: '44px',
fontWeight: 700,
});
@@ -20,7 +20,7 @@ import { useLocation, useParams } from 'react-router-dom';
import {
RecoverableError,
SessionFetchErrorRightAfterLoginOrSignUp,
type SessionFetchErrorRightAfterLoginOrSignUp,
} from '../../unexpected-application-state/errors';
import {
errorDescription,
@@ -33,7 +33,9 @@ import {
} from './affine-error-boundary.css';
import errorBackground from './error-status.assets.svg';
export type AffineErrorBoundaryProps = React.PropsWithChildren;
export type AffineErrorBoundaryProps = React.PropsWithChildren & {
height?: number | string;
};
type AffineError =
| QueryParamError
@@ -81,7 +83,7 @@ export class AffineErrorBoundary extends Component<
if (this.state.error.canRetry()) {
this.state.error.retry();
this.setState({
error: this.state.error,
error: null,
canRetryRecoveredError: this.state.error.canRetry(),
});
} else {
@@ -90,6 +92,10 @@ export class AffineErrorBoundary extends Component<
}
};
private readonly handleRefresh = () => {
this.setState({ error: null });
};
static getDerivedStateFromError(
error: AffineError
): AffineErrorBoundaryState {
@@ -121,14 +127,14 @@ export class AffineErrorBoundary extends Component<
</>
</>
);
} else if (error instanceof SessionFetchErrorRightAfterLoginOrSignUp) {
} else if (error instanceof RecoverableError) {
const retryButtonDesc = this.state.canRetryRecoveredError
? 'Refetch'
: 'Reload';
errorDetail = (
<>
<h1 className={errorTitle}>Sorry.. there was an error</h1>
<span className={errorDescription}> Fetching session failed </span>
<span className={errorDescription}> {error.message} </span>
<span className={errorDescription}>
If you are still experiencing this issue, please{' '}
<a
@@ -151,13 +157,22 @@ export class AffineErrorBoundary extends Component<
} else {
errorDetail = (
<>
<h1>Sorry.. there was an error</h1>
{error.message ?? error.toString()}
<h1 className={errorTitle}>Sorry.. there was an error</h1>
<code className={errorDescription}>
{error.message ?? error.toString()}
</code>
<Button
onClick={this.handleRefresh}
className={errorRetryButton}
type="primary"
>
Refresh
</Button>
</>
);
}
return (
<div className={errorLayout}>
<div className={errorLayout} style={{ height: this.props.height }}>
<div className={errorDetailStyle}>{errorDetail}</div>
<span className={errorDivider} />
<div
@@ -7,6 +7,7 @@ import {
import { Trans } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { Button } from '@toeverything/components/button';
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
import React, { useCallback } from 'react';
import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-status';
@@ -31,7 +32,7 @@ export const AfterSignInSendEmail = ({
onSignedIn?.();
}
const onResendClick = useCallback(async () => {
const onResendClick = useAsyncCallback(async () => {
if (verifyToken) {
await signIn(email, verifyToken, challenge);
}
@@ -6,6 +6,7 @@ import {
} from '@affine/component/auth-components';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { Button } from '@toeverything/components/button';
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
import { type FC, useCallback } from 'react';
import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-status';
@@ -29,7 +30,7 @@ export const AfterSignUpSendEmail: FC<AuthPanelProps> = ({
onSignedIn?.();
}
const onResendClick = useCallback(async () => {
const onResendClick = useAsyncCallback(async () => {
if (verifyToken) {
await signUp(email, verifyToken, challenge);
}
@@ -14,6 +14,7 @@ import {
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useMutation } from '@affine/workspace/affine/gql';
import { Button } from '@toeverything/components/button';
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
import { useSetAtom } from 'jotai/react';
import { useCallback, useState } from 'react';
@@ -146,7 +147,7 @@ export const SendEmail = ({
const buttonContent = useButtonContent(emailType);
const { loading, sendEmail } = useSendEmail(emailType);
const onSendEmail = useCallback(async () => {
const onSendEmail = useAsyncCallback(async () => {
// TODO: add error handler
await sendEmail(email);
@@ -6,6 +6,7 @@ import {
} from '@affine/component/auth-components';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { Button } from '@toeverything/components/button';
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
import { useSession } from 'next-auth/react';
import type { FC } from 'react';
@@ -26,7 +27,7 @@ export const SignInWithPassword: FC<AuthPanelProps> = ({
const [password, setPassword] = useState('');
const [passwordError, setPasswordError] = useState(false);
const onSignIn = useCallback(async () => {
const onSignIn = useAsyncCallback(async () => {
const res = await signInCloud('credentials', {
redirect: false,
email,
@@ -9,6 +9,7 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useMutation } from '@affine/workspace/affine/gql';
import { ArrowDownBigIcon, GoogleDuotoneIcon } from '@blocksuite/icons';
import { Button } from '@toeverything/components/button';
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
import { GraphQLError } from 'graphql';
import { type FC, useState } from 'react';
import { useCallback } from 'react';
@@ -52,7 +53,7 @@ export const SignIn: FC<AuthPanelProps> = ({
onSignedIn?.();
}
const onContinue = useCallback(async () => {
const onContinue = useAsyncCallback(async () => {
if (!validateEmail(email)) {
setIsValidEmail(false);
return;
@@ -10,6 +10,7 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useMutation, useQuery } from '@affine/workspace/affine/gql';
import { Button } from '@toeverything/components/button';
import { Loading } from '@toeverything/components/loading';
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
import { nanoid } from 'nanoid';
import { Suspense, useCallback, useEffect, useMemo } from 'react';
@@ -33,12 +34,12 @@ const usePaymentRedirect = () => {
mutation: checkoutMutation,
});
return useCallback(() => {
checkoutSubscription({ recurring, idempotencyKey })
.then(({ checkout }) => {
window.open(checkout, '_self', 'norefferer');
})
.catch(e => console.error(e));
return useAsyncCallback(async () => {
const { checkout } = await checkoutSubscription({
recurring,
idempotencyKey,
});
window.open(checkout, '_self', 'norefferer');
}, [recurring, idempotencyKey, checkoutSubscription]);
};
@@ -9,6 +9,7 @@ import {
Modal,
} from '@toeverything/components/modal';
import { Tooltip } from '@toeverything/components/tooltip';
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
import type {
LoadDBFileResult,
SelectDBFileLocationResult,
@@ -330,14 +331,13 @@ export const CreateWorkspaceModal = ({
]
);
const onConfirmName = useCallback(
(name: string) => {
const onConfirmName = useAsyncCallback(
async (name: string) => {
setWorkspaceName(name);
// this will be the last step for web for now
// fix me later
createLocalWorkspace(name).then(id => {
onCreate(id);
});
const id = await createLocalWorkspace(name);
onCreate(id);
},
[createLocalWorkspace, onCreate]
);
@@ -19,7 +19,7 @@ export const EnableAffineCloudModal = ({
const setAuthAtom = useSetAtom(authAtom);
const setOnceSignedInEvent = useSetAtom(setOnceSignedInEventAtom);
const confirm = useCallback(async () => {
const confirm = useCallback(() => {
return propsOnConfirm?.();
}, [propsOnConfirm]);
@@ -1,17 +1,17 @@
import { pushNotificationAtom } from '@affine/component/notification-center';
import { SettingRow } from '@affine/component/setting-components';
import { isDesktop } from '@affine/env/constant';
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { Button } from '@toeverything/components/button';
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
import type { SaveDBFileResult } from '@toeverything/infra/type';
import { useSetAtom } from 'jotai';
import { useCallback, useState } from 'react';
import { useState } from 'react';
import type { Doc } from 'yjs';
import { encodeStateAsUpdate } from 'yjs';
async function syncBlobsToSqliteDb(workspace: AffineOfficialWorkspace) {
if (window.apis && isDesktop) {
if (window.apis && environment.isDesktop) {
const bs = workspace.blockSuiteWorkspace.blob;
const blobsInDb = await window.apis.db.getBlobKeys(workspace.id);
const blobsInStorage = await bs.list();
@@ -32,7 +32,7 @@ async function syncBlobsToSqliteDb(workspace: AffineOfficialWorkspace) {
}
async function syncDocsToSqliteDb(workspace: AffineOfficialWorkspace) {
if (window.apis && isDesktop) {
if (window.apis && environment.isDesktop) {
const workspaceId = workspace.blockSuiteWorkspace.doc.guid;
const syncDoc = async (doc: Doc) => {
await window.apis.db.applyDocUpdate(
@@ -56,7 +56,7 @@ export const ExportPanel = ({ workspace }: ExportPanelProps) => {
const t = useAFFiNEI18N();
const [syncing, setSyncing] = useState(false);
const pushNotification = useSetAtom(pushNotificationAtom);
const onExport = useCallback(async () => {
const onExport = useAsyncCallback(async () => {
if (syncing) {
return;
}
@@ -167,7 +167,9 @@ export const CloudWorkspaceMembersPanel = ({
})}
,
<div className={style.goUpgradeWrapper} onClick={handleUpgrade}>
<span className={style.goUpgrade}>go upgrade</span>
<span className={style.goUpgrade}>
{t['com.affine.payment.member.description.go-upgrade']()}
</span>
<ArrowRightBigIcon className={style.arrowRight} />
</div>
</span>
@@ -77,8 +77,8 @@ export const ProfilePanel = ({ workspace, isOwner }: ProfilePanelProps) => {
);
const handleUploadAvatar = useCallback(
async (file: File) => {
await update(file)
(file: File) => {
update(file)
.then(() => {
pushNotification({
title: 'Update workspace avatar success',
@@ -11,9 +11,10 @@ import { WorkspaceFlavour } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { Button } from '@toeverything/components/button';
import { Tooltip } from '@toeverything/components/tooltip';
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
import { noop } from 'foxact/noop';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { toast } from '../../../utils';
import { EnableAffineCloudModal } from '../enable-affine-cloud-modal';
@@ -52,7 +53,7 @@ const PublishPanelAffine = (props: PublishPanelAffineProps) => {
);
}, []);
const copyUrl = useCallback(async () => {
const copyUrl = useAsyncCallback(async () => {
await navigator.clipboard.writeText(shareUrl);
toast(t['Copied link to clipboard']());
}, [shareUrl, t]);
@@ -16,6 +16,7 @@ import { useMutation, useQuery } from '@affine/workspace/affine/gql';
import { ArrowRightSmallIcon, CameraIcon } from '@blocksuite/icons';
import { Avatar } from '@toeverything/components/avatar';
import { Button } from '@toeverything/components/button';
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
import { validateAndReduceImage } from '@toeverything/hooks/use-block-suite-workspace-avatar-url';
import bytes from 'bytes';
import { useSetAtom } from 'jotai';
@@ -50,7 +51,7 @@ export const UserAvatar = () => {
mutation: removeAvatarMutation,
});
const handleUpdateUserAvatar = useCallback(
const handleUpdateUserAvatar = useAsyncCallback(
async (file: File) => {
try {
const reducedFile = await validateAndReduceImage(file);
@@ -1,3 +1,4 @@
import { Skeleton } from '@affine/component';
import { Pagination } from '@affine/component/member-components';
import {
SettingHeader,
@@ -19,9 +20,9 @@ import { Trans } from '@affine/i18n';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useMutation, useQuery } from '@affine/workspace/affine/gql';
import { ArrowRightSmallIcon } from '@blocksuite/icons';
import { Skeleton } from '@mui/material';
import { Button, IconButton } from '@toeverything/components/button';
import { Loading } from '@toeverything/components/loading';
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
import { useSetAtom } from 'jotai';
import { Suspense, useCallback, useMemo, useState } from 'react';
@@ -105,10 +106,10 @@ const SubscriptionSettings = () => {
plan === SubscriptionPlan.Free
? '0'
: price
? recurring === SubscriptionRecurring.Monthly
? String(price.amount / 100)
: String(price.yearlyAmount / 100)
: '?';
? recurring === SubscriptionRecurring.Monthly
? String(price.amount / 100)
: String(price.yearlyAmount / 100)
: '?';
const t = useAFFiNEI18N();
@@ -255,8 +256,8 @@ const PaymentMethodUpdater = () => {
});
const t = useAFFiNEI18N();
const update = useCallback(() => {
trigger(null, {
const update = useAsyncCallback(async () => {
await trigger(null, {
onSuccess: data => {
window.open(data.createCustomerPortal, '_blank', 'noopener noreferrer');
},
@@ -4,9 +4,10 @@ import {
resumeSubscriptionMutation,
} from '@affine/graphql';
import { useMutation } from '@affine/workspace/affine/gql';
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
import { nanoid } from 'nanoid';
import type { PropsWithChildren } from 'react';
import { useCallback, useState } from 'react';
import { useState } from 'react';
import { ConfirmLoadingModal, DowngradeModal } from './modals';
@@ -30,8 +31,8 @@ export const CancelAction = ({
mutation: cancelSubscriptionMutation,
});
const downgrade = useCallback(() => {
trigger(
const downgrade = useAsyncCallback(async () => {
await trigger(
{ idempotencyKey },
{
onSuccess: data => {
@@ -78,8 +79,8 @@ export const ResumeAction = ({
mutation: resumeSubscriptionMutation,
});
const resume = useCallback(() => {
trigger(
const resume = useAsyncCallback(async () => {
await trigger(
{ idempotencyKey },
{
onSuccess: data => {
@@ -13,6 +13,10 @@ export const scrollArea = style({
overflowX: 'auto',
scrollSnapType: 'x mandatory',
paddingBottom: '21px',
/** Avoid box-shadow clipping */
paddingTop: '21px',
marginTop: '-21px',
});
export const scrollBar = style({
@@ -15,6 +15,7 @@ import { useMutation } from '@affine/workspace/affine/gql';
import { DoneIcon } from '@blocksuite/icons';
import { Button } from '@toeverything/components/button';
import { Tooltip } from '@toeverything/components/tooltip';
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
import { useSetAtom } from 'jotai';
import { useAtom } from 'jotai';
import { nanoid } from 'nanoid';
@@ -127,21 +128,25 @@ export function getPlanDetail(t: ReturnType<typeof useAFFiNEI18N>) {
}
export const PlanCard = (props: PlanCardProps) => {
const t = useAFFiNEI18N();
const { detail, subscription, recurring } = props;
const loggedIn = useCurrentLoginStatus() === 'authenticated';
const currentPlan = subscription?.plan ?? SubscriptionPlan.Free;
const isCurrent = loggedIn && detail.plan === currentPlan;
const isPro = detail.plan === SubscriptionPlan.Pro;
return (
<div
data-current={isCurrent}
key={detail.plan}
className={isCurrent ? styles.currentPlanCard : styles.planCard}
className={isPro ? styles.proPlanCard : styles.planCard}
>
<div className={styles.planTitle}>
<p>
{detail.plan}{' '}
<span className={isCurrent ? styles.proPlanTitle : ''}>
{detail.plan}
</span>{' '}
{'discount' in detail &&
recurring === SubscriptionRecurring.Yearly && (
<span className={styles.discountLabel}>
@@ -161,7 +166,9 @@ export const PlanCard = (props: PlanCardProps) => {
? detail.price
: detail.yearlyPrice}
</span>
<span className={styles.planPriceDesc}>per month</span>
<span className={styles.planPriceDesc}>
{t['com.affine.payment.price-description.per-month']()}
</span>
</>
)}
</p>
@@ -364,7 +371,7 @@ const Upgrade = ({
}, [onSubscriptionUpdate]);
const [, openPaymentDisableModal] = useAtom(openPaymentDisableAtom);
const upgrade = useCallback(() => {
const upgrade = useAsyncCallback(async () => {
if (!runtimeConfig.enablePayment) {
openPaymentDisableModal(true);
return;
@@ -373,7 +380,7 @@ const Upgrade = ({
if (newTabRef.current) {
newTabRef.current.focus();
} else {
trigger(
await trigger(
{ recurring, idempotencyKey },
{
onSuccess: data => {
@@ -439,8 +446,8 @@ const ChangeRecurring = ({
mutation: updateSubscriptionMutation,
});
const change = useCallback(() => {
trigger(
const change = useAsyncCallback(async () => {
await trigger(
{ recurring: to, idempotencyKey },
{
onSuccess: data => {
@@ -492,7 +499,7 @@ const ChangeRecurring = ({
const SignUpAction = ({ children }: PropsWithChildren) => {
const setOpen = useSetAtom(authAtom);
const onClickSignIn = useCallback(async () => {
const onClickSignIn = useCallback(() => {
setOpen(state => ({
...state,
openModal: true,
@@ -1,4 +1,4 @@
import { Skeleton } from '@mui/material';
import { Skeleton } from '@affine/component';
import { PlanLayout } from './layout';
import * as styles from './skeleton.css';
@@ -9,7 +9,7 @@ export const recurringRadioGroup = style({
export const radioButtonDiscount = style({
marginLeft: '4px',
color: 'var(--affine-primary-color)',
color: 'var(--affine-brand-color)',
fontWeight: 400,
});
export const radioButtonText = style({
@@ -46,23 +46,41 @@ export const planCard = style({
},
});
export const currentPlanCard = style([
export const proPlanCard = style([
planCard,
{
borderWidth: '2px',
borderColor: 'var(--affine-primary-color)',
borderWidth: '1px',
borderColor: 'var(--affine-brand-color)',
boxShadow: 'var(--affine-shadow-2)',
position: 'relative',
'::after': {
content: '',
position: 'absolute',
inset: '-1px',
borderRadius: 'inherit',
boxShadow: '0px 0px 0px 2px var(--affine-brand-color)',
opacity: 0.3,
zIndex: 1,
pointerEvents: 'none',
},
},
]);
export const proPlanTitle = style({
backgroundColor: 'var(--affine-brand-color)',
color: 'var(--affine-white)',
padding: '0px 6px',
borderRadius: '4px',
height: '24px',
display: 'inline-block',
});
export const discountLabel = style({
color: 'var(--affine-primary-color)',
color: 'var(--affine-text-emphasis-color)',
marginLeft: '8px',
lineHeight: '20px',
fontSize: 'var(--affine-font-xs)',
fontWeight: 500,
padding: '0 4px',
backgroundColor: 'var(--affine-blue-50)',
borderRadius: '4px',
display: 'inline-block',
height: '100%',
@@ -2,6 +2,7 @@ import { pushNotificationAtom } from '@affine/component/notification-center';
import { WorkspaceSubPath } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
import { useSetAtom } from 'jotai';
import { useAtomValue } from 'jotai';
@@ -65,7 +66,7 @@ export const WorkspaceSetting = ({ workspaceId }: { workspaceId: string }) => {
workspaces,
]);
const handleDeleteWorkspace = useCallback(async () => {
const handleDeleteWorkspace = useAsyncCallback(async () => {
closeAndJumpOut();
await deleteWorkspace(workspaceId);
@@ -75,7 +76,7 @@ export const WorkspaceSetting = ({ workspaceId }: { workspaceId: string }) => {
});
}, [closeAndJumpOut, deleteWorkspace, pushNotification, t, workspaceId]);
const handleLeaveWorkspace = useCallback(async () => {
const handleLeaveWorkspace = useAsyncCallback(async () => {
closeAndJumpOut();
await leaveWorkspace(workspaceId, workspaceName);
@@ -1,4 +1,3 @@
import { ShareMenu } from '@affine/component/share-menu';
import {
type AffineOfficialWorkspace,
WorkspaceFlavour,
@@ -6,10 +5,9 @@ import {
import type { Page } from '@blocksuite/store';
import { useCallback, useState } from 'react';
import { useExportPage } from '../../../hooks/affine/use-export-page';
import { useIsSharedPage } from '../../../hooks/affine/use-is-shared-page';
import { useOnTransformWorkspace } from '../../../hooks/root/use-on-transform-workspace';
import { EnableAffineCloudModal } from '../enable-affine-cloud-modal';
import { ShareMenu } from './share-menu';
type SharePageModalProps = {
workspace: AffineOfficialWorkspace;
@@ -19,7 +17,7 @@ type SharePageModalProps = {
export const SharePageModal = ({ workspace, page }: SharePageModalProps) => {
const onTransformWorkspace = useOnTransformWorkspace();
const [open, setOpen] = useState(false);
const exportHandler = useExportPage(page);
const handleConfirm = useCallback(() => {
if (workspace.flavour !== WorkspaceFlavour.LOCAL) {
return;
@@ -31,15 +29,13 @@ export const SharePageModal = ({ workspace, page }: SharePageModalProps) => {
);
setOpen(false);
}, [onTransformWorkspace, workspace]);
return (
<>
<ShareMenu
workspace={workspace}
currentPage={page}
useIsSharedPage={useIsSharedPage}
onEnableAffineCloud={() => setOpen(true)}
togglePagePublic={async () => {}}
exportHandler={exportHandler}
/>
{workspace.flavour === WorkspaceFlavour.LOCAL ? (
<EnableAffineCloudModal
@@ -0,0 +1 @@
export * from './share-menu';
@@ -1,19 +1,16 @@
import { ExportMenuItems } from '@affine/component/page-list';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { LinkIcon } from '@blocksuite/icons';
import { Button } from '@toeverything/components/button';
import { Divider } from '@toeverything/components/divider';
import { ExportMenuItems } from '../page-list/operation-menu-items/export';
import { useExportPage } from '../../../../hooks/affine/use-export-page';
import * as styles from './index.css';
import type { ShareMenuProps } from './share-menu';
import { useSharingUrl } from './use-share-url';
export const ShareExport = ({
workspace,
currentPage,
exportHandler,
}: ShareMenuProps) => {
export const ShareExport = ({ workspace, currentPage }: ShareMenuProps) => {
const t = useAFFiNEI18N();
const workspaceId = workspace.id;
const pageId = currentPage.id;
@@ -22,6 +19,7 @@ export const ShareExport = ({
pageId,
urlType: 'workspace',
});
const exportHandler = useExportPage(currentPage);
return (
<>
@@ -12,9 +12,11 @@ import { Button } from '@toeverything/components/button';
import { Divider } from '@toeverything/components/divider';
import { Menu } from '@toeverything/components/menu';
import { useIsSharedPage } from '../../../../hooks/affine/use-is-shared-page';
import * as styles from './index.css';
import { ShareExport } from './share-export';
import { SharePage } from './share-page';
export interface ShareMenuProps<
Workspace extends AffineOfficialWorkspace =
| AffineCloudWorkspace
@@ -23,13 +25,7 @@ export interface ShareMenuProps<
> {
workspace: Workspace;
currentPage: Page;
useIsSharedPage: (
workspaceId: string,
pageId: string
) => [isSharePage: boolean, setIsSharePage: (enable: boolean) => void];
onEnableAffineCloud: () => void;
togglePagePublic: () => Promise<void>;
exportHandler: (type: 'pdf' | 'html' | 'png' | 'markdown') => Promise<void>;
}
const ShareMenuContent = (props: ShareMenuProps) => {
@@ -73,9 +69,14 @@ const LocalShareMenu = (props: ShareMenuProps) => {
const CloudShareMenu = (props: ShareMenuProps) => {
const t = useAFFiNEI18N();
const { workspace, currentPage, useIsSharedPage } = props;
const [isSharedPage] = useIsSharedPage(workspace.id, currentPage.id);
const {
workspace: { id: workspaceId },
currentPage,
} = props;
const { isSharedPage } = useIsSharedPage(
workspaceId,
currentPage.spaceDoc.guid
);
return (
<Menu
@@ -1,16 +1,23 @@
import {
Input,
RadioButton,
RadioButtonGroup,
Switch,
toast,
} from '@affine/component';
import { PublicLinkDisableModal } from '@affine/component/disable-public-link';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ArrowRightSmallIcon } from '@blocksuite/icons';
import { Button } from '@toeverything/components/button';
import { Menu, MenuItem, MenuTrigger } from '@toeverything/components/menu';
import { useState } from 'react';
import { useAtomValue } from 'jotai';
import { useMemo, useState } from 'react';
import { useCallback } from 'react';
import { RadioButton, RadioButtonGroup } from '../../ui/button';
import Input from '../../ui/input';
import { Switch } from '../../ui/switch';
import { toast } from '../../ui/toast';
import { PublicLinkDisableModal } from './disable-public-link';
import type { PageMode } from '../../../../atoms';
import { currentModeAtom } from '../../../../atoms/mode';
import { useIsSharedPage } from '../../../../hooks/affine/use-is-shared-page';
import * as styles from './index.css';
import type { ShareMenuProps } from './share-menu';
import { useSharingUrl } from './use-share-url';
@@ -63,10 +70,29 @@ export const LocalSharePage = (props: ShareMenuProps) => {
export const AffineSharePage = (props: ShareMenuProps) => {
const {
workspace: { id: workspaceId },
currentPage: { id: pageId },
currentPage,
} = props;
const [isPublic, setIsPublic] = props.useIsSharedPage(workspaceId, pageId);
const pageId = currentPage.id;
const [showDisable, setShowDisable] = useState(false);
const {
isSharedPage,
enableShare,
changeShare,
currentShareMode,
disableShare,
} = useIsSharedPage(workspaceId, currentPage.spaceDoc.guid);
const currentPageMode = useAtomValue(currentModeAtom);
const defaultMode = useMemo(() => {
if (isSharedPage) {
// if it's a shared page, use the share mode
return currentShareMode;
}
// default to current page mode
return currentPageMode;
}, [currentPageMode, currentShareMode, isSharedPage]);
const [mode, setMode] = useState<PageMode>(defaultMode);
const { sharingUrl, onClickCopyLink } = useSharingUrl({
workspaceId,
pageId,
@@ -75,16 +101,26 @@ export const AffineSharePage = (props: ShareMenuProps) => {
const t = useAFFiNEI18N();
const onClickCreateLink = useCallback(() => {
setIsPublic(true);
}, [setIsPublic]);
enableShare(mode);
}, [enableShare, mode]);
const onDisablePublic = useCallback(() => {
setIsPublic(false);
disableShare();
toast('Successfully disabled', {
portal: document.body,
});
setShowDisable(false);
}, [setIsPublic]);
}, [disableShare]);
const onShareModeChange = useCallback(
(value: PageMode) => {
setMode(value);
if (isSharedPage) {
changeShare(value);
}
},
[changeShare, isSharedPage]
);
return (
<>
@@ -103,10 +139,12 @@ export const AffineSharePage = (props: ShareMenuProps) => {
fontSize: 'var(--affine-font-xs)',
lineHeight: '20px',
}}
value={isPublic ? sharingUrl : `${runtimeConfig.serverUrlPrefix}/...`}
value={
isSharedPage ? sharingUrl : `${runtimeConfig.serverUrlPrefix}/...`
}
readOnly
/>
{isPublic ? (
{isSharedPage ? (
<Button
onClick={onClickCopyLink}
data-testid="share-menu-copy-link-button"
@@ -125,36 +163,35 @@ export const AffineSharePage = (props: ShareMenuProps) => {
</Button>
)}
</div>
{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['com.affine.pageMode.page']()}
</RadioButton>
<RadioButton
className={styles.radioButton}
value={'edgeless'}
spanStyle={styles.spanStyle}
>
{t['com.affine.pageMode.edgeless']()}
</RadioButton>
</RadioButtonGroup>
</div>
<div className={styles.rowContainerStyle}>
<div className={styles.subTitleStyle}>
{t['com.affine.share-menu.ShareMode']()}
</div>
) : null}
{isPublic ? (
<div>
<RadioButtonGroup
className={styles.radioButtonGroup}
defaultValue={defaultMode}
value={mode}
onValueChange={onShareModeChange}
>
<RadioButton
className={styles.radioButton}
value={'page'}
spanStyle={styles.spanStyle}
>
{t['com.affine.pageMode.page']()}
</RadioButton>
<RadioButton
className={styles.radioButton}
value={'edgeless'}
spanStyle={styles.spanStyle}
>
{t['com.affine.pageMode.edgeless']()}
</RadioButton>
</RadioButtonGroup>
</div>
</div>
{isSharedPage ? (
<>
{runtimeConfig.enableEnhanceShareMode && (
<>

Some files were not shown because too many files have changed in this diff Show More