mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
Merge remote-tracking branch 'refs/remotes/origin/feat/cloud-sync'
Conflicts: packages/data-center/src/provider/base.ts
This commit is contained in:
@@ -5,7 +5,7 @@
|
|||||||
"fixed": [],
|
"fixed": [],
|
||||||
"linked": [],
|
"linked": [],
|
||||||
"access": "restricted",
|
"access": "restricted",
|
||||||
"baseBranch": "feat/filesystem_and_search",
|
"baseBranch": "feat/cloud-sync",
|
||||||
"updateInternalDependencies": "patch",
|
"updateInternalDependencies": "patch",
|
||||||
"ignore": []
|
"ignore": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,3 +4,5 @@
|
|||||||
**/node_modules/**
|
**/node_modules/**
|
||||||
.github/**
|
.github/**
|
||||||
**/__tests__/**
|
**/__tests__/**
|
||||||
|
**/tests/**
|
||||||
|
|
||||||
|
|||||||
4
.github/ISSUE_TEMPLATE/question.md
vendored
4
.github/ISSUE_TEMPLATE/question.md
vendored
@@ -1,9 +1,7 @@
|
|||||||
---
|
---
|
||||||
name: I have a question
|
name: I have a question
|
||||||
about: Feel free to ask us your questions!
|
about: Feel free to ask us your questions!
|
||||||
title: "[Question]"
|
title: '[Question]'
|
||||||
labels: ''
|
labels: ''
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/changlog.yml
vendored
2
.github/workflows/changlog.yml
vendored
@@ -2,7 +2,7 @@ name: Pathfinder changelog
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [feat/filesystem_and_search, master]
|
branches: [feat/cloud-sync, master]
|
||||||
|
|
||||||
# Cancels all previous workflow runs for pull requests that have not completed.
|
# Cancels all previous workflow runs for pull requests that have not completed.
|
||||||
# See https://docs.github.com/en/actions/using-jobs/using-concurrency
|
# See https://docs.github.com/en/actions/using-jobs/using-concurrency
|
||||||
|
|||||||
128
.github/workflows/temp_test.yml
vendored
Normal file
128
.github/workflows/temp_test.yml
vendored
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
name: Pathfinder Check
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [feat/cloud-sync]
|
||||||
|
pull_request:
|
||||||
|
branches: [feat/cloud-sync]
|
||||||
|
|
||||||
|
# Cancels all previous workflow runs for pull requests that have not completed.
|
||||||
|
# See https://docs.github.com/en/actions/using-jobs/using-concurrency
|
||||||
|
concurrency:
|
||||||
|
# The concurrency group contains the workflow name and the branch name for
|
||||||
|
# pull requests or the commit hash for any other events.
|
||||||
|
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build on Pull Request
|
||||||
|
if: github.ref != 'refs/heads/master'
|
||||||
|
runs-on: self-hosted
|
||||||
|
environment: development
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: pnpm/action-setup@v2
|
||||||
|
with:
|
||||||
|
version: 'latest'
|
||||||
|
|
||||||
|
- name: Use Node.js
|
||||||
|
uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: 18.x
|
||||||
|
registry-url: https://npm.pkg.github.com
|
||||||
|
scope: '@toeverything'
|
||||||
|
cache: 'pnpm'
|
||||||
|
|
||||||
|
- run: node scripts/module-resolve/ci.js
|
||||||
|
|
||||||
|
- name: Restore cache
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
.next/cache
|
||||||
|
# Generate a new cache whenever packages or source files change.
|
||||||
|
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }}
|
||||||
|
# If source files changed but packages didn't, rebuild from a prior cache.
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}-
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --no-frozen-lockfile
|
||||||
|
env:
|
||||||
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_GITHUB_AUTH_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: pnpm build
|
||||||
|
env:
|
||||||
|
NEXT_PUBLIC_FIREBASE_API_KEY: ${{ secrets.NEXT_PUBLIC_FIREBASE_API_KEY }}
|
||||||
|
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN: ${{ secrets.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN }}
|
||||||
|
NEXT_PUBLIC_FIREBASE_PROJECT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_PROJECT_ID }}
|
||||||
|
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET: ${{ secrets.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET }}
|
||||||
|
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID }}
|
||||||
|
NEXT_PUBLIC_FIREBASE_APP_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_APP_ID }}
|
||||||
|
NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID }}
|
||||||
|
|
||||||
|
- name: Export
|
||||||
|
run: pnpm export
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
path: ./packages/app/.next
|
||||||
|
|
||||||
|
lint:
|
||||||
|
name: Lint and E2E Test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment: development
|
||||||
|
needs: build
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- uses: pnpm/action-setup@v2
|
||||||
|
with:
|
||||||
|
version: 'latest'
|
||||||
|
|
||||||
|
- name: Use Node.js
|
||||||
|
uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: 18.x
|
||||||
|
cache: 'pnpm'
|
||||||
|
|
||||||
|
- name: Restore cache
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
.next/cache
|
||||||
|
# Generate a new cache whenever packages or source files change.
|
||||||
|
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }}
|
||||||
|
# If source files changed but packages didn't, rebuild from a prior cache.
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}-
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install
|
||||||
|
env:
|
||||||
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_GITHUB_AUTH_TOKEN }}
|
||||||
|
|
||||||
|
- name: Download artifact
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: artifact
|
||||||
|
path: packages/app/.next/
|
||||||
|
|
||||||
|
- name: Lint & E2E Test
|
||||||
|
run: |
|
||||||
|
pnpm lint --max-warnings=0
|
||||||
|
PLAYWRIGHT_BROWSERS_PATH=0 npx playwright install chromium
|
||||||
|
PLAYWRIGHT_BROWSERS_PATH=0 pnpm test
|
||||||
|
PLAYWRIGHT_BROWSERS_PATH=0 pnpm test:dc
|
||||||
|
env:
|
||||||
|
NEXT_PUBLIC_FIREBASE_API_KEY: ${{ secrets.NEXT_PUBLIC_FIREBASE_API_KEY }}
|
||||||
|
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN: ${{ secrets.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN }}
|
||||||
|
NEXT_PUBLIC_FIREBASE_PROJECT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_PROJECT_ID }}
|
||||||
|
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET: ${{ secrets.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET }}
|
||||||
|
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID }}
|
||||||
|
NEXT_PUBLIC_FIREBASE_APP_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_APP_ID }}
|
||||||
|
NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID: ${{ secrets.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID }}
|
||||||
@@ -18,7 +18,8 @@
|
|||||||
"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",
|
||||||
"notify": "node --experimental-modules scripts/notify.mjs"
|
"notify": "node --experimental-modules scripts/notify.mjs",
|
||||||
|
"check:ci": "pnpm lint & pnpm test"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*": "prettier --write --ignore-unknown",
|
"*": "prettier --write --ignore-unknown",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { getWarningMessage, shouldShowWarning } from './utils';
|
|||||||
import EditorOptionMenu from './header-right-items/editor-option-menu';
|
import EditorOptionMenu from './header-right-items/editor-option-menu';
|
||||||
import TrashButtonGroup from './header-right-items/trash-button-group';
|
import TrashButtonGroup from './header-right-items/trash-button-group';
|
||||||
import ThemeModeSwitch from './header-right-items/theme-mode-switch';
|
import ThemeModeSwitch from './header-right-items/theme-mode-switch';
|
||||||
// import SyncUser from './header-right-items/sync-user';
|
import SyncUser from './header-right-items/sync-user';
|
||||||
|
|
||||||
const BrowserWarning = ({
|
const BrowserWarning = ({
|
||||||
show,
|
show,
|
||||||
@@ -40,7 +40,7 @@ const HeaderRightItems: Record<HeaderRightItemNames, ReactNode> = {
|
|||||||
editorOptionMenu: <EditorOptionMenu key="editorOptionMenu" />,
|
editorOptionMenu: <EditorOptionMenu key="editorOptionMenu" />,
|
||||||
trashButtonGroup: <TrashButtonGroup key="trashButtonGroup" />,
|
trashButtonGroup: <TrashButtonGroup key="trashButtonGroup" />,
|
||||||
themeModeSwitch: <ThemeModeSwitch key="themeModeSwitch" />,
|
themeModeSwitch: <ThemeModeSwitch key="themeModeSwitch" />,
|
||||||
syncUser: null,
|
syncUser: <SyncUser key="syncUser" />,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Header = ({
|
export const Header = ({
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ import {
|
|||||||
StyledArrowButton,
|
StyledArrowButton,
|
||||||
StyledLink,
|
StyledLink,
|
||||||
StyledListItem,
|
StyledListItem,
|
||||||
// StyledListItemForWorkspace,
|
StyledListItemForWorkspace,
|
||||||
StyledNewPageButton,
|
StyledNewPageButton,
|
||||||
StyledSliderBar,
|
StyledSliderBar,
|
||||||
StyledSliderBarWrapper,
|
StyledSliderBarWrapper,
|
||||||
StyledSubListItem,
|
StyledSubListItem,
|
||||||
} from './style';
|
} from './style';
|
||||||
import { Arrow } from './icons';
|
import { Arrow } from './icons';
|
||||||
|
import { WorkspaceSelector } from './WorkspaceSelector';
|
||||||
import Collapse from '@mui/material/Collapse';
|
import Collapse from '@mui/material/Collapse';
|
||||||
import {
|
import {
|
||||||
ArrowDownIcon,
|
ArrowDownIcon,
|
||||||
@@ -26,7 +27,6 @@ import { Tooltip } from '@/ui/tooltip';
|
|||||||
import { useModal } from '@/providers/global-modal-provider';
|
import { useModal } from '@/providers/global-modal-provider';
|
||||||
import { useAppState } from '@/providers/app-state-provider/context';
|
import { useAppState } from '@/providers/app-state-provider/context';
|
||||||
import { IconButton } from '@/ui/button';
|
import { IconButton } from '@/ui/button';
|
||||||
// import { WorkspaceSelector } from './WorkspaceSelector';
|
|
||||||
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';
|
||||||
@@ -109,9 +109,9 @@ export const WorkSpaceSliderBar = () => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<StyledSliderBarWrapper data-testid="sliderBar">
|
<StyledSliderBarWrapper data-testid="sliderBar">
|
||||||
{/* <StyledListItemForWorkspace>
|
<StyledListItemForWorkspace>
|
||||||
<WorkspaceSelector />
|
<WorkspaceSelector />
|
||||||
</StyledListItemForWorkspace> */}
|
</StyledListItemForWorkspace>
|
||||||
<StyledListItem
|
<StyledListItem
|
||||||
data-testid="sliderBar-quickSearchButton"
|
data-testid="sliderBar-quickSearchButton"
|
||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: 'pointer' }}
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import { styled } from '@/styles';
|
||||||
|
|
||||||
|
import { ReactElement, ReactNode } from 'react';
|
||||||
|
import WorkspaceLayout from '@/components/workspace-layout';
|
||||||
|
import { Button } from '@/ui/button';
|
||||||
|
|
||||||
|
export const FeatureCardDiv = styled('section')({
|
||||||
|
width: '800px',
|
||||||
|
border: '1px #eee solid',
|
||||||
|
margin: '20px auto',
|
||||||
|
minHeight: '100px',
|
||||||
|
padding: '15px',
|
||||||
|
});
|
||||||
|
const FeatureCard = (props: {
|
||||||
|
name: string;
|
||||||
|
children: ReactNode | ReactNode[];
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<FeatureCardDiv>
|
||||||
|
<h1>Feature - {props.name}</h1>
|
||||||
|
{props.children}
|
||||||
|
</FeatureCardDiv>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Playground = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FeatureCard name="Account">
|
||||||
|
<Button>Sign In</Button>
|
||||||
|
<Button>Sign Out</Button>
|
||||||
|
</FeatureCard>
|
||||||
|
|
||||||
|
<FeatureCard name="Workspace List">
|
||||||
|
<ul>
|
||||||
|
<li>AFFiNE Demo</li>
|
||||||
|
<li>AFFiNE XXX</li>
|
||||||
|
</ul>
|
||||||
|
<Button>New Workspace</Button>
|
||||||
|
</FeatureCard>
|
||||||
|
|
||||||
|
<FeatureCard name="Active Workspace">
|
||||||
|
<div>Workspace Name /[Workspace Members Count]/[Workspace Avatar]</div>
|
||||||
|
<div>Cloud Sync [Yes/No]</div>
|
||||||
|
<div>Auth [Public/Private]</div>
|
||||||
|
<div>
|
||||||
|
<Button>Update Workspace Name</Button>
|
||||||
|
<Button>Upload Workspace Avatar</Button>
|
||||||
|
<Button>Update Workspace Avatar</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Button>Leave Workspace</Button>
|
||||||
|
<Button>Delete Workspace </Button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
Cloud Sync <Button>Enalbe</Button>
|
||||||
|
<Button>Disable</Button>
|
||||||
|
</div>
|
||||||
|
</FeatureCard>
|
||||||
|
|
||||||
|
<FeatureCard name="Workspace Members">
|
||||||
|
<Button>Add Member</Button>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
terrychinaz@gmail <button>Delete Members</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</FeatureCard>
|
||||||
|
|
||||||
|
<FeatureCard name="Cloud Search">
|
||||||
|
<input type="text" value="AFFiNE Keywords" />
|
||||||
|
<Button>Search</Button>
|
||||||
|
<ul></ul>
|
||||||
|
</FeatureCard>
|
||||||
|
|
||||||
|
<FeatureCard name="Import/Exeport Worpsace">
|
||||||
|
<div>Workspace Name</div>
|
||||||
|
<Button> Export Workspace</Button>
|
||||||
|
<Button> Import Workspace</Button>
|
||||||
|
</FeatureCard>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Playground.getLayout = function getLayout(page: ReactElement) {
|
||||||
|
return <WorkspaceLayout>{page}</WorkspaceLayout>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Playground;
|
||||||
@@ -60,7 +60,9 @@ class Token {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async initToken(token: string) {
|
async initToken(token: string) {
|
||||||
this._setToken(await login({ token, type: 'Google' }));
|
const tokens = await login({ token, type: 'Google' });
|
||||||
|
this._setToken(tokens);
|
||||||
|
return this._user;
|
||||||
}
|
}
|
||||||
|
|
||||||
async refreshToken(token?: string) {
|
async refreshToken(token?: string) {
|
||||||
@@ -153,10 +155,27 @@ export const getAuthorizer = () => {
|
|||||||
|
|
||||||
const googleAuthProvider = new GoogleAuthProvider();
|
const googleAuthProvider = new GoogleAuthProvider();
|
||||||
|
|
||||||
|
const getToken = async () => {
|
||||||
|
const currentUser = firebaseAuth.currentUser;
|
||||||
|
if (currentUser) {
|
||||||
|
await currentUser.getIdTokenResult(true);
|
||||||
|
if (!currentUser.isAnonymous) {
|
||||||
|
return currentUser.getIdToken();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
const signInWithGoogle = async () => {
|
const signInWithGoogle = async () => {
|
||||||
const user = await signInWithPopup(firebaseAuth, googleAuthProvider);
|
const idToken = await getToken();
|
||||||
const idToken = await user.user.getIdToken();
|
if (idToken) {
|
||||||
await token.initToken(idToken);
|
await token.initToken(idToken);
|
||||||
|
} else {
|
||||||
|
const user = await signInWithPopup(firebaseAuth, googleAuthProvider);
|
||||||
|
const idToken = await user.user.getIdToken();
|
||||||
|
await token.initToken(idToken);
|
||||||
|
}
|
||||||
|
return firebaseAuth.currentUser;
|
||||||
};
|
};
|
||||||
|
|
||||||
const onAuthStateChanged = (callback: (user: User | null) => void) => {
|
const onAuthStateChanged = (callback: (user: User | null) => void) => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import assert from 'assert';
|
import assert from 'assert';
|
||||||
import { BlockSchema } from '@blocksuite/blocks/models';
|
import { BlockSchema } from '@blocksuite/blocks/models';
|
||||||
import { Workspace } from '@blocksuite/store';
|
import { Workspace, Signal } from '@blocksuite/store';
|
||||||
|
|
||||||
import { getLogger } from './index.js';
|
import { getLogger } from './index.js';
|
||||||
import { getApis, Apis } from './apis/index.js';
|
import { getApis, Apis } from './apis/index.js';
|
||||||
@@ -16,6 +16,17 @@ type LoadConfig = {
|
|||||||
config?: Record<string, any>;
|
config?: Record<string, any>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type DataCenterSignals = DataCenter['signals'];
|
||||||
|
type WorkspaceItem = {
|
||||||
|
// provider id
|
||||||
|
provider: string;
|
||||||
|
// data exists locally
|
||||||
|
locally: boolean;
|
||||||
|
};
|
||||||
|
type WorkspaceLoadEvent = WorkspaceItem & {
|
||||||
|
workspace: string;
|
||||||
|
};
|
||||||
|
|
||||||
export class DataCenter {
|
export class DataCenter {
|
||||||
private readonly _apis: Apis;
|
private readonly _apis: Apis;
|
||||||
private readonly _providers = new Map<string, typeof BaseProvider>();
|
private readonly _providers = new Map<string, typeof BaseProvider>();
|
||||||
@@ -23,6 +34,11 @@ export class DataCenter {
|
|||||||
private readonly _config;
|
private readonly _config;
|
||||||
private readonly _logger;
|
private readonly _logger;
|
||||||
|
|
||||||
|
readonly signals = {
|
||||||
|
listAdd: new Signal<WorkspaceLoadEvent>(),
|
||||||
|
listRemove: new Signal<string>(),
|
||||||
|
};
|
||||||
|
|
||||||
static async init(debug: boolean): Promise<DataCenter> {
|
static async init(debug: boolean): Promise<DataCenter> {
|
||||||
const dc = new DataCenter(debug);
|
const dc = new DataCenter(debug);
|
||||||
dc.addProvider(AffineProvider);
|
dc.addProvider(AffineProvider);
|
||||||
@@ -36,6 +52,16 @@ export class DataCenter {
|
|||||||
this._config = getKVConfigure('sys');
|
this._config = getKVConfigure('sys');
|
||||||
this._logger = getLogger('dc');
|
this._logger = getLogger('dc');
|
||||||
this._logger.enabled = debug;
|
this._logger.enabled = debug;
|
||||||
|
|
||||||
|
this.signals.listAdd.on(e => {
|
||||||
|
this._config.set(`list:${e.workspace}`, {
|
||||||
|
provider: e.provider,
|
||||||
|
locally: e.locally,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this.signals.listRemove.on(workspace => {
|
||||||
|
this._config.delete(`list:${workspace}`);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
get apis(): Readonly<Apis> {
|
get apis(): Readonly<Apis> {
|
||||||
@@ -86,9 +112,9 @@ export class DataCenter {
|
|||||||
await provider.init({
|
await provider.init({
|
||||||
apis: this._apis,
|
apis: this._apis,
|
||||||
config,
|
config,
|
||||||
globalConfig: getKVConfigure(`provider:${providerId}`),
|
|
||||||
debug: this._logger.enabled,
|
debug: this._logger.enabled,
|
||||||
logger: this._logger.extend(`${Provider.id}:${id}`),
|
logger: this._logger.extend(`${Provider.id}:${id}`),
|
||||||
|
signals: this.signals,
|
||||||
workspace,
|
workspace,
|
||||||
});
|
});
|
||||||
await provider.initData();
|
await provider.initData();
|
||||||
@@ -97,6 +123,21 @@ export class DataCenter {
|
|||||||
return provider;
|
return provider;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async auth(providerId: string, globalConfig?: Record<string, any>) {
|
||||||
|
const Provider = this._providers.get(providerId);
|
||||||
|
if (Provider) {
|
||||||
|
// initial configurator
|
||||||
|
const config = getKVConfigure(`provider:${providerId}`);
|
||||||
|
// set workspace configs
|
||||||
|
const values = Object.entries(globalConfig || {});
|
||||||
|
if (values.length) await config.setMany(values);
|
||||||
|
|
||||||
|
const logger = this._logger.extend(`auth:${providerId}`);
|
||||||
|
logger.enabled = this._logger.enabled;
|
||||||
|
await Provider.auth(config, logger, this.signals);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* load workspace data to memory
|
* load workspace data to memory
|
||||||
* @param workspaceId workspace id
|
* @param workspaceId workspace id
|
||||||
@@ -150,24 +191,18 @@ export class DataCenter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get workspace list
|
* get workspace list,return a map of workspace id and data state
|
||||||
|
* data state is also map, the key is the provider id, and the data exists locally when the value is true, otherwise it does not exist
|
||||||
*/
|
*/
|
||||||
async list(): Promise<Record<string, Record<string, boolean>>> {
|
async list(): Promise<Record<string, Record<string, boolean>>> {
|
||||||
const lists = await Promise.all(
|
const entries: [string, WorkspaceItem][] = await this._config.entries();
|
||||||
Array.from(this._providers.entries()).map(([providerId, provider]) =>
|
return entries.reduce((acc, [k, i]) => {
|
||||||
provider
|
if (k.startsWith('list:')) {
|
||||||
.list(getKVConfigure(`provider:${providerId}`))
|
const key = k.slice(5);
|
||||||
.then(list => [providerId, list || []] as const)
|
acc[key] = acc[key] || {};
|
||||||
)
|
acc[key][i.provider] = i.locally;
|
||||||
);
|
|
||||||
|
|
||||||
return lists.reduce((ret, [providerId, list]) => {
|
|
||||||
for (const [item, isLocal] of list) {
|
|
||||||
const workspace = ret[item] || {};
|
|
||||||
workspace[providerId] = isLocal;
|
|
||||||
ret[item] = workspace;
|
|
||||||
}
|
}
|
||||||
return ret;
|
return acc;
|
||||||
}, {} as Record<string, Record<string, boolean>>);
|
}, {} as Record<string, Record<string, boolean>>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,17 @@ const _initializeDataCenter = () => {
|
|||||||
return (debug = true) => {
|
return (debug = true) => {
|
||||||
if (!_dataCenterInstance) {
|
if (!_dataCenterInstance) {
|
||||||
_dataCenterInstance = DataCenter.init(debug);
|
_dataCenterInstance = DataCenter.init(debug);
|
||||||
|
_dataCenterInstance.then(dc => {
|
||||||
|
try {
|
||||||
|
if (window) {
|
||||||
|
(window as any).dc = dc;
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
return dc;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return _dataCenterInstance;
|
return _dataCenterInstance;
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
import assert from 'assert';
|
import assert from 'assert';
|
||||||
import { applyUpdate } from 'yjs';
|
import { applyUpdate, Doc } from 'yjs';
|
||||||
|
|
||||||
import type { InitialParams } from '../index.js';
|
import type {
|
||||||
import { token, Callback } from '../../apis/index.js';
|
ConfigStore,
|
||||||
|
DataCenterSignals,
|
||||||
|
InitialParams,
|
||||||
|
Logger,
|
||||||
|
} from '../index.js';
|
||||||
|
import { token, Callback, getApis } from '../../apis/index.js';
|
||||||
import { LocalProvider } from '../local/index.js';
|
import { LocalProvider } from '../local/index.js';
|
||||||
|
|
||||||
import { WebsocketProvider } from './sync.js';
|
import { WebsocketProvider } from './sync.js';
|
||||||
|
import { IndexedDBProvider } from '../local/indexeddb.js';
|
||||||
|
|
||||||
export class AffineProvider extends LocalProvider {
|
export class AffineProvider extends LocalProvider {
|
||||||
static id = 'affine';
|
static id = 'affine';
|
||||||
@@ -55,7 +61,14 @@ export class AffineProvider extends LocalProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async initData() {
|
async initData() {
|
||||||
await super.initData();
|
const databases = await indexedDB.databases();
|
||||||
|
await super.initData(
|
||||||
|
// set locally to true if exists a same name db
|
||||||
|
databases
|
||||||
|
.map(db => db.name)
|
||||||
|
.filter(v => v)
|
||||||
|
.includes(this._workspace.room)
|
||||||
|
);
|
||||||
|
|
||||||
const workspace = this._workspace;
|
const workspace = this._workspace;
|
||||||
const doc = workspace.doc;
|
const doc = workspace.doc;
|
||||||
@@ -64,23 +77,29 @@ export class AffineProvider extends LocalProvider {
|
|||||||
|
|
||||||
if (workspace.room && token.isLogin) {
|
if (workspace.room && token.isLogin) {
|
||||||
try {
|
try {
|
||||||
const updates = await this._apis.downloadWorkspace(workspace.room);
|
// init data from cloud
|
||||||
if (updates) {
|
await AffineProvider._initCloudDoc(
|
||||||
await new Promise(resolve => {
|
workspace.room,
|
||||||
doc.once('update', resolve);
|
doc,
|
||||||
applyUpdate(doc, new Uint8Array(updates));
|
this._logger,
|
||||||
});
|
this._signals
|
||||||
// Wait for ws synchronization to complete, otherwise the data will be modified in reverse, which can be optimized later
|
);
|
||||||
this._ws = new WebsocketProvider('/', workspace.room, doc);
|
|
||||||
await new Promise<void>((resolve, reject) => {
|
// Wait for ws synchronization to complete, otherwise the data will be modified in reverse, which can be optimized later
|
||||||
// TODO: synced will also be triggered on reconnection after losing sync
|
this._ws = new WebsocketProvider('/', workspace.room, doc);
|
||||||
// There needs to be an event mechanism to emit the synchronization state to the upper layer
|
await new Promise<void>((resolve, reject) => {
|
||||||
assert(this._ws);
|
// TODO: synced will also be triggered on reconnection after losing sync
|
||||||
this._ws.once('synced', () => resolve());
|
// There needs to be an event mechanism to emit the synchronization state to the upper layer
|
||||||
this._ws.once('lost-connection', () => resolve());
|
assert(this._ws);
|
||||||
this._ws.once('connection-error', () => reject());
|
this._ws.once('synced', () => resolve());
|
||||||
});
|
this._ws.once('lost-connection', () => resolve());
|
||||||
}
|
this._ws.once('connection-error', () => reject());
|
||||||
|
});
|
||||||
|
this._signals.listAdd.emit({
|
||||||
|
workspace: workspace.room,
|
||||||
|
provider: this.id,
|
||||||
|
locally: true,
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this._logger('Failed to init cloud workspace', e);
|
this._logger('Failed to init cloud workspace', e);
|
||||||
}
|
}
|
||||||
@@ -91,4 +110,66 @@ export class AffineProvider extends LocalProvider {
|
|||||||
// just a workaround for yjs
|
// just a workaround for yjs
|
||||||
doc.getMap('space:meta');
|
doc.getMap('space:meta');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async _initCloudDoc(
|
||||||
|
workspace: string,
|
||||||
|
doc: Doc,
|
||||||
|
logger: Logger,
|
||||||
|
signals: DataCenterSignals
|
||||||
|
) {
|
||||||
|
const apis = getApis();
|
||||||
|
logger(`Loading ${workspace}...`);
|
||||||
|
const updates = await apis.downloadWorkspace(workspace);
|
||||||
|
if (updates) {
|
||||||
|
await new Promise(resolve => {
|
||||||
|
doc.once('update', resolve);
|
||||||
|
applyUpdate(doc, new Uint8Array(updates));
|
||||||
|
});
|
||||||
|
logger(`Loaded: ${workspace}`);
|
||||||
|
|
||||||
|
// only add to list as online workspace
|
||||||
|
signals.listAdd.emit({
|
||||||
|
workspace,
|
||||||
|
provider: this.id,
|
||||||
|
// at this time we always download full workspace
|
||||||
|
// but after we support sub doc, we can only download metadata
|
||||||
|
locally: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async auth(
|
||||||
|
config: Readonly<ConfigStore<string>>,
|
||||||
|
logger: Logger,
|
||||||
|
signals: DataCenterSignals
|
||||||
|
) {
|
||||||
|
const refreshToken = await config.get('token');
|
||||||
|
if (refreshToken) {
|
||||||
|
await token.refreshToken(refreshToken);
|
||||||
|
if (token.isLogin && !token.isExpired) {
|
||||||
|
logger('check login success');
|
||||||
|
// login success
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger('start login');
|
||||||
|
// login with google
|
||||||
|
const apis = getApis();
|
||||||
|
assert(apis.signInWithGoogle);
|
||||||
|
const user = await apis.signInWithGoogle();
|
||||||
|
assert(user);
|
||||||
|
logger(`login success: ${user.displayName}`);
|
||||||
|
|
||||||
|
// TODO: refresh local workspace data
|
||||||
|
const workspaces = await apis.getWorkspaces();
|
||||||
|
await Promise.all(
|
||||||
|
workspaces.map(async ({ id }) => {
|
||||||
|
const doc = new Doc();
|
||||||
|
const idb = new IndexedDBProvider(id, doc);
|
||||||
|
await idb.whenSynced;
|
||||||
|
await this._initCloudDoc(id, doc, logger, signals);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,21 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
import type { Workspace } from '@blocksuite/store';
|
import type { Workspace } from '@blocksuite/store';
|
||||||
|
|
||||||
import type { Apis, Logger, InitialParams, ConfigStore } from './index';
|
|
||||||
import type { BlobURL } from '@blocksuite/store/dist/blob/types';
|
import type { BlobURL } from '@blocksuite/store/dist/blob/types';
|
||||||
|
import type {
|
||||||
|
Apis,
|
||||||
|
DataCenterSignals,
|
||||||
|
Logger,
|
||||||
|
InitialParams,
|
||||||
|
ConfigStore,
|
||||||
|
} from './index';
|
||||||
|
|
||||||
export class BaseProvider {
|
export class BaseProvider {
|
||||||
static id = 'base';
|
static id = 'base';
|
||||||
protected _apis!: Readonly<Apis>;
|
protected _apis!: Readonly<Apis>;
|
||||||
protected _config!: Readonly<ConfigStore>;
|
protected _config!: Readonly<ConfigStore>;
|
||||||
protected _globalConfig!: Readonly<ConfigStore>;
|
|
||||||
protected _logger!: Logger;
|
protected _logger!: Logger;
|
||||||
|
protected _signals!: DataCenterSignals;
|
||||||
protected _workspace!: Workspace;
|
protected _workspace!: Workspace;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -23,8 +29,8 @@ export class BaseProvider {
|
|||||||
async init(params: InitialParams) {
|
async init(params: InitialParams) {
|
||||||
this._apis = params.apis;
|
this._apis = params.apis;
|
||||||
this._config = params.config;
|
this._config = params.config;
|
||||||
this._globalConfig = params.globalConfig;
|
|
||||||
this._logger = params.logger;
|
this._logger = params.logger;
|
||||||
|
this._signals = params.signals;
|
||||||
this._workspace = params.workspace;
|
this._workspace = params.workspace;
|
||||||
this._logger.enabled = params.debug;
|
this._logger.enabled = params.debug;
|
||||||
}
|
}
|
||||||
@@ -58,6 +64,14 @@ export class BaseProvider {
|
|||||||
return this._workspace;
|
return this._workspace;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async auth(
|
||||||
|
_config: Readonly<ConfigStore>,
|
||||||
|
logger: Logger,
|
||||||
|
_signals: DataCenterSignals
|
||||||
|
) {
|
||||||
|
logger("This provider doesn't require authentication");
|
||||||
|
}
|
||||||
|
|
||||||
// get workspace list,return a map of workspace id and boolean
|
// get workspace list,return a map of workspace id and boolean
|
||||||
// if value is true, it exists locally, otherwise it does not exist locally
|
// if value is true, it exists locally, otherwise it does not exist locally
|
||||||
static async list(
|
static async list(
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Workspace } from '@blocksuite/store';
|
import type { Workspace } from '@blocksuite/store';
|
||||||
|
|
||||||
import type { Apis } from '../apis';
|
import type { Apis } from '../apis';
|
||||||
|
import type { DataCenterSignals } from '../datacenter';
|
||||||
import type { getLogger } from '../index';
|
import type { getLogger } from '../index';
|
||||||
import type { ConfigStore } from '../store';
|
import type { ConfigStore } from '../store';
|
||||||
|
|
||||||
@@ -9,13 +10,13 @@ export type Logger = ReturnType<typeof getLogger>;
|
|||||||
export type InitialParams = {
|
export type InitialParams = {
|
||||||
apis: Apis;
|
apis: Apis;
|
||||||
config: Readonly<ConfigStore>;
|
config: Readonly<ConfigStore>;
|
||||||
globalConfig: Readonly<ConfigStore>;
|
|
||||||
debug: boolean;
|
debug: boolean;
|
||||||
logger: Logger;
|
logger: Logger;
|
||||||
|
signals: DataCenterSignals;
|
||||||
workspace: Workspace;
|
workspace: Workspace;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type { Apis, ConfigStore, Workspace };
|
export type { Apis, ConfigStore, DataCenterSignals, Workspace };
|
||||||
export type { BaseProvider } from './base.js';
|
export type { BaseProvider } from './base.js';
|
||||||
export { AffineProvider } from './affine/index.js';
|
export { AffineProvider } from './affine/index.js';
|
||||||
export { LocalProvider } from './local/index.js';
|
export { LocalProvider } from './local/index.js';
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export class LocalProvider extends BaseProvider {
|
|||||||
this._blobs = blobs;
|
this._blobs = blobs;
|
||||||
}
|
}
|
||||||
|
|
||||||
async initData() {
|
async initData(locally = true) {
|
||||||
assert(this._workspace.room);
|
assert(this._workspace.room);
|
||||||
this._logger('Loading local data');
|
this._logger('Loading local data');
|
||||||
this._idb = new IndexedDBProvider(
|
this._idb = new IndexedDBProvider(
|
||||||
@@ -32,14 +32,19 @@ export class LocalProvider extends BaseProvider {
|
|||||||
await this._idb.whenSynced;
|
await this._idb.whenSynced;
|
||||||
this._logger('Local data loaded');
|
this._logger('Local data loaded');
|
||||||
|
|
||||||
await this._globalConfig.set(this._workspace.room, true);
|
this._signals.listAdd.emit({
|
||||||
|
workspace: this._workspace.room,
|
||||||
|
provider: this.id,
|
||||||
|
locally,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async clear() {
|
async clear() {
|
||||||
|
assert(this._workspace.room);
|
||||||
await super.clear();
|
await super.clear();
|
||||||
await this._blobs.clear();
|
await this._blobs.clear();
|
||||||
await this._idb?.clearData();
|
await this._idb?.clearData();
|
||||||
await this._globalConfig.delete(this._workspace.room!);
|
this._signals.listRemove.emit(this._workspace.room);
|
||||||
}
|
}
|
||||||
|
|
||||||
async destroy(): Promise<void> {
|
async destroy(): Promise<void> {
|
||||||
@@ -59,6 +64,10 @@ export class LocalProvider extends BaseProvider {
|
|||||||
config: Readonly<ConfigStore<boolean>>
|
config: Readonly<ConfigStore<boolean>>
|
||||||
): Promise<Map<string, boolean> | undefined> {
|
): Promise<Map<string, boolean> | undefined> {
|
||||||
const entries = await config.entries();
|
const entries = await config.entries();
|
||||||
return new Map(entries);
|
return new Map(
|
||||||
|
entries
|
||||||
|
.filter(([key]) => key.startsWith('list:'))
|
||||||
|
.map(([key, value]) => [key.slice(5), value])
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
15
packages/data-center/tests/cloud/auth.spec.ts
Normal file
15
packages/data-center/tests/cloud/auth.spec.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
import { getDataCenter } from '../utils.js';
|
||||||
|
|
||||||
|
import 'fake-indexeddb/auto';
|
||||||
|
|
||||||
|
test.describe('Auth', () => {
|
||||||
|
test('sign in', async () => {});
|
||||||
|
|
||||||
|
test('sign out', async () => {});
|
||||||
|
|
||||||
|
test('isLogin', async () => {});
|
||||||
|
|
||||||
|
test('getUserInfo', async () => {});
|
||||||
|
});
|
||||||
15
packages/data-center/tests/cloud/collaborate.spec.ts
Normal file
15
packages/data-center/tests/cloud/collaborate.spec.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
import { getDataCenter } from '../utils.js';
|
||||||
|
|
||||||
|
import 'fake-indexeddb/auto';
|
||||||
|
|
||||||
|
test.describe('Collaborate', () => {
|
||||||
|
test('collaborate editor content', async () => {});
|
||||||
|
|
||||||
|
test('collaborate workspace name', async () => {});
|
||||||
|
|
||||||
|
test('collaborate workspace avatar', async () => {});
|
||||||
|
|
||||||
|
test('collaborate workspace list', async () => {});
|
||||||
|
});
|
||||||
17
packages/data-center/tests/cloud/permission.spec.ts
Normal file
17
packages/data-center/tests/cloud/permission.spec.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
import { getDataCenter } from '../utils.js';
|
||||||
|
|
||||||
|
import 'fake-indexeddb/auto';
|
||||||
|
|
||||||
|
test.describe('Permission', () => {
|
||||||
|
test('get the public of workspace', async () => {});
|
||||||
|
|
||||||
|
test('make workspace public', async () => {});
|
||||||
|
|
||||||
|
test('make workspace private', async () => {});
|
||||||
|
|
||||||
|
test('un-login user open the public workspace ', async () => {});
|
||||||
|
|
||||||
|
test('un-login user open the private workspace ', async () => {});
|
||||||
|
});
|
||||||
15
packages/data-center/tests/cloud/share.spec.ts
Normal file
15
packages/data-center/tests/cloud/share.spec.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
import { getDataCenter } from '../utils.js';
|
||||||
|
|
||||||
|
import 'fake-indexeddb/auto';
|
||||||
|
|
||||||
|
test.describe('Share', () => {
|
||||||
|
test('add(invite) member by email', async () => {});
|
||||||
|
|
||||||
|
test('accept invite member link', async () => {});
|
||||||
|
|
||||||
|
test('members list', async () => {});
|
||||||
|
|
||||||
|
test('delete member', async () => {});
|
||||||
|
});
|
||||||
23
packages/data-center/tests/cloud/sync.spec.ts
Normal file
23
packages/data-center/tests/cloud/sync.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
import { getDataCenter } from '../utils.js';
|
||||||
|
|
||||||
|
import 'fake-indexeddb/auto';
|
||||||
|
|
||||||
|
test.describe('Sync', () => {
|
||||||
|
test('get cloud the sync flag of workspace', async () => {});
|
||||||
|
|
||||||
|
test('enable [cloud sync feature]', async () => {});
|
||||||
|
|
||||||
|
test('close [cloud sync feature]', async () => {});
|
||||||
|
|
||||||
|
test('editor cloud storage', async () => {});
|
||||||
|
|
||||||
|
test('cloud sync is in-progress', async () => {});
|
||||||
|
|
||||||
|
test('cloud sync is completed', async () => {});
|
||||||
|
|
||||||
|
test('cloud sync is error', async () => {});
|
||||||
|
|
||||||
|
test('cloud storage is right', async () => {});
|
||||||
|
});
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
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: { local: true },
|
|
||||||
test4: { local: true },
|
|
||||||
test5: { local: true },
|
|
||||||
test6: { local: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
await dataCenter.reload('test3', { providerId: 'affine' });
|
|
||||||
expect(await dataCenter.list()).toStrictEqual({
|
|
||||||
test3: { affine: true, local: true },
|
|
||||||
test4: { local: true },
|
|
||||||
test5: { local: true },
|
|
||||||
test6: { local: true },
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
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: { local: true } });
|
|
||||||
});
|
|
||||||
13
packages/data-center/tests/local/attachment.spec.ts
Normal file
13
packages/data-center/tests/local/attachment.spec.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
import { getDataCenter } from '../utils.js';
|
||||||
|
|
||||||
|
import 'fake-indexeddb/auto';
|
||||||
|
|
||||||
|
test.describe('Attachment', () => {
|
||||||
|
test('upload blob', async () => {});
|
||||||
|
|
||||||
|
test('get blob', async () => {});
|
||||||
|
|
||||||
|
test('remove blob', async () => {});
|
||||||
|
});
|
||||||
11
packages/data-center/tests/local/import-export.spec.ts
Normal file
11
packages/data-center/tests/local/import-export.spec.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
import { getDataCenter } from '../utils.js';
|
||||||
|
|
||||||
|
import 'fake-indexeddb/auto';
|
||||||
|
|
||||||
|
test.describe('Import/Export Workspace', () => {
|
||||||
|
test('import workspace', async () => {});
|
||||||
|
|
||||||
|
test('export workspace', async () => {});
|
||||||
|
});
|
||||||
51
packages/data-center/tests/local/init.spec.ts
Normal file
51
packages/data-center/tests/local/init.spec.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
import { getDataCenter } from '../utils.js';
|
||||||
|
|
||||||
|
import 'fake-indexeddb/auto';
|
||||||
|
|
||||||
|
test.describe('Init Data Center', () => {
|
||||||
|
test('init', async () => {
|
||||||
|
const dataCenter = await getDataCenter();
|
||||||
|
expect(dataCenter).toBeTruthy();
|
||||||
|
await dataCenter.clear();
|
||||||
|
|
||||||
|
const workspace = await dataCenter.load('test1');
|
||||||
|
expect(workspace).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('init 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
26
packages/data-center/tests/local/search.spec.ts
Normal file
26
packages/data-center/tests/local/search.spec.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import assert from 'assert';
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
import { getDataCenter, waitOnce } from '../utils.js';
|
||||||
|
|
||||||
|
import 'fake-indexeddb/auto';
|
||||||
|
|
||||||
|
test.describe('Search', () => {
|
||||||
|
test('search result', async () => {
|
||||||
|
const dc = await getDataCenter();
|
||||||
|
const workspace = await dc.load('test');
|
||||||
|
|
||||||
|
assert(workspace);
|
||||||
|
workspace.createPage('test');
|
||||||
|
await waitOnce(workspace.signals.pageAdded);
|
||||||
|
const page = workspace.getPage('test');
|
||||||
|
assert(page);
|
||||||
|
|
||||||
|
const text = new page.Text(page, 'hello world');
|
||||||
|
const blockId = page.addBlock({ flavour: 'affine:paragraph', text });
|
||||||
|
|
||||||
|
expect(workspace.search('hello')).toStrictEqual(
|
||||||
|
new Map([[blockId, 'test']])
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
70
packages/data-center/tests/local/workspace.spec.ts
Normal file
70
packages/data-center/tests/local/workspace.spec.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
import { getDataCenter } from '../utils.js';
|
||||||
|
|
||||||
|
import 'fake-indexeddb/auto';
|
||||||
|
|
||||||
|
test.describe('Workspace', () => {
|
||||||
|
test('create', async () => {});
|
||||||
|
|
||||||
|
test('load', async () => {});
|
||||||
|
|
||||||
|
test('get workspace name', async () => {});
|
||||||
|
test('set workspace name', async () => {});
|
||||||
|
|
||||||
|
test('get workspace avatar', async () => {});
|
||||||
|
test('set workspace avatar', async () => {});
|
||||||
|
|
||||||
|
test('list', 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: { local: true },
|
||||||
|
test4: { local: true },
|
||||||
|
test5: { local: true },
|
||||||
|
test6: { local: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
await dataCenter.reload('test3', { providerId: 'affine' });
|
||||||
|
expect(await dataCenter.list()).toStrictEqual({
|
||||||
|
test3: { affine: true },
|
||||||
|
test4: { local: true },
|
||||||
|
test5: { local: true },
|
||||||
|
test6: { local: true },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('destroy', 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', 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: { local: true } });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
export const getDataCenter = () => {
|
import { Signal } from '@blocksuite/store';
|
||||||
return import('../src/index.js').then(async dataCenter =>
|
|
||||||
dataCenter.getDataCenter(false)
|
export const getDataCenter = async () => {
|
||||||
);
|
const dataCenter = await import('../src/index.js');
|
||||||
|
return await dataCenter.getDataCenter(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const waitOnce = <T>(signal: Signal<T>) =>
|
||||||
|
new Promise<T>(resolve => signal.once(val => resolve(val)));
|
||||||
|
|||||||
@@ -4,11 +4,18 @@ import { loadPage } from './libs/load-page';
|
|||||||
loadPage();
|
loadPage();
|
||||||
|
|
||||||
test.describe('Open contact us', () => {
|
test.describe('Open contact us', () => {
|
||||||
test.skip('Click about us', async ({ page }) => {
|
test('Click about us', async ({ page }) => {
|
||||||
const currentWorkspace = page.getByTestId('current-workspace');
|
const currentWorkspace = page.getByTestId('current-workspace');
|
||||||
await currentWorkspace.click();
|
await currentWorkspace.click();
|
||||||
// await page.waitForTimeout(1000);
|
// await page.waitForTimeout(1000);
|
||||||
await page.getByText('About AFFiNE').click();
|
await page
|
||||||
|
.getByRole('tooltip', {
|
||||||
|
name: 'AFFiNE Log in to sync with affine About AFFiNE',
|
||||||
|
})
|
||||||
|
.locator('div')
|
||||||
|
.filter({ hasText: 'About AFFiNE' })
|
||||||
|
.nth(2)
|
||||||
|
.click();
|
||||||
const contactUsModal = page.locator(
|
const contactUsModal = page.locator(
|
||||||
'[data-testid=contact-us-modal-content]'
|
'[data-testid=contact-us-modal-content]'
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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.waitForSelector('#__next');
|
await page.waitForSelector('#__next');
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
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')
|
||||||
.nth(1)
|
.nth(2)
|
||||||
.click();
|
.click();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { loadPage } from './libs/load-page';
|
|||||||
|
|
||||||
loadPage();
|
loadPage();
|
||||||
|
|
||||||
test.describe.skip('Local first default workspace', () => {
|
test.describe('Local first default workspace', () => {
|
||||||
test('Default workspace name', async ({ page }) => {
|
test('Default workspace name', async ({ page }) => {
|
||||||
const workspaceName = page.getByTestId('workspace-name');
|
const workspaceName = page.getByTestId('workspace-name');
|
||||||
expect(await workspaceName.textContent()).toBe('AFFiNE');
|
expect(await workspaceName.textContent()).toBe('AFFiNE');
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { loadPage } from './libs/load-page';
|
|||||||
|
|
||||||
loadPage();
|
loadPage();
|
||||||
|
|
||||||
test.describe.skip('Login Flow', () => {
|
test.describe('Login Flow', () => {
|
||||||
test('Open login modal by click current workspace', async ({ page }) => {
|
test('Open login modal by click current workspace', async ({ page }) => {
|
||||||
await page.getByTestId('current-workspace').click();
|
await page.getByTestId('current-workspace').click();
|
||||||
await page.waitForTimeout(800);
|
await page.waitForTimeout(800);
|
||||||
@@ -24,21 +24,22 @@ test.describe.skip('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');
|
||||||
});
|
// });
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user