mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-10 05:29:08 +08:00
feat(core): import template (#8000)
This commit is contained in:
+3
-2
@@ -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;
|
||||
};
|
||||
-313
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
+34
-24
@@ -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} />;
|
||||
};
|
||||
+2
-1
@@ -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 = ({
|
||||
+4
@@ -29,3 +29,7 @@ export const scrollbar = style({
|
||||
transform: 'translateX(8px)',
|
||||
width: '4px',
|
||||
});
|
||||
export const workspaceCard = style({
|
||||
height: '44px',
|
||||
padding: '0 12px',
|
||||
});
|
||||
+67
-79
@@ -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';
|
||||
+56
-1
@@ -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,
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user