feat(core): view selfhosted shared docs in electron (#9365)

This commit is contained in:
EYHN
2024-12-27 05:32:22 +00:00
parent 2b27d62b0e
commit 893493cf01
6 changed files with 135 additions and 83 deletions

View File

@@ -4,8 +4,8 @@ import { workbenchRoutes } from '@affine/core/desktop/workbench-router';
import { import {
DefaultServerService, DefaultServerService,
ServersService, ServersService,
WorkspaceServerService,
} from '@affine/core/modules/cloud'; } from '@affine/core/modules/cloud';
import { GlobalDialogService } from '@affine/core/modules/dialogs';
import { DndService } from '@affine/core/modules/dnd/services'; import { DndService } from '@affine/core/modules/dnd/services';
import { GlobalContextService } from '@affine/core/modules/global-context'; import { GlobalContextService } from '@affine/core/modules/global-context';
import { import {
@@ -33,7 +33,6 @@ import { AffineErrorBoundary } from '../../../components/affine/affine-error-bou
import { WorkbenchRoot } from '../../../modules/workbench'; import { WorkbenchRoot } from '../../../modules/workbench';
import { AppContainer } from '../../components/app-container'; import { AppContainer } from '../../components/app-container';
import { PageNotFound } from '../404'; import { PageNotFound } from '../404';
import { SignIn } from '../auth/sign-in';
import { WorkspaceLayout } from './layouts/workspace-layout'; import { WorkspaceLayout } from './layouts/workspace-layout';
import { SharePage } from './share/share-page'; import { SharePage } from './share/share-page';
@@ -53,8 +52,18 @@ declare global {
} }
export const Component = (): ReactElement => { export const Component = (): ReactElement => {
const { workspacesService } = useServices({ const {
workspacesService,
globalDialogService,
serversService,
defaultServerService,
globalContextService,
} = useServices({
WorkspacesService, WorkspacesService,
GlobalDialogService,
ServersService,
DefaultServerService,
GlobalContextService,
}); });
const params = useParams(); const params = useParams();
@@ -88,11 +97,6 @@ export const Component = (): ReactElement => {
const [workspaceNotFound, setWorkspaceNotFound] = useState(false); const [workspaceNotFound, setWorkspaceNotFound] = useState(false);
const listLoading = useLiveData(workspacesService.list.isRevalidating$); const listLoading = useLiveData(workspacesService.list.isRevalidating$);
const workspaces = useLiveData(workspacesService.list.workspaces$); const workspaces = useLiveData(workspacesService.list.workspaces$);
const serversService = useService(ServersService);
const serverFromSearchParams = searchParams.get('server');
const serverNotFound = serverFromSearchParams
? serversService.getServerByBaseUrl(serverFromSearchParams)
: null;
const meta = useMemo(() => { const meta = useMemo(() => {
return workspaces.find(({ id }) => id === params.workspaceId); return workspaces.find(({ id }) => id === params.workspaceId);
}, [workspaces, params.workspaceId]); }, [workspaces, params.workspaceId]);
@@ -125,36 +129,87 @@ export const Component = (): ReactElement => {
return; return;
}, [listLoading, meta, workspaceNotFound, workspacesService]); }, [listLoading, meta, workspaceNotFound, workspacesService]);
if (workspaceNotFound) { // server search params
if (BUILD_CONFIG.isElectron && serverNotFound) { const serverFromSearchParams = useLiveData(
const url = new URL(window.location.href); searchParams.has('server')
url.searchParams.delete('server'); ? serversService.serverByBaseUrl$(searchParams.get('server') as string)
const redirectUrl = url.toString().replace(window.location.origin, ''); : undefined
return ( );
<AffineOtherPageLayout> // server from workspace
<SignIn redirectUrl={redirectUrl} /> const serverFromWorkspace = useLiveData(
</AffineOtherPageLayout> meta?.flavour && meta.flavour !== 'local'
); ? serversService.server$(meta?.flavour)
: undefined
);
const server = serverFromWorkspace ?? serverFromSearchParams;
useEffect(() => {
if (server) {
globalContextService.globalContext.serverId.set(server.id);
return () => {
globalContextService.globalContext.serverId.set(
defaultServerService.server.id
);
};
} }
return;
}, [
defaultServerService.server.id,
globalContextService.globalContext.serverId,
server,
]);
// if server is not found, and we have server in search params, we should show add selfhosted dialog
const needAddSelfhosted = server === undefined && searchParams.has('server');
// use ref to avoid useEffect trigger twice
const addSelfhostedDialogOpened = useRef<boolean>(false);
useEffect(() => {
if (addSelfhostedDialogOpened.current) {
return;
}
addSelfhostedDialogOpened.current = true;
if (BUILD_CONFIG.isElectron && needAddSelfhosted) {
globalDialogService.open('sign-in', {
server: searchParams.get('server') as string,
});
}
return;
}, [
globalDialogService,
needAddSelfhosted,
searchParams,
serverFromSearchParams,
]);
if (workspaceNotFound) {
if (detailDocRoute) { if (detailDocRoute) {
return ( return (
<SharePage <FrameworkScope scope={server?.scope}>
docId={detailDocRoute.docId} <SharePage
workspaceId={detailDocRoute.workspaceId} docId={detailDocRoute.docId}
/> workspaceId={detailDocRoute.workspaceId}
/>
</FrameworkScope>
); );
} }
return ( return (
<AffineOtherPageLayout> <FrameworkScope scope={server?.scope}>
<PageNotFound noPermission /> <AffineOtherPageLayout>
</AffineOtherPageLayout> <PageNotFound noPermission />
</AffineOtherPageLayout>
</FrameworkScope>
); );
} }
if (!meta) { if (!meta) {
return <AppContainer fallback />; return <AppContainer fallback />;
} }
return <WorkspacePage meta={meta} />; return (
<FrameworkScope scope={server?.scope}>
<WorkspacePage meta={meta} />
</FrameworkScope>
);
}; };
const DNDContextProvider = ({ children }: PropsWithChildren) => { const DNDContextProvider = ({ children }: PropsWithChildren) => {
@@ -171,15 +226,12 @@ const DNDContextProvider = ({ children }: PropsWithChildren) => {
}; };
const WorkspacePage = ({ meta }: { meta: WorkspaceMetadata }) => { const WorkspacePage = ({ meta }: { meta: WorkspaceMetadata }) => {
const { workspacesService, globalContextService, defaultServerService } = const { workspacesService, globalContextService } = useServices({
useServices({ WorkspacesService,
WorkspacesService, GlobalContextService,
GlobalContextService, });
DefaultServerService,
});
const [workspace, setWorkspace] = useState<Workspace | null>(null); const [workspace, setWorkspace] = useState<Workspace | null>(null);
const workspaceServer = workspace?.scope.get(WorkspaceServerService).server;
useLayoutEffect(() => { useLayoutEffect(() => {
const ref = workspacesService.open({ metadata: meta }); const ref = workspacesService.open({ metadata: meta });
@@ -238,30 +290,17 @@ const WorkspacePage = ({ meta }: { meta: WorkspaceMetadata }) => {
}; };
localStorage.setItem('last_workspace_id', workspace.id); localStorage.setItem('last_workspace_id', workspace.id);
globalContextService.globalContext.workspaceId.set(workspace.id); globalContextService.globalContext.workspaceId.set(workspace.id);
if (workspaceServer) {
globalContextService.globalContext.serverId.set(workspaceServer.id);
}
globalContextService.globalContext.workspaceFlavour.set( globalContextService.globalContext.workspaceFlavour.set(
workspace.flavour workspace.flavour
); );
return () => { return () => {
window.currentWorkspace = undefined; window.currentWorkspace = undefined;
globalContextService.globalContext.workspaceId.set(null); globalContextService.globalContext.workspaceId.set(null);
if (workspaceServer) {
globalContextService.globalContext.serverId.set(
defaultServerService.server.id
);
}
globalContextService.globalContext.workspaceFlavour.set(null); globalContextService.globalContext.workspaceFlavour.set(null);
}; };
} }
return; return;
}, [ }, [globalContextService, workspace]);
defaultServerService.server.id,
globalContextService,
workspace,
workspaceServer,
]);
if (!workspace) { if (!workspace) {
return null; // skip this, workspace will be set in layout effect return null; // skip this, workspace will be set in layout effect
@@ -269,27 +308,23 @@ const WorkspacePage = ({ meta }: { meta: WorkspaceMetadata }) => {
if (!isRootDocReady) { if (!isRootDocReady) {
return ( return (
<FrameworkScope scope={workspaceServer?.scope}> <FrameworkScope scope={workspace.scope}>
<FrameworkScope scope={workspace.scope}> <DNDContextProvider>
<DNDContextProvider> <AppContainer fallback />
<AppContainer fallback /> </DNDContextProvider>
</DNDContextProvider>
</FrameworkScope>
</FrameworkScope> </FrameworkScope>
); );
} }
return ( return (
<FrameworkScope scope={workspaceServer?.scope}> <FrameworkScope scope={workspace.scope}>
<FrameworkScope scope={workspace.scope}> <DNDContextProvider>
<DNDContextProvider> <AffineErrorBoundary height="100vh">
<AffineErrorBoundary height="100vh"> <WorkspaceLayout>
<WorkspaceLayout> <WorkbenchRoot />
<WorkbenchRoot /> </WorkspaceLayout>
</WorkspaceLayout> </AffineErrorBoundary>
</AffineErrorBoundary> </DNDContextProvider>
</DNDContextProvider>
</FrameworkScope>
</FrameworkScope> </FrameworkScope>
); );
}; };

View File

@@ -9,6 +9,7 @@ import {
AuthService, AuthService,
FetchService, FetchService,
GraphQLService, GraphQLService,
ServerService,
} from '@affine/core/modules/cloud'; } from '@affine/core/modules/cloud';
import { type Doc, DocsService } from '@affine/core/modules/doc'; import { type Doc, DocsService } from '@affine/core/modules/doc';
import { import {
@@ -64,8 +65,9 @@ export const SharePage = ({
workspaceId: string; workspaceId: string;
docId: string; docId: string;
}) => { }) => {
const { shareReaderService } = useServices({ const { shareReaderService, serverService } = useServices({
ShareReaderService, ShareReaderService,
ServerService,
}); });
const isLoading = useLiveData(shareReaderService.reader.isLoading$); const isLoading = useLiveData(shareReaderService.reader.isLoading$);
@@ -104,8 +106,12 @@ export const SharePage = ({
}, [location.search]); }, [location.search]);
useEffect(() => { useEffect(() => {
shareReaderService.reader.loadShare({ workspaceId, docId }); shareReaderService.reader.loadShare({
}, [shareReaderService, docId, workspaceId]); serverId: serverService.server.id,
workspaceId,
docId,
});
}, [shareReaderService, docId, workspaceId, serverService.server.id]);
let element: ReactNode = null; let element: ReactNode = null;
if (isLoading) { if (isLoading) {

View File

@@ -55,6 +55,12 @@ export class ServersService extends Service {
); );
} }
serverByBaseUrl$(url: string) {
return this.servers$.map(servers =>
servers.find(server => server.baseUrl === url)
);
}
private readonly serverPool = new ObjectPool<string, Server>({ private readonly serverPool = new ObjectPool<string, Server>({
onDelete(obj) { onDelete(obj) {
obj.dispose(); obj.dispose();

View File

@@ -31,8 +31,18 @@ export class ShareReader extends Entity {
loadShare = effect( loadShare = effect(
switchMap( switchMap(
({ workspaceId, docId }: { workspaceId: string; docId: string }) => { ({
return fromPromise(this.store.loadShare(workspaceId, docId)).pipe( serverId,
workspaceId,
docId,
}: {
serverId: string;
workspaceId: string;
docId: string;
}) => {
return fromPromise(
this.store.loadShare(serverId, workspaceId, docId)
).pipe(
mergeMap(data => { mergeMap(data => {
if (!data) { if (!data) {
this.data$.next(null); this.data$.next(null);

View File

@@ -5,7 +5,7 @@ export { ShareReaderService } from './services/share-reader';
import { type Framework } from '@toeverything/infra'; import { type Framework } from '@toeverything/infra';
import { RawFetchProvider, WorkspaceServerService } from '../cloud'; import { ServersService, WorkspaceServerService } from '../cloud';
import { DocScope, DocService } from '../doc'; import { DocScope, DocService } from '../doc';
import { import {
WorkspaceLocalCache, WorkspaceLocalCache,
@@ -26,7 +26,7 @@ export function configureShareDocsModule(framework: Framework) {
framework framework
.service(ShareReaderService) .service(ShareReaderService)
.entity(ShareReader, [ShareReaderStore]) .entity(ShareReader, [ShareReaderStore])
.store(ShareReaderStore, [RawFetchProvider]) .store(ShareReaderStore, [ServersService])
.scope(WorkspaceScope) .scope(WorkspaceScope)
.service(ShareDocsListService, [WorkspaceService]) .service(ShareDocsListService, [WorkspaceService])
.store(ShareDocsStore, [WorkspaceServerService]) .store(ShareDocsStore, [WorkspaceServerService])

View File

@@ -2,36 +2,31 @@ import { ErrorNames, UserFriendlyError } from '@affine/graphql';
import type { DocMode } from '@blocksuite/affine/blocks'; import type { DocMode } from '@blocksuite/affine/blocks';
import { Store } from '@toeverything/infra'; import { Store } from '@toeverything/infra';
import type { RawFetchProvider } from '../../cloud'; import type { ServersService } from '../../cloud';
import { isBackendError } from '../../cloud'; import { isBackendError } from '../../cloud';
export class ShareReaderStore extends Store { export class ShareReaderStore extends Store {
constructor(private readonly rawFetch?: RawFetchProvider) { constructor(private readonly serversService: ServersService) {
super(); super();
} }
async loadShare(workspaceId: string, docId: string) { async loadShare(serverId: string, workspaceId: string, docId: string) {
if (!this.rawFetch) { const server = this.serversService.server$(serverId).value;
throw new Error('No Fetch Service'); if (!server) {
throw new Error(`Server ${serverId} not found`);
} }
try { try {
const docResponse = await this.rawFetch.fetch( const docResponse = await server.fetch(
`/api/workspaces/${workspaceId}/docs/${docId}` `/api/workspaces/${workspaceId}/docs/${docId}`
); );
if (docResponse.status !== 200) {
throw new Error('Failed to fetch workspace');
}
const publishMode = docResponse.headers.get( const publishMode = docResponse.headers.get(
'publish-mode' 'publish-mode'
) as DocMode | null; ) as DocMode | null;
const docBinary = await docResponse.arrayBuffer(); const docBinary = await docResponse.arrayBuffer();
const workspaceResponse = await this.rawFetch.fetch( const workspaceResponse = await server.fetch(
`/api/workspaces/${workspaceId}/docs/${workspaceId}` `/api/workspaces/${workspaceId}/docs/${workspaceId}`
); );
if (workspaceResponse.status !== 200) {
throw new Error('Failed to fetch workspace');
}
const workspaceBinary = await workspaceResponse.arrayBuffer(); const workspaceBinary = await workspaceResponse.arrayBuffer();
return { return {