mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 20:38:52 +00:00
feat: create workspace from loading existing exported file (#2122)
Co-authored-by: Himself65 <himself65@outlook.com>
This commit is contained in:
@@ -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',
|
||||
});
|
||||
346
apps/web/src/components/affine/create-workspace-modal/index.tsx
Normal file
346
apps/web/src/components/affine/create-workspace-modal/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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)',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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)',
|
||||
// };
|
||||
// });
|
||||
@@ -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',
|
||||
};
|
||||
});
|
||||
@@ -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} />
|
||||
|
||||
@@ -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') };
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user