mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-21 20:16:26 +08:00
feat(infra): framework
This commit is contained in:
@@ -1,146 +0,0 @@
|
||||
/**
|
||||
* @vitest-environment happy-dom
|
||||
*/
|
||||
import { uploadAvatarMutation } from '@affine/graphql';
|
||||
import { render } from '@testing-library/react';
|
||||
import type { Mock } from 'vitest';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { useMutation } from '../use-mutation';
|
||||
import { useQuery } from '../use-query';
|
||||
|
||||
let fetch: Mock;
|
||||
describe('GraphQL wrapper for SWR', () => {
|
||||
beforeEach(() => {
|
||||
fetch = vi.fn(() =>
|
||||
Promise.resolve(
|
||||
new Response(JSON.stringify({ data: { hello: 1 } }), {
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
vi.stubGlobal('fetch', fetch);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fetch.mockReset();
|
||||
});
|
||||
|
||||
describe('useQuery', () => {
|
||||
const Component = ({ id }: { id: number }) => {
|
||||
const { data, isLoading, error } = useQuery({
|
||||
query: {
|
||||
id: 'query',
|
||||
query: `
|
||||
query {
|
||||
hello
|
||||
}
|
||||
`,
|
||||
operationName: 'query',
|
||||
definitionName: 'query',
|
||||
},
|
||||
// @ts-expect-error forgive the fake variables
|
||||
variables: { id },
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return <div>loading</div>;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <div>error</div>;
|
||||
}
|
||||
|
||||
// @ts-expect-error
|
||||
return <div>number: {data!.hello}</div>;
|
||||
};
|
||||
|
||||
it('should send query correctly', async () => {
|
||||
const component = <Component id={1} />;
|
||||
const renderer = render(component);
|
||||
const el = await renderer.findByText('number: 1');
|
||||
expect(el).toMatchInlineSnapshot(`
|
||||
<div>
|
||||
number:${' '}
|
||||
1
|
||||
</div>
|
||||
`);
|
||||
});
|
||||
|
||||
it('should not send request if cache hit', async () => {
|
||||
const component = <Component id={2} />;
|
||||
const renderer = render(component);
|
||||
expect(fetch).toBeCalledTimes(1);
|
||||
|
||||
renderer.rerender(component);
|
||||
expect(fetch).toBeCalledTimes(1);
|
||||
|
||||
render(<Component id={3} />);
|
||||
|
||||
expect(fetch).toBeCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useMutation', () => {
|
||||
const Component = () => {
|
||||
const { trigger, error, isMutating } = useMutation({
|
||||
mutation: {
|
||||
id: 'mutation',
|
||||
query: `
|
||||
mutation {
|
||||
hello
|
||||
}
|
||||
`,
|
||||
operationName: 'mutation',
|
||||
definitionName: 'mutation',
|
||||
},
|
||||
});
|
||||
|
||||
if (isMutating) {
|
||||
return <div>mutating</div>;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <div>error</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => trigger()}>click</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
it('should trigger mutation', async () => {
|
||||
const component = <Component />;
|
||||
const renderer = render(component);
|
||||
const button = await renderer.findByText('click');
|
||||
|
||||
button.click();
|
||||
expect(fetch).toBeCalledTimes(1);
|
||||
|
||||
renderer.rerender(component);
|
||||
expect(renderer.asFragment()).toMatchInlineSnapshot(`
|
||||
<DocumentFragment>
|
||||
<div>
|
||||
mutating
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`);
|
||||
});
|
||||
|
||||
it('should get rid of generated types', async () => {
|
||||
function _NotActuallyRunDefinedForTypeTesting() {
|
||||
const { trigger } = useMutation({
|
||||
mutation: uploadAvatarMutation,
|
||||
});
|
||||
trigger({
|
||||
avatar: new File([''], 'avatar.png'),
|
||||
});
|
||||
}
|
||||
expect(_NotActuallyRunDefinedForTypeTesting).toBeTypeOf('function');
|
||||
});
|
||||
});
|
||||
});
|
||||
+14
-10
@@ -5,8 +5,12 @@ import 'fake-indexeddb/auto';
|
||||
|
||||
import { configureTestingEnvironment } from '@affine/core/testing';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import type { Workspace } from '@toeverything/infra';
|
||||
import { initEmptyPage, ServiceProviderContext } from '@toeverything/infra';
|
||||
import type { FrameworkProvider, Workspace } from '@toeverything/infra';
|
||||
import {
|
||||
FrameworkRoot,
|
||||
FrameworkScope,
|
||||
initEmptyPage,
|
||||
} from '@toeverything/infra';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
@@ -14,33 +18,33 @@ import { useBlockSuiteDocMeta } from '../use-block-suite-page-meta';
|
||||
import { useDocCollectionHelper } from '../use-block-suite-workspace-helper';
|
||||
|
||||
const configureTestingWorkspace = async () => {
|
||||
const { workspace } = await configureTestingEnvironment();
|
||||
const { framework, workspace } = await configureTestingEnvironment();
|
||||
const docCollection = workspace.docCollection;
|
||||
|
||||
initEmptyPage(docCollection.createDoc({ id: 'page1' }));
|
||||
initEmptyPage(docCollection.createDoc({ id: 'page2' }));
|
||||
|
||||
return workspace;
|
||||
return { framework, workspace };
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.useFakeTimers({ toFake: ['requestIdleCallback'] });
|
||||
});
|
||||
|
||||
const getWrapper = (workspace: Workspace) =>
|
||||
const getWrapper = (framework: FrameworkProvider, workspace: Workspace) =>
|
||||
function Provider({ children }: PropsWithChildren) {
|
||||
return (
|
||||
<ServiceProviderContext.Provider value={workspace.services}>
|
||||
{children}
|
||||
</ServiceProviderContext.Provider>
|
||||
<FrameworkRoot framework={framework}>
|
||||
<FrameworkScope scope={workspace.scope}>{children}</FrameworkScope>
|
||||
</FrameworkRoot>
|
||||
);
|
||||
};
|
||||
|
||||
describe('useDocCollectionHelper', () => {
|
||||
test('should create page', async () => {
|
||||
const workspace = await configureTestingWorkspace();
|
||||
const { framework, workspace } = await configureTestingWorkspace();
|
||||
const docCollection = workspace.docCollection;
|
||||
const Wrapper = getWrapper(workspace);
|
||||
const Wrapper = getWrapper(framework, workspace);
|
||||
|
||||
expect(docCollection.meta.docMetas.length).toBe(3);
|
||||
const helperHook = renderHook(() => useDocCollectionHelper(docCollection), {
|
||||
|
||||
+47
-31
@@ -3,12 +3,13 @@
|
||||
*/
|
||||
import 'fake-indexeddb/auto';
|
||||
|
||||
import { WorkspacePropertiesAdapter } from '@affine/core/modules/workspace';
|
||||
import { WorkspacePropertiesAdapter } from '@affine/core/modules/properties';
|
||||
import { render } from '@testing-library/react';
|
||||
import {
|
||||
ServiceProviderContext,
|
||||
FrameworkRoot,
|
||||
FrameworkScope,
|
||||
useService,
|
||||
Workspace,
|
||||
WorkspaceService,
|
||||
} from '@toeverything/infra';
|
||||
import { createStore, Provider } from 'jotai';
|
||||
import { Suspense } from 'react';
|
||||
@@ -20,8 +21,11 @@ import { useDocCollectionPageTitle } from '../use-block-suite-workspace-page-tit
|
||||
const store = createStore();
|
||||
|
||||
const Component = () => {
|
||||
const workspace = useService(Workspace);
|
||||
const title = useDocCollectionPageTitle(workspace.docCollection, 'page0');
|
||||
const workspaceService = useService(WorkspaceService);
|
||||
const title = useDocCollectionPageTitle(
|
||||
workspaceService.workspace.docCollection,
|
||||
'page0'
|
||||
);
|
||||
return <div>title: {title}</div>;
|
||||
};
|
||||
|
||||
@@ -31,42 +35,54 @@ beforeEach(async () => {
|
||||
|
||||
describe('useDocCollectionPageTitle', () => {
|
||||
test('basic', async () => {
|
||||
const { workspace, page } = await configureTestingEnvironment();
|
||||
const { framework, workspace, doc } = await configureTestingEnvironment();
|
||||
const { findByText, rerender } = render(
|
||||
<ServiceProviderContext.Provider value={page.services}>
|
||||
<Provider store={store}>
|
||||
<Suspense fallback="loading">
|
||||
<Component />
|
||||
</Suspense>
|
||||
</Provider>
|
||||
</ServiceProviderContext.Provider>
|
||||
<FrameworkRoot framework={framework}>
|
||||
<FrameworkScope scope={workspace.scope}>
|
||||
<FrameworkScope scope={doc.scope}>
|
||||
<Provider store={store}>
|
||||
<Suspense fallback="loading">
|
||||
<Component />
|
||||
</Suspense>
|
||||
</Provider>
|
||||
</FrameworkScope>
|
||||
</FrameworkScope>
|
||||
</FrameworkRoot>
|
||||
);
|
||||
expect(await findByText('title: Untitled')).toBeDefined();
|
||||
workspace.docCollection.setDocMeta(page.id, { title: '1' });
|
||||
workspace.docCollection.setDocMeta(doc.id, { title: '1' });
|
||||
rerender(
|
||||
<ServiceProviderContext.Provider value={page.services}>
|
||||
<Provider store={store}>
|
||||
<Suspense fallback="loading">
|
||||
<Component />
|
||||
</Suspense>
|
||||
</Provider>
|
||||
</ServiceProviderContext.Provider>
|
||||
<FrameworkRoot framework={framework}>
|
||||
<FrameworkScope scope={workspace.scope}>
|
||||
<FrameworkScope scope={doc.scope}>
|
||||
<Provider store={store}>
|
||||
<Suspense fallback="loading">
|
||||
<Component />
|
||||
</Suspense>
|
||||
</Provider>
|
||||
</FrameworkScope>
|
||||
</FrameworkScope>
|
||||
</FrameworkRoot>
|
||||
);
|
||||
expect(await findByText('title: 1')).toBeDefined();
|
||||
});
|
||||
|
||||
test('journal', async () => {
|
||||
const { workspace, page } = await configureTestingEnvironment();
|
||||
const adapter = workspace.services.get(WorkspacePropertiesAdapter);
|
||||
adapter.setJournalPageDateString(page.id, '2021-01-01');
|
||||
const { framework, workspace, doc } = await configureTestingEnvironment();
|
||||
const adapter = workspace.scope.get(WorkspacePropertiesAdapter);
|
||||
adapter.setJournalPageDateString(doc.id, '2021-01-01');
|
||||
const { findByText } = render(
|
||||
<ServiceProviderContext.Provider value={page.services}>
|
||||
<Provider store={store}>
|
||||
<Suspense fallback="loading">
|
||||
<Component />
|
||||
</Suspense>
|
||||
</Provider>
|
||||
</ServiceProviderContext.Provider>
|
||||
<FrameworkRoot framework={framework}>
|
||||
<FrameworkScope scope={workspace.scope}>
|
||||
<FrameworkScope scope={doc.scope}>
|
||||
<Provider store={store}>
|
||||
<Suspense fallback="loading">
|
||||
<Component />
|
||||
</Suspense>
|
||||
</Provider>
|
||||
</FrameworkScope>
|
||||
</FrameworkScope>
|
||||
</FrameworkRoot>
|
||||
);
|
||||
expect(await findByText('title: Jan 1, 2021')).toBeDefined();
|
||||
});
|
||||
|
||||
@@ -2,18 +2,26 @@ import { toast } from '@affine/component';
|
||||
import type { AllPageListConfig } from '@affine/core/components/page-list';
|
||||
import { FavoriteTag } from '@affine/core/components/page-list';
|
||||
import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta';
|
||||
import { FavoriteItemsAdapter } from '@affine/core/modules/workspace';
|
||||
import { FavoriteItemsAdapter } from '@affine/core/modules/properties';
|
||||
import { ShareDocsService } from '@affine/core/modules/share-doc';
|
||||
import { PublicPageMode } from '@affine/graphql';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type { DocMeta } from '@blocksuite/store';
|
||||
import { useLiveData, useService, Workspace } from '@toeverything/infra';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
|
||||
import { usePageHelper } from '../../components/blocksuite/block-suite-page-list/utils';
|
||||
import { usePublicPages } from './use-is-shared-page';
|
||||
|
||||
export const useAllPageListConfig = () => {
|
||||
const currentWorkspace = useService(Workspace);
|
||||
const { getPublicMode } = usePublicPages(currentWorkspace);
|
||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
||||
const shareDocService = useService(ShareDocsService);
|
||||
const shareDocs = useLiveData(shareDocService.shareDocs.list$);
|
||||
|
||||
useEffect(() => {
|
||||
// TODO: loading & error UI
|
||||
shareDocService.shareDocs.revalidate();
|
||||
}, [shareDocService]);
|
||||
|
||||
const workspace = currentWorkspace.docCollection;
|
||||
const pageMetas = useBlockSuiteDocMeta(workspace);
|
||||
const { isPreferredEdgeless } = usePageHelper(workspace);
|
||||
@@ -48,7 +56,16 @@ export const useAllPageListConfig = () => {
|
||||
return {
|
||||
allPages: pageMetas,
|
||||
isEdgeless: isPreferredEdgeless,
|
||||
getPublicMode,
|
||||
getPublicMode(id) {
|
||||
const mode = shareDocs?.find(shareDoc => shareDoc.id === id)?.mode;
|
||||
if (mode === PublicPageMode.Edgeless) {
|
||||
return 'edgeless';
|
||||
} else if (mode === PublicPageMode.Page) {
|
||||
return 'page';
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
docCollection: currentWorkspace.docCollection,
|
||||
getPage: id => pageMap[id],
|
||||
favoriteRender: page => {
|
||||
@@ -64,8 +81,8 @@ export const useAllPageListConfig = () => {
|
||||
}, [
|
||||
pageMetas,
|
||||
isPreferredEdgeless,
|
||||
getPublicMode,
|
||||
currentWorkspace.docCollection,
|
||||
shareDocs,
|
||||
pageMap,
|
||||
isActive,
|
||||
onToggleFavoritePage,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||
import { useDocMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta';
|
||||
import { useDocCollectionHelper } from '@affine/core/hooks/use-block-suite-workspace-helper';
|
||||
import { CollectionService } from '@affine/core/modules/collection';
|
||||
import { PageRecordList, useService } from '@toeverything/infra';
|
||||
import { DocsService, useService } from '@toeverything/infra';
|
||||
import { useCallback } from 'react';
|
||||
import { applyUpdate, encodeStateAsUpdate } from 'yjs';
|
||||
|
||||
@@ -17,7 +17,7 @@ export function useBlockSuiteMetaHelper(docCollection: DocCollection) {
|
||||
const { createDoc } = useDocCollectionHelper(docCollection);
|
||||
const { openPage } = useNavigateHelper();
|
||||
const collectionService = useService(CollectionService);
|
||||
const pageRecordList = useService(PageRecordList);
|
||||
const pageRecordList = useService(DocsService).list;
|
||||
|
||||
// TODO-Doma
|
||||
// "Remove" may cause ambiguity here. Consider renaming as "moveToTrash".
|
||||
@@ -85,7 +85,7 @@ export function useBlockSuiteMetaHelper(docCollection: DocCollection) {
|
||||
|
||||
const duplicate = useAsyncCallback(
|
||||
async (pageId: string, openPageAfterDuplication: boolean = true) => {
|
||||
const currentPageMode = pageRecordList.record$(pageId).value?.mode$.value;
|
||||
const currentPageMode = pageRecordList.doc$(pageId).value?.mode$.value;
|
||||
const currentPageMeta = getDocMeta(pageId);
|
||||
const newPage = createDoc();
|
||||
const currentPage = docCollection.getDoc(pageId);
|
||||
@@ -109,9 +109,7 @@ export function useBlockSuiteMetaHelper(docCollection: DocCollection) {
|
||||
const newPageTitle =
|
||||
currentPageMeta.title.replace(lastDigitRegex, '') + `(${newNumber})`;
|
||||
|
||||
pageRecordList
|
||||
.record$(newPage.id)
|
||||
.value?.setMode(currentPageMode || 'page');
|
||||
pageRecordList.doc$(newPage.id).value?.setMode(currentPageMode || 'page');
|
||||
setDocTitle(newPage.id, newPageTitle);
|
||||
openPageAfterDuplication && openPage(docCollection.id, newPage.id);
|
||||
},
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import { allBlobSizesQuery, SubscriptionPlan } from '@affine/graphql';
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import bytes from 'bytes';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { useQuery } from '../use-query';
|
||||
import { useUserQuota } from '../use-quota';
|
||||
import { useUserSubscription } from '../use-subscription';
|
||||
|
||||
/**
|
||||
* Hook to get currentUser's cloud storage usage information.
|
||||
*/
|
||||
export const useCloudStorageUsage = () => {
|
||||
const { data } = useQuery({
|
||||
query: allBlobSizesQuery,
|
||||
});
|
||||
|
||||
const quota = useUserQuota();
|
||||
const [subscription] = useUserSubscription();
|
||||
|
||||
const plan = subscription?.plan ?? SubscriptionPlan.Free;
|
||||
|
||||
const maxLimit = useMemo(() => {
|
||||
if (quota) {
|
||||
return quota.storageQuota;
|
||||
}
|
||||
return bytes.parse(plan === SubscriptionPlan.Free ? '10GB' : '100GB');
|
||||
}, [plan, quota]);
|
||||
|
||||
const used = data?.collectAllBlobSizes?.size ?? 0;
|
||||
const percent = Math.min(
|
||||
100,
|
||||
Math.max(0.5, Number(((used / maxLimit) * 100).toFixed(4)))
|
||||
);
|
||||
const color = percent > 80 ? cssVar('errorColor') : cssVar('processingColor');
|
||||
|
||||
const usedText = bytes.format(used);
|
||||
const maxLimitText = bytes.format(maxLimit);
|
||||
|
||||
return {
|
||||
/** Current subscription plan of logged in user */
|
||||
plan,
|
||||
/** Used storage in bytes */
|
||||
used,
|
||||
/** Formatted used storage */
|
||||
usedText,
|
||||
/** CSS variable name for progress bar color */
|
||||
color,
|
||||
/** Percentage of storage used */
|
||||
percent,
|
||||
/** Maximum storage limit in bytes */
|
||||
maxLimit,
|
||||
/** Formatted maximum storage limit */
|
||||
maxLimitText,
|
||||
};
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
import { useSession } from './use-current-user';
|
||||
|
||||
export function useCurrentLoginStatus() {
|
||||
const session = useSession();
|
||||
return session.status;
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import { getBaseUrl } from '@affine/graphql';
|
||||
import { AIProvider } from '@blocksuite/presets';
|
||||
import { useEffect, useMemo, useReducer } from 'react';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import { SessionFetchErrorRightAfterLoginOrSignUp } from '../../unexpected-application-state/errors';
|
||||
import { useAsyncCallback } from '../affine-async-hooks';
|
||||
|
||||
const logger = new DebugLogger('auth');
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
hasPassword: boolean;
|
||||
avatarUrl: string | null;
|
||||
emailVerified: string | null;
|
||||
}
|
||||
|
||||
export interface Session {
|
||||
user?: User | null;
|
||||
status: 'authenticated' | 'unauthenticated' | 'loading';
|
||||
reload: () => Promise<void>;
|
||||
}
|
||||
|
||||
export type CheckedUser = Session['user'] & {
|
||||
update: (changes?: Partial<User>) => void;
|
||||
};
|
||||
|
||||
export async function getSession(
|
||||
url: string = getBaseUrl() + '/api/auth/session'
|
||||
) {
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
|
||||
if (res.ok) {
|
||||
return (await res.json()) as { user?: User | null };
|
||||
}
|
||||
|
||||
logger.error('Failed to fetch session', res.statusText);
|
||||
throw new Error('Failed to fetch session');
|
||||
} catch (e) {
|
||||
logger.error('Failed to fetch session', e);
|
||||
throw new Error('Failed to fetch session');
|
||||
}
|
||||
}
|
||||
|
||||
export function useSession(): Session {
|
||||
const {
|
||||
data,
|
||||
mutate,
|
||||
isLoading,
|
||||
error: _error, // use error here to avoid uncaught error in the console
|
||||
} = useSWR('session', () => getSession(), {
|
||||
errorRetryCount: 3,
|
||||
errorRetryInterval: 500,
|
||||
shouldRetryOnError: true,
|
||||
suspense: false,
|
||||
});
|
||||
|
||||
return {
|
||||
user: data?.user,
|
||||
status: isLoading
|
||||
? 'loading'
|
||||
: data?.user
|
||||
? 'authenticated'
|
||||
: 'unauthenticated',
|
||||
reload: async () => {
|
||||
return mutate().then(e => {
|
||||
console.error(e);
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
type UpdateSessionAction =
|
||||
| {
|
||||
type: 'update';
|
||||
payload?: Partial<User>;
|
||||
}
|
||||
| {
|
||||
type: 'fetchError';
|
||||
payload: null;
|
||||
};
|
||||
|
||||
function updateSessionReducer(prevState: User, action: UpdateSessionAction) {
|
||||
const { type, payload } = action;
|
||||
switch (type) {
|
||||
case 'update':
|
||||
return { ...prevState, ...payload };
|
||||
case 'fetchError':
|
||||
return prevState;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This hook checks if the user is logged in.
|
||||
* If so, the user object will be cached and returned.
|
||||
* If not, and there is no cache, it will throw an error.
|
||||
* If network error or API response error, it will use the cached value.
|
||||
*/
|
||||
export function useCurrentUser(): CheckedUser {
|
||||
const session = useSession();
|
||||
|
||||
const [user, dispatcher] = useReducer(
|
||||
updateSessionReducer,
|
||||
session.user,
|
||||
firstSession => {
|
||||
if (!firstSession) {
|
||||
// barely possible.
|
||||
// login succeed but the session request failed then.
|
||||
// also need a error boundary to handle this error.
|
||||
throw new SessionFetchErrorRightAfterLoginOrSignUp(
|
||||
'Fetching session failed',
|
||||
() => {
|
||||
getSession()
|
||||
.then(session => {
|
||||
if (session.user) {
|
||||
dispatcher({
|
||||
type: 'update',
|
||||
payload: session.user,
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return firstSession;
|
||||
}
|
||||
);
|
||||
|
||||
const update = useAsyncCallback(
|
||||
async (changes?: Partial<User>) => {
|
||||
dispatcher({
|
||||
type: 'update',
|
||||
payload: changes,
|
||||
});
|
||||
|
||||
await session.reload();
|
||||
},
|
||||
[dispatcher, session]
|
||||
);
|
||||
|
||||
// update user when session reloaded
|
||||
// maybe lift user state up to global state?
|
||||
useEffect(() => {
|
||||
if (session.user) {
|
||||
const user = session.user;
|
||||
dispatcher({ type: 'update', payload: user });
|
||||
// todo: move this to a better place!
|
||||
AIProvider.provide('userInfo', () => {
|
||||
return user;
|
||||
});
|
||||
} else {
|
||||
dispatcher({ type: 'fetchError', payload: null });
|
||||
}
|
||||
}, [
|
||||
session.user,
|
||||
session.user?.id,
|
||||
session.user?.name,
|
||||
session.user?.avatarUrl,
|
||||
]);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
...user,
|
||||
update,
|
||||
}),
|
||||
// only list the things will change as deps
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[user.id, user.avatarUrl, user.name, update]
|
||||
);
|
||||
}
|
||||
@@ -1,13 +1,15 @@
|
||||
import { AuthService } from '@affine/core/modules/cloud';
|
||||
import type { DeleteCollectionInfo } from '@affine/env/filter';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { useSession } from './use-current-user';
|
||||
|
||||
export const useDeleteCollectionInfo = () => {
|
||||
const { user } = useSession();
|
||||
const authService = useService(AuthService);
|
||||
|
||||
const user = useLiveData(authService.session.account$);
|
||||
|
||||
return useMemo<DeleteCollectionInfo | null>(
|
||||
() => (user ? { userName: user.name, userId: user.id } : null),
|
||||
() => (user ? { userName: user.label, userId: user.id } : null),
|
||||
[user]
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useLiveData, useService, Workspace } from '@toeverything/infra';
|
||||
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export function useDocEngineStatus() {
|
||||
const workspace = useService(Workspace);
|
||||
const workspace = useService(WorkspaceService).workspace;
|
||||
|
||||
const engineState = useLiveData(
|
||||
workspace.engine.docEngineState$.throttleTime(100)
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import { useConfirmModal } from '@affine/component';
|
||||
import { authAtom } from '@affine/core/atoms';
|
||||
import { setOnceSignedInEventAtom } from '@affine/core/atoms/event';
|
||||
import { AuthService } from '@affine/core/modules/cloud';
|
||||
import { WorkspaceSubPath } from '@affine/core/shared';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type { Workspace } from '@toeverything/infra';
|
||||
import { useService, WorkspaceManager } from '@toeverything/infra';
|
||||
import {
|
||||
useLiveData,
|
||||
useService,
|
||||
WorkspacesService,
|
||||
} from '@toeverything/infra';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useNavigateHelper } from '../use-navigate-helper';
|
||||
import { useCurrentLoginStatus } from './use-current-login-status';
|
||||
|
||||
interface ConfirmEnableCloudOptions {
|
||||
/**
|
||||
@@ -26,21 +30,21 @@ type ConfirmEnableArgs = [Workspace, ConfirmEnableCloudOptions | undefined];
|
||||
|
||||
export const useEnableCloud = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const loginStatus = useCurrentLoginStatus();
|
||||
const loginStatus = useLiveData(useService(AuthService).session.status$);
|
||||
const setAuthAtom = useSetAtom(authAtom);
|
||||
const setOnceSignedInEvent = useSetAtom(setOnceSignedInEventAtom);
|
||||
const { openConfirmModal, closeConfirmModal } = useConfirmModal();
|
||||
const workspaceManager = useService(WorkspaceManager);
|
||||
const workspacesService = useService(WorkspacesService);
|
||||
const { openPage } = useNavigateHelper();
|
||||
|
||||
const enableCloud = useCallback(
|
||||
async (ws: Workspace | null, options?: ConfirmEnableCloudOptions) => {
|
||||
if (!ws) return;
|
||||
const { id: newId } = await workspaceManager.transformLocalToCloud(ws);
|
||||
const { id: newId } = await workspacesService.transformLocalToCloud(ws);
|
||||
openPage(newId, options?.openPageId || WorkspaceSubPath.ALL);
|
||||
options?.onSuccess?.();
|
||||
},
|
||||
[openPage, workspaceManager]
|
||||
[openPage, workspacesService]
|
||||
);
|
||||
|
||||
const openSignIn = useCallback(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { toast } from '@affine/component';
|
||||
import { useDocMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta';
|
||||
import { CollectionService } from '@affine/core/modules/collection';
|
||||
import { FavoriteItemsAdapter } from '@affine/core/modules/workspace';
|
||||
import { FavoriteItemsAdapter } from '@affine/core/modules/properties';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type {
|
||||
Active,
|
||||
@@ -9,7 +9,7 @@ import type {
|
||||
Over,
|
||||
UniqueIdentifier,
|
||||
} from '@dnd-kit/core';
|
||||
import { useLiveData, useService, Workspace } from '@toeverything/infra';
|
||||
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { useDeleteCollectionInfo } from './use-delete-collection-info';
|
||||
@@ -153,7 +153,7 @@ export type GlobalDragEndIntent = ReturnType<typeof resolveDragEndIntent>;
|
||||
|
||||
export const useGlobalDNDHelper = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
const currentWorkspace = useService(Workspace);
|
||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
||||
const favAdapter = useService(FavoriteItemsAdapter);
|
||||
const workspace = currentWorkspace.docCollection;
|
||||
const { setTrashModal } = useTrashModalHelper(workspace);
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from '@affine/graphql';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { SingleSelectSelectSolidIcon } from '@blocksuite/icons';
|
||||
import type { PageMode, Workspace } from '@toeverything/infra';
|
||||
import type { DocMode, Workspace } from '@toeverything/infra';
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
@@ -64,10 +64,10 @@ export function useIsSharedPage(
|
||||
pageId: string
|
||||
): {
|
||||
isSharedPage: boolean;
|
||||
changeShare: (mode: PageMode) => void;
|
||||
changeShare: (mode: DocMode) => void;
|
||||
disableShare: () => void;
|
||||
currentShareMode: PageMode;
|
||||
enableShare: (mode: PageMode) => void;
|
||||
currentShareMode: DocMode;
|
||||
enableShare: (mode: DocMode) => void;
|
||||
} {
|
||||
const t = useAFFiNEI18N();
|
||||
const { data, mutate } = useQuery({
|
||||
@@ -90,14 +90,14 @@ export function useIsSharedPage(
|
||||
);
|
||||
const isPageShared = !!publicPage;
|
||||
|
||||
const currentShareMode: PageMode =
|
||||
const currentShareMode: DocMode =
|
||||
publicPage?.mode === PublicPageMode.Edgeless ? 'edgeless' : 'page';
|
||||
|
||||
return [isPageShared, currentShareMode];
|
||||
}, [data?.workspace.publicPages, pageId]);
|
||||
|
||||
const enableShare = useCallback(
|
||||
(mode: PageMode) => {
|
||||
(mode: DocMode) => {
|
||||
const publishMode =
|
||||
mode === 'edgeless' ? PublicPageMode.Edgeless : PublicPageMode.Page;
|
||||
|
||||
@@ -125,7 +125,7 @@ export function useIsSharedPage(
|
||||
);
|
||||
|
||||
const changeShare = useCallback(
|
||||
(mode: PageMode) => {
|
||||
(mode: DocMode) => {
|
||||
const publishMode =
|
||||
mode === 'edgeless' ? PublicPageMode.Edgeless : PublicPageMode.Page;
|
||||
|
||||
@@ -211,7 +211,7 @@ export function usePublicPages(workspace: Workspace) {
|
||||
|
||||
const publicPages: {
|
||||
id: string;
|
||||
mode: PageMode;
|
||||
mode: DocMode;
|
||||
}[] = useMemo(
|
||||
() =>
|
||||
maybeData?.workspace.publicPages.map(i => ({
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { getIsOwnerQuery } from '@affine/graphql';
|
||||
import type { WorkspaceMetadata } from '@toeverything/infra';
|
||||
|
||||
import { useQueryImmutable } from '../use-query';
|
||||
|
||||
export function useIsWorkspaceOwner(workspaceMetadata: WorkspaceMetadata) {
|
||||
const { data } = useQueryImmutable(
|
||||
workspaceMetadata.flavour !== WorkspaceFlavour.LOCAL
|
||||
? {
|
||||
query: getIsOwnerQuery,
|
||||
variables: {
|
||||
workspaceId: workspaceMetadata.id,
|
||||
},
|
||||
}
|
||||
: undefined
|
||||
);
|
||||
|
||||
if (workspaceMetadata.flavour === WorkspaceFlavour.LOCAL) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return data.isOwner;
|
||||
}
|
||||
+20
-20
@@ -1,17 +1,17 @@
|
||||
import { toast } from '@affine/component';
|
||||
import { useDocMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta';
|
||||
import { FavoriteItemsAdapter } from '@affine/core/modules/workspace';
|
||||
import { FavoriteItemsAdapter } from '@affine/core/modules/properties';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import { EdgelessIcon, HistoryIcon, PageIcon } from '@blocksuite/icons';
|
||||
import {
|
||||
Doc,
|
||||
DocService,
|
||||
PreconditionStrategy,
|
||||
registerAffineCommand,
|
||||
useLiveData,
|
||||
useService,
|
||||
Workspace,
|
||||
WorkspaceService,
|
||||
} from '@toeverything/infra';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
@@ -22,30 +22,30 @@ import { useExportPage } from './use-export-page';
|
||||
import { useTrashModalHelper } from './use-trash-modal-helper';
|
||||
|
||||
export function useRegisterBlocksuiteEditorCommands() {
|
||||
const page = useService(Doc);
|
||||
const pageId = page.id;
|
||||
const mode = useLiveData(page.mode$);
|
||||
const doc = useService(DocService).doc;
|
||||
const docId = doc.id;
|
||||
const mode = useLiveData(doc.mode$);
|
||||
const t = useAFFiNEI18N();
|
||||
const workspace = useService(Workspace);
|
||||
const workspace = useService(WorkspaceService).workspace;
|
||||
const docCollection = workspace.docCollection;
|
||||
const { getDocMeta } = useDocMetaHelper(docCollection);
|
||||
const currentPage = docCollection.getDoc(pageId);
|
||||
const currentPage = docCollection.getDoc(docId);
|
||||
assertExists(currentPage);
|
||||
const pageMeta = getDocMeta(pageId);
|
||||
const pageMeta = getDocMeta(docId);
|
||||
assertExists(pageMeta);
|
||||
|
||||
const favAdapter = useService(FavoriteItemsAdapter);
|
||||
const favorite = useLiveData(favAdapter.isFavorite$(pageId, 'doc'));
|
||||
const favorite = useLiveData(favAdapter.isFavorite$(docId, 'doc'));
|
||||
const trash = pageMeta.trash ?? false;
|
||||
|
||||
const setPageHistoryModalState = useSetAtom(pageHistoryModalAtom);
|
||||
|
||||
const openHistoryModal = useCallback(() => {
|
||||
setPageHistoryModalState(() => ({
|
||||
pageId,
|
||||
pageId: docId,
|
||||
open: true,
|
||||
}));
|
||||
}, [pageId, setPageHistoryModalState]);
|
||||
}, [docId, setPageHistoryModalState]);
|
||||
|
||||
const { restoreFromTrash, duplicate } =
|
||||
useBlockSuiteMetaHelper(docCollection);
|
||||
@@ -54,10 +54,10 @@ export function useRegisterBlocksuiteEditorCommands() {
|
||||
const onClickDelete = useCallback(() => {
|
||||
setTrashModal({
|
||||
open: true,
|
||||
pageIds: [pageId],
|
||||
pageIds: [docId],
|
||||
pageTitles: [pageMeta.title],
|
||||
});
|
||||
}, [pageId, pageMeta.title, setTrashModal]);
|
||||
}, [docId, pageMeta.title, setTrashModal]);
|
||||
|
||||
const isCloudWorkspace = workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD;
|
||||
|
||||
@@ -97,7 +97,7 @@ export function useRegisterBlocksuiteEditorCommands() {
|
||||
? t['com.affine.favoritePageOperation.remove']()
|
||||
: t['com.affine.favoritePageOperation.add'](),
|
||||
run() {
|
||||
favAdapter.toggle(pageId, 'doc');
|
||||
favAdapter.toggle(docId, 'doc');
|
||||
toast(
|
||||
favorite
|
||||
? t['com.affine.cmdk.affine.editor.remove-from-favourites']()
|
||||
@@ -121,7 +121,7 @@ export function useRegisterBlocksuiteEditorCommands() {
|
||||
: t['com.affine.pageMode.page']()
|
||||
}`,
|
||||
run() {
|
||||
page.toggleMode();
|
||||
doc.toggleMode();
|
||||
toast(
|
||||
mode === 'page'
|
||||
? t['com.affine.toastMessage.edgelessMode']()
|
||||
@@ -140,7 +140,7 @@ export function useRegisterBlocksuiteEditorCommands() {
|
||||
icon: mode === 'page' ? <PageIcon /> : <EdgelessIcon />,
|
||||
label: t['com.affine.header.option.duplicate'](),
|
||||
run() {
|
||||
duplicate(pageId);
|
||||
duplicate(docId);
|
||||
},
|
||||
})
|
||||
);
|
||||
@@ -219,7 +219,7 @@ export function useRegisterBlocksuiteEditorCommands() {
|
||||
icon: mode === 'page' ? <PageIcon /> : <EdgelessIcon />,
|
||||
label: t['com.affine.cmdk.affine.editor.restore-from-trash'](),
|
||||
run() {
|
||||
restoreFromTrash(pageId);
|
||||
restoreFromTrash(docId);
|
||||
},
|
||||
})
|
||||
);
|
||||
@@ -246,14 +246,14 @@ export function useRegisterBlocksuiteEditorCommands() {
|
||||
mode,
|
||||
onClickDelete,
|
||||
exportHandler,
|
||||
pageId,
|
||||
restoreFromTrash,
|
||||
t,
|
||||
trash,
|
||||
isCloudWorkspace,
|
||||
openHistoryModal,
|
||||
duplicate,
|
||||
page,
|
||||
favAdapter,
|
||||
docId,
|
||||
doc,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
import type { ServerConfigQuery, ServerFeature } from '@affine/graphql';
|
||||
import {
|
||||
getBaseUrl,
|
||||
oauthProvidersQuery,
|
||||
serverConfigQuery,
|
||||
} from '@affine/graphql';
|
||||
import type { BareFetcher, Middleware } from 'swr';
|
||||
|
||||
import { useQueryImmutable } from '../use-query';
|
||||
|
||||
const wrappedFetcher = (fetcher: BareFetcher<any> | null, ...args: any[]) =>
|
||||
fetcher?.(...args).catch(() => null);
|
||||
|
||||
const errorHandler: Middleware = useSWRNext => (key, fetcher, config) => {
|
||||
return useSWRNext(key, wrappedFetcher.bind(null, fetcher), config);
|
||||
};
|
||||
|
||||
const useServerConfig = () => {
|
||||
const { data: config, error } = useQueryImmutable(
|
||||
{ query: serverConfigQuery },
|
||||
{
|
||||
use: [errorHandler],
|
||||
}
|
||||
);
|
||||
|
||||
if (error || !config) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return config.serverConfig;
|
||||
};
|
||||
|
||||
type LowercaseServerFeature = Lowercase<ServerFeature>;
|
||||
type ServerFeatureRecord = {
|
||||
[key in LowercaseServerFeature]: boolean;
|
||||
};
|
||||
|
||||
export const useServerFeatures = (): ServerFeatureRecord => {
|
||||
const config = useServerConfig();
|
||||
|
||||
if (!config) {
|
||||
return {} as ServerFeatureRecord;
|
||||
}
|
||||
|
||||
return Array.from(new Set(config.features)).reduce((acc, cur) => {
|
||||
acc[cur.toLowerCase() as LowercaseServerFeature] = true;
|
||||
return acc;
|
||||
}, {} as ServerFeatureRecord);
|
||||
};
|
||||
|
||||
export const useOAuthProviders = () => {
|
||||
const { data, error } = useQueryImmutable(
|
||||
{ query: oauthProvidersQuery },
|
||||
{
|
||||
use: [errorHandler],
|
||||
}
|
||||
);
|
||||
|
||||
if (error || !data) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return data.serverConfig.oauthProviders;
|
||||
};
|
||||
|
||||
export const useServerBaseUrl = () => {
|
||||
const baseUrl = getBaseUrl();
|
||||
|
||||
if (!baseUrl) {
|
||||
if (environment.isDesktop) {
|
||||
// don't use window.location in electron
|
||||
return null;
|
||||
}
|
||||
const { protocol, hostname, port } = window.location;
|
||||
return `${protocol}//${hostname}${port ? `:${port}` : ''}`;
|
||||
}
|
||||
|
||||
return baseUrl;
|
||||
};
|
||||
|
||||
export const useCredentialsRequirement = () => {
|
||||
const config = useServerConfig();
|
||||
|
||||
if (!config) {
|
||||
return {} as ServerConfigQuery['serverConfig']['credentialsRequirement'];
|
||||
}
|
||||
|
||||
return config.credentialsRequirement;
|
||||
};
|
||||
@@ -1,24 +0,0 @@
|
||||
import { FeatureType, getUserFeaturesQuery } from '@affine/graphql';
|
||||
import type { BareFetcher, Middleware } from 'swr';
|
||||
|
||||
import { useQueryImmutable } from '../use-query';
|
||||
|
||||
const wrappedFetcher = (fetcher: BareFetcher<any> | null, ...args: any[]) =>
|
||||
fetcher?.(...args).catch(() => null);
|
||||
|
||||
const errorHandler: Middleware = useSWRNext => (key, fetcher, config) => {
|
||||
return useSWRNext(key, wrappedFetcher.bind(null, fetcher), config);
|
||||
};
|
||||
|
||||
export function useIsEarlyAccess() {
|
||||
const { data } = useQueryImmutable(
|
||||
{
|
||||
query: getUserFeaturesQuery,
|
||||
},
|
||||
{
|
||||
use: [errorHandler],
|
||||
}
|
||||
);
|
||||
|
||||
return data?.currentUser?.features.includes(FeatureType.EarlyAccess) ?? false;
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
import type { Workspace } from '@toeverything/infra';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import { useDebouncedState } from 'foxact/use-debounced-state';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { WorkspacePropertiesAdapter } from '../modules/workspace/properties';
|
||||
import { WorkspacePropertiesAdapter } from '../modules/properties';
|
||||
|
||||
function getProxy<T extends object>(obj: T) {
|
||||
return new Proxy(obj, {});
|
||||
@@ -38,11 +37,3 @@ export function useCurrentWorkspacePropertiesAdapter() {
|
||||
const adapter = useService(WorkspacePropertiesAdapter);
|
||||
return useReactiveAdapter(adapter);
|
||||
}
|
||||
|
||||
export function useWorkspacePropertiesAdapter(workspace: Workspace) {
|
||||
const adapter = useMemo(
|
||||
() => new WorkspacePropertiesAdapter(workspace),
|
||||
[workspace]
|
||||
);
|
||||
return useReactiveAdapter(adapter);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import type { WorkspaceSubPath } from '@affine/core/shared';
|
||||
import { createContext, useCallback, useContext, useMemo } from 'react';
|
||||
import type { NavigateFunction, NavigateOptions, To } from 'react-router-dom';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useCallback, useContext, useMemo } from 'react';
|
||||
import type { NavigateOptions, To } from 'react-router-dom';
|
||||
|
||||
import { router } from '../router';
|
||||
import { NavigateContext, router } from '../router';
|
||||
|
||||
export enum RouteLogic {
|
||||
REPLACE = 'replace',
|
||||
@@ -11,17 +10,16 @@ export enum RouteLogic {
|
||||
}
|
||||
|
||||
function defaultNavigate(to: To, option?: { replace?: boolean }) {
|
||||
router.navigate(to, option).catch(err => {
|
||||
console.error('Failed to navigate', err);
|
||||
});
|
||||
console.log(to, option);
|
||||
setTimeout(() => {
|
||||
router.navigate(to, option).catch(err => {
|
||||
console.error('Failed to navigate', err);
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
|
||||
export const NavigateContext = createContext<NavigateFunction | null>(null);
|
||||
|
||||
// todo: add a name -> path helper in the results
|
||||
export function useNavigateHelper() {
|
||||
const location = useLocation();
|
||||
|
||||
const navigate = useContext(NavigateContext) ?? defaultNavigate;
|
||||
|
||||
const jumpToPage = useCallback(
|
||||
@@ -89,18 +87,6 @@ export function useNavigateHelper() {
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
const jumpToPublicWorkspacePage = useCallback(
|
||||
(
|
||||
workspaceId: string,
|
||||
pageId: string,
|
||||
logic: RouteLogic = RouteLogic.PUSH
|
||||
) => {
|
||||
return navigate(`/public-workspace/${workspaceId}/${pageId}`, {
|
||||
replace: logic === RouteLogic.REPLACE,
|
||||
});
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
const jumpToSubPath = useCallback(
|
||||
(
|
||||
workspaceId: string,
|
||||
@@ -114,19 +100,11 @@ export function useNavigateHelper() {
|
||||
[navigate]
|
||||
);
|
||||
|
||||
const isPublicWorkspace = useMemo(() => {
|
||||
return location.pathname.indexOf('/public-workspace') === 0;
|
||||
}, [location.pathname]);
|
||||
|
||||
const openPage = useCallback(
|
||||
(workspaceId: string, pageId: string) => {
|
||||
if (isPublicWorkspace) {
|
||||
return jumpToPublicWorkspacePage(workspaceId, pageId);
|
||||
} else {
|
||||
return jumpToPage(workspaceId, pageId);
|
||||
}
|
||||
return jumpToPage(workspaceId, pageId);
|
||||
},
|
||||
[jumpToPage, jumpToPublicWorkspacePage, isPublicWorkspace]
|
||||
[jumpToPage]
|
||||
);
|
||||
|
||||
const jumpToIndex = useCallback(
|
||||
@@ -174,7 +152,6 @@ export function useNavigateHelper() {
|
||||
() => ({
|
||||
jumpToPage,
|
||||
jumpToPageBlock,
|
||||
jumpToPublicWorkspacePage,
|
||||
jumpToSubPath,
|
||||
jumpToIndex,
|
||||
jumpTo404,
|
||||
@@ -189,7 +166,6 @@ export function useNavigateHelper() {
|
||||
[
|
||||
jumpToPage,
|
||||
jumpToPageBlock,
|
||||
jumpToPublicWorkspacePage,
|
||||
jumpToSubPath,
|
||||
jumpToIndex,
|
||||
jumpTo404,
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import { quotaQuery, workspaceQuotaQuery } from '@affine/graphql';
|
||||
|
||||
import { useQuery } from './use-query';
|
||||
|
||||
export const useUserQuota = () => {
|
||||
const { data } = useQuery({
|
||||
query: quotaQuery,
|
||||
});
|
||||
|
||||
return data.currentUser?.quota || null;
|
||||
};
|
||||
|
||||
export const useWorkspaceQuota = (id: string) => {
|
||||
const { data } = useQuery({
|
||||
query: workspaceQuotaQuery,
|
||||
variables: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
|
||||
return data.workspace?.quota || null;
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useService, Workspace } from '@toeverything/infra';
|
||||
import { useService, WorkspaceService } from '@toeverything/infra';
|
||||
import { useStore } from 'jotai';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { useEffect } from 'react';
|
||||
@@ -21,7 +21,7 @@ export function useRegisterWorkspaceCommands() {
|
||||
const store = useStore();
|
||||
const t = useAFFiNEI18N();
|
||||
const theme = useTheme();
|
||||
const currentWorkspace = useService(Workspace);
|
||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
||||
const languageHelper = useLanguageHelper();
|
||||
const pageHelper = usePageHelper(currentWorkspace.docCollection);
|
||||
const navigationHelper = useNavigateHelper();
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||
import type { SubscriptionQuery } from '@affine/graphql';
|
||||
import { SubscriptionPlan, subscriptionQuery } from '@affine/graphql';
|
||||
|
||||
import { useServerFeatures } from './affine/use-server-config';
|
||||
import { useQuery } from './use-query';
|
||||
|
||||
export type Subscription = NonNullable<
|
||||
NonNullable<SubscriptionQuery['currentUser']>['subscriptions'][number]
|
||||
>;
|
||||
|
||||
export type SubscriptionMutator = (update?: Partial<Subscription>) => void;
|
||||
|
||||
const selector = (data: SubscriptionQuery, plan: SubscriptionPlan) =>
|
||||
(data.currentUser?.subscriptions ?? []).find(p => p.plan === plan);
|
||||
|
||||
export const useUserSubscription = (
|
||||
plan: SubscriptionPlan = SubscriptionPlan.Pro
|
||||
) => {
|
||||
const { payment: hasPaymentFeature } = useServerFeatures();
|
||||
const { data, mutate } = useQuery(
|
||||
hasPaymentFeature ? { query: subscriptionQuery } : undefined
|
||||
);
|
||||
|
||||
const set: SubscriptionMutator = useAsyncCallback(
|
||||
async (update?: Partial<Subscription>) => {
|
||||
await mutate(prev => {
|
||||
if (!update || !prev?.currentUser?.subscriptions?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
currentUser: {
|
||||
subscriptions: (prev.currentUser?.subscriptions ?? []).map(sub =>
|
||||
sub.plan !== plan ? sub : { ...sub, ...update }
|
||||
),
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
[mutate, plan]
|
||||
);
|
||||
|
||||
if (!hasPaymentFeature) {
|
||||
return [null, () => {}] as const;
|
||||
}
|
||||
|
||||
return [selector(data, plan), set] as const;
|
||||
};
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { WorkspaceMetadata } from '@toeverything/infra';
|
||||
import { useService, WorkspaceManager } from '@toeverything/infra';
|
||||
import { useService, WorkspacesService } from '@toeverything/infra';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export function useWorkspaceBlobObjectUrl(
|
||||
meta?: WorkspaceMetadata,
|
||||
blobKey?: string | null
|
||||
) {
|
||||
const workspaceManager = useService(WorkspaceManager);
|
||||
const workspacesService = useService(WorkspacesService);
|
||||
|
||||
const [blob, setBlob] = useState<string | undefined>(undefined);
|
||||
|
||||
@@ -17,7 +17,7 @@ export function useWorkspaceBlobObjectUrl(
|
||||
}
|
||||
let canceled = false;
|
||||
let objectUrl: string = '';
|
||||
workspaceManager
|
||||
workspacesService
|
||||
.getWorkspaceBlob(meta, blobKey)
|
||||
.then(blob => {
|
||||
if (blob && !canceled) {
|
||||
@@ -33,7 +33,7 @@ export function useWorkspaceBlobObjectUrl(
|
||||
canceled = true;
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
};
|
||||
}, [meta, blobKey, workspaceManager]);
|
||||
}, [meta, blobKey, workspacesService]);
|
||||
|
||||
return blob;
|
||||
}
|
||||
|
||||
@@ -1,26 +1,29 @@
|
||||
import type { WorkspaceMetadata } from '@toeverything/infra';
|
||||
import { useService, WorkspaceManager } from '@toeverything/infra';
|
||||
import {
|
||||
useLiveData,
|
||||
useService,
|
||||
WorkspacesService,
|
||||
} from '@toeverything/infra';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useWorkspaceBlobObjectUrl } from './use-workspace-blob';
|
||||
|
||||
export function useWorkspaceInfo(meta: WorkspaceMetadata) {
|
||||
const workspaceManager = useService(WorkspaceManager);
|
||||
const workspacesService = useService(WorkspacesService);
|
||||
|
||||
const [information, setInformation] = useState(
|
||||
() => workspaceManager.list.getInformation(meta).info
|
||||
const [profile, setProfile] = useState(() =>
|
||||
workspacesService.getProfile(meta)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const information = workspaceManager.list.getInformation(meta);
|
||||
const profile = workspacesService.getProfile(meta);
|
||||
|
||||
setInformation(information.info);
|
||||
return information.onUpdated.on(info => {
|
||||
setInformation(info);
|
||||
}).dispose;
|
||||
}, [meta, workspaceManager]);
|
||||
profile.revalidate();
|
||||
|
||||
return information;
|
||||
setProfile(profile);
|
||||
}, [meta, workspacesService]);
|
||||
|
||||
return useLiveData(profile.profile$);
|
||||
}
|
||||
|
||||
export function useWorkspaceName(meta: WorkspaceMetadata) {
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import { workspaceQuotaQuery } from '@affine/graphql';
|
||||
import bytes from 'bytes';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useQuery } from './use-query';
|
||||
|
||||
export const useWorkspaceQuota = (workspaceId: string) => {
|
||||
const { data } = useQuery({
|
||||
query: workspaceQuotaQuery,
|
||||
variables: {
|
||||
id: workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
const changeToHumanReadable = useCallback((value: string | number) => {
|
||||
return bytes.format(bytes.parse(value));
|
||||
}, []);
|
||||
|
||||
const quotaData = data.workspace.quota;
|
||||
const humanReadableUsedSize = changeToHumanReadable(
|
||||
quotaData.usedSize.toString()
|
||||
);
|
||||
|
||||
return {
|
||||
blobLimit: quotaData.blobLimit,
|
||||
storageQuota: quotaData.storageQuota,
|
||||
usedSize: quotaData.usedSize,
|
||||
humanReadable: {
|
||||
name: quotaData.humanReadable.name,
|
||||
blobLimit: quotaData.humanReadable.blobLimit,
|
||||
storageQuota: quotaData.humanReadable.storageQuota,
|
||||
usedSize: humanReadableUsedSize,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
import type { Workspace, WorkspaceStatus } from '@toeverything/infra';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export function useWorkspaceStatus<
|
||||
Selector extends ((status: WorkspaceStatus) => any) | undefined | null,
|
||||
Status = Selector extends (status: WorkspaceStatus) => any
|
||||
? ReturnType<Selector>
|
||||
: WorkspaceStatus,
|
||||
>(workspace?: Workspace | null, selector?: Selector): Status | null {
|
||||
// avoid re-render when selector is changed
|
||||
const [cachedSelector] = useState(() => selector);
|
||||
|
||||
const [status, setStatus] = useState<Status | null>(() => {
|
||||
if (!workspace) {
|
||||
return null;
|
||||
}
|
||||
return cachedSelector ? cachedSelector(workspace.status) : workspace.status;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!workspace) {
|
||||
setStatus(null);
|
||||
return;
|
||||
}
|
||||
setStatus(
|
||||
cachedSelector ? cachedSelector(workspace.status) : workspace.status
|
||||
);
|
||||
return workspace.onStatusChange.on(status => {
|
||||
requestAnimationFrame(() => {
|
||||
setStatus(cachedSelector ? cachedSelector(status) : status);
|
||||
});
|
||||
}).dispose;
|
||||
}, [cachedSelector, workspace]);
|
||||
|
||||
return status;
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { Workspace, WorkspaceMetadata } from '@toeverything/infra';
|
||||
import { useService, WorkspaceManager } from '@toeverything/infra';
|
||||
import { useService, WorkspacesService } from '@toeverything/infra';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
/**
|
||||
* definitely be careful when using this hook, open workspace is a heavy operation
|
||||
*/
|
||||
export function useWorkspace(meta?: WorkspaceMetadata | null) {
|
||||
const workspaceManager = useService(WorkspaceManager);
|
||||
const workspaceManager = useService(WorkspacesService);
|
||||
|
||||
const [workspace, setWorkspace] = useState<Workspace | null>(null);
|
||||
|
||||
@@ -15,10 +15,10 @@ export function useWorkspace(meta?: WorkspaceMetadata | null) {
|
||||
setWorkspace(null); // set to null if meta is null or undefined
|
||||
return;
|
||||
}
|
||||
const ref = workspaceManager.open(meta);
|
||||
const ref = workspaceManager.open({ metadata: meta });
|
||||
setWorkspace(ref.workspace);
|
||||
return () => {
|
||||
ref.release();
|
||||
ref.dispose();
|
||||
};
|
||||
}, [meta, workspaceManager]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user