mirror of
https://github.com/toeverything/AFFiNE.git
synced 2025-01-02 14:53:31 +03:00
feat(core): import template (#8000)
This commit is contained in:
parent
4ec45a247e
commit
b96ad57568
@ -17,6 +17,12 @@ export class WorkspaceList extends Entity {
|
||||
.flat()
|
||||
.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[]) {
|
||||
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 clsx from 'clsx';
|
||||
import { Fragment } from 'react';
|
||||
|
||||
import type { MenuProps } from '../menu.types';
|
||||
import * as styles from '../styles.css';
|
||||
import * as desktopStyles from './styles.css';
|
||||
|
||||
export const DesktopMenu = ({
|
||||
children,
|
||||
items,
|
||||
portalOptions,
|
||||
rootOptions,
|
||||
noPortal,
|
||||
contentOptions: {
|
||||
className = '',
|
||||
style: contentStyle = {},
|
||||
...otherContentOptions
|
||||
} = {},
|
||||
}: MenuProps) => {
|
||||
const Wrapper = noPortal ? Fragment : DropdownMenu.Portal;
|
||||
const wrapperProps = noPortal ? {} : portalOptions;
|
||||
return (
|
||||
<DropdownMenu.Root {...rootOptions}>
|
||||
<DropdownMenu.Trigger asChild>{children}</DropdownMenu.Trigger>
|
||||
|
||||
<Wrapper {...wrapperProps}>
|
||||
<DropdownMenu.Portal {...portalOptions}>
|
||||
<DropdownMenu.Content
|
||||
className={clsx(styles.menuContent, className)}
|
||||
className={clsx(
|
||||
styles.menuContent,
|
||||
desktopStyles.contentAnimation,
|
||||
className
|
||||
)}
|
||||
sideOffset={5}
|
||||
align="start"
|
||||
style={{ zIndex: 'var(--affine-z-index-popover)', ...contentStyle }}
|
||||
{...otherContentOptions}
|
||||
side="bottom"
|
||||
>
|
||||
{items}
|
||||
</DropdownMenu.Content>
|
||||
</Wrapper>
|
||||
</DropdownMenu.Portal>
|
||||
</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 { Slot } from '@radix-ui/react-slot';
|
||||
import clsx from 'clsx';
|
||||
import {
|
||||
Fragment,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useCallback, useContext, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { observeResize } from '../../../utils';
|
||||
import { Button } from '../../button';
|
||||
import { Modal, type ModalProps } from '../../modal';
|
||||
import { Modal } from '../../modal';
|
||||
import type { MenuProps } from '../menu.types';
|
||||
import type { SubMenuContent } from './context';
|
||||
import { MobileMenuContext } from './context';
|
||||
@ -22,7 +15,6 @@ import { MobileMenuSubRaw } from './sub';
|
||||
export const MobileMenu = ({
|
||||
children,
|
||||
items,
|
||||
noPortal,
|
||||
contentOptions: {
|
||||
className,
|
||||
onPointerDownOutside,
|
||||
@ -56,25 +48,6 @@ export const MobileMenu = ({
|
||||
[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) => {
|
||||
e.preventDefault();
|
||||
setOpen(prev => !prev);
|
||||
@ -127,7 +100,21 @@ export const MobileMenu = ({
|
||||
<MobileMenuContext.Provider
|
||||
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
|
||||
ref={sliderRef}
|
||||
className={styles.slider}
|
||||
@ -159,7 +146,7 @@ export const MobileMenu = ({
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Wrapper>
|
||||
</Modal>
|
||||
</MobileMenuContext.Provider>
|
||||
</>
|
||||
);
|
||||
|
@ -153,14 +153,19 @@ export const ModalInner = forwardRef<HTMLDivElement, ModalProps>(
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const container = createContainer();
|
||||
setContainer(container);
|
||||
return () => {
|
||||
setTimeout(() => {
|
||||
container.remove();
|
||||
}, 1000) as unknown as number;
|
||||
};
|
||||
}, []);
|
||||
if (open) {
|
||||
const container = createContainer();
|
||||
setContainer(container);
|
||||
return () => {
|
||||
setTimeout(() => {
|
||||
container.remove();
|
||||
}, 1000) as unknown as number;
|
||||
};
|
||||
} else {
|
||||
setContainer(null);
|
||||
return;
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
const handlePointerDownOutSide = useCallback(
|
||||
(e: PointerDownOutsideEvent) => {
|
||||
|
@ -1,12 +1,10 @@
|
||||
import { atom } from 'jotai';
|
||||
|
||||
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 { ActiveTab } from '../components/affine/setting-modal/types';
|
||||
// modal atoms
|
||||
export const openWorkspacesModalAtom = atom(false);
|
||||
export const openCreateWorkspaceModalAtom = atom<CreateWorkspaceMode>(false);
|
||||
export const openSignOutModalAtom = atom(false);
|
||||
export const openQuotaModalAtom = atom(false);
|
||||
export const openStarAFFiNEModalAtom = atom(false);
|
||||
|
@ -1,20 +1,19 @@
|
||||
import type { useI18n } from '@affine/i18n';
|
||||
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 { track } from '../mixpanel';
|
||||
import type { CreateWorkspaceDialogService } from '../modules/create-workspace';
|
||||
import { registerAffineCommand } from './registry';
|
||||
|
||||
export function registerAffineCreationCommands({
|
||||
store,
|
||||
pageHelper,
|
||||
t,
|
||||
createWorkspaceDialogService,
|
||||
}: {
|
||||
t: ReturnType<typeof useI18n>;
|
||||
store: ReturnType<typeof createStore>;
|
||||
pageHelper: ReturnType<typeof usePageHelper>;
|
||||
createWorkspaceDialogService: CreateWorkspaceDialogService;
|
||||
}) {
|
||||
const unsubs: Array<() => void> = [];
|
||||
unsubs.push(
|
||||
@ -62,7 +61,7 @@ export function registerAffineCreationCommands({
|
||||
run() {
|
||||
track.$.cmdk.workspace.createWorkspace();
|
||||
|
||||
store.set(openCreateWorkspaceModalAtom, 'new');
|
||||
createWorkspaceDialogService.dialog.open('new');
|
||||
},
|
||||
})
|
||||
);
|
||||
@ -80,7 +79,7 @@ export function registerAffineCreationCommands({
|
||||
control: 'import',
|
||||
});
|
||||
|
||||
store.set(openCreateWorkspaceModalAtom, 'add');
|
||||
createWorkspaceDialogService.dialog.open('add');
|
||||
},
|
||||
})
|
||||
);
|
||||
|
@ -25,8 +25,9 @@ export const inlineTagsContainer = style({
|
||||
|
||||
export const tagsMenu = style({
|
||||
padding: 0,
|
||||
transform:
|
||||
'translate(-3.5px, calc(-3.5px + var(--radix-popper-anchor-height) * -1))',
|
||||
position: 'relative',
|
||||
top: 'calc(-3.5px + var(--radix-popper-anchor-height) * -1)',
|
||||
left: '-3.5px',
|
||||
width: 'calc(var(--radix-popper-anchor-width) + 16px)',
|
||||
overflow: 'hidden',
|
||||
});
|
||||
|
@ -6,7 +6,7 @@ import { debounce } from 'lodash-es';
|
||||
import type { PropsWithChildren, ReactElement } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { WorkspaceSelector } from '../workspace-selector';
|
||||
import { WorkspaceNavigator } from '../workspace-selector';
|
||||
import { fallbackHeaderStyle, fallbackStyle } from './fallback.css';
|
||||
import {
|
||||
floatingMaxWidth,
|
||||
@ -139,7 +139,11 @@ export const AppSidebarFallback = (): ReactElement | null => {
|
||||
<div className={fallbackStyle}>
|
||||
<div className={fallbackHeaderStyle}>
|
||||
{currentWorkspace ? (
|
||||
<WorkspaceSelector />
|
||||
<WorkspaceNavigator
|
||||
showSettingsButton
|
||||
showSyncStatus
|
||||
showEnableCloudButton
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<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';
|
||||
import { ExternalMenuLinkItem } from '../app-sidebar/menu-item/external-menu-link-item';
|
||||
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 {
|
||||
quickSearch,
|
||||
@ -118,6 +118,7 @@ export const RootAppSidebar = (): ReactElement => {
|
||||
},
|
||||
[pageHelper, settings.newDocDefaultMode]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (environment.isDesktop) {
|
||||
return events?.applicationMenu.onNewPageAction(() => onClickNewPage());
|
||||
@ -143,7 +144,11 @@ export const RootAppSidebar = (): ReactElement => {
|
||||
<SidebarContainer>
|
||||
<div className={workspaceAndUserWrapper}>
|
||||
<div className={workspaceWrapper}>
|
||||
<WorkspaceSelector />
|
||||
<WorkspaceNavigator
|
||||
showEnableCloudButton
|
||||
showSettingsButton
|
||||
showSyncStatus
|
||||
/>
|
||||
</div>
|
||||
<UserInfo />
|
||||
</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 { useService, WorkspacesService } from '@toeverything/infra';
|
||||
import { useAtom } from 'jotai';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import type { CreateWorkspaceCallbackPayload } from '@affine/core/modules/create-workspace';
|
||||
import { WorkspaceSubPath } from '@affine/core/shared';
|
||||
import {
|
||||
GlobalContextService,
|
||||
useLiveData,
|
||||
useServices,
|
||||
type WorkspaceMetadata,
|
||||
WorkspacesService,
|
||||
} from '@toeverything/infra';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { openWorkspaceListModalAtom } from '../../atoms';
|
||||
import { UserWithWorkspaceList } from '../pure/workspace-slider-bar/user-with-workspace-list';
|
||||
import { WorkspaceCard } from '../pure/workspace-slider-bar/workspace-card';
|
||||
import { UserWithWorkspaceList } from './user-with-workspace-list';
|
||||
import { WorkspaceCard } from './workspace-card';
|
||||
|
||||
export const WorkspaceSelector = () => {
|
||||
const [isUserWorkspaceListOpened, setOpenUserWorkspaceList] = useAtom(
|
||||
openWorkspaceListModalAtom
|
||||
);
|
||||
interface WorkspaceSelectorProps {
|
||||
open?: boolean;
|
||||
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(() => {
|
||||
setOpenUserWorkspaceList(false);
|
||||
}, [setOpenUserWorkspaceList]);
|
||||
setOpened(false);
|
||||
}, []);
|
||||
const openUserWorkspaceList = useCallback(() => {
|
||||
track.$.navigationPanel.workspaceList.open();
|
||||
setOpenUserWorkspaceList(true);
|
||||
}, [setOpenUserWorkspaceList]);
|
||||
setOpened(true);
|
||||
}, []);
|
||||
|
||||
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
|
||||
useEffect(() => {
|
||||
if (isUserWorkspaceListOpened) {
|
||||
workspaceManager.list.revalidate();
|
||||
if (open) {
|
||||
workspacesService.list.revalidate();
|
||||
}
|
||||
}, [workspaceManager, isUserWorkspaceListOpened]);
|
||||
}, [workspacesService, open]);
|
||||
|
||||
return (
|
||||
<Menu
|
||||
rootOptions={{
|
||||
open: isUserWorkspaceListOpened,
|
||||
open,
|
||||
}}
|
||||
items={<UserWithWorkspaceList onEventEnd={closeUserWorkspaceList} />}
|
||||
items={
|
||||
<UserWithWorkspaceList
|
||||
onEventEnd={closeUserWorkspaceList}
|
||||
onClickWorkspace={onSelectWorkspace}
|
||||
onCreatedWorkspace={onCreatedWorkspace}
|
||||
showEnableCloudButton={showEnableCloudButton}
|
||||
showSettingsButton={showSettingsButton}
|
||||
/>
|
||||
}
|
||||
contentOptions={{
|
||||
// hide trigger
|
||||
sideOffset: -58,
|
||||
onInteractOutside: closeUserWorkspaceList,
|
||||
onEscapeKeyDown: closeUserWorkspaceList,
|
||||
...menuContentOptions,
|
||||
style: {
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
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 { MenuItem } from '@affine/component/ui/menu';
|
||||
import { authAtom } from '@affine/core/atoms';
|
||||
import { track } from '@affine/core/mixpanel';
|
||||
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 { Logo1Icon } from '@blocksuite/icons/rc';
|
||||
import {
|
||||
useLiveData,
|
||||
useService,
|
||||
type WorkspaceMetadata,
|
||||
WorkspacesService,
|
||||
} from '@toeverything/infra';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { Suspense, useCallback } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { authAtom, openCreateWorkspaceModalAtom } from '../../../../atoms';
|
||||
import { AddWorkspace } from './add-workspace';
|
||||
import * as styles from './index.css';
|
||||
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 {
|
||||
onEventEnd?: () => void;
|
||||
onClickWorkspace?: (workspace: WorkspaceMetadata) => void;
|
||||
onCreatedWorkspace?: (payload: CreateWorkspaceCallbackPayload) => void;
|
||||
showSettingsButton?: boolean;
|
||||
showEnableCloudButton?: boolean;
|
||||
}
|
||||
|
||||
const UserWithWorkspaceListInner = ({
|
||||
onEventEnd,
|
||||
onClickWorkspace,
|
||||
onCreatedWorkspace,
|
||||
showSettingsButton,
|
||||
showEnableCloudButton,
|
||||
}: UserWithWorkspaceListProps) => {
|
||||
const createWorkspaceDialogService = useService(CreateWorkspaceDialogService);
|
||||
const session = useLiveData(useService(AuthService).session.session$);
|
||||
|
||||
const isAuthenticated = session.status === 'authenticated';
|
||||
|
||||
const setOpenCreateWorkspaceModal = useSetAtom(openCreateWorkspaceModalAtom);
|
||||
|
||||
const setOpenSignIn = useSetAtom(authAtom);
|
||||
|
||||
const openSignInModal = useCallback(() => {
|
||||
@ -91,23 +92,31 @@ const UserWithWorkspaceListInner = ({
|
||||
return openSignInModal();
|
||||
}
|
||||
track.$.navigationPanel.workspaceList.createWorkspace();
|
||||
setOpenCreateWorkspaceModal('new');
|
||||
createWorkspaceDialogService.dialog.open('new', payload => {
|
||||
if (payload) {
|
||||
onCreatedWorkspace?.(payload);
|
||||
}
|
||||
});
|
||||
onEventEnd?.();
|
||||
}, [
|
||||
createWorkspaceDialogService.dialog,
|
||||
isAuthenticated,
|
||||
onCreatedWorkspace,
|
||||
onEventEnd,
|
||||
openSignInModal,
|
||||
setOpenCreateWorkspaceModal,
|
||||
]);
|
||||
|
||||
track.$.navigationPanel.workspaceList.createWorkspace();
|
||||
const onAddWorkspace = useCallback(() => {
|
||||
track.$.navigationPanel.workspaceList.createWorkspace({
|
||||
control: 'import',
|
||||
});
|
||||
setOpenCreateWorkspaceModal('add');
|
||||
createWorkspaceDialogService.dialog.open('add', payload => {
|
||||
if (payload) {
|
||||
onCreatedWorkspace?.(payload);
|
||||
}
|
||||
});
|
||||
onEventEnd?.();
|
||||
}, [onEventEnd, setOpenCreateWorkspaceModal]);
|
||||
}, [createWorkspaceDialogService.dialog, onCreatedWorkspace, onEventEnd]);
|
||||
|
||||
const workspaceManager = useService(WorkspacesService);
|
||||
const workspaces = useLiveData(workspaceManager.list.workspaces$);
|
||||
@ -123,7 +132,12 @@ const UserWithWorkspaceListInner = ({
|
||||
<SignInItem />
|
||||
)}
|
||||
<Divider size="thinner" />
|
||||
<AFFiNEWorkspaceList onEventEnd={onEventEnd} />
|
||||
<AFFiNEWorkspaceList
|
||||
onEventEnd={onEventEnd}
|
||||
onClickWorkspace={onClickWorkspace}
|
||||
showEnableCloudButton={showEnableCloudButton}
|
||||
showSettingsButton={showSettingsButton}
|
||||
/>
|
||||
{workspaces.length > 0 ? <Divider size="thinner" /> : null}
|
||||
<AddWorkspace
|
||||
onAddWorkspace={onAddWorkspace}
|
||||
@ -134,9 +148,5 @@ const UserWithWorkspaceListInner = ({
|
||||
};
|
||||
|
||||
export const UserWithWorkspaceList = (props: UserWithWorkspaceListProps) => {
|
||||
return (
|
||||
<Suspense fallback={<UserWithWorkspaceListLoading />}>
|
||||
<UserWithWorkspaceListInner {...props} />
|
||||
</Suspense>
|
||||
);
|
||||
return <UserWithWorkspaceListInner {...props} />;
|
||||
};
|
@ -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';
|
||||
|
||||
export const UserAccountItem = ({
|
@ -29,3 +29,7 @@ export const scrollbar = style({
|
||||
transform: 'translateX(8px)',
|
||||
width: '4px',
|
||||
});
|
||||
export const workspaceCard = style({
|
||||
height: '44px',
|
||||
padding: '0 12px',
|
||||
});
|
@ -1,18 +1,13 @@
|
||||
import { ScrollableContainer } from '@affine/component';
|
||||
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 {
|
||||
useWorkspaceInfo,
|
||||
useWorkspaceName,
|
||||
} from '@affine/core/hooks/use-workspace-info';
|
||||
import { AuthService } from '@affine/core/modules/cloud';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { CloudWorkspaceIcon, LocalWorkspaceIcon } from '@blocksuite/icons/rc';
|
||||
import type { WorkspaceMetadata } from '@toeverything/infra';
|
||||
import {
|
||||
GlobalContextService,
|
||||
useLiveData,
|
||||
useService,
|
||||
WorkspacesService,
|
||||
@ -20,39 +15,23 @@ import {
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import {
|
||||
openCreateWorkspaceModalAtom,
|
||||
openSettingModalAtom,
|
||||
} from '../../../../../atoms';
|
||||
import { WorkspaceSubPath } from '../../../../../shared';
|
||||
import { useNavigateHelper } from '../.././../../../hooks/use-navigate-helper';
|
||||
import { WorkspaceCard } from '../../workspace-card';
|
||||
import * as styles from './index.css';
|
||||
|
||||
function useIsWorkspaceOwner(meta: WorkspaceMetadata) {
|
||||
const info = useWorkspaceInfo(meta);
|
||||
|
||||
return info?.isOwner;
|
||||
}
|
||||
|
||||
interface WorkspaceModalProps {
|
||||
disabled?: boolean;
|
||||
workspaces: WorkspaceMetadata[];
|
||||
currentWorkspaceId?: string | null;
|
||||
openingId?: string | null;
|
||||
onClickWorkspace: (workspaceMetadata: WorkspaceMetadata) => void;
|
||||
onClickWorkspaceSetting: (workspaceMetadata: WorkspaceMetadata) => void;
|
||||
onClickWorkspaceSetting?: (workspaceMetadata: WorkspaceMetadata) => void;
|
||||
onClickEnableCloud?: (meta: WorkspaceMetadata) => void;
|
||||
onNewWorkspace: () => void;
|
||||
onAddWorkspace: () => void;
|
||||
}
|
||||
|
||||
const CloudWorkSpaceList = ({
|
||||
disabled,
|
||||
workspaces,
|
||||
onClickWorkspace,
|
||||
onClickWorkspaceSetting,
|
||||
currentWorkspaceId,
|
||||
}: WorkspaceModalProps) => {
|
||||
}: Omit<WorkspaceModalProps, 'onNewWorkspace' | 'onAddWorkspace'>) => {
|
||||
const t = useI18n();
|
||||
if (workspaces.length === 0) {
|
||||
return null;
|
||||
@ -68,27 +47,20 @@ const CloudWorkSpaceList = ({
|
||||
{t['com.affine.workspaceList.workspaceListType.cloud']()}
|
||||
</div>
|
||||
<WorkspaceList
|
||||
disabled={disabled}
|
||||
items={workspaces}
|
||||
currentWorkspaceId={currentWorkspaceId}
|
||||
onClick={onClickWorkspace}
|
||||
onSettingClick={onClickWorkspaceSetting}
|
||||
useIsWorkspaceOwner={useIsWorkspaceOwner}
|
||||
useWorkspaceName={useWorkspaceName}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const LocalWorkspaces = ({
|
||||
disabled,
|
||||
workspaces,
|
||||
onClickWorkspace,
|
||||
onClickWorkspaceSetting,
|
||||
onClickEnableCloud,
|
||||
openingId,
|
||||
currentWorkspaceId,
|
||||
}: WorkspaceModalProps) => {
|
||||
}: Omit<WorkspaceModalProps, 'onNewWorkspace' | 'onAddWorkspace'>) => {
|
||||
const t = useI18n();
|
||||
if (workspaces.length === 0) {
|
||||
return null;
|
||||
@ -104,15 +76,10 @@ const LocalWorkspaces = ({
|
||||
{t['com.affine.workspaceList.workspaceListType.local']()}
|
||||
</div>
|
||||
<WorkspaceList
|
||||
openingId={openingId}
|
||||
disabled={disabled}
|
||||
items={workspaces}
|
||||
currentWorkspaceId={currentWorkspaceId}
|
||||
onClick={onClickWorkspace}
|
||||
onSettingClick={onClickWorkspaceSetting}
|
||||
onEnableCloudClick={onClickEnableCloud}
|
||||
useIsWorkspaceOwner={useIsWorkspaceOwner}
|
||||
useWorkspaceName={useWorkspaceName}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@ -120,20 +87,20 @@ const LocalWorkspaces = ({
|
||||
|
||||
export const AFFiNEWorkspaceList = ({
|
||||
onEventEnd,
|
||||
onClickWorkspace,
|
||||
showEnableCloudButton,
|
||||
showSettingsButton,
|
||||
}: {
|
||||
onClickWorkspace?: (workspaceMetadata: WorkspaceMetadata) => void;
|
||||
onEventEnd?: () => void;
|
||||
showSettingsButton?: boolean;
|
||||
showEnableCloudButton?: boolean;
|
||||
}) => {
|
||||
const workspacesService = useService(WorkspacesService);
|
||||
const workspaces = useLiveData(workspacesService.list.workspaces$);
|
||||
const currentWorkspaceId = useLiveData(
|
||||
useService(GlobalContextService).globalContext.workspaceId.$
|
||||
);
|
||||
|
||||
const setOpenCreateWorkspaceModal = useSetAtom(openCreateWorkspaceModalAtom);
|
||||
const confirmEnableCloud = useEnableCloud();
|
||||
|
||||
const { jumpToSubPath } = useNavigateHelper();
|
||||
|
||||
const setOpenSettingModalAtom = useSetAtom(openSettingModalAtom);
|
||||
|
||||
const session = useService(AuthService).session;
|
||||
@ -181,34 +148,14 @@ export const AFFiNEWorkspaceList = ({
|
||||
[confirmEnableCloud, workspacesService]
|
||||
);
|
||||
|
||||
const onClickWorkspace = useCallback(
|
||||
const handleClickWorkspace = useCallback(
|
||||
(workspaceMetadata: WorkspaceMetadata) => {
|
||||
if (document.startViewTransition) {
|
||||
document.startViewTransition(() => {
|
||||
jumpToSubPath(workspaceMetadata.id, WorkspaceSubPath.ALL);
|
||||
onEventEnd?.();
|
||||
return new Promise(resolve =>
|
||||
setTimeout(resolve, 150)
|
||||
); /* start transition after 150ms */
|
||||
});
|
||||
} else {
|
||||
jumpToSubPath(workspaceMetadata.id, WorkspaceSubPath.ALL);
|
||||
onEventEnd?.();
|
||||
}
|
||||
onClickWorkspace?.(workspaceMetadata);
|
||||
onEventEnd?.();
|
||||
},
|
||||
[jumpToSubPath, onEventEnd]
|
||||
[onClickWorkspace, onEventEnd]
|
||||
);
|
||||
|
||||
const onNewWorkspace = useCallback(() => {
|
||||
setOpenCreateWorkspaceModal('new');
|
||||
onEventEnd?.();
|
||||
}, [onEventEnd, setOpenCreateWorkspaceModal]);
|
||||
|
||||
const onAddWorkspace = useCallback(() => {
|
||||
setOpenCreateWorkspaceModal('add');
|
||||
onEventEnd?.();
|
||||
}, [onEventEnd, setOpenCreateWorkspaceModal]);
|
||||
|
||||
return (
|
||||
<ScrollableContainer
|
||||
className={styles.workspaceListsWrapper}
|
||||
@ -218,11 +165,10 @@ export const AFFiNEWorkspaceList = ({
|
||||
<div>
|
||||
<CloudWorkSpaceList
|
||||
workspaces={cloudWorkspaces}
|
||||
onClickWorkspace={onClickWorkspace}
|
||||
onClickWorkspaceSetting={onClickWorkspaceSetting}
|
||||
onNewWorkspace={onNewWorkspace}
|
||||
onAddWorkspace={onAddWorkspace}
|
||||
currentWorkspaceId={currentWorkspaceId}
|
||||
onClickWorkspace={handleClickWorkspace}
|
||||
onClickWorkspaceSetting={
|
||||
showSettingsButton ? onClickWorkspaceSetting : undefined
|
||||
}
|
||||
/>
|
||||
{localWorkspaces.length > 0 && cloudWorkspaces.length > 0 ? (
|
||||
<Divider size="thinner" />
|
||||
@ -231,13 +177,55 @@ export const AFFiNEWorkspaceList = ({
|
||||
) : null}
|
||||
<LocalWorkspaces
|
||||
workspaces={localWorkspaces}
|
||||
onClickWorkspace={onClickWorkspace}
|
||||
onClickWorkspaceSetting={onClickWorkspaceSetting}
|
||||
onClickEnableCloud={onClickEnableCloud}
|
||||
onNewWorkspace={onNewWorkspace}
|
||||
onAddWorkspace={onAddWorkspace}
|
||||
currentWorkspaceId={currentWorkspaceId}
|
||||
onClickWorkspace={handleClickWorkspace}
|
||||
onClickWorkspaceSetting={
|
||||
showSettingsButton ? onClickWorkspaceSetting : undefined
|
||||
}
|
||||
onClickEnableCloud={
|
||||
showEnableCloudButton ? onClickEnableCloud : undefined
|
||||
}
|
||||
/>
|
||||
</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({
|
||||
height: 42,
|
||||
overflow: 'hidden',
|
||||
@ -59,7 +68,6 @@ export const workspaceInfo = style({
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const workspaceName = style({
|
||||
fontSize: cssVar('fontSm'),
|
||||
lineHeight: '22px',
|
||||
@ -68,6 +76,8 @@ export const workspaceName = style({
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
width: '100%',
|
||||
display: 'inline-block',
|
||||
});
|
||||
|
||||
export const workspaceStatus = style({
|
||||
@ -105,3 +115,48 @@ export const workspaceInfoTooltip = style({
|
||||
padding: '0 8px',
|
||||
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 { useCallback, useContext, useMemo } from 'react';
|
||||
import type { NavigateOptions, To } from 'react-router-dom';
|
||||
import { createContext, useCallback, useContext, useMemo } from 'react';
|
||||
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 {
|
||||
REPLACE = 'replace',
|
||||
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
|
||||
/**
|
||||
* @deprecated use `WorkbenchService` instead
|
||||
* Use this for over workbench navigate, for navigate in workbench, use `WorkbenchService`.
|
||||
*/
|
||||
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(
|
||||
(
|
||||
@ -147,7 +147,7 @@ export function useNavigateHelper() {
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
if (redirectUri) {
|
||||
searchParams.set('redirect_uri', encodeURIComponent(redirectUri));
|
||||
searchParams.set('redirect_uri', redirectUri);
|
||||
}
|
||||
|
||||
if (params) {
|
||||
|
@ -16,6 +16,7 @@ import {
|
||||
registerAffineUpdatesCommands,
|
||||
} from '../commands';
|
||||
import { usePageHelper } from '../components/blocksuite/block-suite-page-list/utils';
|
||||
import { CreateWorkspaceDialogService } from '../modules/create-workspace';
|
||||
import { EditorSettingService } from '../modules/editor-settting';
|
||||
import { CMDKQuickSearchService } from '../modules/quicksearch/services/cmdk';
|
||||
import { useLanguageHelper } from './affine/use-language-helper';
|
||||
@ -69,6 +70,7 @@ export function useRegisterWorkspaceCommands() {
|
||||
const [editor] = useActiveBlocksuiteEditor();
|
||||
const cmdkQuickSearchService = useService(CMDKQuickSearchService);
|
||||
const editorSettingService = useService(EditorSettingService);
|
||||
const createWorkspaceDialogService = useService(CreateWorkspaceDialogService);
|
||||
|
||||
useEffect(() => {
|
||||
const unsub = registerCMDKCommand(cmdkQuickSearchService, editor);
|
||||
@ -131,7 +133,7 @@ export function useRegisterWorkspaceCommands() {
|
||||
// register AffineCreationCommands
|
||||
useEffect(() => {
|
||||
const unsub = registerAffineCreationCommands({
|
||||
store,
|
||||
createWorkspaceDialogService,
|
||||
pageHelper: pageHelper,
|
||||
t,
|
||||
});
|
||||
@ -139,7 +141,7 @@ export function useRegisterWorkspaceCommands() {
|
||||
return () => {
|
||||
unsub();
|
||||
};
|
||||
}, [store, pageHelper, t]);
|
||||
}, [store, pageHelper, t, createWorkspaceDialogService]);
|
||||
|
||||
// register AffineHelpCommands
|
||||
useEffect(() => {
|
||||
|
@ -6,19 +6,19 @@ import {
|
||||
} from '@toeverything/infra';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export function useWorkspaceInfo(meta: WorkspaceMetadata) {
|
||||
export function useWorkspaceInfo(meta?: WorkspaceMetadata) {
|
||||
const workspacesService = useService(WorkspacesService);
|
||||
|
||||
const profile = workspacesService.getProfile(meta);
|
||||
const profile = meta ? workspacesService.getProfile(meta) : undefined;
|
||||
|
||||
useEffect(() => {
|
||||
profile.revalidate();
|
||||
profile?.revalidate();
|
||||
}, [meta, profile]);
|
||||
|
||||
return useLiveData(profile.profile$);
|
||||
return useLiveData(profile?.profile$);
|
||||
}
|
||||
|
||||
export function useWorkspaceName(meta: WorkspaceMetadata) {
|
||||
export function useWorkspaceName(meta?: WorkspaceMetadata) {
|
||||
const information = useWorkspaceInfo(meta);
|
||||
|
||||
return information?.name;
|
||||
|
@ -38,6 +38,7 @@ import {
|
||||
appSidebarResizingAtom,
|
||||
SidebarSwitch,
|
||||
} from '../components/app-sidebar';
|
||||
import { OverCapacityNotification } from '../components/over-capacity';
|
||||
import { AIIsland } from '../components/pure/ai-island';
|
||||
import { RootAppSidebar } from '../components/root-app-sidebar';
|
||||
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 { QuickSearchContainer } from '../modules/quicksearch';
|
||||
import { WorkbenchService } from '../modules/workbench';
|
||||
import {
|
||||
AllWorkspaceModals,
|
||||
CurrentWorkspaceModals,
|
||||
} from '../providers/modal-provider';
|
||||
import { CurrentWorkspaceModals } from '../providers/modal-provider';
|
||||
import { SWRConfigProvider } from '../providers/swr-config-provider';
|
||||
import * as styles from './styles.css';
|
||||
|
||||
@ -63,7 +61,6 @@ export const WorkspaceLayout = function WorkspaceLayout({
|
||||
return (
|
||||
<SWRConfigProvider>
|
||||
{/* load all workspaces is costly, do not block the whole UI */}
|
||||
<AllWorkspaceModals />
|
||||
<CurrentWorkspaceModals />
|
||||
<WorkspaceLayoutInner>{children}</WorkspaceLayoutInner>
|
||||
{/* should show after workspace loaded */}
|
||||
@ -173,6 +170,7 @@ export const WorkspaceLayoutProviders = ({ children }: PropsWithChildren) => {
|
||||
{children}
|
||||
<QuickSearchContainer />
|
||||
<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 { configureCollectionModule } from './collection';
|
||||
import { configureCreateWorkspaceModule } from './create-workspace';
|
||||
import { configureDocLinksModule } from './doc-link';
|
||||
import { configureDocsSearchModule } from './docs-search';
|
||||
import { configureEditorModule } from './editor';
|
||||
@ -10,6 +11,7 @@ import { configureEditorSettingModule } from './editor-settting';
|
||||
import { configureExplorerModule } from './explorer';
|
||||
import { configureFavoriteModule } from './favorite';
|
||||
import { configureFindInPageModule } from './find-in-page';
|
||||
import { configureImportTemplateModule } from './import-template';
|
||||
import { configureNavigationModule } from './navigation';
|
||||
import { configureOrganizeModule } from './organize';
|
||||
import { configurePeekViewModule } from './peek-view';
|
||||
@ -45,4 +47,6 @@ export function configureCommonModules(framework: Framework) {
|
||||
configureEditorModule(framework);
|
||||
configureSystemFontFamilyModule(framework);
|
||||
configureEditorSettingModule(framework);
|
||||
configureImportTemplateModule(framework);
|
||||
configureCreateWorkspaceModule(framework);
|
||||
}
|
||||
|
@ -23,7 +23,10 @@ export const useCustomTheme = (target: HTMLElement) => {
|
||||
const valueMap = themeObj[mode];
|
||||
|
||||
// remove previous style
|
||||
// TOOD(@CatsJuice): find better way to remove previous style
|
||||
target.style.cssText = '';
|
||||
// recover color scheme set by next-themes
|
||||
target.style.colorScheme = mode;
|
||||
|
||||
Object.entries(valueMap).forEach(([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 { useAtomValue } from 'jotai';
|
||||
import { Suspense, useCallback } from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
import { AffineErrorBoundary } from '../../../components/affine/affine-error-boundary';
|
||||
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 leftSidebarOpen = useAtomValue(appSidebarOpenAtom);
|
||||
const workbench = useService(WorkbenchService).workbench;
|
||||
@ -74,7 +75,7 @@ export const RouteContainer = ({ route }: Props) => {
|
||||
|
||||
<AffineErrorBoundary>
|
||||
<Suspense>
|
||||
<route.Component />
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
</AffineErrorBoundary>
|
||||
<ViewBodyTarget viewId={view.id} className={styles.viewBodyContainer} />
|
||||
|
@ -8,15 +8,8 @@ import {
|
||||
useService,
|
||||
} from '@toeverything/infra';
|
||||
import { useAtom, useAtomValue } from 'jotai';
|
||||
import {
|
||||
lazy as reactLazy,
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { type RouteObject, useLocation } from 'react-router-dom';
|
||||
|
||||
import type { View } from '../entities/view';
|
||||
import { WorkbenchService } from '../services/workbench';
|
||||
@ -33,24 +26,6 @@ const useAdapter = environment.isDesktop
|
||||
? useBindWorkbenchToDesktopRouter
|
||||
: 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(() => {
|
||||
const workbench = useService(WorkbenchService).workbench;
|
||||
|
||||
@ -118,9 +93,18 @@ const WorkbenchView = ({ view, index }: { view: View; index: number }) => {
|
||||
return;
|
||||
}, [handleOnFocus]);
|
||||
|
||||
const routes: RouteObject[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
element: <RouteContainer />,
|
||||
children: viewRoutes,
|
||||
},
|
||||
] satisfies RouteObject[];
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={styles.workbenchViewContainer} ref={containerRef}>
|
||||
<ViewRoot routes={warpedRoutes} key={view.id} view={view} />
|
||||
<ViewRoot routes={routes} key={view.id} view={view} />
|
||||
</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 { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import {
|
||||
@ -7,7 +6,6 @@ import {
|
||||
WorkspacesService,
|
||||
} from '@toeverything/infra';
|
||||
import {
|
||||
lazy,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
@ -21,17 +19,11 @@ import {
|
||||
createFirstAppData,
|
||||
} from '../bootstrap/first-app-data';
|
||||
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 { AuthService } from '../modules/cloud';
|
||||
import { WorkspaceSubPath } from '../shared';
|
||||
|
||||
const AllWorkspaceModals = lazy(() =>
|
||||
import('../providers/modal-provider').then(({ AllWorkspaceModals }) => ({
|
||||
default: AllWorkspaceModals,
|
||||
}))
|
||||
);
|
||||
|
||||
export const loader: LoaderFunction = async () => {
|
||||
return null;
|
||||
};
|
||||
@ -41,6 +33,7 @@ export const Component = () => {
|
||||
const [navigating, setNavigating] = useState(true);
|
||||
const [creating, setCreating] = useState(false);
|
||||
const authService = useService(AuthService);
|
||||
|
||||
const loggedIn = useLiveData(
|
||||
authService.session.status$.map(s => s === 'authenticated')
|
||||
);
|
||||
@ -151,35 +144,19 @@ export const Component = () => {
|
||||
|
||||
// TODO(@eyhn): We need a no workspace page
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
left: '50%',
|
||||
top: '50%',
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
left: 'calc(50% - 150px)',
|
||||
top: '50%',
|
||||
}}
|
||||
>
|
||||
<WorkspaceNavigator
|
||||
open={true}
|
||||
menuContentOptions={{
|
||||
forceMount: true,
|
||||
}}
|
||||
>
|
||||
<Menu
|
||||
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 />
|
||||
</>
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
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 { WorkspaceLayout } from '../../layouts/workspace-layout';
|
||||
import { WorkbenchRoot } from '../../modules/workbench';
|
||||
import { AllWorkspaceModals } from '../../providers/modal-provider';
|
||||
import { performanceRenderLogger } from '../../shared';
|
||||
import { PageNotFound } from '../404';
|
||||
import { SharePage } from './share/share-page';
|
||||
@ -206,7 +205,6 @@ const WorkspacePage = ({ meta }: { meta: WorkspaceMetadata }) => {
|
||||
return (
|
||||
<FrameworkScope scope={workspace.scope}>
|
||||
<AppFallback key="workspaceLoading" />
|
||||
<AllWorkspaceModals />
|
||||
</FrameworkScope>
|
||||
);
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { notify } from '@affine/component';
|
||||
import { NotificationCenter, notify } from '@affine/component';
|
||||
import { events } from '@affine/electron-api';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import {
|
||||
GlobalContextService,
|
||||
useLiveData,
|
||||
useService,
|
||||
useServiceOptional,
|
||||
WorkspaceService,
|
||||
WorkspacesService,
|
||||
} from '@toeverything/infra';
|
||||
@ -13,15 +13,9 @@ import type { ReactElement } from 'react';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
import type { SettingAtom } from '../atoms';
|
||||
import {
|
||||
authAtom,
|
||||
openCreateWorkspaceModalAtom,
|
||||
openSettingModalAtom,
|
||||
openSignOutModalAtom,
|
||||
} from '../atoms';
|
||||
import { authAtom, openSettingModalAtom, openSignOutModalAtom } from '../atoms';
|
||||
import { AuthModal as Auth } from '../components/affine/auth';
|
||||
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 { IssueFeedbackModal } from '../components/affine/issue-feedback-modal';
|
||||
import {
|
||||
@ -36,7 +30,9 @@ import { useTrashModalHelper } from '../hooks/affine/use-trash-modal-helper';
|
||||
import { useAsyncCallback } from '../hooks/affine-async-hooks';
|
||||
import { useNavigateHelper } from '../hooks/use-navigate-helper';
|
||||
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 { ImportTemplateDialogProvider } from '../modules/import-template';
|
||||
import { PeekViewManagerModal } from '../modules/peek-view';
|
||||
import { WorkspaceSubPath } from '../shared';
|
||||
|
||||
@ -181,9 +177,16 @@ export const SignOutConfirmModal = () => {
|
||||
const { openPage } = useNavigateHelper();
|
||||
const authService = useService(AuthService);
|
||||
const [open, setOpen] = useAtom(openSignOutModalAtom);
|
||||
const currentWorkspace = useServiceOptional(WorkspaceService)?.workspace;
|
||||
const workspaces = useLiveData(
|
||||
useService(WorkspacesService).list.workspaces$
|
||||
const globalContextService = useService(GlobalContextService);
|
||||
const currentWorkspaceId = useLiveData(
|
||||
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 () => {
|
||||
@ -199,7 +202,7 @@ export const SignOutConfirmModal = () => {
|
||||
}
|
||||
|
||||
// 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(
|
||||
w => w.flavour === WorkspaceFlavour.LOCAL
|
||||
);
|
||||
@ -207,7 +210,13 @@ export const SignOutConfirmModal = () => {
|
||||
openPage(localWorkspace.id, WorkspaceSubPath.ALL);
|
||||
}
|
||||
}
|
||||
}, [authService, currentWorkspace, openPage, setOpen, workspaces]);
|
||||
}, [
|
||||
authService,
|
||||
currentWorkspaceMetadata?.flavour,
|
||||
openPage,
|
||||
setOpen,
|
||||
workspaces,
|
||||
]);
|
||||
|
||||
return (
|
||||
<SignOutModal open={open} onOpenChange={setOpen} onConfirm={onConfirm} />
|
||||
@ -215,35 +224,11 @@ export const SignOutConfirmModal = () => {
|
||||
};
|
||||
|
||||
export const AllWorkspaceModals = (): ReactElement => {
|
||||
const [isOpenCreateWorkspaceModal, setOpenCreateWorkspaceModal] = useAtom(
|
||||
openCreateWorkspaceModalAtom
|
||||
);
|
||||
|
||||
const { jumpToSubPath, jumpToPage } = useNavigateHelper();
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreateWorkspaceModal
|
||||
mode={isOpenCreateWorkspaceModal}
|
||||
onClose={useCallback(() => {
|
||||
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]
|
||||
)}
|
||||
/>
|
||||
<NotificationCenter />
|
||||
<ImportTemplateDialogProvider />
|
||||
<CreateWorkspaceDialogProvider />
|
||||
<AuthModal />
|
||||
<SignOutConfirmModal />
|
||||
</>
|
||||
|
@ -1,15 +1,15 @@
|
||||
import { wrapCreateBrowserRouter } from '@sentry/react';
|
||||
import { createContext, useEffect, useState } from 'react';
|
||||
import type { NavigateFunction, RouteObject } from 'react-router-dom';
|
||||
import { useEffect, useState } from 'react';
|
||||
import type { RouteObject } from 'react-router-dom';
|
||||
import {
|
||||
createBrowserRouter as reactRouterCreateBrowserRouter,
|
||||
Outlet,
|
||||
redirect,
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
useNavigate,
|
||||
} 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() {
|
||||
const navigate = useNavigate();
|
||||
@ -22,7 +22,7 @@ export function RootRouter() {
|
||||
return (
|
||||
ready && (
|
||||
<NavigateContext.Provider value={navigate}>
|
||||
<Outlet />
|
||||
<RootWrapper />
|
||||
</NavigateContext.Provider>
|
||||
)
|
||||
);
|
||||
@ -114,6 +114,10 @@ export const topLevelRoutes = [
|
||||
path: '/theme-editor',
|
||||
lazy: () => import('./pages/theme-editor'),
|
||||
},
|
||||
{
|
||||
path: '/template/import',
|
||||
lazy: () => import('./pages/import-template'),
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
lazy: () => import('./pages/404'),
|
||||
|
@ -1,7 +1,6 @@
|
||||
import '@affine/component/theme/global.css';
|
||||
import '@affine/component/theme/theme.css';
|
||||
|
||||
import { NotificationCenter } from '@affine/component';
|
||||
import { AffineContext } from '@affine/component/context';
|
||||
import { GlobalLoading } from '@affine/component/global-loading';
|
||||
import { AppFallback } from '@affine/core/components/affine/app-container';
|
||||
@ -114,7 +113,6 @@ export function App() {
|
||||
<CustomThemeModifier />
|
||||
<DebugProvider>
|
||||
<GlobalLoading />
|
||||
<NotificationCenter />
|
||||
<RouterProvider
|
||||
fallbackElement={<AppFallback key="RouterFallback" />}
|
||||
router={router}
|
||||
|
@ -1551,5 +1551,10 @@
|
||||
"will be moved to Trash": "{{title}} will be moved to trash",
|
||||
"will delete member": "will delete member",
|
||||
"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/theme.css';
|
||||
|
||||
import { NotificationCenter } from '@affine/component';
|
||||
import { AffineContext } from '@affine/component/context';
|
||||
import { AppFallback } from '@affine/core/components/affine/app-container';
|
||||
import { configureCommonModules } from '@affine/core/modules';
|
||||
@ -78,7 +77,6 @@ export function App() {
|
||||
<FrameworkRoot framework={frameworkProvider}>
|
||||
<AffineContext store={getCurrentStore()}>
|
||||
<Telemetry />
|
||||
<NotificationCenter />
|
||||
<RouterProvider
|
||||
fallbackElement={<AppFallback key="RouterFallback" />}
|
||||
router={router}
|
||||
|
@ -1,10 +1,7 @@
|
||||
import { AffineErrorBoundary } from '@affine/core/components/affine/affine-error-boundary';
|
||||
import { AppFallback } from '@affine/core/components/affine/app-container';
|
||||
import { WorkspaceLayoutProviders } from '@affine/core/layouts/workspace-layout';
|
||||
import {
|
||||
AllWorkspaceModals,
|
||||
CurrentWorkspaceModals,
|
||||
} from '@affine/core/providers/modal-provider';
|
||||
import { CurrentWorkspaceModals } from '@affine/core/providers/modal-provider';
|
||||
import { SWRConfigProvider } from '@affine/core/providers/swr-config-provider';
|
||||
import type { Workspace, WorkspaceMetadata } from '@toeverything/infra';
|
||||
import {
|
||||
@ -73,7 +70,6 @@ export const WorkspaceLayout = ({
|
||||
return (
|
||||
<FrameworkScope scope={workspace.scope}>
|
||||
<AppFallback key="workspaceLoading" />
|
||||
<AllWorkspaceModals />
|
||||
</FrameworkScope>
|
||||
);
|
||||
}
|
||||
@ -82,7 +78,6 @@ export const WorkspaceLayout = ({
|
||||
<FrameworkScope scope={workspace.scope}>
|
||||
<AffineErrorBoundary height="100vh">
|
||||
<SWRConfigProvider>
|
||||
<AllWorkspaceModals />
|
||||
<CurrentWorkspaceModals />
|
||||
<WorkspaceLayoutProviders>{children}</WorkspaceLayoutProviders>
|
||||
</SWRConfigProvider>
|
||||
|
@ -1,7 +1,6 @@
|
||||
import '@affine/component/theme/global.css';
|
||||
import '@affine/component/theme/theme.css';
|
||||
|
||||
import { NotificationCenter } from '@affine/component';
|
||||
import { AffineContext } from '@affine/component/context';
|
||||
import { GlobalLoading } from '@affine/component/global-loading';
|
||||
import { AppFallback } from '@affine/core/components/affine/app-container';
|
||||
@ -100,7 +99,6 @@ export function App() {
|
||||
<CustomThemeModifier />
|
||||
<DebugProvider>
|
||||
<GlobalLoading />
|
||||
<NotificationCenter />
|
||||
<RouterProvider
|
||||
fallbackElement={<AppFallback key="RouterFallback" />}
|
||||
router={router}
|
||||
|
@ -13,6 +13,7 @@ test('Create new workspace, then delete it', async ({ page, workspace }) => {
|
||||
await waitForEditorLoad(page);
|
||||
await clickSideBarCurrentWorkspaceBanner(page);
|
||||
await page.getByTestId('new-workspace').click();
|
||||
await page.waitForTimeout(1000);
|
||||
await page
|
||||
.getByTestId('create-workspace-input')
|
||||
.pressSequentially('Test Workspace', { delay: 50 });
|
||||
|
@ -83,7 +83,7 @@ test.skip('create multi workspace in the workspace list', async ({
|
||||
|
||||
await page.reload();
|
||||
await openWorkspaceListModal(page);
|
||||
await page.getByTestId('draggable-item').nth(1).click();
|
||||
await page.getByTestId('workspace-card').nth(1).click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
const currentWorkspace = await workspace.current();
|
||||
@ -92,8 +92,8 @@ test.skip('create multi workspace in the workspace list', async ({
|
||||
|
||||
await openWorkspaceListModal(page);
|
||||
await page.waitForTimeout(1000);
|
||||
const sourceElement = page.getByTestId('draggable-item').nth(2);
|
||||
const targetElement = page.getByTestId('draggable-item').nth(1);
|
||||
const sourceElement = page.getByTestId('workspace-card').nth(2);
|
||||
const targetElement = page.getByTestId('workspace-card').nth(1);
|
||||
|
||||
const sourceBox = await sourceElement.boundingBox();
|
||||
const targetBox = await targetElement.boundingBox();
|
||||
|
@ -173,7 +173,11 @@ export const selectVisibilitySelector = async (
|
||||
})
|
||||
.click();
|
||||
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await page.keyboard.press('Escape');
|
||||
|
||||
await page.waitForTimeout(500);
|
||||
};
|
||||
|
||||
export const changePropertyVisibility = async (
|
||||
|
@ -9,7 +9,7 @@ export async function clickSideBarAllPageButton(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) {
|
||||
|
Loading…
Reference in New Issue
Block a user