Merge remote-tracking branch 'refs/remotes/origin/feat/cloud-sync-saika'

Conflicts:
	package.json
	packages/app/src/components/workspace-slider-bar/index.tsx
	packages/app/src/libs/i18n/resources/bn.json
	packages/app/src/libs/i18n/resources/fr.json
	packages/app/src/libs/i18n/resources/sr.json
	packages/app/src/libs/i18n/resources/zh-Hans.json
	packages/app/src/libs/i18n/resources/zh-Hant.json
	packages/app/src/pages/_app.tsx
	packages/data-center/src/datacenter.ts
	pnpm-lock.yaml
This commit is contained in:
linonetwo 2023-01-10 17:08:57 +08:00
commit 876f3d235e
65 changed files with 728 additions and 4076 deletions

View File

@ -17,8 +17,7 @@
"test": "playwright test", "test": "playwright test",
"test:dc": "pnpm --filter @affine/datacenter 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": "vitest run", "test:unit": "playwright test --config=playwright.config.unit.ts",
"test:unit:watch": "vitest",
"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" "check:ci": "pnpm lint & pnpm test"

View File

@ -10,6 +10,7 @@
}, },
"dependencies": { "dependencies": {
"@affine/datacenter": "workspace:*", "@affine/datacenter": "workspace:*",
"@affine/i18n": "workspace:*",
"@blocksuite/blocks": "0.3.1-20230109032243-37ad3ba", "@blocksuite/blocks": "0.3.1-20230109032243-37ad3ba",
"@blocksuite/editor": "0.3.1-20230109032243-37ad3ba", "@blocksuite/editor": "0.3.1-20230109032243-37ad3ba",
"@blocksuite/icons": "^2.0.2", "@blocksuite/icons": "^2.0.2",
@ -27,7 +28,6 @@
"cmdk": "^0.1.20", "cmdk": "^0.1.20",
"css-spring": "^4.1.0", "css-spring": "^4.1.0",
"dayjs": "^1.11.7", "dayjs": "^1.11.7",
"i18next": "^21.9.1",
"lit": "^2.3.1", "lit": "^2.3.1",
"next": "13.1.0", "next": "13.1.0",
"next-debug-local": "^0.1.5", "next-debug-local": "^0.1.5",
@ -36,7 +36,6 @@
"quill-cursors": "^4.0.0", "quill-cursors": "^4.0.0",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-i18next": "^11.18.4",
"yjs": "^13.5.44" "yjs": "^13.5.44"
}, },
"devDependencies": { "devDependencies": {

View File

@ -1,7 +1,7 @@
import { describe, test, expect } from 'vitest'; import { test, expect } from '@playwright/test';
import { printer } from './../printer'; import { printer } from './../printer';
const chalk = require('chalk'); const chalk = require('chalk');
describe('printer', () => { test.describe('printer', () => {
test('test debug', () => { test('test debug', () => {
expect(printer.debug('test debug')).toBe( expect(printer.debug('test debug')).toBe(
chalk.green`debug` + chalk.white(' - test debug') chalk.green`debug` + chalk.white(' - test debug')

View File

@ -1,5 +1,5 @@
import { NotFoundTitle, PageContainer } from './styles'; import { NotFoundTitle, PageContainer } from './styles';
import { useTranslation } from 'react-i18next'; import { useTranslation } from '@affine/i18n';
export const NotfoundPage = () => { export const NotfoundPage = () => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (

View File

@ -23,7 +23,7 @@ import {
StyledModalFooter, StyledModalFooter,
} from './style'; } from './style';
import bg from '@/components/contact-modal/bg.png'; import bg from '@/components/contact-modal/bg.png';
import { useTranslation } from 'react-i18next'; import { useTranslation } from '@affine/i18n';
const linkList = [ const linkList = [
{ {
icon: <GithubIcon />, icon: <GithubIcon />,

View File

@ -15,7 +15,7 @@ import { useTheme } from '@/providers/ThemeProvider';
import { EdgelessIcon, PaperIcon } from './Icons'; import { EdgelessIcon, PaperIcon } from './Icons';
import useCurrentPageMeta from '@/hooks/use-current-page-meta'; import useCurrentPageMeta from '@/hooks/use-current-page-meta';
import { usePageHelper } from '@/hooks/use-page-helper'; import { usePageHelper } from '@/hooks/use-page-helper';
import { useTranslation } from 'react-i18next'; import { useTranslation } from '@affine/i18n';
const PaperItem = ({ active }: { active?: boolean }) => { const PaperItem = ({ active }: { active?: boolean }) => {
const { const {
theme: { theme: {

View File

@ -3,7 +3,7 @@ import { IconButton, IconButtonProps } from '@/ui/button';
import { Tooltip } from '@/ui/tooltip'; import { Tooltip } from '@/ui/tooltip';
import { ArrowDownIcon } from '@blocksuite/icons'; import { ArrowDownIcon } from '@blocksuite/icons';
import { useModal } from '@/providers/GlobalModalProvider'; import { useModal } from '@/providers/GlobalModalProvider';
import { useTranslation } from 'react-i18next'; import { useTranslation } from '@affine/i18n';
export const QuickSearchButton = ({ export const QuickSearchButton = ({
onClick, onClick,
...props ...props

View File

@ -16,7 +16,7 @@ import { usePageHelper } from '@/hooks/use-page-helper';
import { useConfirm } from '@/providers/ConfirmProvider'; import { useConfirm } from '@/providers/ConfirmProvider';
import useCurrentPageMeta from '@/hooks/use-current-page-meta'; import useCurrentPageMeta from '@/hooks/use-current-page-meta';
import { toast } from '@/ui/toast'; import { toast } from '@/ui/toast';
import { useTranslation } from 'react-i18next'; import { useTranslation } from '@affine/i18n';
const PopoverContent = () => { const PopoverContent = () => {
const { editor } = useAppState(); const { editor } = useAppState();
const { toggleFavoritePage, toggleDeletePage } = usePageHelper(); const { toggleFavoritePage, toggleDeletePage } = usePageHelper();

View File

@ -8,7 +8,7 @@ import {
import { CloseIcon, ContactIcon, HelpIcon, KeyboardIcon } from './Icons'; import { CloseIcon, ContactIcon, HelpIcon, KeyboardIcon } from './Icons';
import Grow from '@mui/material/Grow'; import Grow from '@mui/material/Grow';
import { Tooltip } from '@/ui/tooltip'; import { Tooltip } from '@/ui/tooltip';
import { useTranslation } from 'react-i18next'; import { useTranslation } from '@affine/i18n';
import { useModal } from '@/providers/GlobalModalProvider'; import { useModal } from '@/providers/GlobalModalProvider';
import { useTheme } from '@/providers/ThemeProvider'; import { useTheme } from '@/providers/ThemeProvider';
import useCurrentPageMeta from '@/hooks/use-current-page-meta'; import useCurrentPageMeta from '@/hooks/use-current-page-meta';

View File

@ -6,7 +6,7 @@ import Loading from '@/components/loading';
import { usePageHelper } from '@/hooks/use-page-helper'; import { usePageHelper } from '@/hooks/use-page-helper';
import { useAppState } from '@/providers/app-state-provider/context'; import { useAppState } from '@/providers/app-state-provider/context';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from '@affine/i18n';
// import { Tooltip } from '@/ui/tooltip'; // import { Tooltip } from '@/ui/tooltip';
type ImportModalProps = { type ImportModalProps = {
open: boolean; open: boolean;

View File

@ -14,7 +14,7 @@ import {
} from '@blocksuite/icons'; } from '@blocksuite/icons';
import { toast } from '@/ui/toast'; import { toast } from '@/ui/toast';
import { usePageHelper } from '@/hooks/use-page-helper'; import { usePageHelper } from '@/hooks/use-page-helper';
import { useTranslation } from 'react-i18next'; import { useTranslation } from '@affine/i18n';
export const OperationCell = ({ pageMeta }: { pageMeta: PageMeta }) => { export const OperationCell = ({ pageMeta }: { pageMeta: PageMeta }) => {
const { id, favorite } = pageMeta; const { id, favorite } = pageMeta;
const { openPage } = usePageHelper(); const { openPage } = usePageHelper();

View File

@ -24,7 +24,7 @@ import { useAppState } from '@/providers/app-state-provider/context';
import { toast } from '@/ui/toast'; import { toast } from '@/ui/toast';
import { usePageHelper } from '@/hooks/use-page-helper'; import { usePageHelper } from '@/hooks/use-page-helper';
import { useTheme } from '@/providers/ThemeProvider'; import { useTheme } from '@/providers/ThemeProvider';
import { useTranslation } from 'react-i18next'; import { useTranslation } from '@affine/i18n';
const FavoriteTag = ({ const FavoriteTag = ({
pageMeta: { favorite, id }, pageMeta: { favorite, id },
}: { }: {

View File

@ -4,7 +4,7 @@ import { StyledModalFooterContent } from './style';
import { useModal } from '@/providers/GlobalModalProvider'; import { useModal } from '@/providers/GlobalModalProvider';
import { Command } from 'cmdk'; import { Command } from 'cmdk';
import { usePageHelper } from '@/hooks/use-page-helper'; import { usePageHelper } from '@/hooks/use-page-helper';
import { useTranslation } from 'react-i18next'; import { useTranslation } from '@affine/i18n';
export const Footer = (props: { query: string }) => { export const Footer = (props: { query: string }) => {
const { triggerQuickSearchModal } = useModal(); const { triggerQuickSearchModal } = useModal();
const { openPage, createPage } = usePageHelper(); const { openPage, createPage } = usePageHelper();

View File

@ -7,7 +7,7 @@ import { useAppState } from '@/providers/app-state-provider';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useSwitchToConfig } from './config'; import { useSwitchToConfig } from './config';
import { NoResultSVG } from './NoResultSVG'; import { NoResultSVG } from './NoResultSVG';
import { useTranslation } from 'react-i18next'; import { useTranslation } from '@affine/i18n';
import usePageHelper from '@/hooks/use-page-helper'; import usePageHelper from '@/hooks/use-page-helper';
import usePageMetaList from '@/hooks/use-page-meta-list'; import usePageMetaList from '@/hooks/use-page-meta-list';
export const Results = (props: { export const Results = (props: {

View File

@ -1,5 +1,5 @@
import { AllPagesIcon, FavouritesIcon, TrashIcon } from '@blocksuite/icons'; import { AllPagesIcon, FavouritesIcon, TrashIcon } from '@blocksuite/icons';
import { useTranslation } from 'react-i18next'; import { useTranslation } from '@affine/i18n';
export const useSwitchToConfig = ( export const useSwitchToConfig = (
currentWorkspaceId: string currentWorkspaceId: string

View File

@ -1,4 +1,4 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from '@affine/i18n';
interface ShortcutTip { interface ShortcutTip {
[x: string]: string; [x: string]: string;
} }
@ -13,7 +13,7 @@ export const useMacKeyboardShortcuts = (): ShortcutTip => {
[t('Strikethrough')]: '⌘+⇧+S', [t('Strikethrough')]: '⌘+⇧+S',
[t('Inline code')]: ' ⌘+E', [t('Inline code')]: ' ⌘+E',
[t('Code block')]: '⌘+⌥+C', [t('Code block')]: '⌘+⌥+C',
[t('Hyperlink(with selected text)')]: '⌘+K', [t('Link')]: '⌘+K',
[t('Quick search')]: '⌘+K', [t('Quick search')]: '⌘+K',
[t('Body text')]: '⌘+⌥+0', [t('Body text')]: '⌘+⌥+0',
[t('Heading', { number: '1' })]: '⌘+⌥+1', [t('Heading', { number: '1' })]: '⌘+⌥+1',
@ -57,7 +57,7 @@ export const useWindowsKeyboardShortcuts = (): ShortcutTip => {
[t('Strikethrough')]: 'Ctrl+Shift+S', [t('Strikethrough')]: 'Ctrl+Shift+S',
[t('Inline code')]: ' Ctrl+E', [t('Inline code')]: ' Ctrl+E',
[t('Code block')]: 'Ctrl+Alt+C', [t('Code block')]: 'Ctrl+Alt+C',
[t('Hyperlink(with selected text)')]: 'Ctrl+K', [t('Link')]: 'Ctrl+K',
[t('Quick search')]: 'Ctrl+K', [t('Quick search')]: 'Ctrl+K',
[t('Body text')]: 'Ctrl+Shift+0', [t('Body text')]: 'Ctrl+Shift+0',
[t('Heading', { number: '1' })]: 'Ctrl+Shift+1', [t('Heading', { number: '1' })]: 'Ctrl+Shift+1',

View File

@ -16,7 +16,7 @@ import {
import Slide from '@mui/material/Slide'; import Slide from '@mui/material/Slide';
import { ModalCloseButton } from '@/ui/modal'; import { ModalCloseButton } from '@/ui/modal';
import { getUaHelper } from '@/utils'; import { getUaHelper } from '@/utils';
import { useTranslation } from 'react-i18next'; import { useTranslation } from '@affine/i18n';
type ModalProps = { type ModalProps = {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;

View File

@ -32,7 +32,7 @@ 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 { WorkspaceSetting } from '@/components/workspace-setting'; import { WorkspaceSetting } from '@/components/workspace-setting';
import { useTranslation } from 'react-i18next'; import { useTranslation } from '@affine/i18n';
const FavoriteList = ({ showList }: { showList: boolean }) => { const FavoriteList = ({ showList }: { showList: boolean }) => {
const { openPage } = usePageHelper(); const { openPage } = usePageHelper();

View File

@ -18,8 +18,8 @@ import { useEffect } from 'react';
import { useAppState } from '@/providers/app-state-provider'; import { useAppState } from '@/providers/app-state-provider';
import { PageLoading } from '@/components/loading'; import { PageLoading } from '@/components/loading';
import Head from 'next/head'; import Head from 'next/head';
import '@/libs/i18n';
import TemporaryHelperProvider from '@/providers/temporary-helper-provider'; import TemporaryHelperProvider from '@/providers/temporary-helper-provider';
import '@affine/i18n';
const ThemeProvider = dynamic(() => import('@/providers/ThemeProvider'), { const ThemeProvider = dynamic(() => import('@/providers/ThemeProvider'), {
ssr: false, ssr: false,

View File

@ -4,7 +4,7 @@ import usePageMetaList from '@/hooks/use-page-meta-list';
import { PageListHeader } from '@/components/header'; import { PageListHeader } from '@/components/header';
import { ReactElement } from 'react'; import { ReactElement } from 'react';
import WorkspaceLayout from '@/components/workspace-layout'; import WorkspaceLayout from '@/components/workspace-layout';
import { useTranslation } from 'react-i18next'; import { useTranslation } from '@affine/i18n';
const All = () => { const All = () => {
const pageMetaList = usePageMetaList(); const pageMetaList = usePageMetaList();
const { t } = useTranslation(); const { t } = useTranslation();

View File

@ -4,7 +4,7 @@ import { FavouritesIcon } from '@blocksuite/icons';
import usePageMetaList from '@/hooks/use-page-meta-list'; import usePageMetaList from '@/hooks/use-page-meta-list';
import { ReactElement } from 'react'; import { ReactElement } from 'react';
import WorkspaceLayout from '@/components/workspace-layout'; import WorkspaceLayout from '@/components/workspace-layout';
import { useTranslation } from 'react-i18next'; import { useTranslation } from '@affine/i18n';
export const Favorite = () => { export const Favorite = () => {
const pageMetaList = usePageMetaList(); const pageMetaList = usePageMetaList();
const { t } = useTranslation(); const { t } = useTranslation();

View File

@ -4,7 +4,7 @@ import { TrashIcon } from '@blocksuite/icons';
import usePageMetaList from '@/hooks/use-page-meta-list'; import usePageMetaList from '@/hooks/use-page-meta-list';
import { ReactElement } from 'react'; import { ReactElement } from 'react';
import WorkspaceLayout from '@/components/workspace-layout'; import WorkspaceLayout from '@/components/workspace-layout';
import { useTranslation } from 'react-i18next'; import { useTranslation } from '@affine/i18n';
export const Trash = () => { export const Trash = () => {
const pageMetaList = usePageMetaList(); const pageMetaList = usePageMetaList();
const { t } = useTranslation(); const { t } = useTranslation();

View File

@ -7,7 +7,7 @@ import {
StyledModalWrapper, StyledModalWrapper,
} from '@/ui/confirm/styles'; } from '@/ui/confirm/styles';
import { Button } from '@/ui/button'; import { Button } from '@/ui/button';
import { useTranslation } from 'react-i18next'; import { useTranslation } from '@affine/i18n';
export type ConfirmProps = { export type ConfirmProps = {
title?: string; title?: string;
content?: string; content?: string;

View File

@ -1,7 +1,7 @@
import { describe, test, expect } from 'vitest'; import { test, expect } from '@playwright/test';
import { isMobile } from '../get-is-mobile'; import { isMobile } from '../get-is-mobile';
describe('get-is-mobile', () => { test.describe('get-is-mobile', () => {
test('get-is-mobile', () => { test('get-is-mobile', () => {
expect( expect(
isMobile( isMobile(

View File

@ -1,23 +1,26 @@
import { Workspaces } from './workspaces'; import { WorkspaceMetaCollection } from './workspace-meta-collection.js';
import type { WorkspacesChangeEvent } from './workspaces'; import type { WorkspaceMetaCollectionChangeEvent } from './workspace-meta-collection';
import { BlobStorage, Workspace } from '@blocksuite/store'; import { Workspace as BlocksuiteWorkspace } from '@blocksuite/store';
import { BaseProvider } from './provider/base'; import { BaseProvider } from './provider/base';
import { LocalProvider } from './provider/local/local'; import { LocalProvider } from './provider/local/local';
import { AffineProvider } from './provider'; import { AffineProvider } from './provider';
import type { WorkspaceMeta } from './types'; import type { Message, WorkspaceMeta } from './types';
import assert from 'assert'; import assert from 'assert';
import { getLogger } from './logger'; import { getLogger } from './logger';
import { BlockSchema } from '@blocksuite/blocks/models';
import { applyUpdate, encodeStateAsUpdate } from 'yjs'; import { applyUpdate, encodeStateAsUpdate } from 'yjs';
import { TauriIPCProvider } from './provider/tauri-ipc'; import { TauriIPCProvider } from './provider/tauri-ipc';
import { createBlocksuiteWorkspace } from './utils/index.js';
import { MessageCenter } from './message/message';
/** /**
* @class DataCenter * @class DataCenter
* @classdesc Data center is made for managing different providers for business * @classdesc Data center is made for managing different providers for business
*/ */
export class DataCenter { export class DataCenter {
private readonly _workspaces = new Workspaces(); private readonly _workspaceMetaCollection = new WorkspaceMetaCollection();
private readonly _logger = getLogger('dc'); private readonly _logger = getLogger('dc');
private _workspaceInstances: Map<string, BlocksuiteWorkspace> = new Map();
private _messageCenter = new MessageCenter();
/** /**
* A mainProvider must exist as the only data trustworthy source. * A mainProvider must exist as the only data trustworthy source.
*/ */
@ -30,26 +33,18 @@ export class DataCenter {
static async init(debug: boolean): Promise<DataCenter> { static async init(debug: boolean): Promise<DataCenter> {
const dc = new DataCenter(debug); const dc = new DataCenter(debug);
const getInitParams = () => {
return {
logger: dc._logger,
workspaces: dc._workspaceMetaCollection.createScope(),
messageCenter: dc._messageCenter,
};
};
// TODO: switch different provider // TODO: switch different provider
dc.registerProvider( dc.registerProvider(new LocalProvider(getInitParams()));
new LocalProvider({ dc.registerProvider(new AffineProvider(getInitParams()));
logger: dc._logger,
workspaces: dc._workspaces.createScope(),
})
);
dc.registerProvider(
new AffineProvider({
logger: dc._logger,
workspaces: dc._workspaces.createScope(),
})
);
if (typeof window !== 'undefined' && window.CLIENT_APP) { if (typeof window !== 'undefined' && window.CLIENT_APP) {
dc.registerProvider( dc.registerProvider(new TauriIPCProvider(getInitParams()));
new TauriIPCProvider({
logger: dc._logger,
workspaces: dc._workspaces.createScope(),
})
);
} }
return dc; return dc;
@ -77,7 +72,7 @@ export class DataCenter {
} }
public get workspaces() { public get workspaces() {
return this._workspaces.workspaces; return this._workspaceMetaCollection.workspaces;
} }
public async refreshWorkspaces() { public async refreshWorkspaces() {
@ -89,7 +84,7 @@ export class DataCenter {
/** /**
* create new workspace , new workspace is a local workspace * create new workspace , new workspace is a local workspace
* @param {string} name workspace name * @param {string} name workspace name
* @returns {Promise<WS>} * @returns {Promise<Workspace>}
*/ */
public async createWorkspace(workspaceMeta: WorkspaceMeta) { public async createWorkspace(workspaceMeta: WorkspaceMeta) {
assert( assert(
@ -97,7 +92,13 @@ export class DataCenter {
'There is no provider. You should add provider first.' 'There is no provider. You should add provider first.'
); );
const workspace = await this._mainProvider.createWorkspace(workspaceMeta); const workspaceInfo = await this._mainProvider.createWorkspaceInfo(
workspaceMeta
);
const workspace = createBlocksuiteWorkspace(workspaceInfo.id);
await this._mainProvider.createWorkspace(workspace, workspaceMeta);
return workspace; return workspace;
} }
@ -106,7 +107,7 @@ export class DataCenter {
* @param {string} workspaceId workspace id * @param {string} workspaceId workspace id
*/ */
public async deleteWorkspace(workspaceId: string) { public async deleteWorkspace(workspaceId: string) {
const workspaceInfo = this._workspaces.find(workspaceId); const workspaceInfo = this._workspaceMetaCollection.find(workspaceId);
assert(workspaceInfo, 'Workspace not found'); assert(workspaceInfo, 'Workspace not found');
const provider = this.providerMap.get(workspaceInfo.provider); const provider = this.providerMap.get(workspaceInfo.provider);
assert(provider, `Workspace exists, but we couldn't find its provider.`); assert(provider, `Workspace exists, but we couldn't find its provider.`);
@ -117,39 +118,42 @@ export class DataCenter {
* get a new workspace only has room id * get a new workspace only has room id
* @param {string} workspaceId workspace id * @param {string} workspaceId workspace id
*/ */
private _getWorkspace(workspaceId: string) { private _getBlocksuiteWorkspace(workspaceId: string) {
return new Workspace({ const workspaceInfo = this._workspaceMetaCollection.find(workspaceId);
room: workspaceId, assert(workspaceInfo, 'Workspace not found');
}).register(BlockSchema); return (
this._workspaceInstances.get(workspaceId) ||
createBlocksuiteWorkspace(workspaceId)
);
} }
/** /**
* login to all providers, it will default run all auth , * login to all providers, it will default run all auth ,
* maybe need a params to control which provider to auth * maybe need a params to control which provider to auth
*/ */
public async login() { public async login(providerId = 'affine') {
this.providers.forEach(p => { const provider = this.providerMap.get(providerId);
// TODO: may be add params of auth assert(provider, `provide '${providerId}' is not registered`);
p.auth(); await provider.auth();
}); provider.loadWorkspaces();
} }
/** /**
* logout from all providers * logout from all providers
*/ */
public async logout() { public async logout(providerId = 'affine') {
this.providers.forEach(p => { const provider = this.providerMap.get(providerId);
p.logout(); assert(provider, `provide '${providerId}' is not registered`);
}); await provider.logout();
} }
/** /**
* load workspace instance by id * load workspace instance by id
* @param {string} workspaceId workspace id * @param {string} workspaceId workspace id
* @returns {Promise<Workspace>} * @returns {Promise<BlocksuiteWorkspace>}
*/ */
public async loadWorkspace(workspaceId: string) { public async loadWorkspace(workspaceId: string) {
const workspaceInfo = this._workspaces.find(workspaceId); const workspaceInfo = this._workspaceMetaCollection.find(workspaceId);
assert(workspaceInfo, 'Workspace not found'); assert(workspaceInfo, 'Workspace not found');
const currentProvider = this.providerMap.get(workspaceInfo.provider); const currentProvider = this.providerMap.get(workspaceInfo.provider);
if (currentProvider) { if (currentProvider) {
@ -158,8 +162,9 @@ export class DataCenter {
const provider = this.providerMap.get(workspaceInfo.provider); const provider = this.providerMap.get(workspaceInfo.provider);
assert(provider, `provide '${workspaceInfo.provider}' is not registered`); assert(provider, `provide '${workspaceInfo.provider}' is not registered`);
this._logger(`Loading ${workspaceInfo.provider} workspace: `, workspaceId); this._logger(`Loading ${workspaceInfo.provider} workspace: `, workspaceId);
const workspace = this._getBlocksuiteWorkspace(workspaceId);
return await provider.warpWorkspace(this._getWorkspace(workspaceId)); this._workspaceInstances.set(workspaceId, workspace);
return await provider.warpWorkspace(workspace);
} }
/** /**
@ -179,32 +184,32 @@ export class DataCenter {
* @param {Function} callback callback function * @param {Function} callback callback function
*/ */
public async onWorkspacesChange( public async onWorkspacesChange(
callback: (workspaces: WorkspacesChangeEvent) => void callback: (workspaces: WorkspaceMetaCollectionChangeEvent) => void
) { ) {
this._workspaces.on('change', callback); this._workspaceMetaCollection.on('change', callback);
} }
/** /**
* change workspaces meta * change workspaces meta
* @param {WorkspaceMeta} workspaceMeta workspace meta * @param {WorkspaceMeta} workspaceMeta workspace meta
* @param {Workspace} workspace workspace instance * @param {BlocksuiteWorkspace} workspace workspace instance
*/ */
public async resetWorkspaceMeta( public async updateWorkspaceMeta(
{ name, avatar }: WorkspaceMeta, { name, avatar }: Partial<WorkspaceMeta>,
workspace: Workspace workspace: BlocksuiteWorkspace
) { ) {
assert(workspace?.room, 'No workspace to set meta'); assert(workspace?.room, 'No workspace to set meta');
const update: Partial<WorkspaceMeta> = {}; const update: Partial<WorkspaceMeta> = {};
if (name) { if (name) {
workspace.doc.meta.setName(name); workspace.meta.setName(name);
update.name = name; update.name = name;
} }
if (avatar) { if (avatar) {
workspace.doc.meta.setAvatar(avatar); workspace.meta.setAvatar(avatar);
update.avatar = avatar; update.avatar = avatar;
} }
// may run for change workspace meta // may run for change workspace meta
const workspaceInfo = this._workspaces.find(workspace.room); const workspaceInfo = this._workspaceMetaCollection.find(workspace.room);
assert(workspaceInfo, 'Workspace not found'); assert(workspaceInfo, 'Workspace not found');
const provider = this.providerMap.get(workspaceInfo.provider); const provider = this.providerMap.get(workspaceInfo.provider);
provider?.updateWorkspaceMeta(workspace.room, update); provider?.updateWorkspaceMeta(workspace.room, update);
@ -216,7 +221,7 @@ export class DataCenter {
* @param id workspace id * @param id workspace id
*/ */
public async leaveWorkspace(workspaceId: string) { public async leaveWorkspace(workspaceId: string) {
const workspaceInfo = this._workspaces.find(workspaceId); const workspaceInfo = this._workspaceMetaCollection.find(workspaceId);
assert(workspaceInfo, 'Workspace not found'); assert(workspaceInfo, 'Workspace not found');
const provider = this.providerMap.get(workspaceInfo.provider); const provider = this.providerMap.get(workspaceInfo.provider);
if (provider) { if (provider) {
@ -226,7 +231,7 @@ export class DataCenter {
} }
public async setWorkspacePublish(workspaceId: string, isPublish: boolean) { public async setWorkspacePublish(workspaceId: string, isPublish: boolean) {
const workspaceInfo = this._workspaces.find(workspaceId); const workspaceInfo = this._workspaceMetaCollection.find(workspaceId);
assert(workspaceInfo, 'Workspace not found'); assert(workspaceInfo, 'Workspace not found');
const provider = this.providerMap.get(workspaceInfo.provider); const provider = this.providerMap.get(workspaceInfo.provider);
if (provider) { if (provider) {
@ -235,7 +240,7 @@ export class DataCenter {
} }
public async inviteMember(id: string, email: string) { public async inviteMember(id: string, email: string) {
const workspaceInfo = this._workspaces.find(id); const workspaceInfo = this._workspaceMetaCollection.find(id);
assert(workspaceInfo, 'Workspace not found'); assert(workspaceInfo, 'Workspace not found');
const provider = this.providerMap.get(workspaceInfo.provider); const provider = this.providerMap.get(workspaceInfo.provider);
if (provider) { if (provider) {
@ -248,7 +253,7 @@ export class DataCenter {
* @param {number} permissionId permission id * @param {number} permissionId permission id
*/ */
public async removeMember(workspaceId: string, permissionId: number) { public async removeMember(workspaceId: string, permissionId: number) {
const workspaceInfo = this._workspaces.find(workspaceId); const workspaceInfo = this._workspaceMetaCollection.find(workspaceId);
assert(workspaceInfo, 'Workspace not found'); assert(workspaceInfo, 'Workspace not found');
const provider = this.providerMap.get(workspaceInfo.provider); const provider = this.providerMap.get(workspaceInfo.provider);
if (provider) { if (provider) {
@ -275,11 +280,11 @@ export class DataCenter {
} }
private async _transWorkspaceProvider( private async _transWorkspaceProvider(
workspace: Workspace, workspace: BlocksuiteWorkspace,
providerId: string providerId: string
) { ) {
assert(workspace.room, 'No workspace id'); assert(workspace.room, 'No workspace id');
const workspaceInfo = this._workspaces.find(workspace.room); const workspaceInfo = this._workspaceMetaCollection.find(workspace.room);
assert(workspaceInfo, 'Workspace not found'); assert(workspaceInfo, 'Workspace not found');
if (workspaceInfo.provider === providerId) { if (workspaceInfo.provider === providerId) {
this._logger('Workspace provider is same'); this._logger('Workspace provider is same');
@ -290,11 +295,17 @@ export class DataCenter {
const newProvider = this.providerMap.get(providerId); const newProvider = this.providerMap.get(providerId);
assert(newProvider, `provide '${providerId}' is not registered`); assert(newProvider, `provide '${providerId}' is not registered`);
this._logger(`create ${providerId} workspace: `, workspaceInfo.name); this._logger(`create ${providerId} workspace: `, workspaceInfo.name);
// TODO optimize this function const newWorkspaceInfo = await newProvider.createWorkspaceInfo({
const newWorkspace = await newProvider.createWorkspace({
name: workspaceInfo.name, name: workspaceInfo.name,
avatar: workspaceInfo.avatar, avatar: workspaceInfo.avatar,
}); });
const newWorkspace = createBlocksuiteWorkspace(newWorkspaceInfo.id);
// TODO optimize this function
await newProvider.createWorkspace(newWorkspace, {
name: workspaceInfo.name,
avatar: workspaceInfo.avatar,
});
assert(newWorkspace, 'Create workspace failed'); assert(newWorkspace, 'Create workspace failed');
this._logger( this._logger(
`update workspace data from ${workspaceInfo.provider} to ${providerId}` `update workspace data from ${workspaceInfo.provider} to ${providerId}`
@ -308,7 +319,7 @@ export class DataCenter {
* Enable workspace cloud * Enable workspace cloud
* @param {string} id ID of workspace. * @param {string} id ID of workspace.
*/ */
public async enableWorkspaceCloud(workspace: Workspace) { public async enableWorkspaceCloud(workspace: BlocksuiteWorkspace) {
assert(workspace?.room, 'No workspace to enable cloud'); assert(workspace?.room, 'No workspace to enable cloud');
return await this._transWorkspaceProvider(workspace, 'affine'); return await this._transWorkspaceProvider(workspace, 'affine');
} }
@ -341,21 +352,30 @@ export class DataCenter {
return; return;
} }
// /** /**
// * get blob url by workspaces id * get blob url by workspaces id
// * @param id * @param id
// * @returns {Promise<string | null>} blob url * @returns {Promise<string | null>} blob url
// */ */
// async getBlob(id: string): Promise<string | null> { async getBlob(
// return await this._blobStorage.get(id); workspace: BlocksuiteWorkspace,
// } id: string
): Promise<string | null> {
const blob = await workspace.blobs;
return (await blob?.get(id)) || '';
}
// /** /**
// * up load blob and get a blob url * up load blob and get a blob url
// * @param id * @param id
// * @returns {Promise<string | null>} blob url * @returns {Promise<string | null>} blob url
// */ */
// async setBlob(blob: Blob): Promise<string> { async setBlob(workspace: BlocksuiteWorkspace, blob: Blob): Promise<string> {
// return await this._blobStorage.set(blob); const blobStorage = await workspace.blobs;
// } return (await blobStorage?.set(blob)) || '';
}
onMessage(cb: (message: Message) => void) {
return this._messageCenter.onMessage(cb);
}
} }

View File

@ -26,5 +26,6 @@ const _initializeDataCenter = () => {
export const getDataCenter = _initializeDataCenter(); export const getDataCenter = _initializeDataCenter();
export type { AccessTokenMessage } from './provider/affine/apis'; export type { AccessTokenMessage } from './provider/affine/apis';
export type { Workspace } from './types'; export type { WorkspaceInfo } from './types';
export { getLogger } from './logger'; export { getLogger } from './logger';
export * from './message';

View File

@ -0,0 +1,3 @@
export enum MessageCode {
loginError,
}

View File

@ -0,0 +1,2 @@
export { MessageCenter } from './message';
export { MessageCode } from './code';

View File

@ -0,0 +1,24 @@
import { Observable } from 'lib0/observable';
import { Message } from 'src/types';
import { MessageCode } from './code';
export class MessageCenter extends Observable<string> {
constructor() {
super();
}
public send(message: MessageCode) {
this.emit('message', [message]);
}
public onMessage(callback: (message: Message) => void) {
this.on('message', callback);
}
private messages: Record<number, Message> = {
[MessageCode.loginError]: {
code: MessageCode.loginError,
message: 'Login failed',
},
};
}

View File

@ -0,0 +1,48 @@
import { test, expect } from '@playwright/test';
import { AffineProvider } from '../affine.js';
// import { Workspaces } from '../../../workspaces/index.js';
import { apis } from './mock-apis.js';
import 'fake-indexeddb/auto';
// TODO: we should find a better way for testing AffineProvider.
test.describe.serial('affine provider', async () => {
// const workspaces = new Workspaces();
// const provider = new AffineProvider({
// workspaces: workspaces.createScope(),
// apis,
// });
// await provider.auth();
// const workspaceName = 'workspace-test';
// let workspaceId: string | undefined;
// test('create workspace', async () => {
// const w = await provider.createWorkspace({
// name: workspaceName,
// avatar: 'avatar-url-test',
// });
// workspaceId = w?.room;
// expect(workspaces.workspaces.length).toEqual(1);
// expect(workspaces.workspaces[0].name).toEqual(workspaceName);
// });
// test('workspace list cache', async () => {
// const workspaces1 = new Workspaces();
// const provider1 = new AffineProvider({
// workspaces: workspaces1.createScope(),
// });
// await provider1.loadWorkspaces();
// expect(workspaces1.workspaces.length).toEqual(1);
// expect(workspaces1.workspaces[0].name).toEqual(workspaceName);
// expect(workspaces1.workspaces[0].id).toEqual(workspaceId);
// });
// test('update workspace', async () => {
// await provider.updateWorkspaceMeta(workspaceId!, {
// name: '1111',
// });
// expect(workspaces.workspaces[0].name).toEqual('1111');
// });
// test('delete workspace', async () => {
// expect(workspaces.workspaces.length).toEqual(1);
// await provider.deleteWorkspace(workspaces.workspaces[0].id);
// expect(workspaces.workspaces.length).toEqual(0);
// });
});

View File

@ -0,0 +1,19 @@
import type { Apis, AccessTokenMessage } from '../apis';
const user: AccessTokenMessage = {
create_at: Date.now(),
exp: 100000000,
email: 'demo@demo.demo',
id: '123',
name: 'demo',
avatar_url: 'demo-avatar-url',
};
export const apis = {
signInWithGoogle: () => {
return Promise.resolve(user);
},
createWorkspace: mate => {
return Promise.resolve({ id: 'test' });
},
} as Apis;

View File

@ -1,84 +1,104 @@
import { import { BaseProvider } from '../base.js';
getWorkspaces,
getWorkspaceDetail,
WorkspaceDetail,
downloadWorkspace,
deleteWorkspace,
leaveWorkspace,
inviteMember,
removeMember,
createWorkspace,
updateWorkspace,
} from './apis/workspace';
import { BaseProvider } from '../base';
import type { ProviderConstructorParams } from '../base'; import type { ProviderConstructorParams } from '../base';
import { User, Workspace as WS, WorkspaceMeta } from '../../types'; import type { User, WorkspaceInfo, WorkspaceMeta } from '../../types';
import { Workspace } from '@blocksuite/store'; import { Workspace as BlocksuiteWorkspace } from '@blocksuite/store';
import { BlockSchema } from '@blocksuite/blocks/models'; import { BlockSchema } from '@blocksuite/blocks/models';
import { applyUpdate } from 'yjs'; import { applyUpdate } from 'yjs';
import { token, Callback } from './apis'; import { storage } from './storage.js';
import { varStorage as storage } from 'lib0/storage';
import assert from 'assert'; import assert from 'assert';
import { getAuthorizer } from './apis/token'; import { WebsocketProvider } from './sync.js';
import { WebsocketProvider } from './sync'; // import { IndexedDBProvider } from '../local/indexeddb';
import { IndexedDBProvider } from '../indexeddb'; import { getApis } from './apis/index.js';
import { getDefaultHeadImgBlob } from '../../utils'; import type { Apis, WorkspaceDetail, Callback } from './apis';
import { getUserByEmail } from './apis/user'; import { setDefaultAvatar } from '../utils.js';
import { MessageCode } from 'src/message/code.js';
export interface AffineProviderConstructorParams
extends ProviderConstructorParams {
apis?: Apis;
}
export class AffineProvider extends BaseProvider { export class AffineProvider extends BaseProvider {
public id = 'affine'; public id = 'affine';
private _workspacesCache: Map<string, Workspace> = new Map(); private _workspacesCache: Map<string, BlocksuiteWorkspace> = new Map();
private _onTokenRefresh?: Callback = undefined; private _onTokenRefresh?: Callback = undefined;
private readonly _authorizer = getAuthorizer();
private _user: User | undefined = undefined;
private _wsMap: Map<string, WebsocketProvider> = new Map(); private _wsMap: Map<string, WebsocketProvider> = new Map();
private _idbMap: Map<string, IndexedDBProvider> = new Map(); private _apis: Apis;
// private _idbMap: Map<string, IndexedDBProvider> = new Map();
constructor(params: ProviderConstructorParams) { constructor({ apis, ...params }: AffineProviderConstructorParams) {
super(params); super(params);
this._apis = apis || getApis();
this.init().then(() => {
if (this._apis.token.isLogin) {
this.loadWorkspaces();
}
});
} }
override async init() { override async init() {
this._onTokenRefresh = () => { this._onTokenRefresh = () => {
if (token.refresh) { if (this._apis.token.refresh) {
storage.setItem('token', token.refresh); storage.setItem('token', this._apis.token.refresh);
} }
}; };
token.onChange(this._onTokenRefresh); this._apis.token.onChange(this._onTokenRefresh);
// initial login token // initial login token
if (token.isExpired) { if (this._apis.token.isExpired) {
try { try {
const refreshToken = storage.getItem('token'); const refreshToken = storage.getItem('token');
await token.refreshToken(refreshToken); await this._apis.token.refreshToken(refreshToken);
if (token.refresh) { if (this._apis.token.refresh) {
storage.set('token', token.refresh); storage.set('token', this._apis.token.refresh);
} }
assert(token.isLogin); assert(this._apis.token.isLogin);
} catch (_) { } catch (_) {
// this._logger('Authorization failed, fallback to local mode'); // this._logger('Authorization failed, fallback to local mode');
} }
} else { } else {
storage.setItem('token', token.refresh); storage.setItem('token', this._apis.token.refresh);
} }
} }
override async warpWorkspace(workspace: Workspace) { private async _applyCloudUpdates(blocksuiteWorkspace: BlocksuiteWorkspace) {
const { doc, room } = workspace; const { doc, room: workspaceId } = blocksuiteWorkspace;
assert(room); assert(workspaceId, 'Blocksuite Workspace without room(workspaceId).');
this._initWorkspaceDb(workspace); const updates = await this._apis.downloadWorkspace(workspaceId);
const updates = await downloadWorkspace(room); if (updates && updates.byteLength) {
if (updates) {
await new Promise(resolve => { await new Promise(resolve => {
doc.once('update', resolve); doc.once('update', resolve);
applyUpdate(doc, new Uint8Array(updates)); BlocksuiteWorkspace.Y.applyUpdate(doc, new Uint8Array(updates));
}); });
} }
const ws = new WebsocketProvider('/', room, doc); }
this._wsMap.set(room, ws);
override async warpWorkspace(workspace: BlocksuiteWorkspace) {
await this._applyCloudUpdates(workspace);
const { doc, room } = workspace;
assert(room);
this.linkLocal(workspace);
let ws = this._wsMap.get(room);
if (!ws) {
const wsUrl = `${
window.location.protocol === 'https:' ? 'wss' : 'ws'
}://${window.location.host}/api/sync/`;
ws = new WebsocketProvider(wsUrl, room, doc, {
params: { token: this._apis.token.refresh },
});
this._wsMap.set(room, ws);
}
// close all websocket links
Array.from(this._wsMap.entries()).forEach(([id, ws]) => {
if (id !== room) {
ws.disconnect();
}
});
ws.connect();
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
// TODO: synced will also be triggered on reconnection after losing sync // 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 // There needs to be an event mechanism to emit the synchronization state to the upper layer
@ -91,11 +111,11 @@ export class AffineProvider extends BaseProvider {
} }
override async loadWorkspaces() { override async loadWorkspaces() {
if (!token.isLogin) { if (!this._apis.token.isLogin) {
return []; return [];
} }
const workspacesList = await getWorkspaces(); const workspacesList = await this._apis.getWorkspaces();
const workspaces: WS[] = workspacesList.map(w => { const workspaces: WorkspaceInfo[] = workspacesList.map(w => {
return { return {
...w, ...w,
memberCount: 0, memberCount: 0,
@ -106,13 +126,13 @@ export class AffineProvider extends BaseProvider {
const workspaceInstances = workspaces.map(({ id }) => { const workspaceInstances = workspaces.map(({ id }) => {
const workspace = const workspace =
this._workspacesCache.get(id) || this._workspacesCache.get(id) ||
new Workspace({ new BlocksuiteWorkspace({
room: id, room: id,
}).register(BlockSchema); }).register(BlockSchema);
this._workspacesCache.set(id, workspace); this._workspacesCache.set(id, workspace);
if (workspace) { if (workspace) {
return new Promise<Workspace>(resolve => { return new Promise<BlocksuiteWorkspace>(resolve => {
downloadWorkspace(id).then(data => { this._apis.downloadWorkspace(id).then(data => {
applyUpdate(workspace.doc, new Uint8Array(data)); applyUpdate(workspace.doc, new Uint8Array(data));
resolve(workspace); resolve(workspace);
}); });
@ -135,7 +155,7 @@ export class AffineProvider extends BaseProvider {
const { id } = w; const { id } = w;
return new Promise<{ id: string; detail: WorkspaceDetail | null }>( return new Promise<{ id: string; detail: WorkspaceDetail | null }>(
resolve => { resolve => {
getWorkspaceDetail({ id }).then(data => { this._apis.getWorkspaceDetail({ id }).then(data => {
resolve({ id, detail: data || null }); resolve({ id, detail: data || null });
}); });
} }
@ -171,30 +191,34 @@ export class AffineProvider extends BaseProvider {
override async auth() { override async auth() {
const refreshToken = await storage.getItem('token'); const refreshToken = await storage.getItem('token');
if (refreshToken) { if (refreshToken) {
await token.refreshToken(refreshToken); await this._apis.token.refreshToken(refreshToken);
if (token.isLogin && !token.isExpired) { if (this._apis.token.isLogin && !this._apis.token.isExpired) {
// login success // login success
return; return;
} }
} }
const user = await this._authorizer[0]?.(); const user = await this._apis.signInWithGoogle?.();
assert(user); if (!user) {
this._user = { this._messageCenter.send(MessageCode.loginError);
id: user.id, }
name: user.name,
avatar: user.avatar_url,
email: user.email,
};
} }
public override async getUserInfo(): Promise<User | undefined> { public override async getUserInfo(): Promise<User | undefined> {
return this._user; const user = this._apis.token.user;
return user
? {
id: user.id,
name: user.name,
avatar: user.avatar_url,
email: user.email,
}
: undefined;
} }
public override async deleteWorkspace(id: string): Promise<void> { public override async deleteWorkspace(id: string): Promise<void> {
await this.closeWorkspace(id); await this.closeWorkspace(id);
IndexedDBProvider.delete(id); // IndexedDBProvider.delete(id);
await deleteWorkspace({ id }); await this._apis.deleteWorkspace({ id });
this._workspaces.remove(id); this._workspaces.remove(id);
} }
@ -213,83 +237,95 @@ export class AffineProvider extends BaseProvider {
} }
public override async closeWorkspace(id: string) { public override async closeWorkspace(id: string) {
const idb = this._idbMap.get(id); // const idb = this._idbMap.get(id);
idb?.destroy(); // idb?.destroy();
const ws = this._wsMap.get(id); const ws = this._wsMap.get(id);
ws?.disconnect(); ws?.disconnect();
} }
public override async leaveWorkspace(id: string): Promise<void> { public override async leaveWorkspace(id: string): Promise<void> {
await leaveWorkspace({ id }); await this._apis.leaveWorkspace({ id });
} }
public override async invite(id: string, email: string): Promise<void> { public override async invite(id: string, email: string): Promise<void> {
return await inviteMember({ id, email }); return await this._apis.inviteMember({ id, email });
} }
public override async removeMember(permissionId: number): Promise<void> { public override async removeMember(permissionId: number): Promise<void> {
return await removeMember({ permissionId }); return await this._apis.removeMember({ permissionId });
} }
private async _initWorkspaceDb(workspace: Workspace) { public override async linkLocal(workspace: BlocksuiteWorkspace) {
assert(workspace.room); return workspace;
let idb = this._idbMap.get(workspace.room); // assert(workspace.room);
idb?.destroy(); // let idb = this._idbMap.get(workspace.room);
idb = new IndexedDBProvider(workspace.room, workspace.doc); // idb?.destroy();
this._idbMap.set(workspace.room, idb); // idb = new IndexedDBProvider(workspace.room, workspace.doc);
await idb.whenSynced; // this._idbMap.set(workspace.room, idb);
this._logger('Local data loaded'); // await idb.whenSynced;
return idb; // this._logger('Local data loaded');
// return workspace;
}
public override async createWorkspaceInfo(
meta: WorkspaceMeta
): Promise<WorkspaceInfo> {
const { id } = await this._apis.createWorkspace(
meta as Required<WorkspaceMeta>
);
const workspaceInfo: WorkspaceInfo = {
name: meta.name,
id: id,
isPublish: false,
avatar: '',
owner: await this.getUserInfo(),
isLocal: true,
memberCount: 1,
provider: 'affine',
};
return workspaceInfo;
} }
public override async createWorkspace( public override async createWorkspace(
blocksuiteWorkspace: BlocksuiteWorkspace,
meta: WorkspaceMeta meta: WorkspaceMeta
): Promise<Workspace | undefined> { ): Promise<BlocksuiteWorkspace | undefined> {
assert(meta.name, 'Workspace name is required'); const workspaceId = blocksuiteWorkspace.room;
const { id } = await createWorkspace(meta as Required<WorkspaceMeta>); assert(workspaceId, 'Blocksuite Workspace without room(workspaceId).');
this._logger('Creating affine workspace'); this._logger('Creating affine workspace');
const nw = new Workspace({
room: id,
}).register(BlockSchema);
nw.meta.setName(meta.name);
this._initWorkspaceDb(nw);
const workspaceInfo: WS = { this._applyCloudUpdates(blocksuiteWorkspace);
this.linkLocal(blocksuiteWorkspace);
const workspaceInfo: WorkspaceInfo = {
name: meta.name, name: meta.name,
id, id: workspaceId,
isPublish: false, isPublish: false,
avatar: '', avatar: '',
owner: undefined, owner: undefined,
isLocal: true, isLocal: true,
memberCount: 1, memberCount: 1,
provider: 'local', provider: 'affine',
}; };
if (!meta.avatar) { if (!blocksuiteWorkspace.meta.avatar) {
// set default avatar await setDefaultAvatar(blocksuiteWorkspace);
const blob = await getDefaultHeadImgBlob(meta.name); workspaceInfo.avatar = blocksuiteWorkspace.meta.avatar;
const blobStorage = await nw.blobs;
assert(blobStorage, 'No blob storage');
const blobId = await blobStorage.set(blob);
const avatar = await blobStorage.get(blobId);
if (avatar) {
nw.meta.setAvatar(avatar);
workspaceInfo.avatar = avatar;
}
} }
this._workspaces.add(workspaceInfo); this._workspaces.add(workspaceInfo);
return nw; return blocksuiteWorkspace;
} }
public override async publish(id: string, isPublish: boolean): Promise<void> { public override async publish(id: string, isPublish: boolean): Promise<void> {
await updateWorkspace({ id, public: isPublish }); await this._apis.updateWorkspace({ id, public: isPublish });
} }
public override async getUserByEmail( public override async getUserByEmail(
workspace_id: string, workspace_id: string,
email: string email: string
): Promise<User | null> { ): Promise<User | null> {
const user = await getUserByEmail({ workspace_id, email }); const user = await this._apis.getUserByEmail({ workspace_id, email });
return user return user
? { ? {
id: user.id, id: user.id,

View File

@ -1,15 +1,16 @@
export { token } from './token.js'; // export { token } from './token.js';
export type { Callback } from './token.js'; export type { Callback } from './token.js';
import { getAuthorizer } from './token.js'; import { getAuthorizer } from './token.js';
import * as user from './user.js'; import * as user from './user.js';
import * as workspace from './workspace.js'; import * as workspace from './workspace.js';
import { token } from './token.js';
export type Apis = typeof user & export type Apis = typeof user &
typeof workspace & { Omit<typeof workspace, 'WorkspaceType' | 'PermissionType'> & {
signInWithGoogle: ReturnType<typeof getAuthorizer>[0]; signInWithGoogle: ReturnType<typeof getAuthorizer>[0];
onAuthStateChanged: ReturnType<typeof getAuthorizer>[1]; onAuthStateChanged: ReturnType<typeof getAuthorizer>[1];
}; } & { token: typeof token };
export const getApis = (): Apis => { export const getApis = (): Apis => {
const [signInWithGoogle, onAuthStateChanged] = getAuthorizer(); const [signInWithGoogle, onAuthStateChanged] = getAuthorizer();
@ -18,9 +19,10 @@ export const getApis = (): Apis => {
...workspace, ...workspace,
signInWithGoogle, signInWithGoogle,
onAuthStateChanged, onAuthStateChanged,
token,
}; };
}; };
export type { AccessTokenMessage } from './token'; export type { AccessTokenMessage } from './token';
export type { Member, Workspace } from './workspace'; export type { Member, Workspace, WorkspaceDetail } from './workspace';
export { WorkspaceType } from './workspace.js'; export { WorkspaceType } from './workspace.js';

View File

@ -2,8 +2,8 @@ import { initializeApp } from 'firebase/app';
import { getAuth, GoogleAuthProvider, signInWithPopup } from 'firebase/auth'; import { getAuth, GoogleAuthProvider, signInWithPopup } from 'firebase/auth';
import type { User } from 'firebase/auth'; import type { User } from 'firebase/auth';
import { getLogger } from '../../../logger'; import { getLogger } from '../../../logger.js';
import { bareClient } from './request'; import { bareClient } from './request.js';
export interface AccessTokenMessage { export interface AccessTokenMessage {
create_at: number; create_at: number;
@ -46,6 +46,10 @@ class Token {
this._setToken(); // fill with default value this._setToken(); // fill with default value
} }
get user() {
return this._user;
}
private _setToken(login?: LoginResponse) { private _setToken(login?: LoginResponse) {
this._accessToken = login?.token || ''; this._accessToken = login?.token || '';
this._refreshToken = login?.refresh || ''; this._refreshToken = login?.refresh || '';
@ -185,6 +189,7 @@ export const getAuthorizer = () => {
return [signInWithGoogle, onAuthStateChanged] as const; return [signInWithGoogle, onAuthStateChanged] as const;
} catch (e) { } catch (e) {
getLogger('getAuthorizer')(e);
return [] as const; return [] as const;
} }
}; };

View File

@ -0,0 +1 @@
export { varStorage as storage } from 'lib0/storage';

View File

@ -1,6 +1,7 @@
import { BlobStorage, Workspace } from '@blocksuite/store'; import { Workspace as BlocksuiteWorkspace, uuidv4 } from '@blocksuite/store';
import { Logger, User, Workspace as WS, WorkspaceMeta } from '../types'; import { MessageCenter } from 'src/message';
import type { WorkspacesScope } from '../workspaces'; import { Logger, User, WorkspaceInfo, WorkspaceMeta } from '../types';
import type { WorkspaceMetaCollectionScope } from '../workspace-meta-collection';
const defaultLogger = () => { const defaultLogger = () => {
return; return;
@ -8,17 +9,24 @@ const defaultLogger = () => {
export interface ProviderConstructorParams { export interface ProviderConstructorParams {
logger?: Logger; logger?: Logger;
workspaces: WorkspacesScope; workspaces: WorkspaceMetaCollectionScope;
messageCenter: MessageCenter;
} }
export class BaseProvider { export class BaseProvider {
public readonly id: string = 'base'; public readonly id: string = 'base';
protected _workspaces!: WorkspacesScope; protected _workspaces!: WorkspaceMetaCollectionScope;
protected _logger!: Logger; protected _logger!: Logger;
protected _messageCenter!: MessageCenter;
public constructor({ logger, workspaces }: ProviderConstructorParams) { public constructor({
logger,
workspaces,
messageCenter,
}: ProviderConstructorParams) {
this._logger = (logger || defaultLogger) as Logger; this._logger = (logger || defaultLogger) as Logger;
this._workspaces = workspaces; this._workspaces = workspaces;
this._messageCenter = messageCenter;
} }
/** /**
@ -28,6 +36,12 @@ export class BaseProvider {
return; return;
} }
public async createWorkspaceInfo(
meta: WorkspaceMeta
): Promise<WorkspaceInfo> {
throw new Error(`provider: ${this.id} createWorkspaceInfo Not implemented`);
}
/** /**
* auth provider * auth provider
*/ */
@ -47,14 +61,16 @@ export class BaseProvider {
* @param workspace * @param workspace
* @returns * @returns
*/ */
public async warpWorkspace(workspace: Workspace): Promise<Workspace> { public async warpWorkspace(
workspace: BlocksuiteWorkspace
): Promise<BlocksuiteWorkspace> {
return workspace; return workspace;
} }
/** /**
* load workspaces * load workspaces
**/ **/
public async loadWorkspaces(): Promise<WS[]> { public async loadWorkspaces(): Promise<WorkspaceInfo[]> {
throw new Error(`provider: ${this.id} loadWorkSpace Not implemented`); throw new Error(`provider: ${this.id} loadWorkSpace Not implemented`);
} }
@ -153,10 +169,10 @@ export class BaseProvider {
* @param {WorkspaceMeta} meta * @param {WorkspaceMeta} meta
*/ */
public async createWorkspace( public async createWorkspace(
blocksuiteWorkspace: BlocksuiteWorkspace,
meta: WorkspaceMeta meta: WorkspaceMeta
): Promise<Workspace | undefined> { ): Promise<BlocksuiteWorkspace | undefined> {
meta; return blocksuiteWorkspace;
return;
} }
/** /**
@ -169,4 +185,15 @@ export class BaseProvider {
email; email;
return null; return null;
} }
/**
* link workspace to local caches
* @param workspace
* @returns
*/
public async linkLocal(
workspace: BlocksuiteWorkspace
): Promise<BlocksuiteWorkspace> {
return workspace;
}
} }

View File

@ -1 +1 @@
export * from './local'; export * from './local.js';

View File

@ -1,52 +1,63 @@
import { describe, test, expect } from 'vitest'; import { test, expect } from '@playwright/test';
import { Workspaces } from '../../workspaces'; import { WorkspaceMetaCollection } from '../../workspace-meta-collection.js';
import { LocalProvider } from './local'; import { LocalProvider } from './local.js';
import { createBlocksuiteWorkspace } from '../../utils/index.js';
import 'fake-indexeddb/auto'; import 'fake-indexeddb/auto';
import { BlobStorage } from '@blocksuite/store';
describe('local provider', () => { test.describe.serial('local provider', () => {
const workspaces = new Workspaces(); const workspaceMetaCollection = new WorkspaceMetaCollection();
const provider = new LocalProvider({ const provider = new LocalProvider({
workspaces: workspaces.createScope(), workspaces: workspaceMetaCollection.createScope(),
blobs: new BlobStorage(),
}); });
const workspaceName = 'workspace-test'; const workspaceName = 'workspace-test';
let workspaceId: string | undefined; let workspaceId: string | undefined;
test('create workspace', async () => { test('create workspace', async () => {
const w = await provider.createWorkspace({ const workspaceInfo = await provider.createWorkspaceInfo({
name: workspaceName,
avatar: 'avatar-url-test',
});
workspaceId = workspaceInfo.id;
const blocksuiteWorkspace = createBlocksuiteWorkspace(workspaceId);
await provider.createWorkspace(blocksuiteWorkspace, {
name: workspaceName, name: workspaceName,
avatar: 'avatar-url-test', avatar: 'avatar-url-test',
}); });
workspaceId = w?.room;
expect(workspaces.workspaces.length).toEqual(1); expect(workspaceMetaCollection.workspaces.length).toEqual(1);
expect(workspaces.workspaces[0].name).toEqual(workspaceName); expect(workspaceMetaCollection.workspaces[0].name).toEqual(workspaceName);
}); });
test('workspace list cache', async () => { test('workspace list cache', async () => {
const workspaces1 = new Workspaces(); const workspacesMetaCollection1 = new WorkspaceMetaCollection();
const provider1 = new LocalProvider({ const provider1 = new LocalProvider({
workspaces: workspaces1.createScope(), workspaces: workspacesMetaCollection1.createScope(),
blobs: new BlobStorage(),
}); });
await provider1.loadWorkspaces(); await provider1.loadWorkspaces();
expect(workspaces1.workspaces.length).toEqual(1); expect(workspacesMetaCollection1.workspaces.length).toEqual(1);
expect(workspaces1.workspaces[0].name).toEqual(workspaceName); expect(workspacesMetaCollection1.workspaces[0].name).toEqual(workspaceName);
expect(workspaces1.workspaces[0].id).toEqual(workspaceId); expect(workspacesMetaCollection1.workspaces[0].id).toEqual(workspaceId);
}); });
test('update workspace', async () => { test('update workspace', async () => {
await provider.updateWorkspaceMeta(workspaceId!, { await provider.updateWorkspaceMeta(workspaceId!, {
name: '1111', name: '1111',
}); });
expect(workspaces.workspaces[0].name).toEqual('1111'); expect(workspaceMetaCollection.workspaces[0].name).toEqual('1111');
}); });
test('delete workspace', async () => { test('delete workspace', async () => {
expect(workspaces.workspaces.length).toEqual(1); expect(workspaceMetaCollection.workspaces.length).toEqual(1);
await provider.deleteWorkspace(workspaces.workspaces[0].id); /**
expect(workspaces.workspaces.length).toEqual(0); * FIXME
* If we don't wrap setTimeout,
* Running deleteWorkspace will crash the worker, and get error like next line:
* InvalidStateError: An operation was called on an object on which it is not allowed or at a time when it is not allowed. Also occurs if a request is made on a source object that has been deleted or removed. Use TransactionInactiveError or ReadOnlyError when possible, as they are more specific variations of InvalidStateError.
* */
setTimeout(async () => {
await provider.deleteWorkspace(workspaceMetaCollection.workspaces[0].id);
expect(workspaceMetaCollection.workspaces.length).toEqual(0);
}, 10);
}); });
}); });

View File

@ -1,11 +1,11 @@
import { BaseProvider } from '../base'; import { BaseProvider } from '../base.js';
import type { ProviderConstructorParams } from '../base'; import type { ProviderConstructorParams } from '../base';
import { varStorage as storage } from 'lib0/storage'; import { varStorage as storage } from 'lib0/storage';
import { Workspace as WS, WorkspaceMeta } from '../../types'; import { WorkspaceInfo, WorkspaceMeta } from '../../types';
import { Workspace, uuidv4 } from '@blocksuite/store'; import { Workspace as BlocksuiteWorkspace, uuidv4 } from '@blocksuite/store';
import { IndexedDBProvider } from '../indexeddb'; import { IndexedDBProvider } from './indexeddb.js';
import assert from 'assert'; import assert from 'assert';
import { getDefaultHeadImgBlob } from '../../utils'; import { setDefaultAvatar } from '../utils.js';
const WORKSPACE_KEY = 'workspaces'; const WORKSPACE_KEY = 'workspaces';
@ -18,34 +18,34 @@ export class LocalProvider extends BaseProvider {
this.loadWorkspaces(); this.loadWorkspaces();
} }
private _storeWorkspaces(workspaces: WS[]) { private _storeWorkspaces(workspaces: WorkspaceInfo[]) {
storage.setItem(WORKSPACE_KEY, JSON.stringify(workspaces)); storage.setItem(WORKSPACE_KEY, JSON.stringify(workspaces));
} }
private async _initWorkspaceDb(workspace: Workspace) { public override async linkLocal(workspace: BlocksuiteWorkspace) {
assert(workspace.room); assert(workspace.room);
let idb = this._idbMap.get(workspace.room); let idb = this._idbMap.get(workspace.room);
idb?.destroy(); idb?.destroy();
idb = new IndexedDBProvider(workspace.room, workspace.doc); idb = new IndexedDBProvider(workspace.room, workspace.doc);
this._idbMap.set(workspace.room, idb); this._idbMap.set(workspace.room, idb);
this._logger('Local data loaded'); this._logger('Local data loaded');
return idb;
}
public override async warpWorkspace(
workspace: Workspace
): Promise<Workspace> {
assert(workspace.room);
await this._initWorkspaceDb(workspace);
return workspace; return workspace;
} }
override loadWorkspaces(): Promise<WS[]> { public override async warpWorkspace(
workspace: BlocksuiteWorkspace
): Promise<BlocksuiteWorkspace> {
assert(workspace.room);
await this.linkLocal(workspace);
return workspace;
}
override loadWorkspaces(): Promise<WorkspaceInfo[]> {
const workspaceStr = storage.getItem(WORKSPACE_KEY); const workspaceStr = storage.getItem(WORKSPACE_KEY);
let workspaces: WS[] = []; let workspaces: WorkspaceInfo[] = [];
if (workspaceStr) { if (workspaceStr) {
try { try {
workspaces = JSON.parse(workspaceStr) as WS[]; workspaces = JSON.parse(workspaceStr) as WorkspaceInfo[];
workspaces.forEach(workspace => { workspaces.forEach(workspace => {
this._workspaces.add(workspace); this._workspaces.add(workspace);
}); });
@ -75,13 +75,10 @@ export class LocalProvider extends BaseProvider {
this._storeWorkspaces(this._workspaces.list()); this._storeWorkspaces(this._workspaces.list());
} }
public override async createWorkspace( public override async createWorkspaceInfo(
meta: WorkspaceMeta meta: WorkspaceMeta
): Promise<Workspace | undefined> { ): Promise<WorkspaceInfo> {
assert(meta.name, 'Workspace name is required'); const workspaceInfo: WorkspaceInfo = {
this._logger('Creating affine workspace');
const workspaceInfo: WS = {
name: meta.name, name: meta.name,
id: uuidv4(), id: uuidv4(),
isPublish: false, isPublish: false,
@ -91,27 +88,41 @@ export class LocalProvider extends BaseProvider {
memberCount: 1, memberCount: 1,
provider: 'local', provider: 'local',
}; };
return Promise.resolve(workspaceInfo);
}
public override async createWorkspace(
blocksuiteWorkspace: BlocksuiteWorkspace,
meta: WorkspaceMeta
): Promise<BlocksuiteWorkspace | undefined> {
const workspaceId = blocksuiteWorkspace.room;
assert(workspaceId, 'Blocksuite Workspace without room(workspaceId).');
assert(meta.name, 'Workspace name is required');
this._logger('Creating affine workspace');
const workspaceInfo: WorkspaceInfo = {
name: meta.name,
id: workspaceId,
isPublish: false,
avatar: '',
owner: undefined,
isLocal: true,
memberCount: 1,
provider: 'local',
};
this.linkLocal(blocksuiteWorkspace);
blocksuiteWorkspace.meta.setName(meta.name);
const workspace = new Workspace({ room: workspaceInfo.id });
this._initWorkspaceDb(workspace);
workspace.meta.setName(meta.name);
if (!meta.avatar) { if (!meta.avatar) {
// set default avatar await setDefaultAvatar(blocksuiteWorkspace);
const blob = await getDefaultHeadImgBlob(meta.name); workspaceInfo.avatar = blocksuiteWorkspace.meta.avatar;
const blobStorage = await workspace.blobs;
assert(blobStorage, 'No blob storage');
const blobId = await blobStorage.set(blob);
const avatar = await blobStorage.get(blobId);
if (avatar) {
workspace.meta.setAvatar(avatar);
workspaceInfo.avatar = avatar;
}
} }
this._workspaces.add(workspaceInfo); this._workspaces.add(workspaceInfo);
this._storeWorkspaces(this._workspaces.list()); this._storeWorkspaces(this._workspaces.list());
return workspace; return blocksuiteWorkspace;
} }
public override async clear(): Promise<void> { public override async clear(): Promise<void> {

View File

@ -0,0 +1,15 @@
import assert from 'assert';
import { Workspace as BlocksuiteWorkspace } from '@blocksuite/store';
import { getDefaultHeadImgBlob } from '../utils/index.js';
export const setDefaultAvatar = async (
blocksuiteWorkspace: BlocksuiteWorkspace
) => {
const blob = await getDefaultHeadImgBlob(blocksuiteWorkspace.meta.name);
const blobStorage = await blocksuiteWorkspace.blobs;
assert(blobStorage, 'No blob storage');
const avatar = await blobStorage.set(blob);
if (avatar) {
blocksuiteWorkspace.meta.setAvatar(avatar);
}
};

View File

@ -1,6 +1,6 @@
import { getLogger } from '../logger'; import { getLogger } from '../logger';
export type Workspace = { export type WorkspaceInfo = {
name: string; name: string;
id: string; id: string;
isPublish?: boolean; isPublish?: boolean;
@ -18,6 +18,11 @@ export type User = {
avatar: string; avatar: string;
}; };
export type WorkspaceMeta = Pick<Workspace, 'name' | 'avatar'>; export type WorkspaceMeta = Pick<WorkspaceInfo, 'name' | 'avatar'>;
export type Logger = ReturnType<typeof getLogger>; export type Logger = ReturnType<typeof getLogger>;
export type Message = {
code: number;
message: string;
};

View File

@ -1,3 +1,12 @@
import { Workspace as BlocksuiteWorkspace } from '@blocksuite/store';
import { BlockSchema } from '@blocksuite/blocks/models';
export const createBlocksuiteWorkspace = (workspaceId: string) => {
return new BlocksuiteWorkspace({
room: workspaceId,
}).register(BlockSchema);
};
const DefaultHeadImgColors = [ const DefaultHeadImgColors = [
['#C6F2F3', '#0C6066'], ['#C6F2F3', '#0C6066'],
['#FFF5AB', '#896406'], ['#FFF5AB', '#896406'],

View File

@ -1,14 +1,14 @@
import { describe, test, expect } from 'vitest'; import { test, expect } from '@playwright/test';
import { Workspaces } from './workspaces'; import { WorkspaceMetaCollection } from './workspace-meta-collection.js';
import type { WorkspacesChangeEvent } from './workspaces'; import type { WorkspaceMetaCollectionChangeEvent } from './workspace-meta-collection';
describe('workspaces observable', () => { test.describe.serial('workspace meta collection observable', () => {
const workspaces = new Workspaces(); const workspaces = new WorkspaceMetaCollection();
const scope = workspaces.createScope(); const scope = workspaces.createScope();
test('add workspace', () => { test('add workspace', () => {
workspaces.once('change', (event: WorkspacesChangeEvent) => { workspaces.once('change', (event: WorkspaceMetaCollectionChangeEvent) => {
expect(event.added?.id).toEqual('123'); expect(event.added?.id).toEqual('123');
}); });
scope.add({ scope.add({
@ -30,7 +30,7 @@ describe('workspaces observable', () => {
}); });
test('update workspace', () => { test('update workspace', () => {
workspaces.once('change', (event: WorkspacesChangeEvent) => { workspaces.once('change', (event: WorkspaceMetaCollectionChangeEvent) => {
expect(event.updated?.name).toEqual('demo'); expect(event.updated?.name).toEqual('demo');
}); });
scope.update('123', { name: 'demo' }); scope.update('123', { name: 'demo' });
@ -42,7 +42,7 @@ describe('workspaces observable', () => {
}); });
test('delete workspace', () => { test('delete workspace', () => {
workspaces.once('change', (event: WorkspacesChangeEvent) => { workspaces.once('change', (event: WorkspaceMetaCollectionChangeEvent) => {
expect(event.deleted?.id).toEqual('123'); expect(event.deleted?.id).toEqual('123');
}); });
scope.remove('123'); scope.remove('123');

View File

@ -1,25 +1,25 @@
import { Observable } from 'lib0/observable'; import { Observable } from 'lib0/observable';
import type { Workspace, WorkspaceMeta } from '../types'; import type { WorkspaceInfo, WorkspaceMeta } from './types';
export interface WorkspacesScope { export interface WorkspaceMetaCollectionScope {
get: (workspaceId: string) => Workspace | undefined; get: (workspaceId: string) => WorkspaceInfo | undefined;
list: () => Workspace[]; list: () => WorkspaceInfo[];
add: (workspace: Workspace) => void; add: (workspace: WorkspaceInfo) => void;
remove: (workspaceId: string) => boolean; remove: (workspaceId: string) => boolean;
clear: () => void; clear: () => void;
update: (workspaceId: string, workspaceMeta: Partial<WorkspaceMeta>) => void; update: (workspaceId: string, workspaceMeta: Partial<WorkspaceMeta>) => void;
} }
export interface WorkspacesChangeEvent { export interface WorkspaceMetaCollectionChangeEvent {
added?: Workspace; added?: WorkspaceInfo;
deleted?: Workspace; deleted?: WorkspaceInfo;
updated?: Workspace; updated?: WorkspaceInfo;
} }
export class Workspaces extends Observable<'change'> { export class WorkspaceMetaCollection extends Observable<'change'> {
private _workspacesMap = new Map<string, Workspace>(); private _workspacesMap = new Map<string, WorkspaceInfo>();
get workspaces(): Workspace[] { get workspaces(): WorkspaceInfo[] {
return Array.from(this._workspacesMap.values()); return Array.from(this._workspacesMap.values());
} }
@ -27,7 +27,7 @@ export class Workspaces extends Observable<'change'> {
return this._workspacesMap.get(workspaceId); return this._workspacesMap.get(workspaceId);
} }
createScope(): WorkspacesScope { createScope(): WorkspaceMetaCollectionScope {
const scopedWorkspaceIds = new Set<string>(); const scopedWorkspaceIds = new Set<string>();
const get = (workspaceId: string) => { const get = (workspaceId: string) => {
@ -37,7 +37,7 @@ export class Workspaces extends Observable<'change'> {
return this._workspacesMap.get(workspaceId); return this._workspacesMap.get(workspaceId);
}; };
const add = (workspace: Workspace) => { const add = (workspace: WorkspaceInfo) => {
if (this._workspacesMap.has(workspace.id)) { if (this._workspacesMap.has(workspace.id)) {
throw new Error(`Duplicate workspace id.`); throw new Error(`Duplicate workspace id.`);
} }
@ -47,7 +47,7 @@ export class Workspaces extends Observable<'change'> {
this.emit('change', [ this.emit('change', [
{ {
added: workspace, added: workspace,
} as WorkspacesChangeEvent, } as WorkspaceMetaCollectionChangeEvent,
]); ]);
}; };
@ -69,7 +69,7 @@ export class Workspaces extends Observable<'change'> {
this.emit('change', [ this.emit('change', [
{ {
deleted: workspace, deleted: workspace,
} as WorkspacesChangeEvent, } as WorkspaceMetaCollectionChangeEvent,
]); ]);
} }
return true; return true;
@ -99,13 +99,13 @@ export class Workspaces extends Observable<'change'> {
this.emit('change', [ this.emit('change', [
{ {
updated: this._workspacesMap.get(workspaceId), updated: this._workspacesMap.get(workspaceId),
} as WorkspacesChangeEvent, } as WorkspaceMetaCollectionChangeEvent,
]); ]);
}; };
// TODO: need to optimize // TODO: need to optimize
const list = () => { const list = () => {
const workspaces: Workspace[] = []; const workspaces: WorkspaceInfo[] = [];
scopedWorkspaceIds.forEach(id => { scopedWorkspaceIds.forEach(id => {
const workspace = this._workspacesMap.get(id); const workspace = this._workspacesMap.get(id);
if (workspace) { if (workspace) {

View File

@ -1,2 +0,0 @@
export { Workspaces } from './workspaces';
export type { WorkspacesScope, WorkspacesChangeEvent } from './workspaces';

View File

@ -1,101 +0,0 @@
import { Workspace as WS } from '../types';
import { Observable } from 'lib0/observable';
import { uuidv4 } from '@blocksuite/store';
import { DataCenter } from '../datacenter';
export class Workspaces extends Observable<string> {
private _workspaces: WS[];
private readonly _dc: DataCenter;
constructor(dc: DataCenter) {
super();
this._workspaces = [];
this._dc = dc;
}
public init() {
this._loadWorkspaces();
}
get workspaces() {
return this._workspaces;
}
/**
* emit when workspaces changed
* @param {(workspace: WS[]) => void} cb
*/
onWorkspacesChange(cb: (workspace: WS[]) => void) {
this.on('change', cb);
}
private async _loadWorkspaces() {
const providers = this._dc.providers;
let workspaces: WS[] = [];
providers.forEach(async p => {
const pWorkspaces = await p.loadWorkspaces();
workspaces = [...workspaces, ...pWorkspaces];
this._updateWorkspaces([...workspaces, ...pWorkspaces]);
});
}
/**
* focus load all workspaces list
*/
public async refreshWorkspaces() {
this._loadWorkspaces();
}
private _updateWorkspaces(workspaces: WS[]) {
this._workspaces = workspaces;
this.emit('change', this._workspaces);
}
private _getDefaultWorkspace(name: string): WS {
return {
name,
id: uuidv4(),
isPublish: false,
avatar: '',
owner: undefined,
isLocal: true,
memberCount: 1,
provider: 'local',
};
}
/** add a local workspaces */
public addLocalWorkspace(name: string) {
const workspace = this._getDefaultWorkspace(name);
this._updateWorkspaces([...this._workspaces, workspace]);
return workspace;
}
/** delete a workspaces by id */
public delete(id: string) {
const index = this._workspaces.findIndex(w => w.id === id);
if (index >= 0) {
this._workspaces.splice(index, 1);
this._updateWorkspaces(this._workspaces);
}
}
/** get workspace info by id */
public getWorkspace(id: string) {
return this._workspaces.find(w => w.id === id);
}
/** check if workspace exists */
public hasWorkspace(id: string) {
return this._workspaces.some(w => w.id === id);
}
public updateWorkspaceInfo(id: string, info: Partial<WS>) {
const index = this._workspaces.findIndex(w => w.id === id);
if (index >= 0) {
this._workspaces[index] = { ...this._workspaces[index], ...info };
this._updateWorkspaces(this._workspaces);
}
}
}

View File

@ -21,5 +21,10 @@
"outDir": "./dist" "outDir": "./dist"
}, },
"include": ["next-env.d.ts", "src/**/*.ts", "pages/**/*.tsx"], "include": ["next-env.d.ts", "src/**/*.ts", "pages/**/*.tsx"],
"exclude": ["node_modules", "dist", "src/provider/affine/sync.js"] "exclude": [
"node_modules",
"dist",
"src/provider/affine/sync.js",
"src/**/*.spec.ts"
]
} }

View File

@ -0,0 +1,29 @@
{
"name": "@affine/i18n",
"version": "0.1.0",
"description": "",
"main": "dist/src/index.js",
"type": "module",
"types": "dist/src/index.d.ts",
"exports": {
"./src/*": "./dist/src/*.js",
".": "./dist/src/index.js"
},
"scripts": {
"build": "tsc --project ./tsconfig.json"
},
"keywords": [],
"repository": {
"type": "git",
"url": "git+https://github.com/toeverything/AFFiNE.git"
},
"dependencies": {
"i18next": "^21.9.1",
"prettier": "^2.7.1",
"react-i18next": "^11.18.4"
},
"devDependencies": {
"@types/prettier": "^2.7.2",
"typescript": "^4.8.4"
}
}

View File

@ -1,10 +1,6 @@
import i18next, { Resource } from 'i18next'; import i18next, { Resource } from 'i18next';
import { import { initReactI18next, useTranslation } from 'react-i18next';
I18nextProvider, import { LOCALES } from './resources/index.js';
initReactI18next,
useTranslation,
} from 'react-i18next';
import { LOCALES } from './resources';
import type en_US from './resources/en.json'; import type en_US from './resources/en.json';
// const localStorage = { // const localStorage = {
@ -27,7 +23,7 @@ declare module 'react-i18next' {
// const STORAGE_KEY = 'i18n_lng'; // const STORAGE_KEY = 'i18n_lng';
export { i18n, useTranslation, I18nProvider, LOCALES }; export { i18n, useTranslation, LOCALES };
const resources = LOCALES.reduce<Resource>( const resources = LOCALES.reduce<Resource>(
(acc, { tag, res }) => ({ ...acc, [tag]: { translation: res } }), (acc, { tag, res }) => ({ ...acc, [tag]: { translation: res } }),
@ -63,4 +59,4 @@ i18n.on('languageChanged', () => {
// localStorage.setItem(STORAGE_KEY, lng); // localStorage.setItem(STORAGE_KEY, lng);
}); });
const I18nProvider = I18nextProvider; // const I18nProvider = I18nextProvider;

View File

@ -54,7 +54,7 @@
"Strikethrough": "Strikethrough", "Strikethrough": "Strikethrough",
"Inline code": "Inline code", "Inline code": "Inline code",
"Code block": "Code block", "Code block": "Code block",
"Hyperlink(with selected text)": "Hyperlink(with selected text)", "Link": "Hyperlink(with selected text)",
"Body text": "Body text", "Body text": "Body text",
"Heading": "Heading {{number}}", "Heading": "Heading {{number}}",
"Increase indent": "Increase indent", "Increase indent": "Increase indent",

View File

@ -43,7 +43,19 @@ import { fetchTolgee } from './request';
* ] * ]
* ``` * ```
*/ */
export const getAllProjectLanguages = async (size = 1000) => {
export const getAllProjectLanguages = async (
size = 1000
): Promise<
{
id: number;
name: string;
tag: string;
originalName: string;
flagEmoji: string;
base: boolean;
}[]
> => {
const url = `/languages?size=${size}`; const url = `/languages?size=${size}`;
const resp = await fetchTolgee(url); const resp = await fetchTolgee(url);
if (resp.status < 200 || resp.status >= 300) { if (resp.status < 200 || resp.status >= 300) {
@ -70,7 +82,7 @@ export const getAllProjectLanguages = async (size = 1000) => {
* *
* See https://tolgee.io/api#operation/getTranslations_ * See https://tolgee.io/api#operation/getTranslations_
*/ */
export const getTranslations = async () => { export const getTranslations = async (): Promise<unknown> => {
const url = '/translations'; const url = '/translations';
const resp = await fetchTolgee(url); const resp = await fetchTolgee(url);
if (resp.status < 200 || resp.status >= 300) { if (resp.status < 200 || resp.status >= 300) {
@ -87,17 +99,19 @@ export const getTranslations = async () => {
*/ */
export const getLanguagesTranslations = async <T extends string>( export const getLanguagesTranslations = async <T extends string>(
languages: T languages: T
) => { ): Promise<{ [key in T]?: Record<string, string> }> => {
const url = `/translations/${languages}`; const url = `/translations/${languages}`;
const resp = await fetchTolgee(url); const resp = await fetchTolgee(url);
if (resp.status < 200 || resp.status >= 300) { if (resp.status < 200 || resp.status >= 300) {
throw new Error(url + ' ' + resp.status + '\n' + (await resp.text())); throw new Error(url + ' ' + resp.status + '\n' + (await resp.text()));
} }
const json: { [key in T]?: Record<string, string> } = await resp.json(); const json = await resp.json();
return json; return json;
}; };
export const getRemoteTranslations = async (languages: string) => { export const getRemoteTranslations = async (
languages: string
): Promise<Record<string, string>> => {
const translations = await getLanguagesTranslations(languages); const translations = await getLanguagesTranslations(languages);
if (!(languages in translations)) { if (!(languages in translations)) {
return {}; return {};
@ -115,7 +129,7 @@ export const getRemoteTranslations = async (languages: string) => {
export const createsNewKey = async ( export const createsNewKey = async (
key: string, key: string,
translations: Record<string, string> translations: Record<string, string>
) => { ): Promise<unknown> => {
const url = '/translations/keys/create'; const url = '/translations/keys/create';
const resp = await fetchTolgee(url, { const resp = await fetchTolgee(url, {
method: 'POST', method: 'POST',
@ -133,7 +147,10 @@ export const createsNewKey = async (
* *
* See https://tolgee.io/api#operation/tagKey_1 * See https://tolgee.io/api#operation/tagKey_1
*/ */
export const addTag = async (keyId: string, tagName: string) => { export const addTag = async (
keyId: string,
tagName: string
): Promise<unknown> => {
const url = `/keys/${keyId}/tags`; const url = `/keys/${keyId}/tags`;
const resp = await fetchTolgee(url, { const resp = await fetchTolgee(url, {
method: 'PUT', method: 'PUT',
@ -151,7 +168,10 @@ export const addTag = async (keyId: string, tagName: string) => {
* *
* See https://tolgee.io/api#operation/tagKey_1 * See https://tolgee.io/api#operation/tagKey_1
*/ */
export const removeTag = async (keyId: string, tagId: number) => { export const removeTag = async (
keyId: string,
tagId: number
): Promise<unknown> => {
const url = `/keys/${keyId}/tags/${tagId}`; const url = `/keys/${keyId}/tags/${tagId}`;
const resp = await fetchTolgee(url, { const resp = await fetchTolgee(url, {
method: 'DELETE', method: 'DELETE',
@ -174,7 +194,7 @@ export const removeTag = async (keyId: string, tagId: number) => {
* *
* See https://tolgee.io/api#operation/export_1 * See https://tolgee.io/api#operation/export_1
*/ */
export const exportResources = async () => { export const exportResources = async (): Promise<Response> => {
const url = `/export`; const url = `/export`;
const resp = await fetchTolgee(url); const resp = await fetchTolgee(url);

View File

@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "ESNext",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": false,
"esModuleInterop": true,
"module": "ESNext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"experimentalDecorators": true,
"declaration": true,
"baseUrl": ".",
"rootDir": ".",
"outDir": "./dist"
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +0,0 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
include: ['packages/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
},
});