mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-27 02:42:25 +08:00
Merge branch 'develop' into feature/fix-backspace
This commit is contained in:
107
libs/components/account/src/login/fs.tsx
Normal file
107
libs/components/account/src/login/fs.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
/* eslint-disable filename-rules/match */
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { LogoImg } from '@toeverything/components/common';
|
||||
import {
|
||||
MuiButton,
|
||||
MuiBox,
|
||||
MuiGrid,
|
||||
MuiSnackbar,
|
||||
} from '@toeverything/components/ui';
|
||||
import { services } from '@toeverything/datasource/db-service';
|
||||
import { useLocalTrigger } from '@toeverything/datasource/state';
|
||||
|
||||
import { Error } from './../error';
|
||||
|
||||
const requestPermission = async (workspace: string) => {
|
||||
indexedDB.deleteDatabase(workspace);
|
||||
const dirHandler = await window.showDirectoryPicker({
|
||||
id: 'AFFiNE_' + workspace,
|
||||
mode: 'readwrite',
|
||||
startIn: 'documents',
|
||||
});
|
||||
const fileHandle = await dirHandler.getFileHandle('affine.db', {
|
||||
create: true,
|
||||
});
|
||||
const file = await fileHandle.getFile();
|
||||
const initialData = new Uint8Array(await file.arrayBuffer());
|
||||
|
||||
const exporter = async (contents: Uint8Array) => {
|
||||
try {
|
||||
const writable = await fileHandle.createWritable();
|
||||
await writable.write(contents);
|
||||
await writable.close();
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
};
|
||||
|
||||
await services.api.editorBlock.setupDataExporter(
|
||||
workspace,
|
||||
new Uint8Array(initialData),
|
||||
exporter
|
||||
);
|
||||
};
|
||||
|
||||
export const FileSystem = () => {
|
||||
const onSelected = useLocalTrigger();
|
||||
const [error, setError] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (process.env['NX_E2E']) {
|
||||
onSelected();
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<MuiGrid container>
|
||||
<MuiSnackbar
|
||||
anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||||
open={error}
|
||||
message="Login failed, please check if you have permission"
|
||||
/>
|
||||
<MuiGrid item xs={8}>
|
||||
<Error
|
||||
title="Welcome to AFFiNE"
|
||||
subTitle="blocks of knowledge to power your team"
|
||||
action1Text="Login or Register"
|
||||
/>
|
||||
</MuiGrid>
|
||||
|
||||
<MuiGrid item xs={4}>
|
||||
<MuiBox
|
||||
onClick={async () => {
|
||||
try {
|
||||
await requestPermission('AFFiNE');
|
||||
onSelected();
|
||||
} catch (e) {
|
||||
setError(true);
|
||||
onSelected();
|
||||
setTimeout(() => setError(false), 3000);
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
width: '300px',
|
||||
margin: '300px auto 20px auto',
|
||||
}}
|
||||
sx={{ mt: 1 }}
|
||||
>
|
||||
<LogoImg
|
||||
style={{
|
||||
width: '100px',
|
||||
}}
|
||||
/>
|
||||
|
||||
<MuiButton
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
style={{ textTransform: 'none' }}
|
||||
>
|
||||
Sync to Disk
|
||||
</MuiButton>
|
||||
</MuiBox>
|
||||
</MuiGrid>
|
||||
</MuiGrid>
|
||||
);
|
||||
};
|
||||
@@ -1,11 +1,13 @@
|
||||
/* eslint-disable filename-rules/match */
|
||||
// import { Authing } from './authing';
|
||||
import { Firebase } from './firebase';
|
||||
import { FileSystem } from './fs';
|
||||
|
||||
export function Login() {
|
||||
return (
|
||||
<>
|
||||
{/* <Authing /> */}
|
||||
<Firebase />
|
||||
{process.env['NX_LOCAL'] ? <FileSystem /> : <Firebase />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
/* eslint-disable max-lines */
|
||||
import * as React from 'react';
|
||||
import {
|
||||
memo,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
useMemo,
|
||||
useState,
|
||||
type RefObject,
|
||||
} from 'react';
|
||||
import { Renderer } from '@tldraw/core';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
import {
|
||||
@@ -132,13 +140,13 @@ export function Tldraw({
|
||||
getSession,
|
||||
tools,
|
||||
}: TldrawProps) {
|
||||
const [sId, set_sid] = React.useState(id);
|
||||
const [sId, setSid] = useState(id);
|
||||
const { pageClientWidth } = usePageClientWidth();
|
||||
// page padding left and right total 300px
|
||||
const editorShapeInitSize = pageClientWidth - 300;
|
||||
|
||||
// Create a new app when the component mounts.
|
||||
const [app, setApp] = React.useState(() => {
|
||||
const [app, setApp] = useState(() => {
|
||||
const app = new TldrawApp({
|
||||
id,
|
||||
callbacks,
|
||||
@@ -151,7 +159,7 @@ export function Tldraw({
|
||||
});
|
||||
|
||||
// Create a new app if the `id` prop changes.
|
||||
React.useLayoutEffect(() => {
|
||||
useLayoutEffect(() => {
|
||||
if (id === sId) return;
|
||||
const newApp = new TldrawApp({
|
||||
id,
|
||||
@@ -161,14 +169,14 @@ export function Tldraw({
|
||||
tools,
|
||||
});
|
||||
|
||||
set_sid(id);
|
||||
setSid(id);
|
||||
|
||||
setApp(newApp);
|
||||
}, [sId, id]);
|
||||
|
||||
// Update the document if the `document` prop changes but the ids,
|
||||
// are the same, or else load a new document if the ids are different.
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (!document) return;
|
||||
|
||||
if (document.id === app.document.id) {
|
||||
@@ -179,34 +187,34 @@ export function Tldraw({
|
||||
}, [document, app]);
|
||||
|
||||
// Disable assets when the `disableAssets` prop changes.
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
app.setDisableAssets(disableAssets);
|
||||
}, [app, disableAssets]);
|
||||
|
||||
// Change the page when the `currentPageId` prop changes.
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (!currentPageId) return;
|
||||
app.changePage(currentPageId);
|
||||
}, [currentPageId, app]);
|
||||
|
||||
// Toggle the app's readOnly mode when the `readOnly` prop changes.
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
app.readOnly = readOnly;
|
||||
}, [app, readOnly]);
|
||||
|
||||
// Toggle the app's darkMode when the `darkMode` prop changes.
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (darkMode !== app.settings.isDarkMode) {
|
||||
app.toggleDarkMode();
|
||||
}
|
||||
}, [app, darkMode]);
|
||||
|
||||
// Update the app's callbacks when any callback changes.
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
app.callbacks = callbacks || {};
|
||||
}, [app, callbacks]);
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
useLayoutEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
if (!window.document?.fonts) return;
|
||||
|
||||
@@ -260,7 +268,7 @@ interface InnerTldrawProps {
|
||||
showSponsorLink?: boolean;
|
||||
}
|
||||
|
||||
const InnerTldraw = React.memo(function InnerTldraw({
|
||||
const InnerTldraw = memo(function InnerTldraw({
|
||||
id,
|
||||
autofocus,
|
||||
showPages,
|
||||
@@ -276,7 +284,7 @@ const InnerTldraw = React.memo(function InnerTldraw({
|
||||
}: InnerTldrawProps) {
|
||||
const app = useTldrawApp();
|
||||
|
||||
const rWrapper = React.useRef<HTMLDivElement>(null);
|
||||
const rWrapper = useRef<HTMLDivElement>(null);
|
||||
|
||||
const state = app.useStore();
|
||||
|
||||
@@ -299,7 +307,7 @@ const InnerTldraw = React.memo(function InnerTldraw({
|
||||
TLDR.get_shape_util(page.shapes[selectedIds[0]].type).hideResizeHandles;
|
||||
|
||||
// Custom rendering meta, with dark mode for shapes
|
||||
const meta: TDMeta = React.useMemo(() => {
|
||||
const meta: TDMeta = useMemo(() => {
|
||||
return { isDarkMode: settings.isDarkMode, app };
|
||||
}, [settings.isDarkMode, app]);
|
||||
|
||||
@@ -308,7 +316,7 @@ const InnerTldraw = React.memo(function InnerTldraw({
|
||||
: appState.selectByContain;
|
||||
|
||||
// Custom theme, based on darkmode
|
||||
const theme = React.useMemo(() => {
|
||||
const theme = useMemo(() => {
|
||||
const { selectByContain } = appState;
|
||||
const { isDarkMode, isCadSelectMode } = settings;
|
||||
|
||||
@@ -373,9 +381,11 @@ const InnerTldraw = React.memo(function InnerTldraw({
|
||||
!isSelecting ||
|
||||
!settings.showCloneHandles ||
|
||||
pageState.camera.zoom < 0.2;
|
||||
|
||||
return (
|
||||
<StyledLayout
|
||||
ref={rWrapper}
|
||||
panning={settings.forcePanning}
|
||||
tabIndex={-0}
|
||||
penColor={app?.appState?.currentStyle?.stroke}
|
||||
>
|
||||
@@ -477,17 +487,17 @@ const InnerTldraw = React.memo(function InnerTldraw({
|
||||
);
|
||||
});
|
||||
|
||||
const OneOff = React.memo(function OneOff({
|
||||
const OneOff = memo(function OneOff({
|
||||
focusableRef,
|
||||
autofocus,
|
||||
}: {
|
||||
autofocus?: boolean;
|
||||
focusableRef: React.RefObject<HTMLDivElement>;
|
||||
focusableRef: RefObject<HTMLDivElement>;
|
||||
}) {
|
||||
useKeyboardShortcuts(focusableRef);
|
||||
useStylesheet();
|
||||
|
||||
React.useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (autofocus) {
|
||||
focusableRef.current?.focus();
|
||||
}
|
||||
@@ -496,8 +506,8 @@ const OneOff = React.memo(function OneOff({
|
||||
return null;
|
||||
});
|
||||
|
||||
const StyledLayout = styled('div')<{ penColor: string }>(
|
||||
({ theme, penColor }) => {
|
||||
const StyledLayout = styled('div')<{ penColor: string; panning: boolean }>(
|
||||
({ theme, panning, penColor }) => {
|
||||
return {
|
||||
position: 'relative',
|
||||
height: '100%',
|
||||
@@ -509,6 +519,7 @@ const StyledLayout = styled('div')<{ penColor: string }>(
|
||||
overflow: 'hidden',
|
||||
boxSizing: 'border-box',
|
||||
outline: 'none',
|
||||
cursor: panning ? 'grab' : 'unset',
|
||||
|
||||
'& .tl-container': {
|
||||
position: 'absolute',
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
Tooltip,
|
||||
PopoverContainer,
|
||||
IconButton,
|
||||
useTheme,
|
||||
} from '@toeverything/components/ui';
|
||||
import {
|
||||
FrameIcon,
|
||||
@@ -71,6 +72,7 @@ export const ToolsPanel: FC<{ app: TldrawApp }> = ({ app }) => {
|
||||
const activeTool = app.useStore(activeToolSelector);
|
||||
|
||||
const isToolLocked = app.useStore(toolLockedSelector);
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<PopoverContainer
|
||||
@@ -105,7 +107,8 @@ export const ToolsPanel: FC<{ app: TldrawApp }> = ({ app }) => {
|
||||
style={{
|
||||
color:
|
||||
activeTool === type
|
||||
? 'blue'
|
||||
? theme.affine.palette
|
||||
.primary
|
||||
: '',
|
||||
}}
|
||||
onClick={() => {
|
||||
|
||||
@@ -219,8 +219,6 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||
|
||||
isPointing = false;
|
||||
|
||||
isForcePanning = false;
|
||||
|
||||
editingStartTime = -1;
|
||||
|
||||
fileSystemHandle: FileSystemHandle | null = null;
|
||||
@@ -262,7 +260,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||
|
||||
constructor(props: TldrawAppCtorProps) {
|
||||
super(
|
||||
TldrawApp.default_state,
|
||||
TldrawApp.defaultState,
|
||||
props.id,
|
||||
TldrawApp.version,
|
||||
(prev, next, prevVersion) => {
|
||||
@@ -326,9 +324,9 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||
);
|
||||
|
||||
this.patchState({
|
||||
...TldrawApp.default_state,
|
||||
...TldrawApp.defaultState,
|
||||
appState: {
|
||||
...TldrawApp.default_state.appState,
|
||||
...TldrawApp.defaultState.appState,
|
||||
status: TDStatus.Idle,
|
||||
},
|
||||
});
|
||||
@@ -1473,13 +1471,13 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||
|
||||
this.replace_state(
|
||||
{
|
||||
...TldrawApp.default_state,
|
||||
...TldrawApp.defaultState,
|
||||
settings: {
|
||||
...this.state.settings,
|
||||
},
|
||||
document: migrate(document, TldrawApp.version),
|
||||
appState: {
|
||||
...TldrawApp.default_state.appState,
|
||||
...TldrawApp.defaultState.appState,
|
||||
...this.state.appState,
|
||||
currentPageId: Object.keys(document.pages)[0],
|
||||
disableAssets: this.disableAssets,
|
||||
@@ -3913,7 +3911,11 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||
break;
|
||||
}
|
||||
case ' ': {
|
||||
this.isForcePanning = true;
|
||||
this.patchState({
|
||||
settings: {
|
||||
forcePanning: true,
|
||||
},
|
||||
});
|
||||
this.spaceKey = true;
|
||||
break;
|
||||
}
|
||||
@@ -3976,7 +3978,12 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||
break;
|
||||
}
|
||||
case ' ': {
|
||||
this.isForcePanning = false;
|
||||
this.patchState({
|
||||
settings: {
|
||||
forcePanning:
|
||||
this.currentTool.type === TDShapeType.HandDraw,
|
||||
},
|
||||
});
|
||||
this.spaceKey = false;
|
||||
break;
|
||||
}
|
||||
@@ -4069,13 +4076,18 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||
this.pan(delta);
|
||||
|
||||
// When panning, we also want to call onPointerMove, except when "force panning" via spacebar / middle wheel button (it's called elsewhere in that case)
|
||||
if (!this.isForcePanning)
|
||||
if (!this.useStore.getState().settings.forcePanning)
|
||||
this.onPointerMove(info, e as unknown as React.PointerEvent);
|
||||
};
|
||||
|
||||
onZoom: TLWheelEventHandler = (info, e) => {
|
||||
if (this.state.appState.status !== TDStatus.Idle) return;
|
||||
const delta = info.delta[2] / 50;
|
||||
// Normalize zoom scroll
|
||||
// Fix https://github.com/toeverything/AFFiNE/issues/135
|
||||
const delta =
|
||||
Math.abs(info.delta[2]) > 10
|
||||
? 0.2 * Math.sign(info.delta[2])
|
||||
: info.delta[2] / 50;
|
||||
this.zoomBy(delta, info.point);
|
||||
this.onPointerMove(info, e as unknown as React.PointerEvent);
|
||||
};
|
||||
@@ -4093,7 +4105,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||
onPointerMove: TLPointerEventHandler = (info, e) => {
|
||||
this.previousPoint = this.currentPoint;
|
||||
this.updateInputs(info, e);
|
||||
if (this.isForcePanning && this.isPointing) {
|
||||
if (this.useStore.getState().settings.forcePanning && this.isPointing) {
|
||||
this.onPan?.(
|
||||
{ ...info, delta: Vec.neg(info.delta) },
|
||||
e as unknown as WheelEvent
|
||||
@@ -4117,20 +4129,23 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||
|
||||
onPointerDown: TLPointerEventHandler = (info, e) => {
|
||||
if (e.buttons === 4) {
|
||||
this.isForcePanning = true;
|
||||
this.patchState({
|
||||
settings: {
|
||||
forcePanning: true,
|
||||
},
|
||||
});
|
||||
} else if (this.isPointing) {
|
||||
return;
|
||||
}
|
||||
this.isPointing = true;
|
||||
this.originPoint = this.getPagePoint(info.point).concat(info.pressure);
|
||||
this.updateInputs(info, e);
|
||||
if (this.isForcePanning) return;
|
||||
if (this.useStore.getState().settings.forcePanning) return;
|
||||
this.currentTool.onPointerDown?.(info, e);
|
||||
};
|
||||
|
||||
onPointerUp: TLPointerEventHandler = (info, e) => {
|
||||
this.isPointing = false;
|
||||
if (!this.shiftKey) this.isForcePanning = false;
|
||||
this.updateInputs(info, e);
|
||||
this.currentTool.onPointerUp?.(info, e);
|
||||
};
|
||||
@@ -4517,7 +4532,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||
assets: {},
|
||||
};
|
||||
|
||||
static default_state: TDSnapshot = {
|
||||
static defaultState: TDSnapshot = {
|
||||
settings: {
|
||||
isCadSelectMode: false,
|
||||
isPenMode: false,
|
||||
@@ -4527,6 +4542,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
|
||||
isSnapping: false,
|
||||
isDebugMode: false,
|
||||
isReadonlyMode: false,
|
||||
forcePanning: false,
|
||||
keepStyleMenuOpen: false,
|
||||
nudgeDistanceLarge: 16,
|
||||
nudgeDistanceSmall: 1,
|
||||
|
||||
@@ -18,34 +18,19 @@ export class HandDrawTool extends BaseTool {
|
||||
|
||||
/* ----------------- Event Handlers ----------------- */
|
||||
|
||||
override onPointerDown: TLPointerEventHandler = () => {
|
||||
if (this.app.readOnly) return;
|
||||
if (this.status !== Status.Idle) return;
|
||||
|
||||
this.set_status(Status.Pointing);
|
||||
override onEnter = () => {
|
||||
this.app.patchState({
|
||||
settings: {
|
||||
forcePanning: true,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
override onPointerMove: TLPointerEventHandler = (info, e) => {
|
||||
if (this.app.readOnly) return;
|
||||
const delta = Vec.div(info.delta, this.app.camera.zoom);
|
||||
const prev = this.app.camera.point;
|
||||
const next = Vec.sub(prev, delta);
|
||||
if (Vec.isEqual(next, prev)) return;
|
||||
|
||||
switch (this.status) {
|
||||
case Status.Pointing: {
|
||||
this.app.pan(Vec.neg(delta));
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
override onPointerUp: TLPointerEventHandler = () => {
|
||||
this.set_status(Status.Idle);
|
||||
};
|
||||
|
||||
override onCancel = () => {
|
||||
this.set_status(Status.Idle);
|
||||
override onExit = () => {
|
||||
this.app.patchState({
|
||||
settings: {
|
||||
forcePanning: false,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@ export interface TDSnapshot {
|
||||
isPenMode: boolean;
|
||||
isReadonlyMode: boolean;
|
||||
isZoomSnap: boolean;
|
||||
forcePanning: boolean;
|
||||
keepStyleMenuOpen: boolean;
|
||||
nudgeDistanceSmall: number;
|
||||
nudgeDistanceLarge: number;
|
||||
|
||||
@@ -13,6 +13,7 @@ const StyledContainer = styled('div')({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
cursor: 'pointer',
|
||||
paddingLeft: '12px',
|
||||
'&:hover': {
|
||||
background: '#f5f7f8',
|
||||
borderRadius: '5px',
|
||||
@@ -36,11 +37,6 @@ export function CollapsibleTitle(props: CollapsibleTitleProps) {
|
||||
return (
|
||||
<>
|
||||
<StyledContainer onClick={() => setOpen(prev => !prev)}>
|
||||
{open ? (
|
||||
<ArrowDropDownIcon sx={{ color: '#566B7D' }} />
|
||||
) : (
|
||||
<ArrowRightIcon sx={{ color: '#566B7D' }} />
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
color: '#98ACBD',
|
||||
|
||||
@@ -36,6 +36,9 @@ export class GridBlock extends BaseView {
|
||||
}
|
||||
return block.remove();
|
||||
}
|
||||
if (block.childrenIds.length === 0) {
|
||||
return block.remove();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import {
|
||||
addNewGroup,
|
||||
LINE_GAP,
|
||||
RecastScene,
|
||||
TAG_GAP,
|
||||
useCurrentView,
|
||||
useOnSelect,
|
||||
} from '@toeverything/components/editor-core';
|
||||
@@ -38,6 +40,7 @@ const GroupActionWrapper = styled('div')(({ theme }) => ({
|
||||
visibility: 'hidden',
|
||||
fontSize: theme.affine.typography.xs.fontSize,
|
||||
color: theme.affine.palette.icons,
|
||||
opacity: 0.6,
|
||||
'.line': {
|
||||
flex: 1,
|
||||
height: '15px',
|
||||
@@ -60,7 +63,7 @@ const GroupContainer = styled('div')<{ isSelect?: boolean }>(
|
||||
({ isSelect, theme }) => ({
|
||||
background: theme.affine.palette.white,
|
||||
border: '2px solid rgba(236,241,251,.5)',
|
||||
padding: `15px 16px 0 16px`,
|
||||
padding: `15px 16px ${LINE_GAP - TAG_GAP * 2}px 16px`,
|
||||
borderRadius: '10px',
|
||||
...(isSelect
|
||||
? {
|
||||
|
||||
@@ -60,6 +60,9 @@ export const CardContext = (props: Props) => {
|
||||
|
||||
const StyledCardContainer = styled('div')`
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
z-index: 1;
|
||||
}
|
||||
&:focus-within {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@@ -109,12 +109,15 @@ export const PageView: FC<CreateView> = ({ block, editor }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const PageTitleBlock = styled('div')({
|
||||
'.title': {
|
||||
fontSize: Theme.typography.page.fontSize,
|
||||
lineHeight: Theme.typography.page.lineHeight,
|
||||
},
|
||||
'.content': {
|
||||
outline: 'none',
|
||||
},
|
||||
const PageTitleBlock = styled('div')(({ theme }) => {
|
||||
return {
|
||||
'.title': {
|
||||
fontSize: theme.affine.typography.page.fontSize,
|
||||
lineHeight: theme.affine.typography.page.lineHeight,
|
||||
fontWeight: theme.affine.typography.page.fontWeight,
|
||||
},
|
||||
'.content': {
|
||||
outline: 'none',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -46,6 +46,7 @@ const TextBlock = styled(TextManage)<{ type: string }>(({ theme, type }) => {
|
||||
return {
|
||||
fontSize: textStyleMap.text.fontSize,
|
||||
lineHeight: textStyleMap.text.lineHeight,
|
||||
fontWeight: textStyleMap.text.fontWeight,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -150,6 +150,7 @@ const TodoBlock = styled('div')({
|
||||
display: 'flex',
|
||||
'.checkBoxContainer': {
|
||||
marginRight: '4px',
|
||||
padding: '0 4px',
|
||||
height: '22px',
|
||||
},
|
||||
'.textContainer': {
|
||||
|
||||
@@ -27,5 +27,6 @@ export const BlockContainer: FC<BlockContainerProps> = function ({
|
||||
export const Container = styled('div')<{ selected: boolean }>(
|
||||
({ selected, theme }) => ({
|
||||
backgroundColor: selected ? theme.affine.palette.textSelected : '',
|
||||
marginBottom: '2px',
|
||||
})
|
||||
);
|
||||
|
||||
@@ -39,6 +39,9 @@ export type ExtendedTextUtils = SlateUtils & {
|
||||
};
|
||||
const TextBlockContainer = styled(Text)(({ theme }) => ({
|
||||
lineHeight: theme.affine.typography.body1.lineHeight,
|
||||
fontFamily: theme.affine.typography.body1.fontFamily,
|
||||
color: theme.affine.typography.body1.color,
|
||||
letterSpacing: '0.1px',
|
||||
}));
|
||||
|
||||
const findSlice = (arr: string[], p: string, q: string) => {
|
||||
|
||||
@@ -102,6 +102,12 @@ export const RenderRoot: FC<PropsWithChildren<RenderRootProps>> = ({
|
||||
editor.getHooks().onRootNodeMouseLeave(event);
|
||||
};
|
||||
|
||||
const onContextmenu = (
|
||||
event: React.MouseEvent<HTMLDivElement, MouseEvent>
|
||||
) => {
|
||||
selectionRef.current?.onContextmenu(event);
|
||||
};
|
||||
|
||||
const onKeyDown: React.KeyboardEventHandler<HTMLDivElement> = event => {
|
||||
// IMP move into keyboard managers?
|
||||
editor.getHooks().onRootNodeKeyDown(event);
|
||||
@@ -165,6 +171,7 @@ export const RenderRoot: FC<PropsWithChildren<RenderRootProps>> = ({
|
||||
onMouseUp={onMouseUp}
|
||||
onMouseLeave={onMouseLeave}
|
||||
onMouseOut={onMouseOut}
|
||||
onContextMenu={onContextmenu}
|
||||
onKeyDown={onKeyDown}
|
||||
onKeyDownCapture={onKeyDownCapture}
|
||||
onKeyUp={onKeyUp}
|
||||
|
||||
@@ -29,6 +29,9 @@ export type SelectionRef = {
|
||||
onMouseDown: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
||||
onMouseMove: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
||||
onMouseUp: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
||||
onContextmenu: (
|
||||
event: React.MouseEvent<HTMLDivElement, MouseEvent>
|
||||
) => void;
|
||||
};
|
||||
|
||||
const getFixedPoint = (
|
||||
@@ -207,10 +210,17 @@ export const SelectionRect = forwardRef<SelectionRef, SelectionProps>(
|
||||
scrollManager.stopAutoScroll();
|
||||
};
|
||||
|
||||
const onContextmenu = () => {
|
||||
if (mouseType.current === 'down') {
|
||||
onMouseUp();
|
||||
}
|
||||
};
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
onMouseDown,
|
||||
onMouseMove,
|
||||
onMouseUp,
|
||||
onContextmenu,
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -3,6 +3,8 @@ import { styled } from '@toeverything/components/ui';
|
||||
import type { AsyncBlock } from '../editor';
|
||||
import { PendantPopover } from './pendant-popover';
|
||||
import { PendantRender } from './pendant-render';
|
||||
import { useRef } from 'react';
|
||||
import { getRecastItemValue, useRecastBlockMeta } from '../recast-block';
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
@@ -14,13 +16,27 @@ export const BlockPendantProvider: FC<PropsWithChildren<BlockTagProps>> = ({
|
||||
block,
|
||||
children,
|
||||
}) => {
|
||||
const triggerRef = useRef<HTMLDivElement>();
|
||||
const { getProperties } = useRecastBlockMeta();
|
||||
const properties = getProperties();
|
||||
const { getValue } = getRecastItemValue(block);
|
||||
const showTriggerLine =
|
||||
properties.filter(property => getValue(property.id)).length === 0;
|
||||
|
||||
return (
|
||||
<Container>
|
||||
{children}
|
||||
|
||||
<PendantPopover block={block}>
|
||||
<StyledTriggerLine />
|
||||
</PendantPopover>
|
||||
{showTriggerLine ? (
|
||||
<StyledPendantContainer ref={triggerRef}>
|
||||
<PendantPopover
|
||||
block={block}
|
||||
container={triggerRef.current}
|
||||
>
|
||||
<StyledTriggerLine />
|
||||
</PendantPopover>
|
||||
</StyledPendantContainer>
|
||||
) : null}
|
||||
|
||||
<PendantRender block={block} />
|
||||
</Container>
|
||||
@@ -28,7 +44,7 @@ export const BlockPendantProvider: FC<PropsWithChildren<BlockTagProps>> = ({
|
||||
};
|
||||
|
||||
export const LINE_GAP = 16;
|
||||
const TAG_GAP = 4;
|
||||
export const TAG_GAP = 4;
|
||||
|
||||
const StyledTriggerLine = styled('div')({
|
||||
padding: `${TAG_GAP}px 0`,
|
||||
@@ -43,10 +59,12 @@ const StyledTriggerLine = styled('div')({
|
||||
width: '100%',
|
||||
height: '2px',
|
||||
background: '#dadada',
|
||||
display: 'none',
|
||||
display: 'flex',
|
||||
position: 'absolute',
|
||||
left: '0',
|
||||
top: '4px',
|
||||
transition: 'opacity .2s',
|
||||
opacity: '0',
|
||||
},
|
||||
'::after': {
|
||||
content: "''",
|
||||
@@ -60,18 +78,24 @@ const StyledTriggerLine = styled('div')({
|
||||
transition: 'width .3s',
|
||||
},
|
||||
});
|
||||
|
||||
const Container = styled('div')({
|
||||
position: 'relative',
|
||||
paddingBottom: `${LINE_GAP - TAG_GAP * 2}px`,
|
||||
const StyledPendantContainer = styled('div')({
|
||||
width: '100px',
|
||||
'&:hover': {
|
||||
[StyledTriggerLine.toString()]: {
|
||||
'&::before': {
|
||||
display: 'flex',
|
||||
},
|
||||
[`${StyledTriggerLine}`]: {
|
||||
'&::after': {
|
||||
width: '100%',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const Container = styled('div')({
|
||||
position: 'relative',
|
||||
padding: `${TAG_GAP * 2}px 0 ${LINE_GAP - TAG_GAP * 4}px 0`,
|
||||
'&:hover': {
|
||||
[`${StyledTriggerLine}`]: {
|
||||
'&::before': {
|
||||
opacity: '1',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -29,6 +29,7 @@ export const PendantHistoryPanel = ({
|
||||
|
||||
const [history, setHistory] = useState<RecastBlockValue[]>([]);
|
||||
const popoverHandlerRef = useRef<{ [key: string]: PopperHandler }>({});
|
||||
const historyPanelRef = useRef<HTMLDivElement>();
|
||||
const { getValueHistory } = getRecastItemValue(block);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -84,7 +85,7 @@ export const PendantHistoryPanel = ({
|
||||
}, [block, getProperties, groupBlock, recastBlock]);
|
||||
|
||||
return (
|
||||
<StyledPendantHistoryPanel>
|
||||
<StyledPendantHistoryPanel ref={historyPanelRef}>
|
||||
{history.map(item => {
|
||||
const property = getProperty(item.id);
|
||||
return (
|
||||
@@ -116,6 +117,7 @@ export const PendantHistoryPanel = ({
|
||||
/>
|
||||
}
|
||||
trigger="click"
|
||||
container={historyPanelRef.current}
|
||||
>
|
||||
<PendantTag
|
||||
style={{
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Input, Option, Select, Tooltip } from '@toeverything/components/ui';
|
||||
import {
|
||||
Input,
|
||||
message,
|
||||
Option,
|
||||
Select,
|
||||
Tooltip,
|
||||
} from '@toeverything/components/ui';
|
||||
import { HelpCenterIcon } from '@toeverything/components/icons';
|
||||
import { AsyncBlock } from '../../editor';
|
||||
|
||||
@@ -18,6 +24,7 @@ import {
|
||||
generateRandomFieldName,
|
||||
generateInitialOptions,
|
||||
getPendantConfigByType,
|
||||
checkPendantForm,
|
||||
} from '../utils';
|
||||
import { useOnCreateSure } from './hooks';
|
||||
|
||||
@@ -74,7 +81,7 @@ export const CreatePendantPanel = ({
|
||||
setFieldName(e.target.value);
|
||||
}}
|
||||
endAdornment={
|
||||
<Tooltip content="Help info here">
|
||||
<Tooltip content="Help info here" placement="top">
|
||||
<StyledInputEndAdornment>
|
||||
<HelpCenterIcon />
|
||||
</StyledInputEndAdornment>
|
||||
@@ -98,6 +105,17 @@ export const CreatePendantPanel = ({
|
||||
)}
|
||||
iconConfig={getPendantConfigByType(selectedOption.type)}
|
||||
onSure={async (type, newPropertyItem, newValue) => {
|
||||
const checkResult = checkPendantForm(
|
||||
type,
|
||||
fieldName,
|
||||
newPropertyItem,
|
||||
newValue
|
||||
);
|
||||
|
||||
if (!checkResult.passed) {
|
||||
await message.error(checkResult.message);
|
||||
return;
|
||||
}
|
||||
await onCreateSure({
|
||||
type,
|
||||
newPropertyItem,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { Input, Tooltip } from '@toeverything/components/ui';
|
||||
import { Input, message, Tooltip } from '@toeverything/components/ui';
|
||||
import { HelpCenterIcon } from '@toeverything/components/icons';
|
||||
import { PendantModifyPanel } from '../pendant-modify-panel';
|
||||
import type { AsyncBlock } from '../../editor';
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
type RecastBlockValue,
|
||||
type RecastMetaProperty,
|
||||
} from '../../recast-block';
|
||||
import { getPendantConfigByType } from '../utils';
|
||||
import { checkPendantForm, getPendantConfigByType } from '../utils';
|
||||
import {
|
||||
StyledPopoverWrapper,
|
||||
StyledOperationLabel,
|
||||
@@ -70,7 +70,7 @@ export const UpdatePendantPanel = ({
|
||||
setFieldName(e.target.value);
|
||||
}}
|
||||
endAdornment={
|
||||
<Tooltip content="Help info here">
|
||||
<Tooltip content="Help info here" placement="top">
|
||||
<StyledInputEndAdornment>
|
||||
<HelpCenterIcon />
|
||||
</StyledInputEndAdornment>
|
||||
@@ -98,6 +98,17 @@ export const UpdatePendantPanel = ({
|
||||
property={property}
|
||||
type={property.type}
|
||||
onSure={async (type, newPropertyItem, newValue) => {
|
||||
const checkResult = checkPendantForm(
|
||||
type,
|
||||
fieldName,
|
||||
newPropertyItem,
|
||||
newValue
|
||||
);
|
||||
|
||||
if (!checkResult.passed) {
|
||||
await message.error(checkResult.message);
|
||||
return;
|
||||
}
|
||||
await onUpdateSure({
|
||||
type,
|
||||
newPropertyItem,
|
||||
|
||||
@@ -23,12 +23,7 @@ import {
|
||||
PendantTypes,
|
||||
type TempInformationType,
|
||||
} from '../types';
|
||||
import {
|
||||
checkPendantForm,
|
||||
getOfficialSelected,
|
||||
getPendantConfigByType,
|
||||
} from '../utils';
|
||||
import { message } from '@toeverything/components/ui';
|
||||
import { getOfficialSelected, getPendantConfigByType } from '../utils';
|
||||
|
||||
type SelectPropertyType = MultiSelectProperty | SelectProperty;
|
||||
type SureParams = {
|
||||
@@ -56,18 +51,6 @@ export const useOnCreateSure = ({ block }: { block: AsyncBlock }) => {
|
||||
newPropertyItem,
|
||||
newValue,
|
||||
}: SureParams) => {
|
||||
const checkResult = checkPendantForm(
|
||||
type,
|
||||
fieldName,
|
||||
newPropertyItem,
|
||||
newValue
|
||||
);
|
||||
|
||||
if (!checkResult.passed) {
|
||||
await message.error(checkResult.message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
type === PendantTypes.MultiSelect ||
|
||||
type === PendantTypes.Select ||
|
||||
@@ -181,18 +164,6 @@ export const useOnUpdateSure = ({
|
||||
newPropertyItem,
|
||||
newValue,
|
||||
}: SureParams) => {
|
||||
const checkResult = checkPendantForm(
|
||||
type,
|
||||
fieldName,
|
||||
newPropertyItem,
|
||||
newValue
|
||||
);
|
||||
|
||||
if (!checkResult.passed) {
|
||||
await message.error(checkResult.message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
type === PendantTypes.MultiSelect ||
|
||||
type === PendantTypes.Select ||
|
||||
|
||||
@@ -26,6 +26,7 @@ export const PendantPopover: FC<
|
||||
block={block}
|
||||
endElement={
|
||||
<AddPendantPopover
|
||||
container={popoverProps.container}
|
||||
block={block}
|
||||
onSure={() => {
|
||||
popoverHandlerRef.current?.setVisible(false);
|
||||
|
||||
@@ -105,6 +105,8 @@ export const PendantRender = ({ block }: { block: AsyncBlock }) => {
|
||||
<AddPendantPopover
|
||||
block={block}
|
||||
iconStyle={{ marginTop: 4 }}
|
||||
trigger="click"
|
||||
// trigger={isKanbanView ? 'hover' : 'click'}
|
||||
container={blockRenderContainerRef.current}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -162,7 +162,7 @@ export class BlockCommands {
|
||||
public async moveInNewGridItem(
|
||||
blockId: string,
|
||||
gridItemId: string,
|
||||
isBefore = false
|
||||
type = GridDropType.left
|
||||
) {
|
||||
const block = await this._editor.getBlockById(blockId);
|
||||
if (block) {
|
||||
@@ -175,7 +175,7 @@ export class BlockCommands {
|
||||
await block.remove();
|
||||
await gridItemBlock.append(block);
|
||||
if (targetGridItemBlock && gridItemBlock) {
|
||||
if (isBefore) {
|
||||
if (type === GridDropType.left) {
|
||||
await targetGridItemBlock.before(gridItemBlock);
|
||||
} else {
|
||||
await targetGridItemBlock.after(gridItemBlock);
|
||||
|
||||
@@ -95,6 +95,9 @@ export class DragDropManager {
|
||||
}
|
||||
|
||||
private async _handleDropBlock(event: React.DragEvent<Element>) {
|
||||
const targetBlock = await this._editor.getBlockById(
|
||||
this._blockDragTargetId
|
||||
);
|
||||
if (this._blockDragDirection !== BlockDropPlacement.none) {
|
||||
const blockId = event.dataTransfer.getData(this._blockIdKey);
|
||||
if (!(await this._canBeDrop(event))) return;
|
||||
@@ -109,13 +112,24 @@ export class DragDropManager {
|
||||
this._blockDragDirection
|
||||
)
|
||||
) {
|
||||
await this._editor.commands.blockCommands.createLayoutBlock(
|
||||
blockId,
|
||||
this._blockDragTargetId,
|
||||
const dropType =
|
||||
this._blockDragDirection === BlockDropPlacement.left
|
||||
? GridDropType.left
|
||||
: GridDropType.right
|
||||
);
|
||||
: GridDropType.right;
|
||||
// if target is a grid item create grid item
|
||||
if (targetBlock.type !== Protocol.Block.Type.gridItem) {
|
||||
await this._editor.commands.blockCommands.createLayoutBlock(
|
||||
blockId,
|
||||
this._blockDragTargetId,
|
||||
dropType
|
||||
);
|
||||
} else {
|
||||
await this._editor.commands.blockCommands.moveInNewGridItem(
|
||||
blockId,
|
||||
this._blockDragTargetId,
|
||||
dropType
|
||||
);
|
||||
}
|
||||
}
|
||||
if (
|
||||
[
|
||||
@@ -123,9 +137,6 @@ export class DragDropManager {
|
||||
BlockDropPlacement.outerRight,
|
||||
].includes(this._blockDragDirection)
|
||||
) {
|
||||
const targetBlock = await this._editor.getBlockById(
|
||||
this._blockDragTargetId
|
||||
);
|
||||
if (targetBlock.type !== Protocol.Block.Type.grid) {
|
||||
await this._editor.commands.blockCommands.createLayoutBlock(
|
||||
blockId,
|
||||
@@ -154,7 +165,7 @@ export class DragDropManager {
|
||||
await this._editor.commands.blockCommands.moveInNewGridItem(
|
||||
blockId,
|
||||
gridItems[0].id,
|
||||
true
|
||||
GridDropType.right
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -347,10 +358,10 @@ export class DragDropManager {
|
||||
blockId: string
|
||||
) {
|
||||
const { clientX, clientY } = event;
|
||||
this._setBlockDragTargetId(blockId);
|
||||
const path = await this._editor.getBlockPath(blockId);
|
||||
const mousePoint = new Point(clientX, clientY);
|
||||
const rect = domToRect(blockDom);
|
||||
let targetBlock: AsyncBlock = path[path.length - 1];
|
||||
/**
|
||||
* IMP: compute the level of the target block
|
||||
* future feature drag drop has level support do not delete
|
||||
@@ -386,13 +397,30 @@ export class DragDropManager {
|
||||
const gridBlocks = path.filter(
|
||||
block => block.type === Protocol.Block.Type.grid
|
||||
);
|
||||
// limit grid block floor counts, when drag block to init grid
|
||||
if (gridBlocks.length >= MAX_GRID_BLOCK_FLOOR) {
|
||||
const parentBlock = path[path.length - 2];
|
||||
// a new grid should not be grid item`s child
|
||||
if (
|
||||
parentBlock &&
|
||||
parentBlock.type === Protocol.Block.Type.gridItem
|
||||
) {
|
||||
targetBlock = parentBlock;
|
||||
// gridItem`s parent must be grid block
|
||||
const gridItemCounts = (await path[path.length - 3].children())
|
||||
.length;
|
||||
if (
|
||||
gridItemCounts >=
|
||||
this._editor.configManager.grid.maxGridItemCount
|
||||
) {
|
||||
direction = BlockDropPlacement.none;
|
||||
}
|
||||
// limit grid block floor counts, when drag block to init grid
|
||||
} else if (gridBlocks.length >= MAX_GRID_BLOCK_FLOOR) {
|
||||
direction = BlockDropPlacement.none;
|
||||
}
|
||||
}
|
||||
this._setBlockDragTargetId(targetBlock.id);
|
||||
this._setBlockDragDirection(direction);
|
||||
return direction;
|
||||
return { direction, block: targetBlock };
|
||||
}
|
||||
|
||||
public handlerEditorDrop(event: React.DragEvent<Element>) {
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
menuItemsMap,
|
||||
} from './config';
|
||||
import { QueryResult } from '../../search';
|
||||
|
||||
export type CommandMenuProps = {
|
||||
editor: Virgo;
|
||||
hooks: PluginHooks;
|
||||
@@ -82,7 +81,6 @@ export const CommandMenu = ({ editor, hooks, style }: CommandMenuProps) => {
|
||||
const checkIfShowCommandMenu = useCallback(
|
||||
async (event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
const { type, anchorNode } = editor.selection.currentSelectInfo;
|
||||
// console.log(await editor.getBlockById(anchorNode.id));
|
||||
if (!anchorNode?.id) {
|
||||
return;
|
||||
}
|
||||
@@ -127,12 +125,12 @@ export const CommandMenu = ({ editor, hooks, style }: CommandMenuProps) => {
|
||||
|
||||
const COMMAND_MENU_HEIGHT =
|
||||
window.innerHeight * 0.4;
|
||||
const { top, left } =
|
||||
const { top, left, bottom } =
|
||||
editor.container.getBoundingClientRect();
|
||||
if (clientHeight - rectTop <= COMMAND_MENU_HEIGHT) {
|
||||
setCommandMenuPosition({
|
||||
left: rect.left - left,
|
||||
bottom: rectTop - top + 10,
|
||||
bottom: bottom - rect.bottom + 24,
|
||||
top: 'initial',
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -168,6 +168,12 @@ export const GroupMenu = function ({ editor, hooks }: GroupMenuProps) {
|
||||
|
||||
useEffect(() => {
|
||||
setShowMenu(false);
|
||||
|
||||
if (groupBlock) {
|
||||
const unobserve = groupBlock.onUpdate(() => setGroupBlock(null));
|
||||
return unobserve;
|
||||
}
|
||||
return undefined;
|
||||
}, [groupBlock]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
BlockDropPlacement,
|
||||
LINE_GAP,
|
||||
AsyncBlock,
|
||||
TAG_GAP,
|
||||
} from '@toeverything/framework/virgo';
|
||||
import { Button } from '@toeverything/components/common';
|
||||
import { styled } from '@toeverything/components/ui';
|
||||
@@ -78,13 +79,13 @@ function Line(props: { lineInfo: LineInfo; rootRect: DOMRect }) {
|
||||
};
|
||||
const bottomLineStyle = {
|
||||
...horizontalLineStyle,
|
||||
top: intersectionRect.bottom + 1 - rootRect.y - LINE_GAP,
|
||||
top: intersectionRect.bottom + 1 - rootRect.y - LINE_GAP + TAG_GAP,
|
||||
};
|
||||
|
||||
const verticalLineStyle = {
|
||||
...lineStyle,
|
||||
width: 2,
|
||||
height: intersectionRect.height - LINE_GAP,
|
||||
height: intersectionRect.height - LINE_GAP + TAG_GAP,
|
||||
top: intersectionRect.y - rootRect.y,
|
||||
};
|
||||
const leftLineStyle = {
|
||||
@@ -184,6 +185,14 @@ export const LeftMenuDraggable: FC<LeftMenuProps> = props => {
|
||||
return () => sub.unsubscribe();
|
||||
}, [blockInfo, editor]);
|
||||
|
||||
useEffect(() => {
|
||||
if (block?.block != null) {
|
||||
const unobserve = block.block.onUpdate(() => setBlock(undefined));
|
||||
return unobserve;
|
||||
}
|
||||
return undefined;
|
||||
}, [block?.block]);
|
||||
|
||||
useEffect(() => {
|
||||
const sub = lineInfo.subscribe(data => {
|
||||
if (data == null) {
|
||||
@@ -220,7 +229,7 @@ export const LeftMenuDraggable: FC<LeftMenuProps> = props => {
|
||||
MENU_WIDTH -
|
||||
MENU_BUTTON_OFFSET -
|
||||
rootRect.left,
|
||||
top: block.rect.top - rootRect.top,
|
||||
top: block.rect.top - rootRect.top + TAG_GAP * 2,
|
||||
opacity: visible ? 1 : 0,
|
||||
zIndex: 1,
|
||||
}}
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
import { PluginRenderRoot } from '../../utils';
|
||||
import { Subject, throttleTime } from 'rxjs';
|
||||
import { domToRect, last, Point } from '@toeverything/utils';
|
||||
const DRAG_THROTTLE_DELAY = 150;
|
||||
const DRAG_THROTTLE_DELAY = 60;
|
||||
export class LeftMenuPlugin extends BasePlugin {
|
||||
private _mousedown?: boolean;
|
||||
private _root?: PluginRenderRoot;
|
||||
@@ -105,16 +105,17 @@ export class LeftMenuPlugin extends BasePlugin {
|
||||
new Point(event.clientX, event.clientY)
|
||||
);
|
||||
if (block == null || ignoreBlockTypes.includes(block.type)) return;
|
||||
const direction = await this.editor.dragDropManager.checkBlockDragTypes(
|
||||
event,
|
||||
block.dom,
|
||||
block.id
|
||||
);
|
||||
const { direction, block: targetBlock } =
|
||||
await this.editor.dragDropManager.checkBlockDragTypes(
|
||||
event,
|
||||
block.dom,
|
||||
block.id
|
||||
);
|
||||
this._lineInfo.next({
|
||||
direction,
|
||||
blockInfo: {
|
||||
block,
|
||||
rect: block.dom.getBoundingClientRect(),
|
||||
block: targetBlock,
|
||||
rect: targetBlock.dom.getBoundingClientRect(),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -17,16 +17,20 @@ export const StatusIcon = ({ mode }: StatusIconProps) => {
|
||||
const IconWrapper = styled('div')<Pick<StatusIconProps, 'mode'>>(
|
||||
({ theme, mode }) => {
|
||||
return {
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
borderRadius: '5px',
|
||||
boxShadow: theme.affine.shadows.shadow1,
|
||||
color: theme.affine.palette.primary,
|
||||
cursor: 'pointer',
|
||||
backgroundColor: theme.affine.palette.white,
|
||||
transform: `translateX(${mode === DocMode.doc ? 0 : 20}px)`,
|
||||
transform: `translateX(${mode === DocMode.doc ? 0 : 30}px)`,
|
||||
transition: 'transform 300ms ease',
|
||||
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
|
||||
'& > svg': {
|
||||
fontSize: '20px',
|
||||
},
|
||||
|
||||
@@ -2,26 +2,37 @@ import { styled } from '@toeverything/components/ui';
|
||||
|
||||
type StatusTextProps = {
|
||||
children: string;
|
||||
width?: string;
|
||||
active?: boolean;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
export const StatusText = ({ children, active, onClick }: StatusTextProps) => {
|
||||
export const StatusText = ({
|
||||
children,
|
||||
width,
|
||||
active,
|
||||
onClick,
|
||||
}: StatusTextProps) => {
|
||||
return (
|
||||
<StyledText active={active} onClick={onClick}>
|
||||
<StyledText width={width} active={active} onClick={onClick}>
|
||||
{children}
|
||||
</StyledText>
|
||||
);
|
||||
};
|
||||
|
||||
const StyledText = styled('div')<StatusTextProps>(({ theme, active }) => {
|
||||
return {
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
color: theme.affine.palette.primary,
|
||||
fontWeight: active ? '500' : '300',
|
||||
fontSize: '15px',
|
||||
cursor: 'pointer',
|
||||
padding: '0 6px',
|
||||
};
|
||||
});
|
||||
const StyledText = styled('div')<StatusTextProps>(
|
||||
({ theme, width, active }) => {
|
||||
return {
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
color: active
|
||||
? theme.affine.palette.primary
|
||||
: 'rgba(62, 111, 219, 0.6)',
|
||||
fontWeight: active ? '600' : '400',
|
||||
fontSize: '16px',
|
||||
lineHeight: '22px',
|
||||
cursor: 'pointer',
|
||||
...(!!width && { width }),
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@@ -18,11 +18,14 @@ export const StatusTrack: FC<StatusTrackProps> = ({ mode, onClick }) => {
|
||||
|
||||
const Container = styled('div')(({ theme }) => {
|
||||
return {
|
||||
backgroundColor: theme.affine.palette.textHover,
|
||||
borderRadius: '5px',
|
||||
height: '30px',
|
||||
width: '50px',
|
||||
width: '64px',
|
||||
height: '32px',
|
||||
border: '1px solid #ECF1FB',
|
||||
borderRadius: '8px',
|
||||
cursor: 'pointer',
|
||||
padding: '5px',
|
||||
margin: '0 8px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '0 4px',
|
||||
};
|
||||
});
|
||||
|
||||
@@ -32,6 +32,7 @@ export const Switcher = () => {
|
||||
return (
|
||||
<StyledContainerForSwitcher>
|
||||
<StatusText
|
||||
width={'44px'}
|
||||
active={pageViewMode === DocMode.doc}
|
||||
onClick={() => switchToPageView(DocMode.doc)}
|
||||
>
|
||||
@@ -48,6 +49,7 @@ export const Switcher = () => {
|
||||
}}
|
||||
/>
|
||||
<StatusText
|
||||
width={'56px'}
|
||||
active={pageViewMode === DocMode.board}
|
||||
onClick={() => switchToPageView(DocMode.board)}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IconButton, styled } from '@toeverything/components/ui';
|
||||
import { IconButton, styled, MuiButton } from '@toeverything/components/ui';
|
||||
import {
|
||||
LogoIcon,
|
||||
SideBarViewIcon,
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
SideBarViewCloseIcon,
|
||||
} from '@toeverything/components/icons';
|
||||
import { useShowSettingsSidebar } from '@toeverything/datasource/state';
|
||||
|
||||
import { CurrentPageTitle } from './Title';
|
||||
import { EditorBoardSwitcher } from './EditorBoardSwitcher';
|
||||
|
||||
@@ -24,9 +25,14 @@ export const LayoutHeader = () => {
|
||||
</FlexContainer>
|
||||
<FlexContainer>
|
||||
<StyledHelper>
|
||||
<StyledShare>Share</StyledShare>
|
||||
<StyledShare disabled={true}>Share</StyledShare>
|
||||
<div style={{ margin: '0px 12px' }}>
|
||||
<IconButton size="large">
|
||||
<IconButton
|
||||
size="large"
|
||||
hoverColor={'transparent'}
|
||||
disabled={true}
|
||||
style={{ cursor: 'not-allowed' }}
|
||||
>
|
||||
<SearchIcon />
|
||||
</IconButton>
|
||||
</div>
|
||||
@@ -119,17 +125,19 @@ const StyledHelper = styled('div')({
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
const StyledShare = styled('div')({
|
||||
const StyledShare = styled('div')<{ disabled?: boolean }>({
|
||||
padding: '10px 12px',
|
||||
fontWeight: 600,
|
||||
fontSize: '14px',
|
||||
color: '#3E6FDB',
|
||||
cursor: 'pointer',
|
||||
|
||||
'&:hover': {
|
||||
background: '#F5F7F8',
|
||||
borderRadius: '5px',
|
||||
},
|
||||
cursor: 'not-allowed',
|
||||
color: '#98ACBD',
|
||||
textTransform: 'none',
|
||||
/* disabled for current time */
|
||||
// color: '#3E6FDB',
|
||||
// '&:hover': {
|
||||
// background: '#F5F7F8',
|
||||
// borderRadius: '5px',
|
||||
// },
|
||||
});
|
||||
|
||||
const StyledLogoIcon = styled(LogoIcon)(({ theme }) => {
|
||||
@@ -141,9 +149,7 @@ const StyledLogoIcon = styled(LogoIcon)(({ theme }) => {
|
||||
|
||||
const StyledContainerForEditorBoardSwitcher = styled('div')(({ theme }) => {
|
||||
return {
|
||||
width: '100%',
|
||||
position: 'absolute',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
left: '50%',
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { message } from '@toeverything/components/ui';
|
||||
import { useSettingFlags, type SettingFlags } from './use-setting-flags';
|
||||
import { copyToClipboard } from '@toeverything/utils';
|
||||
import {
|
||||
@@ -91,7 +92,10 @@ export const useSettings = (): SettingItem[] => {
|
||||
{
|
||||
type: 'button',
|
||||
name: 'Copy Page Link',
|
||||
onClick: () => copyToClipboard(window.location.href),
|
||||
onClick: () => {
|
||||
copyToClipboard(window.location.href);
|
||||
message.success('Page link copied successfully');
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'separator',
|
||||
|
||||
@@ -10,9 +10,10 @@ import {
|
||||
} from '@toeverything/components/ui';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { DotIcon } from '../dot-icon';
|
||||
|
||||
const StyledWrapper = styled('div')({
|
||||
paddingLeft: '12px',
|
||||
width: '100%',
|
||||
span: {
|
||||
textOverflow: 'ellipsis',
|
||||
overflow: 'hidden',
|
||||
@@ -22,8 +23,8 @@ const StyledWrapper = styled('div')({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingRight: '20px',
|
||||
whiteSpace: 'nowrap',
|
||||
paddingLeft: '12px',
|
||||
'&:hover': {
|
||||
background: '#f5f7f8',
|
||||
borderRadius: '5px',
|
||||
@@ -106,6 +107,8 @@ export const Activities = () => {
|
||||
const { id, title, updated } = item;
|
||||
return (
|
||||
<ListItem className="item" key={id}>
|
||||
<DotIcon />
|
||||
|
||||
<StyledItemContent
|
||||
onClick={() => {
|
||||
navigate(`/${currentSpaceId}/${id}`);
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { PageInPageTreeIcon } from '@toeverything/components/icons';
|
||||
|
||||
export const DotIcon = () => {
|
||||
return (
|
||||
<PageInPageTreeIcon
|
||||
style={{ fill: '#98ACBD', width: '20px', height: '20px' }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { DotIcon } from './DotIcon';
|
||||
@@ -44,7 +44,7 @@ export type DndTreeProps = {
|
||||
*/
|
||||
export function DndTree(props: DndTreeProps) {
|
||||
const {
|
||||
indentationWidth = 12,
|
||||
indentationWidth = 20,
|
||||
collapsible,
|
||||
removable,
|
||||
showDragIndicator,
|
||||
|
||||
@@ -3,10 +3,8 @@ import { DndTree } from './DndTree';
|
||||
import { useDndTreeAutoUpdate } from './use-page-tree';
|
||||
|
||||
const Root = styled('div')({
|
||||
minWidth: 160,
|
||||
maxWidth: 260,
|
||||
marginLeft: 18,
|
||||
marginRight: 6,
|
||||
minWidth: '160px',
|
||||
maxWidth: '276px',
|
||||
});
|
||||
|
||||
export const PageTree = () => {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useParams } from 'react-router-dom';
|
||||
import { useFlag } from '@toeverything/datasource/feature-flags';
|
||||
|
||||
import MoreActions from './MoreActions';
|
||||
import { DotIcon } from '../../dot-icon';
|
||||
import {
|
||||
ActionButton,
|
||||
Counter,
|
||||
@@ -76,24 +77,25 @@ export const TreeItem = forwardRef<HTMLDivElement, TreeItemProps>(
|
||||
ghost={ghost}
|
||||
disableSelection={disableSelection}
|
||||
disableInteraction={disableInteraction}
|
||||
spacing={`${indentationWidth * depth}px`}
|
||||
spacing={`${indentationWidth * depth + 12}px`}
|
||||
active={pageId === page_id}
|
||||
{...props}
|
||||
>
|
||||
<TreeItemContainer ref={ref} style={style} title={value}>
|
||||
<ActionButton tabIndex={0} onClick={onCollapse}>
|
||||
{childCount !== 0 &&
|
||||
(collapsed ? (
|
||||
{childCount !== 0 ? (
|
||||
collapsed ? (
|
||||
<ArrowRightIcon />
|
||||
) : (
|
||||
<ArrowDropDownIcon />
|
||||
))}
|
||||
)
|
||||
) : (
|
||||
<DotIcon />
|
||||
)}
|
||||
</ActionButton>
|
||||
|
||||
<TreeItemContent {...handleProps}>
|
||||
<TextLink
|
||||
to={`/${workspace_id}/${pageId}`}
|
||||
active={pageId === page_id}
|
||||
>
|
||||
<TextLink to={`/${workspace_id}/${pageId}`}>
|
||||
{value}
|
||||
</TextLink>
|
||||
{BooleanPageTreeItemMoreActions && (
|
||||
|
||||
@@ -15,11 +15,14 @@ export const Wrapper = styled('li')<{
|
||||
indicator?: boolean;
|
||||
disableSelection?: boolean;
|
||||
disableInteraction?: boolean;
|
||||
active?: boolean;
|
||||
}>`
|
||||
box-sizing: border-box;
|
||||
padding-left: ${({ spacing }) => spacing};
|
||||
list-style: none;
|
||||
font-size: 14px;
|
||||
background-color: ${({ active }) => (active ? '#f5f7f8' : 'transparent')};
|
||||
border-radius: 5px;
|
||||
|
||||
${({ clone, disableSelection }) =>
|
||||
(clone || disableSelection) &&
|
||||
@@ -126,8 +129,6 @@ export const ActionButton = styled('button')<{
|
||||
fill?: string;
|
||||
}>`
|
||||
display: flex;
|
||||
width: 12px;
|
||||
padding: 0 15px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 auto;
|
||||
@@ -141,9 +142,10 @@ export const ActionButton = styled('button')<{
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
|
||||
svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
flex: 0 0 auto;
|
||||
margin: auto;
|
||||
height: 100%;
|
||||
overflow: visible;
|
||||
fill: #919eab;
|
||||
}
|
||||
@@ -168,7 +170,9 @@ export const TreeItemMoreActions = styled('div')`
|
||||
visibility: hidden;
|
||||
`;
|
||||
|
||||
export const TextLink = styled(Link)<{ active?: boolean }>`
|
||||
export const TextLink = styled(Link, {
|
||||
shouldForwardProp: (prop: string) => !['active'].includes(prop),
|
||||
})<{ active?: boolean }>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
@@ -180,8 +184,7 @@ export const TextLink = styled(Link)<{ active?: boolean }>`
|
||||
appearance: none;
|
||||
text-decoration: none;
|
||||
user-select: none;
|
||||
color: ${({ theme, active }) =>
|
||||
active ? theme.affine.palette.primary : 'unset'};
|
||||
color: #4c6275;
|
||||
`;
|
||||
|
||||
export const TreeItemContent = styled('div')`
|
||||
@@ -193,7 +196,7 @@ export const TreeItemContent = styled('div')`
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
color: #4c6275;
|
||||
padding-right: 0.5rem;
|
||||
padding-right: 12px;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
|
||||
@@ -12,6 +12,7 @@ import SelectUnstyled, {
|
||||
} from '@mui/base/SelectUnstyled';
|
||||
/* eslint-disable no-restricted-imports */
|
||||
import PopperUnstyled from '@mui/base/PopperUnstyled';
|
||||
import { ArrowDropDownIcon } from '@toeverything/components/icons';
|
||||
import { styled } from '../styled';
|
||||
|
||||
type ExtendSelectProps = {
|
||||
@@ -41,20 +42,29 @@ export const Select = forwardRef(function CustomSelect<TValue>(
|
||||
const { width = '100%', style, listboxStyle, placeholder } = props;
|
||||
const components: SelectUnstyledProps<TValue>['components'] = {
|
||||
// Root: generateStyledRoot({ width, ...style }),
|
||||
Root: forwardRef((rootProps, rootRef) => (
|
||||
<StyledRoot
|
||||
ref={rootRef}
|
||||
{...rootProps}
|
||||
style={{
|
||||
width,
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
{rootProps.children || (
|
||||
<StyledPlaceholder>{placeholder}</StyledPlaceholder>
|
||||
)}
|
||||
</StyledRoot>
|
||||
)),
|
||||
Root: forwardRef((rootProps, rootRef) => {
|
||||
const {
|
||||
ownerState: { open },
|
||||
} = rootProps;
|
||||
|
||||
return (
|
||||
<StyledRoot
|
||||
ref={rootRef}
|
||||
{...rootProps}
|
||||
style={{
|
||||
width,
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
{rootProps.children || (
|
||||
<StyledPlaceholder>{placeholder}</StyledPlaceholder>
|
||||
)}
|
||||
<StyledSelectedArrowWrapper open={open}>
|
||||
<ArrowDropDownIcon />
|
||||
</StyledSelectedArrowWrapper>
|
||||
</StyledRoot>
|
||||
);
|
||||
}),
|
||||
Listbox: forwardRef((listboxProps, listboxRef) => (
|
||||
<StyledListbox
|
||||
ref={listboxRef}
|
||||
@@ -73,6 +83,20 @@ export const Select = forwardRef(function CustomSelect<TValue>(
|
||||
RefAttributes<HTMLUListElement>
|
||||
) => JSX.Element;
|
||||
|
||||
const StyledSelectedArrowWrapper = styled('div')<{ open: boolean }>(
|
||||
({ open }) => ({
|
||||
position: 'absolute',
|
||||
top: '0',
|
||||
bottom: '0',
|
||||
right: '12px',
|
||||
margin: 'auto',
|
||||
lineHeight: '32px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
transform: `rotate(${open ? '180deg' : '0'})`,
|
||||
})
|
||||
);
|
||||
|
||||
const StyledRoot = styled('div')(({ theme }) => ({
|
||||
height: '32px',
|
||||
border: `1px solid ${theme.affine.palette.borderColor}`,
|
||||
@@ -95,18 +119,6 @@ const StyledRoot = styled('div')(({ theme }) => ({
|
||||
|
||||
[`&.${selectUnstyledClasses.expanded}`]: {
|
||||
borderColor: `${theme.affine.palette.primary}`,
|
||||
'&::after': {
|
||||
content: '"▴"',
|
||||
},
|
||||
},
|
||||
'&::after': {
|
||||
content: '"▾"',
|
||||
position: ' absolute',
|
||||
top: '0',
|
||||
bottom: '0',
|
||||
right: '12px',
|
||||
margin: 'auto',
|
||||
lineHeight: '32px',
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
@@ -173,26 +173,34 @@ export const Theme = {
|
||||
body1: {
|
||||
fontSize: '16px',
|
||||
lineHeight: '22px',
|
||||
fontWeight: 400,
|
||||
fontFamily: 'PingFang SC',
|
||||
color: '#3A4C5C',
|
||||
},
|
||||
h1: {
|
||||
fontSize: '28px',
|
||||
lineHeight: '40px',
|
||||
fontWeight: 600,
|
||||
},
|
||||
h2: {
|
||||
fontSize: '24px',
|
||||
lineHeight: '34px',
|
||||
fontWeight: 600,
|
||||
},
|
||||
h3: {
|
||||
fontSize: '20px',
|
||||
lineHeight: '28px',
|
||||
fontWeight: 600,
|
||||
},
|
||||
h4: {
|
||||
fontSize: '16px',
|
||||
lineHeight: '22px',
|
||||
fontWeight: 600,
|
||||
},
|
||||
page: {
|
||||
fontSize: '36px',
|
||||
lineHeight: '44px',
|
||||
fontWeight: 600,
|
||||
},
|
||||
callout: {
|
||||
fontSize: '36px',
|
||||
@@ -221,6 +229,7 @@ export const Theme = {
|
||||
articleTitle: {
|
||||
fontSize: '36px',
|
||||
lineHeight: '54px',
|
||||
fontWeight: 600,
|
||||
},
|
||||
},
|
||||
shadows: {
|
||||
|
||||
@@ -154,6 +154,14 @@ export abstract class ServiceBaseClass {
|
||||
await this.database.unregisterTagExporter(workspace, name);
|
||||
}
|
||||
|
||||
async setupDataExporter(
|
||||
workspace: string,
|
||||
initialData: Uint8Array,
|
||||
cb: (data: Uint8Array) => Promise<void>
|
||||
) {
|
||||
await this.database.setupDataExporter(workspace, initialData, cb);
|
||||
}
|
||||
|
||||
protected async _observe(
|
||||
workspace: string,
|
||||
blockId: string,
|
||||
|
||||
@@ -192,4 +192,13 @@ export class Database {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async setupDataExporter(
|
||||
workspace: string,
|
||||
initialData: Uint8Array,
|
||||
callback: (binary: Uint8Array) => Promise<void>
|
||||
) {
|
||||
const db = await this.getDatabase(workspace);
|
||||
await db.setupDataExporter(initialData, callback);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ export class IndexedDBProvider extends Observable<string> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys this instance and removes all data from SQLite.
|
||||
* Destroys this instance and removes all data from indexeddb.
|
||||
*
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Observable } from 'lib0/observable.js';
|
||||
const PREFERRED_TRIM_SIZE = 500;
|
||||
|
||||
const _stmts = {
|
||||
create: 'CREATE TABLE updates (key INTEGER PRIMARY KEY AUTOINCREMENT, value BLOB);',
|
||||
create: 'CREATE TABLE IF NOT EXISTS updates (key INTEGER PRIMARY KEY AUTOINCREMENT, value BLOB);',
|
||||
selectAll: 'SELECT * FROM updates where key >= $idx',
|
||||
selectCount: 'SELECT count(*) FROM updates',
|
||||
insert: 'INSERT INTO updates VALUES (null, $data);',
|
||||
@@ -41,6 +41,7 @@ const initSQLiteInstance = async () => {
|
||||
_sqliteProcessing = true;
|
||||
_sqliteInstance = await sqlite({
|
||||
locateFile: () =>
|
||||
// @ts-ignore
|
||||
new URL('sql.js/dist/sql-wasm.wasm', import.meta.url).href,
|
||||
});
|
||||
_sqliteProcessing = false;
|
||||
@@ -58,7 +59,7 @@ export class SQLiteProvider extends Observable<string> {
|
||||
private _size: number;
|
||||
private _destroyed: boolean;
|
||||
private _db: Promise<Database>;
|
||||
private _saver?: (binary: Uint8Array) => void;
|
||||
private _saver?: (binary: Uint8Array) => Promise<void> | undefined;
|
||||
private _destroy: () => void;
|
||||
|
||||
constructor(name: string, doc: Y.Doc, origin?: Uint8Array) {
|
||||
@@ -81,8 +82,9 @@ export class SQLiteProvider extends Observable<string> {
|
||||
this.whenSynced = this._db.then(async db => {
|
||||
this.db = db;
|
||||
const currState = Y.encodeStateAsUpdate(doc);
|
||||
await this._fetchUpdates();
|
||||
await this._fetchUpdates(true);
|
||||
db.exec(_stmts.insert, { $data: currState });
|
||||
this._storeState();
|
||||
if (this._destroyed) return this;
|
||||
this.emit('synced', [this]);
|
||||
this.synced = true;
|
||||
@@ -90,21 +92,38 @@ export class SQLiteProvider extends Observable<string> {
|
||||
});
|
||||
|
||||
// Timeout in ms until data is merged and persisted in sqlite.
|
||||
const storeTimeout = 1000;
|
||||
const storeTimeout = 500;
|
||||
let storeTimeoutId: NodeJS.Timer | undefined = undefined;
|
||||
let lastSize = 0;
|
||||
|
||||
const debouncedStoreState = (force = false) => {
|
||||
// debounce store call
|
||||
if (storeTimeoutId) clearTimeout(storeTimeoutId);
|
||||
|
||||
if (force) {
|
||||
if (lastSize !== this._size) {
|
||||
this._storeState();
|
||||
storeTimeoutId = undefined;
|
||||
lastSize = this._size;
|
||||
}
|
||||
} else {
|
||||
storeTimeoutId = setTimeout(() => {
|
||||
this._storeState();
|
||||
storeTimeoutId = undefined;
|
||||
}, storeTimeout);
|
||||
}
|
||||
};
|
||||
const storeStateInterval = setInterval(
|
||||
() => debouncedStoreState(true),
|
||||
1000
|
||||
);
|
||||
|
||||
const storeUpdate = (update: Uint8Array, origin: any) => {
|
||||
if (this._saver && this.db && origin !== this) {
|
||||
this.db.exec(_stmts.insert, { $data: update });
|
||||
|
||||
if (++this._size >= PREFERRED_TRIM_SIZE) {
|
||||
// debounce store call
|
||||
if (storeTimeoutId) clearTimeout(storeTimeoutId);
|
||||
|
||||
storeTimeoutId = setTimeout(() => {
|
||||
this._storeState();
|
||||
storeTimeoutId = undefined;
|
||||
}, storeTimeout);
|
||||
debouncedStoreState();
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -115,35 +134,54 @@ export class SQLiteProvider extends Observable<string> {
|
||||
|
||||
this._destroy = () => {
|
||||
if (storeTimeoutId) clearTimeout(storeTimeoutId);
|
||||
if (storeStateInterval) clearInterval(storeStateInterval);
|
||||
|
||||
this.doc.off('update', storeUpdate);
|
||||
this.doc.off('destroy', this.destroy);
|
||||
};
|
||||
}
|
||||
|
||||
registerExporter(saver: (binary: Uint8Array) => void) {
|
||||
registerExporter(saver: (binary: Uint8Array) => Promise<void> | undefined) {
|
||||
this._saver = saver;
|
||||
}
|
||||
|
||||
private async _storeState() {
|
||||
private async _storeState(force?: boolean) {
|
||||
await this._fetchUpdates();
|
||||
|
||||
if (this.db && this._size >= PREFERRED_TRIM_SIZE) {
|
||||
this.db.exec(_stmts.insert, {
|
||||
$data: Y.encodeStateAsUpdate(this.doc),
|
||||
});
|
||||
if (this.db) {
|
||||
if (force || this._size >= PREFERRED_TRIM_SIZE) {
|
||||
this.db.exec(_stmts.insert, {
|
||||
$data: Y.encodeStateAsUpdate(this.doc),
|
||||
});
|
||||
|
||||
clearUpdates(this.db, this._ref);
|
||||
clearUpdates(this.db, this._ref);
|
||||
|
||||
this._size = countUpdates(this.db);
|
||||
this._size = countUpdates(this.db);
|
||||
}
|
||||
|
||||
this._saver?.(this.db?.export());
|
||||
await this._saver?.(this.db?.export());
|
||||
}
|
||||
}
|
||||
|
||||
private async _fetchUpdates() {
|
||||
private _waitUpdate(updates: any[], sync = false) {
|
||||
if (updates.length && sync) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const final = (_: any, origin: any) => {
|
||||
if (origin === this) {
|
||||
this.doc.off('update', final);
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
this.doc.on('update', final);
|
||||
});
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private async _fetchUpdates(sync = false) {
|
||||
if (this.db) {
|
||||
const updates = getAllUpdates(this.db, this._ref);
|
||||
const wait = this._waitUpdate(updates, sync);
|
||||
|
||||
Y.transact(
|
||||
this.doc,
|
||||
@@ -159,6 +197,7 @@ export class SQLiteProvider extends Observable<string> {
|
||||
const lastKey = Math.max(...updates.map(([idx]) => idx));
|
||||
this._ref = lastKey + 1;
|
||||
this._size = countUpdates(this.db);
|
||||
await wait;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -136,6 +136,7 @@ interface BlockInstance<C extends ContentOperation> {
|
||||
|
||||
interface AsyncDatabaseAdapter<C extends ContentOperation> {
|
||||
inspector(): Record<string, any>;
|
||||
reload(): void;
|
||||
createBlock(
|
||||
options: Pick<BlockItem<C>, 'type' | 'flavor'> & {
|
||||
binary?: ArrayBuffer;
|
||||
@@ -156,6 +157,33 @@ interface AsyncDatabaseAdapter<C extends ContentOperation> {
|
||||
getUserId(): string;
|
||||
}
|
||||
|
||||
export type DataExporter = (binary: Uint8Array) => Promise<void>;
|
||||
|
||||
export const getDataExporter = () => {
|
||||
let exporter: DataExporter | undefined = undefined;
|
||||
let importer: (() => Uint8Array | undefined) | undefined = undefined;
|
||||
|
||||
const importData = () => importer?.();
|
||||
const exportData = (binary: Uint8Array) => exporter?.(binary);
|
||||
const hasExporter = () => !!exporter;
|
||||
|
||||
const installExporter = (
|
||||
initialData: Uint8Array | undefined,
|
||||
cb: DataExporter
|
||||
) => {
|
||||
return new Promise<void>(resolve => {
|
||||
importer = () => initialData;
|
||||
exporter = async (data: Uint8Array) => {
|
||||
exporter = cb;
|
||||
await cb(data);
|
||||
resolve();
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
return { importData, exportData, hasExporter, installExporter };
|
||||
};
|
||||
|
||||
export type {
|
||||
AsyncDatabaseAdapter,
|
||||
BlockPosition,
|
||||
|
||||
@@ -18,11 +18,7 @@ import {
|
||||
snapshot,
|
||||
} from 'yjs';
|
||||
|
||||
import {
|
||||
IndexedDBProvider,
|
||||
SQLiteProvider,
|
||||
WebsocketProvider,
|
||||
} from '@toeverything/datasource/jwt-rpc';
|
||||
import { IndexedDBProvider } from '@toeverything/datasource/jwt-rpc';
|
||||
|
||||
import {
|
||||
AsyncDatabaseAdapter,
|
||||
@@ -31,7 +27,7 @@ import {
|
||||
Connectivity,
|
||||
HistoryManager,
|
||||
} from '../../adapter';
|
||||
import { BucketBackend, BlockItem, BlockTypes } from '../../types';
|
||||
import { BlockItem, BlockTypes } from '../../types';
|
||||
import { getLogger, sha3, sleep } from '../../utils';
|
||||
|
||||
import { YjsRemoteBinaries } from './binary';
|
||||
@@ -43,51 +39,26 @@ import {
|
||||
} from './operation';
|
||||
import { EmitEvents, Suspend } from './listener';
|
||||
import { YjsHistoryManager } from './history';
|
||||
import { YjsProvider } from './provider';
|
||||
|
||||
declare const JWT_DEV: boolean;
|
||||
const logger = getLogger('BlockDB:yjs');
|
||||
|
||||
type ConnectivityListener = (
|
||||
workspace: string,
|
||||
connectivity: Connectivity
|
||||
) => void;
|
||||
type YjsProviders = {
|
||||
awareness: Awareness;
|
||||
idb: IndexedDBProvider;
|
||||
binariesIdb: IndexedDBProvider;
|
||||
fstore?: SQLiteProvider;
|
||||
ws?: WebsocketProvider;
|
||||
backend: string;
|
||||
gatekeeper: GateKeeper;
|
||||
connListener: { listeners?: ConnectivityListener };
|
||||
userId: string;
|
||||
remoteToken?: string; // remote storage token
|
||||
};
|
||||
const _yjsDatabaseInstance = new Map<string, YjsProviders>();
|
||||
|
||||
async function _initWebsocketProvider(
|
||||
url: string,
|
||||
room: string,
|
||||
doc: Doc,
|
||||
token?: string,
|
||||
params?: YjsInitOptions['params']
|
||||
): Promise<[Awareness, WebsocketProvider | undefined]> {
|
||||
const awareness = new Awareness(doc);
|
||||
|
||||
if (token) {
|
||||
const ws = new WebsocketProvider(token, url, room, doc, {
|
||||
awareness,
|
||||
params,
|
||||
}) as any; // TODO: type is erased after cascading references
|
||||
|
||||
// Wait for ws synchronization to complete, otherwise the data will be modified in reverse, which can be optimized later
|
||||
return new Promise((resolve, reject) => {
|
||||
// TODO: synced will also be triggered on reconnection after losing sync
|
||||
// There needs to be an event mechanism to emit the synchronization state to the upper layer
|
||||
ws.once('synced', () => resolve([awareness, ws]));
|
||||
ws.once('lost-connection', () => resolve([awareness, ws]));
|
||||
ws.once('connection-error', () => reject());
|
||||
});
|
||||
} else {
|
||||
return [awareness, undefined];
|
||||
}
|
||||
}
|
||||
|
||||
const _asyncInitLoading = new Set<string>();
|
||||
const _waitLoading = async (workspace: string) => {
|
||||
while (_asyncInitLoading.has(workspace)) {
|
||||
@@ -96,14 +67,11 @@ const _waitLoading = async (workspace: string) => {
|
||||
};
|
||||
|
||||
async function _initYjsDatabase(
|
||||
backend: string,
|
||||
workspace: string,
|
||||
options: {
|
||||
params: YjsInitOptions['params'];
|
||||
userId: string;
|
||||
token?: string;
|
||||
importData?: Uint8Array;
|
||||
exportData?: (binary: Uint8Array) => void;
|
||||
provider?: Record<string, YjsProvider>;
|
||||
}
|
||||
): Promise<YjsProviders> {
|
||||
if (_asyncInitLoading.has(workspace)) {
|
||||
@@ -119,28 +87,10 @@ async function _initYjsDatabase(
|
||||
}
|
||||
// if (instance) return instance;
|
||||
_asyncInitLoading.add(workspace);
|
||||
const { params, userId, token: remoteToken } = options;
|
||||
const { userId, token } = options;
|
||||
|
||||
const doc = new Doc({ autoLoad: true, shouldLoad: true });
|
||||
|
||||
const idbp = new IndexedDBProvider(workspace, doc).whenSynced;
|
||||
|
||||
const fs = new SQLiteProvider(workspace, doc, options.importData);
|
||||
if (options.exportData) fs.registerExporter(options.exportData);
|
||||
|
||||
const wsp = _initWebsocketProvider(
|
||||
backend,
|
||||
workspace,
|
||||
doc,
|
||||
remoteToken,
|
||||
params
|
||||
);
|
||||
|
||||
const [idb, [awareness, ws], fstore] = await Promise.all([
|
||||
idbp,
|
||||
wsp,
|
||||
fs.whenSynced,
|
||||
]);
|
||||
const idb = await new IndexedDBProvider(workspace, doc).whenSynced;
|
||||
|
||||
const binaries = new Doc({ autoLoad: true, shouldLoad: true });
|
||||
const binariesIdb = await new IndexedDBProvider(
|
||||
@@ -148,6 +98,8 @@ async function _initYjsDatabase(
|
||||
binaries
|
||||
).whenSynced;
|
||||
|
||||
const awareness = new Awareness(doc);
|
||||
|
||||
const gateKeeperData = doc.getMap<YMap<string>>('gatekeeper');
|
||||
|
||||
const gatekeeper = new GateKeeper(
|
||||
@@ -157,80 +109,74 @@ async function _initYjsDatabase(
|
||||
gateKeeperData.get('common') || gateKeeperData.set('common', new YMap())
|
||||
);
|
||||
|
||||
_yjsDatabaseInstance.set(workspace, {
|
||||
const connListener: { listeners?: ConnectivityListener } = {};
|
||||
if (options.provider) {
|
||||
const emitState = (c: Connectivity) =>
|
||||
connListener.listeners?.(workspace, c);
|
||||
await Promise.all(
|
||||
Object.entries(options.provider).map(async ([, p]) =>
|
||||
p({ awareness, doc, token, workspace, emitState })
|
||||
)
|
||||
);
|
||||
}
|
||||
const newInstance = {
|
||||
awareness,
|
||||
idb,
|
||||
binariesIdb,
|
||||
fstore,
|
||||
ws,
|
||||
backend,
|
||||
gatekeeper,
|
||||
connListener,
|
||||
userId,
|
||||
remoteToken,
|
||||
});
|
||||
remoteToken: token,
|
||||
};
|
||||
|
||||
_yjsDatabaseInstance.set(workspace, newInstance);
|
||||
|
||||
_asyncInitLoading.delete(workspace);
|
||||
|
||||
return {
|
||||
awareness,
|
||||
idb,
|
||||
binariesIdb,
|
||||
fstore,
|
||||
ws,
|
||||
backend,
|
||||
gatekeeper,
|
||||
userId,
|
||||
remoteToken,
|
||||
};
|
||||
return newInstance;
|
||||
}
|
||||
|
||||
export type { YjsBlockInstance } from './block';
|
||||
export type { YjsContentOperation } from './operation';
|
||||
|
||||
export type YjsInitOptions = {
|
||||
backend: typeof BucketBackend[keyof typeof BucketBackend];
|
||||
params?: Record<string, string>;
|
||||
userId?: string;
|
||||
token?: string;
|
||||
importData?: Uint8Array;
|
||||
exportData?: (binary: Uint8Array) => void;
|
||||
provider?: Record<string, YjsProvider>;
|
||||
};
|
||||
|
||||
export { getYjsProviders } from './provider';
|
||||
export type { YjsProviderOptions } from './provider';
|
||||
|
||||
export class YjsAdapter implements AsyncDatabaseAdapter<YjsContentOperation> {
|
||||
private readonly _provider: YjsProviders;
|
||||
private readonly _doc: Doc; // doc instance
|
||||
private readonly _awareness: Awareness; // lightweight state synchronization
|
||||
private readonly _gatekeeper: GateKeeper; // Simple access control
|
||||
private readonly _history: YjsHistoryManager;
|
||||
private readonly _history!: YjsHistoryManager;
|
||||
|
||||
// Block Collection
|
||||
// key is a randomly generated global id
|
||||
private readonly _blocks: YMap<YMap<unknown>>;
|
||||
private readonly _blockUpdated: YMap<number>;
|
||||
private readonly _blocks!: YMap<YMap<unknown>>;
|
||||
private readonly _blockUpdated!: YMap<number>;
|
||||
// Maximum cache Block 1024, ttl 10 minutes
|
||||
private readonly _blockCaches: LRUCache<string, YjsBlockInstance>;
|
||||
private readonly _blockCaches!: LRUCache<string, YjsBlockInstance>;
|
||||
|
||||
private readonly _binaries: YjsRemoteBinaries;
|
||||
private readonly _binaries!: YjsRemoteBinaries;
|
||||
|
||||
private readonly _listener: Map<string, BlockListener<any>>;
|
||||
|
||||
private readonly _reload: () => void;
|
||||
|
||||
static async init(
|
||||
workspace: string,
|
||||
options: YjsInitOptions
|
||||
): Promise<YjsAdapter> {
|
||||
const {
|
||||
backend,
|
||||
params = {},
|
||||
userId = 'default',
|
||||
token,
|
||||
importData,
|
||||
exportData,
|
||||
} = options;
|
||||
const providers = await _initYjsDatabase(backend, workspace, {
|
||||
params,
|
||||
const { userId = 'default', token, provider } = options;
|
||||
const providers = await _initYjsDatabase(workspace, {
|
||||
userId,
|
||||
token,
|
||||
importData,
|
||||
exportData,
|
||||
provider,
|
||||
});
|
||||
return new YjsAdapter(providers);
|
||||
}
|
||||
@@ -240,33 +186,39 @@ export class YjsAdapter implements AsyncDatabaseAdapter<YjsContentOperation> {
|
||||
this._doc = providers.idb.doc;
|
||||
this._awareness = providers.awareness;
|
||||
this._gatekeeper = providers.gatekeeper;
|
||||
|
||||
const blocks = this._doc.getMap<YMap<any>>('blocks');
|
||||
this._blocks =
|
||||
blocks.get('content') || blocks.set('content', new YMap());
|
||||
this._blockUpdated =
|
||||
blocks.get('updated') || blocks.set('updated', new YMap());
|
||||
this._blockCaches = new LRUCache({ max: 1024, ttl: 1000 * 60 * 10 });
|
||||
this._binaries = new YjsRemoteBinaries(
|
||||
providers.binariesIdb.doc.getMap(),
|
||||
providers.remoteToken
|
||||
);
|
||||
this._history = new YjsHistoryManager(this._blocks);
|
||||
this._reload = () => {
|
||||
const blocks = this._doc.getMap<YMap<any>>('blocks');
|
||||
// @ts-ignore
|
||||
this._blocks =
|
||||
blocks.get('content') || blocks.set('content', new YMap());
|
||||
// @ts-ignore
|
||||
this._blockUpdated =
|
||||
blocks.get('updated') || blocks.set('updated', new YMap());
|
||||
// @ts-ignore
|
||||
this._blockCaches = new LRUCache({
|
||||
max: 1024,
|
||||
ttl: 1000 * 60 * 10,
|
||||
});
|
||||
// @ts-ignore
|
||||
this._binaries = new YjsRemoteBinaries(
|
||||
providers.binariesIdb.doc.getMap(),
|
||||
providers.remoteToken
|
||||
);
|
||||
// @ts-ignore
|
||||
this._history = new YjsHistoryManager(this._blocks);
|
||||
};
|
||||
this._reload();
|
||||
|
||||
this._listener = new Map();
|
||||
|
||||
const ws = providers.ws as any;
|
||||
if (ws) {
|
||||
const workspace = providers.idb.name;
|
||||
const emitState = (connectivity: Connectivity) => {
|
||||
this._listener.get('connectivity')?.(
|
||||
new Map([[workspace, connectivity]])
|
||||
);
|
||||
};
|
||||
ws.on('synced', () => emitState('connected'));
|
||||
ws.on('lost-connection', () => emitState('retry'));
|
||||
ws.on('connection-error', () => emitState('retry'));
|
||||
}
|
||||
providers.connListener.listeners = (
|
||||
workspace: string,
|
||||
connectivity: Connectivity
|
||||
) => {
|
||||
this._listener.get('connectivity')?.(
|
||||
new Map([[workspace, connectivity]])
|
||||
);
|
||||
};
|
||||
|
||||
const debounced_editing_notifier = debounce(
|
||||
() => {
|
||||
@@ -341,6 +293,10 @@ export class YjsAdapter implements AsyncDatabaseAdapter<YjsContentOperation> {
|
||||
});
|
||||
}
|
||||
|
||||
reload() {
|
||||
this._reload();
|
||||
}
|
||||
|
||||
getUserId(): string {
|
||||
return this._provider.userId;
|
||||
}
|
||||
|
||||
@@ -67,8 +67,12 @@ export function ChildrenListenerHandler(
|
||||
const keys = Array.from(event.keys.entries()).map(
|
||||
([key, { action }]) => [key, action] as [string, ChangedStateKeys]
|
||||
);
|
||||
const deleted = Array.from(event.changes.deleted.values())
|
||||
.flatMap(val => val.content.getContent() as string[])
|
||||
.filter(v => v)
|
||||
.map(k => [k, 'delete'] as [string, ChangedStateKeys]);
|
||||
for (const listener of listeners.values()) {
|
||||
EmitEvents(keys, listener);
|
||||
EmitEvents([...keys, ...deleted], listener);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
83
libs/datasource/jwt/src/adapter/yjs/provider.ts
Normal file
83
libs/datasource/jwt/src/adapter/yjs/provider.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { Doc } from 'yjs';
|
||||
import { Awareness } from 'y-protocols/awareness.js';
|
||||
|
||||
import {
|
||||
SQLiteProvider,
|
||||
WebsocketProvider,
|
||||
} from '@toeverything/datasource/jwt-rpc';
|
||||
|
||||
import { Connectivity } from '../../adapter';
|
||||
import { BucketBackend } from '../../types';
|
||||
|
||||
type YjsDefaultInstances = {
|
||||
awareness: Awareness;
|
||||
doc: Doc;
|
||||
token?: string;
|
||||
workspace: string;
|
||||
emitState: (connectivity: Connectivity) => void;
|
||||
};
|
||||
|
||||
export type YjsProvider = (instances: YjsDefaultInstances) => Promise<void>;
|
||||
|
||||
export type YjsProviderOptions = {
|
||||
backend: typeof BucketBackend[keyof typeof BucketBackend];
|
||||
params?: Record<string, string>;
|
||||
importData?: () => Promise<Uint8Array> | Uint8Array | undefined;
|
||||
exportData?: (binary: Uint8Array) => Promise<void> | undefined;
|
||||
hasExporter?: () => boolean;
|
||||
};
|
||||
|
||||
export const getYjsProviders = (
|
||||
options: YjsProviderOptions
|
||||
): Record<string, YjsProvider> => {
|
||||
return {
|
||||
sqlite: async (instances: YjsDefaultInstances) => {
|
||||
const fsHandle = setInterval(async () => {
|
||||
if (options.hasExporter?.()) {
|
||||
clearInterval(fsHandle);
|
||||
const fs = new SQLiteProvider(
|
||||
instances.workspace,
|
||||
instances.doc,
|
||||
await options.importData?.()
|
||||
);
|
||||
if (options.exportData) {
|
||||
fs.registerExporter(options.exportData);
|
||||
}
|
||||
await fs.whenSynced;
|
||||
}
|
||||
}, 500);
|
||||
},
|
||||
ws: async (instances: YjsDefaultInstances) => {
|
||||
if (instances.token) {
|
||||
const ws = new WebsocketProvider(
|
||||
instances.token,
|
||||
options.backend,
|
||||
instances.workspace,
|
||||
instances.doc,
|
||||
{
|
||||
awareness: instances.awareness,
|
||||
params: options.params,
|
||||
}
|
||||
) as any; // TODO: type is erased after cascading references
|
||||
|
||||
// Wait for ws synchronization to complete, otherwise the data will be modified in reverse, which can be optimized later
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
// TODO: synced will also be triggered on reconnection after losing sync
|
||||
// There needs to be an event mechanism to emit the synchronization state to the upper layer
|
||||
ws.once('synced', () => resolve());
|
||||
ws.once('lost-connection', () => resolve());
|
||||
ws.once('connection-error', () => reject());
|
||||
ws.on('synced', () => instances.emitState('connected'));
|
||||
ws.on('lost-connection', () =>
|
||||
instances.emitState('retry')
|
||||
);
|
||||
ws.on('connection-error', () =>
|
||||
instances.emitState('retry')
|
||||
);
|
||||
});
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -27,12 +27,13 @@ export class AbstractBlock<
|
||||
C extends ContentOperation
|
||||
> {
|
||||
private readonly _id: string;
|
||||
readonly #block: BlockInstance<C>;
|
||||
private readonly _block: BlockInstance<C>;
|
||||
private readonly _history: HistoryManager;
|
||||
private readonly _root?: AbstractBlock<B, C>;
|
||||
private readonly _parentListener: Map<string, BlockListener>;
|
||||
|
||||
_parent?: AbstractBlock<B, C>;
|
||||
private _parent?: AbstractBlock<B, C>;
|
||||
private _changeParent?: () => void;
|
||||
|
||||
constructor(
|
||||
block: B,
|
||||
@@ -40,20 +41,14 @@ export class AbstractBlock<
|
||||
parent?: AbstractBlock<B, C>
|
||||
) {
|
||||
this._id = block.id;
|
||||
this.#block = block;
|
||||
this._history = this.#block.scopedHistory([this._id]);
|
||||
this._block = block;
|
||||
this._history = this._block.scopedHistory([this._id]);
|
||||
|
||||
this._root = root;
|
||||
this._parentListener = new Map();
|
||||
this._parent = parent;
|
||||
|
||||
JWT_DEV && logger_debug(`init: exists ${this._id}`);
|
||||
if (parent) {
|
||||
parent.addChildrenListener(this._id, states => {
|
||||
if (states.get(this._id) === 'delete') {
|
||||
this._emitParent(parent._id, 'delete');
|
||||
}
|
||||
});
|
||||
}
|
||||
if (parent) this._refreshParent(parent);
|
||||
}
|
||||
|
||||
public get root() {
|
||||
@@ -66,7 +61,7 @@ export class AbstractBlock<
|
||||
|
||||
protected _getParentPage(warning = true): string | undefined {
|
||||
if (this.flavor === 'page') {
|
||||
return this.#block.id;
|
||||
return this._block.id;
|
||||
} else if (!this._parent) {
|
||||
if (warning && this.flavor !== 'workspace') {
|
||||
console.warn('parent not found');
|
||||
@@ -89,7 +84,7 @@ export class AbstractBlock<
|
||||
if (event === 'parent') {
|
||||
this._parentListener.set(name, callback);
|
||||
} else {
|
||||
this.#block.on(event, name, callback);
|
||||
this._block.on(event, name, callback);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,42 +92,40 @@ export class AbstractBlock<
|
||||
if (event === 'parent') {
|
||||
this._parentListener.delete(name);
|
||||
} else {
|
||||
this.#block.off(event, name);
|
||||
this._block.off(event, name);
|
||||
}
|
||||
}
|
||||
|
||||
public addChildrenListener(name: string, listener: BlockListener) {
|
||||
this.#block.addChildrenListener(name, listener);
|
||||
this._block.addChildrenListener(name, listener);
|
||||
}
|
||||
|
||||
public removeChildrenListener(name: string) {
|
||||
this.#block.removeChildrenListener(name);
|
||||
this._block.removeChildrenListener(name);
|
||||
}
|
||||
|
||||
public addContentListener(name: string, listener: BlockListener) {
|
||||
this.#block.addContentListener(name, listener);
|
||||
this._block.addContentListener(name, listener);
|
||||
}
|
||||
|
||||
public removeContentListener(name: string) {
|
||||
this.#block.removeContentListener(name);
|
||||
this._block.removeContentListener(name);
|
||||
}
|
||||
|
||||
public getContent<
|
||||
T extends ContentTypes = ContentOperation
|
||||
>(): MapOperation<T> {
|
||||
if (this.#block.type === BlockTypes.block) {
|
||||
return this.#block.content.asMap() as MapOperation<T>;
|
||||
if (this._block.type === BlockTypes.block) {
|
||||
return this._block.content.asMap() as MapOperation<T>;
|
||||
}
|
||||
throw new Error(
|
||||
`this block not a structured block: ${this._id}, ${
|
||||
this.#block.type
|
||||
}`
|
||||
`this block not a structured block: ${this._id}, ${this._block.type}`
|
||||
);
|
||||
}
|
||||
|
||||
public getBinary(): ArrayBuffer | undefined {
|
||||
if (this.#block.type === BlockTypes.binary) {
|
||||
return this.#block.content.asArray<ArrayBuffer>()?.get(0);
|
||||
if (this._block.type === BlockTypes.binary) {
|
||||
return this._block.content.asArray<ArrayBuffer>()?.get(0);
|
||||
}
|
||||
throw new Error('this block not a binary block');
|
||||
}
|
||||
@@ -162,7 +155,7 @@ export class AbstractBlock<
|
||||
|
||||
// Last update UTC time
|
||||
public get lastUpdated(): number {
|
||||
return this.#block.updated || this.#block.created;
|
||||
return this._block.updated || this._block.created;
|
||||
}
|
||||
|
||||
private get last_updated_date(): string | undefined {
|
||||
@@ -171,7 +164,7 @@ export class AbstractBlock<
|
||||
|
||||
// create UTC time
|
||||
public get created(): number {
|
||||
return this.#block.created;
|
||||
return this._block.created;
|
||||
}
|
||||
|
||||
private get created_date(): string | undefined {
|
||||
@@ -180,11 +173,11 @@ export class AbstractBlock<
|
||||
|
||||
// creator id
|
||||
public get creator(): string | undefined {
|
||||
return this.#block.creator;
|
||||
return this._block.creator;
|
||||
}
|
||||
|
||||
[_GET_BLOCK]() {
|
||||
return this.#block;
|
||||
return this._block;
|
||||
}
|
||||
|
||||
private _emitParent(
|
||||
@@ -199,8 +192,20 @@ export class AbstractBlock<
|
||||
}
|
||||
}
|
||||
|
||||
[_SET_PARENT](parent: AbstractBlock<B, C>) {
|
||||
private _refreshParent(parent: AbstractBlock<B, C>) {
|
||||
this._changeParent?.();
|
||||
parent.addChildrenListener(this._id, states => {
|
||||
if (states.get(this._id) === 'delete') {
|
||||
this._emitParent(parent._id, 'delete');
|
||||
}
|
||||
});
|
||||
|
||||
this._parent = parent;
|
||||
this._changeParent = () => parent.removeChildrenListener(this._id);
|
||||
}
|
||||
|
||||
[_SET_PARENT](parent: AbstractBlock<B, C>) {
|
||||
this._refreshParent(parent);
|
||||
this._emitParent(parent.id);
|
||||
}
|
||||
|
||||
@@ -234,23 +239,23 @@ export class AbstractBlock<
|
||||
* current block type
|
||||
*/
|
||||
public get type(): typeof BlockTypes[BlockTypeKeys] {
|
||||
return this.#block.type;
|
||||
return this._block.type;
|
||||
}
|
||||
|
||||
/**
|
||||
* current block flavor
|
||||
*/
|
||||
public get flavor(): typeof BlockFlavors[BlockFlavorKeys] {
|
||||
return this.#block.flavor;
|
||||
return this._block.flavor;
|
||||
}
|
||||
|
||||
// TODO: flavor needs optimization
|
||||
setFlavor(flavor: typeof BlockFlavors[BlockFlavorKeys]) {
|
||||
this.#block.setFlavor(flavor);
|
||||
this._block.setFlavor(flavor);
|
||||
}
|
||||
|
||||
public get children(): string[] {
|
||||
return this.#block.children;
|
||||
return this._block.children;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -274,12 +279,12 @@ export class AbstractBlock<
|
||||
throw new Error('insertChildren: binary not allow insert children');
|
||||
}
|
||||
|
||||
this.#block.insertChildren(block[_GET_BLOCK](), position);
|
||||
this._block.insertChildren(block[_GET_BLOCK](), position);
|
||||
block[_SET_PARENT](this);
|
||||
}
|
||||
|
||||
public hasChildren(id: string): boolean {
|
||||
return this.#block.hasChildren(id);
|
||||
return this._block.hasChildren(id);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -289,11 +294,11 @@ export class AbstractBlock<
|
||||
*/
|
||||
protected get_children(blockId?: string): BlockInstance<C>[] {
|
||||
JWT_DEV && logger(`get children: ${blockId}`);
|
||||
return this.#block.getChildren([blockId]);
|
||||
return this._block.getChildren([blockId]);
|
||||
}
|
||||
|
||||
public removeChildren(blockId?: string) {
|
||||
this.#block.removeChildren([blockId]);
|
||||
this._block.removeChildren([blockId]);
|
||||
}
|
||||
|
||||
public remove() {
|
||||
|
||||
@@ -14,8 +14,14 @@ import {
|
||||
HistoryManager,
|
||||
ContentTypes,
|
||||
Connectivity,
|
||||
DataExporter,
|
||||
getDataExporter,
|
||||
} from './adapter';
|
||||
import { YjsBlockInstance } from './adapter/yjs';
|
||||
import {
|
||||
getYjsProviders,
|
||||
YjsBlockInstance,
|
||||
YjsProviderOptions,
|
||||
} from './adapter/yjs';
|
||||
import {
|
||||
BaseBlock,
|
||||
BlockIndexer,
|
||||
@@ -27,11 +33,11 @@ import {
|
||||
BlockTypes,
|
||||
BlockTypeKeys,
|
||||
BlockFlavors,
|
||||
BucketBackend,
|
||||
UUID,
|
||||
BlockFlavorKeys,
|
||||
BlockItem,
|
||||
ExcludeFunction,
|
||||
BucketBackend,
|
||||
} from './types';
|
||||
import { BlockEventBus, genUUID, getLogger } from './utils';
|
||||
|
||||
@@ -62,6 +68,10 @@ type BlockClientOptions = {
|
||||
content?: BlockExporters<string>;
|
||||
metadata?: BlockExporters<Array<[string, number | string | string[]]>>;
|
||||
tagger?: BlockExporters<string[]>;
|
||||
installExporter: (
|
||||
initialData: Uint8Array,
|
||||
exporter: DataExporter
|
||||
) => Promise<void>;
|
||||
};
|
||||
|
||||
export class BlockClient<
|
||||
@@ -91,10 +101,15 @@ export class BlockClient<
|
||||
|
||||
private readonly _root: { node?: BaseBlock<B, C> };
|
||||
|
||||
private readonly _installExporter: (
|
||||
initialData: Uint8Array,
|
||||
exporter: DataExporter
|
||||
) => Promise<void>;
|
||||
|
||||
private constructor(
|
||||
adapter: A,
|
||||
workspace: string,
|
||||
options?: BlockClientOptions
|
||||
options: BlockClientOptions
|
||||
) {
|
||||
this._adapter = adapter;
|
||||
this._workspace = workspace;
|
||||
@@ -138,6 +153,7 @@ export class BlockClient<
|
||||
});
|
||||
|
||||
this._root = {};
|
||||
this._installExporter = options.installExporter;
|
||||
}
|
||||
|
||||
public addBlockListener(tag: string, listener: BlockListener) {
|
||||
@@ -586,15 +602,34 @@ export class BlockClient<
|
||||
return this._adapter.history();
|
||||
}
|
||||
|
||||
public async setupDataExporter(initialData: Uint8Array, cb: DataExporter) {
|
||||
await this._installExporter(initialData, cb);
|
||||
this._adapter.reload();
|
||||
}
|
||||
|
||||
public static async init(
|
||||
workspace: string,
|
||||
options: Partial<YjsInitOptions & BlockClientOptions> = {}
|
||||
options: Partial<
|
||||
YjsInitOptions & YjsProviderOptions & BlockClientOptions
|
||||
> = {}
|
||||
): Promise<BlockClientInstance> {
|
||||
const { importData, exportData, hasExporter, installExporter } =
|
||||
getDataExporter();
|
||||
|
||||
const instance = await YjsAdapter.init(workspace, {
|
||||
backend: BucketBackend.YjsWebSocketAffine,
|
||||
provider: getYjsProviders({
|
||||
backend: BucketBackend.YjsWebSocketAffine,
|
||||
importData,
|
||||
exportData,
|
||||
hasExporter,
|
||||
...options,
|
||||
}),
|
||||
...options,
|
||||
});
|
||||
return new BlockClient(instance, workspace, options);
|
||||
return new BlockClient(instance, workspace, {
|
||||
...options,
|
||||
installExporter,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,28 +55,39 @@ const _useUserAndSpace = () => {
|
||||
|
||||
const currentSpaceId: string | undefined = useMemo(() => user?.id, [user]);
|
||||
|
||||
return {
|
||||
user,
|
||||
currentSpaceId,
|
||||
loading,
|
||||
};
|
||||
return { user, currentSpaceId, loading };
|
||||
};
|
||||
|
||||
const BRAND_ID = 'AFFiNE';
|
||||
|
||||
const _localTrigger = atom<boolean>(false);
|
||||
const _useUserAndSpacesForFreeLogin = () => {
|
||||
const [user, setUser] = useAtom(_userAtom);
|
||||
const [loading, setLoading] = useAtom(_loadingAtom);
|
||||
const [localTrigger] = useAtom(_localTrigger);
|
||||
|
||||
useEffect(() => setLoading(false), []);
|
||||
const BRAND_ID = 'AFFiNE';
|
||||
return {
|
||||
user: {
|
||||
photo: '',
|
||||
id: BRAND_ID,
|
||||
nickname: BRAND_ID,
|
||||
email: '',
|
||||
} as UserInfo,
|
||||
currentSpaceId: BRAND_ID,
|
||||
loading,
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (localTrigger) {
|
||||
setUser({
|
||||
photo: '',
|
||||
id: BRAND_ID,
|
||||
username: BRAND_ID,
|
||||
nickname: BRAND_ID,
|
||||
email: '',
|
||||
});
|
||||
}
|
||||
}, [localTrigger, setLoading, setUser]);
|
||||
|
||||
const currentSpaceId: string | undefined = useMemo(() => user?.id, [user]);
|
||||
|
||||
return { user, currentSpaceId, loading };
|
||||
};
|
||||
|
||||
export const useLocalTrigger = () => {
|
||||
const [, setTrigger] = useAtom(_localTrigger);
|
||||
return () => setTrigger(true);
|
||||
};
|
||||
|
||||
export const useUserAndSpaces = process.env['NX_LOCAL']
|
||||
|
||||
Reference in New Issue
Block a user