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

View File

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

View File

@@ -31,8 +31,18 @@ export class ShareReader extends Entity {
loadShare = effect(
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 => {
if (!data) {
this.data$.next(null);

View File

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

View File

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