feat(core): view selfhosted shared docs in electron (#9365)
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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])
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user