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
@@ -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)',
});
@@ -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>
);
};
@@ -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']()}
@@ -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 (
@@ -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>
</>
);
};
@@ -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)',
};
});
@@ -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)',
// };
// });