mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-06 11:59:51 +08:00
refactor(infra): directory structure (#4615)
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* @vitest-environment happy-dom
|
||||
*/
|
||||
import 'fake-indexeddb/auto';
|
||||
|
||||
import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import { Schema, Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { beforeEach } from 'vitest';
|
||||
|
||||
import { useBlockSuitePagePreview } from '../use-block-suite-page-preview';
|
||||
import { useBlockSuiteWorkspaceName } from '../use-block-suite-workspace-name';
|
||||
import { useBlockSuiteWorkspacePageTitle } from '../use-block-suite-workspace-page-title';
|
||||
|
||||
let blockSuiteWorkspace: BlockSuiteWorkspace;
|
||||
|
||||
const schema = new Schema();
|
||||
schema.register(AffineSchemas).register(__unstableSchemas);
|
||||
|
||||
beforeEach(async () => {
|
||||
blockSuiteWorkspace = new BlockSuiteWorkspace({ id: 'test', schema });
|
||||
const initPage = async (page: Page) => {
|
||||
await page.waitForLoaded();
|
||||
expect(page).not.toBeNull();
|
||||
assertExists(page);
|
||||
const pageBlockId = page.addBlock('affine:page', {
|
||||
title: new page.Text(''),
|
||||
});
|
||||
const frameId = page.addBlock('affine:note', {}, pageBlockId);
|
||||
page.addBlock('affine:paragraph', {}, frameId);
|
||||
};
|
||||
await initPage(blockSuiteWorkspace.createPage({ id: 'page0' }));
|
||||
await initPage(blockSuiteWorkspace.createPage({ id: 'page1' }));
|
||||
await initPage(blockSuiteWorkspace.createPage({ id: 'page2' }));
|
||||
});
|
||||
|
||||
describe('useBlockSuiteWorkspaceName', () => {
|
||||
test('basic', async () => {
|
||||
blockSuiteWorkspace.meta.setName('test 1');
|
||||
const workspaceNameHook = renderHook(() =>
|
||||
useBlockSuiteWorkspaceName(blockSuiteWorkspace)
|
||||
);
|
||||
expect(workspaceNameHook.result.current[0]).toBe('test 1');
|
||||
blockSuiteWorkspace.meta.setName('test 2');
|
||||
workspaceNameHook.rerender();
|
||||
expect(workspaceNameHook.result.current[0]).toBe('test 2');
|
||||
workspaceNameHook.result.current[1]('test 3');
|
||||
expect(blockSuiteWorkspace.meta.name).toBe('test 3');
|
||||
});
|
||||
});
|
||||
|
||||
describe('useBlockSuiteWorkspacePageTitle', () => {
|
||||
test('basic', async () => {
|
||||
const pageTitleHook = renderHook(() =>
|
||||
useBlockSuiteWorkspacePageTitle(blockSuiteWorkspace, 'page0')
|
||||
);
|
||||
expect(pageTitleHook.result.current).toBe('Untitled');
|
||||
blockSuiteWorkspace.setPageMeta('page0', { title: '1' });
|
||||
pageTitleHook.rerender();
|
||||
expect(pageTitleHook.result.current).toBe('1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('useBlockSuitePagePreview', () => {
|
||||
test('basic', async () => {
|
||||
const page = blockSuiteWorkspace.getPage('page0') as Page;
|
||||
const id = page.addBlock(
|
||||
'affine:paragraph',
|
||||
{
|
||||
text: new page.Text('Hello, world!'),
|
||||
},
|
||||
page.getBlockByFlavour('affine:note')[0].id
|
||||
);
|
||||
const hook = renderHook(() => useAtomValue(useBlockSuitePagePreview(page)));
|
||||
expect(hook.result.current).toBe('\nHello, world!');
|
||||
page.transact(() => {
|
||||
page.getBlockById(id)!.text!.insert('Test', 0);
|
||||
});
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
hook.rerender();
|
||||
expect(hook.result.current).toBe('\nTestHello, world!');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* @vitest-environment happy-dom
|
||||
*/
|
||||
import 'fake-indexeddb/auto';
|
||||
|
||||
import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models';
|
||||
import { Schema, Workspace } from '@blocksuite/store';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { initEmptyPage } from '@toeverything/infra/blocksuite';
|
||||
import { beforeEach, describe, expect, test } from 'vitest';
|
||||
|
||||
import { useBlockSuitePageMeta } from '../use-block-suite-page-meta';
|
||||
import { useBlockSuiteWorkspaceHelper } from '../use-block-suite-workspace-helper';
|
||||
|
||||
let blockSuiteWorkspace: Workspace;
|
||||
|
||||
const schema = new Schema();
|
||||
schema.register(AffineSchemas).register(__unstableSchemas);
|
||||
|
||||
beforeEach(async () => {
|
||||
blockSuiteWorkspace = new Workspace({
|
||||
id: 'test',
|
||||
schema,
|
||||
});
|
||||
await initEmptyPage(blockSuiteWorkspace.createPage({ id: 'page0' }));
|
||||
await initEmptyPage(blockSuiteWorkspace.createPage({ id: 'page1' }));
|
||||
await initEmptyPage(blockSuiteWorkspace.createPage({ id: 'page2' }));
|
||||
});
|
||||
|
||||
describe('useBlockSuiteWorkspaceHelper', () => {
|
||||
test('should create page', () => {
|
||||
expect(blockSuiteWorkspace.meta.pageMetas.length).toBe(3);
|
||||
const helperHook = renderHook(() =>
|
||||
useBlockSuiteWorkspaceHelper(blockSuiteWorkspace)
|
||||
);
|
||||
const pageMetaHook = renderHook(() =>
|
||||
useBlockSuitePageMeta(blockSuiteWorkspace)
|
||||
);
|
||||
expect(pageMetaHook.result.current.length).toBe(3);
|
||||
expect(blockSuiteWorkspace.meta.pageMetas.length).toBe(3);
|
||||
const page = helperHook.result.current.createPage('page4');
|
||||
expect(page.id).toBe('page4');
|
||||
expect(blockSuiteWorkspace.meta.pageMetas.length).toBe(4);
|
||||
pageMetaHook.rerender();
|
||||
expect(pageMetaHook.result.current.length).toBe(4);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
|
||||
declare global {
|
||||
interface IPCRenderer {
|
||||
send(channel: string, ...args: any[]): void;
|
||||
invoke(channel: string, ...args: any[]): Promise<any>;
|
||||
on(
|
||||
channel: string,
|
||||
listener: (event: unknown, ...args: any[]) => void
|
||||
): this;
|
||||
once(
|
||||
channel: string,
|
||||
listener: (event: unknown, ...args: any[]) => void
|
||||
): this;
|
||||
removeListener(channel: string, listener: (...args: any[]) => void): this;
|
||||
}
|
||||
|
||||
interface Window {
|
||||
affine: {
|
||||
ipcRenderer: IPCRenderer;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsafe
|
||||
*/
|
||||
export function useAffineAsyncCallback(channel: string) {
|
||||
return useCallback(
|
||||
(...args: any[]): Promise<any> => {
|
||||
return window.affine.ipcRenderer.invoke(channel, ...args);
|
||||
},
|
||||
[channel]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsafe
|
||||
*/
|
||||
export function useAffineListener(
|
||||
channel: string,
|
||||
listener: (event: unknown, ...args: any[]) => void,
|
||||
once?: boolean
|
||||
): void {
|
||||
const fnRef = useRef<((event: unknown, ...args: any[]) => void) | null>(null);
|
||||
if (!fnRef.current) {
|
||||
fnRef.current = listener;
|
||||
}
|
||||
const ipcListener = fnRef.current ?? (fnRef.current = listener);
|
||||
useEffect(() => {
|
||||
if (once) {
|
||||
window.affine.ipcRenderer.once(channel, ipcListener);
|
||||
} else {
|
||||
window.affine.ipcRenderer.on(channel, ipcListener);
|
||||
}
|
||||
return () => {
|
||||
window.affine.ipcRenderer.removeListener(channel, ipcListener);
|
||||
};
|
||||
}, [channel, once, ipcListener]);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import type { PageBlockModel } from '@blocksuite/blocks';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import type { PageMeta, Workspace } from '@blocksuite/store';
|
||||
import type { Atom } from 'jotai';
|
||||
import { atom, useAtomValue } from 'jotai';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
const weakMap = new WeakMap<Workspace, Atom<PageMeta[]>>();
|
||||
|
||||
export function useBlockSuitePageMeta(
|
||||
blockSuiteWorkspace: Workspace
|
||||
): PageMeta[] {
|
||||
if (!weakMap.has(blockSuiteWorkspace)) {
|
||||
const baseAtom = atom<PageMeta[]>(blockSuiteWorkspace.meta.pageMetas);
|
||||
weakMap.set(blockSuiteWorkspace, baseAtom);
|
||||
baseAtom.onMount = set => {
|
||||
const dispose = blockSuiteWorkspace.meta.pageMetasUpdated.on(() => {
|
||||
set(blockSuiteWorkspace.meta.pageMetas);
|
||||
});
|
||||
return () => {
|
||||
dispose.dispose();
|
||||
};
|
||||
};
|
||||
}
|
||||
return useAtomValue(weakMap.get(blockSuiteWorkspace) as Atom<PageMeta[]>);
|
||||
}
|
||||
|
||||
export function usePageMetaHelper(blockSuiteWorkspace: Workspace) {
|
||||
return useMemo(
|
||||
() => ({
|
||||
setPageTitle: (pageId: string, newTitle: string) => {
|
||||
const page = blockSuiteWorkspace.getPage(pageId);
|
||||
assertExists(page);
|
||||
const pageBlock = page
|
||||
.getBlockByFlavour('affine:page')
|
||||
.at(0) as PageBlockModel;
|
||||
assertExists(pageBlock);
|
||||
page.transact(() => {
|
||||
pageBlock.title.delete(0, pageBlock.title.length);
|
||||
pageBlock.title.insert(newTitle, 0);
|
||||
});
|
||||
blockSuiteWorkspace.meta.setPageMeta(pageId, { title: newTitle });
|
||||
},
|
||||
setPageReadonly: (pageId: string, readonly: boolean) => {
|
||||
const page = blockSuiteWorkspace.getPage(pageId);
|
||||
assertExists(page);
|
||||
page.awarenessStore.setReadonly(page, readonly);
|
||||
},
|
||||
setPageMeta: (pageId: string, pageMeta: Partial<PageMeta>) => {
|
||||
blockSuiteWorkspace.meta.setPageMeta(pageId, pageMeta);
|
||||
},
|
||||
getPageMeta: (pageId: string) => {
|
||||
return blockSuiteWorkspace.meta.getPageMeta(pageId);
|
||||
},
|
||||
}),
|
||||
[blockSuiteWorkspace]
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { ParagraphBlockModel } from '@blocksuite/blocks/models';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import type { Atom } from 'jotai';
|
||||
import { atom } from 'jotai';
|
||||
|
||||
const weakMap = new WeakMap<Page, Atom<string>>();
|
||||
|
||||
export const getPagePreviewText = (page: Page) => {
|
||||
// TODO this is incorrect, since the order of blocks is not guaranteed
|
||||
const paragraphBlocks = page.getBlockByFlavour(
|
||||
'affine:paragraph'
|
||||
) as ParagraphBlockModel[];
|
||||
const text = paragraphBlocks
|
||||
.slice(0, 10)
|
||||
.map(block => block.text.toString())
|
||||
.join('\n');
|
||||
return text.slice(0, 30);
|
||||
};
|
||||
|
||||
export function useBlockSuitePagePreview(page: Page): Atom<string> {
|
||||
if (weakMap.has(page)) {
|
||||
return weakMap.get(page) as Atom<string>;
|
||||
} else {
|
||||
const baseAtom = atom<string>(getPagePreviewText(page));
|
||||
baseAtom.onMount = set => {
|
||||
const disposable = page.slots.yUpdated.on(() => {
|
||||
set(getPagePreviewText(page));
|
||||
});
|
||||
set(getPagePreviewText(page));
|
||||
return () => {
|
||||
disposable.dispose();
|
||||
};
|
||||
};
|
||||
weakMap.set(page, baseAtom);
|
||||
return baseAtom;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import type { Page, Workspace } from '@blocksuite/store';
|
||||
import { type Atom, atom, useAtomValue } from 'jotai';
|
||||
|
||||
import { useBlockSuiteWorkspacePage } from './use-block-suite-workspace-page';
|
||||
|
||||
const weakMap = new WeakMap<Page, Atom<string[]>>();
|
||||
function getPageReferences(page: Page): string[] {
|
||||
// todo: is there a way to use page indexer to get all references?
|
||||
return ['affine:paragraph', 'affine:list', 'affine:database']
|
||||
.flatMap(f => page.getBlockByFlavour(f))
|
||||
.flatMap(b => b.text?.toDelta())
|
||||
.map(v => v?.attributes?.reference?.pageId)
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
const getPageReferencesAtom = (page: Page | null) => {
|
||||
if (!page) {
|
||||
return atom([]);
|
||||
}
|
||||
|
||||
if (!weakMap.has(page)) {
|
||||
const baseAtom = atom<string[]>(getPageReferences(page));
|
||||
baseAtom.onMount = set => {
|
||||
const dispose = page.slots.yUpdated.on(() => {
|
||||
set(getPageReferences(page));
|
||||
});
|
||||
return () => {
|
||||
dispose.dispose();
|
||||
};
|
||||
};
|
||||
weakMap.set(page, baseAtom);
|
||||
}
|
||||
return weakMap.get(page) as Atom<string[]>;
|
||||
};
|
||||
|
||||
export function useBlockSuitePageReferences(
|
||||
blockSuiteWorkspace: Workspace,
|
||||
pageId: string
|
||||
): string[] {
|
||||
const page = useBlockSuiteWorkspacePage(blockSuiteWorkspace, pageId);
|
||||
return useAtomValue(getPageReferencesAtom(page));
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import type { Workspace } from '@blocksuite/store';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import useSWR from 'swr';
|
||||
|
||||
export function useBlockSuiteWorkspaceAvatarUrl(
|
||||
blockSuiteWorkspace: Workspace
|
||||
) {
|
||||
const [url, set] = useState(() => blockSuiteWorkspace.meta.avatar);
|
||||
if (url !== blockSuiteWorkspace.meta.avatar) {
|
||||
set(blockSuiteWorkspace.meta.avatar);
|
||||
}
|
||||
const { data: avatar, mutate } = useSWR(url, {
|
||||
fetcher: async avatar => {
|
||||
assertExists(blockSuiteWorkspace);
|
||||
const blobs = blockSuiteWorkspace.blobs;
|
||||
const blob = await blobs.get(avatar);
|
||||
if (blob) {
|
||||
return URL.createObjectURL(blob);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
suspense: true,
|
||||
fallbackData: null,
|
||||
});
|
||||
const setAvatar = useCallback(
|
||||
async (file: File | null) => {
|
||||
assertExists(blockSuiteWorkspace);
|
||||
if (!file) {
|
||||
blockSuiteWorkspace.meta.setAvatar('');
|
||||
return;
|
||||
}
|
||||
const blob = new Blob([file], { type: file.type });
|
||||
const blobs = blockSuiteWorkspace.blobs;
|
||||
const blobId = await blobs.set(blob);
|
||||
blockSuiteWorkspace.meta.setAvatar(blobId);
|
||||
await mutate(blobId);
|
||||
},
|
||||
[blockSuiteWorkspace, mutate]
|
||||
);
|
||||
useEffect(() => {
|
||||
if (blockSuiteWorkspace) {
|
||||
const dispose = blockSuiteWorkspace.meta.commonFieldsUpdated.on(() => {
|
||||
set(blockSuiteWorkspace.meta.avatar);
|
||||
});
|
||||
return () => {
|
||||
dispose.dispose();
|
||||
};
|
||||
}
|
||||
return;
|
||||
}, [blockSuiteWorkspace]);
|
||||
return [avatar ?? null, setAvatar] as const;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { Page, Workspace } from '@blocksuite/store';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export function useBlockSuiteWorkspaceHelper(blockSuiteWorkspace: Workspace) {
|
||||
return useMemo(
|
||||
() => ({
|
||||
createPage: (pageId?: string): Page => {
|
||||
return blockSuiteWorkspace.createPage({ id: pageId });
|
||||
},
|
||||
}),
|
||||
[blockSuiteWorkspace]
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { UNTITLED_WORKSPACE_NAME } from '@affine/env/constant';
|
||||
import type { Workspace } from '@blocksuite/store';
|
||||
import type { Atom, WritableAtom } from 'jotai';
|
||||
import { atom, useAtom } from 'jotai';
|
||||
|
||||
type StringAtom = WritableAtom<string, [string], void> & Atom<string>;
|
||||
|
||||
const weakMap = new WeakMap<Workspace, StringAtom>();
|
||||
|
||||
export function useBlockSuiteWorkspaceName(blockSuiteWorkspace: Workspace) {
|
||||
let nameAtom: StringAtom;
|
||||
if (!weakMap.has(blockSuiteWorkspace)) {
|
||||
const baseAtom = atom<string>(
|
||||
blockSuiteWorkspace.meta.name ?? UNTITLED_WORKSPACE_NAME
|
||||
);
|
||||
const writableAtom = atom(
|
||||
get => get(baseAtom),
|
||||
(_, set, name: string) => {
|
||||
blockSuiteWorkspace.meta.setName(name);
|
||||
set(baseAtom, name);
|
||||
}
|
||||
);
|
||||
baseAtom.onMount = set => {
|
||||
const dispose = blockSuiteWorkspace.meta.commonFieldsUpdated.on(() => {
|
||||
set(blockSuiteWorkspace.meta.name ?? '');
|
||||
});
|
||||
return () => {
|
||||
dispose.dispose();
|
||||
};
|
||||
};
|
||||
weakMap.set(blockSuiteWorkspace, writableAtom);
|
||||
nameAtom = writableAtom;
|
||||
} else {
|
||||
nameAtom = weakMap.get(blockSuiteWorkspace) as StringAtom;
|
||||
}
|
||||
return useAtom(nameAtom);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import type { Workspace } from '@blocksuite/store';
|
||||
import type { Atom } from 'jotai';
|
||||
import { atom, useAtomValue } from 'jotai';
|
||||
|
||||
const weakMap = new WeakMap<Workspace, Map<string, Atom<string>>>();
|
||||
|
||||
function getAtom(w: Workspace, pageId: string): Atom<string> {
|
||||
if (!weakMap.has(w)) {
|
||||
weakMap.set(w, new Map());
|
||||
}
|
||||
const map = weakMap.get(w);
|
||||
assertExists(map);
|
||||
if (!map.has(pageId)) {
|
||||
const baseAtom = atom<string>(w.getPage(pageId)?.meta.title || 'Untitled');
|
||||
baseAtom.onMount = set => {
|
||||
const disposable = w.meta.pageMetasUpdated.on(() => {
|
||||
const page = w.getPage(pageId);
|
||||
set(page?.meta.title || 'Untitled');
|
||||
});
|
||||
return () => {
|
||||
disposable.dispose();
|
||||
};
|
||||
};
|
||||
map.set(pageId, baseAtom);
|
||||
return baseAtom;
|
||||
} else {
|
||||
return map.get(pageId) as Atom<string>;
|
||||
}
|
||||
}
|
||||
|
||||
export function useBlockSuiteWorkspacePageTitle(
|
||||
blockSuiteWorkspace: Workspace,
|
||||
pageId: string
|
||||
) {
|
||||
const titleAtom = getAtom(blockSuiteWorkspace, pageId);
|
||||
assertExists(titleAtom);
|
||||
return useAtomValue(titleAtom);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { assertExists, DisposableGroup } from '@blocksuite/global/utils';
|
||||
import type { Page, Workspace } from '@blocksuite/store';
|
||||
import type { Atom } from 'jotai';
|
||||
import { atom, useAtomValue } from 'jotai';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const weakMap = new WeakMap<Workspace, Map<string, Atom<Page | null>>>();
|
||||
|
||||
const emptyAtom = atom<Page | null>(null);
|
||||
|
||||
function getAtom(w: Workspace, pageId: string | null): Atom<Page | null> {
|
||||
if (!pageId) {
|
||||
return emptyAtom;
|
||||
}
|
||||
if (!weakMap.has(w)) {
|
||||
weakMap.set(w, new Map());
|
||||
}
|
||||
const map = weakMap.get(w);
|
||||
assertExists(map);
|
||||
if (!map.has(pageId)) {
|
||||
const baseAtom = atom(w.getPage(pageId));
|
||||
baseAtom.onMount = set => {
|
||||
const group = new DisposableGroup();
|
||||
group.add(
|
||||
w.slots.pageAdded.on(id => {
|
||||
if (pageId === id) {
|
||||
set(w.getPage(id));
|
||||
}
|
||||
})
|
||||
);
|
||||
group.add(
|
||||
w.slots.pageRemoved.on(id => {
|
||||
if (pageId === id) {
|
||||
set(null);
|
||||
}
|
||||
})
|
||||
);
|
||||
return () => {
|
||||
group.dispose();
|
||||
};
|
||||
};
|
||||
map.set(pageId, baseAtom);
|
||||
return baseAtom;
|
||||
} else {
|
||||
return map.get(pageId) as Atom<Page | null>;
|
||||
}
|
||||
}
|
||||
|
||||
export function useBlockSuiteWorkspacePage(
|
||||
blockSuiteWorkspace: Workspace,
|
||||
pageId: string | null
|
||||
): Page | null {
|
||||
const pageAtom = getAtom(blockSuiteWorkspace, pageId);
|
||||
assertExists(pageAtom);
|
||||
const page = useAtomValue(pageAtom);
|
||||
|
||||
useEffect(() => {
|
||||
if (!page?.loaded) {
|
||||
page?.waitForLoaded().catch(console.error);
|
||||
}
|
||||
}, [page]);
|
||||
|
||||
return page;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { useCallback, useSyncExternalStore } from 'react';
|
||||
import type { DataSourceAdapter, Status } from 'y-provider';
|
||||
|
||||
type UIStatus =
|
||||
| Status
|
||||
| {
|
||||
type: 'unknown';
|
||||
};
|
||||
|
||||
export function useDataSourceStatus(provider: DataSourceAdapter): UIStatus {
|
||||
return useSyncExternalStore(
|
||||
provider.subscribeStatusChange,
|
||||
useCallback(() => provider.status, [provider])
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import 'foxact/use-debounced-state';
|
||||
|
||||
import debounce from 'lodash.debounce';
|
||||
import { type RefObject, useEffect, useState } from 'react';
|
||||
|
||||
export function useIsTinyScreen({
|
||||
mainContainer,
|
||||
leftStatic,
|
||||
leftSlot,
|
||||
centerDom,
|
||||
rightStatic,
|
||||
rightSlot,
|
||||
}: {
|
||||
mainContainer: HTMLElement | null;
|
||||
leftStatic: RefObject<HTMLElement>;
|
||||
leftSlot: RefObject<HTMLElement>[];
|
||||
centerDom: RefObject<HTMLElement>;
|
||||
rightStatic: RefObject<HTMLElement>;
|
||||
rightSlot: RefObject<HTMLElement>[];
|
||||
}) {
|
||||
const [isTinyScreen, setIsTinyScreen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!mainContainer) {
|
||||
return;
|
||||
}
|
||||
const handleResize = debounce(() => {
|
||||
if (!centerDom.current) {
|
||||
return;
|
||||
}
|
||||
const leftStaticWidth = leftStatic.current?.clientWidth || 0;
|
||||
const leftSlotWidth = leftSlot.reduce((accWidth, dom) => {
|
||||
return accWidth + (dom.current?.clientWidth || 0);
|
||||
}, 0);
|
||||
|
||||
const rightStaticWidth = rightStatic.current?.clientWidth || 0;
|
||||
|
||||
const rightSlotWidth = rightSlot.reduce((accWidth, dom) => {
|
||||
return accWidth + (dom.current?.clientWidth || 0);
|
||||
}, 0);
|
||||
|
||||
if (!leftSlotWidth && !rightSlotWidth) {
|
||||
if (isTinyScreen) {
|
||||
setIsTinyScreen(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const containerRect = mainContainer.getBoundingClientRect();
|
||||
const centerRect = centerDom.current.getBoundingClientRect();
|
||||
|
||||
if (
|
||||
leftStaticWidth + leftSlotWidth + containerRect.left >=
|
||||
centerRect.left ||
|
||||
containerRect.right - centerRect.right <=
|
||||
rightSlotWidth + rightStaticWidth
|
||||
) {
|
||||
setIsTinyScreen(true);
|
||||
} else {
|
||||
setIsTinyScreen(false);
|
||||
}
|
||||
}, 100);
|
||||
|
||||
handleResize();
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
handleResize();
|
||||
});
|
||||
|
||||
resizeObserver.observe(mainContainer);
|
||||
|
||||
return () => {
|
||||
resizeObserver.unobserve(mainContainer);
|
||||
};
|
||||
}, [
|
||||
centerDom,
|
||||
isTinyScreen,
|
||||
leftSlot,
|
||||
leftStatic,
|
||||
mainContainer,
|
||||
rightSlot,
|
||||
rightStatic,
|
||||
]);
|
||||
|
||||
return isTinyScreen;
|
||||
}
|
||||
Reference in New Issue
Block a user