mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-19 02:51:47 +08:00
merge master
This commit is contained in:
Vendored
+1
-1
@@ -3,5 +3,5 @@
|
|||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"editor.formatOnSaveMode": "file",
|
"editor.formatOnSaveMode": "file",
|
||||||
"cSpell.words": ["blocksuite", "livedemo", "pnpm", "testid"]
|
"cSpell.words": ["blocksuite", "datacenter", "livedemo", "pnpm", "testid"]
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -7,12 +7,12 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "pnpm --filter=!@affine/app build && pnpm --filter @affine/app dev",
|
"dev": "pnpm --filter=!@affine/app build && pnpm --filter @affine/app dev",
|
||||||
"dev:ac": "pnpm --filter=!@affine/app build && pnpm --filter @affine/app dev:ac",
|
"dev:ac": "pnpm --filter=!@affine/app build && pnpm --filter @affine/app dev:ac",
|
||||||
"build": " pnpm --filter=!@affine/app build && pnpm --filter!=@affine/data-services -r build",
|
"build": " pnpm --filter=!@affine/app build && pnpm --filter!=@affine/datacenter -r build",
|
||||||
"export": "pnpm --filter @affine/app export",
|
"export": "pnpm --filter @affine/app export",
|
||||||
"start": "pnpm --filter @affine/app start",
|
"start": "pnpm --filter @affine/app start",
|
||||||
"lint": "pnpm --filter @affine/app lint",
|
"lint": "pnpm --filter @affine/app lint",
|
||||||
"test": "playwright test",
|
"test": "playwright test",
|
||||||
"test:dc": "pnpm --filter @affine/data-services test",
|
"test:dc": "pnpm --filter @affine/datacenter test",
|
||||||
"test:e2e:codegen": "npx playwright codegen http://localhost:8080",
|
"test:e2e:codegen": "npx playwright codegen http://localhost:8080",
|
||||||
"test:unit": "jest",
|
"test:unit": "jest",
|
||||||
"postinstall": "husky install",
|
"postinstall": "husky install",
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ const nextConfig = {
|
|||||||
EDITOR_VERSION: dependencies['@blocksuite/editor'],
|
EDITOR_VERSION: dependencies['@blocksuite/editor'],
|
||||||
},
|
},
|
||||||
webpack: config => {
|
webpack: config => {
|
||||||
|
config.experiments = { ...config.experiments, topLevelAwait: true };
|
||||||
config.resolve.alias['yjs'] = require.resolve('yjs');
|
config.resolve.alias['yjs'] = require.resolve('yjs');
|
||||||
config.module.rules.push({
|
config.module.rules.push({
|
||||||
test: /\.md$/i,
|
test: /\.md$/i,
|
||||||
|
|||||||
@@ -10,11 +10,11 @@
|
|||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@affine/data-services": "workspace:*",
|
"@affine/datacenter": "workspace:*",
|
||||||
"@blocksuite/blocks": "0.3.0-20221230100352-5dfe65e",
|
"@blocksuite/blocks": "0.3.1",
|
||||||
"@blocksuite/editor": "0.3.0-20221230100352-5dfe65e",
|
"@blocksuite/editor": "0.3.1",
|
||||||
"@blocksuite/icons": "^2.0.2",
|
"@blocksuite/icons": "^2.0.2",
|
||||||
"@blocksuite/store": "0.3.0-20221230100352-5dfe65e",
|
"@blocksuite/store": "0.3.1",
|
||||||
"@emotion/css": "^11.10.0",
|
"@emotion/css": "^11.10.0",
|
||||||
"@emotion/react": "^11.10.4",
|
"@emotion/react": "^11.10.4",
|
||||||
"@emotion/server": "^11.10.0",
|
"@emotion/server": "^11.10.0",
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
*.js
|
||||||
|
*.map
|
||||||
@@ -87,7 +87,7 @@ const PopoverContent = () => {
|
|||||||
confirmType: 'danger',
|
confirmType: 'danger',
|
||||||
}).then(confirm => {
|
}).then(confirm => {
|
||||||
confirm && toggleDeletePage(id);
|
confirm && toggleDeletePage(id);
|
||||||
toast('Moved to Trash');
|
confirm && toast('Moved to Trash');
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
icon={<TrashIcon />}
|
icon={<TrashIcon />}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Modal, ModalWrapper, ModalCloseButton } from '@/ui/modal';
|
|||||||
import { Button } from '@/ui/button';
|
import { Button } from '@/ui/button';
|
||||||
import Input from '@/ui/input';
|
import Input from '@/ui/input';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { inviteMember, getUserByEmail } from '@affine/data-services';
|
import { getDataCenter } from '@affine/datacenter';
|
||||||
import { Avatar } from '@mui/material';
|
import { Avatar } from '@mui/material';
|
||||||
interface LoginModalProps {
|
interface LoginModalProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -60,15 +60,19 @@ export const InviteMembers = ({
|
|||||||
setShowTip(false);
|
setShowTip(false);
|
||||||
debounce(
|
debounce(
|
||||||
() => {
|
() => {
|
||||||
getUserByEmail({
|
getDataCenter()
|
||||||
email: value,
|
.then(dc =>
|
||||||
workspace_id: workspaceId,
|
dc.apis.getUserByEmail({
|
||||||
}).then(data => {
|
email: value,
|
||||||
if (data?.name) {
|
workspace_id: workspaceId,
|
||||||
setUserData(data);
|
})
|
||||||
setShowTip(false);
|
)
|
||||||
}
|
.then(data => {
|
||||||
});
|
if (data?.name) {
|
||||||
|
setUserData(data);
|
||||||
|
setShowTip(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
300,
|
300,
|
||||||
true
|
true
|
||||||
@@ -130,7 +134,8 @@ export const InviteMembers = ({
|
|||||||
shape="circle"
|
shape="circle"
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
inviteMember({ id: workspaceId, email: email })
|
getDataCenter()
|
||||||
|
.then(dc => dc.apis.inviteMember({ id: workspaceId, email }))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
onClose();
|
onClose();
|
||||||
onInviteSuccess && onInviteSuccess();
|
onInviteSuccess && onInviteSuccess();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { signInWithGoogle } from '@affine/data-services';
|
import { getDataCenter } from '@affine/datacenter';
|
||||||
import { styled } from '@/styles';
|
import { styled } from '@/styles';
|
||||||
import { Button } from '@/ui/button';
|
import { Button } from '@/ui/button';
|
||||||
import { useModal } from '@/providers/global-modal-provider';
|
import { useModal } from '@/providers/global-modal-provider';
|
||||||
@@ -9,7 +9,8 @@ export const GoogleLoginButton = () => {
|
|||||||
return (
|
return (
|
||||||
<StyledGoogleButton
|
<StyledGoogleButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
signInWithGoogle()
|
getDataCenter()
|
||||||
|
.then(dc => dc.apis.signInWithGoogle?.())
|
||||||
.then(() => {
|
.then(() => {
|
||||||
triggerLoginModal();
|
triggerLoginModal();
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { ModalCloseButton } from '@/ui/modal';
|
import { ModalCloseButton } from '@/ui/modal';
|
||||||
import { Button } from '@/ui/button';
|
import { Button } from '@/ui/button';
|
||||||
import { deleteWorkspace } from '@affine/data-services';
|
import { getDataCenter } from '@affine/datacenter';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useAppState } from '@/providers/app-state-provider';
|
import { useAppState } from '@/providers/app-state-provider';
|
||||||
|
|
||||||
@@ -39,7 +39,8 @@ export const WorkspaceDelete = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
await deleteWorkspace({ id: workspaceId });
|
const dc = await getDataCenter();
|
||||||
|
await dc.apis.deleteWorkspace({ id: workspaceId });
|
||||||
router.push(`/workspace/${nextWorkSpaceId}`);
|
router.push(`/workspace/${nextWorkSpaceId}`);
|
||||||
refreshWorkspacesMeta();
|
refreshWorkspacesMeta();
|
||||||
onClose();
|
onClose();
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { StyledSettingH2 } from '../style';
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Button } from '@/ui/button';
|
import { Button } from '@/ui/button';
|
||||||
import Input from '@/ui/input';
|
import Input from '@/ui/input';
|
||||||
import { uploadBlob, Workspace, WorkspaceType } from '@affine/data-services';
|
import { getDataCenter, Workspace, WorkspaceType } from '@affine/datacenter';
|
||||||
import { useAppState } from '@/providers/app-state-provider';
|
import { useAppState } from '@/providers/app-state-provider';
|
||||||
import { WorkspaceDetails } from '@/components/workspace-slider-bar/WorkspaceSelector/SelectorPopperContent';
|
import { WorkspaceDetails } from '@/components/workspace-slider-bar/WorkspaceSelector/SelectorPopperContent';
|
||||||
import { WorkspaceDelete } from './delete';
|
import { WorkspaceDelete } from './delete';
|
||||||
@@ -74,9 +74,11 @@ export const GeneralPage = ({
|
|||||||
const fileChange = async (file: File) => {
|
const fileChange = async (file: File) => {
|
||||||
setUploading(true);
|
setUploading(true);
|
||||||
const blob = new Blob([file], { type: file.type });
|
const blob = new Blob([file], { type: file.type });
|
||||||
const blobId = await uploadBlob({ blob }).finally(() => {
|
const blobId = await getDataCenter()
|
||||||
setUploading(false);
|
.then(dc => dc.apis.uploadBlob({ blob }))
|
||||||
});
|
.finally(() => {
|
||||||
|
setUploading(false);
|
||||||
|
});
|
||||||
if (blobId) {
|
if (blobId) {
|
||||||
currentWorkspace?.meta.setAvatar(blobId);
|
currentWorkspace?.meta.setAvatar(blobId);
|
||||||
workspaces[workspace.id]?.meta.setAvatar(blobId);
|
workspaces[workspace.id]?.meta.setAvatar(blobId);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
} from './style';
|
} from './style';
|
||||||
import { ModalCloseButton } from '@/ui/modal';
|
import { ModalCloseButton } from '@/ui/modal';
|
||||||
import { Button } from '@/ui/button';
|
import { Button } from '@/ui/button';
|
||||||
import { leaveWorkspace } from '@affine/data-services';
|
import { getDataCenter } from '@affine/datacenter';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useAppState } from '@/providers/app-state-provider';
|
import { useAppState } from '@/providers/app-state-provider';
|
||||||
|
|
||||||
@@ -28,7 +28,8 @@ export const WorkspaceLeave = ({
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { refreshWorkspacesMeta } = useAppState();
|
const { refreshWorkspacesMeta } = useAppState();
|
||||||
const handleLeave = async () => {
|
const handleLeave = async () => {
|
||||||
await leaveWorkspace({ id: workspaceId });
|
const dc = await getDataCenter();
|
||||||
|
await dc.apis.leaveWorkspace({ id: workspaceId });
|
||||||
router.push(`/workspace/${nextWorkSpaceId}`);
|
router.push(`/workspace/${nextWorkSpaceId}`);
|
||||||
refreshWorkspacesMeta();
|
refreshWorkspacesMeta();
|
||||||
onClose();
|
onClose();
|
||||||
|
|||||||
@@ -36,13 +36,7 @@ import { useCallback, useEffect, useState } from 'react';
|
|||||||
import { Button, IconButton } from '@/ui/button';
|
import { Button, IconButton } from '@/ui/button';
|
||||||
import Input from '@/ui/input';
|
import Input from '@/ui/input';
|
||||||
import { InviteMembers } from '../invite-members/index';
|
import { InviteMembers } from '../invite-members/index';
|
||||||
import {
|
import { Workspace, Member, getDataCenter } from '@affine/datacenter';
|
||||||
getWorkspaceMembers,
|
|
||||||
Workspace,
|
|
||||||
Member,
|
|
||||||
removeMember,
|
|
||||||
updateWorkspace,
|
|
||||||
} from '@affine/data-services';
|
|
||||||
import { Avatar } from '@mui/material';
|
import { Avatar } from '@mui/material';
|
||||||
import { Menu, MenuItem } from '@/ui/menu';
|
import { Menu, MenuItem } from '@/ui/menu';
|
||||||
import { toast } from '@/ui/toast';
|
import { toast } from '@/ui/toast';
|
||||||
@@ -169,9 +163,12 @@ const MembersPage = ({ workspace }: { workspace: Workspace }) => {
|
|||||||
const [isInviteModalShow, setIsInviteModalShow] = useState(false);
|
const [isInviteModalShow, setIsInviteModalShow] = useState(false);
|
||||||
const [members, setMembers] = useState<Member[]>([]);
|
const [members, setMembers] = useState<Member[]>([]);
|
||||||
const refreshMembers = useCallback(() => {
|
const refreshMembers = useCallback(() => {
|
||||||
getWorkspaceMembers({
|
getDataCenter()
|
||||||
id: workspace.id,
|
.then(dc =>
|
||||||
})
|
dc.apis.getWorkspaceMembers({
|
||||||
|
id: workspace.id,
|
||||||
|
})
|
||||||
|
)
|
||||||
.then(data => {
|
.then(data => {
|
||||||
setMembers(data);
|
setMembers(data);
|
||||||
})
|
})
|
||||||
@@ -236,13 +233,17 @@ const MembersPage = ({ workspace }: { workspace: Workspace }) => {
|
|||||||
// confirmText: 'Delete',
|
// confirmText: 'Delete',
|
||||||
// confirmType: 'danger',
|
// confirmType: 'danger',
|
||||||
// }).then(confirm => {
|
// }).then(confirm => {
|
||||||
removeMember({
|
getDataCenter()
|
||||||
permissionId: member.id,
|
.then(dc =>
|
||||||
}).then(() => {
|
dc.apis.removeMember({
|
||||||
// console.log('data: ', data);
|
permissionId: member.id,
|
||||||
toast('Moved to Trash');
|
})
|
||||||
refreshMembers();
|
)
|
||||||
});
|
.then(() => {
|
||||||
|
// console.log('data: ', data);
|
||||||
|
toast('Moved to Trash');
|
||||||
|
refreshMembers();
|
||||||
|
});
|
||||||
// });
|
// });
|
||||||
}}
|
}}
|
||||||
icon={<TrashIcon />}
|
icon={<TrashIcon />}
|
||||||
@@ -297,13 +298,17 @@ const PublishPage = ({ workspace }: { workspace: Workspace }) => {
|
|||||||
workspace.public
|
workspace.public
|
||||||
);
|
);
|
||||||
const togglePublic = (flag: boolean) => {
|
const togglePublic = (flag: boolean) => {
|
||||||
updateWorkspace({
|
getDataCenter()
|
||||||
id: workspace.id,
|
.then(dc =>
|
||||||
public: flag,
|
dc.apis.updateWorkspace({
|
||||||
}).then(data => {
|
id: workspace.id,
|
||||||
setPublicStatus(data?.public);
|
public: flag,
|
||||||
toast('Updated Public Status Success');
|
})
|
||||||
});
|
)
|
||||||
|
.then(data => {
|
||||||
|
setPublicStatus(data?.public);
|
||||||
|
toast('Updated Public Status Success');
|
||||||
|
});
|
||||||
};
|
};
|
||||||
const copyUrl = () => {
|
const copyUrl = () => {
|
||||||
navigator.clipboard.writeText(shareUrl);
|
navigator.clipboard.writeText(shareUrl);
|
||||||
|
|||||||
+3
-2
@@ -12,7 +12,7 @@ import {
|
|||||||
} from './WorkspaceItem';
|
} from './WorkspaceItem';
|
||||||
import { WorkspaceSetting } from '@/components/workspace-setting';
|
import { WorkspaceSetting } from '@/components/workspace-setting';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { getWorkspaceDetail, WorkspaceType } from '@affine/data-services';
|
import { getDataCenter, WorkspaceType } from '@affine/datacenter';
|
||||||
import { useModal } from '@/providers/global-modal-provider';
|
import { useModal } from '@/providers/global-modal-provider';
|
||||||
|
|
||||||
export type WorkspaceDetails = Record<
|
export type WorkspaceDetails = Record<
|
||||||
@@ -54,7 +54,8 @@ export const SelectorPopperContent = ({
|
|||||||
if (type === WorkspaceType.Private) {
|
if (type === WorkspaceType.Private) {
|
||||||
return { id, member_count: 1, owner: user };
|
return { id, member_count: 1, owner: user };
|
||||||
} else {
|
} else {
|
||||||
const data = await getWorkspaceDetail({ id });
|
const dc = await getDataCenter();
|
||||||
|
const data = await dc.apis.getWorkspaceDetail({ id });
|
||||||
return { id, ...data } || { id, member_count: 0, owner: user };
|
return { id, ...data } || { id, member_count: 0, owner: user };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-3
@@ -1,4 +1,4 @@
|
|||||||
import { createWorkspace, uploadBlob } from '@affine/data-services';
|
import { getDataCenter } from '@affine/datacenter';
|
||||||
import Modal from '@/ui/modal';
|
import Modal from '@/ui/modal';
|
||||||
import Input from '@/ui/input';
|
import Input from '@/ui/input';
|
||||||
import {
|
import {
|
||||||
@@ -52,7 +52,9 @@ export const WorkspaceCreate = ({ open, onClose }: WorkspaceCreateProps) => {
|
|||||||
ctx.fillText(workspaceName[0], 50, 50);
|
ctx.fillText(workspaceName[0], 50, 50);
|
||||||
canvas.toBlob(blob => {
|
canvas.toBlob(blob => {
|
||||||
if (blob) {
|
if (blob) {
|
||||||
const blobId = uploadBlob({ blob });
|
const blobId = getDataCenter().then(dc =>
|
||||||
|
dc.apis.uploadBlob({ blob })
|
||||||
|
);
|
||||||
resolve(blobId);
|
resolve(blobId);
|
||||||
} else {
|
} else {
|
||||||
reject();
|
reject();
|
||||||
@@ -69,7 +71,10 @@ export const WorkspaceCreate = ({ open, onClose }: WorkspaceCreateProps) => {
|
|||||||
setCreating(false);
|
setCreating(false);
|
||||||
});
|
});
|
||||||
if (blobId) {
|
if (blobId) {
|
||||||
createWorkspace({ name: workspaceName, avatar: blobId })
|
getDataCenter()
|
||||||
|
.then(dc =>
|
||||||
|
dc.apis.createWorkspace({ name: workspaceName, avatar: blobId })
|
||||||
|
)
|
||||||
.then(async data => {
|
.then(async data => {
|
||||||
await refreshWorkspacesMeta();
|
await refreshWorkspacesMeta();
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@ import {
|
|||||||
} from '../styles';
|
} from '../styles';
|
||||||
import { FooterSetting } from './FooterSetting';
|
import { FooterSetting } from './FooterSetting';
|
||||||
import { FooterUsers } from './FooterUsers';
|
import { FooterUsers } from './FooterUsers';
|
||||||
import { WorkspaceType } from '@affine/data-services';
|
import { WorkspaceType } from '@affine/datacenter';
|
||||||
import { useAppState } from '@/providers/app-state-provider';
|
import { useAppState } from '@/providers/app-state-provider';
|
||||||
|
|
||||||
interface WorkspaceItemProps {
|
interface WorkspaceItemProps {
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@ import { Avatar, WorkspaceName, SelectorWrapper } from './styles';
|
|||||||
import { SelectorPopperContent } from './SelectorPopperContent';
|
import { SelectorPopperContent } from './SelectorPopperContent';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useAppState } from '@/providers/app-state-provider';
|
import { useAppState } from '@/providers/app-state-provider';
|
||||||
import { WorkspaceType } from '@affine/data-services';
|
import { WorkspaceType } from '@affine/datacenter';
|
||||||
import { AffineIcon } from '../icons/icons';
|
import { AffineIcon } from '../icons/icons';
|
||||||
|
|
||||||
export const WorkspaceSelector = () => {
|
export const WorkspaceSelector = () => {
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ export const AffineIcon = () => {
|
|||||||
fill="#FFF"
|
fill="#FFF"
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
fill-rule="evenodd"
|
fillRule="evenodd"
|
||||||
clip-rule="evenodd"
|
clipRule="evenodd"
|
||||||
d="M18.6303 8.79688L11.2559 29.8393H15.5752L20.2661 15.2858L24.959 29.8393H29.2637L21.8881 8.79688H18.6303Z"
|
d="M18.6303 8.79688L11.2559 29.8393H15.5752L20.2661 15.2858L24.959 29.8393H29.2637L21.8881 8.79688H18.6303Z"
|
||||||
fill="#6880FF"
|
fill="#6880FF"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
StyledListItem,
|
StyledListItem,
|
||||||
StyledListItemForWorkspace,
|
StyledListItemForWorkspace,
|
||||||
StyledNewPageButton,
|
StyledNewPageButton,
|
||||||
StyledQuickSearch,
|
|
||||||
StyledSliderBar,
|
StyledSliderBar,
|
||||||
StyledSliderBarWrapper,
|
StyledSliderBarWrapper,
|
||||||
StyledSubListItem,
|
StyledSubListItem,
|
||||||
@@ -32,11 +31,7 @@ import { IconButton } from '@/ui/button';
|
|||||||
import useLocalStorage from '@/hooks/use-local-storage';
|
import useLocalStorage from '@/hooks/use-local-storage';
|
||||||
import usePageMetaList from '@/hooks/use-page-meta-list';
|
import usePageMetaList from '@/hooks/use-page-meta-list';
|
||||||
import { usePageHelper } from '@/hooks/use-page-helper';
|
import { usePageHelper } from '@/hooks/use-page-helper';
|
||||||
import { getUaHelper } from '@/utils';
|
|
||||||
|
|
||||||
const isMac = () => {
|
|
||||||
return getUaHelper().isMacOs;
|
|
||||||
};
|
|
||||||
const FavoriteList = ({ showList }: { showList: boolean }) => {
|
const FavoriteList = ({ showList }: { showList: boolean }) => {
|
||||||
const { openPage } = usePageHelper();
|
const { openPage } = usePageHelper();
|
||||||
const pageList = usePageMetaList();
|
const pageList = usePageMetaList();
|
||||||
@@ -117,7 +112,7 @@ export const WorkSpaceSliderBar = () => {
|
|||||||
<StyledListItemForWorkspace>
|
<StyledListItemForWorkspace>
|
||||||
<WorkspaceSelector />
|
<WorkspaceSelector />
|
||||||
</StyledListItemForWorkspace>
|
</StyledListItemForWorkspace>
|
||||||
<StyledQuickSearch
|
<StyledListItem
|
||||||
data-testid="sliderBar-quickSearchButton"
|
data-testid="sliderBar-quickSearchButton"
|
||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: 'pointer' }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -126,8 +121,7 @@ export const WorkSpaceSliderBar = () => {
|
|||||||
>
|
>
|
||||||
<SearchIcon />
|
<SearchIcon />
|
||||||
Quick search
|
Quick search
|
||||||
<span>{isMac() ? '⌘ + K' : 'Ctrl + K'}</span>
|
</StyledListItem>
|
||||||
</StyledQuickSearch>
|
|
||||||
<Link href={{ pathname: paths.all }}>
|
<Link href={{ pathname: paths.all }}>
|
||||||
<StyledListItem active={router.asPath === paths.all}>
|
<StyledListItem active={router.asPath === paths.all}>
|
||||||
<AllPagesIcon /> <span>All pages</span>
|
<AllPagesIcon /> <span>All pages</span>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export const StyledSliderBarWrapper = styled.div(() => {
|
|||||||
height: '100%',
|
height: '100%',
|
||||||
overflowX: 'hidden',
|
overflowX: 'hidden',
|
||||||
overflowY: 'auto',
|
overflowY: 'auto',
|
||||||
|
position: 'relative',
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -141,34 +142,3 @@ export const StyledSubListItem = styled.button<{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
export const StyledQuickSearch = styled.div(({ theme }) => {
|
|
||||||
return {
|
|
||||||
width: '296px',
|
|
||||||
height: '32px',
|
|
||||||
marginTop: '12px',
|
|
||||||
fontSize: theme.font.sm,
|
|
||||||
backgroundColor: theme.colors.hoverBackground,
|
|
||||||
color: theme.colors.popoverColor,
|
|
||||||
paddingLeft: '12px',
|
|
||||||
borderRadius: '5px',
|
|
||||||
...displayFlex('flex-start', 'center'),
|
|
||||||
'>svg': {
|
|
||||||
fontSize: '20px',
|
|
||||||
marginRight: '12px',
|
|
||||||
},
|
|
||||||
'>span': {
|
|
||||||
fontSize: theme.font.xs,
|
|
||||||
margin: 'auto',
|
|
||||||
marginRight: '12px',
|
|
||||||
color: theme.colors.hoverBackground,
|
|
||||||
transition: 'all .15s',
|
|
||||||
},
|
|
||||||
':hover': {
|
|
||||||
color: theme.colors.popoverColor,
|
|
||||||
'>span': {
|
|
||||||
color: theme.colors.popoverColor,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -30,18 +30,18 @@ export const useEnsureWorkspace = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// If user is not login and input a custom workspaceId, jump to 404 page
|
// If user is not login and input a custom workspaceId, jump to 404 page
|
||||||
if (
|
// if (
|
||||||
!user &&
|
// !user &&
|
||||||
router.query.workspaceId &&
|
// router.query.workspaceId &&
|
||||||
router.query.workspaceId !== defaultOutLineWorkspaceId
|
// router.query.workspaceId !== defaultOutLineWorkspaceId
|
||||||
) {
|
// ) {
|
||||||
router.push('/404');
|
// router.push('/404');
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
const workspaceId = user
|
const workspaceId = user
|
||||||
? (router.query.workspaceId as string) || workspacesMeta?.[0]?.id
|
? (router.query.workspaceId as string) || workspacesMeta[0]?.id
|
||||||
: defaultOutLineWorkspaceId;
|
: (router.query.workspaceId as string) || defaultOutLineWorkspaceId;
|
||||||
|
|
||||||
loadWorkspace(workspaceId).finally(() => {
|
loadWorkspace(workspaceId).finally(() => {
|
||||||
setWorkspaceLoaded(true);
|
setWorkspaceLoaded(true);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { styled } from '@/styles';
|
import { styled } from '@/styles';
|
||||||
import { Empty } from '@/ui/empty';
|
import { Empty } from '@/ui/empty';
|
||||||
import { Avatar } from '@mui/material';
|
import { Avatar } from '@mui/material';
|
||||||
import { acceptInviting } from '@affine/data-services';
|
import { getDataCenter } from '@affine/datacenter';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
@@ -24,7 +24,12 @@ export default function DevPage() {
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const [inviteData, setInviteData] = useState<any>(null);
|
const [inviteData, setInviteData] = useState<any>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
acceptInviting({ invitingCode: router.query.invite_code as string })
|
getDataCenter()
|
||||||
|
.then(dc =>
|
||||||
|
dc.apis.acceptInviting({
|
||||||
|
invitingCode: router.query.invite_code as string,
|
||||||
|
})
|
||||||
|
)
|
||||||
.then(data => {
|
.then(data => {
|
||||||
setSuccessInvited(true);
|
setSuccessInvited(true);
|
||||||
setInviteData(data);
|
setInviteData(data);
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ const Page: NextPageWithLayout = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.title = currentPage?.meta.title || 'Untitled';
|
||||||
return ret;
|
return ret;
|
||||||
}, [currentWorkspace, currentPage, createEditor, setEditor]);
|
}, [currentWorkspace, currentPage, createEditor, setEditor]);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { createContext, MutableRefObject, useContext } from 'react';
|
import { createContext, MutableRefObject, useContext } from 'react';
|
||||||
import type { Workspace } from '@affine/data-services';
|
import type { AccessTokenMessage, Workspace } from '@affine/datacenter';
|
||||||
import { AccessTokenMessage } from '@affine/data-services';
|
|
||||||
import type {
|
import type {
|
||||||
Page as StorePage,
|
Page as StorePage,
|
||||||
Workspace as StoreWorkspace,
|
Workspace as StoreWorkspace,
|
||||||
@@ -8,7 +7,6 @@ import type {
|
|||||||
import type { EditorContainer } from '@blocksuite/editor';
|
import type { EditorContainer } from '@blocksuite/editor';
|
||||||
export type LoadWorkspaceHandler = (
|
export type LoadWorkspaceHandler = (
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
websocket?: boolean,
|
|
||||||
user?: AccessTokenMessage | null
|
user?: AccessTokenMessage | null
|
||||||
) => Promise<StoreWorkspace | null> | null;
|
) => Promise<StoreWorkspace | null> | null;
|
||||||
export type CreateEditorHandler = (page: StorePage) => EditorContainer | null;
|
export type CreateEditorHandler = (page: StorePage) => EditorContainer | null;
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import type { Page } from '@blocksuite/store';
|
import type { Page } from '@blocksuite/store';
|
||||||
import {
|
|
||||||
IndexedDBDocProvider,
|
|
||||||
Workspace as StoreWorkspace,
|
|
||||||
} from '@blocksuite/store';
|
|
||||||
import '@blocksuite/blocks';
|
import '@blocksuite/blocks';
|
||||||
import { EditorContainer } from '@blocksuite/editor';
|
import { EditorContainer } from '@blocksuite/editor';
|
||||||
import { BlockSchema } from '@blocksuite/blocks/models';
|
|
||||||
import type { LoadWorkspaceHandler, CreateEditorHandler } from './context';
|
import type { LoadWorkspaceHandler, CreateEditorHandler } from './context';
|
||||||
|
import { getDataCenter } from '@affine/datacenter';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
setLoadWorkspaceHandler: (handler: LoadWorkspaceHandler) => void;
|
setLoadWorkspaceHandler: (handler: LoadWorkspaceHandler) => void;
|
||||||
@@ -19,66 +15,14 @@ const DynamicBlocksuite = ({
|
|||||||
setCreateEditorHandler,
|
setCreateEditorHandler,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const openWorkspace: LoadWorkspaceHandler = (
|
const openWorkspace: LoadWorkspaceHandler = async (workspaceId: string) => {
|
||||||
workspaceId: string,
|
if (workspaceId) {
|
||||||
websocket = false,
|
const dc = await getDataCenter();
|
||||||
user
|
return dc.load(workspaceId, { providerId: 'affine' });
|
||||||
) =>
|
} else {
|
||||||
// eslint-disable-next-line no-async-promise-executor
|
return null;
|
||||||
new Promise(async resolve => {
|
}
|
||||||
const workspace = new StoreWorkspace({
|
};
|
||||||
room: workspaceId,
|
|
||||||
providers: [IndexedDBDocProvider],
|
|
||||||
}).register(BlockSchema);
|
|
||||||
console.log('websocket', websocket);
|
|
||||||
console.log('user', user);
|
|
||||||
|
|
||||||
// if (websocket && token.refresh) {
|
|
||||||
// // FIXME: if add websocket provider, the first page will be blank
|
|
||||||
// const ws = new WebsocketProvider(
|
|
||||||
// `ws${window.location.protocol === 'https:' ? 's' : ''}://${
|
|
||||||
// window.location.host
|
|
||||||
// }/api/sync/`,
|
|
||||||
// workspaceId,
|
|
||||||
// workspace.doc,
|
|
||||||
// {
|
|
||||||
// params: {
|
|
||||||
// token: token.refresh,
|
|
||||||
// },
|
|
||||||
// awareness: workspace.meta.awareness.awareness,
|
|
||||||
// }
|
|
||||||
// );
|
|
||||||
//
|
|
||||||
// ws.shouldConnect = false;
|
|
||||||
//
|
|
||||||
// // FIXME: there needs some method to destroy websocket.
|
|
||||||
// // Or we need a manager to manage websocket.
|
|
||||||
// // eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// // @ts-expect-error
|
|
||||||
// workspace.__ws__ = ws;
|
|
||||||
// }
|
|
||||||
|
|
||||||
const indexDBProvider = workspace.providers.find(
|
|
||||||
p => p instanceof IndexedDBDocProvider
|
|
||||||
);
|
|
||||||
// if (user) {
|
|
||||||
// const updates = await downloadWorkspace({ workspaceId });
|
|
||||||
// updates &&
|
|
||||||
// StoreWorkspace.Y.applyUpdate(
|
|
||||||
// workspace.doc,
|
|
||||||
// new Uint8Array(updates)
|
|
||||||
// );
|
|
||||||
// // if after update, the space:meta is empty, then we need to get map with doc
|
|
||||||
// workspace.doc.getMap('space:meta');
|
|
||||||
// }
|
|
||||||
if (indexDBProvider) {
|
|
||||||
(indexDBProvider as IndexedDBDocProvider).whenSynced.then(() => {
|
|
||||||
resolve(workspace);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
resolve(workspace);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
setLoadWorkspaceHandler(openWorkspace);
|
setLoadWorkspaceHandler(openWorkspace);
|
||||||
}, [setLoadWorkspaceHandler]);
|
}, [setLoadWorkspaceHandler]);
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
import { useEffect } from 'react';
|
|
||||||
import {
|
|
||||||
AccessTokenMessage,
|
|
||||||
getWorkspaces,
|
|
||||||
token,
|
|
||||||
} from '@affine/data-services';
|
|
||||||
import { LoadWorkspaceHandler } from '../context';
|
|
||||||
|
|
||||||
export const useSyncData = ({
|
|
||||||
loadWorkspaceHandler,
|
|
||||||
}: {
|
|
||||||
loadWorkspaceHandler: LoadWorkspaceHandler;
|
|
||||||
}) => {
|
|
||||||
useEffect(() => {
|
|
||||||
if (!loadWorkspaceHandler) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const start = async () => {
|
|
||||||
const isLogin = await token.refreshToken().catch(() => false);
|
|
||||||
return isLogin;
|
|
||||||
};
|
|
||||||
start();
|
|
||||||
|
|
||||||
const callback = async (user: AccessTokenMessage | null) => {
|
|
||||||
const workspacesMeta = user
|
|
||||||
? await getWorkspaces().catch(() => {
|
|
||||||
return [];
|
|
||||||
})
|
|
||||||
: [];
|
|
||||||
// setState(state => ({
|
|
||||||
// ...state,
|
|
||||||
// user: user,
|
|
||||||
// workspacesMeta,
|
|
||||||
// synced: true,
|
|
||||||
// }));
|
|
||||||
return workspacesMeta;
|
|
||||||
};
|
|
||||||
|
|
||||||
token.onChange(callback);
|
|
||||||
token.refreshToken().catch(err => {
|
|
||||||
// FIXME: should resolve invalid refresh token
|
|
||||||
console.log(err);
|
|
||||||
});
|
|
||||||
return () => {
|
|
||||||
token.offChange(callback);
|
|
||||||
};
|
|
||||||
}, [loadWorkspaceHandler]);
|
|
||||||
};
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useMemo, useState, useEffect, useCallback, useRef } from 'react';
|
import { useMemo, useState, useEffect, useCallback, useRef } from 'react';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
import { getWorkspaces } from '@affine/data-services';
|
import { getDataCenter } from '@affine/datacenter';
|
||||||
import { AppState, AppStateContext } from './context';
|
import { AppState, AppStateContext } from './context';
|
||||||
import type {
|
import type {
|
||||||
AppStateValue,
|
AppStateValue,
|
||||||
@@ -16,7 +16,8 @@ const DynamicBlocksuite = dynamic(() => import('./dynamic-blocksuite'), {
|
|||||||
|
|
||||||
export const AppStateProvider = ({ children }: { children?: ReactNode }) => {
|
export const AppStateProvider = ({ children }: { children?: ReactNode }) => {
|
||||||
const refreshWorkspacesMeta = async () => {
|
const refreshWorkspacesMeta = async () => {
|
||||||
const workspacesMeta = await getWorkspaces().catch(() => {
|
const dc = await getDataCenter();
|
||||||
|
const workspacesMeta = await dc.apis.getWorkspaces().catch(() => {
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
setState(state => ({ ...state, workspacesMeta }));
|
setState(state => ({ ...state, workspacesMeta }));
|
||||||
@@ -41,7 +42,7 @@ export const AppStateProvider = ({ children }: { children?: ReactNode }) => {
|
|||||||
const workspacesList = await Promise.all(
|
const workspacesList = await Promise.all(
|
||||||
state.workspacesMeta.map(async ({ id }) => {
|
state.workspacesMeta.map(async ({ id }) => {
|
||||||
const workspace =
|
const workspace =
|
||||||
(await loadWorkspaceHandler?.(id, false, state.user)) || null;
|
(await loadWorkspaceHandler?.(id, state.user)) || null;
|
||||||
return { id, workspace };
|
return { id, workspace };
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -84,7 +85,7 @@ export const AppStateProvider = ({ children }: { children?: ReactNode }) => {
|
|||||||
return state.currentWorkspace;
|
return state.currentWorkspace;
|
||||||
}
|
}
|
||||||
const workspace =
|
const workspace =
|
||||||
(await loadWorkspaceHandler?.(workspaceId, true, state.user)) || null;
|
(await loadWorkspaceHandler?.(workspaceId, state.user)) || null;
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "@affine/data-services",
|
"name": "@affine/datacenter",
|
||||||
"version": "0.3.0",
|
"version": "0.3.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -21,14 +21,22 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.29.1",
|
"@playwright/test": "^1.29.1",
|
||||||
|
"@types/debug": "^4.1.7",
|
||||||
|
"fake-indexeddb": "4.0.1",
|
||||||
"typescript": "^4.8.4"
|
"typescript": "^4.8.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@blocksuite/blocks": "^0.3.1",
|
||||||
|
"@blocksuite/store": "^0.3.1",
|
||||||
|
"debug": "^4.3.4",
|
||||||
"encoding": "^0.1.13",
|
"encoding": "^0.1.13",
|
||||||
"firebase": "^9.13.0",
|
"firebase": "^9.15.0",
|
||||||
|
"idb-keyval": "^6.2.0",
|
||||||
"ky": "^0.33.0",
|
"ky": "^0.33.0",
|
||||||
|
"ky-universal": "^0.11.0",
|
||||||
"lib0": "^0.2.58",
|
"lib0": "^0.2.58",
|
||||||
"swr": "^2.0.0",
|
"swr": "^2.0.0",
|
||||||
|
"yjs": "^13.5.44",
|
||||||
"y-protocols": "^1.0.5"
|
"y-protocols": "^1.0.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
export { token } from './token.js';
|
||||||
|
export type { Callback } from './token.js';
|
||||||
|
|
||||||
|
import { getAuthorizer } from './token.js';
|
||||||
|
import * as user from './user.js';
|
||||||
|
import * as workspace from './workspace.js';
|
||||||
|
|
||||||
|
export type Apis = typeof user &
|
||||||
|
typeof workspace & {
|
||||||
|
signInWithGoogle: ReturnType<typeof getAuthorizer>[0];
|
||||||
|
onAuthStateChanged: ReturnType<typeof getAuthorizer>[1];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getApis = (): Apis => {
|
||||||
|
const [signInWithGoogle, onAuthStateChanged] = getAuthorizer();
|
||||||
|
return { ...user, ...workspace, signInWithGoogle, onAuthStateChanged };
|
||||||
|
};
|
||||||
|
|
||||||
|
export type { AccessTokenMessage } from './token';
|
||||||
|
export type { Member, Workspace } from './workspace';
|
||||||
|
export { WorkspaceType } from './workspace.js';
|
||||||
+5
-5
@@ -1,7 +1,9 @@
|
|||||||
import ky from 'ky';
|
import kyOrigin from 'ky';
|
||||||
import { token } from './token';
|
import ky from 'ky-universal';
|
||||||
|
import { token } from './token.js';
|
||||||
|
|
||||||
export const bareClient = ky.extend({
|
export const bareClient = ky.extend({
|
||||||
|
prefixUrl: 'http://localhost:8080',
|
||||||
retry: 1,
|
retry: 1,
|
||||||
hooks: {
|
hooks: {
|
||||||
// afterResponse: [
|
// afterResponse: [
|
||||||
@@ -19,6 +21,7 @@ export const bareClient = ky.extend({
|
|||||||
// ],
|
// ],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const client = bareClient.extend({
|
export const client = bareClient.extend({
|
||||||
hooks: {
|
hooks: {
|
||||||
beforeRequest: [
|
beforeRequest: [
|
||||||
@@ -40,6 +43,3 @@ export const client = bareClient.extend({
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export type { AccessTokenMessage } from './token';
|
|
||||||
export { token };
|
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
import { initializeApp } from 'firebase/app';
|
||||||
|
import { getAuth, GoogleAuthProvider, signInWithPopup } from 'firebase/auth';
|
||||||
|
import type { User } from 'firebase/auth';
|
||||||
|
|
||||||
|
import { getLogger } from '../index.js';
|
||||||
|
import { bareClient } from './request.js';
|
||||||
|
|
||||||
|
export interface AccessTokenMessage {
|
||||||
|
create_at: number;
|
||||||
|
exp: number;
|
||||||
|
email: string;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
avatar_url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Callback = (user: AccessTokenMessage | null) => void;
|
||||||
|
|
||||||
|
type LoginParams = {
|
||||||
|
type: 'Google' | 'Refresh';
|
||||||
|
token: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type LoginResponse = {
|
||||||
|
// access token, expires in a very short time
|
||||||
|
token: string;
|
||||||
|
// Refresh token
|
||||||
|
refresh: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const login = (params: LoginParams): Promise<LoginResponse> =>
|
||||||
|
bareClient.post('api/user/token', { json: params }).json();
|
||||||
|
|
||||||
|
class Token {
|
||||||
|
private readonly _logger;
|
||||||
|
private _accessToken!: string;
|
||||||
|
private _refreshToken!: string;
|
||||||
|
|
||||||
|
private _user!: AccessTokenMessage | null;
|
||||||
|
private _padding?: Promise<LoginResponse>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this._logger = getLogger('token');
|
||||||
|
this._logger.enabled = true;
|
||||||
|
|
||||||
|
this._setToken(); // fill with default value
|
||||||
|
}
|
||||||
|
|
||||||
|
private _setToken(login?: LoginResponse) {
|
||||||
|
this._accessToken = login?.token || '';
|
||||||
|
this._refreshToken = login?.refresh || '';
|
||||||
|
|
||||||
|
this._user = Token.parse(this._accessToken);
|
||||||
|
if (login) {
|
||||||
|
this._logger('set login', login);
|
||||||
|
this.triggerChange(this._user);
|
||||||
|
} else {
|
||||||
|
this._logger('empty login');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async initToken(token: string) {
|
||||||
|
this._setToken(await login({ token, type: 'Google' }));
|
||||||
|
}
|
||||||
|
|
||||||
|
async refreshToken(token?: string) {
|
||||||
|
if (!this._refreshToken && !token) {
|
||||||
|
throw new Error('No authorization token.');
|
||||||
|
}
|
||||||
|
if (!this._padding) {
|
||||||
|
this._padding = login({
|
||||||
|
type: 'Refresh',
|
||||||
|
token: this._refreshToken || token!,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this._setToken(await this._padding);
|
||||||
|
this._padding = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
get token() {
|
||||||
|
return this._accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
get refresh() {
|
||||||
|
return this._refreshToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isLogin() {
|
||||||
|
return !!this._refreshToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isExpired() {
|
||||||
|
if (!this._user) return true;
|
||||||
|
return Date.now() - this._user.create_at > this._user.exp;
|
||||||
|
}
|
||||||
|
|
||||||
|
static parse(token: string): AccessTokenMessage | null {
|
||||||
|
try {
|
||||||
|
return JSON.parse(
|
||||||
|
String.fromCharCode.apply(
|
||||||
|
null,
|
||||||
|
Array.from(
|
||||||
|
Uint8Array.from(
|
||||||
|
window.atob(
|
||||||
|
// split jwt
|
||||||
|
token.split('.')[1]
|
||||||
|
),
|
||||||
|
c => c.charCodeAt(0)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private callbacks: Callback[] = [];
|
||||||
|
private lastState: AccessTokenMessage | null = null;
|
||||||
|
|
||||||
|
triggerChange(user: AccessTokenMessage | null) {
|
||||||
|
this.lastState = user;
|
||||||
|
this.callbacks.forEach(callback => callback(user));
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange(callback: Callback) {
|
||||||
|
this.callbacks.push(callback);
|
||||||
|
callback(this.lastState);
|
||||||
|
}
|
||||||
|
|
||||||
|
offChange(callback: Callback) {
|
||||||
|
const index = this.callbacks.indexOf(callback);
|
||||||
|
if (index > -1) {
|
||||||
|
this.callbacks.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const token = new Token();
|
||||||
|
|
||||||
|
export const getAuthorizer = () => {
|
||||||
|
const app = initializeApp({
|
||||||
|
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
|
||||||
|
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
|
||||||
|
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
|
||||||
|
storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
|
||||||
|
messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
|
||||||
|
appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
|
||||||
|
measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const firebaseAuth = getAuth(app);
|
||||||
|
|
||||||
|
const googleAuthProvider = new GoogleAuthProvider();
|
||||||
|
|
||||||
|
const signInWithGoogle = async () => {
|
||||||
|
const user = await signInWithPopup(firebaseAuth, googleAuthProvider);
|
||||||
|
const idToken = await user.user.getIdToken();
|
||||||
|
await token.initToken(idToken);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAuthStateChanged = (callback: (user: User | null) => void) => {
|
||||||
|
firebaseAuth.onAuthStateChanged(callback);
|
||||||
|
};
|
||||||
|
|
||||||
|
return [signInWithGoogle, onAuthStateChanged] as const;
|
||||||
|
} catch (e) {
|
||||||
|
return [] as const;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { client } from '../request';
|
import { client } from './request.js';
|
||||||
|
|
||||||
export interface GetUserByEmailParams {
|
export interface GetUserByEmailParams {
|
||||||
email: string;
|
email: string;
|
||||||
@@ -17,5 +17,5 @@ export async function getUserByEmail(
|
|||||||
params: GetUserByEmailParams
|
params: GetUserByEmailParams
|
||||||
): Promise<User | null> {
|
): Promise<User | null> {
|
||||||
const searchParams = new URLSearchParams({ ...params });
|
const searchParams = new URLSearchParams({ ...params });
|
||||||
return client.get('/api/user', { searchParams }).json<User | null>();
|
return client.get('api/user', { searchParams }).json<User | null>();
|
||||||
}
|
}
|
||||||
+21
-23
@@ -1,5 +1,5 @@
|
|||||||
import { client, bareClient } from '../request';
|
import { bareClient, client } from './request.js';
|
||||||
import { User } from './user';
|
import type { User } from './user';
|
||||||
|
|
||||||
export interface GetWorkspaceDetailParams {
|
export interface GetWorkspaceDetailParams {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -27,7 +27,7 @@ export interface Workspace {
|
|||||||
|
|
||||||
export async function getWorkspaces(): Promise<Workspace[]> {
|
export async function getWorkspaces(): Promise<Workspace[]> {
|
||||||
return client
|
return client
|
||||||
.get('/api/workspace', {
|
.get('api/workspace', {
|
||||||
headers: {
|
headers: {
|
||||||
'Cache-Control': 'no-cache',
|
'Cache-Control': 'no-cache',
|
||||||
},
|
},
|
||||||
@@ -43,7 +43,7 @@ export interface WorkspaceDetail extends Workspace {
|
|||||||
export async function getWorkspaceDetail(
|
export async function getWorkspaceDetail(
|
||||||
params: GetWorkspaceDetailParams
|
params: GetWorkspaceDetailParams
|
||||||
): Promise<WorkspaceDetail | null> {
|
): Promise<WorkspaceDetail | null> {
|
||||||
return client.get(`/api/workspace/${params.id}`).json();
|
return client.get(`api/workspace/${params.id}`).json();
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Permission {
|
export interface Permission {
|
||||||
@@ -74,7 +74,7 @@ export interface GetWorkspaceMembersParams {
|
|||||||
export async function getWorkspaceMembers(
|
export async function getWorkspaceMembers(
|
||||||
params: GetWorkspaceDetailParams
|
params: GetWorkspaceDetailParams
|
||||||
): Promise<Member[]> {
|
): Promise<Member[]> {
|
||||||
return client.get(`/api/workspace/${params.id}/permission`).json();
|
return client.get(`api/workspace/${params.id}/permission`).json();
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateWorkspaceParams {
|
export interface CreateWorkspaceParams {
|
||||||
@@ -85,7 +85,7 @@ export interface CreateWorkspaceParams {
|
|||||||
export async function createWorkspace(
|
export async function createWorkspace(
|
||||||
params: CreateWorkspaceParams
|
params: CreateWorkspaceParams
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
return client.post('/api/workspace', { json: params }).json();
|
return client.post('api/workspace', { json: params }).json();
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateWorkspaceParams {
|
export interface UpdateWorkspaceParams {
|
||||||
@@ -97,7 +97,7 @@ export async function updateWorkspace(
|
|||||||
params: UpdateWorkspaceParams
|
params: UpdateWorkspaceParams
|
||||||
): Promise<{ public: boolean | null }> {
|
): Promise<{ public: boolean | null }> {
|
||||||
return client
|
return client
|
||||||
.post(`/api/workspace/${params.id}`, {
|
.post(`api/workspace/${params.id}`, {
|
||||||
json: {
|
json: {
|
||||||
public: params.public,
|
public: params.public,
|
||||||
},
|
},
|
||||||
@@ -112,7 +112,7 @@ export interface DeleteWorkspaceParams {
|
|||||||
export async function deleteWorkspace(
|
export async function deleteWorkspace(
|
||||||
params: DeleteWorkspaceParams
|
params: DeleteWorkspaceParams
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await client.delete(`/api/workspace/${params.id}`);
|
await client.delete(`api/workspace/${params.id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InviteMemberParams {
|
export interface InviteMemberParams {
|
||||||
@@ -125,7 +125,7 @@ export interface InviteMemberParams {
|
|||||||
*/
|
*/
|
||||||
export async function inviteMember(params: InviteMemberParams): Promise<void> {
|
export async function inviteMember(params: InviteMemberParams): Promise<void> {
|
||||||
return client
|
return client
|
||||||
.post(`/api/workspace/${params.id}/permission`, {
|
.post(`api/workspace/${params.id}/permission`, {
|
||||||
json: {
|
json: {
|
||||||
email: params.email,
|
email: params.email,
|
||||||
},
|
},
|
||||||
@@ -138,7 +138,7 @@ export interface RemoveMemberParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function removeMember(params: RemoveMemberParams): Promise<void> {
|
export async function removeMember(params: RemoveMemberParams): Promise<void> {
|
||||||
await client.delete(`/api/permission/${params.permissionId}`);
|
await client.delete(`api/permission/${params.permissionId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AcceptInvitingParams {
|
export interface AcceptInvitingParams {
|
||||||
@@ -148,31 +148,29 @@ export interface AcceptInvitingParams {
|
|||||||
export async function acceptInviting(
|
export async function acceptInviting(
|
||||||
params: AcceptInvitingParams
|
params: AcceptInvitingParams
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await bareClient.post(`/api/invitation/${params.invitingCode}`);
|
await bareClient.post(`api/invitation/${params.invitingCode}`);
|
||||||
}
|
|
||||||
|
|
||||||
export interface DownloadWOrkspaceParams {
|
|
||||||
workspaceId: string;
|
|
||||||
}
|
|
||||||
export async function downloadWorkspace(
|
|
||||||
params: DownloadWOrkspaceParams
|
|
||||||
): Promise<ArrayBuffer> {
|
|
||||||
return client.get(`/api/workspace/${params.workspaceId}/doc`).arrayBuffer();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function uploadBlob(params: { blob: Blob }): Promise<string> {
|
export async function uploadBlob(params: { blob: Blob }): Promise<string> {
|
||||||
return client.put('/api/blob', { body: params.blob }).text();
|
return client.put('api/blob', { body: params.blob }).text();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getBlob(params: {
|
export async function getBlob(params: {
|
||||||
blobId: string;
|
blobId: string;
|
||||||
}): Promise<ArrayBuffer> {
|
}): Promise<ArrayBuffer> {
|
||||||
return client.get(`/api/blob/${params.blobId}`).arrayBuffer();
|
return client.get(`api/blob/${params.blobId}`).arrayBuffer();
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LeaveWorkspaceParams {
|
export interface LeaveWorkspaceParams {
|
||||||
id: number | string;
|
id: number | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function leaveWorkspace({ id }: LeaveWorkspaceParams) {
|
export async function leaveWorkspace({ id }: LeaveWorkspaceParams) {
|
||||||
await client.delete(`/api/workspace/${id}/permission`).json();
|
await client.delete(`api/workspace/${id}/permission`).json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function downloadWorkspace(
|
||||||
|
workspaceId: string
|
||||||
|
): Promise<ArrayBuffer> {
|
||||||
|
return client.get(`api/workspace/${workspaceId}/doc`).arrayBuffer();
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
import assert from 'assert';
|
||||||
|
import { BlockSchema } from '@blocksuite/blocks/models';
|
||||||
|
import { Workspace } from '@blocksuite/store';
|
||||||
|
|
||||||
|
import { getLogger } from './index.js';
|
||||||
|
import { getApis, Apis } from './apis/index.js';
|
||||||
|
import { AffineProvider, BaseProvider } from './provider/index.js';
|
||||||
|
import { LocalProvider } from './provider/index.js';
|
||||||
|
import { getKVConfigure } from './store.js';
|
||||||
|
|
||||||
|
// load workspace's config
|
||||||
|
type LoadConfig = {
|
||||||
|
// use witch provider load data
|
||||||
|
providerId?: string;
|
||||||
|
// provider config
|
||||||
|
config?: Record<string, any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class DataCenter {
|
||||||
|
private readonly _apis: Apis;
|
||||||
|
private readonly _providers = new Map<string, typeof BaseProvider>();
|
||||||
|
private readonly _workspaces = new Map<string, Promise<BaseProvider>>();
|
||||||
|
private readonly _config;
|
||||||
|
private readonly _logger;
|
||||||
|
|
||||||
|
static async init(debug: boolean): Promise<DataCenter> {
|
||||||
|
const dc = new DataCenter(debug);
|
||||||
|
dc.addProvider(AffineProvider);
|
||||||
|
dc.addProvider(LocalProvider);
|
||||||
|
|
||||||
|
return dc;
|
||||||
|
}
|
||||||
|
|
||||||
|
private constructor(debug: boolean) {
|
||||||
|
this._apis = getApis();
|
||||||
|
this._config = getKVConfigure('sys');
|
||||||
|
this._logger = getLogger('dc');
|
||||||
|
this._logger.enabled = debug;
|
||||||
|
}
|
||||||
|
|
||||||
|
get apis(): Readonly<Apis> {
|
||||||
|
return this._apis;
|
||||||
|
}
|
||||||
|
|
||||||
|
private addProvider(provider: typeof BaseProvider) {
|
||||||
|
this._providers.set(provider.id, provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _getProvider(id: string, providerId: string): Promise<string> {
|
||||||
|
const providerKey = `workspace:${id}:provider`;
|
||||||
|
if (this._providers.has(providerId)) {
|
||||||
|
await this._config.set(providerKey, providerId);
|
||||||
|
return providerId;
|
||||||
|
} else {
|
||||||
|
const providerValue = await this._config.get(providerKey);
|
||||||
|
if (providerValue) return providerValue;
|
||||||
|
}
|
||||||
|
throw Error(`Provider ${providerId} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _getWorkspace(id: string, pid: string): Promise<BaseProvider> {
|
||||||
|
this._logger(`Init workspace ${id} with ${pid}`);
|
||||||
|
|
||||||
|
const providerId = await this._getProvider(id, pid);
|
||||||
|
|
||||||
|
// init workspace & register block schema
|
||||||
|
const workspace = new Workspace({ room: id }).register(BlockSchema);
|
||||||
|
|
||||||
|
const Provider = this._providers.get(providerId);
|
||||||
|
assert(Provider);
|
||||||
|
const provider = new Provider();
|
||||||
|
|
||||||
|
await provider.init({
|
||||||
|
apis: this._apis,
|
||||||
|
config: getKVConfigure(id),
|
||||||
|
debug: this._logger.enabled,
|
||||||
|
logger: this._logger.extend(`${Provider.id}:${id}`),
|
||||||
|
workspace,
|
||||||
|
});
|
||||||
|
await provider.initData();
|
||||||
|
this._logger(`Workspace ${id} loaded`);
|
||||||
|
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setConfig(workspace: string, config: Record<string, any>) {
|
||||||
|
const values = Object.entries(config);
|
||||||
|
if (values.length) {
|
||||||
|
const configure = getKVConfigure(workspace);
|
||||||
|
await configure.setMany(values);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// load workspace data to memory
|
||||||
|
async load(
|
||||||
|
workspaceId: string,
|
||||||
|
params: LoadConfig = {}
|
||||||
|
): Promise<Workspace | null> {
|
||||||
|
const { providerId = 'local', config = {} } = params;
|
||||||
|
if (workspaceId) {
|
||||||
|
if (!this._workspaces.has(workspaceId)) {
|
||||||
|
this._workspaces.set(
|
||||||
|
workspaceId,
|
||||||
|
this.setConfig(workspaceId, config).then(() =>
|
||||||
|
this._getWorkspace(workspaceId, providerId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const workspace = this._workspaces.get(workspaceId);
|
||||||
|
assert(workspace);
|
||||||
|
return workspace.then(w => w.workspace);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// destroy workspace's instance in memory
|
||||||
|
async destroy(workspaceId: string) {
|
||||||
|
const provider = await this._workspaces.get(workspaceId);
|
||||||
|
if (provider) {
|
||||||
|
this._workspaces.delete(workspaceId);
|
||||||
|
await provider.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async reload(
|
||||||
|
workspaceId: string,
|
||||||
|
config: LoadConfig = {}
|
||||||
|
): Promise<Workspace | null> {
|
||||||
|
await this.destroy(workspaceId);
|
||||||
|
return this.load(workspaceId, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
async list() {
|
||||||
|
const keys = await this._config.keys();
|
||||||
|
return keys
|
||||||
|
.filter(k => k.startsWith('workspace:'))
|
||||||
|
.map(k => k.split(':')[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete local workspace's data
|
||||||
|
async delete(workspaceId: string) {
|
||||||
|
await this._config.delete(`workspace:${workspaceId}:provider`);
|
||||||
|
const provider = await this._workspaces.get(workspaceId);
|
||||||
|
if (provider) {
|
||||||
|
this._workspaces.delete(workspaceId);
|
||||||
|
// clear workspace data implement by provider
|
||||||
|
await provider.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear all local workspace's data
|
||||||
|
async clear() {
|
||||||
|
const workspaces = await this.list();
|
||||||
|
await Promise.all(workspaces.map(id => this.delete(id)));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import debug from 'debug';
|
||||||
|
import { DataCenter } from './datacenter.js';
|
||||||
|
|
||||||
|
const _initializeDataCenter = () => {
|
||||||
|
let _dataCenterInstance: Promise<DataCenter>;
|
||||||
|
|
||||||
|
return (debug = true) => {
|
||||||
|
if (!_dataCenterInstance) {
|
||||||
|
_dataCenterInstance = DataCenter.init(debug);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _dataCenterInstance;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDataCenter = _initializeDataCenter();
|
||||||
|
|
||||||
|
export function getLogger(namespace: string) {
|
||||||
|
const logger = debug(namespace);
|
||||||
|
logger.log = console.log.bind(console);
|
||||||
|
return logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { AccessTokenMessage, Member, Workspace } from './apis';
|
||||||
|
export { WorkspaceType } from './apis/index.js';
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import assert from 'assert';
|
||||||
|
import { applyUpdate } from 'yjs';
|
||||||
|
|
||||||
|
import type { InitialParams } from '../index.js';
|
||||||
|
import { token, Callback } from '../../apis/index.js';
|
||||||
|
import { LocalProvider } from '../local/index.js';
|
||||||
|
|
||||||
|
import { WebsocketProvider } from './sync.js';
|
||||||
|
|
||||||
|
export class AffineProvider extends LocalProvider {
|
||||||
|
static id = 'affine';
|
||||||
|
private _onTokenRefresh?: Callback = undefined;
|
||||||
|
private _ws?: WebsocketProvider;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
async init(params: InitialParams) {
|
||||||
|
super.init(params);
|
||||||
|
|
||||||
|
this._onTokenRefresh = () => {
|
||||||
|
if (token.refresh) {
|
||||||
|
this._config.set('token', token.refresh);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
assert(this._onTokenRefresh);
|
||||||
|
|
||||||
|
token.onChange(this._onTokenRefresh);
|
||||||
|
|
||||||
|
// initial login token
|
||||||
|
if (token.isExpired) {
|
||||||
|
try {
|
||||||
|
const refreshToken = await this._config.get('token');
|
||||||
|
await token.refreshToken(refreshToken);
|
||||||
|
|
||||||
|
if (token.refresh) {
|
||||||
|
this._config.set('token', token.refresh);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(token.isLogin);
|
||||||
|
} catch (_) {
|
||||||
|
this._logger('Authorization failed, fallback to local mode');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this._config.set('token', token.refresh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async destroy() {
|
||||||
|
if (this._onTokenRefresh) {
|
||||||
|
token.offChange(this._onTokenRefresh);
|
||||||
|
}
|
||||||
|
if (this._ws) {
|
||||||
|
this._ws.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async initData() {
|
||||||
|
await super.initData();
|
||||||
|
|
||||||
|
const workspace = this._workspace;
|
||||||
|
const doc = workspace.doc;
|
||||||
|
|
||||||
|
this._logger(`Login: ${token.isLogin}`);
|
||||||
|
|
||||||
|
if (workspace.room && token.isLogin) {
|
||||||
|
try {
|
||||||
|
const updates = await this._apis.downloadWorkspace(workspace.room);
|
||||||
|
if (updates) {
|
||||||
|
await new Promise(resolve => {
|
||||||
|
doc.once('update', resolve);
|
||||||
|
applyUpdate(doc, new Uint8Array(updates));
|
||||||
|
});
|
||||||
|
// TODO: wait util data loaded
|
||||||
|
this._ws = new WebsocketProvider('/', workspace.room, doc);
|
||||||
|
// Wait for ws synchronization to complete, otherwise the data will be modified in reverse, which can be optimized later
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
// TODO: synced will also be triggered on reconnection after losing sync
|
||||||
|
// There needs to be an event mechanism to emit the synchronization state to the upper layer
|
||||||
|
assert(this._ws);
|
||||||
|
this._ws.once('synced', () => resolve());
|
||||||
|
this._ws.once('lost-connection', () => resolve());
|
||||||
|
this._ws.once('connection-error', () => reject());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this._logger('Failed to init cloud workspace', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if after update, the space:meta is empty
|
||||||
|
// then we need to get map with doc
|
||||||
|
// just a workaround for yjs
|
||||||
|
doc.getMap('space:meta');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
import type { Workspace } from '@blocksuite/store';
|
||||||
|
|
||||||
|
import type { Apis, Logger, InitialParams, ConfigStore } from './index';
|
||||||
|
|
||||||
|
export class BaseProvider {
|
||||||
|
static id = 'base';
|
||||||
|
protected _apis!: Readonly<Apis>;
|
||||||
|
protected _config!: Readonly<ConfigStore>;
|
||||||
|
protected _logger!: Logger;
|
||||||
|
protected _workspace!: Workspace;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
// Nothing to do here
|
||||||
|
}
|
||||||
|
|
||||||
|
async init(params: InitialParams) {
|
||||||
|
this._apis = params.apis;
|
||||||
|
this._config = params.config;
|
||||||
|
this._logger = params.logger;
|
||||||
|
this._workspace = params.workspace;
|
||||||
|
this._logger.enabled = params.debug;
|
||||||
|
}
|
||||||
|
|
||||||
|
async clear() {
|
||||||
|
await this.destroy();
|
||||||
|
await this._config.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
async destroy() {
|
||||||
|
// Nothing to do here
|
||||||
|
}
|
||||||
|
|
||||||
|
async initData() {
|
||||||
|
throw Error('Not implemented: initData');
|
||||||
|
}
|
||||||
|
|
||||||
|
// should return a blob url
|
||||||
|
async getBlob(_id: string): Promise<string | null> {
|
||||||
|
throw Error('Not implemented: getBlob');
|
||||||
|
}
|
||||||
|
|
||||||
|
// should return a blob unique id
|
||||||
|
async setBlob(_blob: Blob): Promise<string> {
|
||||||
|
throw Error('Not implemented: setBlob');
|
||||||
|
}
|
||||||
|
|
||||||
|
get workspace() {
|
||||||
|
return this._workspace;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import type { Workspace } from '@blocksuite/store';
|
||||||
|
|
||||||
|
import type { Apis } from '../apis';
|
||||||
|
import type { getLogger } from '../index';
|
||||||
|
import type { ConfigStore } from '../store';
|
||||||
|
|
||||||
|
export type Logger = ReturnType<typeof getLogger>;
|
||||||
|
|
||||||
|
export type InitialParams = {
|
||||||
|
apis: Apis;
|
||||||
|
config: ConfigStore;
|
||||||
|
debug: boolean;
|
||||||
|
logger: Logger;
|
||||||
|
workspace: Workspace;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type { Apis, ConfigStore, Workspace };
|
||||||
|
export type { BaseProvider } from './base.js';
|
||||||
|
export { AffineProvider } from './affine/index.js';
|
||||||
|
export { LocalProvider } from './local/index.js';
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import type { BlobStorage } from '@blocksuite/store';
|
||||||
|
import assert from 'assert';
|
||||||
|
|
||||||
|
import type { InitialParams } from '../index.js';
|
||||||
|
import { BaseProvider } from '../base.js';
|
||||||
|
import { IndexedDBProvider } from './indexeddb.js';
|
||||||
|
|
||||||
|
export class LocalProvider extends BaseProvider {
|
||||||
|
static id = 'local';
|
||||||
|
private _blobs!: BlobStorage;
|
||||||
|
private _idb?: IndexedDBProvider = undefined;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
async init(params: InitialParams) {
|
||||||
|
super.init(params);
|
||||||
|
|
||||||
|
const blobs = await this._workspace.blobs;
|
||||||
|
assert(blobs);
|
||||||
|
this._blobs = blobs;
|
||||||
|
}
|
||||||
|
|
||||||
|
async initData() {
|
||||||
|
assert(this._workspace.room);
|
||||||
|
this._logger('Loading local data');
|
||||||
|
this._idb = new IndexedDBProvider(
|
||||||
|
this._workspace.room,
|
||||||
|
this._workspace.doc
|
||||||
|
);
|
||||||
|
|
||||||
|
await this._idb.whenSynced;
|
||||||
|
this._logger('Local data loaded');
|
||||||
|
}
|
||||||
|
|
||||||
|
async clear() {
|
||||||
|
await super.clear();
|
||||||
|
await this._blobs.clear();
|
||||||
|
this._idb?.clearData();
|
||||||
|
}
|
||||||
|
|
||||||
|
async destroy(): Promise<void> {
|
||||||
|
super.destroy();
|
||||||
|
if (this._idb) {
|
||||||
|
await this._idb.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getBlob(id: string): Promise<string | null> {
|
||||||
|
return this._blobs.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setBlob(blob: Blob): Promise<string> {
|
||||||
|
return this._blobs.set(blob);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import * as idb from 'lib0/indexeddb.js';
|
||||||
|
import { Observable } from 'lib0/observable.js';
|
||||||
|
import type { Doc } from 'yjs';
|
||||||
|
import { applyUpdate, encodeStateAsUpdate, transact } from 'yjs';
|
||||||
|
|
||||||
|
const customStoreName = 'custom';
|
||||||
|
const updatesStoreName = 'updates';
|
||||||
|
|
||||||
|
const PREFERRED_TRIM_SIZE = 500;
|
||||||
|
|
||||||
|
const fetchUpdates = async (provider: IndexedDBProvider) => {
|
||||||
|
const [updatesStore] = idb.transact(provider.db as IDBDatabase, [
|
||||||
|
updatesStoreName,
|
||||||
|
]); // , 'readonly')
|
||||||
|
if (updatesStore) {
|
||||||
|
const updates = await idb.getAll(
|
||||||
|
updatesStore,
|
||||||
|
idb.createIDBKeyRangeLowerBound(provider._dbref, false)
|
||||||
|
);
|
||||||
|
transact(
|
||||||
|
provider.doc,
|
||||||
|
() => {
|
||||||
|
updates.forEach(val => applyUpdate(provider.doc, val));
|
||||||
|
},
|
||||||
|
provider,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
const lastKey = await idb.getLastKey(updatesStore);
|
||||||
|
provider._dbref = lastKey + 1;
|
||||||
|
const cnt = await idb.count(updatesStore);
|
||||||
|
provider._dbsize = cnt;
|
||||||
|
}
|
||||||
|
return updatesStore;
|
||||||
|
};
|
||||||
|
|
||||||
|
const storeState = (provider: IndexedDBProvider, forceStore = true) =>
|
||||||
|
fetchUpdates(provider).then(updatesStore => {
|
||||||
|
if (
|
||||||
|
updatesStore &&
|
||||||
|
(forceStore || provider._dbsize >= PREFERRED_TRIM_SIZE)
|
||||||
|
) {
|
||||||
|
idb
|
||||||
|
.addAutoKey(updatesStore, encodeStateAsUpdate(provider.doc))
|
||||||
|
.then(() =>
|
||||||
|
idb.del(
|
||||||
|
updatesStore,
|
||||||
|
idb.createIDBKeyRangeUpperBound(provider._dbref, true)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.then(() =>
|
||||||
|
idb.count(updatesStore).then(cnt => {
|
||||||
|
provider._dbsize = cnt;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export class IndexedDBProvider extends Observable<string> {
|
||||||
|
doc: Doc;
|
||||||
|
name: string;
|
||||||
|
_dbref: number;
|
||||||
|
_dbsize: number;
|
||||||
|
private _destroyed: boolean;
|
||||||
|
whenSynced: Promise<IndexedDBProvider>;
|
||||||
|
db: IDBDatabase | null;
|
||||||
|
private _db: Promise<IDBDatabase>;
|
||||||
|
private _storeTimeout: number;
|
||||||
|
private _storeTimeoutId: NodeJS.Timeout | null;
|
||||||
|
private _storeUpdate: (update: Uint8Array, origin: any) => void;
|
||||||
|
|
||||||
|
constructor(name: string, doc: Doc) {
|
||||||
|
super();
|
||||||
|
this.doc = doc;
|
||||||
|
this.name = name;
|
||||||
|
this._dbref = 0;
|
||||||
|
this._dbsize = 0;
|
||||||
|
this._destroyed = false;
|
||||||
|
this.db = null;
|
||||||
|
this._db = idb.openDB(name, db =>
|
||||||
|
idb.createStores(db, [['updates', { autoIncrement: true }], ['custom']])
|
||||||
|
);
|
||||||
|
|
||||||
|
this.whenSynced = this._db.then(async db => {
|
||||||
|
this.db = db;
|
||||||
|
const currState = encodeStateAsUpdate(doc);
|
||||||
|
const updatesStore = await fetchUpdates(this);
|
||||||
|
if (updatesStore) {
|
||||||
|
await idb.addAutoKey(updatesStore, currState);
|
||||||
|
}
|
||||||
|
if (this._destroyed) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
this.emit('synced', [this]);
|
||||||
|
return this;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Timeout in ms untill data is merged and persisted in idb.
|
||||||
|
this._storeTimeout = 1000;
|
||||||
|
|
||||||
|
this._storeTimeoutId = null;
|
||||||
|
|
||||||
|
this._storeUpdate = (update: Uint8Array, origin: any) => {
|
||||||
|
if (this.db && origin !== this) {
|
||||||
|
const [updatesStore] = idb.transact(
|
||||||
|
/** @type {IDBDatabase} */ this.db,
|
||||||
|
[updatesStoreName]
|
||||||
|
);
|
||||||
|
if (updatesStore) {
|
||||||
|
idb.addAutoKey(updatesStore, update);
|
||||||
|
}
|
||||||
|
if (++this._dbsize >= PREFERRED_TRIM_SIZE) {
|
||||||
|
// debounce store call
|
||||||
|
if (this._storeTimeoutId !== null) {
|
||||||
|
clearTimeout(this._storeTimeoutId);
|
||||||
|
}
|
||||||
|
this._storeTimeoutId = setTimeout(() => {
|
||||||
|
storeState(this, false);
|
||||||
|
this._storeTimeoutId = null;
|
||||||
|
}, this._storeTimeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
doc.on('update', this._storeUpdate);
|
||||||
|
this.destroy = this.destroy.bind(this);
|
||||||
|
doc.on('destroy', this.destroy);
|
||||||
|
}
|
||||||
|
|
||||||
|
override destroy() {
|
||||||
|
if (this._storeTimeoutId) {
|
||||||
|
clearTimeout(this._storeTimeoutId);
|
||||||
|
}
|
||||||
|
this.doc.off('update', this._storeUpdate);
|
||||||
|
this.doc.off('destroy', this.destroy);
|
||||||
|
this._destroyed = true;
|
||||||
|
return this._db.then(db => {
|
||||||
|
db.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroys this instance and removes all data from indexeddb.
|
||||||
|
*
|
||||||
|
* @return {Promise<void>}
|
||||||
|
*/
|
||||||
|
async clearData(): Promise<void> {
|
||||||
|
return this.destroy().then(() => {
|
||||||
|
idb.deleteDB(this.name);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {String | number | ArrayBuffer | Date} key
|
||||||
|
* @return {Promise<String | number | ArrayBuffer | Date | any>}
|
||||||
|
*/
|
||||||
|
async get(
|
||||||
|
key: string | number | ArrayBuffer | Date
|
||||||
|
): Promise<string | number | ArrayBuffer | Date | any> {
|
||||||
|
return this._db.then(db => {
|
||||||
|
const [custom] = idb.transact(db, [customStoreName], 'readonly');
|
||||||
|
if (custom) {
|
||||||
|
return idb.get(custom, key);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {String | number | ArrayBuffer | Date} key
|
||||||
|
* @param {String | number | ArrayBuffer | Date} value
|
||||||
|
* @return {Promise<String | number | ArrayBuffer | Date>}
|
||||||
|
*/
|
||||||
|
async set(
|
||||||
|
key: string | number | ArrayBuffer | Date,
|
||||||
|
value: string | number | ArrayBuffer | Date
|
||||||
|
): Promise<string | number | ArrayBuffer | Date> {
|
||||||
|
return this._db.then(db => {
|
||||||
|
const [custom] = idb.transact(db, [customStoreName]);
|
||||||
|
if (custom) {
|
||||||
|
return idb.put(custom, value, key);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {String | number | ArrayBuffer | Date} key
|
||||||
|
* @return {Promise<undefined>}
|
||||||
|
*/
|
||||||
|
async del(key: string | number | ArrayBuffer | Date): Promise<undefined> {
|
||||||
|
return this._db.then(db => {
|
||||||
|
const [custom] = idb.transact(db, [customStoreName]);
|
||||||
|
if (custom) {
|
||||||
|
return idb.del(custom, key);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { createStore, del, get, keys, set, setMany, clear } from 'idb-keyval';
|
||||||
|
|
||||||
|
export type ConfigStore<T = any> = {
|
||||||
|
get: (key: string) => Promise<T | undefined>;
|
||||||
|
set: (key: string, value: T) => Promise<void>;
|
||||||
|
setMany: (values: [string, T][]) => Promise<void>;
|
||||||
|
keys: () => Promise<string[]>;
|
||||||
|
delete: (key: string) => Promise<void>;
|
||||||
|
clear: () => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialIndexedDB = <T = any>(database: string): ConfigStore<T> => {
|
||||||
|
const store = createStore(`affine:${database}`, 'database');
|
||||||
|
return {
|
||||||
|
get: (key: string) => get<T>(key, store),
|
||||||
|
set: (key: string, value: T) => set(key, value, store),
|
||||||
|
setMany: (values: [string, T][]) => setMany(values, store),
|
||||||
|
keys: () => keys(store),
|
||||||
|
delete: (key: string) => del(key, store),
|
||||||
|
clear: () => clear(store),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const scopedIndexedDB = () => {
|
||||||
|
const idb = initialIndexedDB('global');
|
||||||
|
const storeCache = new Map<string, Readonly<ConfigStore>>();
|
||||||
|
|
||||||
|
return <T = any>(scope: string): Readonly<ConfigStore<T>> => {
|
||||||
|
if (!storeCache.has(scope)) {
|
||||||
|
const store = {
|
||||||
|
get: async (key: string) => idb.get(`${scope}:${key}`),
|
||||||
|
set: (key: string, value: T) => idb.set(`${scope}:${key}`, value),
|
||||||
|
setMany: (values: [string, T][]) =>
|
||||||
|
idb.setMany(values.map(([k, v]) => [`${scope}:${k}`, v])),
|
||||||
|
keys: () =>
|
||||||
|
idb
|
||||||
|
.keys()
|
||||||
|
.then(keys =>
|
||||||
|
keys
|
||||||
|
.filter(k => k.startsWith(`${scope}:`))
|
||||||
|
.map(k => k.replace(`${scope}:`, ''))
|
||||||
|
),
|
||||||
|
delete: (key: string) => idb.delete(`${scope}:${key}`),
|
||||||
|
clear: async () => {
|
||||||
|
await idb
|
||||||
|
.keys()
|
||||||
|
.then(keys =>
|
||||||
|
Promise.all(
|
||||||
|
keys.filter(k => k.startsWith(`${scope}:`)).map(k => del(k))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
storeCache.set(scope, store);
|
||||||
|
}
|
||||||
|
|
||||||
|
return storeCache.get(scope) as ConfigStore<T>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
let lazyKVConfigure: ReturnType<typeof scopedIndexedDB> | undefined = undefined;
|
||||||
|
|
||||||
|
export const getKVConfigure = (scope: string) => {
|
||||||
|
if (!lazyKVConfigure) {
|
||||||
|
lazyKVConfigure = scopedIndexedDB();
|
||||||
|
}
|
||||||
|
return lazyKVConfigure(scope);
|
||||||
|
};
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
import { getDataCenter } from './utils.js';
|
||||||
|
|
||||||
|
import 'fake-indexeddb/auto';
|
||||||
|
|
||||||
|
test('init data center', async () => {
|
||||||
|
const dataCenter = await getDataCenter();
|
||||||
|
expect(dataCenter).toBeTruthy();
|
||||||
|
await dataCenter.clear();
|
||||||
|
|
||||||
|
const workspace = await dataCenter.load('test1');
|
||||||
|
expect(workspace).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('init data center singleton', async () => {
|
||||||
|
// data center is singleton
|
||||||
|
const [dc1, dc2] = await Promise.all([getDataCenter(), getDataCenter()]);
|
||||||
|
expect(dc1).toEqual(dc2);
|
||||||
|
|
||||||
|
// load same workspace will get same instance
|
||||||
|
const [ws1, ws2] = await Promise.all([dc1.load('test1'), dc2.load('test1')]);
|
||||||
|
expect(ws1).toEqual(ws2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should init error with unknown provider', async () => {
|
||||||
|
const dc = await getDataCenter();
|
||||||
|
await dc.clear();
|
||||||
|
|
||||||
|
// load workspace with unknown provider will throw error
|
||||||
|
test.fail();
|
||||||
|
await dc.load('test2', { providerId: 'not exist provider' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test.skip('init affine provider', async () => {
|
||||||
|
const dataCenter = await getDataCenter();
|
||||||
|
await dataCenter.clear();
|
||||||
|
|
||||||
|
// load workspace with affine provider
|
||||||
|
// TODO: set constant token for testing
|
||||||
|
const workspace = await dataCenter.load('6', {
|
||||||
|
providerId: 'affine',
|
||||||
|
config: { token: 'YOUR_TOKEN' },
|
||||||
|
});
|
||||||
|
expect(workspace).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('list workspaces', async () => {
|
||||||
|
const dataCenter = await getDataCenter();
|
||||||
|
await dataCenter.clear();
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
dataCenter.load('test3'),
|
||||||
|
dataCenter.load('test4'),
|
||||||
|
dataCenter.load('test5'),
|
||||||
|
dataCenter.load('test6'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(await dataCenter.list()).toStrictEqual([
|
||||||
|
'test3',
|
||||||
|
'test4',
|
||||||
|
'test5',
|
||||||
|
'test6',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('destroy workspaces', async () => {
|
||||||
|
const dataCenter = await getDataCenter();
|
||||||
|
await dataCenter.clear();
|
||||||
|
|
||||||
|
// return new workspace if origin workspace is destroyed
|
||||||
|
const ws1 = await dataCenter.load('test7');
|
||||||
|
await dataCenter.destroy('test7');
|
||||||
|
const ws2 = await dataCenter.load('test7');
|
||||||
|
expect(ws1 !== ws2).toBeTruthy();
|
||||||
|
|
||||||
|
// return new workspace if workspace is reload
|
||||||
|
const ws3 = await dataCenter.load('test8');
|
||||||
|
const ws4 = await dataCenter.reload('test8', { providerId: 'affine' });
|
||||||
|
expect(ws3 !== ws4).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('remove workspaces', async () => {
|
||||||
|
const dataCenter = await getDataCenter();
|
||||||
|
await dataCenter.clear();
|
||||||
|
|
||||||
|
// remove workspace will remove workspace data
|
||||||
|
await Promise.all([dataCenter.load('test9'), dataCenter.load('test10')]);
|
||||||
|
await dataCenter.delete('test9');
|
||||||
|
expect(await dataCenter.list()).toStrictEqual(['test10']);
|
||||||
|
});
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export const getDataCenter = () => {
|
||||||
|
return import('../src/index.js').then(async dataCenter =>
|
||||||
|
dataCenter.getDataCenter(false)
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import { initializeApp } from 'firebase/app';
|
|
||||||
import {
|
|
||||||
getAuth,
|
|
||||||
createUserWithEmailAndPassword,
|
|
||||||
signInWithEmailAndPassword,
|
|
||||||
GoogleAuthProvider,
|
|
||||||
signInWithPopup,
|
|
||||||
} from 'firebase/auth';
|
|
||||||
import type { User } from 'firebase/auth';
|
|
||||||
import { token } from './request';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* firebaseConfig reference: https://firebase.google.com/docs/web/setup#add_firebase_to_your_app
|
|
||||||
*/
|
|
||||||
const app = initializeApp({
|
|
||||||
apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
|
|
||||||
authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
|
|
||||||
projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
|
|
||||||
storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
|
|
||||||
messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
|
|
||||||
appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
|
|
||||||
measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const firebaseAuth = getAuth(app);
|
|
||||||
|
|
||||||
const signUp = (email: string, password: string) => {
|
|
||||||
return createUserWithEmailAndPassword(firebaseAuth, email, password);
|
|
||||||
};
|
|
||||||
|
|
||||||
const signIn = (email: string, password: string) => {
|
|
||||||
return signInWithEmailAndPassword(firebaseAuth, email, password);
|
|
||||||
};
|
|
||||||
|
|
||||||
const googleAuthProvider = new GoogleAuthProvider();
|
|
||||||
export const signInWithGoogle = async () => {
|
|
||||||
const user = await signInWithPopup(firebaseAuth, googleAuthProvider);
|
|
||||||
const idToken = await user.user.getIdToken();
|
|
||||||
await token.initToken(idToken);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const onAuthStateChanged = (callback: (user: User | null) => void) => {
|
|
||||||
firebaseAuth.onAuthStateChanged(callback);
|
|
||||||
};
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
class DataCenter {
|
|
||||||
static async init() {
|
|
||||||
return new DataCenter();
|
|
||||||
}
|
|
||||||
|
|
||||||
private constructor() {
|
|
||||||
// TODO
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let _dataCenterInstance: Promise<DataCenter>;
|
|
||||||
|
|
||||||
export const getDataCenter = () => {
|
|
||||||
if (!_dataCenterInstance) {
|
|
||||||
_dataCenterInstance = DataCenter.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
return _dataCenterInstance;
|
|
||||||
};
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
export { signInWithGoogle, onAuthStateChanged } from './auth';
|
|
||||||
export * from './request';
|
|
||||||
export * from './sdks';
|
|
||||||
export * from './websocket';
|
|
||||||
|
|
||||||
export { getDataCenter } from './data-center';
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import { AccessTokenMessage } from './token';
|
|
||||||
|
|
||||||
export type Callback = (user: AccessTokenMessage | null) => void;
|
|
||||||
|
|
||||||
export class AuthorizationEvent {
|
|
||||||
private callbacks: Callback[] = [];
|
|
||||||
private lastState: AccessTokenMessage | null = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Callback will execute when call this function.
|
|
||||||
*/
|
|
||||||
onChange(callback: Callback) {
|
|
||||||
this.callbacks.push(callback);
|
|
||||||
callback(this.lastState);
|
|
||||||
}
|
|
||||||
|
|
||||||
triggerChange(user: AccessTokenMessage | null) {
|
|
||||||
this.lastState = user;
|
|
||||||
this.callbacks.forEach(callback => callback(user));
|
|
||||||
}
|
|
||||||
|
|
||||||
removeCallback(callback: Callback) {
|
|
||||||
const index = this.callbacks.indexOf(callback);
|
|
||||||
if (index > -1) {
|
|
||||||
this.callbacks.splice(index, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
import { bareClient } from '.';
|
|
||||||
import { AuthorizationEvent, Callback } from './events';
|
|
||||||
|
|
||||||
export interface AccessTokenMessage {
|
|
||||||
create_at: number;
|
|
||||||
exp: number;
|
|
||||||
email: string;
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
avatar_url: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const TOKEN_KEY = 'affine_token';
|
|
||||||
|
|
||||||
type LoginParams = {
|
|
||||||
type: 'Google' | 'Refresh';
|
|
||||||
token: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type LoginResponse = {
|
|
||||||
// JWT, expires in a very short time
|
|
||||||
token: string;
|
|
||||||
// Refresh token
|
|
||||||
refresh: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const login = (params: LoginParams): Promise<LoginResponse> =>
|
|
||||||
bareClient.post('/api/user/token', { json: params }).json();
|
|
||||||
|
|
||||||
function b64DecodeUnicode(str: string) {
|
|
||||||
// Going backwards: from byte stream, to percent-encoding, to original string.
|
|
||||||
return decodeURIComponent(
|
|
||||||
window
|
|
||||||
.atob(str)
|
|
||||||
.split('')
|
|
||||||
.map(function (c) {
|
|
||||||
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
|
|
||||||
})
|
|
||||||
.join('')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRefreshToken() {
|
|
||||||
try {
|
|
||||||
return localStorage.getItem(TOKEN_KEY) || '';
|
|
||||||
} catch (_) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function setRefreshToken(token: string) {
|
|
||||||
try {
|
|
||||||
localStorage.setItem(TOKEN_KEY, token);
|
|
||||||
} catch (_) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Token {
|
|
||||||
private readonly _event: AuthorizationEvent;
|
|
||||||
private _accessToken: string;
|
|
||||||
private _refreshToken: string;
|
|
||||||
|
|
||||||
private _user: AccessTokenMessage | null;
|
|
||||||
private _padding?: Promise<LoginResponse>;
|
|
||||||
|
|
||||||
constructor(refreshToken?: string) {
|
|
||||||
this._accessToken = '';
|
|
||||||
this._refreshToken = refreshToken || getRefreshToken();
|
|
||||||
this._event = new AuthorizationEvent();
|
|
||||||
|
|
||||||
this._user = Token.parse(this._accessToken);
|
|
||||||
this._event.triggerChange(this._user);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _setToken(login: LoginResponse) {
|
|
||||||
this._accessToken = login.token;
|
|
||||||
this._refreshToken = login.refresh;
|
|
||||||
this._user = Token.parse(login.token);
|
|
||||||
|
|
||||||
this._event.triggerChange(this._user);
|
|
||||||
|
|
||||||
setRefreshToken(login.refresh);
|
|
||||||
}
|
|
||||||
|
|
||||||
async initToken(token: string) {
|
|
||||||
this._setToken(await login({ token, type: 'Google' }));
|
|
||||||
}
|
|
||||||
|
|
||||||
async refreshToken() {
|
|
||||||
if (!this._refreshToken) {
|
|
||||||
throw new Error('No authorization token.');
|
|
||||||
}
|
|
||||||
if (!this._padding) {
|
|
||||||
this._padding = login({
|
|
||||||
type: 'Refresh',
|
|
||||||
token: this._refreshToken,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this._setToken(await this._padding);
|
|
||||||
this._padding = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
get token() {
|
|
||||||
return this._accessToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
get refresh() {
|
|
||||||
return this._refreshToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
get isLogin() {
|
|
||||||
return !!this._refreshToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
get isExpired() {
|
|
||||||
if (!this._user) return true;
|
|
||||||
return Date.now() - this._user.create_at > this._user.exp;
|
|
||||||
}
|
|
||||||
|
|
||||||
static parse(token: string): AccessTokenMessage | null {
|
|
||||||
try {
|
|
||||||
const message: AccessTokenMessage = JSON.parse(
|
|
||||||
b64DecodeUnicode(token.split('.')[1])
|
|
||||||
);
|
|
||||||
return message;
|
|
||||||
} catch (error) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange(callback: Callback) {
|
|
||||||
this._event.onChange(callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
offChange(callback: Callback) {
|
|
||||||
this._event.removeCallback(callback);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const token = new Token();
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export * from './workspace';
|
|
||||||
export * from './workspace.hook';
|
|
||||||
export * from './user';
|
|
||||||
export * from './user.hook';
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export type CommonError = { error: { code: string; message: string } };
|
|
||||||
export type MayError = Partial<CommonError>;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './common';
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import useSWR from 'swr';
|
|
||||||
import type { SWRConfiguration } from 'swr';
|
|
||||||
import { getUserByEmail } from './user';
|
|
||||||
import type { GetUserByEmailParams, User } from './user';
|
|
||||||
|
|
||||||
export const GET_USER_BY_EMAIL_SWR_TOKEN = 'user.getUserByEmail';
|
|
||||||
export function useGetUserByEmail(
|
|
||||||
params: GetUserByEmailParams,
|
|
||||||
config?: SWRConfiguration
|
|
||||||
) {
|
|
||||||
const { data, error, isLoading, mutate } = useSWR<User | null>(
|
|
||||||
[GET_USER_BY_EMAIL_SWR_TOKEN, params],
|
|
||||||
([_, params]) => getUserByEmail(params),
|
|
||||||
config
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
loading: isLoading,
|
|
||||||
data,
|
|
||||||
error,
|
|
||||||
mutate,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
import useSWR from 'swr';
|
|
||||||
import type { SWRConfiguration } from 'swr';
|
|
||||||
import {
|
|
||||||
getWorkspaceDetail,
|
|
||||||
updateWorkspace,
|
|
||||||
deleteWorkspace,
|
|
||||||
inviteMember,
|
|
||||||
Workspace,
|
|
||||||
} from './workspace';
|
|
||||||
import {
|
|
||||||
GetWorkspaceDetailParams,
|
|
||||||
WorkspaceDetail,
|
|
||||||
UpdateWorkspaceParams,
|
|
||||||
DeleteWorkspaceParams,
|
|
||||||
InviteMemberParams,
|
|
||||||
getWorkspaces,
|
|
||||||
} from './workspace';
|
|
||||||
|
|
||||||
export const GET_WORKSPACE_DETAIL_SWR_TOKEN = 'workspace.getWorkspaceDetail';
|
|
||||||
export function useGetWorkspaceDetail(
|
|
||||||
params: GetWorkspaceDetailParams,
|
|
||||||
config?: SWRConfiguration
|
|
||||||
) {
|
|
||||||
const { data, error, isLoading, mutate } = useSWR<WorkspaceDetail | null>(
|
|
||||||
[GET_WORKSPACE_DETAIL_SWR_TOKEN, params],
|
|
||||||
([_, params]) => getWorkspaceDetail(params),
|
|
||||||
config
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
data,
|
|
||||||
error,
|
|
||||||
loading: isLoading,
|
|
||||||
mutate,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const GET_WORKSPACES_SWR_TOKEN = 'workspace.getWorkspaces';
|
|
||||||
export function useGetWorkspaces(config?: SWRConfiguration) {
|
|
||||||
const { data, error, isLoading } = useSWR<Workspace[]>(
|
|
||||||
[GET_WORKSPACES_SWR_TOKEN],
|
|
||||||
() => getWorkspaces(),
|
|
||||||
config
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
data,
|
|
||||||
error,
|
|
||||||
loading: isLoading,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const UPDATE_WORKSPACE_SWR_TOKEN = 'workspace.updateWorkspace';
|
|
||||||
/**
|
|
||||||
* I don't think a hook needed for update workspace.
|
|
||||||
* If you figure out the scene, please implement this function.
|
|
||||||
*/
|
|
||||||
export function useUpdateWorkspace() {}
|
|
||||||
|
|
||||||
export const DELETE_WORKSPACE_SWR_TOKEN = 'workspace.deleteWorkspace';
|
|
||||||
/**
|
|
||||||
* I don't think a hook needed for delete workspace.
|
|
||||||
* If you figure out the scene, please implement this function.
|
|
||||||
*/
|
|
||||||
export function useDeleteWorkspace() {}
|
|
||||||
|
|
||||||
export const INVITE_MEMBER_SWR_TOKEN = 'workspace.inviteMember';
|
|
||||||
/**
|
|
||||||
* I don't think a hook needed for invite member.
|
|
||||||
* If you figure out the scene, please implement this function.
|
|
||||||
*/
|
|
||||||
export function useInviteMember() {}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export { WebsocketProvider } from './y-websocket';
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
|
||||||
import { getDataCenter } from './utils.js';
|
|
||||||
|
|
||||||
test('can init data center', async () => {
|
|
||||||
const dataCenter = await getDataCenter();
|
|
||||||
|
|
||||||
expect(dataCenter).toBeTruthy();
|
|
||||||
});
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export const getDataCenter = () =>
|
|
||||||
import('../src/data-center.js').then(async dataCenter =>
|
|
||||||
dataCenter.getDataCenter()
|
|
||||||
);
|
|
||||||
Generated
+996
-967
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,11 @@
|
|||||||
import { test } from '@playwright/test';
|
import { test } from '@playwright/test';
|
||||||
|
import type { Page } from '@playwright/test';
|
||||||
|
|
||||||
|
interface IType {
|
||||||
|
page: Page;
|
||||||
|
}
|
||||||
export function loadPage() {
|
export function loadPage() {
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }: IType) => {
|
||||||
await page.goto('http://localhost:8080');
|
await page.goto('http://localhost:8080');
|
||||||
// waiting for page loading end
|
// waiting for page loading end
|
||||||
// await page.waitForTimeout(1000);
|
// await page.waitForTimeout(1000);
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
export async function newPage(page) {
|
import type { Page } from '@playwright/test';
|
||||||
|
|
||||||
|
export async function newPage(page: Page) {
|
||||||
return page.getByTestId('sliderBar').getByText('New Page').click();
|
return page.getByTestId('sliderBar').getByText('New Page').click();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function clickPageMoreActions(page) {
|
export async function clickPageMoreActions(page: Page) {
|
||||||
return page
|
return page
|
||||||
.getByTestId('editor-header-items')
|
.getByTestId('editor-header-items')
|
||||||
.getByRole('button')
|
.getByRole('button')
|
||||||
|
|||||||
+17
-16
@@ -24,21 +24,22 @@ test.describe('Login Flow', () => {
|
|||||||
.click();
|
.click();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Open google firebase page', async ({ page }) => {
|
// not stable
|
||||||
await page.getByTestId('current-workspace').click();
|
// test.skip('Open google firebase page', async ({ page }) => {
|
||||||
await page.waitForTimeout(800);
|
// await page.getByTestId('current-workspace').click();
|
||||||
// why don't we use waitForSelector, It seems that waitForSelector not stable?
|
// await page.waitForTimeout(800);
|
||||||
await page.getByTestId('open-login-modal').click();
|
// // why don't we use waitForSelector, It seems that waitForSelector not stable?
|
||||||
await page.waitForTimeout(800);
|
// await page.getByTestId('open-login-modal').click();
|
||||||
const [firebasePage] = await Promise.all([
|
// await page.waitForTimeout(800);
|
||||||
page.waitForEvent('popup'),
|
// const [firebasePage] = await Promise.all([
|
||||||
page
|
// page.waitForEvent('popup'),
|
||||||
.getByRole('button', {
|
// page
|
||||||
name: 'Google Continue with Google Set up an AFFiNE account to sync data',
|
// .getByRole('button', {
|
||||||
})
|
// name: 'Google Continue with Google Set up an AFFiNE account to sync data',
|
||||||
.click(),
|
// })
|
||||||
]);
|
// .click(),
|
||||||
|
// ]);
|
||||||
|
|
||||||
expect(firebasePage.url()).toContain('.firebaseapp.com/__/auth/handler');
|
// expect(firebasePage.url()).toContain('.firebaseapp.com/__/auth/handler');
|
||||||
});
|
// });
|
||||||
});
|
});
|
||||||
|
|||||||
+29
-51
@@ -1,38 +1,26 @@
|
|||||||
import { test, expect, type Page } from '@playwright/test';
|
import { test, expect, type Page } from '@playwright/test';
|
||||||
import { loadPage } from './libs/load-page';
|
import { loadPage } from './libs/load-page';
|
||||||
import { withCtrlOrMeta } from './libs/keyboard';
|
import { withCtrlOrMeta } from './libs/keyboard';
|
||||||
|
import { newPage } from './libs/page-logic';
|
||||||
loadPage();
|
loadPage();
|
||||||
|
|
||||||
const openQuickSearchByShortcut = async (page: Page) =>
|
const openQuickSearchByShortcut = async (page: Page) =>
|
||||||
await withCtrlOrMeta(page, () => page.keyboard.press('k', { delay: 50 }));
|
await withCtrlOrMeta(page, () => page.keyboard.press('k', { delay: 50 }));
|
||||||
|
|
||||||
async function assertTitleTexts(
|
async function assertTitleTexts(page: Page, texts: string[]) {
|
||||||
page: Page,
|
|
||||||
texts: string[],
|
|
||||||
option?: { delay: number }
|
|
||||||
) {
|
|
||||||
await page.mouse.move(100, 100);
|
|
||||||
const actual = await page
|
const actual = await page
|
||||||
.locator('[class=affine-default-page-block-title]')
|
.locator('.affine-default-page-block-title')
|
||||||
.allInnerTexts();
|
.allTextContents();
|
||||||
await page.waitForTimeout(option?.delay || 0);
|
|
||||||
|
|
||||||
expect(actual).toEqual(texts);
|
expect(actual).toEqual(texts);
|
||||||
}
|
}
|
||||||
async function assertResultList(
|
async function assertResultList(page: Page, texts: string[]) {
|
||||||
page: Page,
|
|
||||||
texts: string[],
|
|
||||||
option?: { delay: number }
|
|
||||||
) {
|
|
||||||
await page.mouse.move(100, 100);
|
|
||||||
const actual = await page.locator('[cmdk-item]').allInnerTexts();
|
const actual = await page.locator('[cmdk-item]').allInnerTexts();
|
||||||
await page.waitForTimeout(option?.delay || 0);
|
|
||||||
expect(actual).toEqual(texts);
|
expect(actual).toEqual(texts);
|
||||||
}
|
}
|
||||||
|
|
||||||
test.describe('Open quick search', () => {
|
test.describe('Open quick search', () => {
|
||||||
test('Click slider bar button', async ({ page }) => {
|
test('Click slider bar button', async ({ page }) => {
|
||||||
|
await newPage(page);
|
||||||
const quickSearchButton = page.locator(
|
const quickSearchButton = page.locator(
|
||||||
'[data-testid=sliderBar-quickSearchButton]'
|
'[data-testid=sliderBar-quickSearchButton]'
|
||||||
);
|
);
|
||||||
@@ -42,7 +30,7 @@ test.describe('Open quick search', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Click arrowDown icon after title', async ({ page }) => {
|
test('Click arrowDown icon after title', async ({ page }) => {
|
||||||
//header-quickSearchButton
|
await newPage(page);
|
||||||
const quickSearchButton = page.locator(
|
const quickSearchButton = page.locator(
|
||||||
'[data-testid=header-quickSearchButton]'
|
'[data-testid=header-quickSearchButton]'
|
||||||
);
|
);
|
||||||
@@ -51,58 +39,48 @@ test.describe('Open quick search', () => {
|
|||||||
await expect(quickSearch).toBeVisible();
|
await expect(quickSearch).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
// test('Press the shortcut key cmd+k', async ({ page }) => {
|
test('Press the shortcut key cmd+k', async ({ page }) => {
|
||||||
// // why 1000ms page wait?
|
await newPage(page);
|
||||||
// page.waitForTimeout(1000);
|
await openQuickSearchByShortcut(page);
|
||||||
// await openQuickSearchByShortcut(page);
|
const quickSearch = page.locator('[data-testid=quickSearch]');
|
||||||
// const quickSearch = page.locator('[data-testid=quickSearch]');
|
await expect(quickSearch).toBeVisible();
|
||||||
|
});
|
||||||
// await expect(quickSearch).toBeVisible();
|
|
||||||
// });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe('Add new page in quick search', () => {
|
test.describe('Add new page in quick search', () => {
|
||||||
// FIXME: not working
|
// FIXME: not working
|
||||||
test.skip('Create a new page without keyword', async ({ page }) => {
|
test('Create a new page without keyword', async ({ page }) => {
|
||||||
|
await newPage(page);
|
||||||
await openQuickSearchByShortcut(page);
|
await openQuickSearchByShortcut(page);
|
||||||
const addNewPage = page.locator('[data-testid=quickSearch-addNewPage]');
|
const addNewPage = page.locator('[data-testid=quickSearch-addNewPage]');
|
||||||
await addNewPage.click();
|
await addNewPage.click();
|
||||||
await assertTitleTexts(page, [''], { delay: 50 });
|
await page.waitForTimeout(200);
|
||||||
|
await assertTitleTexts(page, ['']);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.skip('Create a new page with keyword', async ({ page }) => {
|
test('Create a new page with keyword', async ({ page }) => {
|
||||||
|
await newPage(page);
|
||||||
await openQuickSearchByShortcut(page);
|
await openQuickSearchByShortcut(page);
|
||||||
await page.keyboard.insertText('test');
|
await page.keyboard.insertText('test123456');
|
||||||
const addNewPage = page.locator('[data-testid=quickSearch-addNewPage]');
|
const addNewPage = page.locator('[data-testid=quickSearch-addNewPage]');
|
||||||
await addNewPage.click();
|
await addNewPage.click();
|
||||||
await assertTitleTexts(page, ['test'], { delay: 50 });
|
await page.waitForTimeout(200);
|
||||||
|
await assertTitleTexts(page, ['test123456']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe('Search and select', () => {
|
test.describe('Search and select', () => {
|
||||||
// test('Search and get results', async ({ page }) => {
|
test('Create a new page and search this page', async ({ page }) => {
|
||||||
// // why 1000ms page wait?
|
await newPage(page);
|
||||||
// await openQuickSearchByShortcut(page);
|
|
||||||
// await page.keyboard.insertText('Welcome');
|
|
||||||
// await assertResultList(page, ['Welcome to the AFFiNE Alpha'], {
|
|
||||||
// delay: 50,
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
//TODO FIXME: This test is not working
|
|
||||||
test.skip('Create a new page and search this page', async ({ page }) => {
|
|
||||||
await openQuickSearchByShortcut(page);
|
await openQuickSearchByShortcut(page);
|
||||||
await page.keyboard.insertText('Welcome');
|
await page.keyboard.insertText('test123456');
|
||||||
const addNewPage = page.locator('[data-testid=quickSearch-addNewPage]');
|
const addNewPage = page.locator('[data-testid=quickSearch-addNewPage]');
|
||||||
await addNewPage.click();
|
await addNewPage.click();
|
||||||
await page.waitForTimeout(500);
|
await page.waitForTimeout(200);
|
||||||
await openQuickSearchByShortcut(page);
|
await openQuickSearchByShortcut(page);
|
||||||
await page.keyboard.insertText('Welcome');
|
await page.keyboard.insertText('test123456');
|
||||||
await assertResultList(page, ['Welcome to the AFFiNE Alpha', 'Welcome']);
|
await assertResultList(page, ['test123456']);
|
||||||
|
|
||||||
await page.keyboard.press('ArrowDown', { delay: 50 });
|
|
||||||
await page.keyboard.press('Enter', { delay: 50 });
|
await page.keyboard.press('Enter', { delay: 50 });
|
||||||
await assertTitleTexts(page, ['Welcome'], {
|
await assertTitleTexts(page, ['test123456']);
|
||||||
delay: 50,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user