feat(core): import template (#8000)
This commit is contained in:
@@ -17,6 +17,12 @@ export class WorkspaceList extends Entity {
|
|||||||
.flat()
|
.flat()
|
||||||
.map(isLoadings => isLoadings.some(isLoading => isLoading));
|
.map(isLoadings => isLoadings.some(isLoading => isLoading));
|
||||||
|
|
||||||
|
workspace$(id: string) {
|
||||||
|
return this.workspaces$.map(workspaces =>
|
||||||
|
workspaces.find(workspace => workspace.id === id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
constructor(private readonly providers: WorkspaceFlavourProvider[]) {
|
constructor(private readonly providers: WorkspaceFlavourProvider[]) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,111 +0,0 @@
|
|||||||
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
|
|
||||||
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
|
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
|
||||||
import { CollaborationIcon, SettingsIcon } from '@blocksuite/icons/rc';
|
|
||||||
import type { WorkspaceMetadata } from '@toeverything/infra';
|
|
||||||
import { type MouseEvent, useCallback } from 'react';
|
|
||||||
|
|
||||||
import { Button } from '../../../ui/button';
|
|
||||||
import { Skeleton } from '../../../ui/skeleton';
|
|
||||||
import * as styles from './styles.css';
|
|
||||||
|
|
||||||
export interface WorkspaceTypeProps {
|
|
||||||
flavour: WorkspaceFlavour;
|
|
||||||
isOwner: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WorkspaceCardProps {
|
|
||||||
currentWorkspaceId?: string | null;
|
|
||||||
meta: WorkspaceMetadata;
|
|
||||||
onClick: (metadata: WorkspaceMetadata) => void;
|
|
||||||
onSettingClick: (metadata: WorkspaceMetadata) => void;
|
|
||||||
onEnableCloudClick?: (meta: WorkspaceMetadata) => void;
|
|
||||||
isOwner?: boolean;
|
|
||||||
openingId?: string | null;
|
|
||||||
enableCloudText?: string;
|
|
||||||
name?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const WorkspaceCardSkeleton = () => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className={styles.card} data-testid="workspace-card">
|
|
||||||
<Skeleton variant="circular" width={28} height={28} />
|
|
||||||
<Skeleton
|
|
||||||
variant="rectangular"
|
|
||||||
height={43}
|
|
||||||
width={220}
|
|
||||||
style={{ marginLeft: '12px' }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const WorkspaceCard = ({
|
|
||||||
onClick,
|
|
||||||
onSettingClick,
|
|
||||||
onEnableCloudClick,
|
|
||||||
openingId,
|
|
||||||
currentWorkspaceId,
|
|
||||||
meta,
|
|
||||||
isOwner = true,
|
|
||||||
enableCloudText = 'Enable Cloud',
|
|
||||||
name,
|
|
||||||
}: WorkspaceCardProps) => {
|
|
||||||
const isLocal = meta.flavour === WorkspaceFlavour.LOCAL;
|
|
||||||
const displayName = name ?? UNTITLED_WORKSPACE_NAME;
|
|
||||||
|
|
||||||
const onEnableCloud = useCallback(
|
|
||||||
(e: MouseEvent) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onEnableCloudClick?.(meta);
|
|
||||||
},
|
|
||||||
[meta, onEnableCloudClick]
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={styles.card}
|
|
||||||
data-active={meta.id === currentWorkspaceId}
|
|
||||||
data-testid="workspace-card"
|
|
||||||
onClick={useCallback(() => {
|
|
||||||
onClick(meta);
|
|
||||||
}, [onClick, meta])}
|
|
||||||
>
|
|
||||||
<WorkspaceAvatar
|
|
||||||
key={meta.id}
|
|
||||||
meta={meta}
|
|
||||||
rounded={3}
|
|
||||||
size={28}
|
|
||||||
name={name}
|
|
||||||
colorfulFallback
|
|
||||||
/>
|
|
||||||
<div className={styles.workspaceInfo}>
|
|
||||||
<div className={styles.workspaceTitle}>{displayName}</div>
|
|
||||||
|
|
||||||
<div className={styles.actionButtons}>
|
|
||||||
{isLocal ? (
|
|
||||||
<Button
|
|
||||||
loading={!!openingId && openingId === meta.id}
|
|
||||||
disabled={!!openingId}
|
|
||||||
className={styles.showOnCardHover}
|
|
||||||
onClick={onEnableCloud}
|
|
||||||
>
|
|
||||||
{enableCloudText}
|
|
||||||
</Button>
|
|
||||||
) : null}
|
|
||||||
{isOwner ? null : <CollaborationIcon />}
|
|
||||||
<div
|
|
||||||
className={styles.settingButton}
|
|
||||||
onClick={e => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onSettingClick(meta);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SettingsIcon width={16} height={16} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
import { cssVar } from '@toeverything/theme';
|
|
||||||
import { style } from '@vanilla-extract/css';
|
|
||||||
|
|
||||||
import { displayFlex, textEllipsis } from '../../../styles';
|
|
||||||
|
|
||||||
export const card = style({
|
|
||||||
width: '100%',
|
|
||||||
cursor: 'pointer',
|
|
||||||
padding: '8px 12px',
|
|
||||||
borderRadius: 4,
|
|
||||||
// border: `1px solid ${borderColor}`,
|
|
||||||
boxShadow: 'inset 0 0 0 1px transparent',
|
|
||||||
...displayFlex('flex-start', 'flex-start'),
|
|
||||||
transition: 'background .2s',
|
|
||||||
position: 'relative',
|
|
||||||
color: cssVar('textSecondaryColor'),
|
|
||||||
background: 'transparent',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 12,
|
|
||||||
|
|
||||||
selectors: {
|
|
||||||
'&:hover': {
|
|
||||||
background: cssVar('hoverColor'),
|
|
||||||
},
|
|
||||||
'&[data-active="true"]': {
|
|
||||||
boxShadow: 'inset 0 0 0 1px ' + cssVar('brandColor'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const workspaceInfo = style({
|
|
||||||
width: 0,
|
|
||||||
flex: 1,
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
gap: 2,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const workspaceTitle = style({
|
|
||||||
width: 0,
|
|
||||||
flex: 1,
|
|
||||||
fontSize: cssVar('fontSm'),
|
|
||||||
fontWeight: 500,
|
|
||||||
lineHeight: '22px',
|
|
||||||
maxWidth: '190px',
|
|
||||||
color: cssVar('textPrimaryColor'),
|
|
||||||
...textEllipsis(1),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const actionButtons = style({
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
});
|
|
||||||
|
|
||||||
export const settingButtonWrapper = style({});
|
|
||||||
|
|
||||||
export const settingButton = style({
|
|
||||||
transition: 'all 0.13s ease',
|
|
||||||
width: 0,
|
|
||||||
height: 20,
|
|
||||||
overflow: 'hidden',
|
|
||||||
marginLeft: 0,
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
placeItems: 'center',
|
|
||||||
|
|
||||||
borderRadius: 4,
|
|
||||||
boxShadow: 'none',
|
|
||||||
background: 'transparent',
|
|
||||||
cursor: 'pointer',
|
|
||||||
|
|
||||||
selectors: {
|
|
||||||
[`.${card}:hover &`]: {
|
|
||||||
width: 20,
|
|
||||||
marginLeft: 8,
|
|
||||||
boxShadow: cssVar('shadow1'),
|
|
||||||
background: cssVar('white80'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const showOnCardHover = style({
|
|
||||||
visibility: 'hidden',
|
|
||||||
opacity: 0,
|
|
||||||
selectors: {
|
|
||||||
[`.${card}:hover &`]: {
|
|
||||||
visibility: 'visible',
|
|
||||||
opacity: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import { style } from '@vanilla-extract/css';
|
|
||||||
export const workspaceItemStyle = style({
|
|
||||||
'@media': {
|
|
||||||
'screen and (max-width: 720px)': {
|
|
||||||
width: '100%',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
|
||||||
import type { WorkspaceMetadata } from '@toeverything/infra';
|
|
||||||
import { Suspense } from 'react';
|
|
||||||
|
|
||||||
import {
|
|
||||||
WorkspaceCard,
|
|
||||||
WorkspaceCardSkeleton,
|
|
||||||
} from '../../components/card/workspace-card';
|
|
||||||
import { workspaceItemStyle } from './index.css';
|
|
||||||
|
|
||||||
export interface WorkspaceListProps {
|
|
||||||
disabled?: boolean;
|
|
||||||
currentWorkspaceId?: string | null;
|
|
||||||
items: WorkspaceMetadata[];
|
|
||||||
openingId?: string | null;
|
|
||||||
onClick: (workspace: WorkspaceMetadata) => void;
|
|
||||||
onSettingClick: (workspace: WorkspaceMetadata) => void;
|
|
||||||
onEnableCloudClick?: (meta: WorkspaceMetadata) => void;
|
|
||||||
useIsWorkspaceOwner: (
|
|
||||||
workspaceMetadata: WorkspaceMetadata
|
|
||||||
) => boolean | undefined;
|
|
||||||
useWorkspaceName: (
|
|
||||||
workspaceMetadata: WorkspaceMetadata
|
|
||||||
) => string | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SortableWorkspaceItemProps extends Omit<WorkspaceListProps, 'items'> {
|
|
||||||
item: WorkspaceMetadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SortableWorkspaceItem = ({
|
|
||||||
item,
|
|
||||||
openingId,
|
|
||||||
useIsWorkspaceOwner,
|
|
||||||
useWorkspaceName,
|
|
||||||
currentWorkspaceId,
|
|
||||||
onClick,
|
|
||||||
onSettingClick,
|
|
||||||
onEnableCloudClick,
|
|
||||||
}: SortableWorkspaceItemProps) => {
|
|
||||||
const isOwner = useIsWorkspaceOwner?.(item);
|
|
||||||
const name = useWorkspaceName?.(item);
|
|
||||||
return (
|
|
||||||
<div className={workspaceItemStyle} data-testid="draggable-item">
|
|
||||||
<WorkspaceCard
|
|
||||||
currentWorkspaceId={currentWorkspaceId}
|
|
||||||
meta={item}
|
|
||||||
onClick={onClick}
|
|
||||||
onSettingClick={onSettingClick}
|
|
||||||
onEnableCloudClick={onEnableCloudClick}
|
|
||||||
openingId={openingId}
|
|
||||||
isOwner={isOwner}
|
|
||||||
name={name}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const WorkspaceList = (props: WorkspaceListProps) => {
|
|
||||||
const workspaceList = props.items;
|
|
||||||
|
|
||||||
return workspaceList
|
|
||||||
.filter(
|
|
||||||
w => w.flavour !== WorkspaceFlavour.AFFINE_CLOUD || w.initialized === true
|
|
||||||
)
|
|
||||||
.map(item => (
|
|
||||||
<Suspense fallback={<WorkspaceCardSkeleton />} key={item.id}>
|
|
||||||
<SortableWorkspaceItem key={item.id} {...props} item={item} />
|
|
||||||
</Suspense>
|
|
||||||
));
|
|
||||||
};
|
|
||||||
@@ -1,39 +1,41 @@
|
|||||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { Fragment } from 'react';
|
|
||||||
|
|
||||||
import type { MenuProps } from '../menu.types';
|
import type { MenuProps } from '../menu.types';
|
||||||
import * as styles from '../styles.css';
|
import * as styles from '../styles.css';
|
||||||
|
import * as desktopStyles from './styles.css';
|
||||||
|
|
||||||
export const DesktopMenu = ({
|
export const DesktopMenu = ({
|
||||||
children,
|
children,
|
||||||
items,
|
items,
|
||||||
portalOptions,
|
portalOptions,
|
||||||
rootOptions,
|
rootOptions,
|
||||||
noPortal,
|
|
||||||
contentOptions: {
|
contentOptions: {
|
||||||
className = '',
|
className = '',
|
||||||
style: contentStyle = {},
|
style: contentStyle = {},
|
||||||
...otherContentOptions
|
...otherContentOptions
|
||||||
} = {},
|
} = {},
|
||||||
}: MenuProps) => {
|
}: MenuProps) => {
|
||||||
const Wrapper = noPortal ? Fragment : DropdownMenu.Portal;
|
|
||||||
const wrapperProps = noPortal ? {} : portalOptions;
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu.Root {...rootOptions}>
|
<DropdownMenu.Root {...rootOptions}>
|
||||||
<DropdownMenu.Trigger asChild>{children}</DropdownMenu.Trigger>
|
<DropdownMenu.Trigger asChild>{children}</DropdownMenu.Trigger>
|
||||||
|
|
||||||
<Wrapper {...wrapperProps}>
|
<DropdownMenu.Portal {...portalOptions}>
|
||||||
<DropdownMenu.Content
|
<DropdownMenu.Content
|
||||||
className={clsx(styles.menuContent, className)}
|
className={clsx(
|
||||||
|
styles.menuContent,
|
||||||
|
desktopStyles.contentAnimation,
|
||||||
|
className
|
||||||
|
)}
|
||||||
sideOffset={5}
|
sideOffset={5}
|
||||||
align="start"
|
align="start"
|
||||||
style={{ zIndex: 'var(--affine-z-index-popover)', ...contentStyle }}
|
style={{ zIndex: 'var(--affine-z-index-popover)', ...contentStyle }}
|
||||||
{...otherContentOptions}
|
{...otherContentOptions}
|
||||||
|
side="bottom"
|
||||||
>
|
>
|
||||||
{items}
|
{items}
|
||||||
</DropdownMenu.Content>
|
</DropdownMenu.Content>
|
||||||
</Wrapper>
|
</DropdownMenu.Portal>
|
||||||
</DropdownMenu.Root>
|
</DropdownMenu.Root>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { keyframes, style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
const slideDown = keyframes({
|
||||||
|
from: {
|
||||||
|
opacity: 0,
|
||||||
|
transform: 'translateY(-10px)',
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
opacity: 1,
|
||||||
|
transform: 'translateY(0)',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const slideUp = keyframes({
|
||||||
|
to: {
|
||||||
|
opacity: 0,
|
||||||
|
transform: 'translateY(-10px)',
|
||||||
|
},
|
||||||
|
from: {
|
||||||
|
opacity: 1,
|
||||||
|
transform: 'translateY(0)',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const contentAnimation = style({
|
||||||
|
animation: `${slideDown} 150ms cubic-bezier(0.42, 0, 0.58, 1)`,
|
||||||
|
selectors: {
|
||||||
|
'&[data-state="closed"]': {
|
||||||
|
pointerEvents: 'none',
|
||||||
|
animation: `${slideUp} 150ms cubic-bezier(0.42, 0, 0.58, 1)`,
|
||||||
|
animationFillMode: 'forwards',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,18 +1,11 @@
|
|||||||
import { ArrowLeftSmallIcon } from '@blocksuite/icons/rc';
|
import { ArrowLeftSmallIcon } from '@blocksuite/icons/rc';
|
||||||
import { Slot } from '@radix-ui/react-slot';
|
import { Slot } from '@radix-ui/react-slot';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import {
|
import { useCallback, useContext, useEffect, useRef, useState } from 'react';
|
||||||
Fragment,
|
|
||||||
useCallback,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
|
|
||||||
import { observeResize } from '../../../utils';
|
import { observeResize } from '../../../utils';
|
||||||
import { Button } from '../../button';
|
import { Button } from '../../button';
|
||||||
import { Modal, type ModalProps } from '../../modal';
|
import { Modal } from '../../modal';
|
||||||
import type { MenuProps } from '../menu.types';
|
import type { MenuProps } from '../menu.types';
|
||||||
import type { SubMenuContent } from './context';
|
import type { SubMenuContent } from './context';
|
||||||
import { MobileMenuContext } from './context';
|
import { MobileMenuContext } from './context';
|
||||||
@@ -22,7 +15,6 @@ import { MobileMenuSubRaw } from './sub';
|
|||||||
export const MobileMenu = ({
|
export const MobileMenu = ({
|
||||||
children,
|
children,
|
||||||
items,
|
items,
|
||||||
noPortal,
|
|
||||||
contentOptions: {
|
contentOptions: {
|
||||||
className,
|
className,
|
||||||
onPointerDownOutside,
|
onPointerDownOutside,
|
||||||
@@ -56,25 +48,6 @@ export const MobileMenu = ({
|
|||||||
[onPointerDownOutside, rootOptions]
|
[onPointerDownOutside, rootOptions]
|
||||||
);
|
);
|
||||||
|
|
||||||
const Wrapper = noPortal ? Fragment : Modal;
|
|
||||||
const wrapperProps = noPortal
|
|
||||||
? {}
|
|
||||||
: ({
|
|
||||||
open: finalOpen,
|
|
||||||
onOpenChange,
|
|
||||||
width: '100%',
|
|
||||||
animation: 'slideBottom',
|
|
||||||
withoutCloseButton: true,
|
|
||||||
contentOptions: {
|
|
||||||
className: clsx(className, styles.mobileMenuModal),
|
|
||||||
...otherContentOptions,
|
|
||||||
},
|
|
||||||
contentWrapperStyle: {
|
|
||||||
alignItems: 'end',
|
|
||||||
paddingBottom: 10,
|
|
||||||
},
|
|
||||||
} satisfies ModalProps);
|
|
||||||
|
|
||||||
const onItemClick = useCallback((e: any) => {
|
const onItemClick = useCallback((e: any) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setOpen(prev => !prev);
|
setOpen(prev => !prev);
|
||||||
@@ -127,7 +100,21 @@ export const MobileMenu = ({
|
|||||||
<MobileMenuContext.Provider
|
<MobileMenuContext.Provider
|
||||||
value={{ subMenus, setSubMenus, setOpen: onOpenChange }}
|
value={{ subMenus, setSubMenus, setOpen: onOpenChange }}
|
||||||
>
|
>
|
||||||
<Wrapper {...wrapperProps}>
|
<Modal
|
||||||
|
open={finalOpen}
|
||||||
|
onOpenChange={onOpenChange}
|
||||||
|
width="100%"
|
||||||
|
animation="slideBottom"
|
||||||
|
withoutCloseButton={true}
|
||||||
|
contentOptions={{
|
||||||
|
className: clsx(className, styles.mobileMenuModal),
|
||||||
|
...otherContentOptions,
|
||||||
|
}}
|
||||||
|
contentWrapperStyle={{
|
||||||
|
alignItems: 'end',
|
||||||
|
paddingBottom: 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
ref={sliderRef}
|
ref={sliderRef}
|
||||||
className={styles.slider}
|
className={styles.slider}
|
||||||
@@ -159,7 +146,7 @@ export const MobileMenu = ({
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</Wrapper>
|
</Modal>
|
||||||
</MobileMenuContext.Provider>
|
</MobileMenuContext.Provider>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -153,14 +153,19 @@ export const ModalInner = forwardRef<HTMLDivElement, ModalProps>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const container = createContainer();
|
if (open) {
|
||||||
setContainer(container);
|
const container = createContainer();
|
||||||
return () => {
|
setContainer(container);
|
||||||
setTimeout(() => {
|
return () => {
|
||||||
container.remove();
|
setTimeout(() => {
|
||||||
}, 1000) as unknown as number;
|
container.remove();
|
||||||
};
|
}, 1000) as unknown as number;
|
||||||
}, []);
|
};
|
||||||
|
} else {
|
||||||
|
setContainer(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
const handlePointerDownOutSide = useCallback(
|
const handlePointerDownOutSide = useCallback(
|
||||||
(e: PointerDownOutsideEvent) => {
|
(e: PointerDownOutsideEvent) => {
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import { atom } from 'jotai';
|
import { atom } from 'jotai';
|
||||||
|
|
||||||
import type { AuthProps } from '../components/affine/auth';
|
import type { AuthProps } from '../components/affine/auth';
|
||||||
import type { CreateWorkspaceMode } from '../components/affine/create-workspace-modal';
|
|
||||||
import type { SettingProps } from '../components/affine/setting-modal';
|
import type { SettingProps } from '../components/affine/setting-modal';
|
||||||
import type { ActiveTab } from '../components/affine/setting-modal/types';
|
import type { ActiveTab } from '../components/affine/setting-modal/types';
|
||||||
// modal atoms
|
// modal atoms
|
||||||
export const openWorkspacesModalAtom = atom(false);
|
export const openWorkspacesModalAtom = atom(false);
|
||||||
export const openCreateWorkspaceModalAtom = atom<CreateWorkspaceMode>(false);
|
|
||||||
export const openSignOutModalAtom = atom(false);
|
export const openSignOutModalAtom = atom(false);
|
||||||
export const openQuotaModalAtom = atom(false);
|
export const openQuotaModalAtom = atom(false);
|
||||||
export const openStarAFFiNEModalAtom = atom(false);
|
export const openStarAFFiNEModalAtom = atom(false);
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
import type { useI18n } from '@affine/i18n';
|
import type { useI18n } from '@affine/i18n';
|
||||||
import { ImportIcon, PlusIcon } from '@blocksuite/icons/rc';
|
import { ImportIcon, PlusIcon } from '@blocksuite/icons/rc';
|
||||||
import type { createStore } from 'jotai';
|
|
||||||
|
|
||||||
import { openCreateWorkspaceModalAtom } from '../atoms';
|
|
||||||
import type { usePageHelper } from '../components/blocksuite/block-suite-page-list/utils';
|
import type { usePageHelper } from '../components/blocksuite/block-suite-page-list/utils';
|
||||||
import { track } from '../mixpanel';
|
import { track } from '../mixpanel';
|
||||||
|
import type { CreateWorkspaceDialogService } from '../modules/create-workspace';
|
||||||
import { registerAffineCommand } from './registry';
|
import { registerAffineCommand } from './registry';
|
||||||
|
|
||||||
export function registerAffineCreationCommands({
|
export function registerAffineCreationCommands({
|
||||||
store,
|
|
||||||
pageHelper,
|
pageHelper,
|
||||||
t,
|
t,
|
||||||
|
createWorkspaceDialogService,
|
||||||
}: {
|
}: {
|
||||||
t: ReturnType<typeof useI18n>;
|
t: ReturnType<typeof useI18n>;
|
||||||
store: ReturnType<typeof createStore>;
|
|
||||||
pageHelper: ReturnType<typeof usePageHelper>;
|
pageHelper: ReturnType<typeof usePageHelper>;
|
||||||
|
createWorkspaceDialogService: CreateWorkspaceDialogService;
|
||||||
}) {
|
}) {
|
||||||
const unsubs: Array<() => void> = [];
|
const unsubs: Array<() => void> = [];
|
||||||
unsubs.push(
|
unsubs.push(
|
||||||
@@ -62,7 +61,7 @@ export function registerAffineCreationCommands({
|
|||||||
run() {
|
run() {
|
||||||
track.$.cmdk.workspace.createWorkspace();
|
track.$.cmdk.workspace.createWorkspace();
|
||||||
|
|
||||||
store.set(openCreateWorkspaceModalAtom, 'new');
|
createWorkspaceDialogService.dialog.open('new');
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -80,7 +79,7 @@ export function registerAffineCreationCommands({
|
|||||||
control: 'import',
|
control: 'import',
|
||||||
});
|
});
|
||||||
|
|
||||||
store.set(openCreateWorkspaceModalAtom, 'add');
|
createWorkspaceDialogService.dialog.open('add');
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -25,8 +25,9 @@ export const inlineTagsContainer = style({
|
|||||||
|
|
||||||
export const tagsMenu = style({
|
export const tagsMenu = style({
|
||||||
padding: 0,
|
padding: 0,
|
||||||
transform:
|
position: 'relative',
|
||||||
'translate(-3.5px, calc(-3.5px + var(--radix-popper-anchor-height) * -1))',
|
top: 'calc(-3.5px + var(--radix-popper-anchor-height) * -1)',
|
||||||
|
left: '-3.5px',
|
||||||
width: 'calc(var(--radix-popper-anchor-width) + 16px)',
|
width: 'calc(var(--radix-popper-anchor-width) + 16px)',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { debounce } from 'lodash-es';
|
|||||||
import type { PropsWithChildren, ReactElement } from 'react';
|
import type { PropsWithChildren, ReactElement } from 'react';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
import { WorkspaceSelector } from '../workspace-selector';
|
import { WorkspaceNavigator } from '../workspace-selector';
|
||||||
import { fallbackHeaderStyle, fallbackStyle } from './fallback.css';
|
import { fallbackHeaderStyle, fallbackStyle } from './fallback.css';
|
||||||
import {
|
import {
|
||||||
floatingMaxWidth,
|
floatingMaxWidth,
|
||||||
@@ -139,7 +139,11 @@ export const AppSidebarFallback = (): ReactElement | null => {
|
|||||||
<div className={fallbackStyle}>
|
<div className={fallbackStyle}>
|
||||||
<div className={fallbackHeaderStyle}>
|
<div className={fallbackHeaderStyle}>
|
||||||
{currentWorkspace ? (
|
{currentWorkspace ? (
|
||||||
<WorkspaceSelector />
|
<WorkspaceNavigator
|
||||||
|
showSettingsButton
|
||||||
|
showSyncStatus
|
||||||
|
showEnableCloudButton
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Skeleton variant="circular" width={40} height={40} />
|
<Skeleton variant="circular" width={40} height={40} />
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { notify } from '@affine/component';
|
||||||
|
import { openSettingModalAtom } from '@affine/core/atoms';
|
||||||
|
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
||||||
|
import { useI18n } from '@affine/i18n';
|
||||||
|
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
|
||||||
|
import { useSetAtom } from 'jotai';
|
||||||
|
import { debounce } from 'lodash-es';
|
||||||
|
import { useCallback, useEffect } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO(eyhn): refactor this
|
||||||
|
*/
|
||||||
|
export const OverCapacityNotification = () => {
|
||||||
|
const t = useI18n();
|
||||||
|
const currentWorkspace = useService(WorkspaceService).workspace;
|
||||||
|
const permissionService = useService(WorkspacePermissionService);
|
||||||
|
const isOwner = useLiveData(permissionService.permission.isOwner$);
|
||||||
|
useEffect(() => {
|
||||||
|
// revalidate permission
|
||||||
|
permissionService.permission.revalidate();
|
||||||
|
}, [permissionService]);
|
||||||
|
|
||||||
|
const setSettingModalAtom = useSetAtom(openSettingModalAtom);
|
||||||
|
const jumpToPricePlan = useCallback(() => {
|
||||||
|
setSettingModalAtom({
|
||||||
|
open: true,
|
||||||
|
activeTab: 'plans',
|
||||||
|
scrollAnchor: 'cloudPricingPlan',
|
||||||
|
});
|
||||||
|
}, [setSettingModalAtom]);
|
||||||
|
|
||||||
|
// debounce sync engine status
|
||||||
|
useEffect(() => {
|
||||||
|
const disposableOverCapacity =
|
||||||
|
currentWorkspace.engine.blob.isStorageOverCapacity$.subscribe(
|
||||||
|
debounce((isStorageOverCapacity: boolean) => {
|
||||||
|
const isOver = isStorageOverCapacity;
|
||||||
|
if (!isOver) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isOwner) {
|
||||||
|
notify.warning({
|
||||||
|
title: t['com.affine.payment.storage-limit.title'](),
|
||||||
|
message:
|
||||||
|
t['com.affine.payment.storage-limit.description.owner'](),
|
||||||
|
action: {
|
||||||
|
label: t['com.affine.payment.storage-limit.view'](),
|
||||||
|
onClick: jumpToPricePlan,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
notify.warning({
|
||||||
|
title: t['com.affine.payment.storage-limit.title'](),
|
||||||
|
message:
|
||||||
|
t['com.affine.payment.storage-limit.description.member'](),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return () => {
|
||||||
|
disposableOverCapacity?.unsubscribe();
|
||||||
|
};
|
||||||
|
}, [currentWorkspace, isOwner, jumpToPricePlan, t]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import type { DocCollection } from '@blocksuite/store';
|
|
||||||
|
|
||||||
export type FavoriteListProps = {
|
|
||||||
docCollection: DocCollection;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CollectionsListProps = {
|
|
||||||
docCollection: DocCollection;
|
|
||||||
onCreate?: () => void;
|
|
||||||
};
|
|
||||||
@@ -1,313 +0,0 @@
|
|||||||
import { notify, Tooltip } from '@affine/component';
|
|
||||||
import { Loading } from '@affine/component/ui/loading';
|
|
||||||
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
|
|
||||||
import { openSettingModalAtom } from '@affine/core/atoms';
|
|
||||||
import { useDocEngineStatus } from '@affine/core/hooks/affine/use-doc-engine-status';
|
|
||||||
import { useWorkspaceInfo } from '@affine/core/hooks/use-workspace-info';
|
|
||||||
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
|
||||||
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
|
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
|
||||||
import { useI18n } from '@affine/i18n';
|
|
||||||
import {
|
|
||||||
CloudWorkspaceIcon,
|
|
||||||
InformationFillDuotoneIcon,
|
|
||||||
LocalWorkspaceIcon,
|
|
||||||
NoNetworkIcon,
|
|
||||||
UnsyncIcon,
|
|
||||||
} from '@blocksuite/icons/rc';
|
|
||||||
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
|
|
||||||
import { cssVar } from '@toeverything/theme';
|
|
||||||
import { useSetAtom } from 'jotai';
|
|
||||||
import { debounce } from 'lodash-es';
|
|
||||||
import type { HTMLAttributes } from 'react';
|
|
||||||
import { forwardRef, useCallback, useEffect, useMemo, useState } from 'react';
|
|
||||||
|
|
||||||
import { useSystemOnline } from '../../../../hooks/use-system-online';
|
|
||||||
import * as styles from './styles.css';
|
|
||||||
|
|
||||||
// FIXME:
|
|
||||||
// 2. Refactor the code to improve readability
|
|
||||||
const CloudWorkspaceStatus = () => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<CloudWorkspaceIcon />
|
|
||||||
Cloud
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const SyncingWorkspaceStatus = ({ progress }: { progress?: number }) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Loading progress={progress} speed={progress ? 0 : undefined} />
|
|
||||||
Syncing...
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const UnSyncWorkspaceStatus = () => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<UnsyncIcon />
|
|
||||||
Wait for upload
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const LocalWorkspaceStatus = () => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{!environment.isDesktop ? (
|
|
||||||
<InformationFillDuotoneIcon style={{ color: cssVar('errorColor') }} />
|
|
||||||
) : (
|
|
||||||
<LocalWorkspaceIcon />
|
|
||||||
)}
|
|
||||||
Local
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const OfflineStatus = () => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<NoNetworkIcon />
|
|
||||||
Offline
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const useSyncEngineSyncProgress = () => {
|
|
||||||
const t = useI18n();
|
|
||||||
const isOnline = useSystemOnline();
|
|
||||||
const { syncing, progress, retrying, errorMessage } = useDocEngineStatus();
|
|
||||||
const [isOverCapacity, setIsOverCapacity] = useState(false);
|
|
||||||
|
|
||||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
|
||||||
const permissionService = useService(WorkspacePermissionService);
|
|
||||||
const isOwner = useLiveData(permissionService.permission.isOwner$);
|
|
||||||
useEffect(() => {
|
|
||||||
// revalidate permission
|
|
||||||
permissionService.permission.revalidate();
|
|
||||||
}, [permissionService]);
|
|
||||||
|
|
||||||
const setSettingModalAtom = useSetAtom(openSettingModalAtom);
|
|
||||||
const jumpToPricePlan = useCallback(() => {
|
|
||||||
setSettingModalAtom({
|
|
||||||
open: true,
|
|
||||||
activeTab: 'plans',
|
|
||||||
scrollAnchor: 'cloudPricingPlan',
|
|
||||||
});
|
|
||||||
}, [setSettingModalAtom]);
|
|
||||||
|
|
||||||
// debounce sync engine status
|
|
||||||
useEffect(() => {
|
|
||||||
const disposableOverCapacity =
|
|
||||||
currentWorkspace.engine.blob.isStorageOverCapacity$.subscribe(
|
|
||||||
debounce((isStorageOverCapacity: boolean) => {
|
|
||||||
const isOver = isStorageOverCapacity;
|
|
||||||
if (!isOver) {
|
|
||||||
setIsOverCapacity(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setIsOverCapacity(true);
|
|
||||||
if (isOwner) {
|
|
||||||
notify.warning({
|
|
||||||
title: t['com.affine.payment.storage-limit.title'](),
|
|
||||||
message:
|
|
||||||
t['com.affine.payment.storage-limit.description.owner'](),
|
|
||||||
action: {
|
|
||||||
label: t['com.affine.payment.storage-limit.view'](),
|
|
||||||
onClick: jumpToPricePlan,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
notify.warning({
|
|
||||||
title: t['com.affine.payment.storage-limit.title'](),
|
|
||||||
message:
|
|
||||||
t['com.affine.payment.storage-limit.description.member'](),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return () => {
|
|
||||||
disposableOverCapacity?.unsubscribe();
|
|
||||||
};
|
|
||||||
}, [currentWorkspace, isOwner, jumpToPricePlan, t]);
|
|
||||||
|
|
||||||
const content = useMemo(() => {
|
|
||||||
// TODO(@eyhn): add i18n
|
|
||||||
if (currentWorkspace.flavour === WorkspaceFlavour.LOCAL) {
|
|
||||||
if (!environment.isDesktop) {
|
|
||||||
return 'This is a local demo workspace.';
|
|
||||||
}
|
|
||||||
return 'Saved locally';
|
|
||||||
}
|
|
||||||
if (!isOnline) {
|
|
||||||
return 'Disconnected, please check your network connection';
|
|
||||||
}
|
|
||||||
if (isOverCapacity) {
|
|
||||||
return 'Sync failed due to insufficient cloud storage space.';
|
|
||||||
}
|
|
||||||
if (retrying && errorMessage) {
|
|
||||||
return `${errorMessage}, reconnecting.`;
|
|
||||||
}
|
|
||||||
if (retrying) {
|
|
||||||
return 'Sync disconnected due to unexpected issues, reconnecting.';
|
|
||||||
}
|
|
||||||
if (syncing) {
|
|
||||||
return (
|
|
||||||
`Syncing with AFFiNE Cloud` +
|
|
||||||
(progress ? ` (${Math.floor(progress * 100)}%)` : '')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'Synced with AFFiNE Cloud';
|
|
||||||
}, [
|
|
||||||
currentWorkspace.flavour,
|
|
||||||
errorMessage,
|
|
||||||
isOnline,
|
|
||||||
isOverCapacity,
|
|
||||||
progress,
|
|
||||||
retrying,
|
|
||||||
syncing,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const CloudWorkspaceSyncStatus = useCallback(() => {
|
|
||||||
if (syncing) {
|
|
||||||
return SyncingWorkspaceStatus({
|
|
||||||
progress: progress ? Math.max(progress, 0.2) : undefined,
|
|
||||||
});
|
|
||||||
} else if (retrying) {
|
|
||||||
return UnSyncWorkspaceStatus();
|
|
||||||
} else {
|
|
||||||
return CloudWorkspaceStatus();
|
|
||||||
}
|
|
||||||
}, [progress, retrying, syncing]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
message: content,
|
|
||||||
icon:
|
|
||||||
currentWorkspace.flavour === WorkspaceFlavour.AFFINE_CLOUD ? (
|
|
||||||
!isOnline ? (
|
|
||||||
<OfflineStatus />
|
|
||||||
) : (
|
|
||||||
<CloudWorkspaceSyncStatus />
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<LocalWorkspaceStatus />
|
|
||||||
),
|
|
||||||
active:
|
|
||||||
currentWorkspace.flavour === WorkspaceFlavour.AFFINE_CLOUD &&
|
|
||||||
((syncing && progress !== undefined) || retrying) && // active if syncing or retrying
|
|
||||||
!isOverCapacity, // not active if isOffline or OverCapacity
|
|
||||||
};
|
|
||||||
};
|
|
||||||
const usePauseAnimation = (timeToResume = 5000) => {
|
|
||||||
const [paused, setPaused] = useState(false);
|
|
||||||
|
|
||||||
const resume = useCallback(() => {
|
|
||||||
setPaused(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const pause = useCallback(() => {
|
|
||||||
setPaused(true);
|
|
||||||
if (timeToResume > 0) {
|
|
||||||
setTimeout(resume, timeToResume);
|
|
||||||
}
|
|
||||||
}, [resume, timeToResume]);
|
|
||||||
|
|
||||||
return { paused, pause };
|
|
||||||
};
|
|
||||||
|
|
||||||
const WorkspaceInfo = ({ name }: { name: string }) => {
|
|
||||||
const { message, active } = useSyncEngineSyncProgress();
|
|
||||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
|
||||||
const isCloud = currentWorkspace.flavour === WorkspaceFlavour.AFFINE_CLOUD;
|
|
||||||
const { progress } = useDocEngineStatus();
|
|
||||||
const { paused, pause } = usePauseAnimation();
|
|
||||||
|
|
||||||
// to make sure that animation will play first time
|
|
||||||
const [delayActive, setDelayActive] = useState(false);
|
|
||||||
useEffect(() => {
|
|
||||||
if (paused) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const delayOpen = 0;
|
|
||||||
const delayClose = 200;
|
|
||||||
let timer: ReturnType<typeof setTimeout>;
|
|
||||||
if (active) {
|
|
||||||
timer = setTimeout(() => {
|
|
||||||
setDelayActive(active);
|
|
||||||
}, delayOpen);
|
|
||||||
} else {
|
|
||||||
timer = setTimeout(() => {
|
|
||||||
setDelayActive(active);
|
|
||||||
pause();
|
|
||||||
}, delayClose);
|
|
||||||
}
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}, [active, pause, paused]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.workspaceInfoSlider} data-active={delayActive}>
|
|
||||||
<div className={styles.workspaceInfoSlide}>
|
|
||||||
<div className={styles.workspaceInfo} data-type="normal">
|
|
||||||
<div className={styles.workspaceName} data-testid="workspace-name">
|
|
||||||
{name}
|
|
||||||
</div>
|
|
||||||
<div className={styles.workspaceStatus}>
|
|
||||||
{isCloud ? <CloudWorkspaceStatus /> : <LocalWorkspaceStatus />}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* when syncing/offline/... */}
|
|
||||||
<div className={styles.workspaceInfo} data-type="events">
|
|
||||||
<Tooltip
|
|
||||||
content={message}
|
|
||||||
options={{ className: styles.workspaceInfoTooltip }}
|
|
||||||
>
|
|
||||||
<div className={styles.workspaceActiveStatus}>
|
|
||||||
<SyncingWorkspaceStatus progress={progress} />
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const WorkspaceCard = forwardRef<
|
|
||||||
HTMLDivElement,
|
|
||||||
HTMLAttributes<HTMLDivElement>
|
|
||||||
>(({ ...props }, ref) => {
|
|
||||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
|
||||||
|
|
||||||
const information = useWorkspaceInfo(currentWorkspace.meta);
|
|
||||||
|
|
||||||
const name = information?.name ?? UNTITLED_WORKSPACE_NAME;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={styles.container}
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
data-testid="current-workspace"
|
|
||||||
id="current-workspace"
|
|
||||||
ref={ref}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<WorkspaceAvatar
|
|
||||||
key={currentWorkspace.id}
|
|
||||||
meta={currentWorkspace.meta}
|
|
||||||
rounded={3}
|
|
||||||
data-testid="workspace-avatar"
|
|
||||||
size={32}
|
|
||||||
name={name}
|
|
||||||
colorfulFallback
|
|
||||||
/>
|
|
||||||
<WorkspaceInfo name={name} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
WorkspaceCard.displayName = 'WorkspaceCard';
|
|
||||||
@@ -45,7 +45,7 @@ import {
|
|||||||
} from '../app-sidebar';
|
} from '../app-sidebar';
|
||||||
import { ExternalMenuLinkItem } from '../app-sidebar/menu-item/external-menu-link-item';
|
import { ExternalMenuLinkItem } from '../app-sidebar/menu-item/external-menu-link-item';
|
||||||
import { usePageHelper } from '../blocksuite/block-suite-page-list/utils';
|
import { usePageHelper } from '../blocksuite/block-suite-page-list/utils';
|
||||||
import { WorkspaceSelector } from '../workspace-selector';
|
import { WorkspaceNavigator } from '../workspace-selector';
|
||||||
import ImportPage from './import-page';
|
import ImportPage from './import-page';
|
||||||
import {
|
import {
|
||||||
quickSearch,
|
quickSearch,
|
||||||
@@ -118,6 +118,7 @@ export const RootAppSidebar = (): ReactElement => {
|
|||||||
},
|
},
|
||||||
[pageHelper, settings.newDocDefaultMode]
|
[pageHelper, settings.newDocDefaultMode]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (environment.isDesktop) {
|
if (environment.isDesktop) {
|
||||||
return events?.applicationMenu.onNewPageAction(() => onClickNewPage());
|
return events?.applicationMenu.onNewPageAction(() => onClickNewPage());
|
||||||
@@ -143,7 +144,11 @@ export const RootAppSidebar = (): ReactElement => {
|
|||||||
<SidebarContainer>
|
<SidebarContainer>
|
||||||
<div className={workspaceAndUserWrapper}>
|
<div className={workspaceAndUserWrapper}>
|
||||||
<div className={workspaceWrapper}>
|
<div className={workspaceWrapper}>
|
||||||
<WorkspaceSelector />
|
<WorkspaceNavigator
|
||||||
|
showEnableCloudButton
|
||||||
|
showSettingsButton
|
||||||
|
showSyncStatus
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<UserInfo />
|
<UserInfo />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,51 +1,173 @@
|
|||||||
import { Menu } from '@affine/component';
|
import { Menu, type MenuProps } from '@affine/component';
|
||||||
|
import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
|
||||||
import { track } from '@affine/core/mixpanel';
|
import { track } from '@affine/core/mixpanel';
|
||||||
import { useService, WorkspacesService } from '@toeverything/infra';
|
import type { CreateWorkspaceCallbackPayload } from '@affine/core/modules/create-workspace';
|
||||||
import { useAtom } from 'jotai';
|
import { WorkspaceSubPath } from '@affine/core/shared';
|
||||||
import { useCallback, useEffect } from 'react';
|
import {
|
||||||
|
GlobalContextService,
|
||||||
|
useLiveData,
|
||||||
|
useServices,
|
||||||
|
type WorkspaceMetadata,
|
||||||
|
WorkspacesService,
|
||||||
|
} from '@toeverything/infra';
|
||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { openWorkspaceListModalAtom } from '../../atoms';
|
import { UserWithWorkspaceList } from './user-with-workspace-list';
|
||||||
import { UserWithWorkspaceList } from '../pure/workspace-slider-bar/user-with-workspace-list';
|
import { WorkspaceCard } from './workspace-card';
|
||||||
import { WorkspaceCard } from '../pure/workspace-slider-bar/workspace-card';
|
|
||||||
|
|
||||||
export const WorkspaceSelector = () => {
|
interface WorkspaceSelectorProps {
|
||||||
const [isUserWorkspaceListOpened, setOpenUserWorkspaceList] = useAtom(
|
open?: boolean;
|
||||||
openWorkspaceListModalAtom
|
workspaceMetadata?: WorkspaceMetadata;
|
||||||
);
|
onSelectWorkspace?: (workspaceMetadata: WorkspaceMetadata) => void;
|
||||||
|
onCreatedWorkspace?: (payload: CreateWorkspaceCallbackPayload) => void;
|
||||||
|
showSettingsButton?: boolean;
|
||||||
|
showEnableCloudButton?: boolean;
|
||||||
|
showArrowDownIcon?: boolean;
|
||||||
|
showSyncStatus?: boolean;
|
||||||
|
disable?: boolean;
|
||||||
|
menuContentOptions?: MenuProps['contentOptions'];
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WorkspaceSelector = ({
|
||||||
|
workspaceMetadata: outerWorkspaceMetadata,
|
||||||
|
onSelectWorkspace,
|
||||||
|
onCreatedWorkspace,
|
||||||
|
showSettingsButton,
|
||||||
|
showArrowDownIcon,
|
||||||
|
disable,
|
||||||
|
open: outerOpen,
|
||||||
|
showEnableCloudButton,
|
||||||
|
showSyncStatus,
|
||||||
|
className,
|
||||||
|
menuContentOptions,
|
||||||
|
}: WorkspaceSelectorProps) => {
|
||||||
|
const { workspacesService, globalContextService } = useServices({
|
||||||
|
GlobalContextService,
|
||||||
|
WorkspacesService,
|
||||||
|
});
|
||||||
|
const [innerOpen, setOpened] = useState(false);
|
||||||
|
const open = outerOpen ?? innerOpen;
|
||||||
const closeUserWorkspaceList = useCallback(() => {
|
const closeUserWorkspaceList = useCallback(() => {
|
||||||
setOpenUserWorkspaceList(false);
|
setOpened(false);
|
||||||
}, [setOpenUserWorkspaceList]);
|
}, []);
|
||||||
const openUserWorkspaceList = useCallback(() => {
|
const openUserWorkspaceList = useCallback(() => {
|
||||||
track.$.navigationPanel.workspaceList.open();
|
track.$.navigationPanel.workspaceList.open();
|
||||||
setOpenUserWorkspaceList(true);
|
setOpened(true);
|
||||||
}, [setOpenUserWorkspaceList]);
|
}, []);
|
||||||
|
|
||||||
const workspaceManager = useService(WorkspacesService);
|
const currentWorkspaceId = useLiveData(
|
||||||
|
globalContextService.globalContext.workspaceId.$
|
||||||
|
);
|
||||||
|
const currentWorkspaceMetadata = useLiveData(
|
||||||
|
currentWorkspaceId
|
||||||
|
? workspacesService.list.workspace$(currentWorkspaceId)
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
const workspaceMetadata = outerWorkspaceMetadata ?? currentWorkspaceMetadata;
|
||||||
|
|
||||||
// revalidate workspace list when open workspace list
|
// revalidate workspace list when open workspace list
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isUserWorkspaceListOpened) {
|
if (open) {
|
||||||
workspaceManager.list.revalidate();
|
workspacesService.list.revalidate();
|
||||||
}
|
}
|
||||||
}, [workspaceManager, isUserWorkspaceListOpened]);
|
}, [workspacesService, open]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu
|
<Menu
|
||||||
rootOptions={{
|
rootOptions={{
|
||||||
open: isUserWorkspaceListOpened,
|
open,
|
||||||
}}
|
}}
|
||||||
items={<UserWithWorkspaceList onEventEnd={closeUserWorkspaceList} />}
|
items={
|
||||||
|
<UserWithWorkspaceList
|
||||||
|
onEventEnd={closeUserWorkspaceList}
|
||||||
|
onClickWorkspace={onSelectWorkspace}
|
||||||
|
onCreatedWorkspace={onCreatedWorkspace}
|
||||||
|
showEnableCloudButton={showEnableCloudButton}
|
||||||
|
showSettingsButton={showSettingsButton}
|
||||||
|
/>
|
||||||
|
}
|
||||||
contentOptions={{
|
contentOptions={{
|
||||||
// hide trigger
|
// hide trigger
|
||||||
sideOffset: -58,
|
sideOffset: -58,
|
||||||
onInteractOutside: closeUserWorkspaceList,
|
onInteractOutside: closeUserWorkspaceList,
|
||||||
onEscapeKeyDown: closeUserWorkspaceList,
|
onEscapeKeyDown: closeUserWorkspaceList,
|
||||||
|
...menuContentOptions,
|
||||||
style: {
|
style: {
|
||||||
width: '300px',
|
width: '300px',
|
||||||
|
...menuContentOptions?.style,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<WorkspaceCard onClick={openUserWorkspaceList} />
|
{workspaceMetadata ? (
|
||||||
|
<WorkspaceCard
|
||||||
|
workspaceMetadata={workspaceMetadata}
|
||||||
|
onClick={openUserWorkspaceList}
|
||||||
|
showSyncStatus={showSyncStatus}
|
||||||
|
className={className}
|
||||||
|
showArrowDownIcon={showArrowDownIcon}
|
||||||
|
disable={disable}
|
||||||
|
data-testid="current-workspace-card"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span></span>
|
||||||
|
)}
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const WorkspaceNavigator = ({
|
||||||
|
onSelectWorkspace,
|
||||||
|
onCreatedWorkspace,
|
||||||
|
...props
|
||||||
|
}: WorkspaceSelectorProps) => {
|
||||||
|
const { jumpToSubPath, jumpToPage } = useNavigateHelper();
|
||||||
|
|
||||||
|
const handleClickWorkspace = useCallback(
|
||||||
|
(workspaceMetadata: WorkspaceMetadata) => {
|
||||||
|
onSelectWorkspace?.(workspaceMetadata);
|
||||||
|
if (document.startViewTransition) {
|
||||||
|
document.startViewTransition(() => {
|
||||||
|
jumpToSubPath(workspaceMetadata.id, WorkspaceSubPath.ALL);
|
||||||
|
return new Promise(resolve =>
|
||||||
|
setTimeout(resolve, 150)
|
||||||
|
); /* start transition after 150ms */
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
jumpToSubPath(workspaceMetadata.id, WorkspaceSubPath.ALL);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onSelectWorkspace, jumpToSubPath]
|
||||||
|
);
|
||||||
|
const handleCreatedWorkspace = useCallback(
|
||||||
|
(payload: CreateWorkspaceCallbackPayload) => {
|
||||||
|
onCreatedWorkspace?.(payload);
|
||||||
|
if (document.startViewTransition) {
|
||||||
|
document.startViewTransition(() => {
|
||||||
|
if (payload.defaultDocId) {
|
||||||
|
jumpToPage(payload.meta.id, payload.defaultDocId);
|
||||||
|
} else {
|
||||||
|
jumpToSubPath(payload.meta.id, WorkspaceSubPath.ALL);
|
||||||
|
}
|
||||||
|
return new Promise(resolve =>
|
||||||
|
setTimeout(resolve, 150)
|
||||||
|
); /* start transition after 150ms */
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (payload.defaultDocId) {
|
||||||
|
jumpToPage(payload.meta.id, payload.defaultDocId);
|
||||||
|
} else {
|
||||||
|
jumpToSubPath(payload.meta.id, WorkspaceSubPath.ALL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[jumpToPage, jumpToSubPath, onCreatedWorkspace]
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<WorkspaceSelector
|
||||||
|
onSelectWorkspace={handleClickWorkspace}
|
||||||
|
onCreatedWorkspace={handleCreatedWorkspace}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,19 +1,21 @@
|
|||||||
import { Loading } from '@affine/component';
|
|
||||||
import { Divider } from '@affine/component/ui/divider';
|
import { Divider } from '@affine/component/ui/divider';
|
||||||
import { MenuItem } from '@affine/component/ui/menu';
|
import { MenuItem } from '@affine/component/ui/menu';
|
||||||
|
import { authAtom } from '@affine/core/atoms';
|
||||||
import { track } from '@affine/core/mixpanel';
|
import { track } from '@affine/core/mixpanel';
|
||||||
import { AuthService } from '@affine/core/modules/cloud';
|
import { AuthService } from '@affine/core/modules/cloud';
|
||||||
|
import { CreateWorkspaceDialogService } from '@affine/core/modules/create-workspace';
|
||||||
|
import type { CreateWorkspaceCallbackPayload } from '@affine/core/modules/create-workspace/types';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { Logo1Icon } from '@blocksuite/icons/rc';
|
import { Logo1Icon } from '@blocksuite/icons/rc';
|
||||||
import {
|
import {
|
||||||
useLiveData,
|
useLiveData,
|
||||||
useService,
|
useService,
|
||||||
|
type WorkspaceMetadata,
|
||||||
WorkspacesService,
|
WorkspacesService,
|
||||||
} from '@toeverything/infra';
|
} from '@toeverything/infra';
|
||||||
import { useSetAtom } from 'jotai';
|
import { useSetAtom } from 'jotai';
|
||||||
import { Suspense, useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import { authAtom, openCreateWorkspaceModalAtom } from '../../../../atoms';
|
|
||||||
import { AddWorkspace } from './add-workspace';
|
import { AddWorkspace } from './add-workspace';
|
||||||
import * as styles from './index.css';
|
import * as styles from './index.css';
|
||||||
import { UserAccountItem } from './user-account';
|
import { UserAccountItem } from './user-account';
|
||||||
@@ -56,27 +58,26 @@ export const SignInItem = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const UserWithWorkspaceListLoading = () => {
|
|
||||||
return (
|
|
||||||
<div className={styles.loadingWrapper}>
|
|
||||||
<Loading size={24} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface UserWithWorkspaceListProps {
|
interface UserWithWorkspaceListProps {
|
||||||
onEventEnd?: () => void;
|
onEventEnd?: () => void;
|
||||||
|
onClickWorkspace?: (workspace: WorkspaceMetadata) => void;
|
||||||
|
onCreatedWorkspace?: (payload: CreateWorkspaceCallbackPayload) => void;
|
||||||
|
showSettingsButton?: boolean;
|
||||||
|
showEnableCloudButton?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const UserWithWorkspaceListInner = ({
|
const UserWithWorkspaceListInner = ({
|
||||||
onEventEnd,
|
onEventEnd,
|
||||||
|
onClickWorkspace,
|
||||||
|
onCreatedWorkspace,
|
||||||
|
showSettingsButton,
|
||||||
|
showEnableCloudButton,
|
||||||
}: UserWithWorkspaceListProps) => {
|
}: UserWithWorkspaceListProps) => {
|
||||||
|
const createWorkspaceDialogService = useService(CreateWorkspaceDialogService);
|
||||||
const session = useLiveData(useService(AuthService).session.session$);
|
const session = useLiveData(useService(AuthService).session.session$);
|
||||||
|
|
||||||
const isAuthenticated = session.status === 'authenticated';
|
const isAuthenticated = session.status === 'authenticated';
|
||||||
|
|
||||||
const setOpenCreateWorkspaceModal = useSetAtom(openCreateWorkspaceModalAtom);
|
|
||||||
|
|
||||||
const setOpenSignIn = useSetAtom(authAtom);
|
const setOpenSignIn = useSetAtom(authAtom);
|
||||||
|
|
||||||
const openSignInModal = useCallback(() => {
|
const openSignInModal = useCallback(() => {
|
||||||
@@ -91,23 +92,31 @@ const UserWithWorkspaceListInner = ({
|
|||||||
return openSignInModal();
|
return openSignInModal();
|
||||||
}
|
}
|
||||||
track.$.navigationPanel.workspaceList.createWorkspace();
|
track.$.navigationPanel.workspaceList.createWorkspace();
|
||||||
setOpenCreateWorkspaceModal('new');
|
createWorkspaceDialogService.dialog.open('new', payload => {
|
||||||
|
if (payload) {
|
||||||
|
onCreatedWorkspace?.(payload);
|
||||||
|
}
|
||||||
|
});
|
||||||
onEventEnd?.();
|
onEventEnd?.();
|
||||||
}, [
|
}, [
|
||||||
|
createWorkspaceDialogService.dialog,
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
|
onCreatedWorkspace,
|
||||||
onEventEnd,
|
onEventEnd,
|
||||||
openSignInModal,
|
openSignInModal,
|
||||||
setOpenCreateWorkspaceModal,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
track.$.navigationPanel.workspaceList.createWorkspace();
|
|
||||||
const onAddWorkspace = useCallback(() => {
|
const onAddWorkspace = useCallback(() => {
|
||||||
track.$.navigationPanel.workspaceList.createWorkspace({
|
track.$.navigationPanel.workspaceList.createWorkspace({
|
||||||
control: 'import',
|
control: 'import',
|
||||||
});
|
});
|
||||||
setOpenCreateWorkspaceModal('add');
|
createWorkspaceDialogService.dialog.open('add', payload => {
|
||||||
|
if (payload) {
|
||||||
|
onCreatedWorkspace?.(payload);
|
||||||
|
}
|
||||||
|
});
|
||||||
onEventEnd?.();
|
onEventEnd?.();
|
||||||
}, [onEventEnd, setOpenCreateWorkspaceModal]);
|
}, [createWorkspaceDialogService.dialog, onCreatedWorkspace, onEventEnd]);
|
||||||
|
|
||||||
const workspaceManager = useService(WorkspacesService);
|
const workspaceManager = useService(WorkspacesService);
|
||||||
const workspaces = useLiveData(workspaceManager.list.workspaces$);
|
const workspaces = useLiveData(workspaceManager.list.workspaces$);
|
||||||
@@ -123,7 +132,12 @@ const UserWithWorkspaceListInner = ({
|
|||||||
<SignInItem />
|
<SignInItem />
|
||||||
)}
|
)}
|
||||||
<Divider size="thinner" />
|
<Divider size="thinner" />
|
||||||
<AFFiNEWorkspaceList onEventEnd={onEventEnd} />
|
<AFFiNEWorkspaceList
|
||||||
|
onEventEnd={onEventEnd}
|
||||||
|
onClickWorkspace={onClickWorkspace}
|
||||||
|
showEnableCloudButton={showEnableCloudButton}
|
||||||
|
showSettingsButton={showSettingsButton}
|
||||||
|
/>
|
||||||
{workspaces.length > 0 ? <Divider size="thinner" /> : null}
|
{workspaces.length > 0 ? <Divider size="thinner" /> : null}
|
||||||
<AddWorkspace
|
<AddWorkspace
|
||||||
onAddWorkspace={onAddWorkspace}
|
onAddWorkspace={onAddWorkspace}
|
||||||
@@ -134,9 +148,5 @@ const UserWithWorkspaceListInner = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const UserWithWorkspaceList = (props: UserWithWorkspaceListProps) => {
|
export const UserWithWorkspaceList = (props: UserWithWorkspaceListProps) => {
|
||||||
return (
|
return <UserWithWorkspaceListInner {...props} />;
|
||||||
<Suspense fallback={<UserWithWorkspaceListLoading />}>
|
|
||||||
<UserWithWorkspaceListInner {...props} />
|
|
||||||
</Suspense>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { UserPlanButton } from '../../../../affine/auth/user-plan-button';
|
import { UserPlanButton } from '@affine/core/components/affine/auth/user-plan-button';
|
||||||
|
|
||||||
import * as styles from './index.css';
|
import * as styles from './index.css';
|
||||||
|
|
||||||
export const UserAccountItem = ({
|
export const UserAccountItem = ({
|
||||||
@@ -29,3 +29,7 @@ export const scrollbar = style({
|
|||||||
transform: 'translateX(8px)',
|
transform: 'translateX(8px)',
|
||||||
width: '4px',
|
width: '4px',
|
||||||
});
|
});
|
||||||
|
export const workspaceCard = style({
|
||||||
|
height: '44px',
|
||||||
|
padding: '0 12px',
|
||||||
|
});
|
||||||
@@ -1,18 +1,13 @@
|
|||||||
import { ScrollableContainer } from '@affine/component';
|
import { ScrollableContainer } from '@affine/component';
|
||||||
import { Divider } from '@affine/component/ui/divider';
|
import { Divider } from '@affine/component/ui/divider';
|
||||||
import { WorkspaceList } from '@affine/component/workspace-list';
|
import { openSettingModalAtom } from '@affine/core/atoms';
|
||||||
import { useEnableCloud } from '@affine/core/hooks/affine/use-enable-cloud';
|
import { useEnableCloud } from '@affine/core/hooks/affine/use-enable-cloud';
|
||||||
import {
|
|
||||||
useWorkspaceInfo,
|
|
||||||
useWorkspaceName,
|
|
||||||
} from '@affine/core/hooks/use-workspace-info';
|
|
||||||
import { AuthService } from '@affine/core/modules/cloud';
|
import { AuthService } from '@affine/core/modules/cloud';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import { useI18n } from '@affine/i18n';
|
import { useI18n } from '@affine/i18n';
|
||||||
import { CloudWorkspaceIcon, LocalWorkspaceIcon } from '@blocksuite/icons/rc';
|
import { CloudWorkspaceIcon, LocalWorkspaceIcon } from '@blocksuite/icons/rc';
|
||||||
import type { WorkspaceMetadata } from '@toeverything/infra';
|
import type { WorkspaceMetadata } from '@toeverything/infra';
|
||||||
import {
|
import {
|
||||||
GlobalContextService,
|
|
||||||
useLiveData,
|
useLiveData,
|
||||||
useService,
|
useService,
|
||||||
WorkspacesService,
|
WorkspacesService,
|
||||||
@@ -20,39 +15,23 @@ import {
|
|||||||
import { useSetAtom } from 'jotai';
|
import { useSetAtom } from 'jotai';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
import {
|
import { WorkspaceCard } from '../../workspace-card';
|
||||||
openCreateWorkspaceModalAtom,
|
|
||||||
openSettingModalAtom,
|
|
||||||
} from '../../../../../atoms';
|
|
||||||
import { WorkspaceSubPath } from '../../../../../shared';
|
|
||||||
import { useNavigateHelper } from '../.././../../../hooks/use-navigate-helper';
|
|
||||||
import * as styles from './index.css';
|
import * as styles from './index.css';
|
||||||
|
|
||||||
function useIsWorkspaceOwner(meta: WorkspaceMetadata) {
|
|
||||||
const info = useWorkspaceInfo(meta);
|
|
||||||
|
|
||||||
return info?.isOwner;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface WorkspaceModalProps {
|
interface WorkspaceModalProps {
|
||||||
disabled?: boolean;
|
|
||||||
workspaces: WorkspaceMetadata[];
|
workspaces: WorkspaceMetadata[];
|
||||||
currentWorkspaceId?: string | null;
|
|
||||||
openingId?: string | null;
|
|
||||||
onClickWorkspace: (workspaceMetadata: WorkspaceMetadata) => void;
|
onClickWorkspace: (workspaceMetadata: WorkspaceMetadata) => void;
|
||||||
onClickWorkspaceSetting: (workspaceMetadata: WorkspaceMetadata) => void;
|
onClickWorkspaceSetting?: (workspaceMetadata: WorkspaceMetadata) => void;
|
||||||
onClickEnableCloud?: (meta: WorkspaceMetadata) => void;
|
onClickEnableCloud?: (meta: WorkspaceMetadata) => void;
|
||||||
onNewWorkspace: () => void;
|
onNewWorkspace: () => void;
|
||||||
onAddWorkspace: () => void;
|
onAddWorkspace: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CloudWorkSpaceList = ({
|
const CloudWorkSpaceList = ({
|
||||||
disabled,
|
|
||||||
workspaces,
|
workspaces,
|
||||||
onClickWorkspace,
|
onClickWorkspace,
|
||||||
onClickWorkspaceSetting,
|
onClickWorkspaceSetting,
|
||||||
currentWorkspaceId,
|
}: Omit<WorkspaceModalProps, 'onNewWorkspace' | 'onAddWorkspace'>) => {
|
||||||
}: WorkspaceModalProps) => {
|
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
if (workspaces.length === 0) {
|
if (workspaces.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
@@ -68,27 +47,20 @@ const CloudWorkSpaceList = ({
|
|||||||
{t['com.affine.workspaceList.workspaceListType.cloud']()}
|
{t['com.affine.workspaceList.workspaceListType.cloud']()}
|
||||||
</div>
|
</div>
|
||||||
<WorkspaceList
|
<WorkspaceList
|
||||||
disabled={disabled}
|
|
||||||
items={workspaces}
|
items={workspaces}
|
||||||
currentWorkspaceId={currentWorkspaceId}
|
|
||||||
onClick={onClickWorkspace}
|
onClick={onClickWorkspace}
|
||||||
onSettingClick={onClickWorkspaceSetting}
|
onSettingClick={onClickWorkspaceSetting}
|
||||||
useIsWorkspaceOwner={useIsWorkspaceOwner}
|
|
||||||
useWorkspaceName={useWorkspaceName}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const LocalWorkspaces = ({
|
const LocalWorkspaces = ({
|
||||||
disabled,
|
|
||||||
workspaces,
|
workspaces,
|
||||||
onClickWorkspace,
|
onClickWorkspace,
|
||||||
onClickWorkspaceSetting,
|
onClickWorkspaceSetting,
|
||||||
onClickEnableCloud,
|
onClickEnableCloud,
|
||||||
openingId,
|
}: Omit<WorkspaceModalProps, 'onNewWorkspace' | 'onAddWorkspace'>) => {
|
||||||
currentWorkspaceId,
|
|
||||||
}: WorkspaceModalProps) => {
|
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
if (workspaces.length === 0) {
|
if (workspaces.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
@@ -104,15 +76,10 @@ const LocalWorkspaces = ({
|
|||||||
{t['com.affine.workspaceList.workspaceListType.local']()}
|
{t['com.affine.workspaceList.workspaceListType.local']()}
|
||||||
</div>
|
</div>
|
||||||
<WorkspaceList
|
<WorkspaceList
|
||||||
openingId={openingId}
|
|
||||||
disabled={disabled}
|
|
||||||
items={workspaces}
|
items={workspaces}
|
||||||
currentWorkspaceId={currentWorkspaceId}
|
|
||||||
onClick={onClickWorkspace}
|
onClick={onClickWorkspace}
|
||||||
onSettingClick={onClickWorkspaceSetting}
|
onSettingClick={onClickWorkspaceSetting}
|
||||||
onEnableCloudClick={onClickEnableCloud}
|
onEnableCloudClick={onClickEnableCloud}
|
||||||
useIsWorkspaceOwner={useIsWorkspaceOwner}
|
|
||||||
useWorkspaceName={useWorkspaceName}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -120,20 +87,20 @@ const LocalWorkspaces = ({
|
|||||||
|
|
||||||
export const AFFiNEWorkspaceList = ({
|
export const AFFiNEWorkspaceList = ({
|
||||||
onEventEnd,
|
onEventEnd,
|
||||||
|
onClickWorkspace,
|
||||||
|
showEnableCloudButton,
|
||||||
|
showSettingsButton,
|
||||||
}: {
|
}: {
|
||||||
|
onClickWorkspace?: (workspaceMetadata: WorkspaceMetadata) => void;
|
||||||
onEventEnd?: () => void;
|
onEventEnd?: () => void;
|
||||||
|
showSettingsButton?: boolean;
|
||||||
|
showEnableCloudButton?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const workspacesService = useService(WorkspacesService);
|
const workspacesService = useService(WorkspacesService);
|
||||||
const workspaces = useLiveData(workspacesService.list.workspaces$);
|
const workspaces = useLiveData(workspacesService.list.workspaces$);
|
||||||
const currentWorkspaceId = useLiveData(
|
|
||||||
useService(GlobalContextService).globalContext.workspaceId.$
|
|
||||||
);
|
|
||||||
|
|
||||||
const setOpenCreateWorkspaceModal = useSetAtom(openCreateWorkspaceModalAtom);
|
|
||||||
const confirmEnableCloud = useEnableCloud();
|
const confirmEnableCloud = useEnableCloud();
|
||||||
|
|
||||||
const { jumpToSubPath } = useNavigateHelper();
|
|
||||||
|
|
||||||
const setOpenSettingModalAtom = useSetAtom(openSettingModalAtom);
|
const setOpenSettingModalAtom = useSetAtom(openSettingModalAtom);
|
||||||
|
|
||||||
const session = useService(AuthService).session;
|
const session = useService(AuthService).session;
|
||||||
@@ -181,34 +148,14 @@ export const AFFiNEWorkspaceList = ({
|
|||||||
[confirmEnableCloud, workspacesService]
|
[confirmEnableCloud, workspacesService]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onClickWorkspace = useCallback(
|
const handleClickWorkspace = useCallback(
|
||||||
(workspaceMetadata: WorkspaceMetadata) => {
|
(workspaceMetadata: WorkspaceMetadata) => {
|
||||||
if (document.startViewTransition) {
|
onClickWorkspace?.(workspaceMetadata);
|
||||||
document.startViewTransition(() => {
|
onEventEnd?.();
|
||||||
jumpToSubPath(workspaceMetadata.id, WorkspaceSubPath.ALL);
|
|
||||||
onEventEnd?.();
|
|
||||||
return new Promise(resolve =>
|
|
||||||
setTimeout(resolve, 150)
|
|
||||||
); /* start transition after 150ms */
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
jumpToSubPath(workspaceMetadata.id, WorkspaceSubPath.ALL);
|
|
||||||
onEventEnd?.();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[jumpToSubPath, onEventEnd]
|
[onClickWorkspace, onEventEnd]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onNewWorkspace = useCallback(() => {
|
|
||||||
setOpenCreateWorkspaceModal('new');
|
|
||||||
onEventEnd?.();
|
|
||||||
}, [onEventEnd, setOpenCreateWorkspaceModal]);
|
|
||||||
|
|
||||||
const onAddWorkspace = useCallback(() => {
|
|
||||||
setOpenCreateWorkspaceModal('add');
|
|
||||||
onEventEnd?.();
|
|
||||||
}, [onEventEnd, setOpenCreateWorkspaceModal]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollableContainer
|
<ScrollableContainer
|
||||||
className={styles.workspaceListsWrapper}
|
className={styles.workspaceListsWrapper}
|
||||||
@@ -218,11 +165,10 @@ export const AFFiNEWorkspaceList = ({
|
|||||||
<div>
|
<div>
|
||||||
<CloudWorkSpaceList
|
<CloudWorkSpaceList
|
||||||
workspaces={cloudWorkspaces}
|
workspaces={cloudWorkspaces}
|
||||||
onClickWorkspace={onClickWorkspace}
|
onClickWorkspace={handleClickWorkspace}
|
||||||
onClickWorkspaceSetting={onClickWorkspaceSetting}
|
onClickWorkspaceSetting={
|
||||||
onNewWorkspace={onNewWorkspace}
|
showSettingsButton ? onClickWorkspaceSetting : undefined
|
||||||
onAddWorkspace={onAddWorkspace}
|
}
|
||||||
currentWorkspaceId={currentWorkspaceId}
|
|
||||||
/>
|
/>
|
||||||
{localWorkspaces.length > 0 && cloudWorkspaces.length > 0 ? (
|
{localWorkspaces.length > 0 && cloudWorkspaces.length > 0 ? (
|
||||||
<Divider size="thinner" />
|
<Divider size="thinner" />
|
||||||
@@ -231,13 +177,55 @@ export const AFFiNEWorkspaceList = ({
|
|||||||
) : null}
|
) : null}
|
||||||
<LocalWorkspaces
|
<LocalWorkspaces
|
||||||
workspaces={localWorkspaces}
|
workspaces={localWorkspaces}
|
||||||
onClickWorkspace={onClickWorkspace}
|
onClickWorkspace={handleClickWorkspace}
|
||||||
onClickWorkspaceSetting={onClickWorkspaceSetting}
|
onClickWorkspaceSetting={
|
||||||
onClickEnableCloud={onClickEnableCloud}
|
showSettingsButton ? onClickWorkspaceSetting : undefined
|
||||||
onNewWorkspace={onNewWorkspace}
|
}
|
||||||
onAddWorkspace={onAddWorkspace}
|
onClickEnableCloud={
|
||||||
currentWorkspaceId={currentWorkspaceId}
|
showEnableCloudButton ? onClickEnableCloud : undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</ScrollableContainer>
|
</ScrollableContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface WorkspaceListProps {
|
||||||
|
items: WorkspaceMetadata[];
|
||||||
|
onClick: (workspace: WorkspaceMetadata) => void;
|
||||||
|
onSettingClick?: (workspace: WorkspaceMetadata) => void;
|
||||||
|
onEnableCloudClick?: (meta: WorkspaceMetadata) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SortableWorkspaceItemProps extends Omit<WorkspaceListProps, 'items'> {
|
||||||
|
workspaceMetadata: WorkspaceMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SortableWorkspaceItem = ({
|
||||||
|
workspaceMetadata,
|
||||||
|
onClick,
|
||||||
|
onSettingClick,
|
||||||
|
onEnableCloudClick,
|
||||||
|
}: SortableWorkspaceItemProps) => {
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
|
onClick(workspaceMetadata);
|
||||||
|
}, [onClick, workspaceMetadata]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WorkspaceCard
|
||||||
|
className={styles.workspaceCard}
|
||||||
|
workspaceMetadata={workspaceMetadata}
|
||||||
|
onClick={handleClick}
|
||||||
|
avatarSize={28}
|
||||||
|
onClickOpenSettings={onSettingClick}
|
||||||
|
onClickEnableCloud={onEnableCloudClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WorkspaceList = (props: WorkspaceListProps) => {
|
||||||
|
const workspaceList = props.items;
|
||||||
|
|
||||||
|
return workspaceList.map(item => (
|
||||||
|
<SortableWorkspaceItem key={item.id} {...props} workspaceMetadata={item} />
|
||||||
|
));
|
||||||
|
};
|
||||||
@@ -0,0 +1,335 @@
|
|||||||
|
import { Button, Skeleton, Tooltip } from '@affine/component';
|
||||||
|
import { Loading } from '@affine/component/ui/loading';
|
||||||
|
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
|
||||||
|
import { useSystemOnline } from '@affine/core/hooks/use-system-online';
|
||||||
|
import { useWorkspace } from '@affine/core/hooks/use-workspace';
|
||||||
|
import { useWorkspaceInfo } from '@affine/core/hooks/use-workspace-info';
|
||||||
|
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
|
||||||
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
|
import {
|
||||||
|
ArrowDownSmallIcon,
|
||||||
|
CloudWorkspaceIcon,
|
||||||
|
CollaborationIcon,
|
||||||
|
InformationFillDuotoneIcon,
|
||||||
|
LocalWorkspaceIcon,
|
||||||
|
NoNetworkIcon,
|
||||||
|
SettingsIcon,
|
||||||
|
UnsyncIcon,
|
||||||
|
} from '@blocksuite/icons/rc';
|
||||||
|
import {
|
||||||
|
useLiveData,
|
||||||
|
useService,
|
||||||
|
type WorkspaceMetadata,
|
||||||
|
type WorkspaceProfileInfo,
|
||||||
|
WorkspaceService,
|
||||||
|
} from '@toeverything/infra';
|
||||||
|
import { cssVar } from '@toeverything/theme';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import type { HTMLAttributes } from 'react';
|
||||||
|
import { forwardRef, useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import * as styles from './styles.css';
|
||||||
|
|
||||||
|
const CloudWorkspaceStatus = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CloudWorkspaceIcon />
|
||||||
|
Cloud
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const SyncingWorkspaceStatus = ({ progress }: { progress?: number }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Loading progress={progress} speed={progress ? 0 : undefined} />
|
||||||
|
Syncing...
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const UnSyncWorkspaceStatus = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<UnsyncIcon />
|
||||||
|
Wait for upload
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const LocalWorkspaceStatus = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{!environment.isDesktop ? (
|
||||||
|
<InformationFillDuotoneIcon style={{ color: cssVar('errorColor') }} />
|
||||||
|
) : (
|
||||||
|
<LocalWorkspaceIcon />
|
||||||
|
)}
|
||||||
|
Local
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const OfflineStatus = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<NoNetworkIcon />
|
||||||
|
Offline
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const useSyncEngineSyncProgress = (meta: WorkspaceMetadata) => {
|
||||||
|
const isOnline = useSystemOnline();
|
||||||
|
const workspace = useWorkspace(meta);
|
||||||
|
|
||||||
|
const engineState = useLiveData(
|
||||||
|
workspace?.engine.docEngineState$.throttleTime(100)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!engineState || !workspace) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const progress =
|
||||||
|
(engineState.total - engineState.syncing) / engineState.total;
|
||||||
|
const syncing = engineState.syncing > 0 || engineState.retrying;
|
||||||
|
|
||||||
|
let content;
|
||||||
|
// TODO(@eyhn): add i18n
|
||||||
|
if (workspace.flavour === WorkspaceFlavour.LOCAL) {
|
||||||
|
if (!environment.isDesktop) {
|
||||||
|
content = 'This is a local demo workspace.';
|
||||||
|
} else {
|
||||||
|
content = 'Saved locally';
|
||||||
|
}
|
||||||
|
} else if (!isOnline) {
|
||||||
|
content = 'Disconnected, please check your network connection';
|
||||||
|
} else if (engineState.retrying && engineState.errorMessage) {
|
||||||
|
content = `${engineState.errorMessage}, reconnecting.`;
|
||||||
|
} else if (engineState.retrying) {
|
||||||
|
content = 'Sync disconnected due to unexpected issues, reconnecting.';
|
||||||
|
} else if (syncing) {
|
||||||
|
content =
|
||||||
|
`Syncing with AFFiNE Cloud` +
|
||||||
|
(progress ? ` (${Math.floor(progress * 100)}%)` : '');
|
||||||
|
} else {
|
||||||
|
content = 'Synced with AFFiNE Cloud';
|
||||||
|
}
|
||||||
|
|
||||||
|
const CloudWorkspaceSyncStatus = () => {
|
||||||
|
if (syncing) {
|
||||||
|
return SyncingWorkspaceStatus({
|
||||||
|
progress: progress ? Math.max(progress, 0.2) : undefined,
|
||||||
|
});
|
||||||
|
} else if (engineState.retrying) {
|
||||||
|
return UnSyncWorkspaceStatus();
|
||||||
|
} else {
|
||||||
|
return CloudWorkspaceStatus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: content,
|
||||||
|
icon:
|
||||||
|
workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD ? (
|
||||||
|
!isOnline ? (
|
||||||
|
<OfflineStatus />
|
||||||
|
) : (
|
||||||
|
<CloudWorkspaceSyncStatus />
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<LocalWorkspaceStatus />
|
||||||
|
),
|
||||||
|
progress,
|
||||||
|
active:
|
||||||
|
workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD &&
|
||||||
|
((syncing && progress !== undefined) || engineState.retrying), // active if syncing or retrying,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const usePauseAnimation = (timeToResume = 5000) => {
|
||||||
|
const [paused, setPaused] = useState(false);
|
||||||
|
|
||||||
|
const resume = useCallback(() => {
|
||||||
|
setPaused(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const pause = useCallback(() => {
|
||||||
|
setPaused(true);
|
||||||
|
if (timeToResume > 0) {
|
||||||
|
setTimeout(resume, timeToResume);
|
||||||
|
}
|
||||||
|
}, [resume, timeToResume]);
|
||||||
|
|
||||||
|
return { paused, pause };
|
||||||
|
};
|
||||||
|
|
||||||
|
const WorkspaceSyncInfo = ({
|
||||||
|
workspaceMetadata,
|
||||||
|
workspaceProfile,
|
||||||
|
}: {
|
||||||
|
workspaceMetadata: WorkspaceMetadata;
|
||||||
|
workspaceProfile: WorkspaceProfileInfo;
|
||||||
|
}) => {
|
||||||
|
const syncStatus = useSyncEngineSyncProgress(workspaceMetadata);
|
||||||
|
const currentWorkspace = useService(WorkspaceService).workspace;
|
||||||
|
const isCloud = currentWorkspace.flavour === WorkspaceFlavour.AFFINE_CLOUD;
|
||||||
|
const { paused, pause } = usePauseAnimation();
|
||||||
|
|
||||||
|
// to make sure that animation will play first time
|
||||||
|
const [delayActive, setDelayActive] = useState(false);
|
||||||
|
useEffect(() => {
|
||||||
|
if (paused || !syncStatus) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const delayOpen = 0;
|
||||||
|
const delayClose = 200;
|
||||||
|
let timer: ReturnType<typeof setTimeout>;
|
||||||
|
if (syncStatus.active) {
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
setDelayActive(syncStatus.active);
|
||||||
|
}, delayOpen);
|
||||||
|
} else {
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
setDelayActive(syncStatus.active);
|
||||||
|
pause();
|
||||||
|
}, delayClose);
|
||||||
|
}
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [pause, paused, syncStatus]);
|
||||||
|
|
||||||
|
if (!workspaceProfile) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.workspaceInfoSlider} data-active={delayActive}>
|
||||||
|
<div className={styles.workspaceInfoSlide}>
|
||||||
|
<div className={styles.workspaceInfo} data-type="normal">
|
||||||
|
<div className={styles.workspaceName} data-testid="workspace-name">
|
||||||
|
{workspaceProfile.name}
|
||||||
|
</div>
|
||||||
|
<div className={styles.workspaceStatus}>
|
||||||
|
{isCloud ? <CloudWorkspaceStatus /> : <LocalWorkspaceStatus />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* when syncing/offline/... */}
|
||||||
|
{syncStatus && (
|
||||||
|
<div className={styles.workspaceInfo} data-type="events">
|
||||||
|
<Tooltip
|
||||||
|
content={syncStatus.message}
|
||||||
|
options={{ className: styles.workspaceInfoTooltip }}
|
||||||
|
>
|
||||||
|
<div className={styles.workspaceActiveStatus}>
|
||||||
|
<SyncingWorkspaceStatus progress={syncStatus.progress} />
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WorkspaceCard = forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
HTMLAttributes<HTMLDivElement> & {
|
||||||
|
workspaceMetadata: WorkspaceMetadata;
|
||||||
|
showSyncStatus?: boolean;
|
||||||
|
showArrowDownIcon?: boolean;
|
||||||
|
avatarSize?: number;
|
||||||
|
disable?: boolean;
|
||||||
|
onClickOpenSettings?: (workspaceMetadata: WorkspaceMetadata) => void;
|
||||||
|
onClickEnableCloud?: (workspaceMetadata: WorkspaceMetadata) => void;
|
||||||
|
}
|
||||||
|
>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
workspaceMetadata,
|
||||||
|
showSyncStatus,
|
||||||
|
showArrowDownIcon,
|
||||||
|
avatarSize = 32,
|
||||||
|
onClickOpenSettings,
|
||||||
|
onClickEnableCloud,
|
||||||
|
className,
|
||||||
|
disable,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
|
const information = useWorkspaceInfo(workspaceMetadata);
|
||||||
|
|
||||||
|
const name = information?.name ?? UNTITLED_WORKSPACE_NAME;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
styles.container,
|
||||||
|
disable ? styles.disable : null,
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
data-testid="workspace-card"
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{information ? (
|
||||||
|
<WorkspaceAvatar
|
||||||
|
meta={workspaceMetadata}
|
||||||
|
rounded={3}
|
||||||
|
data-testid="workspace-avatar"
|
||||||
|
size={avatarSize}
|
||||||
|
name={name}
|
||||||
|
colorfulFallback
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Skeleton width={avatarSize} height={avatarSize} />
|
||||||
|
)}
|
||||||
|
<div className={styles.workspaceTitleContainer}>
|
||||||
|
{information ? (
|
||||||
|
showSyncStatus ? (
|
||||||
|
<WorkspaceSyncInfo
|
||||||
|
workspaceProfile={information}
|
||||||
|
workspaceMetadata={workspaceMetadata}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span className={styles.workspaceName}>{information.name}</span>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<Skeleton width={100} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{onClickEnableCloud &&
|
||||||
|
workspaceMetadata.flavour === WorkspaceFlavour.LOCAL ? (
|
||||||
|
<Button
|
||||||
|
className={styles.showOnCardHover}
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onClickEnableCloud(workspaceMetadata);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Enable Cloud
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
{information?.isOwner ? null : <CollaborationIcon />}
|
||||||
|
{onClickOpenSettings && (
|
||||||
|
<div
|
||||||
|
className={styles.settingButton}
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onClickOpenSettings(workspaceMetadata);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SettingsIcon width={16} height={16} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{showArrowDownIcon && <ArrowDownSmallIcon />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
WorkspaceCard.displayName = 'WorkspaceCard';
|
||||||
@@ -24,6 +24,15 @@ export const container = style({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const disable = style({
|
||||||
|
pointerEvents: 'none',
|
||||||
|
opacity: 0.8,
|
||||||
|
':hover': {
|
||||||
|
cursor: 'default',
|
||||||
|
background: 'none',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const workspaceInfoSlider = style({
|
export const workspaceInfoSlider = style({
|
||||||
height: 42,
|
height: 42,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
@@ -59,7 +68,6 @@ export const workspaceInfo = style({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const workspaceName = style({
|
export const workspaceName = style({
|
||||||
fontSize: cssVar('fontSm'),
|
fontSize: cssVar('fontSm'),
|
||||||
lineHeight: '22px',
|
lineHeight: '22px',
|
||||||
@@ -68,6 +76,8 @@ export const workspaceName = style({
|
|||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
textOverflow: 'ellipsis',
|
textOverflow: 'ellipsis',
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
|
width: '100%',
|
||||||
|
display: 'inline-block',
|
||||||
});
|
});
|
||||||
|
|
||||||
export const workspaceStatus = style({
|
export const workspaceStatus = style({
|
||||||
@@ -105,3 +115,48 @@ export const workspaceInfoTooltip = style({
|
|||||||
padding: '0 8px',
|
padding: '0 8px',
|
||||||
minHeight: 20,
|
minHeight: 20,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const workspaceTitleContainer = style({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
flex: 1,
|
||||||
|
overflow: 'hidden',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const settingButton = style({
|
||||||
|
transition: 'all 0.13s ease',
|
||||||
|
width: 0,
|
||||||
|
height: 20,
|
||||||
|
overflow: 'hidden',
|
||||||
|
marginLeft: 0,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
placeItems: 'center',
|
||||||
|
|
||||||
|
borderRadius: 4,
|
||||||
|
boxShadow: 'none',
|
||||||
|
background: 'transparent',
|
||||||
|
cursor: 'pointer',
|
||||||
|
|
||||||
|
selectors: {
|
||||||
|
[`.${container}:hover &`]: {
|
||||||
|
width: 20,
|
||||||
|
marginLeft: 8,
|
||||||
|
boxShadow: cssVar('shadow1'),
|
||||||
|
background: cssVar('white80'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const showOnCardHover = style({
|
||||||
|
visibility: 'hidden',
|
||||||
|
opacity: 0,
|
||||||
|
selectors: {
|
||||||
|
[`.${container}:hover &`]: {
|
||||||
|
visibility: 'visible',
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
|
|
||||||
export function useDocEngineStatus() {
|
|
||||||
const workspace = useService(WorkspaceService).workspace;
|
|
||||||
|
|
||||||
const engineState = useLiveData(
|
|
||||||
workspace.engine.docEngineState$.throttleTime(100)
|
|
||||||
);
|
|
||||||
const progress =
|
|
||||||
(engineState.total - engineState.syncing) / engineState.total;
|
|
||||||
|
|
||||||
return useMemo(
|
|
||||||
() => ({
|
|
||||||
...engineState,
|
|
||||||
progress,
|
|
||||||
syncing: engineState.syncing > 0 || engineState.retrying,
|
|
||||||
}),
|
|
||||||
[engineState, progress]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,28 +1,28 @@
|
|||||||
import type { WorkspaceSubPath } from '@affine/core/shared';
|
import type { WorkspaceSubPath } from '@affine/core/shared';
|
||||||
import { useCallback, useContext, useMemo } from 'react';
|
import { createContext, useCallback, useContext, useMemo } from 'react';
|
||||||
import type { NavigateOptions, To } from 'react-router-dom';
|
import type { NavigateFunction, NavigateOptions } from 'react-router-dom';
|
||||||
|
|
||||||
import { NavigateContext, router } from '../router';
|
/**
|
||||||
|
* In workbench, we use nested react-router, so default `useNavigate` can't get correct navigate function in workbench.
|
||||||
|
* We use this context to provide navigate function for whole app.
|
||||||
|
*/
|
||||||
|
export const NavigateContext = createContext<NavigateFunction | null>(null);
|
||||||
|
|
||||||
export enum RouteLogic {
|
export enum RouteLogic {
|
||||||
REPLACE = 'replace',
|
REPLACE = 'replace',
|
||||||
PUSH = 'push',
|
PUSH = 'push',
|
||||||
}
|
}
|
||||||
|
|
||||||
function defaultNavigate(to: To, option?: { replace?: boolean }) {
|
|
||||||
setTimeout(() => {
|
|
||||||
router?.navigate(to, option).catch(err => {
|
|
||||||
console.error('Failed to navigate', err);
|
|
||||||
});
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(@eyhn): add a name -> path helper in the results
|
// TODO(@eyhn): add a name -> path helper in the results
|
||||||
/**
|
/**
|
||||||
* @deprecated use `WorkbenchService` instead
|
* Use this for over workbench navigate, for navigate in workbench, use `WorkbenchService`.
|
||||||
*/
|
*/
|
||||||
export function useNavigateHelper() {
|
export function useNavigateHelper() {
|
||||||
const navigate = useContext(NavigateContext) ?? defaultNavigate;
|
const navigate = useContext(NavigateContext);
|
||||||
|
|
||||||
|
if (!navigate) {
|
||||||
|
throw new Error('useNavigateHelper must be used within a NavigateProvider');
|
||||||
|
}
|
||||||
|
|
||||||
const jumpToPage = useCallback(
|
const jumpToPage = useCallback(
|
||||||
(
|
(
|
||||||
@@ -147,7 +147,7 @@ export function useNavigateHelper() {
|
|||||||
const searchParams = new URLSearchParams();
|
const searchParams = new URLSearchParams();
|
||||||
|
|
||||||
if (redirectUri) {
|
if (redirectUri) {
|
||||||
searchParams.set('redirect_uri', encodeURIComponent(redirectUri));
|
searchParams.set('redirect_uri', redirectUri);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params) {
|
if (params) {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
registerAffineUpdatesCommands,
|
registerAffineUpdatesCommands,
|
||||||
} from '../commands';
|
} from '../commands';
|
||||||
import { usePageHelper } from '../components/blocksuite/block-suite-page-list/utils';
|
import { usePageHelper } from '../components/blocksuite/block-suite-page-list/utils';
|
||||||
|
import { CreateWorkspaceDialogService } from '../modules/create-workspace';
|
||||||
import { EditorSettingService } from '../modules/editor-settting';
|
import { EditorSettingService } from '../modules/editor-settting';
|
||||||
import { CMDKQuickSearchService } from '../modules/quicksearch/services/cmdk';
|
import { CMDKQuickSearchService } from '../modules/quicksearch/services/cmdk';
|
||||||
import { useLanguageHelper } from './affine/use-language-helper';
|
import { useLanguageHelper } from './affine/use-language-helper';
|
||||||
@@ -69,6 +70,7 @@ export function useRegisterWorkspaceCommands() {
|
|||||||
const [editor] = useActiveBlocksuiteEditor();
|
const [editor] = useActiveBlocksuiteEditor();
|
||||||
const cmdkQuickSearchService = useService(CMDKQuickSearchService);
|
const cmdkQuickSearchService = useService(CMDKQuickSearchService);
|
||||||
const editorSettingService = useService(EditorSettingService);
|
const editorSettingService = useService(EditorSettingService);
|
||||||
|
const createWorkspaceDialogService = useService(CreateWorkspaceDialogService);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsub = registerCMDKCommand(cmdkQuickSearchService, editor);
|
const unsub = registerCMDKCommand(cmdkQuickSearchService, editor);
|
||||||
@@ -131,7 +133,7 @@ export function useRegisterWorkspaceCommands() {
|
|||||||
// register AffineCreationCommands
|
// register AffineCreationCommands
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsub = registerAffineCreationCommands({
|
const unsub = registerAffineCreationCommands({
|
||||||
store,
|
createWorkspaceDialogService,
|
||||||
pageHelper: pageHelper,
|
pageHelper: pageHelper,
|
||||||
t,
|
t,
|
||||||
});
|
});
|
||||||
@@ -139,7 +141,7 @@ export function useRegisterWorkspaceCommands() {
|
|||||||
return () => {
|
return () => {
|
||||||
unsub();
|
unsub();
|
||||||
};
|
};
|
||||||
}, [store, pageHelper, t]);
|
}, [store, pageHelper, t, createWorkspaceDialogService]);
|
||||||
|
|
||||||
// register AffineHelpCommands
|
// register AffineHelpCommands
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -6,19 +6,19 @@ import {
|
|||||||
} from '@toeverything/infra';
|
} from '@toeverything/infra';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
export function useWorkspaceInfo(meta: WorkspaceMetadata) {
|
export function useWorkspaceInfo(meta?: WorkspaceMetadata) {
|
||||||
const workspacesService = useService(WorkspacesService);
|
const workspacesService = useService(WorkspacesService);
|
||||||
|
|
||||||
const profile = workspacesService.getProfile(meta);
|
const profile = meta ? workspacesService.getProfile(meta) : undefined;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
profile.revalidate();
|
profile?.revalidate();
|
||||||
}, [meta, profile]);
|
}, [meta, profile]);
|
||||||
|
|
||||||
return useLiveData(profile.profile$);
|
return useLiveData(profile?.profile$);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useWorkspaceName(meta: WorkspaceMetadata) {
|
export function useWorkspaceName(meta?: WorkspaceMetadata) {
|
||||||
const information = useWorkspaceInfo(meta);
|
const information = useWorkspaceInfo(meta);
|
||||||
|
|
||||||
return information?.name;
|
return information?.name;
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import {
|
|||||||
appSidebarResizingAtom,
|
appSidebarResizingAtom,
|
||||||
SidebarSwitch,
|
SidebarSwitch,
|
||||||
} from '../components/app-sidebar';
|
} from '../components/app-sidebar';
|
||||||
|
import { OverCapacityNotification } from '../components/over-capacity';
|
||||||
import { AIIsland } from '../components/pure/ai-island';
|
import { AIIsland } from '../components/pure/ai-island';
|
||||||
import { RootAppSidebar } from '../components/root-app-sidebar';
|
import { RootAppSidebar } from '../components/root-app-sidebar';
|
||||||
import { MainContainer } from '../components/workspace';
|
import { MainContainer } from '../components/workspace';
|
||||||
@@ -50,10 +51,7 @@ import { NavigationButtons } from '../modules/navigation';
|
|||||||
import { useRegisterNavigationCommands } from '../modules/navigation/view/use-register-navigation-commands';
|
import { useRegisterNavigationCommands } from '../modules/navigation/view/use-register-navigation-commands';
|
||||||
import { QuickSearchContainer } from '../modules/quicksearch';
|
import { QuickSearchContainer } from '../modules/quicksearch';
|
||||||
import { WorkbenchService } from '../modules/workbench';
|
import { WorkbenchService } from '../modules/workbench';
|
||||||
import {
|
import { CurrentWorkspaceModals } from '../providers/modal-provider';
|
||||||
AllWorkspaceModals,
|
|
||||||
CurrentWorkspaceModals,
|
|
||||||
} from '../providers/modal-provider';
|
|
||||||
import { SWRConfigProvider } from '../providers/swr-config-provider';
|
import { SWRConfigProvider } from '../providers/swr-config-provider';
|
||||||
import * as styles from './styles.css';
|
import * as styles from './styles.css';
|
||||||
|
|
||||||
@@ -63,7 +61,6 @@ export const WorkspaceLayout = function WorkspaceLayout({
|
|||||||
return (
|
return (
|
||||||
<SWRConfigProvider>
|
<SWRConfigProvider>
|
||||||
{/* load all workspaces is costly, do not block the whole UI */}
|
{/* load all workspaces is costly, do not block the whole UI */}
|
||||||
<AllWorkspaceModals />
|
|
||||||
<CurrentWorkspaceModals />
|
<CurrentWorkspaceModals />
|
||||||
<WorkspaceLayoutInner>{children}</WorkspaceLayoutInner>
|
<WorkspaceLayoutInner>{children}</WorkspaceLayoutInner>
|
||||||
{/* should show after workspace loaded */}
|
{/* should show after workspace loaded */}
|
||||||
@@ -173,6 +170,7 @@ export const WorkspaceLayoutProviders = ({ children }: PropsWithChildren) => {
|
|||||||
{children}
|
{children}
|
||||||
<QuickSearchContainer />
|
<QuickSearchContainer />
|
||||||
<SyncAwareness />
|
<SyncAwareness />
|
||||||
|
<OverCapacityNotification />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { Entity, LiveData } from '@toeverything/infra';
|
||||||
|
|
||||||
|
import type { CreateWorkspaceCallbackPayload } from '../types';
|
||||||
|
|
||||||
|
export class CreateWorkspaceDialog extends Entity {
|
||||||
|
readonly mode$ = new LiveData<'new' | 'add'>('new');
|
||||||
|
readonly isOpen$ = new LiveData(false);
|
||||||
|
readonly callback$ = new LiveData<
|
||||||
|
(data: CreateWorkspaceCallbackPayload | undefined) => void
|
||||||
|
>(() => {});
|
||||||
|
|
||||||
|
open(
|
||||||
|
mode: 'new' | 'add',
|
||||||
|
callback?: (data: CreateWorkspaceCallbackPayload | undefined) => void
|
||||||
|
) {
|
||||||
|
this.callback(undefined);
|
||||||
|
this.mode$.next(mode);
|
||||||
|
this.isOpen$.next(true);
|
||||||
|
if (callback) {
|
||||||
|
this.callback$.next(callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(payload: CreateWorkspaceCallbackPayload | undefined) {
|
||||||
|
this.callback$.value(payload);
|
||||||
|
this.callback$.next(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.isOpen$.next(false);
|
||||||
|
this.callback(undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
12
packages/frontend/core/src/modules/create-workspace/index.ts
Normal file
12
packages/frontend/core/src/modules/create-workspace/index.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import type { Framework } from '@toeverything/infra';
|
||||||
|
|
||||||
|
import { CreateWorkspaceDialog } from './entities/dialog';
|
||||||
|
import { CreateWorkspaceDialogService } from './services/dialog';
|
||||||
|
|
||||||
|
export { CreateWorkspaceDialogService } from './services/dialog';
|
||||||
|
export type { CreateWorkspaceCallbackPayload } from './types';
|
||||||
|
export { CreateWorkspaceDialogProvider } from './views/dialog';
|
||||||
|
|
||||||
|
export function configureCreateWorkspaceModule(framework: Framework) {
|
||||||
|
framework.service(CreateWorkspaceDialogService).entity(CreateWorkspaceDialog);
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { Service } from '@toeverything/infra';
|
||||||
|
|
||||||
|
import { CreateWorkspaceDialog } from '../entities/dialog';
|
||||||
|
|
||||||
|
export class CreateWorkspaceDialogService extends Service {
|
||||||
|
dialog = this.framework.createEntity(CreateWorkspaceDialog);
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import type { WorkspaceMetadata } from '@toeverything/infra';
|
||||||
|
|
||||||
|
export type CreateWorkspaceMode = 'add' | 'new';
|
||||||
|
export type CreateWorkspaceCallbackPayload = {
|
||||||
|
meta: WorkspaceMetadata;
|
||||||
|
defaultDocId?: string;
|
||||||
|
};
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import { cssVar } from '@toeverything/theme';
|
||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const header = style({
|
||||||
|
position: 'relative',
|
||||||
|
marginTop: '44px',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const subTitle = style({
|
||||||
|
fontSize: cssVar('fontSm'),
|
||||||
|
color: cssVar('textPrimaryColor'),
|
||||||
|
fontWeight: 600,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const avatarWrapper = style({
|
||||||
|
display: 'flex',
|
||||||
|
margin: '10px 0',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const workspaceNameWrapper = style({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '8px',
|
||||||
|
padding: '12px 0',
|
||||||
|
});
|
||||||
|
export const affineCloudWrapper = style({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '6px',
|
||||||
|
paddingTop: '10px',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const card = style({
|
||||||
|
padding: '12px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
borderRadius: '8px',
|
||||||
|
backgroundColor: cssVar('backgroundSecondaryColor'),
|
||||||
|
minHeight: '114px',
|
||||||
|
position: 'relative',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const cardText = style({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
width: '100%',
|
||||||
|
gap: '12px',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const cardTitle = style({
|
||||||
|
fontSize: cssVar('fontBase'),
|
||||||
|
color: cssVar('textPrimaryColor'),
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
});
|
||||||
|
export const cardDescription = style({
|
||||||
|
fontSize: cssVar('fontXs'),
|
||||||
|
color: cssVar('textSecondaryColor'),
|
||||||
|
maxWidth: '288px',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const cloudTips = style({
|
||||||
|
fontSize: cssVar('fontXs'),
|
||||||
|
color: cssVar('textSecondaryColor'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const cloudSvgContainer = style({
|
||||||
|
width: '146px',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
alignItems: 'center',
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: '0',
|
||||||
|
right: '0',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
});
|
||||||
@@ -0,0 +1,272 @@
|
|||||||
|
import { Avatar, ConfirmModal, Input, Switch, toast } from '@affine/component';
|
||||||
|
import type { ConfirmModalProps } from '@affine/component/ui/modal';
|
||||||
|
import { authAtom } from '@affine/core/atoms';
|
||||||
|
import { CloudSvg } from '@affine/core/components/affine/share-page-modal/cloud-svg';
|
||||||
|
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||||
|
import { track } from '@affine/core/mixpanel';
|
||||||
|
import { DebugLogger } from '@affine/debug';
|
||||||
|
import { apis } from '@affine/electron-api';
|
||||||
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
|
import { useI18n } from '@affine/i18n';
|
||||||
|
import {
|
||||||
|
initEmptyPage,
|
||||||
|
useLiveData,
|
||||||
|
useService,
|
||||||
|
WorkspacesService,
|
||||||
|
} from '@toeverything/infra';
|
||||||
|
import { useSetAtom } from 'jotai';
|
||||||
|
import type { KeyboardEvent } from 'react';
|
||||||
|
import { useCallback, useLayoutEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { buildShowcaseWorkspace } from '../../../bootstrap/first-app-data';
|
||||||
|
import { AuthService } from '../../../modules/cloud';
|
||||||
|
import { _addLocalWorkspace } from '../../../modules/workspace-engine';
|
||||||
|
import { CreateWorkspaceDialogService } from '../services/dialog';
|
||||||
|
import * as styles from './dialog.css';
|
||||||
|
|
||||||
|
const logger = new DebugLogger('CreateWorkspaceModal');
|
||||||
|
|
||||||
|
interface NameWorkspaceContentProps extends ConfirmModalProps {
|
||||||
|
loading: boolean;
|
||||||
|
onConfirmName: (
|
||||||
|
name: string,
|
||||||
|
workspaceFlavour: WorkspaceFlavour,
|
||||||
|
avatar?: File
|
||||||
|
) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldEnableCloud = !runtimeConfig.allowLocalWorkspace;
|
||||||
|
|
||||||
|
const NameWorkspaceContent = ({
|
||||||
|
loading,
|
||||||
|
onConfirmName,
|
||||||
|
...props
|
||||||
|
}: NameWorkspaceContentProps) => {
|
||||||
|
const t = useI18n();
|
||||||
|
const [workspaceName, setWorkspaceName] = useState('');
|
||||||
|
const [enable, setEnable] = useState(shouldEnableCloud);
|
||||||
|
const session = useService(AuthService).session;
|
||||||
|
const loginStatus = useLiveData(session.status$);
|
||||||
|
|
||||||
|
const setOpenSignIn = useSetAtom(authAtom);
|
||||||
|
|
||||||
|
const openSignInModal = useCallback(() => {
|
||||||
|
setOpenSignIn(state => ({
|
||||||
|
...state,
|
||||||
|
openModal: true,
|
||||||
|
}));
|
||||||
|
}, [setOpenSignIn]);
|
||||||
|
|
||||||
|
const onSwitchChange = useCallback(
|
||||||
|
(checked: boolean) => {
|
||||||
|
if (loginStatus !== 'authenticated') {
|
||||||
|
return openSignInModal();
|
||||||
|
}
|
||||||
|
return setEnable(checked);
|
||||||
|
},
|
||||||
|
[loginStatus, openSignInModal]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleCreateWorkspace = useCallback(() => {
|
||||||
|
onConfirmName(
|
||||||
|
workspaceName,
|
||||||
|
enable ? WorkspaceFlavour.AFFINE_CLOUD : WorkspaceFlavour.LOCAL
|
||||||
|
);
|
||||||
|
}, [enable, onConfirmName, workspaceName]);
|
||||||
|
|
||||||
|
const handleKeyDown = useCallback(
|
||||||
|
(event: KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (event.key === 'Enter' && workspaceName) {
|
||||||
|
handleCreateWorkspace();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[handleCreateWorkspace, workspaceName]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Currently, when we create a new workspace and upload an avatar at the same time,
|
||||||
|
// an error occurs after the creation is successful: get blob 404 not found
|
||||||
|
return (
|
||||||
|
<ConfirmModal
|
||||||
|
defaultOpen={true}
|
||||||
|
title={t['com.affine.nameWorkspace.title']()}
|
||||||
|
description={t['com.affine.nameWorkspace.description']()}
|
||||||
|
cancelText={t['com.affine.nameWorkspace.button.cancel']()}
|
||||||
|
confirmText={t['com.affine.nameWorkspace.button.create']()}
|
||||||
|
confirmButtonOptions={{
|
||||||
|
variant: 'primary',
|
||||||
|
loading,
|
||||||
|
disabled: !workspaceName,
|
||||||
|
['data-testid' as string]: 'create-workspace-create-button',
|
||||||
|
}}
|
||||||
|
closeButtonOptions={{
|
||||||
|
['data-testid' as string]: 'create-workspace-close-button',
|
||||||
|
}}
|
||||||
|
onConfirm={handleCreateWorkspace}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div className={styles.avatarWrapper}>
|
||||||
|
<Avatar size={56} name={workspaceName} colorfulFallback />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.workspaceNameWrapper}>
|
||||||
|
<div className={styles.subTitle}>
|
||||||
|
{t['com.affine.nameWorkspace.subtitle.workspace-name']()}
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
autoFocus
|
||||||
|
data-testid="create-workspace-input"
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder={t['com.affine.nameWorkspace.placeholder']()}
|
||||||
|
maxLength={64}
|
||||||
|
minLength={0}
|
||||||
|
onChange={setWorkspaceName}
|
||||||
|
size="large"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.affineCloudWrapper}>
|
||||||
|
<div className={styles.subTitle}>{t['AFFiNE Cloud']()}</div>
|
||||||
|
<div className={styles.card}>
|
||||||
|
<div className={styles.cardText}>
|
||||||
|
<div className={styles.cardTitle}>
|
||||||
|
<span>{t['com.affine.nameWorkspace.affine-cloud.title']()}</span>
|
||||||
|
<Switch
|
||||||
|
checked={enable}
|
||||||
|
onChange={onSwitchChange}
|
||||||
|
disabled={shouldEnableCloud}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.cardDescription}>
|
||||||
|
{t['com.affine.nameWorkspace.affine-cloud.description']()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.cloudSvgContainer}>
|
||||||
|
<CloudSvg />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{shouldEnableCloud ? (
|
||||||
|
<a
|
||||||
|
className={styles.cloudTips}
|
||||||
|
href={runtimeConfig.downloadUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
{t['com.affine.nameWorkspace.affine-cloud.web-tips']()}
|
||||||
|
</a>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</ConfirmModal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const CreateWorkspaceDialog = () => {
|
||||||
|
const createWorkspaceDialogService = useService(CreateWorkspaceDialogService);
|
||||||
|
const mode = useLiveData(createWorkspaceDialogService.dialog.mode$);
|
||||||
|
const t = useI18n();
|
||||||
|
const workspacesService = useService(WorkspacesService);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
// TODO(@Peng): maybe refactor using xstate?
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
let canceled = false;
|
||||||
|
// if mode changed, reset step
|
||||||
|
if (mode === 'add') {
|
||||||
|
// a hack for now
|
||||||
|
// when adding a workspace, we will immediately let user select a db file
|
||||||
|
// after it is done, it will effectively add a new workspace to app-data folder
|
||||||
|
// so after that, we will be able to load it via importLocalWorkspace
|
||||||
|
(async () => {
|
||||||
|
if (!apis) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logger.info('load db file');
|
||||||
|
const result = await apis.dialog.loadDBFile();
|
||||||
|
if (result.workspaceId && !canceled) {
|
||||||
|
_addLocalWorkspace(result.workspaceId);
|
||||||
|
workspacesService.list.revalidate();
|
||||||
|
createWorkspaceDialogService.dialog.callback({
|
||||||
|
meta: {
|
||||||
|
flavour: WorkspaceFlavour.LOCAL,
|
||||||
|
id: result.workspaceId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else if (result.error || result.canceled) {
|
||||||
|
if (result.error) {
|
||||||
|
toast(t[result.error]());
|
||||||
|
}
|
||||||
|
createWorkspaceDialogService.dialog.callback(undefined);
|
||||||
|
createWorkspaceDialogService.dialog.close();
|
||||||
|
}
|
||||||
|
})().catch(err => {
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
canceled = true;
|
||||||
|
};
|
||||||
|
}, [createWorkspaceDialogService, mode, t, workspacesService]);
|
||||||
|
|
||||||
|
const onConfirmName = useAsyncCallback(
|
||||||
|
async (name: string, workspaceFlavour: WorkspaceFlavour) => {
|
||||||
|
track.$.$.$.createWorkspace({ flavour: workspaceFlavour });
|
||||||
|
if (loading) return;
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// this will be the last step for web for now
|
||||||
|
// fix me later
|
||||||
|
if (runtimeConfig.enablePreloading) {
|
||||||
|
const { meta, defaultDocId } = await buildShowcaseWorkspace(
|
||||||
|
workspacesService,
|
||||||
|
workspaceFlavour,
|
||||||
|
name
|
||||||
|
);
|
||||||
|
createWorkspaceDialogService.dialog.callback({ meta, defaultDocId });
|
||||||
|
} else {
|
||||||
|
let defaultDocId: string | undefined = undefined;
|
||||||
|
const meta = await workspacesService.create(
|
||||||
|
workspaceFlavour,
|
||||||
|
async workspace => {
|
||||||
|
workspace.meta.initialize();
|
||||||
|
workspace.meta.setName(name);
|
||||||
|
const page = workspace.createDoc();
|
||||||
|
defaultDocId = page.id;
|
||||||
|
initEmptyPage(page);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
createWorkspaceDialogService.dialog.callback({ meta, defaultDocId });
|
||||||
|
}
|
||||||
|
|
||||||
|
createWorkspaceDialogService.dialog.close();
|
||||||
|
setLoading(false);
|
||||||
|
},
|
||||||
|
[createWorkspaceDialogService.dialog, loading, workspacesService]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onOpenChange = useCallback(
|
||||||
|
(open: boolean) => {
|
||||||
|
if (!open) {
|
||||||
|
createWorkspaceDialogService.dialog.close();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[createWorkspaceDialogService]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (mode === 'new') {
|
||||||
|
return (
|
||||||
|
<NameWorkspaceContent
|
||||||
|
loading={loading}
|
||||||
|
open
|
||||||
|
onOpenChange={onOpenChange}
|
||||||
|
onConfirmName={onConfirmName}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CreateWorkspaceDialogProvider = () => {
|
||||||
|
const createWorkspaceDialogService = useService(CreateWorkspaceDialogService);
|
||||||
|
const isOpen = useLiveData(createWorkspaceDialogService.dialog.isOpen$);
|
||||||
|
|
||||||
|
return isOpen ? <CreateWorkspaceDialog /> : null;
|
||||||
|
};
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { Entity, LiveData } from '@toeverything/infra';
|
||||||
|
|
||||||
|
export class ImportTemplateDialog extends Entity {
|
||||||
|
readonly isOpen$ = new LiveData(false);
|
||||||
|
readonly template$ = new LiveData<{
|
||||||
|
workspaceId: string;
|
||||||
|
docId: string;
|
||||||
|
templateName: string;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
open(workspaceId: string, docId: string, templateName: string) {
|
||||||
|
this.template$.next({ workspaceId, docId, templateName });
|
||||||
|
this.isOpen$.next(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.isOpen$.next(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import {
|
||||||
|
backoffRetry,
|
||||||
|
catchErrorInto,
|
||||||
|
effect,
|
||||||
|
Entity,
|
||||||
|
fromPromise,
|
||||||
|
LiveData,
|
||||||
|
onComplete,
|
||||||
|
onStart,
|
||||||
|
} from '@toeverything/infra';
|
||||||
|
import { EMPTY, mergeMap, switchMap } from 'rxjs';
|
||||||
|
|
||||||
|
import { isBackendError, isNetworkError } from '../../cloud';
|
||||||
|
import type { TemplateDownloaderStore } from '../store/downloader';
|
||||||
|
|
||||||
|
export class TemplateDownloader extends Entity {
|
||||||
|
constructor(private readonly store: TemplateDownloaderStore) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly isDownloading$ = new LiveData<boolean>(false);
|
||||||
|
readonly data$ = new LiveData<Uint8Array | null>(null);
|
||||||
|
readonly error$ = new LiveData<any | null>(null);
|
||||||
|
|
||||||
|
readonly download = effect(
|
||||||
|
switchMap(
|
||||||
|
({ workspaceId, docId }: { workspaceId: string; docId: string }) => {
|
||||||
|
return fromPromise(() => this.store.download(workspaceId, docId)).pipe(
|
||||||
|
mergeMap(({ data }) => {
|
||||||
|
this.data$.next(data);
|
||||||
|
return EMPTY;
|
||||||
|
}),
|
||||||
|
backoffRetry({
|
||||||
|
when: isNetworkError,
|
||||||
|
count: Infinity,
|
||||||
|
}),
|
||||||
|
backoffRetry({
|
||||||
|
when: isBackendError,
|
||||||
|
}),
|
||||||
|
catchErrorInto(this.error$),
|
||||||
|
onStart(() => {
|
||||||
|
this.isDownloading$.next(true);
|
||||||
|
this.data$.next(null);
|
||||||
|
this.error$.next(null);
|
||||||
|
}),
|
||||||
|
onComplete(() => this.isDownloading$.next(false))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
22
packages/frontend/core/src/modules/import-template/index.ts
Normal file
22
packages/frontend/core/src/modules/import-template/index.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { type Framework, WorkspacesService } from '@toeverything/infra';
|
||||||
|
|
||||||
|
import { FetchService } from '../cloud';
|
||||||
|
import { ImportTemplateDialog } from './entities/dialog';
|
||||||
|
import { TemplateDownloader } from './entities/downloader';
|
||||||
|
import { ImportTemplateDialogService } from './services/dialog';
|
||||||
|
import { TemplateDownloaderService } from './services/downloader';
|
||||||
|
import { ImportTemplateService } from './services/import';
|
||||||
|
import { TemplateDownloaderStore } from './store/downloader';
|
||||||
|
|
||||||
|
export { ImportTemplateDialogService } from './services/dialog';
|
||||||
|
export { ImportTemplateDialogProvider } from './views/dialog';
|
||||||
|
|
||||||
|
export function configureImportTemplateModule(framework: Framework) {
|
||||||
|
framework
|
||||||
|
.service(ImportTemplateDialogService)
|
||||||
|
.entity(ImportTemplateDialog)
|
||||||
|
.service(TemplateDownloaderService)
|
||||||
|
.entity(TemplateDownloader, [TemplateDownloaderStore])
|
||||||
|
.store(TemplateDownloaderStore, [FetchService])
|
||||||
|
.service(ImportTemplateService, [WorkspacesService]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { Service } from '@toeverything/infra';
|
||||||
|
|
||||||
|
import { ImportTemplateDialog } from '../entities/dialog';
|
||||||
|
|
||||||
|
export class ImportTemplateDialogService extends Service {
|
||||||
|
dialog = this.framework.createEntity(ImportTemplateDialog);
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { Service } from '@toeverything/infra';
|
||||||
|
|
||||||
|
import { TemplateDownloader } from '../entities/downloader';
|
||||||
|
|
||||||
|
export class TemplateDownloaderService extends Service {
|
||||||
|
downloader = this.framework.createEntity(TemplateDownloader);
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import type { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
|
import type { WorkspaceMetadata, WorkspacesService } from '@toeverything/infra';
|
||||||
|
import { Service } from '@toeverything/infra';
|
||||||
|
|
||||||
|
export class ImportTemplateService extends Service {
|
||||||
|
constructor(private readonly workspacesService: WorkspacesService) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
async importToWorkspace(
|
||||||
|
workspaceMetadata: WorkspaceMetadata,
|
||||||
|
docBinary: Uint8Array
|
||||||
|
) {
|
||||||
|
const { workspace, dispose: disposeWorkspace } =
|
||||||
|
this.workspacesService.open({
|
||||||
|
metadata: workspaceMetadata,
|
||||||
|
});
|
||||||
|
await workspace.engine.waitForRootDocReady();
|
||||||
|
const newDoc = workspace.docCollection.createDoc({});
|
||||||
|
await workspace.engine.doc.storage.behavior.doc.set(
|
||||||
|
newDoc.spaceDoc.guid,
|
||||||
|
docBinary
|
||||||
|
);
|
||||||
|
disposeWorkspace();
|
||||||
|
return newDoc.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
async importToNewWorkspace(
|
||||||
|
flavour: WorkspaceFlavour,
|
||||||
|
workspaceName: string,
|
||||||
|
docBinary: Uint8Array
|
||||||
|
) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
let docId: string = null!;
|
||||||
|
const { id: workspaceId } = await this.workspacesService.create(
|
||||||
|
flavour,
|
||||||
|
async (docCollection, _, docStorage) => {
|
||||||
|
docCollection.meta.initialize();
|
||||||
|
docCollection.meta.setName(workspaceName);
|
||||||
|
const doc = docCollection.createDoc();
|
||||||
|
docId = doc.id;
|
||||||
|
await docStorage.doc.set(doc.spaceDoc.guid, docBinary);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return { workspaceId, docId };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { Store } from '@toeverything/infra';
|
||||||
|
|
||||||
|
import type { FetchService } from '../../cloud';
|
||||||
|
|
||||||
|
export class TemplateDownloaderStore extends Store {
|
||||||
|
constructor(private readonly fetchService: FetchService) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
async download(workspaceId: string, docId: string) {
|
||||||
|
const response = await this.fetchService.fetch(
|
||||||
|
`/api/workspaces/${workspaceId}/docs/${docId}`,
|
||||||
|
{
|
||||||
|
priority: 'high',
|
||||||
|
} as any
|
||||||
|
);
|
||||||
|
const arrayBuffer = await response.arrayBuffer();
|
||||||
|
|
||||||
|
return { data: new Uint8Array(arrayBuffer) };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const dialogContainer = style({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
color: cssVarV2('text/primary'),
|
||||||
|
padding: '16px',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const mainIcon = style({
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
color: cssVarV2('icon/primary'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const mainTitle = style({
|
||||||
|
fontSize: '18px',
|
||||||
|
lineHeight: '26px',
|
||||||
|
textAlign: 'center',
|
||||||
|
marginTop: '16px',
|
||||||
|
fontWeight: 600,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const desc = style({
|
||||||
|
textAlign: 'center',
|
||||||
|
color: cssVarV2('text/secondary'),
|
||||||
|
marginBottom: '20px',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const mainButton = style({
|
||||||
|
width: '100%',
|
||||||
|
fontSize: '14px',
|
||||||
|
height: '42px',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const modal = style({
|
||||||
|
maxWidth: '400px',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const workspaceSelector = style({
|
||||||
|
margin: '0 -16px',
|
||||||
|
width: 'calc(100% + 32px)',
|
||||||
|
border: `1px solid ${cssVarV2('layer/insideBorder/border')}`,
|
||||||
|
padding: '0 16px',
|
||||||
|
});
|
||||||
@@ -0,0 +1,249 @@
|
|||||||
|
import { Button, Modal } from '@affine/component';
|
||||||
|
import { WorkspaceSelector } from '@affine/core/components/workspace-selector';
|
||||||
|
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||||
|
import { useNavigateHelper } from '@affine/core/hooks/use-navigate-helper';
|
||||||
|
import { useWorkspaceName } from '@affine/core/hooks/use-workspace-info';
|
||||||
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
|
import { useI18n } from '@affine/i18n';
|
||||||
|
import { AllDocsIcon } from '@blocksuite/icons/rc';
|
||||||
|
import {
|
||||||
|
useLiveData,
|
||||||
|
useService,
|
||||||
|
type WorkspaceMetadata,
|
||||||
|
WorkspacesService,
|
||||||
|
} from '@toeverything/infra';
|
||||||
|
import { cssVar } from '@toeverything/theme';
|
||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { AuthService } from '../../cloud';
|
||||||
|
import type { CreateWorkspaceCallbackPayload } from '../../create-workspace';
|
||||||
|
import { ImportTemplateDialogService } from '../services/dialog';
|
||||||
|
import { TemplateDownloaderService } from '../services/downloader';
|
||||||
|
import { ImportTemplateService } from '../services/import';
|
||||||
|
import * as styles from './dialog.css';
|
||||||
|
|
||||||
|
const Dialog = ({
|
||||||
|
workspaceId,
|
||||||
|
docId,
|
||||||
|
templateName,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
|
docId: string;
|
||||||
|
templateName: string;
|
||||||
|
onClose?: () => void;
|
||||||
|
}) => {
|
||||||
|
const t = useI18n();
|
||||||
|
const session = useService(AuthService).session;
|
||||||
|
const notLogin = useLiveData(session.status$) === 'unauthenticated';
|
||||||
|
const isSessionRevalidating = useLiveData(session.isRevalidating$);
|
||||||
|
|
||||||
|
const [importing, setImporting] = useState(false);
|
||||||
|
const [importingError, setImportingError] = useState<any>(null);
|
||||||
|
const workspacesService = useService(WorkspacesService);
|
||||||
|
const templateDownloaderService = useService(TemplateDownloaderService);
|
||||||
|
const importTemplateService = useService(ImportTemplateService);
|
||||||
|
const templateDownloader = templateDownloaderService.downloader;
|
||||||
|
const isDownloading = useLiveData(templateDownloader.isDownloading$);
|
||||||
|
const downloadError = useLiveData(templateDownloader.error$);
|
||||||
|
const workspaces = useLiveData(workspacesService.list.workspaces$);
|
||||||
|
const [rawSelectedWorkspace, setSelectedWorkspace] =
|
||||||
|
useState<WorkspaceMetadata | null>(null);
|
||||||
|
const selectedWorkspace =
|
||||||
|
rawSelectedWorkspace ??
|
||||||
|
workspaces.find(w => w.flavour === WorkspaceFlavour.AFFINE_CLOUD) ??
|
||||||
|
workspaces.at(0);
|
||||||
|
const selectedWorkspaceName = useWorkspaceName(selectedWorkspace);
|
||||||
|
const { openPage, jumpToSignIn } = useNavigateHelper();
|
||||||
|
|
||||||
|
const noWorkspace = workspaces.length === 0;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
workspacesService.list.revalidate();
|
||||||
|
}, [workspacesService]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
session.revalidate();
|
||||||
|
}, [session]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isSessionRevalidating && notLogin) {
|
||||||
|
jumpToSignIn(
|
||||||
|
'/template/import?workspaceId=' +
|
||||||
|
workspaceId +
|
||||||
|
'&docId=' +
|
||||||
|
docId +
|
||||||
|
'&name=' +
|
||||||
|
templateName
|
||||||
|
);
|
||||||
|
onClose?.();
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
docId,
|
||||||
|
isSessionRevalidating,
|
||||||
|
jumpToSignIn,
|
||||||
|
notLogin,
|
||||||
|
onClose,
|
||||||
|
templateName,
|
||||||
|
workspaceId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
templateDownloader.download({ workspaceId, docId });
|
||||||
|
}, [docId, templateDownloader, workspaceId]);
|
||||||
|
|
||||||
|
const handleSelectedWorkspace = useCallback(
|
||||||
|
(workspaceMetadata: WorkspaceMetadata) => {
|
||||||
|
return setSelectedWorkspace(workspaceMetadata);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleCreatedWorkspace = useCallback(
|
||||||
|
(payload: CreateWorkspaceCallbackPayload) => {
|
||||||
|
return setSelectedWorkspace(payload.meta);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleImportToSelectedWorkspace = useAsyncCallback(async () => {
|
||||||
|
if (templateDownloader.data$.value && selectedWorkspace) {
|
||||||
|
setImporting(true);
|
||||||
|
try {
|
||||||
|
const docId = await importTemplateService.importToWorkspace(
|
||||||
|
selectedWorkspace,
|
||||||
|
templateDownloader.data$.value
|
||||||
|
);
|
||||||
|
openPage(selectedWorkspace.id, docId);
|
||||||
|
onClose?.();
|
||||||
|
} catch (err) {
|
||||||
|
setImportingError(err);
|
||||||
|
} finally {
|
||||||
|
setImporting(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
importTemplateService,
|
||||||
|
onClose,
|
||||||
|
openPage,
|
||||||
|
selectedWorkspace,
|
||||||
|
templateDownloader.data$.value,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleImportToNewWorkspace = useAsyncCallback(async () => {
|
||||||
|
if (!templateDownloader.data$.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setImporting(true);
|
||||||
|
try {
|
||||||
|
const { workspaceId, docId } =
|
||||||
|
await importTemplateService.importToNewWorkspace(
|
||||||
|
WorkspaceFlavour.AFFINE_CLOUD,
|
||||||
|
'Workspace',
|
||||||
|
templateDownloader.data$.value
|
||||||
|
);
|
||||||
|
openPage(workspaceId, docId);
|
||||||
|
onClose?.();
|
||||||
|
} catch (err) {
|
||||||
|
setImportingError(err);
|
||||||
|
} finally {
|
||||||
|
setImporting(false);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
importTemplateService,
|
||||||
|
onClose,
|
||||||
|
openPage,
|
||||||
|
templateDownloader.data$.value,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const disabled = isDownloading || importing || notLogin;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={styles.dialogContainer}>
|
||||||
|
<AllDocsIcon className={styles.mainIcon} />
|
||||||
|
<h6 className={styles.mainTitle}>
|
||||||
|
{t['com.affine.import-template.dialog.createDocWithTemplate']({
|
||||||
|
templateName,
|
||||||
|
})}
|
||||||
|
</h6>
|
||||||
|
{noWorkspace ? (
|
||||||
|
<p className={styles.desc}>A new workspace will be created.</p>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<p className={styles.desc}>Choose a workspace.</p>
|
||||||
|
<WorkspaceSelector
|
||||||
|
workspaceMetadata={selectedWorkspace}
|
||||||
|
onSelectWorkspace={handleSelectedWorkspace}
|
||||||
|
onCreatedWorkspace={handleCreatedWorkspace}
|
||||||
|
className={styles.workspaceSelector}
|
||||||
|
showArrowDownIcon
|
||||||
|
disable={disabled}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{importingError && (
|
||||||
|
<span style={{ color: cssVar('warningColor') }}>
|
||||||
|
{t['com.affine.import-template.dialog.errorImport']()}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{downloadError ? (
|
||||||
|
<span style={{ color: cssVar('warningColor') }}>
|
||||||
|
{t['com.affine.import-template.dialog.errorLoad']()}
|
||||||
|
</span>
|
||||||
|
) : selectedWorkspace ? (
|
||||||
|
<Button
|
||||||
|
className={styles.mainButton}
|
||||||
|
variant={disabled ? 'secondary' : 'primary'}
|
||||||
|
loading={disabled}
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={handleImportToSelectedWorkspace}
|
||||||
|
>
|
||||||
|
{selectedWorkspaceName &&
|
||||||
|
t['com.affine.import-template.dialog.createDocToWorkspace']({
|
||||||
|
workspace: selectedWorkspaceName,
|
||||||
|
})}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
className={styles.mainButton}
|
||||||
|
variant="primary"
|
||||||
|
loading={disabled}
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={handleImportToNewWorkspace}
|
||||||
|
>
|
||||||
|
{t['com.affine.import-template.dialog.createDocToNewWorkspace']()}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ImportTemplateDialogProvider = () => {
|
||||||
|
const importTemplateDialogService = useService(ImportTemplateDialogService);
|
||||||
|
const isOpen = useLiveData(importTemplateDialogService.dialog.isOpen$);
|
||||||
|
const template = useLiveData(importTemplateDialogService.dialog.template$);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
open={isOpen}
|
||||||
|
modal={true}
|
||||||
|
persistent
|
||||||
|
withoutCloseButton
|
||||||
|
contentOptions={{
|
||||||
|
className: styles.modal,
|
||||||
|
}}
|
||||||
|
onOpenChange={() => importTemplateDialogService.dialog.close()}
|
||||||
|
>
|
||||||
|
{template && (
|
||||||
|
<Dialog
|
||||||
|
docId={template.docId}
|
||||||
|
templateName={template.templateName}
|
||||||
|
workspaceId={template.workspaceId}
|
||||||
|
onClose={() => importTemplateDialogService.dialog.close()}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -3,6 +3,7 @@ import { configureInfraModules, type Framework } from '@toeverything/infra';
|
|||||||
|
|
||||||
import { configureCloudModule } from './cloud';
|
import { configureCloudModule } from './cloud';
|
||||||
import { configureCollectionModule } from './collection';
|
import { configureCollectionModule } from './collection';
|
||||||
|
import { configureCreateWorkspaceModule } from './create-workspace';
|
||||||
import { configureDocLinksModule } from './doc-link';
|
import { configureDocLinksModule } from './doc-link';
|
||||||
import { configureDocsSearchModule } from './docs-search';
|
import { configureDocsSearchModule } from './docs-search';
|
||||||
import { configureEditorModule } from './editor';
|
import { configureEditorModule } from './editor';
|
||||||
@@ -10,6 +11,7 @@ import { configureEditorSettingModule } from './editor-settting';
|
|||||||
import { configureExplorerModule } from './explorer';
|
import { configureExplorerModule } from './explorer';
|
||||||
import { configureFavoriteModule } from './favorite';
|
import { configureFavoriteModule } from './favorite';
|
||||||
import { configureFindInPageModule } from './find-in-page';
|
import { configureFindInPageModule } from './find-in-page';
|
||||||
|
import { configureImportTemplateModule } from './import-template';
|
||||||
import { configureNavigationModule } from './navigation';
|
import { configureNavigationModule } from './navigation';
|
||||||
import { configureOrganizeModule } from './organize';
|
import { configureOrganizeModule } from './organize';
|
||||||
import { configurePeekViewModule } from './peek-view';
|
import { configurePeekViewModule } from './peek-view';
|
||||||
@@ -45,4 +47,6 @@ export function configureCommonModules(framework: Framework) {
|
|||||||
configureEditorModule(framework);
|
configureEditorModule(framework);
|
||||||
configureSystemFontFamilyModule(framework);
|
configureSystemFontFamilyModule(framework);
|
||||||
configureEditorSettingModule(framework);
|
configureEditorSettingModule(framework);
|
||||||
|
configureImportTemplateModule(framework);
|
||||||
|
configureCreateWorkspaceModule(framework);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,10 @@ export const useCustomTheme = (target: HTMLElement) => {
|
|||||||
const valueMap = themeObj[mode];
|
const valueMap = themeObj[mode];
|
||||||
|
|
||||||
// remove previous style
|
// remove previous style
|
||||||
|
// TOOD(@CatsJuice): find better way to remove previous style
|
||||||
target.style.cssText = '';
|
target.style.cssText = '';
|
||||||
|
// recover color scheme set by next-themes
|
||||||
|
target.style.colorScheme = mode;
|
||||||
|
|
||||||
Object.entries(valueMap).forEach(([key, value]) => {
|
Object.entries(valueMap).forEach(([key, value]) => {
|
||||||
value && target.style.setProperty(key, value);
|
value && target.style.setProperty(key, value);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { RightSidebarIcon } from '@blocksuite/icons/rc';
|
|||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import { Suspense, useCallback } from 'react';
|
import { Suspense, useCallback } from 'react';
|
||||||
|
import { Outlet } from 'react-router-dom';
|
||||||
|
|
||||||
import { AffineErrorBoundary } from '../../../components/affine/affine-error-boundary';
|
import { AffineErrorBoundary } from '../../../components/affine/affine-error-boundary';
|
||||||
import { appSidebarOpenAtom } from '../../../components/app-sidebar/index.jotai';
|
import { appSidebarOpenAtom } from '../../../components/app-sidebar/index.jotai';
|
||||||
@@ -40,7 +41,7 @@ const ToggleButton = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const RouteContainer = ({ route }: Props) => {
|
export const RouteContainer = () => {
|
||||||
const viewPosition = useViewPosition();
|
const viewPosition = useViewPosition();
|
||||||
const leftSidebarOpen = useAtomValue(appSidebarOpenAtom);
|
const leftSidebarOpen = useAtomValue(appSidebarOpenAtom);
|
||||||
const workbench = useService(WorkbenchService).workbench;
|
const workbench = useService(WorkbenchService).workbench;
|
||||||
@@ -74,7 +75,7 @@ export const RouteContainer = ({ route }: Props) => {
|
|||||||
|
|
||||||
<AffineErrorBoundary>
|
<AffineErrorBoundary>
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<route.Component />
|
<Outlet />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</AffineErrorBoundary>
|
</AffineErrorBoundary>
|
||||||
<ViewBodyTarget viewId={view.id} className={styles.viewBodyContainer} />
|
<ViewBodyTarget viewId={view.id} className={styles.viewBodyContainer} />
|
||||||
|
|||||||
@@ -8,15 +8,8 @@ import {
|
|||||||
useService,
|
useService,
|
||||||
} from '@toeverything/infra';
|
} from '@toeverything/infra';
|
||||||
import { useAtom, useAtomValue } from 'jotai';
|
import { useAtom, useAtomValue } from 'jotai';
|
||||||
import {
|
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
lazy as reactLazy,
|
import { type RouteObject, useLocation } from 'react-router-dom';
|
||||||
memo,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
import { useLocation } from 'react-router-dom';
|
|
||||||
|
|
||||||
import type { View } from '../entities/view';
|
import type { View } from '../entities/view';
|
||||||
import { WorkbenchService } from '../services/workbench';
|
import { WorkbenchService } from '../services/workbench';
|
||||||
@@ -33,24 +26,6 @@ const useAdapter = environment.isDesktop
|
|||||||
? useBindWorkbenchToDesktopRouter
|
? useBindWorkbenchToDesktopRouter
|
||||||
: useBindWorkbenchToBrowserRouter;
|
: useBindWorkbenchToBrowserRouter;
|
||||||
|
|
||||||
const warpedRoutes = viewRoutes.map(({ path, lazy }) => {
|
|
||||||
const Component = reactLazy(() =>
|
|
||||||
lazy().then(m => ({
|
|
||||||
default: m.Component as React.ComponentType,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
const route = {
|
|
||||||
Component,
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
path,
|
|
||||||
Component: () => {
|
|
||||||
return <RouteContainer route={route} />;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
export const WorkbenchRoot = memo(() => {
|
export const WorkbenchRoot = memo(() => {
|
||||||
const workbench = useService(WorkbenchService).workbench;
|
const workbench = useService(WorkbenchService).workbench;
|
||||||
|
|
||||||
@@ -118,9 +93,18 @@ const WorkbenchView = ({ view, index }: { view: View; index: number }) => {
|
|||||||
return;
|
return;
|
||||||
}, [handleOnFocus]);
|
}, [handleOnFocus]);
|
||||||
|
|
||||||
|
const routes: RouteObject[] = useMemo(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
element: <RouteContainer />,
|
||||||
|
children: viewRoutes,
|
||||||
|
},
|
||||||
|
] satisfies RouteObject[];
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.workbenchViewContainer} ref={containerRef}>
|
<div className={styles.workbenchViewContainer} ref={containerRef}>
|
||||||
<ViewRoot routes={warpedRoutes} key={view.id} view={view} />
|
<ViewRoot routes={routes} key={view.id} view={view} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
21
packages/frontend/core/src/pages/import-template.tsx
Normal file
21
packages/frontend/core/src/pages/import-template.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { useService } from '@toeverything/infra';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||||
|
import { ImportTemplateDialogService } from '../modules/import-template';
|
||||||
|
|
||||||
|
export const Component = () => {
|
||||||
|
const importTemplateDialogService = useService(ImportTemplateDialogService);
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const { jumpToIndex } = useNavigateHelper();
|
||||||
|
useEffect(() => {
|
||||||
|
importTemplateDialogService.dialog.open(
|
||||||
|
searchParams.get('workspaceId') ?? '',
|
||||||
|
searchParams.get('docId') ?? '',
|
||||||
|
searchParams.get('name') ?? ''
|
||||||
|
);
|
||||||
|
}, [importTemplateDialogService.dialog, jumpToIndex, searchParams]);
|
||||||
|
// no ui for this route, just open the dialog
|
||||||
|
return null;
|
||||||
|
};
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { Menu } from '@affine/component/ui/menu';
|
|
||||||
import { apis } from '@affine/electron-api';
|
import { apis } from '@affine/electron-api';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import {
|
import {
|
||||||
@@ -7,7 +6,6 @@ import {
|
|||||||
WorkspacesService,
|
WorkspacesService,
|
||||||
} from '@toeverything/infra';
|
} from '@toeverything/infra';
|
||||||
import {
|
import {
|
||||||
lazy,
|
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useLayoutEffect,
|
useLayoutEffect,
|
||||||
@@ -21,17 +19,11 @@ import {
|
|||||||
createFirstAppData,
|
createFirstAppData,
|
||||||
} from '../bootstrap/first-app-data';
|
} from '../bootstrap/first-app-data';
|
||||||
import { AppFallback } from '../components/affine/app-container';
|
import { AppFallback } from '../components/affine/app-container';
|
||||||
import { UserWithWorkspaceList } from '../components/pure/workspace-slider-bar/user-with-workspace-list';
|
import { WorkspaceNavigator } from '../components/workspace-selector';
|
||||||
import { useNavigateHelper } from '../hooks/use-navigate-helper';
|
import { useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||||
import { AuthService } from '../modules/cloud';
|
import { AuthService } from '../modules/cloud';
|
||||||
import { WorkspaceSubPath } from '../shared';
|
import { WorkspaceSubPath } from '../shared';
|
||||||
|
|
||||||
const AllWorkspaceModals = lazy(() =>
|
|
||||||
import('../providers/modal-provider').then(({ AllWorkspaceModals }) => ({
|
|
||||||
default: AllWorkspaceModals,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
|
|
||||||
export const loader: LoaderFunction = async () => {
|
export const loader: LoaderFunction = async () => {
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
@@ -41,6 +33,7 @@ export const Component = () => {
|
|||||||
const [navigating, setNavigating] = useState(true);
|
const [navigating, setNavigating] = useState(true);
|
||||||
const [creating, setCreating] = useState(false);
|
const [creating, setCreating] = useState(false);
|
||||||
const authService = useService(AuthService);
|
const authService = useService(AuthService);
|
||||||
|
|
||||||
const loggedIn = useLiveData(
|
const loggedIn = useLiveData(
|
||||||
authService.session.status$.map(s => s === 'authenticated')
|
authService.session.status$.map(s => s === 'authenticated')
|
||||||
);
|
);
|
||||||
@@ -151,35 +144,19 @@ export const Component = () => {
|
|||||||
|
|
||||||
// TODO(@eyhn): We need a no workspace page
|
// TODO(@eyhn): We need a no workspace page
|
||||||
return (
|
return (
|
||||||
<>
|
<div
|
||||||
<div
|
style={{
|
||||||
style={{
|
position: 'fixed',
|
||||||
position: 'fixed',
|
left: 'calc(50% - 150px)',
|
||||||
left: '50%',
|
top: '50%',
|
||||||
top: '50%',
|
}}
|
||||||
|
>
|
||||||
|
<WorkspaceNavigator
|
||||||
|
open={true}
|
||||||
|
menuContentOptions={{
|
||||||
|
forceMount: true,
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
<Menu
|
</div>
|
||||||
rootOptions={{
|
|
||||||
open: true,
|
|
||||||
}}
|
|
||||||
items={<UserWithWorkspaceList />}
|
|
||||||
noPortal
|
|
||||||
contentOptions={{
|
|
||||||
style: {
|
|
||||||
width: 300,
|
|
||||||
transform: 'translate(-50%, -50%)',
|
|
||||||
borderRadius: '8px',
|
|
||||||
boxShadow: 'var(--affine-shadow-2)',
|
|
||||||
backgroundColor: 'var(--affine-background-overlay-panel-color)',
|
|
||||||
padding: '16px 12px',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div></div>
|
|
||||||
</Menu>
|
|
||||||
</div>
|
|
||||||
<AllWorkspaceModals />
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
12
packages/frontend/core/src/pages/root.tsx
Normal file
12
packages/frontend/core/src/pages/root.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { Outlet } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { AllWorkspaceModals } from '../providers/modal-provider';
|
||||||
|
|
||||||
|
export const RootWrapper = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AllWorkspaceModals />
|
||||||
|
<Outlet />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -16,7 +16,6 @@ import { matchPath, useLocation, useParams } from 'react-router-dom';
|
|||||||
import { AffineErrorBoundary } from '../../components/affine/affine-error-boundary';
|
import { AffineErrorBoundary } from '../../components/affine/affine-error-boundary';
|
||||||
import { WorkspaceLayout } from '../../layouts/workspace-layout';
|
import { WorkspaceLayout } from '../../layouts/workspace-layout';
|
||||||
import { WorkbenchRoot } from '../../modules/workbench';
|
import { WorkbenchRoot } from '../../modules/workbench';
|
||||||
import { AllWorkspaceModals } from '../../providers/modal-provider';
|
|
||||||
import { performanceRenderLogger } from '../../shared';
|
import { performanceRenderLogger } from '../../shared';
|
||||||
import { PageNotFound } from '../404';
|
import { PageNotFound } from '../404';
|
||||||
import { SharePage } from './share/share-page';
|
import { SharePage } from './share/share-page';
|
||||||
@@ -206,7 +205,6 @@ const WorkspacePage = ({ meta }: { meta: WorkspaceMetadata }) => {
|
|||||||
return (
|
return (
|
||||||
<FrameworkScope scope={workspace.scope}>
|
<FrameworkScope scope={workspace.scope}>
|
||||||
<AppFallback key="workspaceLoading" />
|
<AppFallback key="workspaceLoading" />
|
||||||
<AllWorkspaceModals />
|
|
||||||
</FrameworkScope>
|
</FrameworkScope>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { notify } from '@affine/component';
|
import { NotificationCenter, notify } from '@affine/component';
|
||||||
import { events } from '@affine/electron-api';
|
import { events } from '@affine/electron-api';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import {
|
import {
|
||||||
|
GlobalContextService,
|
||||||
useLiveData,
|
useLiveData,
|
||||||
useService,
|
useService,
|
||||||
useServiceOptional,
|
|
||||||
WorkspaceService,
|
WorkspaceService,
|
||||||
WorkspacesService,
|
WorkspacesService,
|
||||||
} from '@toeverything/infra';
|
} from '@toeverything/infra';
|
||||||
@@ -13,15 +13,9 @@ import type { ReactElement } from 'react';
|
|||||||
import { useCallback, useEffect } from 'react';
|
import { useCallback, useEffect } from 'react';
|
||||||
|
|
||||||
import type { SettingAtom } from '../atoms';
|
import type { SettingAtom } from '../atoms';
|
||||||
import {
|
import { authAtom, openSettingModalAtom, openSignOutModalAtom } from '../atoms';
|
||||||
authAtom,
|
|
||||||
openCreateWorkspaceModalAtom,
|
|
||||||
openSettingModalAtom,
|
|
||||||
openSignOutModalAtom,
|
|
||||||
} from '../atoms';
|
|
||||||
import { AuthModal as Auth } from '../components/affine/auth';
|
import { AuthModal as Auth } from '../components/affine/auth';
|
||||||
import { AiLoginRequiredModal } from '../components/affine/auth/ai-login-required';
|
import { AiLoginRequiredModal } from '../components/affine/auth/ai-login-required';
|
||||||
import { CreateWorkspaceModal } from '../components/affine/create-workspace-modal';
|
|
||||||
import { HistoryTipsModal } from '../components/affine/history-tips-modal';
|
import { HistoryTipsModal } from '../components/affine/history-tips-modal';
|
||||||
import { IssueFeedbackModal } from '../components/affine/issue-feedback-modal';
|
import { IssueFeedbackModal } from '../components/affine/issue-feedback-modal';
|
||||||
import {
|
import {
|
||||||
@@ -36,7 +30,9 @@ import { useTrashModalHelper } from '../hooks/affine/use-trash-modal-helper';
|
|||||||
import { useAsyncCallback } from '../hooks/affine-async-hooks';
|
import { useAsyncCallback } from '../hooks/affine-async-hooks';
|
||||||
import { useNavigateHelper } from '../hooks/use-navigate-helper';
|
import { useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||||
import { AuthService } from '../modules/cloud/services/auth';
|
import { AuthService } from '../modules/cloud/services/auth';
|
||||||
|
import { CreateWorkspaceDialogProvider } from '../modules/create-workspace';
|
||||||
import { FindInPageModal } from '../modules/find-in-page/view/find-in-page-modal';
|
import { FindInPageModal } from '../modules/find-in-page/view/find-in-page-modal';
|
||||||
|
import { ImportTemplateDialogProvider } from '../modules/import-template';
|
||||||
import { PeekViewManagerModal } from '../modules/peek-view';
|
import { PeekViewManagerModal } from '../modules/peek-view';
|
||||||
import { WorkspaceSubPath } from '../shared';
|
import { WorkspaceSubPath } from '../shared';
|
||||||
|
|
||||||
@@ -181,9 +177,16 @@ export const SignOutConfirmModal = () => {
|
|||||||
const { openPage } = useNavigateHelper();
|
const { openPage } = useNavigateHelper();
|
||||||
const authService = useService(AuthService);
|
const authService = useService(AuthService);
|
||||||
const [open, setOpen] = useAtom(openSignOutModalAtom);
|
const [open, setOpen] = useAtom(openSignOutModalAtom);
|
||||||
const currentWorkspace = useServiceOptional(WorkspaceService)?.workspace;
|
const globalContextService = useService(GlobalContextService);
|
||||||
const workspaces = useLiveData(
|
const currentWorkspaceId = useLiveData(
|
||||||
useService(WorkspacesService).list.workspaces$
|
globalContextService.globalContext.workspaceId.$
|
||||||
|
);
|
||||||
|
const workspacesService = useService(WorkspacesService);
|
||||||
|
const workspaces = useLiveData(workspacesService.list.workspaces$);
|
||||||
|
const currentWorkspaceMetadata = useLiveData(
|
||||||
|
currentWorkspaceId
|
||||||
|
? workspacesService.list.workspace$(currentWorkspaceId)
|
||||||
|
: undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
const onConfirm = useAsyncCallback(async () => {
|
const onConfirm = useAsyncCallback(async () => {
|
||||||
@@ -199,7 +202,7 @@ export const SignOutConfirmModal = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// if current workspace is affine cloud, switch to local workspace
|
// if current workspace is affine cloud, switch to local workspace
|
||||||
if (currentWorkspace?.flavour === WorkspaceFlavour.AFFINE_CLOUD) {
|
if (currentWorkspaceMetadata?.flavour === WorkspaceFlavour.AFFINE_CLOUD) {
|
||||||
const localWorkspace = workspaces.find(
|
const localWorkspace = workspaces.find(
|
||||||
w => w.flavour === WorkspaceFlavour.LOCAL
|
w => w.flavour === WorkspaceFlavour.LOCAL
|
||||||
);
|
);
|
||||||
@@ -207,7 +210,13 @@ export const SignOutConfirmModal = () => {
|
|||||||
openPage(localWorkspace.id, WorkspaceSubPath.ALL);
|
openPage(localWorkspace.id, WorkspaceSubPath.ALL);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [authService, currentWorkspace, openPage, setOpen, workspaces]);
|
}, [
|
||||||
|
authService,
|
||||||
|
currentWorkspaceMetadata?.flavour,
|
||||||
|
openPage,
|
||||||
|
setOpen,
|
||||||
|
workspaces,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SignOutModal open={open} onOpenChange={setOpen} onConfirm={onConfirm} />
|
<SignOutModal open={open} onOpenChange={setOpen} onConfirm={onConfirm} />
|
||||||
@@ -215,35 +224,11 @@ export const SignOutConfirmModal = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const AllWorkspaceModals = (): ReactElement => {
|
export const AllWorkspaceModals = (): ReactElement => {
|
||||||
const [isOpenCreateWorkspaceModal, setOpenCreateWorkspaceModal] = useAtom(
|
|
||||||
openCreateWorkspaceModalAtom
|
|
||||||
);
|
|
||||||
|
|
||||||
const { jumpToSubPath, jumpToPage } = useNavigateHelper();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CreateWorkspaceModal
|
<NotificationCenter />
|
||||||
mode={isOpenCreateWorkspaceModal}
|
<ImportTemplateDialogProvider />
|
||||||
onClose={useCallback(() => {
|
<CreateWorkspaceDialogProvider />
|
||||||
setOpenCreateWorkspaceModal(false);
|
|
||||||
}, [setOpenCreateWorkspaceModal])}
|
|
||||||
onCreate={useCallback(
|
|
||||||
(id, defaultDocId) => {
|
|
||||||
setOpenCreateWorkspaceModal(false);
|
|
||||||
// if jumping immediately, the page may stuck in loading state
|
|
||||||
// not sure why yet .. here is a workaround
|
|
||||||
setTimeout(() => {
|
|
||||||
if (!defaultDocId) {
|
|
||||||
jumpToSubPath(id, WorkspaceSubPath.ALL);
|
|
||||||
} else {
|
|
||||||
jumpToPage(id, defaultDocId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[jumpToPage, jumpToSubPath, setOpenCreateWorkspaceModal]
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<AuthModal />
|
<AuthModal />
|
||||||
<SignOutConfirmModal />
|
<SignOutConfirmModal />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { wrapCreateBrowserRouter } from '@sentry/react';
|
import { wrapCreateBrowserRouter } from '@sentry/react';
|
||||||
import { createContext, useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import type { NavigateFunction, RouteObject } from 'react-router-dom';
|
import type { RouteObject } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
createBrowserRouter as reactRouterCreateBrowserRouter,
|
createBrowserRouter as reactRouterCreateBrowserRouter,
|
||||||
Outlet,
|
|
||||||
redirect,
|
redirect,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||||
useNavigate,
|
useNavigate,
|
||||||
} from 'react-router-dom';
|
} from 'react-router-dom';
|
||||||
|
|
||||||
export const NavigateContext = createContext<NavigateFunction | null>(null);
|
import { NavigateContext } from './hooks/use-navigate-helper';
|
||||||
|
import { RootWrapper } from './pages/root';
|
||||||
|
|
||||||
export function RootRouter() {
|
export function RootRouter() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -22,7 +22,7 @@ export function RootRouter() {
|
|||||||
return (
|
return (
|
||||||
ready && (
|
ready && (
|
||||||
<NavigateContext.Provider value={navigate}>
|
<NavigateContext.Provider value={navigate}>
|
||||||
<Outlet />
|
<RootWrapper />
|
||||||
</NavigateContext.Provider>
|
</NavigateContext.Provider>
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -114,6 +114,10 @@ export const topLevelRoutes = [
|
|||||||
path: '/theme-editor',
|
path: '/theme-editor',
|
||||||
lazy: () => import('./pages/theme-editor'),
|
lazy: () => import('./pages/theme-editor'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/template/import',
|
||||||
|
lazy: () => import('./pages/import-template'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '*',
|
path: '*',
|
||||||
lazy: () => import('./pages/404'),
|
lazy: () => import('./pages/404'),
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import '@affine/component/theme/global.css';
|
import '@affine/component/theme/global.css';
|
||||||
import '@affine/component/theme/theme.css';
|
import '@affine/component/theme/theme.css';
|
||||||
|
|
||||||
import { NotificationCenter } from '@affine/component';
|
|
||||||
import { AffineContext } from '@affine/component/context';
|
import { AffineContext } from '@affine/component/context';
|
||||||
import { GlobalLoading } from '@affine/component/global-loading';
|
import { GlobalLoading } from '@affine/component/global-loading';
|
||||||
import { AppFallback } from '@affine/core/components/affine/app-container';
|
import { AppFallback } from '@affine/core/components/affine/app-container';
|
||||||
@@ -114,7 +113,6 @@ export function App() {
|
|||||||
<CustomThemeModifier />
|
<CustomThemeModifier />
|
||||||
<DebugProvider>
|
<DebugProvider>
|
||||||
<GlobalLoading />
|
<GlobalLoading />
|
||||||
<NotificationCenter />
|
|
||||||
<RouterProvider
|
<RouterProvider
|
||||||
fallbackElement={<AppFallback key="RouterFallback" />}
|
fallbackElement={<AppFallback key="RouterFallback" />}
|
||||||
router={router}
|
router={router}
|
||||||
|
|||||||
@@ -1551,5 +1551,10 @@
|
|||||||
"will be moved to Trash": "{{title}} will be moved to trash",
|
"will be moved to Trash": "{{title}} will be moved to trash",
|
||||||
"will delete member": "will delete member",
|
"will delete member": "will delete member",
|
||||||
"com.affine.app-sidebar.star-us": "Star us",
|
"com.affine.app-sidebar.star-us": "Star us",
|
||||||
"com.affine.app-sidebar.learn-more": "Learn more"
|
"com.affine.app-sidebar.learn-more": "Learn more",
|
||||||
|
"com.affine.import-template.dialog.errorImport": "Failed to import template, please try again.",
|
||||||
|
"com.affine.import-template.dialog.errorLoad": "Failed to load template, please try again.",
|
||||||
|
"com.affine.import-template.dialog.createDocToWorkspace": "Create doc to \"{{workspace}}\"",
|
||||||
|
"com.affine.import-template.dialog.createDocToNewWorkspace": "Create into a New Workspace",
|
||||||
|
"com.affine.import-template.dialog.createDocWithTemplate": "Create doc with \"{{templateName}}\" template"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import '@affine/component/theme/global.css';
|
import '@affine/component/theme/global.css';
|
||||||
import '@affine/component/theme/theme.css';
|
import '@affine/component/theme/theme.css';
|
||||||
|
|
||||||
import { NotificationCenter } from '@affine/component';
|
|
||||||
import { AffineContext } from '@affine/component/context';
|
import { AffineContext } from '@affine/component/context';
|
||||||
import { AppFallback } from '@affine/core/components/affine/app-container';
|
import { AppFallback } from '@affine/core/components/affine/app-container';
|
||||||
import { configureCommonModules } from '@affine/core/modules';
|
import { configureCommonModules } from '@affine/core/modules';
|
||||||
@@ -78,7 +77,6 @@ export function App() {
|
|||||||
<FrameworkRoot framework={frameworkProvider}>
|
<FrameworkRoot framework={frameworkProvider}>
|
||||||
<AffineContext store={getCurrentStore()}>
|
<AffineContext store={getCurrentStore()}>
|
||||||
<Telemetry />
|
<Telemetry />
|
||||||
<NotificationCenter />
|
|
||||||
<RouterProvider
|
<RouterProvider
|
||||||
fallbackElement={<AppFallback key="RouterFallback" />}
|
fallbackElement={<AppFallback key="RouterFallback" />}
|
||||||
router={router}
|
router={router}
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import { AffineErrorBoundary } from '@affine/core/components/affine/affine-error-boundary';
|
import { AffineErrorBoundary } from '@affine/core/components/affine/affine-error-boundary';
|
||||||
import { AppFallback } from '@affine/core/components/affine/app-container';
|
import { AppFallback } from '@affine/core/components/affine/app-container';
|
||||||
import { WorkspaceLayoutProviders } from '@affine/core/layouts/workspace-layout';
|
import { WorkspaceLayoutProviders } from '@affine/core/layouts/workspace-layout';
|
||||||
import {
|
import { CurrentWorkspaceModals } from '@affine/core/providers/modal-provider';
|
||||||
AllWorkspaceModals,
|
|
||||||
CurrentWorkspaceModals,
|
|
||||||
} from '@affine/core/providers/modal-provider';
|
|
||||||
import { SWRConfigProvider } from '@affine/core/providers/swr-config-provider';
|
import { SWRConfigProvider } from '@affine/core/providers/swr-config-provider';
|
||||||
import type { Workspace, WorkspaceMetadata } from '@toeverything/infra';
|
import type { Workspace, WorkspaceMetadata } from '@toeverything/infra';
|
||||||
import {
|
import {
|
||||||
@@ -73,7 +70,6 @@ export const WorkspaceLayout = ({
|
|||||||
return (
|
return (
|
||||||
<FrameworkScope scope={workspace.scope}>
|
<FrameworkScope scope={workspace.scope}>
|
||||||
<AppFallback key="workspaceLoading" />
|
<AppFallback key="workspaceLoading" />
|
||||||
<AllWorkspaceModals />
|
|
||||||
</FrameworkScope>
|
</FrameworkScope>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -82,7 +78,6 @@ export const WorkspaceLayout = ({
|
|||||||
<FrameworkScope scope={workspace.scope}>
|
<FrameworkScope scope={workspace.scope}>
|
||||||
<AffineErrorBoundary height="100vh">
|
<AffineErrorBoundary height="100vh">
|
||||||
<SWRConfigProvider>
|
<SWRConfigProvider>
|
||||||
<AllWorkspaceModals />
|
|
||||||
<CurrentWorkspaceModals />
|
<CurrentWorkspaceModals />
|
||||||
<WorkspaceLayoutProviders>{children}</WorkspaceLayoutProviders>
|
<WorkspaceLayoutProviders>{children}</WorkspaceLayoutProviders>
|
||||||
</SWRConfigProvider>
|
</SWRConfigProvider>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import '@affine/component/theme/global.css';
|
import '@affine/component/theme/global.css';
|
||||||
import '@affine/component/theme/theme.css';
|
import '@affine/component/theme/theme.css';
|
||||||
|
|
||||||
import { NotificationCenter } from '@affine/component';
|
|
||||||
import { AffineContext } from '@affine/component/context';
|
import { AffineContext } from '@affine/component/context';
|
||||||
import { GlobalLoading } from '@affine/component/global-loading';
|
import { GlobalLoading } from '@affine/component/global-loading';
|
||||||
import { AppFallback } from '@affine/core/components/affine/app-container';
|
import { AppFallback } from '@affine/core/components/affine/app-container';
|
||||||
@@ -100,7 +99,6 @@ export function App() {
|
|||||||
<CustomThemeModifier />
|
<CustomThemeModifier />
|
||||||
<DebugProvider>
|
<DebugProvider>
|
||||||
<GlobalLoading />
|
<GlobalLoading />
|
||||||
<NotificationCenter />
|
|
||||||
<RouterProvider
|
<RouterProvider
|
||||||
fallbackElement={<AppFallback key="RouterFallback" />}
|
fallbackElement={<AppFallback key="RouterFallback" />}
|
||||||
router={router}
|
router={router}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ test('Create new workspace, then delete it', async ({ page, workspace }) => {
|
|||||||
await waitForEditorLoad(page);
|
await waitForEditorLoad(page);
|
||||||
await clickSideBarCurrentWorkspaceBanner(page);
|
await clickSideBarCurrentWorkspaceBanner(page);
|
||||||
await page.getByTestId('new-workspace').click();
|
await page.getByTestId('new-workspace').click();
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
await page
|
await page
|
||||||
.getByTestId('create-workspace-input')
|
.getByTestId('create-workspace-input')
|
||||||
.pressSequentially('Test Workspace', { delay: 50 });
|
.pressSequentially('Test Workspace', { delay: 50 });
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ test.skip('create multi workspace in the workspace list', async ({
|
|||||||
|
|
||||||
await page.reload();
|
await page.reload();
|
||||||
await openWorkspaceListModal(page);
|
await openWorkspaceListModal(page);
|
||||||
await page.getByTestId('draggable-item').nth(1).click();
|
await page.getByTestId('workspace-card').nth(1).click();
|
||||||
await page.waitForTimeout(500);
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
const currentWorkspace = await workspace.current();
|
const currentWorkspace = await workspace.current();
|
||||||
@@ -92,8 +92,8 @@ test.skip('create multi workspace in the workspace list', async ({
|
|||||||
|
|
||||||
await openWorkspaceListModal(page);
|
await openWorkspaceListModal(page);
|
||||||
await page.waitForTimeout(1000);
|
await page.waitForTimeout(1000);
|
||||||
const sourceElement = page.getByTestId('draggable-item').nth(2);
|
const sourceElement = page.getByTestId('workspace-card').nth(2);
|
||||||
const targetElement = page.getByTestId('draggable-item').nth(1);
|
const targetElement = page.getByTestId('workspace-card').nth(1);
|
||||||
|
|
||||||
const sourceBox = await sourceElement.boundingBox();
|
const sourceBox = await sourceElement.boundingBox();
|
||||||
const targetBox = await targetElement.boundingBox();
|
const targetBox = await targetElement.boundingBox();
|
||||||
|
|||||||
@@ -173,7 +173,11 @@ export const selectVisibilitySelector = async (
|
|||||||
})
|
})
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
await page.keyboard.press('Escape');
|
await page.keyboard.press('Escape');
|
||||||
|
|
||||||
|
await page.waitForTimeout(500);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const changePropertyVisibility = async (
|
export const changePropertyVisibility = async (
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export async function clickSideBarAllPageButton(page: Page) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function clickSideBarCurrentWorkspaceBanner(page: Page) {
|
export async function clickSideBarCurrentWorkspaceBanner(page: Page) {
|
||||||
return page.getByTestId('current-workspace').click();
|
return page.getByTestId('current-workspace-card').click();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function clickSideBarUseAvatar(page: Page) {
|
export async function clickSideBarUseAvatar(page: Page) {
|
||||||
|
|||||||
Reference in New Issue
Block a user