mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-15 00:56:26 +08:00
feat!: affine cloud support (#3813)
Co-authored-by: Hongtao Lye <codert.sn@gmail.com> Co-authored-by: liuyi <forehalo@gmail.com> Co-authored-by: LongYinan <lynweklm@gmail.com> Co-authored-by: X1a0t <405028157@qq.com> Co-authored-by: JimmFly <yangjinfei001@gmail.com> Co-authored-by: Peng Xiao <pengxiao@outlook.com> Co-authored-by: xiaodong zuo <53252747+zuoxiaodong0815@users.noreply.github.com> Co-authored-by: DarkSky <25152247+darkskygit@users.noreply.github.com> Co-authored-by: Qi <474021214@qq.com> Co-authored-by: danielchim <kahungchim@gmail.com>
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
import { useSession } from 'next-auth/react';
|
||||
|
||||
export function useCurrenLoginStatus():
|
||||
| 'authenticated'
|
||||
| 'unauthenticated'
|
||||
| 'loading' {
|
||||
const session = useSession();
|
||||
return session.status;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { DefaultSession } from 'next-auth';
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
import { useSession } from 'next-auth/react';
|
||||
export type CheckedUser = {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
image: string;
|
||||
hasPassword: boolean;
|
||||
update: ReturnType<typeof useSession>['update'];
|
||||
};
|
||||
|
||||
// FIXME: Should this namespace be here?
|
||||
declare module 'next-auth' {
|
||||
interface Session {
|
||||
user: {
|
||||
id: string;
|
||||
hasPassword: boolean;
|
||||
} & DefaultSession['user'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This hook checks if the user is logged in.
|
||||
* If not, it will throw an error.
|
||||
*/
|
||||
export function useCurrentUser(): CheckedUser {
|
||||
const { data: session, status, update } = useSession();
|
||||
// If you are seeing this error, it means that you are not logged in.
|
||||
// This should be prohibited in the development environment, please re-write your component logic.
|
||||
if (status === 'unauthenticated') {
|
||||
throw new Error('session.status should be authenticated');
|
||||
}
|
||||
|
||||
const user = session?.user;
|
||||
|
||||
return {
|
||||
id: user?.id ?? 'REPLACE_ME_DEFAULT_ID',
|
||||
name: user?.name ?? 'REPLACE_ME_DEFAULT_NAME',
|
||||
email: user?.email ?? 'REPLACE_ME_DEFAULT_EMAIL',
|
||||
image: user?.image ?? 'REPLACE_ME_DEFAULT_URL',
|
||||
hasPassword: user?.hasPassword ?? false,
|
||||
update,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { Permission } from '@affine/graphql';
|
||||
import { inviteByEmailMutation } from '@affine/graphql';
|
||||
import { useMutation } from '@affine/workspace/affine/gql';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useMutateCloud } from './use-mutate-cloud';
|
||||
|
||||
export function useInviteMember(workspaceId: string) {
|
||||
const { trigger, isMutating } = useMutation({
|
||||
mutation: inviteByEmailMutation,
|
||||
});
|
||||
const mutate = useMutateCloud();
|
||||
return {
|
||||
invite: useCallback(
|
||||
async (email: string, permission: Permission, sendInviteMail = false) => {
|
||||
const res = await trigger({
|
||||
workspaceId,
|
||||
email,
|
||||
permission,
|
||||
sendInviteMail,
|
||||
});
|
||||
await mutate();
|
||||
// return is successful
|
||||
return res?.invite;
|
||||
},
|
||||
[mutate, trigger, workspaceId]
|
||||
),
|
||||
isMutating,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import {
|
||||
getWorkspaceSharedPagesQuery,
|
||||
revokePageMutation,
|
||||
sharePageMutation,
|
||||
} from '@affine/graphql';
|
||||
import { useMutation, useQuery } from '@affine/workspace/affine/gql';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
export function useIsSharedPage(
|
||||
workspaceId: string,
|
||||
pageId: string
|
||||
): [isSharedPage: boolean, setSharedPage: (enable: boolean) => void] {
|
||||
const { data, mutate } = useQuery({
|
||||
query: getWorkspaceSharedPagesQuery,
|
||||
variables: {
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
const { trigger: enableSharePage } = useMutation({
|
||||
mutation: sharePageMutation,
|
||||
});
|
||||
const { trigger: disableSharePage } = useMutation({
|
||||
mutation: revokePageMutation,
|
||||
});
|
||||
return [
|
||||
useMemo(
|
||||
() => data.workspace.sharedPages.some(id => id === pageId),
|
||||
[data.workspace.sharedPages, pageId]
|
||||
),
|
||||
useCallback(
|
||||
(enable: boolean) => {
|
||||
// todo: push notification
|
||||
if (enable) {
|
||||
enableSharePage({
|
||||
workspaceId,
|
||||
pageId,
|
||||
})
|
||||
.then(() => {
|
||||
return mutate();
|
||||
})
|
||||
.catch(console.error);
|
||||
} else {
|
||||
disableSharePage({
|
||||
workspaceId,
|
||||
pageId,
|
||||
})
|
||||
.then(() => {
|
||||
return mutate();
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
mutate().catch(console.error);
|
||||
},
|
||||
[disableSharePage, enableSharePage, mutate, pageId, workspaceId]
|
||||
),
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { getIsOwnerQuery } from '@affine/graphql';
|
||||
import { useQuery } from '@affine/workspace/affine/gql';
|
||||
|
||||
export function useIsWorkspaceOwner(workspaceId: string) {
|
||||
const { data } = useQuery({
|
||||
query: getIsOwnerQuery,
|
||||
variables: {
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
return data.isOwner;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { leaveWorkspaceMutation } from '@affine/graphql';
|
||||
import { useMutation } from '@affine/workspace/affine/gql';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useAppHelper } from '../use-workspaces';
|
||||
|
||||
export function useLeaveWorkspace() {
|
||||
const { deleteWorkspaceMeta } = useAppHelper();
|
||||
|
||||
const { trigger: leaveWorkspace } = useMutation({
|
||||
mutation: leaveWorkspaceMutation,
|
||||
});
|
||||
|
||||
return useCallback(
|
||||
async (workspaceId: string) => {
|
||||
deleteWorkspaceMeta(workspaceId);
|
||||
await leaveWorkspace({
|
||||
workspaceId,
|
||||
});
|
||||
},
|
||||
[deleteWorkspaceMeta, leaveWorkspace]
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import {
|
||||
type GetMembersByWorkspaceIdQuery,
|
||||
getMembersByWorkspaceIdQuery,
|
||||
} from '@affine/graphql';
|
||||
import { useQuery } from '@affine/workspace/affine/gql';
|
||||
|
||||
export type Member = Omit<
|
||||
GetMembersByWorkspaceIdQuery['workspace']['members'][number],
|
||||
'__typename'
|
||||
>;
|
||||
export function useMembers(workspaceId: string) {
|
||||
const { data } = useQuery({
|
||||
query: getMembersByWorkspaceIdQuery,
|
||||
variables: {
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
return data.workspace.members;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useSWRConfig } from 'swr';
|
||||
|
||||
export function useMutateCloud() {
|
||||
const { mutate } = useSWRConfig();
|
||||
return useCallback(async () => {
|
||||
return mutate(key => {
|
||||
if (Array.isArray(key)) {
|
||||
return key[0] === 'cloud';
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}, [mutate]);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { revokeMemberPermissionMutation } from '@affine/graphql';
|
||||
import { useMutation } from '@affine/workspace/affine/gql';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useMutateCloud } from './use-mutate-cloud';
|
||||
|
||||
export function useRevokeMemberPermission(workspaceId: string) {
|
||||
const mutate = useMutateCloud();
|
||||
const { trigger } = useMutation({
|
||||
mutation: revokeMemberPermissionMutation,
|
||||
});
|
||||
|
||||
return useCallback(
|
||||
async (userId: string) => {
|
||||
await trigger({
|
||||
workspaceId,
|
||||
userId,
|
||||
});
|
||||
await mutate();
|
||||
},
|
||||
[mutate, trigger, workspaceId]
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
'use client';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export function useShareLink(workspaceId: string): string {
|
||||
return useMemo(() => {
|
||||
if (environment.isServer) {
|
||||
throw new Error('useShareLink is not available on server side');
|
||||
}
|
||||
if (environment.isDesktop) {
|
||||
return '???';
|
||||
} else {
|
||||
return origin + '/share/' + workspaceId;
|
||||
}
|
||||
}, [workspaceId]);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { setWorkspacePublicByIdMutation } from '@affine/graphql';
|
||||
import { useMutation } from '@affine/workspace/affine/gql';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useMutateCloud } from './use-mutate-cloud';
|
||||
|
||||
export function useToggleCloudPublic(workspaceId: string) {
|
||||
const mutate = useMutateCloud();
|
||||
const { trigger } = useMutation({
|
||||
mutation: setWorkspacePublicByIdMutation,
|
||||
});
|
||||
return useCallback(
|
||||
async (isPublic: boolean) => {
|
||||
await trigger({
|
||||
id: workspaceId,
|
||||
public: isPublic,
|
||||
});
|
||||
await mutate();
|
||||
},
|
||||
[mutate, trigger, workspaceId]
|
||||
);
|
||||
}
|
||||
@@ -1,34 +1,62 @@
|
||||
import type { WorkspaceRegistry } from '@affine/env/workspace';
|
||||
import type { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { WorkspaceSubPath } from '@affine/env/workspace';
|
||||
import {
|
||||
rootWorkspacesMetadataAtom,
|
||||
workspaceAdaptersAtom,
|
||||
} from '@affine/workspace/atom';
|
||||
import { currentPageIdAtom } from '@toeverything/infra/atom';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { WorkspaceVersion } from '@toeverything/infra/blocksuite';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useTransformWorkspace } from '../use-transform-workspace';
|
||||
import { openSettingModalAtom } from '../../atoms';
|
||||
import { useNavigateHelper } from '../use-navigate-helper';
|
||||
|
||||
export function useOnTransformWorkspace() {
|
||||
const transformWorkspace = useTransformWorkspace();
|
||||
const setWorkspaceId = useSetAtom(currentPageIdAtom);
|
||||
const setSettingModal = useSetAtom(openSettingModalAtom);
|
||||
const WorkspaceAdapters = useAtomValue(workspaceAdaptersAtom);
|
||||
const setMetadata = useSetAtom(rootWorkspacesMetadataAtom);
|
||||
const { openPage } = useNavigateHelper();
|
||||
const currentPageId = useAtomValue(currentPageIdAtom);
|
||||
return useCallback(
|
||||
async <From extends WorkspaceFlavour, To extends WorkspaceFlavour>(
|
||||
from: From,
|
||||
to: To,
|
||||
workspace: WorkspaceRegistry[From]
|
||||
): Promise<void> => {
|
||||
const workspaceId = await transformWorkspace(from, to, workspace);
|
||||
// create first, then delete, in case of failure
|
||||
const newId = await WorkspaceAdapters[to].CRUD.create(
|
||||
workspace.blockSuiteWorkspace
|
||||
);
|
||||
await WorkspaceAdapters[from].CRUD.delete(workspace.blockSuiteWorkspace);
|
||||
setMetadata(workspaces => {
|
||||
const idx = workspaces.findIndex(ws => ws.id === workspace.id);
|
||||
workspaces.splice(idx, 1, {
|
||||
id: newId,
|
||||
flavour: to,
|
||||
version: WorkspaceVersion.SubDoc,
|
||||
});
|
||||
return [...workspaces];
|
||||
}, newId);
|
||||
// fixme(himself65): setting modal could still open and open the non-exist workspace
|
||||
setSettingModal(settings => ({
|
||||
...settings,
|
||||
open: false,
|
||||
}));
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('affine-workspace:transform', {
|
||||
detail: {
|
||||
from,
|
||||
to,
|
||||
oldId: workspace.id,
|
||||
newId: workspaceId,
|
||||
newId: newId,
|
||||
},
|
||||
})
|
||||
);
|
||||
setWorkspaceId(workspaceId);
|
||||
openPage(newId, currentPageId ?? WorkspaceSubPath.ALL);
|
||||
},
|
||||
[setWorkspaceId, transformWorkspace]
|
||||
[WorkspaceAdapters, setMetadata, setSettingModal, openPage, currentPageId]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import type { WorkspaceSubPath } from '@affine/env/workspace';
|
||||
import { useCallback } from 'react';
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
type NavigateOptions,
|
||||
useLocation,
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
useNavigate,
|
||||
} from 'react-router-dom';
|
||||
|
||||
export enum RouteLogic {
|
||||
REPLACE = 'replace',
|
||||
@@ -78,6 +82,26 @@ export function useNavigateHelper() {
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
const jumpToExpired = useCallback(
|
||||
(logic: RouteLogic = RouteLogic.PUSH) => {
|
||||
return navigate('/expired', {
|
||||
replace: logic === RouteLogic.REPLACE,
|
||||
});
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
const jumpToSignIn = useCallback(
|
||||
(
|
||||
logic: RouteLogic = RouteLogic.PUSH,
|
||||
otherOptions?: Omit<NavigateOptions, 'replace'>
|
||||
) => {
|
||||
return navigate('/signIn', {
|
||||
replace: logic === RouteLogic.REPLACE,
|
||||
...otherOptions,
|
||||
});
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
|
||||
return {
|
||||
jumpToPage,
|
||||
@@ -86,5 +110,7 @@ export function useNavigateHelper() {
|
||||
jumpToIndex,
|
||||
jumpTo404,
|
||||
openPage,
|
||||
jumpToExpired,
|
||||
jumpToSignIn,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import type { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import type { WorkspaceRegistry } from '@affine/env/workspace';
|
||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
||||
import { WorkspaceVersion } from '@toeverything/infra/blocksuite';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { WorkspaceAdapters } from '../adapters/workspace';
|
||||
|
||||
/**
|
||||
* Transform workspace from one flavor to another
|
||||
*
|
||||
* The logic here is to delete the old workspace and create a new one.
|
||||
*/
|
||||
export function useTransformWorkspace() {
|
||||
const set = useSetAtom(rootWorkspacesMetadataAtom);
|
||||
return useCallback(
|
||||
async <From extends WorkspaceFlavour, To extends WorkspaceFlavour>(
|
||||
from: From,
|
||||
to: To,
|
||||
workspace: WorkspaceRegistry[From]
|
||||
): Promise<string> => {
|
||||
// create first, then delete, in case of failure
|
||||
const newId = await WorkspaceAdapters[to].CRUD.create(
|
||||
workspace.blockSuiteWorkspace
|
||||
);
|
||||
await WorkspaceAdapters[from].CRUD.delete(workspace as any);
|
||||
set(workspaces => {
|
||||
const idx = workspaces.findIndex(ws => ws.id === workspace.id);
|
||||
workspaces.splice(idx, 1, {
|
||||
id: newId,
|
||||
flavour: to,
|
||||
version: WorkspaceVersion.DatabaseV3,
|
||||
});
|
||||
return [...workspaces];
|
||||
});
|
||||
return newId;
|
||||
},
|
||||
[set]
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
|
||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import type { Workspace } from '@blocksuite/store';
|
||||
@@ -5,8 +6,6 @@ import { useStaticBlockSuiteWorkspace } from '@toeverything/infra/__internal__/r
|
||||
import type { Atom } from 'jotai';
|
||||
import { atom, useAtomValue } from 'jotai';
|
||||
|
||||
import type { AffineOfficialWorkspace } from '../shared';
|
||||
|
||||
const workspaceWeakMap = new WeakMap<
|
||||
Workspace,
|
||||
Atom<Promise<AffineOfficialWorkspace>>
|
||||
@@ -18,7 +17,7 @@ export function useWorkspace(workspaceId: string): AffineOfficialWorkspace {
|
||||
const baseAtom = atom(async get => {
|
||||
const metadata = await get(rootWorkspacesMetadataAtom);
|
||||
const flavour = metadata.find(({ id }) => id === workspaceId)?.flavour;
|
||||
assertExists(flavour);
|
||||
assertExists(flavour, 'workspace flavour not found');
|
||||
return {
|
||||
id: workspaceId,
|
||||
flavour,
|
||||
|
||||
@@ -43,6 +43,21 @@ export function useAppHelper() {
|
||||
},
|
||||
[set]
|
||||
),
|
||||
addCloudWorkspace: useCallback(
|
||||
(workspaceId: string) => {
|
||||
getOrCreateWorkspace(workspaceId, WorkspaceFlavour.AFFINE_CLOUD);
|
||||
set(workspaces => [
|
||||
...workspaces,
|
||||
{
|
||||
id: workspaceId,
|
||||
flavour: WorkspaceFlavour.AFFINE_CLOUD,
|
||||
version: WorkspaceVersion.DatabaseV3,
|
||||
},
|
||||
]);
|
||||
logger.debug('imported cloud workspace', workspaceId);
|
||||
},
|
||||
[set]
|
||||
),
|
||||
createLocalWorkspace: useCallback(
|
||||
async (name: string): Promise<string> => {
|
||||
const blockSuiteWorkspace = getOrCreateWorkspace(
|
||||
@@ -97,5 +112,11 @@ export function useAppHelper() {
|
||||
},
|
||||
[jotaiWorkspaces, set]
|
||||
),
|
||||
deleteWorkspaceMeta: useCallback(
|
||||
(workspaceId: string) => {
|
||||
set(workspaces => workspaces.filter(ws => ws.id !== workspaceId));
|
||||
},
|
||||
[set]
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user