feat: create workspace from loading existing exported file (#2122)

Co-authored-by: Himself65 <himself65@outlook.com>
This commit is contained in:
Peng Xiao
2023-05-09 15:30:01 +08:00
committed by GitHub
parent 5432aae85c
commit 7c2574b1ca
93 changed files with 2999 additions and 1406 deletions

View File

@@ -0,0 +1,43 @@
import { globalStyle, style } from '@vanilla-extract/css';
export const header = style({
position: 'relative',
height: '44px',
});
export const content = style({
padding: '0 40px',
fontSize: '18px',
lineHeight: '26px',
});
globalStyle(`${content} p`, {
marginTop: '12px',
marginBottom: '16px',
});
export const contentTitle = style({
fontSize: '20px',
lineHeight: '28px',
fontWeight: 600,
paddingBottom: '16px',
});
export const buttonGroup = style({
display: 'flex',
justifyContent: 'flex-end',
gap: '20px',
margin: '24px 0',
});
export const radioGroup = style({
display: 'flex',
flexDirection: 'column',
gap: '8px',
});
export const radio = style({
cursor: 'pointer',
appearance: 'auto',
marginRight: '12px',
});

View File

@@ -0,0 +1,346 @@
import {
Button,
Input,
Modal,
ModalCloseButton,
ModalWrapper,
toast,
Tooltip,
} from '@affine/component';
import { DebugLogger } from '@affine/debug';
import { config } from '@affine/env';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { HelpIcon } from '@blocksuite/icons';
import { useSetAtom } from 'jotai';
import type { KeyboardEvent } from 'react';
import { useEffect } from 'react';
import { useLayoutEffect } from 'react';
import { useCallback, useRef, useState } from 'react';
import { openDisableCloudAlertModalAtom } from '../../../atoms';
import { useAppHelper } from '../../../hooks/use-workspaces';
import * as style from './index.css';
type CreateWorkspaceStep =
| 'set-db-location'
| 'name-workspace'
| 'set-syncing-mode';
export type CreateWorkspaceMode = 'add' | 'new' | false;
const logger = new DebugLogger('CreateWorkspaceModal');
interface ModalProps {
mode: CreateWorkspaceMode; // false means not open
onClose: () => void;
onCreate: (id: string) => void;
}
interface NameWorkspaceContentProps {
onClose: () => void;
onConfirmName: (name: string) => void;
}
const NameWorkspaceContent = ({
onConfirmName,
onClose,
}: NameWorkspaceContentProps) => {
const [workspaceName, setWorkspaceName] = useState('');
const isComposition = useRef(false);
const handleCreateWorkspace = useCallback(() => {
onConfirmName(workspaceName);
}, [onConfirmName, workspaceName]);
const handleKeyDown = useCallback(
(event: KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter' && workspaceName && !isComposition.current) {
handleCreateWorkspace();
}
},
[handleCreateWorkspace, workspaceName]
);
const t = useAFFiNEI18N();
return (
<div className={style.content}>
<div className={style.contentTitle}>{t['Name Your Workspace']()}</div>
<p>{t['Workspace description']()}</p>
<Input
ref={ref => {
if (ref) {
setTimeout(() => ref.focus(), 0);
}
}}
data-testid="create-workspace-input"
onKeyDown={handleKeyDown}
placeholder={t['Set a Workspace name']()}
maxLength={15} // TODO: the max workspace name length?
minLength={0}
onChange={value => {
setWorkspaceName(value);
}}
onCompositionStart={() => {
isComposition.current = true;
}}
onCompositionEnd={() => {
isComposition.current = false;
}}
/>
<div className={style.buttonGroup}>
<Button
data-testid="create-workspace-close-button"
type="light"
onClick={() => {
onClose();
}}
>
{t.Cancel()}
</Button>
<Button
data-testid="create-workspace-create-button"
disabled={!workspaceName}
style={{
opacity: !workspaceName ? 0.5 : 1,
}}
type="primary"
onClick={() => {
handleCreateWorkspace();
}}
>
{t.Create()}
</Button>
</div>
</div>
);
};
interface SetDBLocationContentProps {
onConfirmLocation: (dir?: string) => void;
}
const SetDBLocationContent = ({
onConfirmLocation,
}: SetDBLocationContentProps) => {
const t = useAFFiNEI18N();
const [defaultDBLocation, setDefaultDBLocation] = useState('');
useEffect(() => {
window.apis?.db.getDefaultStorageLocation().then(dir => {
setDefaultDBLocation(dir);
});
}, []);
return (
<div className={style.content}>
<div className={style.contentTitle}>{t['Set database location']()}</div>
<p>{t['Workspace database storage description']()}</p>
<div className={style.buttonGroup}>
<Button
data-testid="create-workspace-customize-button"
type="light"
onClick={async () => {
const result = await window.apis?.dialog.selectDBFileLocation();
if (result) {
onConfirmLocation(result.filePath);
}
}}
>
{t['Customize']()}
</Button>
<Tooltip
zIndex={1000}
content={t['Default db location hint']({
location: defaultDBLocation,
})}
placement="top-start"
>
<Button
data-testid="create-workspace-default-location-button"
type="primary"
onClick={() => {
onConfirmLocation();
}}
icon={<HelpIcon />}
iconPosition="end"
>
{t['Default Location']()}
</Button>
</Tooltip>
</div>
</div>
);
};
interface SetSyncingModeContentProps {
mode: CreateWorkspaceMode;
onConfirmMode: (enableCloudSyncing: boolean) => void;
}
const SetSyncingModeContent = ({
mode,
onConfirmMode,
}: SetSyncingModeContentProps) => {
const t = useAFFiNEI18N();
const [enableCloudSyncing, setEnableCloudSyncing] = useState(false);
return (
<div className={style.content}>
<div className={style.contentTitle}>
{t[mode === 'new' ? 'Created Successfully' : 'Added Successfully']()}
</div>
<div className={style.radioGroup}>
<label onClick={() => setEnableCloudSyncing(false)}>
<input
className={style.radio}
type="radio"
readOnly
checked={!enableCloudSyncing}
/>
{t['Use on current device only']()}
</label>
<label onClick={() => setEnableCloudSyncing(true)}>
<input
className={style.radio}
type="radio"
readOnly
checked={enableCloudSyncing}
/>
{t['Sync across devices with AFFiNE Cloud']()}
</label>
</div>
<div className={style.buttonGroup}>
<Button
data-testid="create-workspace-continue-button"
type="primary"
onClick={() => {
onConfirmMode(enableCloudSyncing);
}}
>
{t['Continue']()}
</Button>
</div>
</div>
);
};
export const CreateWorkspaceModal = ({
mode,
onClose,
onCreate,
}: ModalProps) => {
const { createLocalWorkspace, addLocalWorkspace } = useAppHelper();
const [step, setStep] = useState<CreateWorkspaceStep>();
const [addedId, setAddedId] = useState<string>();
const [workspaceName, setWorkspaceName] = useState<string>();
const [dbFileLocation, setDBFileLocation] = useState<string>();
const setOpenDisableCloudAlertModal = useSetAtom(
openDisableCloudAlertModalAtom
);
const t = useAFFiNEI18N();
// todo: 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 (!window.apis) {
return;
}
logger.info('load db file');
setStep(undefined);
const result = await window.apis.dialog.loadDBFile();
if (result.workspaceId && !canceled) {
setAddedId(result.workspaceId);
setStep('set-syncing-mode');
} else if (result.error || result.canceled) {
if (result.error) {
toast(t[result.error]());
}
onClose();
}
})();
} else if (mode === 'new') {
setStep(environment.isDesktop ? 'set-db-location' : 'name-workspace');
} else {
setStep(undefined);
}
return () => {
canceled = true;
};
}, [mode, onClose, t]);
return (
<Modal open={mode !== false && !!step} onClose={onClose}>
<ModalWrapper width={560} style={{ padding: '10px' }}>
<div className={style.header}>
<ModalCloseButton
top={6}
right={6}
onClick={() => {
onClose();
}}
/>
</div>
{step === 'name-workspace' && (
<NameWorkspaceContent
// go to previous step instead?
onClose={onClose}
onConfirmName={async name => {
setWorkspaceName(name);
if (environment.isDesktop) {
setStep('set-syncing-mode');
} else {
// this will be the last step for web for now
// fix me later
const id = await createLocalWorkspace(name);
onCreate(id);
}
}}
/>
)}
{step === 'set-db-location' && (
<SetDBLocationContent
onConfirmLocation={dir => {
setDBFileLocation(dir);
setStep('name-workspace');
}}
/>
)}
{step === 'set-syncing-mode' && (
<SetSyncingModeContent
mode={mode}
onConfirmMode={async enableCloudSyncing => {
if (!config.enableLegacyCloud && enableCloudSyncing) {
setOpenDisableCloudAlertModal(true);
} else {
let id = addedId;
// syncing mode is also the last step
if (addedId && mode === 'add') {
await addLocalWorkspace(addedId);
} else if (mode === 'new' && workspaceName) {
id = await createLocalWorkspace(workspaceName);
// if dbFileLocation is set, move db file to that location
if (dbFileLocation) {
await window.apis?.dialog.moveDBFile(id, dbFileLocation);
}
} else {
logger.error('invalid state');
return;
}
if (id) {
onCreate(id);
}
}
}}
/>
)}
</ModalWrapper>
</Modal>
);
};

View File

@@ -26,7 +26,7 @@ export const StyleTips = styled('div')(() => {
userSelect: 'none',
margin: '20px 0',
a: {
color: 'var(--affine-background-primary-color)',
color: 'var(--affine-primary-color)',
},
};
});

View File

@@ -0,0 +1,192 @@
// import { styled } from '@affine/component';
// import { FlexWrapper } from '@affine/component';
import { globalStyle, style, styleVariants } from '@vanilla-extract/css';
export const container = style({
display: 'flex',
flexDirection: 'column',
padding: '52px 52px 0 52px',
height: 'calc(100vh - 52px)',
});
export const sidebar = style({
marginTop: '52px',
});
export const content = style({
overflow: 'auto',
flex: 1,
marginTop: '40px',
});
const baseAvatar = style({
position: 'relative',
marginRight: '20px',
cursor: 'pointer',
});
globalStyle(`${baseAvatar} .camera-icon`, {
position: 'absolute',
top: 0,
left: 0,
display: 'none',
width: '100%',
height: '100%',
borderRadius: '50%',
backgroundColor: 'rgba(60, 61, 63, 0.5)',
justifyContent: 'center',
alignItems: 'center',
zIndex: 10,
});
globalStyle(`${baseAvatar}:hover .camera-icon`, {
display: 'flex',
});
export const avatar = styleVariants({
disabled: [
baseAvatar,
{
cursor: 'default',
},
],
enabled: [
baseAvatar,
{
cursor: 'pointer',
},
],
});
const baseTagItem = style({
display: 'flex',
margin: '0 48px 0 0',
height: '34px',
fontWeight: '500',
fontSize: 'var(--affine-font-h6)',
lineHeight: 'var(--affine-line-height)',
cursor: 'pointer',
transition: 'all 0.15s ease',
});
export const tagItem = styleVariants({
active: [
baseTagItem,
{
color: 'var(--affine-primary-color)',
},
],
inactive: [
baseTagItem,
{
color: 'var(--affine-text-secondary-color)',
},
],
});
export const settingKey = style({
width: '140px',
fontSize: 'var(--affine-font-base)',
fontWeight: 500,
marginRight: '56px',
flexShrink: 0,
});
export const settingItemLabel = style({
fontSize: 'var(--affine-font-base)',
fontWeight: 600,
flexShrink: 0,
});
export const settingItemLabelHint = style({
fontSize: 'var(--affine-font-sm)',
color: 'var(--affine-text-secondary-color)',
fontWeight: 400,
flexShrink: 0,
marginTop: '4px',
});
export const row = style({
padding: '40px 0',
display: 'flex',
gap: '60px',
selectors: {
'&': {
borderBottom: '1px solid var(--affine-border-color)',
},
'&:first-child': {
paddingTop: 0,
},
},
});
export const col = style({
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
flexShrink: 0,
selectors: {
[`${row} &:nth-child(1)`]: {
flex: 3,
},
[`${row} &:nth-child(2)`]: {
flex: 5,
},
[`${row} &:nth-child(3)`]: {
flex: 2,
alignItems: 'flex-end',
},
},
});
export const workspaceName = style({
fontWeight: '400',
fontSize: 'var(--affine-font-h6)',
});
export const indicator = style({
height: '2px',
background: 'var(--affine-primary-color)',
position: 'absolute',
left: '0',
bottom: '0',
transition: 'left .3s, width .3s',
});
export const tabButtonWrapper = style({
display: 'flex',
position: 'relative',
});
export const storageTypeWrapper = style({
width: '100%',
display: 'flex',
alignItems: 'flex-start',
padding: '12px',
borderRadius: '10px',
gap: '12px',
boxShadow: 'var(--affine-shadow-1)',
cursor: 'pointer',
selectors: {
'&:hover': {
boxShadow: 'var(--affine-shadow-2)',
},
'&:not(:last-child)': {
marginBottom: '12px',
},
},
});
export const storageTypeLabelWrapper = style({
flex: 1,
});
export const storageTypeLabel = style({
fontSize: 'var(--affine-font-base)',
});
export const storageTypeLabelHint = style({
fontSize: 'var(--affine-font-sm)',
color: 'var(--affine-text-secondary-color)',
});

View File

@@ -9,18 +9,12 @@ import { preload } from 'swr';
import { useIsWorkspaceOwner } from '../../../hooks/affine/use-is-workspace-owner';
import { fetcher, QueryKey } from '../../../plugins/affine/fetcher';
import type { AffineOfficialWorkspace } from '../../../shared';
import * as style from './index.css';
import { CollaborationPanel } from './panel/collaboration';
import { ExportPanel } from './panel/export';
import { GeneralPanel } from './panel/general';
import { PublishPanel } from './panel/publish';
import { SyncPanel } from './panel/sync';
import {
StyledIndicator,
StyledSettingContainer,
StyledSettingContent,
StyledTabButtonWrapper,
WorkspaceSettingTagItem,
} from './style';
export type WorkspaceSettingDetailProps = {
workspace: AffineOfficialWorkspace;
@@ -133,39 +127,43 @@ export const WorkspaceSettingDetail: React.FC<
);
const Component = useMemo(() => panelMap[currentTab].ui, [currentTab]);
return (
<StyledSettingContainer
<div
className={style.container}
aria-label="workspace-setting-detail"
ref={containerRef}
>
<StyledTabButtonWrapper>
<div className={style.tabButtonWrapper}>
{Object.entries(panelMap).map(([key, value]) => {
if ('enable' in value && !value.enable(workspace.flavour)) {
return null;
}
return (
<WorkspaceSettingTagItem
<div
className={
style.tagItem[currentTab === key ? 'active' : 'inactive']
}
key={key}
isActive={currentTab === key}
data-tab-key={key}
onClick={handleTabClick}
>
{t[value.name]()}
</WorkspaceSettingTagItem>
</div>
);
})}
<StyledIndicator
<div
className={style.indicator}
ref={ref => {
indicatorRef.current = ref;
startTransaction();
}}
/>
</StyledTabButtonWrapper>
<StyledSettingContent>
</div>
<div className={style.content}>
{/* todo: add skeleton */}
<Suspense fallback="loading panel...">
<Component {...props} key={currentTab} data-tab-ui={currentTab} />
</Suspense>
</StyledSettingContent>
</StyledSettingContainer>
</div>
</div>
);
};

View File

@@ -1,7 +1,10 @@
import { Button, Wrapper } from '@affine/component';
import { Button, toast, Wrapper } from '@affine/component';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { rootCurrentWorkspaceIdAtom } from '@affine/workspace/atom';
import { useAtomValue } from 'jotai';
export const ExportPanel = () => {
const id = useAtomValue(rootCurrentWorkspaceIdAtom);
const t = useAFFiNEI18N();
return (
<>
@@ -9,9 +12,12 @@ export const ExportPanel = () => {
<Button
type="light"
shape="circle"
disabled={!environment.isDesktop}
onClick={() => {
window.apis.openSaveDBFileDialog();
disabled={!environment.isDesktop || !id}
data-testid="export-affine-backup"
onClick={async () => {
if (id && (await window.apis?.dialog.saveDBFileAs(id))) {
toast(t['Export success']());
}
}}
>
{t['Export AFFiNE backup file']()}

View File

@@ -37,11 +37,15 @@ export const WorkspaceDeleteModal = ({
const t = useAFFiNEI18N();
const handleDelete = useCallback(() => {
onDeleteWorkspace().then(() => {
toast(t['Successfully deleted'](), {
portal: document.body,
onDeleteWorkspace()
.then(() => {
toast(t['Successfully deleted'](), {
portal: document.body,
});
})
.catch(() => {
// ignore error
});
});
}, [onDeleteWorkspace, t]);
return (

View File

@@ -1,30 +1,27 @@
import { Button, FlexWrapper, MuiFade } from '@affine/component';
import { Button, toast } from '@affine/component';
import { WorkspaceAvatar } from '@affine/component/workspace-avatar';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { WorkspaceFlavour } from '@affine/workspace/type';
import {
ArrowRightSmallIcon,
DeleteIcon,
FolderIcon,
MoveToIcon,
SaveIcon,
} from '@blocksuite/icons';
import { useBlockSuiteWorkspaceAvatarUrl } from '@toeverything/hooks/use-block-suite-workspace-avatar-url';
import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite-workspace-name';
import clsx from 'clsx';
import type React from 'react';
import { useState } from 'react';
import { useIsWorkspaceOwner } from '../../../../../hooks/affine/use-is-workspace-owner';
import { Upload } from '../../../../pure/file-upload';
import {
CloudWorkspaceIcon,
JoinedWorkspaceIcon,
LocalWorkspaceIcon,
} from '../../../../pure/icons';
import type { PanelProps } from '../../index';
import { StyledRow, StyledSettingKey } from '../../style';
import * as style from '../../index.css';
import { WorkspaceDeleteModal } from './delete';
import { CameraIcon } from './icons';
import { WorkspaceLeave } from './leave';
import {
StyledAvatar,
StyledEditButton,
StyledInput,
StyledWorkspaceInfo,
} from './style';
import { StyledInput } from './style';
export const GeneralPanel: React.FC<PanelProps> = ({
workspace,
@@ -37,11 +34,11 @@ export const GeneralPanel: React.FC<PanelProps> = ({
);
const [input, setInput] = useState<string>(name);
const isOwner = useIsWorkspaceOwner(workspace);
const [showEditInput, setShowEditInput] = useState(false);
const t = useAFFiNEI18N();
const handleUpdateWorkspaceName = (name: string) => {
setName(name);
toast(t['Update workspace name success']());
};
const [, update] = useBlockSuiteWorkspaceAvatarUrl(
@@ -49,187 +46,189 @@ export const GeneralPanel: React.FC<PanelProps> = ({
);
return (
<>
<StyledRow>
<StyledSettingKey>{t['Workspace Avatar']()}</StyledSettingKey>
<StyledAvatar disabled={!isOwner}>
{isOwner ? (
<Upload
accept="image/gif,image/jpeg,image/jpg,image/png,image/svg"
fileChange={update}
data-testid="upload-avatar"
<div data-testid="avatar-row" className={style.row}>
<div className={style.col}>
<div className={style.settingItemLabel}>
{t['Workspace Avatar']()}
</div>
<div className={style.settingItemLabelHint}>
{t['Change avatar hint']()}
</div>
</div>
<div className={clsx(style.col)}>
<div className={style.avatar[isOwner ? 'enabled' : 'disabled']}>
{isOwner ? (
<Upload
accept="image/gif,image/jpeg,image/jpg,image/png,image/svg"
fileChange={update}
data-testid="upload-avatar"
>
<>
<div className="camera-icon">
<CameraIcon></CameraIcon>
</div>
<WorkspaceAvatar size={72} workspace={workspace} />
</>
</Upload>
) : (
<WorkspaceAvatar size={72} workspace={workspace} />
)}
</div>
</div>
<div className={clsx(style.col)}></div>
</div>
<div data-testid="workspace-name-row" className={style.row}>
<div className={style.col}>
<div className={style.settingItemLabel}>{t['Workspace Name']()}</div>
<div className={style.settingItemLabelHint}>
{t['Change workspace name hint']()}
</div>
</div>
<div className={style.col}>
<StyledInput
width={284}
height={38}
value={input}
data-testid="workspace-name-input"
placeholder={t['Workspace Name']()}
maxLength={50}
minLength={0}
onChange={newName => {
setInput(newName);
}}
></StyledInput>
</div>
<div className={style.col}>
<Button
type="light"
size="middle"
data-testid="save-workspace-name"
icon={<SaveIcon />}
disabled={input === workspace.blockSuiteWorkspace.meta.name}
onClick={() => {
handleUpdateWorkspaceName(input);
}}
>
{t['Save']()}
</Button>
</div>
</div>
{environment.isDesktop && (
<div className={style.row}>
<div className={style.col}>
<div className={style.settingItemLabel}>
{t['Storage Folder']()}
</div>
<div className={style.settingItemLabelHint}>
{t['Storage Folder Hint']()}
</div>
</div>
<div className={style.col}>
<div
className={style.storageTypeWrapper}
onClick={() => {
if (environment.isDesktop) {
window.apis?.dialog.revealDBFile(workspace.id);
}
}}
>
<>
<div className="camera-icon">
<CameraIcon></CameraIcon>
<FolderIcon color="var(--affine-primary-color)" />
<div className={style.storageTypeLabelWrapper}>
<div className={style.storageTypeLabel}>
{t['Open folder']()}
</div>
<WorkspaceAvatar size={72} workspace={workspace} />
</>
</Upload>
<div className={style.storageTypeLabelHint}>
{t['Open folder hint']()}
</div>
</div>
<ArrowRightSmallIcon color="var(--affine-primary-color)" />
</div>
<div
data-testid="move-folder"
className={style.storageTypeWrapper}
onClick={async () => {
if (await window.apis?.dialog.moveDBFile(workspace.id)) {
toast(t['Move folder success']());
}
}}
>
<MoveToIcon color="var(--affine-primary-color)" />
<div className={style.storageTypeLabelWrapper}>
<div className={style.storageTypeLabel}>
{t['Move folder']()}
</div>
<div className={style.storageTypeLabelHint}>
{t['Move folder hint']()}
</div>
</div>
<ArrowRightSmallIcon color="var(--affine-primary-color)" />
</div>
</div>
<div className={style.col}></div>
</div>
)}
<div className={style.row}>
<div className={style.col}>
<div className={style.settingItemLabel}>
{t['Delete Workspace']()}
</div>
<div className={style.settingItemLabelHint}>
{t['Delete Workspace Label Hint']()}
</div>
</div>
<div className={style.col}></div>
<div className={style.col}>
{isOwner ? (
<>
<Button
type="warning"
data-testid="delete-workspace-button"
size="middle"
icon={<DeleteIcon />}
onClick={() => {
setShowDelete(true);
}}
>
{t['Delete']()}
</Button>
<WorkspaceDeleteModal
onDeleteWorkspace={onDeleteWorkspace}
open={showDelete}
onClose={() => {
setShowDelete(false);
}}
workspace={workspace}
/>
</>
) : (
<WorkspaceAvatar size={72} workspace={workspace} />
)}
</StyledAvatar>
</StyledRow>
<StyledRow>
<StyledSettingKey>{t['Workspace Name']()}</StyledSettingKey>
<div style={{ position: 'relative' }}>
<MuiFade in={!showEditInput}>
<FlexWrapper>
{name}
{isOwner && (
<StyledEditButton
onClick={() => {
setShowEditInput(true);
}}
>
{t['Edit']()}
</StyledEditButton>
)}
</FlexWrapper>
</MuiFade>
{isOwner && (
<MuiFade in={showEditInput}>
<FlexWrapper style={{ position: 'absolute', top: 0, left: 0 }}>
<StyledInput
width={284}
height={38}
value={input}
placeholder={t['Workspace Name']()}
maxLength={50}
minLength={0}
onChange={newName => {
setInput(newName);
}}
></StyledInput>
<Button
type="light"
shape="circle"
style={{ marginLeft: '24px' }}
disabled={input === workspace.blockSuiteWorkspace.meta.name}
onClick={() => {
handleUpdateWorkspaceName(input);
setShowEditInput(false);
}}
>
{t['Confirm']()}
</Button>
<Button
type="default"
shape="circle"
style={{ marginLeft: '24px' }}
onClick={() => {
setInput(workspace.blockSuiteWorkspace.meta.name ?? '');
setShowEditInput(false);
}}
>
{t['Cancel']()}
</Button>
</FlexWrapper>
</MuiFade>
<>
<Button
type="warning"
size="middle"
onClick={() => {
setShowLeave(true);
}}
>
{t['Leave']()}
</Button>
<WorkspaceLeave
open={showLeave}
onClose={() => {
setShowLeave(false);
}}
/>
</>
)}
</div>
</StyledRow>
{/* fixme(himself65): how to know a workspace owner by api? */}
{/*{!isOwner && (*/}
{/* <StyledRow>*/}
{/* <StyledSettingKey>{t('Workspace Owner')}</StyledSettingKey>*/}
{/* <FlexWrapper alignItems="center">*/}
{/* <MuiAvatar*/}
{/* sx={{ width: 72, height: 72, marginRight: '12px' }}*/}
{/* alt="owner avatar"*/}
{/* // src={currentWorkspace?.owner?.avatar}*/}
{/* >*/}
{/* <EmailIcon />*/}
{/* </MuiAvatar>*/}
{/* /!*<span>{currentWorkspace?.owner?.name}</span>*!/*/}
{/* </FlexWrapper>*/}
{/* </StyledRow>*/}
{/*)}*/}
{/*{!isOwner && (*/}
{/* <StyledRow>*/}
{/* <StyledSettingKey>{t('Members')}</StyledSettingKey>*/}
{/* <FlexWrapper alignItems="center">*/}
{/* /!*<span>{currentWorkspace?.memberCount}</span>*!/*/}
{/* </FlexWrapper>*/}
{/* </StyledRow>*/}
{/*)}*/}
<StyledRow
onClick={() => {
if (environment.isDesktop) {
window.apis.openDBFolder();
}
}}
>
<StyledSettingKey>{t['Workspace Type']()}</StyledSettingKey>
{isOwner ? (
workspace.flavour === WorkspaceFlavour.LOCAL ? (
<StyledWorkspaceInfo>
<LocalWorkspaceIcon />
<span>{t['Local Workspace']()}</span>
</StyledWorkspaceInfo>
) : (
<StyledWorkspaceInfo>
<CloudWorkspaceIcon />
<span>{t['Cloud Workspace']()}</span>
</StyledWorkspaceInfo>
)
) : (
<StyledWorkspaceInfo>
<JoinedWorkspaceIcon />
<span>{t['Joined Workspace']()}</span>
</StyledWorkspaceInfo>
)}
</StyledRow>
<StyledRow>
<StyledSettingKey> {t['Delete Workspace']()}</StyledSettingKey>
{isOwner ? (
<>
<Button
type="warning"
shape="circle"
style={{ borderRadius: '40px' }}
data-testid="delete-workspace-button"
onClick={() => {
setShowDelete(true);
}}
>
{t['Delete Workspace']()}
</Button>
<WorkspaceDeleteModal
onDeleteWorkspace={onDeleteWorkspace}
open={showDelete}
onClose={() => {
setShowDelete(false);
}}
workspace={workspace}
/>
</>
) : (
<>
<Button
type="warning"
shape="circle"
onClick={() => {
setShowLeave(true);
}}
>
{t['Leave Workspace']()}
</Button>
<WorkspaceLeave
open={showLeave}
onClose={() => {
setShowLeave(false);
}}
/>
</>
)}
</StyledRow>
</div>
</>
);
};

View File

@@ -4,7 +4,7 @@ import { Input } from '@affine/component';
export const StyledInput = styled(Input)(() => {
return {
border: '1px solid var(--affine-border-color)',
borderRadius: '10px',
borderRadius: '8px',
fontSize: 'var(--affine-font-sm)',
};
});

View File

@@ -1,115 +0,0 @@
import { styled } from '@affine/component';
import { FlexWrapper } from '@affine/component';
export const StyledSettingContainer = styled('div')(() => {
return {
display: 'flex',
flexDirection: 'column',
padding: '52px 0 0 52px',
height: 'calc(100vh - 52px)',
};
});
export const StyledSettingSidebar = styled('div')(() => {
{
return {
marginTop: '52px',
};
}
});
export const StyledSettingContent = styled('div')(() => {
return {
overflow: 'auto',
flex: 1,
paddingTop: '48px',
};
});
export const WorkspaceSettingTagItem = styled('li')<{ isActive?: boolean }>(
({ isActive }) => {
{
return {
display: 'flex',
margin: '0 48px 0 0',
height: '34px',
color: isActive
? 'var(--affine-primary-color)'
: 'var(--affine-text-primary-color)',
fontWeight: '500',
fontSize: 'var(--affine-font-h6)',
lineHeight: 'var(--affine-line-height)',
cursor: 'pointer',
transition: 'all 0.15s ease',
};
}
}
);
export const StyledSettingKey = styled('div')(() => {
return {
width: '140px',
fontSize: 'var(--affine-font-base)',
fontWeight: 500,
marginRight: '56px',
flexShrink: 0,
};
});
export const StyledRow = styled(FlexWrapper)(() => {
return {
marginBottom: '42px',
};
});
export const StyledWorkspaceName = styled('span')(() => {
return {
fontWeight: '400',
fontSize: 'var(--affine-font-h6)',
};
});
export const StyledIndicator = styled('div')(() => {
return {
height: '2px',
background: 'var(--affine-primary-color)',
position: 'absolute',
left: '0',
bottom: '0',
transition: 'left .3s, width .3s',
};
});
export const StyledTabButtonWrapper = styled('div')(() => {
return {
display: 'flex',
position: 'relative',
};
});
// export const StyledDownloadCard = styled('div')<{ active?: boolean }>(
// ({ theme, active }) => {
// return {
// width: '240px',
// height: '86px',
// border: '1px solid',
// borderColor: active
// ? 'var(--affine-primary-color)'
// : 'var(--affine-border-color)',
// borderRadius: '10px',
// padding: '8px 12px',
// position: 'relative',
// ':not(:last-of-type)': {
// marginRight: '24px',
// },
// svg: {
// display: active ? 'block' : 'none',
// ...positionAbsolute({ top: '-12px', right: '-12px' }),
// },
// };
// }
// );
// export const StyledDownloadCardDes = styled('div')(({ theme }) => {
// return {
// fontSize: 'var(--affine-font-sm)',
// color: 'var(--affine-icon-color)',
// };
// });

View File

@@ -1,123 +0,0 @@
import {
Button,
Input,
Modal,
ModalCloseButton,
ModalWrapper,
styled,
} from '@affine/component';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { KeyboardEvent } from 'react';
import { useCallback, useRef, useState } from 'react';
interface ModalProps {
open: boolean;
onClose: () => void;
onCreate: (name: string) => void;
}
export const CreateWorkspaceModal = ({
open,
onClose,
onCreate,
}: ModalProps) => {
const [workspaceName, setWorkspaceName] = useState('');
const isComposition = useRef(false);
const handleCreateWorkspace = useCallback(() => {
onCreate(workspaceName);
}, [onCreate, workspaceName]);
const handleKeyDown = useCallback(
(event: KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter' && workspaceName && !isComposition.current) {
handleCreateWorkspace();
}
},
[handleCreateWorkspace, workspaceName]
);
const t = useAFFiNEI18N();
return (
<Modal open={open} onClose={onClose}>
<ModalWrapper width={560} height={342} style={{ padding: '10px' }}>
<Header>
<ModalCloseButton
top={6}
right={6}
onClick={() => {
onClose();
}}
/>
</Header>
<Content>
<ContentTitle>{t['New Workspace']()}</ContentTitle>
<p>{t['Workspace description']()}</p>
<Input
ref={ref => {
if (ref) {
setTimeout(() => ref.focus(), 0);
}
}}
data-testid="create-workspace-input"
onKeyDown={handleKeyDown}
placeholder={t['Set a Workspace name']()}
maxLength={15}
minLength={0}
onChange={value => {
setWorkspaceName(value);
}}
onCompositionStart={() => {
isComposition.current = true;
}}
onCompositionEnd={() => {
isComposition.current = false;
}}
/>
<Button
data-testid="create-workspace-button"
disabled={!workspaceName}
style={{
width: '260px',
textAlign: 'center',
marginTop: '16px',
opacity: !workspaceName ? 0.5 : 1,
}}
type="primary"
onClick={() => {
handleCreateWorkspace();
}}
>
{t['Create']()}
</Button>
</Content>
</ModalWrapper>
</Modal>
);
};
const Header = styled('div')({
position: 'relative',
height: '44px',
});
const Content = styled('div')(() => {
return {
padding: '0 84px',
textAlign: 'center',
fontSize: '18px',
lineHeight: '26px',
p: {
marginTop: '12px',
marginBottom: '16px',
},
};
});
const ContentTitle = styled('div')(() => {
return {
fontSize: '20px',
lineHeight: '28px',
fontWeight: 600,
textAlign: 'center',
paddingBottom: '16px',
};
});

View File

@@ -1,4 +1,6 @@
import {
Menu,
MenuItem,
Modal,
ModalCloseButton,
ModalWrapper,
@@ -9,14 +11,19 @@ import { useAFFiNEI18N } from '@affine/i18n/hooks';
import type { AccessTokenMessage } from '@affine/workspace/affine/login';
import type { AffineWorkspace, LocalWorkspace } from '@affine/workspace/type';
import { WorkspaceFlavour } from '@affine/workspace/type';
import { HelpIcon, PlusIcon } from '@blocksuite/icons';
import { HelpIcon, ImportIcon, PlusIcon } from '@blocksuite/icons';
import type { DragEndEvent } from '@dnd-kit/core';
import { useCallback } from 'react';
import { useCallback, useRef } from 'react';
import type { AllWorkspace } from '../../../shared';
import { Footer } from '../footer';
import {
StyledCreateWorkspaceCard,
StyledCreateWorkspaceCardPill,
StyledCreateWorkspaceCardPillContainer,
StyledCreateWorkspaceCardPillContent,
StyledCreateWorkspaceCardPillIcon,
StyledCreateWorkspaceCardPillTextSecondary,
StyledHelperContainer,
StyledModalContent,
StyledModalHeader,
@@ -39,7 +46,8 @@ interface WorkspaceModalProps {
onClickWorkspaceSetting: (workspace: AllWorkspace) => void;
onClickLogin: () => void;
onClickLogout: () => void;
onCreateWorkspace: () => void;
onNewWorkspace: () => void;
onAddWorkspace: () => void;
onMoveWorkspace: (activeId: string, overId: string) => void;
}
@@ -53,12 +61,13 @@ export const WorkspaceListModal = ({
onClickLogout,
onClickWorkspace,
onClickWorkspaceSetting,
onCreateWorkspace,
onNewWorkspace,
onAddWorkspace,
currentWorkspaceId,
onMoveWorkspace,
}: WorkspaceModalProps) => {
const t = useAFFiNEI18N();
const anchorEL = useRef<HTMLDivElement>(null);
return (
<Modal open={open} onClose={onClose}>
<ModalWrapper
@@ -115,19 +124,96 @@ export const WorkspaceListModal = ({
[onMoveWorkspace]
)}
/>
<StyledCreateWorkspaceCard
data-testid="new-workspace"
onClick={onCreateWorkspace}
>
<StyleWorkspaceAdd className="add-icon">
<PlusIcon />
</StyleWorkspaceAdd>
{!environment.isDesktop && (
<StyledCreateWorkspaceCard
onClick={onNewWorkspace}
data-testid="new-workspace"
>
<StyleWorkspaceAdd className="add-icon">
<PlusIcon />
</StyleWorkspaceAdd>
<StyleWorkspaceInfo>
<StyleWorkspaceTitle>{t['New Workspace']()}</StyleWorkspaceTitle>
<p>{t['Create Or Import']()}</p>
</StyleWorkspaceInfo>
</StyledCreateWorkspaceCard>
<StyleWorkspaceInfo>
<StyleWorkspaceTitle>
{t['New Workspace']()}
</StyleWorkspaceTitle>
<p>{t['Create Or Import']()}</p>
</StyleWorkspaceInfo>
</StyledCreateWorkspaceCard>
)}
{environment.isDesktop && (
<Menu
placement="auto"
trigger={['click', 'hover']}
zIndex={1000}
content={
<StyledCreateWorkspaceCardPillContainer>
<StyledCreateWorkspaceCardPill>
<MenuItem
style={{
height: 'auto',
padding: '8px 12px',
}}
onClick={onNewWorkspace}
data-testid="new-workspace"
>
<StyledCreateWorkspaceCardPillContent>
<div>
<p>{t['New Workspace']()}</p>
<StyledCreateWorkspaceCardPillTextSecondary>
<p>{t['Create your own workspace']()}</p>
</StyledCreateWorkspaceCardPillTextSecondary>
</div>
<StyledCreateWorkspaceCardPillIcon>
<PlusIcon />
</StyledCreateWorkspaceCardPillIcon>
</StyledCreateWorkspaceCardPillContent>
</MenuItem>
</StyledCreateWorkspaceCardPill>
<StyledCreateWorkspaceCardPill>
<MenuItem
disabled={!environment.isDesktop}
onClick={onAddWorkspace}
data-testid="add-workspace"
style={{
height: 'auto',
padding: '8px 12px',
}}
>
<StyledCreateWorkspaceCardPillContent>
<div>
<p>{t['Add Workspace']()}</p>
<StyledCreateWorkspaceCardPillTextSecondary>
<p>{t['Add Workspace Hint']()}</p>
</StyledCreateWorkspaceCardPillTextSecondary>
</div>
<StyledCreateWorkspaceCardPillIcon>
<ImportIcon />
</StyledCreateWorkspaceCardPillIcon>
</StyledCreateWorkspaceCardPillContent>
</MenuItem>
</StyledCreateWorkspaceCardPill>
</StyledCreateWorkspaceCardPillContainer>
}
>
<StyledCreateWorkspaceCard
ref={anchorEL}
data-testid="add-or-new-workspace"
>
<StyleWorkspaceAdd className="add-icon">
<PlusIcon />
</StyleWorkspaceAdd>
<StyleWorkspaceInfo>
<StyleWorkspaceTitle>
{t['New Workspace']()}
</StyleWorkspaceTitle>
<p>{t['Create Or Import']()}</p>
</StyleWorkspaceInfo>
</StyledCreateWorkspaceCard>
</Menu>
)}
</StyledModalContent>
<Footer user={user} onLogin={onClickLogin} onLogout={onClickLogout} />

View File

@@ -64,6 +64,50 @@ export const StyledCreateWorkspaceCard = styled('div')(() => {
},
};
});
export const StyledCreateWorkspaceCardPillContainer = styled('div')(() => {
return {
padding: '12px',
borderRadius: '10px',
display: 'flex',
margin: '-8px -4px',
flexFlow: 'column',
gap: '12px',
background: 'var(--affine-background-overlay-panel-color)',
};
});
export const StyledCreateWorkspaceCardPill = styled('div')(() => {
return {
borderRadius: '5px',
display: 'flex',
boxShadow: '0px 0px 6px 0px rgba(0, 0, 0, 0.1)',
background: 'var(--affine-background-primary-color)',
};
});
export const StyledCreateWorkspaceCardPillContent = styled('div')(() => {
return {
display: 'flex',
gap: '12px',
alignItems: 'center',
justifyContent: 'space-between',
};
});
export const StyledCreateWorkspaceCardPillIcon = styled('div')(() => {
return {
fontSize: '20px',
width: '1em',
height: '1em',
};
});
export const StyledCreateWorkspaceCardPillTextSecondary = styled('div')(() => {
return {
fontSize: '12px',
color: 'var(--affine-text-secondary-color)',
};
});
export const StyledModalHeaderLeft = styled('div')(() => {
return { ...displayFlex('flex-start', 'center') };

View File

@@ -77,7 +77,7 @@ export const RootAppSidebar = ({
const sidebarOpen = useAtomValue(appSidebarOpenAtom);
useEffect(() => {
if (environment.isDesktop && typeof sidebarOpen === 'boolean') {
window.apis?.onSidebarVisibilityChange(sidebarOpen);
window.apis?.ui.handleSidebarVisibilityChange(sidebarOpen);
}
}, [sidebarOpen]);
const [ref, setRef] = useState<HTMLElement | null>(null);