mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-11 03:48:39 +00:00
Compare commits
7 Commits
v0.7.0-can
...
v0.7.0-can
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4f60f22cf | ||
|
|
f05cd66368 | ||
|
|
869d98d019 | ||
|
|
cff741e9ba | ||
|
|
9f105b5806 | ||
|
|
cac609d36f | ||
|
|
c319e7e707 |
@@ -2,7 +2,7 @@
|
||||
"name": "@affine/core",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"version": "0.7.0-canary.49",
|
||||
"version": "0.7.0-canary.51",
|
||||
"scripts": {
|
||||
"build": "yarn -T run build-core",
|
||||
"dev": "yarn -T run dev-core",
|
||||
|
||||
@@ -200,7 +200,7 @@ export const Header = forwardRef<
|
||||
data-testid="editor-header-items"
|
||||
data-is-edgeless={mode === 'edgeless'}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<div className={styles.headerLeftSide}>
|
||||
{!open && <SidebarSwitch />}
|
||||
{props.leftSlot}
|
||||
</div>
|
||||
|
||||
@@ -83,13 +83,23 @@ export const titleWrapper = style({
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
export const headerLeftSide = style({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
width: '150px',
|
||||
'@media': {
|
||||
'(max-width: 900px)': {
|
||||
width: 'auto',
|
||||
},
|
||||
},
|
||||
});
|
||||
export const headerRightSide = style({
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '12px',
|
||||
zIndex: 1,
|
||||
justifyContent: 'flex-end',
|
||||
});
|
||||
|
||||
export const browserWarning = style({
|
||||
@@ -153,7 +163,6 @@ export const allPageListTitleWrapper = style({
|
||||
fontSize: 'var(--affine-font-base)',
|
||||
color: 'var(--affine-text-primary-color)',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
'::after': {
|
||||
content: '""',
|
||||
|
||||
@@ -52,7 +52,7 @@ export const ReferencePage = ({
|
||||
data-type="favorite-list-item"
|
||||
data-testid={`favorite-list-item-${pageId}`}
|
||||
active={active}
|
||||
href={`/workspace/${workspace.id}/${pageId}`}
|
||||
to={`/workspace/${workspace.id}/${pageId}`}
|
||||
icon={icon}
|
||||
collapsed={collapsible ? collapsed : undefined}
|
||||
onCollapsedChange={setCollapsed}
|
||||
|
||||
@@ -53,7 +53,7 @@ const RouteMenuLinkItem = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
{
|
||||
currentPath: string; // todo: pass through useRouter?
|
||||
path?: string | null;
|
||||
path: string;
|
||||
icon: ReactElement;
|
||||
children?: ReactElement;
|
||||
isDraggedOver?: boolean;
|
||||
@@ -66,7 +66,7 @@ const RouteMenuLinkItem = React.forwardRef<
|
||||
ref={ref}
|
||||
{...props}
|
||||
active={active}
|
||||
href={path ?? ''}
|
||||
to={path ?? ''}
|
||||
icon={icon}
|
||||
>
|
||||
{children}
|
||||
@@ -169,7 +169,7 @@ export const RootAppSidebar = ({
|
||||
<RouteMenuLinkItem
|
||||
icon={<FolderIcon />}
|
||||
currentPath={currentPath}
|
||||
path={currentWorkspaceId && paths.all(currentWorkspaceId)}
|
||||
path={paths.all(currentWorkspaceId)}
|
||||
onClick={backToAll}
|
||||
>
|
||||
<span data-testid="all-pages">{t['All pages']()}</span>
|
||||
@@ -198,7 +198,7 @@ export const RootAppSidebar = ({
|
||||
isDraggedOver={trashDroppable.isOver}
|
||||
icon={<DeleteTemporarilyIcon />}
|
||||
currentPath={currentPath}
|
||||
path={currentWorkspaceId && paths.trash(currentWorkspaceId)}
|
||||
path={paths.trash(currentWorkspaceId)}
|
||||
>
|
||||
<span data-testid="trash-page">{t['Trash']()}</span>
|
||||
</RouteMenuLinkItem>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@affine/docs",
|
||||
"version": "0.7.0-canary.49",
|
||||
"version": "0.7.0-canary.51",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@affine/electron",
|
||||
"private": true,
|
||||
"version": "0.7.0-canary.49",
|
||||
"version": "0.7.0-canary.51",
|
||||
"author": "affine",
|
||||
"repository": {
|
||||
"url": "https://github.com/toeverything/AFFiNE",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@affine/server",
|
||||
"private": true,
|
||||
"version": "0.7.0-canary.49",
|
||||
"version": "0.7.0-canary.51",
|
||||
"description": "Affine Node.js server",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
|
||||
@@ -48,5 +48,5 @@
|
||||
"@blocksuite/lit": "*",
|
||||
"@blocksuite/store": "*"
|
||||
},
|
||||
"version": "0.7.0-canary.49"
|
||||
"version": "0.7.0-canary.51"
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
import { useAtom } from 'jotai';
|
||||
import { type PropsWithChildren, useState } from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
|
||||
export default {
|
||||
title: 'Components/AppSidebar',
|
||||
@@ -31,18 +32,20 @@ export default {
|
||||
} satisfies Meta;
|
||||
|
||||
const Container = ({ children }: PropsWithChildren) => (
|
||||
<main
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: '100vw',
|
||||
height: 'calc(100vh - 40px)',
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</main>
|
||||
<MemoryRouter>
|
||||
<main
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: '100vw',
|
||||
height: 'calc(100vh - 40px)',
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</main>
|
||||
</MemoryRouter>
|
||||
);
|
||||
const Main = () => {
|
||||
const [open, setOpen] = useAtom(appSidebarOpenAtom);
|
||||
@@ -94,21 +97,21 @@ export const WithItems: StoryFn = () => {
|
||||
<div style={{ height: '20px' }} />
|
||||
<MenuLinkItem
|
||||
icon={<SettingsIcon />}
|
||||
href="/test"
|
||||
to="/test"
|
||||
onClick={() => alert('opened')}
|
||||
>
|
||||
Settings
|
||||
</MenuLinkItem>
|
||||
<MenuLinkItem
|
||||
icon={<SettingsIcon />}
|
||||
href="/test"
|
||||
to="/test"
|
||||
onClick={() => alert('opened')}
|
||||
>
|
||||
Settings
|
||||
</MenuLinkItem>
|
||||
<MenuLinkItem
|
||||
icon={<SettingsIcon />}
|
||||
href="/test"
|
||||
to="/test"
|
||||
onClick={() => alert('opened')}
|
||||
>
|
||||
Settings
|
||||
@@ -121,7 +124,7 @@ export const WithItems: StoryFn = () => {
|
||||
collapsed={collapsed}
|
||||
onCollapsedChange={setCollapsed}
|
||||
icon={<SettingsIcon />}
|
||||
href="/test"
|
||||
to="/test"
|
||||
onClick={() => alert('opened')}
|
||||
>
|
||||
Collapsible Item
|
||||
@@ -130,14 +133,14 @@ export const WithItems: StoryFn = () => {
|
||||
collapsed={!collapsed}
|
||||
onCollapsedChange={setCollapsed}
|
||||
icon={<SettingsIcon />}
|
||||
href="/test"
|
||||
to="/test"
|
||||
onClick={() => alert('opened')}
|
||||
>
|
||||
Collapsible Item
|
||||
</MenuLinkItem>
|
||||
<MenuLinkItem
|
||||
icon={<SettingsIcon />}
|
||||
href="/test"
|
||||
to="/test"
|
||||
onClick={() => alert('opened')}
|
||||
>
|
||||
Settings
|
||||
@@ -146,7 +149,7 @@ export const WithItems: StoryFn = () => {
|
||||
<CategoryDivider label="Others" />
|
||||
<MenuLinkItem
|
||||
icon={<DeleteTemporarilyIcon />}
|
||||
href="/test"
|
||||
to="/test"
|
||||
onClick={() => alert('opened')}
|
||||
>
|
||||
Trash
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@affine/monorepo",
|
||||
"version": "0.7.0-canary.49",
|
||||
"version": "0.7.0-canary.51",
|
||||
"private": true,
|
||||
"author": "toeverything",
|
||||
"license": "MPL-2.0",
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"peerDependencies": {
|
||||
"ts-node": "*"
|
||||
},
|
||||
"version": "0.7.0-canary.49"
|
||||
"version": "0.7.0-canary.51"
|
||||
}
|
||||
|
||||
@@ -67,5 +67,5 @@
|
||||
"vite": "^4.4.4",
|
||||
"yjs": "^13.6.7"
|
||||
},
|
||||
"version": "0.7.0-canary.49"
|
||||
"version": "0.7.0-canary.51"
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ export const Default: StoryFn = () => {
|
||||
</MenuItem>
|
||||
<MenuLinkItem
|
||||
icon={<SettingsIcon />}
|
||||
href="/test"
|
||||
to="/test"
|
||||
onClick={() => alert('opened')}
|
||||
>
|
||||
Normal Link Item
|
||||
@@ -26,7 +26,7 @@ export const Default: StoryFn = () => {
|
||||
<MenuLinkItem
|
||||
active
|
||||
icon={<SettingsIcon />}
|
||||
href="/test"
|
||||
to="/test"
|
||||
onClick={() => alert('opened')}
|
||||
>
|
||||
Primary Item
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { ArrowDownSmallIcon } from '@blocksuite/icons';
|
||||
import { Link, type LinkProps } from '@mui/material';
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import type { LinkProps } from 'react-router-dom';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import * as styles from './index.css';
|
||||
|
||||
@@ -16,7 +17,7 @@ export interface MenuItemProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
|
||||
export interface MenuLinkItemProps
|
||||
extends MenuItemProps,
|
||||
Pick<LinkProps, 'href'> {}
|
||||
Pick<LinkProps, 'to'> {}
|
||||
|
||||
const stopPropagation: React.MouseEventHandler = e => {
|
||||
e.stopPropagation();
|
||||
@@ -90,9 +91,9 @@ export const MenuItem = React.forwardRef<HTMLDivElement, MenuItemProps>(
|
||||
MenuItem.displayName = 'MenuItem';
|
||||
|
||||
export const MenuLinkItem = React.forwardRef<HTMLDivElement, MenuLinkItemProps>(
|
||||
({ href, ...props }, ref) => {
|
||||
({ to, ...props }, ref) => {
|
||||
return (
|
||||
<Link href={href} className={styles.linkItemRoot}>
|
||||
<Link to={to} className={styles.linkItemRoot}>
|
||||
{/* The <a> element rendered by Link does not generate display box due to `display: contents` style */}
|
||||
{/* Thus ref is passed to MenuItem instead of Link */}
|
||||
<MenuItem ref={ref} {...props}></MenuItem>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Tooltip } from '@affine/component';
|
||||
import { EditCollectionModel } from '@affine/component/page-list';
|
||||
import type { PropertiesMeta } from '@affine/env/filter';
|
||||
import type { GetPageInfoById } from '@affine/env/page-info';
|
||||
@@ -102,9 +103,21 @@ export const CollectionBar = ({
|
||||
width: 20,
|
||||
}}
|
||||
/>
|
||||
<div style={{ marginRight: 10 }}>
|
||||
{setting.currentCollection.name}
|
||||
</div>
|
||||
<Tooltip
|
||||
content={setting.currentCollection.name}
|
||||
pointerEnterDelay={1500}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
marginRight: 10,
|
||||
maxWidth: 200,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
>
|
||||
{setting.currentCollection.name}
|
||||
</div>
|
||||
</Tooltip>
|
||||
{actions.map(action => {
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
import { globalStyle, style } from '@vanilla-extract/css';
|
||||
|
||||
export const menuTitleStyle = style({
|
||||
marginLeft: '12px',
|
||||
@@ -20,6 +20,8 @@ export const viewButton = style({
|
||||
padding: '4px 8px',
|
||||
fontSize: 'var(--affine-font-xs)',
|
||||
background: 'var(--affine-white)',
|
||||
maxWidth: '200px',
|
||||
overflow: 'hidden',
|
||||
color: 'var(--affine-text-secondary-color)',
|
||||
border: '1px solid var(--affine-border-color)',
|
||||
transition: 'margin-left 0.2s ease-in-out',
|
||||
@@ -28,6 +30,20 @@ export const viewButton = style({
|
||||
background: 'var(--affine-hover-color)',
|
||||
},
|
||||
marginRight: '20px',
|
||||
'@media': {
|
||||
'(max-width: 1200px)': {
|
||||
maxWidth: '100px',
|
||||
},
|
||||
'(max-width: 900px)': {
|
||||
maxWidth: '150px',
|
||||
marginRight: '10px',
|
||||
},
|
||||
},
|
||||
});
|
||||
globalStyle(`${viewButton} > span`, {
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
});
|
||||
export const viewMenu = style({});
|
||||
export const viewOption = style({
|
||||
|
||||
@@ -16,7 +16,7 @@ import { useAtom } from 'jotai';
|
||||
import type { MouseEvent, ReactNode } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { Button, MenuItem } from '../../..';
|
||||
import { Button, MenuItem, Tooltip } from '../../..';
|
||||
import Menu from '../../../ui/menu/menu';
|
||||
import { appSidebarOpenAtom } from '../../app-sidebar';
|
||||
import { CreateFilterMenu } from '../filter/vars';
|
||||
@@ -80,39 +80,53 @@ const CollectionOption = ({
|
||||
key={collection.id}
|
||||
className={styles.viewMenu}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
<Tooltip
|
||||
content={collection.name}
|
||||
placement="right"
|
||||
pointerEnterDelay={1500}
|
||||
>
|
||||
<div>{collection.name}</div>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
{actions.map((v, i) => {
|
||||
const onClick = (e: MouseEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation();
|
||||
v.click();
|
||||
};
|
||||
return (
|
||||
<div
|
||||
data-testid={`collection-select-option-${v.name}`}
|
||||
key={i}
|
||||
onClick={onClick}
|
||||
style={{ marginLeft: i === 0 ? 28 : undefined }}
|
||||
className={clsx(styles.viewOption, v.className)}
|
||||
>
|
||||
{v.icon}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div
|
||||
style={{
|
||||
maxWidth: '150px',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
>
|
||||
{collection.name}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{actions.map((v, i) => {
|
||||
const onClick = (e: MouseEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation();
|
||||
v.click();
|
||||
};
|
||||
return (
|
||||
<div
|
||||
data-testid={`collection-select-option-${v.name}`}
|
||||
key={i}
|
||||
onClick={onClick}
|
||||
style={{ marginLeft: i === 0 ? 28 : undefined }}
|
||||
className={clsx(styles.viewOption, v.className)}
|
||||
>
|
||||
{v.icon}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</MenuItem>
|
||||
);
|
||||
};
|
||||
@@ -155,7 +169,6 @@ export const CollectionList = ({
|
||||
[styles.filterButtonCollapse]: !open,
|
||||
})}
|
||||
style={{
|
||||
marginLeft: 4,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
@@ -199,7 +212,12 @@ export const CollectionList = ({
|
||||
hoverColor="var(--affine-icon-color)"
|
||||
data-testid="collection-select"
|
||||
>
|
||||
{setting.currentCollection.name}
|
||||
<Tooltip
|
||||
content={setting.currentCollection.name}
|
||||
pointerEnterDelay={1500}
|
||||
>
|
||||
<>{setting.currentCollection.name}</>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
</Menu>
|
||||
)}
|
||||
|
||||
@@ -22,7 +22,7 @@ export const Popper = ({
|
||||
defaultVisible = false,
|
||||
visible: propsVisible,
|
||||
trigger = 'hover',
|
||||
pointerEnterDelay = 100,
|
||||
pointerEnterDelay = 500,
|
||||
pointerLeaveDelay = 100,
|
||||
onVisibleChange,
|
||||
popoverStyle,
|
||||
|
||||
@@ -15,6 +15,7 @@ const StyledTooltip = styled(StyledPopperContainer)(() => {
|
||||
fontSize: 'var(--affine-font-sm)',
|
||||
borderRadius: '8px',
|
||||
marginBottom: '12px',
|
||||
overflowWrap: 'break-word',
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"devDependencies": {
|
||||
"@types/debug": "^4.1.8"
|
||||
},
|
||||
"version": "0.7.0-canary.49"
|
||||
"version": "0.7.0-canary.51"
|
||||
}
|
||||
|
||||
2
packages/env/package.json
vendored
2
packages/env/package.json
vendored
@@ -27,5 +27,5 @@
|
||||
"dependencies": {
|
||||
"lit": "^2.7.6"
|
||||
},
|
||||
"version": "0.7.0-canary.49"
|
||||
"version": "0.7.0-canary.51"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@affine/graphql",
|
||||
"version": "0.7.0-canary.49",
|
||||
"version": "0.7.0-canary.51",
|
||||
"description": "Autogenerated GraphQL client for affine.pro",
|
||||
"license": "MPL-2.0",
|
||||
"type": "module",
|
||||
|
||||
@@ -25,5 +25,5 @@
|
||||
"@blocksuite/lit": "*",
|
||||
"@blocksuite/store": "*"
|
||||
},
|
||||
"version": "0.7.0-canary.49"
|
||||
"version": "0.7.0-canary.51"
|
||||
}
|
||||
|
||||
@@ -37,5 +37,5 @@
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"version": "0.7.0-canary.49"
|
||||
"version": "0.7.0-canary.51"
|
||||
}
|
||||
|
||||
@@ -46,5 +46,5 @@
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"version": "0.7.0-canary.49"
|
||||
"version": "0.7.0-canary.51"
|
||||
}
|
||||
|
||||
@@ -23,5 +23,5 @@
|
||||
"@blocksuite/store": "*",
|
||||
"lottie-web": "*"
|
||||
},
|
||||
"version": "0.7.0-canary.49"
|
||||
"version": "0.7.0-canary.51"
|
||||
}
|
||||
|
||||
@@ -38,5 +38,5 @@
|
||||
"test": "cross-env TS_NODE_TRANSPILE_ONLY=1 TS_NODE_PROJECT=./tsconfig.json node --test --loader ts-node/esm --experimental-specifier-resolution=node ./__tests__/**/*.mts",
|
||||
"version": "napi version"
|
||||
},
|
||||
"version": "0.7.0-canary.49"
|
||||
"version": "0.7.0-canary.51"
|
||||
}
|
||||
|
||||
@@ -54,5 +54,5 @@
|
||||
"react": "*",
|
||||
"react-dom": "*"
|
||||
},
|
||||
"version": "0.7.0-canary.49"
|
||||
"version": "0.7.0-canary.51"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@affine/storage",
|
||||
"version": "0.7.0-canary.49",
|
||||
"version": "0.7.0-canary.51",
|
||||
"engines": {
|
||||
"node": ">= 10.16.0 < 11 || >= 11.8.0"
|
||||
},
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"./*.md": "./*.md",
|
||||
"./preloading.json": "./preloading.json"
|
||||
},
|
||||
"version": "0.7.0-canary.49"
|
||||
"version": "0.7.0-canary.51"
|
||||
}
|
||||
|
||||
@@ -35,5 +35,5 @@
|
||||
"@types/ws": "^8.5.5",
|
||||
"ws": "^8.13.0"
|
||||
},
|
||||
"version": "0.7.0-canary.49"
|
||||
"version": "0.7.0-canary.51"
|
||||
}
|
||||
|
||||
@@ -81,9 +81,30 @@ const rootWorkspacesMetadataPromiseAtom = atom<
|
||||
// fixme(himself65): we might not need step 1
|
||||
// step 1: try load metadata from localStorage
|
||||
{
|
||||
const loadFromLocalStorage = (): RootWorkspaceMetadata[] => {
|
||||
// don't change this key,
|
||||
// otherwise it will cause the data loss in the production
|
||||
const primitiveMetadata = localStorage.getItem(METADATA_STORAGE_KEY);
|
||||
if (primitiveMetadata) {
|
||||
try {
|
||||
const items = JSON.parse(primitiveMetadata) as z.infer<
|
||||
typeof rootWorkspaceMetadataArraySchema
|
||||
>;
|
||||
rootWorkspaceMetadataArraySchema.parse(items);
|
||||
return [...items];
|
||||
} catch (e) {
|
||||
console.error('cannot parse worksapce', e);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
const maybeMetadata = loadFromLocalStorage();
|
||||
|
||||
// migration step, only data in `METADATA_STORAGE_KEY` will be migrated
|
||||
if (
|
||||
metadata.some(meta => !('version' in meta)) &&
|
||||
maybeMetadata.some(meta => !('version' in meta)) &&
|
||||
!globalThis.$migrationDone
|
||||
) {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
@@ -94,20 +115,7 @@ const rootWorkspacesMetadataPromiseAtom = atom<
|
||||
});
|
||||
}
|
||||
|
||||
// don't change this key,
|
||||
// otherwise it will cause the data loss in the production
|
||||
const primitiveMetadata = localStorage.getItem(METADATA_STORAGE_KEY);
|
||||
if (primitiveMetadata) {
|
||||
try {
|
||||
const items = JSON.parse(primitiveMetadata) as z.infer<
|
||||
typeof rootWorkspaceMetadataArraySchema
|
||||
>;
|
||||
rootWorkspaceMetadataArraySchema.parse(items);
|
||||
metadata.push(...items);
|
||||
} catch (e) {
|
||||
console.error('cannot parse worksapce', e);
|
||||
}
|
||||
}
|
||||
metadata.push(...loadFromLocalStorage());
|
||||
}
|
||||
// step 2: fetch from adapters
|
||||
{
|
||||
|
||||
19
packages/workspace/src/providers/datasource-doc-adapter.ts
Normal file
19
packages/workspace/src/providers/datasource-doc-adapter.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export interface DatasourceDocAdapter {
|
||||
// request diff update from other clients
|
||||
queryDocState: (
|
||||
guid: string,
|
||||
options?: {
|
||||
stateVector?: Uint8Array;
|
||||
targetClientId?: number;
|
||||
}
|
||||
) => Promise<Uint8Array | false>;
|
||||
|
||||
// send update to the datasource
|
||||
sendDocUpdate: (guid: string, update: Uint8Array) => Promise<void>;
|
||||
|
||||
// listen to update from the datasource. Returns a function to unsubscribe.
|
||||
// this is optional because some datasource might not support it
|
||||
onDocUpdate?(
|
||||
callback: (guid: string, update: Uint8Array) => void
|
||||
): () => void;
|
||||
}
|
||||
148
packages/workspace/src/providers/lazy-provider.ts
Normal file
148
packages/workspace/src/providers/lazy-provider.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import type { PassiveDocProvider } from '@blocksuite/store';
|
||||
import {
|
||||
applyUpdate,
|
||||
type Doc,
|
||||
encodeStateAsUpdate,
|
||||
encodeStateVectorFromUpdate,
|
||||
} from 'yjs';
|
||||
|
||||
import type { DatasourceDocAdapter } from './datasource-doc-adapter';
|
||||
|
||||
const selfUpdateOrigin = 'lazy-provider-self-origin';
|
||||
|
||||
function getDoc(doc: Doc, guid: string): Doc | undefined {
|
||||
if (doc.guid === guid) {
|
||||
return doc;
|
||||
}
|
||||
for (const subdoc of doc.subdocs) {
|
||||
const found = getDoc(subdoc, guid);
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a lazy provider that connects to a datasource and synchronizes a root document.
|
||||
*/
|
||||
export const createLazyProvider = (
|
||||
rootDoc: Doc,
|
||||
datasource: DatasourceDocAdapter
|
||||
): Omit<PassiveDocProvider, 'flavour'> => {
|
||||
let connected = false;
|
||||
const pendingMap = new Map<string, Uint8Array[]>(); // guid -> pending-updates
|
||||
const disposableMap = new Map<string, Set<() => void>>();
|
||||
let datasourceUnsub: (() => void) | undefined;
|
||||
|
||||
async function syncDoc(doc: Doc) {
|
||||
const guid = doc.guid;
|
||||
// perf: optimize me
|
||||
const currentUpdate = encodeStateAsUpdate(doc);
|
||||
|
||||
const remoteUpdate = await datasource.queryDocState(guid, {
|
||||
stateVector: encodeStateVectorFromUpdate(currentUpdate),
|
||||
});
|
||||
|
||||
const updates = [currentUpdate];
|
||||
pendingMap.set(guid, []);
|
||||
|
||||
if (remoteUpdate) {
|
||||
applyUpdate(doc, remoteUpdate, selfUpdateOrigin);
|
||||
const newUpdate = encodeStateAsUpdate(
|
||||
doc,
|
||||
encodeStateVectorFromUpdate(remoteUpdate)
|
||||
);
|
||||
updates.push(newUpdate);
|
||||
await datasource.sendDocUpdate(guid, newUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
function setupDocListener(doc: Doc) {
|
||||
const disposables = new Set<() => void>();
|
||||
disposableMap.set(doc.guid, disposables);
|
||||
const updateHandler = async (update: Uint8Array, origin: unknown) => {
|
||||
if (origin === selfUpdateOrigin) {
|
||||
return;
|
||||
}
|
||||
datasource.sendDocUpdate(doc.guid, update).catch(console.error);
|
||||
};
|
||||
|
||||
const subdocLoadHandler = (event: { loaded: Set<Doc> }) => {
|
||||
event.loaded.forEach(subdoc => {
|
||||
connectDoc(subdoc).catch(console.error);
|
||||
});
|
||||
};
|
||||
|
||||
doc.on('update', updateHandler);
|
||||
doc.on('subdocs', subdocLoadHandler);
|
||||
// todo: handle destroy?
|
||||
disposables.add(() => {
|
||||
doc.off('update', updateHandler);
|
||||
doc.off('subdocs', subdocLoadHandler);
|
||||
});
|
||||
}
|
||||
|
||||
function setupDatasourceListeners() {
|
||||
datasourceUnsub = datasource.onDocUpdate?.((guid, update) => {
|
||||
const doc = getDoc(rootDoc, guid);
|
||||
if (doc) {
|
||||
applyUpdate(doc, update);
|
||||
//
|
||||
if (pendingMap.has(guid)) {
|
||||
pendingMap.get(guid)?.forEach(update => applyUpdate(doc, update));
|
||||
pendingMap.delete(guid);
|
||||
}
|
||||
} else {
|
||||
// This case happens when the father doc is not yet updated,
|
||||
// so that the child doc is not yet created.
|
||||
// We need to put it into cache so that it can be applied later.
|
||||
console.warn('idb: doc not found', guid);
|
||||
pendingMap.set(guid, (pendingMap.get(guid) ?? []).concat(update));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// when a subdoc is loaded, we need to sync it with the datasource and setup listeners
|
||||
async function connectDoc(doc: Doc) {
|
||||
setupDocListener(doc);
|
||||
await syncDoc(doc);
|
||||
await Promise.all(
|
||||
[...doc.subdocs]
|
||||
.filter(subdoc => subdoc.shouldLoad)
|
||||
.map(subdoc => connectDoc(subdoc))
|
||||
);
|
||||
}
|
||||
|
||||
function disposeAll() {
|
||||
disposableMap.forEach(disposables => {
|
||||
disposables.forEach(dispose => dispose());
|
||||
});
|
||||
disposableMap.clear();
|
||||
}
|
||||
|
||||
function connect() {
|
||||
connected = true;
|
||||
|
||||
// root doc should be already loaded,
|
||||
// but we want to populate the cache for later update events
|
||||
connectDoc(rootDoc).catch(console.error);
|
||||
setupDatasourceListeners();
|
||||
}
|
||||
|
||||
async function disconnect() {
|
||||
connected = false;
|
||||
disposeAll();
|
||||
datasourceUnsub?.();
|
||||
datasourceUnsub = undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
get connected() {
|
||||
return connected;
|
||||
},
|
||||
passive: true,
|
||||
connect,
|
||||
disconnect,
|
||||
};
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@toeverything/y-indexeddb",
|
||||
"type": "module",
|
||||
"version": "0.7.0-canary.49",
|
||||
"version": "0.7.0-canary.51",
|
||||
"description": "IndexedDB database adapter for Yjs",
|
||||
"repository": "toeverything/AFFiNE",
|
||||
"author": "toeverything",
|
||||
|
||||
8
packages/y-provider/README.md
Normal file
8
packages/y-provider/README.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# A set of provider utilities for Yjs
|
||||
|
||||
## createLazyProvider
|
||||
|
||||
A factory function to create a lazy provider. It will not download the document from the provider until the first time a document is loaded at the parent doc.
|
||||
|
||||
To use it, first define a `DatasourceDocAdapter`.
|
||||
Then, create a `LazyProvider` with `createLazyProvider(rootDoc, datasource)`.
|
||||
17
packages/y-provider/package.json
Normal file
17
packages/y-provider/package.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "@affine/y-provider",
|
||||
"type": "module",
|
||||
"version": "0.7.0-canary.51",
|
||||
"description": "Yjs provider utilities for AFFiNE",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"main": "./src/index.ts",
|
||||
"module": "./src/index.ts",
|
||||
"devDependencies": {
|
||||
"@blocksuite/store": "0.0.0-20230719163314-76d863fc-nightly"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"yjs": "^13.5.51"
|
||||
}
|
||||
}
|
||||
181
packages/y-provider/src/__tests__/index.spec.ts
Normal file
181
packages/y-provider/src/__tests__/index.spec.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
import { setTimeout } from 'node:timers/promises';
|
||||
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { applyUpdate, Doc, encodeStateAsUpdate } from 'yjs';
|
||||
|
||||
import { createLazyProvider } from '../lazy-provider';
|
||||
import type { DatasourceDocAdapter } from '../types';
|
||||
import { getDoc } from '../utils';
|
||||
|
||||
const createMemoryDatasource = (rootDoc: Doc) => {
|
||||
const selfUpdateOrigin = Symbol('self-origin');
|
||||
const listeners = new Set<(guid: string, update: Uint8Array) => void>();
|
||||
|
||||
function trackDoc(doc: Doc) {
|
||||
doc.on('update', (update, origin) => {
|
||||
if (origin === selfUpdateOrigin) {
|
||||
return;
|
||||
}
|
||||
for (const listener of listeners) {
|
||||
listener(doc.guid, update);
|
||||
}
|
||||
});
|
||||
|
||||
doc.on('subdocs', () => {
|
||||
for (const subdoc of rootDoc.subdocs) {
|
||||
trackDoc(subdoc);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
trackDoc(rootDoc);
|
||||
|
||||
const adapter = {
|
||||
queryDocState: async (guid, options) => {
|
||||
const subdoc = getDoc(rootDoc, guid);
|
||||
if (!subdoc) {
|
||||
return false;
|
||||
}
|
||||
return encodeStateAsUpdate(subdoc, options?.stateVector);
|
||||
},
|
||||
sendDocUpdate: async (guid, update) => {
|
||||
const subdoc = getDoc(rootDoc, guid);
|
||||
if (!subdoc) {
|
||||
return;
|
||||
}
|
||||
applyUpdate(subdoc, update, selfUpdateOrigin);
|
||||
},
|
||||
onDocUpdate: callback => {
|
||||
listeners.add(callback);
|
||||
return () => {
|
||||
listeners.delete(callback);
|
||||
};
|
||||
},
|
||||
} satisfies DatasourceDocAdapter;
|
||||
return {
|
||||
rootDoc, // expose rootDoc for testing
|
||||
...adapter,
|
||||
};
|
||||
};
|
||||
|
||||
describe('y-provider', () => {
|
||||
test('should sync a subdoc if it is loaded after connect', async () => {
|
||||
const remoteRootDoc = new Doc(); // this is the remote doc lives in remote
|
||||
const datasource = createMemoryDatasource(remoteRootDoc);
|
||||
|
||||
const remotesubdoc = new Doc();
|
||||
remotesubdoc.getText('text').insert(0, 'test-subdoc-value');
|
||||
// populate remote doc with simple data
|
||||
remoteRootDoc.getMap('map').set('test-0', 'test-0-value');
|
||||
remoteRootDoc.getMap('map').set('subdoc', remotesubdoc);
|
||||
|
||||
const rootDoc = new Doc({ guid: remoteRootDoc.guid }); // this is the doc that we want to sync
|
||||
const provider = createLazyProvider(rootDoc, datasource);
|
||||
|
||||
provider.connect();
|
||||
|
||||
await setTimeout(); // wait for the provider to sync
|
||||
|
||||
const subdoc = rootDoc.getMap('map').get('subdoc') as Doc;
|
||||
|
||||
expect(rootDoc.getMap('map').get('test-0')).toBe('test-0-value');
|
||||
expect(subdoc.getText('text').toJSON()).toBe('');
|
||||
|
||||
// onload, the provider should sync the subdoc
|
||||
subdoc.load();
|
||||
await setTimeout();
|
||||
expect(subdoc.getText('text').toJSON()).toBe('test-subdoc-value');
|
||||
|
||||
remotesubdoc.getText('text').insert(0, 'prefix-');
|
||||
await setTimeout();
|
||||
expect(subdoc.getText('text').toJSON()).toBe('prefix-test-subdoc-value');
|
||||
});
|
||||
|
||||
test('should sync a shouldLoad=true subdoc on connect', async () => {
|
||||
const remoteRootDoc = new Doc(); // this is the remote doc lives in remote
|
||||
const datasource = createMemoryDatasource(remoteRootDoc);
|
||||
|
||||
const remotesubdoc = new Doc();
|
||||
remotesubdoc.getText('text').insert(0, 'test-subdoc-value');
|
||||
|
||||
// populate remote doc with simple data
|
||||
remoteRootDoc.getMap('map').set('test-0', 'test-0-value');
|
||||
remoteRootDoc.getMap('map').set('subdoc', remotesubdoc);
|
||||
|
||||
const rootDoc = new Doc({ guid: remoteRootDoc.guid }); // this is the doc that we want to sync
|
||||
applyUpdate(rootDoc, encodeStateAsUpdate(remoteRootDoc)); // sync rootDoc with remoteRootDoc
|
||||
|
||||
const subdoc = rootDoc.getMap('map').get('subdoc') as Doc;
|
||||
expect(subdoc.getText('text').toJSON()).toBe('');
|
||||
|
||||
subdoc.load();
|
||||
const provider = createLazyProvider(rootDoc, datasource);
|
||||
|
||||
provider.connect();
|
||||
await setTimeout(); // wait for the provider to sync
|
||||
expect(subdoc.getText('text').toJSON()).toBe('test-subdoc-value');
|
||||
});
|
||||
|
||||
test('should send existing local update to remote on connect', async () => {
|
||||
const remoteRootDoc = new Doc(); // this is the remote doc lives in remote
|
||||
const datasource = createMemoryDatasource(remoteRootDoc);
|
||||
|
||||
const rootDoc = new Doc({ guid: remoteRootDoc.guid }); // this is the doc that we want to sync
|
||||
applyUpdate(rootDoc, encodeStateAsUpdate(remoteRootDoc)); // sync rootDoc with remoteRootDoc
|
||||
|
||||
rootDoc.getText('text').insert(0, 'test-value');
|
||||
const provider = createLazyProvider(rootDoc, datasource);
|
||||
provider.connect();
|
||||
await setTimeout(); // wait for the provider to sync
|
||||
|
||||
expect(remoteRootDoc.getText('text').toJSON()).toBe('test-value');
|
||||
});
|
||||
|
||||
test('should send local update to remote for subdoc after connect', async () => {
|
||||
const remoteRootDoc = new Doc(); // this is the remote doc lives in remote
|
||||
const datasource = createMemoryDatasource(remoteRootDoc);
|
||||
|
||||
const rootDoc = new Doc({ guid: remoteRootDoc.guid }); // this is the doc that we want to sync
|
||||
const provider = createLazyProvider(rootDoc, datasource);
|
||||
|
||||
provider.connect();
|
||||
|
||||
await setTimeout(); // wait for the provider to sync
|
||||
|
||||
const subdoc = new Doc();
|
||||
rootDoc.getMap('map').set('subdoc', subdoc);
|
||||
subdoc.getText('text').insert(0, 'test-subdoc-value');
|
||||
|
||||
await setTimeout(); // wait for the provider to sync
|
||||
|
||||
const remoteSubdoc = remoteRootDoc.getMap('map').get('subdoc') as Doc;
|
||||
expect(remoteSubdoc.getText('text').toJSON()).toBe('test-subdoc-value');
|
||||
});
|
||||
|
||||
test('should not send local update to remote for subdoc after disconnect', async () => {
|
||||
const remoteRootDoc = new Doc(); // this is the remote doc lives in remote
|
||||
const datasource = createMemoryDatasource(remoteRootDoc);
|
||||
|
||||
const rootDoc = new Doc({ guid: remoteRootDoc.guid }); // this is the doc that we want to sync
|
||||
const provider = createLazyProvider(rootDoc, datasource);
|
||||
|
||||
provider.connect();
|
||||
|
||||
await setTimeout(); // wait for the provider to sync
|
||||
|
||||
const subdoc = new Doc();
|
||||
rootDoc.getMap('map').set('subdoc', subdoc);
|
||||
|
||||
await setTimeout(); // wait for the provider to sync
|
||||
|
||||
const remoteSubdoc = remoteRootDoc.getMap('map').get('subdoc') as Doc;
|
||||
expect(remoteSubdoc.getText('text').toJSON()).toBe('');
|
||||
|
||||
provider.disconnect();
|
||||
subdoc.getText('text').insert(0, 'test-subdoc-value');
|
||||
setTimeout();
|
||||
expect(remoteSubdoc.getText('text').toJSON()).toBe('');
|
||||
|
||||
expect(provider.connected).toBe(false);
|
||||
});
|
||||
});
|
||||
2
packages/y-provider/src/index.ts
Normal file
2
packages/y-provider/src/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './lazy-provider';
|
||||
export * from './types';
|
||||
182
packages/y-provider/src/lazy-provider.ts
Normal file
182
packages/y-provider/src/lazy-provider.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
import type { PassiveDocProvider } from '@blocksuite/store';
|
||||
import {
|
||||
applyUpdate,
|
||||
type Doc,
|
||||
encodeStateAsUpdate,
|
||||
encodeStateVectorFromUpdate,
|
||||
} from 'yjs';
|
||||
|
||||
import type { DatasourceDocAdapter } from './types';
|
||||
|
||||
const selfUpdateOrigin = 'lazy-provider-self-origin';
|
||||
|
||||
function getDoc(doc: Doc, guid: string): Doc | undefined {
|
||||
if (doc.guid === guid) {
|
||||
return doc;
|
||||
}
|
||||
for (const subdoc of doc.subdocs) {
|
||||
const found = getDoc(subdoc, guid);
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a lazy provider that connects to a datasource and synchronizes a root document.
|
||||
*/
|
||||
export const createLazyProvider = (
|
||||
rootDoc: Doc,
|
||||
datasource: DatasourceDocAdapter
|
||||
): Omit<PassiveDocProvider, 'flavour'> => {
|
||||
let connected = false;
|
||||
const pendingMap = new Map<string, Uint8Array[]>(); // guid -> pending-updates
|
||||
const disposableMap = new Map<string, Set<() => void>>();
|
||||
const connectedDocs = new Set();
|
||||
let datasourceUnsub: (() => void) | undefined;
|
||||
|
||||
async function syncDoc(doc: Doc) {
|
||||
const guid = doc.guid;
|
||||
// perf: optimize me
|
||||
const currentUpdate = encodeStateAsUpdate(doc);
|
||||
|
||||
const remoteUpdate = await datasource.queryDocState(guid, {
|
||||
stateVector: encodeStateVectorFromUpdate(currentUpdate),
|
||||
});
|
||||
|
||||
const updates = [currentUpdate];
|
||||
pendingMap.set(guid, []);
|
||||
|
||||
if (remoteUpdate) {
|
||||
applyUpdate(doc, remoteUpdate, selfUpdateOrigin);
|
||||
const newUpdate = encodeStateAsUpdate(
|
||||
doc,
|
||||
encodeStateVectorFromUpdate(remoteUpdate)
|
||||
);
|
||||
updates.push(newUpdate);
|
||||
await datasource.sendDocUpdate(guid, newUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up event listeners for a Yjs document.
|
||||
* @param doc - The Yjs document to set up listeners for.
|
||||
*/
|
||||
function setupDocListener(doc: Doc) {
|
||||
const disposables = new Set<() => void>();
|
||||
disposableMap.set(doc.guid, disposables);
|
||||
const updateHandler = async (update: Uint8Array, origin: unknown) => {
|
||||
if (origin === selfUpdateOrigin) {
|
||||
return;
|
||||
}
|
||||
datasource.sendDocUpdate(doc.guid, update).catch(console.error);
|
||||
};
|
||||
|
||||
const subdocLoadHandler = (event: {
|
||||
loaded: Set<Doc>;
|
||||
removed: Set<Doc>;
|
||||
}) => {
|
||||
event.loaded.forEach(subdoc => {
|
||||
connectDoc(subdoc).catch(console.error);
|
||||
});
|
||||
event.removed.forEach(subdoc => {
|
||||
disposeDoc(subdoc);
|
||||
});
|
||||
};
|
||||
|
||||
doc.on('update', updateHandler);
|
||||
doc.on('subdocs', subdocLoadHandler);
|
||||
// todo: handle destroy?
|
||||
disposables.add(() => {
|
||||
doc.off('update', updateHandler);
|
||||
doc.off('subdocs', subdocLoadHandler);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up event listeners for the datasource.
|
||||
* Specifically, listens for updates to documents and applies them to the corresponding Yjs document.
|
||||
*/
|
||||
function setupDatasourceListeners() {
|
||||
datasourceUnsub = datasource.onDocUpdate?.((guid, update) => {
|
||||
const doc = getDoc(rootDoc, guid);
|
||||
if (doc) {
|
||||
applyUpdate(doc, update);
|
||||
//
|
||||
if (pendingMap.has(guid)) {
|
||||
pendingMap.get(guid)?.forEach(update => applyUpdate(doc, update));
|
||||
pendingMap.delete(guid);
|
||||
}
|
||||
} else {
|
||||
// This case happens when the father doc is not yet updated,
|
||||
// so that the child doc is not yet created.
|
||||
// We need to put it into cache so that it can be applied later.
|
||||
console.warn('idb: doc not found', guid);
|
||||
pendingMap.set(guid, (pendingMap.get(guid) ?? []).concat(update));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// when a subdoc is loaded, we need to sync it with the datasource and setup listeners
|
||||
async function connectDoc(doc: Doc) {
|
||||
// skip if already connected
|
||||
if (connectedDocs.has(doc.guid)) {
|
||||
return;
|
||||
}
|
||||
connectedDocs.add(doc.guid);
|
||||
setupDocListener(doc);
|
||||
await syncDoc(doc);
|
||||
await Promise.all(
|
||||
[...doc.subdocs]
|
||||
.filter(subdoc => subdoc.shouldLoad)
|
||||
.map(subdoc => connectDoc(subdoc))
|
||||
);
|
||||
}
|
||||
|
||||
function disposeDoc(doc: Doc) {
|
||||
connectedDocs.delete(doc.guid);
|
||||
const disposables = disposableMap.get(doc.guid);
|
||||
if (disposables) {
|
||||
disposables.forEach(dispose => dispose());
|
||||
disposableMap.delete(doc.guid);
|
||||
}
|
||||
// also dispose all subdocs
|
||||
doc.subdocs.forEach(disposeDoc);
|
||||
}
|
||||
|
||||
function disposeAll() {
|
||||
disposableMap.forEach(disposables => {
|
||||
disposables.forEach(dispose => dispose());
|
||||
});
|
||||
disposableMap.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects to the datasource and sets up event listeners for document updates.
|
||||
*/
|
||||
function connect() {
|
||||
connected = true;
|
||||
|
||||
// root doc should be already loaded,
|
||||
// but we want to populate the cache for later update events
|
||||
connectDoc(rootDoc).catch(console.error);
|
||||
setupDatasourceListeners();
|
||||
}
|
||||
|
||||
async function disconnect() {
|
||||
connected = false;
|
||||
disposeAll();
|
||||
datasourceUnsub?.();
|
||||
datasourceUnsub = undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
get connected() {
|
||||
return connected;
|
||||
},
|
||||
passive: true,
|
||||
connect,
|
||||
disconnect,
|
||||
};
|
||||
};
|
||||
19
packages/y-provider/src/types.ts
Normal file
19
packages/y-provider/src/types.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export interface DatasourceDocAdapter {
|
||||
// request diff update from other clients
|
||||
queryDocState: (
|
||||
guid: string,
|
||||
options?: {
|
||||
stateVector?: Uint8Array;
|
||||
targetClientId?: number;
|
||||
}
|
||||
) => Promise<Uint8Array | false>;
|
||||
|
||||
// send update to the datasource
|
||||
sendDocUpdate: (guid: string, update: Uint8Array) => Promise<void>;
|
||||
|
||||
// listen to update from the datasource. Returns a function to unsubscribe.
|
||||
// this is optional because some datasource might not support it
|
||||
onDocUpdate?(
|
||||
callback: (guid: string, update: Uint8Array) => void
|
||||
): () => void;
|
||||
}
|
||||
14
packages/y-provider/src/utils.ts
Normal file
14
packages/y-provider/src/utils.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { Doc } from 'yjs';
|
||||
|
||||
export function getDoc(doc: Doc, guid: string): Doc | undefined {
|
||||
if (doc.guid === guid) {
|
||||
return doc;
|
||||
}
|
||||
for (const subdoc of doc.subdocs) {
|
||||
const found = getDoc(subdoc, guid);
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
9
packages/y-provider/tsconfig.json
Normal file
9
packages/y-provider/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["./src"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"noEmit": false,
|
||||
"outDir": "lib"
|
||||
}
|
||||
}
|
||||
@@ -15,5 +15,5 @@
|
||||
"@toeverything/plugin-infra": "workspace:*",
|
||||
"link-preview-js": "^3.0.4"
|
||||
},
|
||||
"version": "0.7.0-canary.49"
|
||||
"version": "0.7.0-canary.51"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@affine/bookmark-plugin",
|
||||
"version": "0.7.0-canary.49",
|
||||
"version": "0.7.0-canary.51",
|
||||
"affinePlugin": {
|
||||
"release": true,
|
||||
"entry": {
|
||||
|
||||
@@ -27,5 +27,5 @@
|
||||
"react": "*",
|
||||
"react-dom": "*"
|
||||
},
|
||||
"version": "0.7.0-canary.49"
|
||||
"version": "0.7.0-canary.51"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@affine/hello-world-plugin",
|
||||
"private": true,
|
||||
"version": "0.7.0-canary.49",
|
||||
"version": "0.7.0-canary.51",
|
||||
"affinePlugin": {
|
||||
"release": false,
|
||||
"entry": {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IconButton, Tooltip } from '@affine/component';
|
||||
import { AffineLogoSBlue2_1Icon } from '@blocksuite/icons';
|
||||
import { Logo1Icon } from '@blocksuite/icons';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export const HeaderItem = () => {
|
||||
@@ -10,7 +10,7 @@ export const HeaderItem = () => {
|
||||
console.log('clicked hello world!');
|
||||
}, [])}
|
||||
>
|
||||
<AffineLogoSBlue2_1Icon />
|
||||
<Logo1Icon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
@@ -84,8 +84,6 @@ test('init page', async ({ page, context }) => {
|
||||
await switchToNext();
|
||||
await page.waitForTimeout(1000);
|
||||
await page.goto('http://localhost:8081/');
|
||||
await page.waitForTimeout(1000);
|
||||
await page.goto('http://localhost:8081/');
|
||||
await page.waitForSelector('v-line', {
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
@@ -21,5 +21,5 @@
|
||||
"serve": "^14.2.0",
|
||||
"vitest": "^0.33.0"
|
||||
},
|
||||
"version": "0.7.0-canary.49"
|
||||
"version": "0.7.0-canary.51"
|
||||
}
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
"@affine-test/kit": "workspace:*",
|
||||
"@playwright/test": "^1.35.1"
|
||||
},
|
||||
"version": "0.7.0-canary.49"
|
||||
"version": "0.7.0-canary.51"
|
||||
}
|
||||
|
||||
2
tests/fixtures/package.json
vendored
2
tests/fixtures/package.json
vendored
@@ -3,5 +3,5 @@
|
||||
"exports": {
|
||||
"./*": "./*"
|
||||
},
|
||||
"version": "0.7.0-canary.49"
|
||||
"version": "0.7.0-canary.51"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@affine-test/kit",
|
||||
"private": true,
|
||||
"version": "0.7.0-canary.49",
|
||||
"version": "0.7.0-canary.51",
|
||||
"exports": {
|
||||
"./playwright": "./playwright.ts",
|
||||
"./utils/*": "./utils/*.ts"
|
||||
|
||||
@@ -117,9 +117,15 @@
|
||||
{
|
||||
"path": "./plugins/bookmark-block"
|
||||
},
|
||||
{
|
||||
"path": "./plugins/bookmark"
|
||||
},
|
||||
{
|
||||
"path": "./plugins/copilot"
|
||||
},
|
||||
{
|
||||
"path": "./plugins/hello-world"
|
||||
},
|
||||
// Packages
|
||||
{
|
||||
"path": "./packages/cli"
|
||||
|
||||
10
yarn.lock
10
yarn.lock
@@ -636,6 +636,16 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@affine/y-provider@workspace:packages/y-provider":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@affine/y-provider@workspace:packages/y-provider"
|
||||
dependencies:
|
||||
"@blocksuite/store": 0.0.0-20230719163314-76d863fc-nightly
|
||||
peerDependencies:
|
||||
yjs: ^13.5.51
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@alloc/quick-lru@npm:^5.2.0":
|
||||
version: 5.2.0
|
||||
resolution: "@alloc/quick-lru@npm:5.2.0"
|
||||
|
||||
Reference in New Issue
Block a user