mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-07 01:09:54 +08:00
Merge branch 'canary' into stable
This commit is contained in:
@@ -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;
|
||||
+22
@@ -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]': {
|
||||
|
||||
+1
-1
@@ -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 (
|
||||
|
||||
+4
-4
@@ -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
-2
@@ -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';
|
||||
|
||||
+8
-4
@@ -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,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)',
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
});
|
||||
@@ -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(),
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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,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]);
|
||||
|
||||
|
||||
+5
-5
@@ -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;
|
||||
}
|
||||
|
||||
+3
-1
@@ -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>
|
||||
|
||||
+2
-2
@@ -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',
|
||||
|
||||
+3
-2
@@ -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]);
|
||||
|
||||
+2
-1
@@ -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);
|
||||
|
||||
+8
-7
@@ -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');
|
||||
},
|
||||
|
||||
+6
-5
@@ -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 => {
|
||||
|
||||
+4
@@ -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
-8
@@ -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
-1
@@ -1,4 +1,4 @@
|
||||
import { Skeleton } from '@mui/material';
|
||||
import { Skeleton } from '@affine/component';
|
||||
|
||||
import { PlanLayout } from './layout';
|
||||
import * as styles from './skeleton.css';
|
||||
|
||||
+25
-7
@@ -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%',
|
||||
|
||||
+3
-2
@@ -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';
|
||||
+4
-6
@@ -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 (
|
||||
<>
|
||||
+10
-9
@@ -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
|
||||
+80
-43
@@ -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
Reference in New Issue
Block a user