feat: support cross-space page mentions (#1979)
This commit is contained in:
parent
dcc2bacb22
commit
2309d1434b
9 changed files with 103 additions and 71 deletions
|
|
@ -130,6 +130,7 @@
|
||||||
"pages": "pages",
|
"pages": "pages",
|
||||||
"Password": "Password",
|
"Password": "Password",
|
||||||
"Password changed successfully": "Password changed successfully",
|
"Password changed successfully": "Password changed successfully",
|
||||||
|
"People": "People",
|
||||||
"Pending": "Pending",
|
"Pending": "Pending",
|
||||||
"Please confirm your action": "Please confirm your action",
|
"Please confirm your action": "Please confirm your action",
|
||||||
"Preferences": "Preferences",
|
"Preferences": "Preferences",
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,6 @@ export const handlePaste = (
|
||||||
const url = clipboardData.trim();
|
const url = clipboardData.trim();
|
||||||
const { from: pos, empty } = editor.state.selection;
|
const { from: pos, empty } = editor.state.selection;
|
||||||
const match = INTERNAL_LINK_REGEX.exec(url);
|
const match = INTERNAL_LINK_REGEX.exec(url);
|
||||||
const currentPageMatch = INTERNAL_LINK_REGEX.exec(window.location.href);
|
|
||||||
|
|
||||||
// pasted link must be from the same workspace/domain and must not be on a selection
|
// pasted link must be from the same workspace/domain and must not be on a selection
|
||||||
if (!empty || match[2] !== window.location.host) {
|
if (!empty || match[2] !== window.location.host) {
|
||||||
|
|
@ -41,12 +40,6 @@ export const handlePaste = (
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// for now, we only support internal links from the same space
|
|
||||||
// compare space name
|
|
||||||
if (currentPageMatch[4].toLowerCase() !== match[4].toLowerCase()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const anchorId = match[6] ? match[6].split("#")[0] : undefined;
|
const anchorId = match[6] ? match[6].split("#")[0] : undefined;
|
||||||
const urlWithoutAnchor = anchorId
|
const urlWithoutAnchor = anchorId
|
||||||
? url.substring(0, url.indexOf("#"))
|
? url.substring(0, url.indexOf("#"))
|
||||||
|
|
|
||||||
|
|
@ -31,13 +31,17 @@ import {
|
||||||
MentionSuggestionItem,
|
MentionSuggestionItem,
|
||||||
} from "@/features/editor/components/mention/mention.type.ts";
|
} from "@/features/editor/components/mention/mention.type.ts";
|
||||||
import { IPage } from "@/features/page/types/page.types";
|
import { IPage } from "@/features/page/types/page.types";
|
||||||
import { useCreatePageMutation, usePageQuery } from "@/features/page/queries/page-query";
|
import {
|
||||||
|
useCreatePageMutation,
|
||||||
|
usePageQuery,
|
||||||
|
} from "@/features/page/queries/page-query";
|
||||||
import { treeDataAtom } from "@/features/page/tree/atoms/tree-data-atom";
|
import { treeDataAtom } from "@/features/page/tree/atoms/tree-data-atom";
|
||||||
import { SimpleTree } from "react-arborist";
|
import { SimpleTree } from "react-arborist";
|
||||||
import { SpaceTreeNode } from "@/features/page/tree/types";
|
import { SpaceTreeNode } from "@/features/page/tree/types";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useQueryEmit } from "@/features/websocket/use-query-emit";
|
import { useQueryEmit } from "@/features/websocket/use-query-emit";
|
||||||
import { extractPageSlugId } from "@/lib";
|
import { extractPageSlugId } from "@/lib";
|
||||||
|
import { AutoTooltipText } from "@/components/ui/auto-tooltip-text.tsx";
|
||||||
|
|
||||||
const MentionList = forwardRef<any, MentionListProps>((props, ref) => {
|
const MentionList = forwardRef<any, MentionListProps>((props, ref) => {
|
||||||
const [selectedIndex, setSelectedIndex] = useState(1);
|
const [selectedIndex, setSelectedIndex] = useState(1);
|
||||||
|
|
@ -59,11 +63,11 @@ const MentionList = forwardRef<any, MentionListProps>((props, ref) => {
|
||||||
includeUsers: true,
|
includeUsers: true,
|
||||||
includePages: true,
|
includePages: true,
|
||||||
spaceId: space.id,
|
spaceId: space.id,
|
||||||
limit: 10,
|
limit: props.query ? 10 : 5,
|
||||||
preload: true,
|
preload: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const createPageItem = (label: string) : MentionSuggestionItem => {
|
const createPageItem = (label: string): MentionSuggestionItem => {
|
||||||
return {
|
return {
|
||||||
id: null,
|
id: null,
|
||||||
label: label,
|
label: label,
|
||||||
|
|
@ -71,15 +75,15 @@ const MentionList = forwardRef<any, MentionListProps>((props, ref) => {
|
||||||
entityId: null,
|
entityId: null,
|
||||||
slugId: null,
|
slugId: null,
|
||||||
icon: null,
|
icon: null,
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (suggestion && !isLoading) {
|
if (suggestion && !isLoading) {
|
||||||
let items: MentionSuggestionItem[] = [];
|
let items: MentionSuggestionItem[] = [];
|
||||||
|
|
||||||
if (suggestion?.users?.length > 0) {
|
if (suggestion?.users?.length > 0) {
|
||||||
items.push({ entityType: "header", label: t("Users") });
|
items.push({ entityType: "header", label: t("People") });
|
||||||
|
|
||||||
items = items.concat(
|
items = items.concat(
|
||||||
suggestion.users.map((user) => ({
|
suggestion.users.map((user) => ({
|
||||||
|
|
@ -97,11 +101,13 @@ const MentionList = forwardRef<any, MentionListProps>((props, ref) => {
|
||||||
items = items.concat(
|
items = items.concat(
|
||||||
suggestion.pages.map((page) => ({
|
suggestion.pages.map((page) => ({
|
||||||
id: uuid7(),
|
id: uuid7(),
|
||||||
label: page.title || "Untitled",
|
label: page.title || t("Untitled"),
|
||||||
entityType: "page",
|
entityType: "page",
|
||||||
entityId: page.id,
|
entityId: page.id,
|
||||||
slugId: page.slugId,
|
slugId: page.slugId,
|
||||||
icon: page.icon,
|
icon: page.icon,
|
||||||
|
spaceName: page.space?.name,
|
||||||
|
spaceSlug: page.space?.slug,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -129,17 +135,17 @@ const MentionList = forwardRef<any, MentionListProps>((props, ref) => {
|
||||||
creatorId: currentUser?.user.id,
|
creatorId: currentUser?.user.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (item.entityType === "page" && item.id!==null) {
|
if (item.entityType === "page" && item.id !== null) {
|
||||||
props.command({
|
props.command({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
label: item.label || "Untitled",
|
label: item.label || t("Untitled"),
|
||||||
entityType: "page",
|
entityType: "page",
|
||||||
entityId: item.entityId,
|
entityId: item.entityId,
|
||||||
slugId: item.slugId,
|
slugId: item.slugId,
|
||||||
creatorId: currentUser?.user.id,
|
creatorId: currentUser?.user.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (item.entityType === "page" && item.id===null) {
|
if (item.entityType === "page" && item.id === null) {
|
||||||
createPage(item.label);
|
createPage(item.label);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -207,7 +213,7 @@ const MentionList = forwardRef<any, MentionListProps>((props, ref) => {
|
||||||
const payload: { spaceId: string; parentPageId?: string; title: string } = {
|
const payload: { spaceId: string; parentPageId?: string; title: string } = {
|
||||||
spaceId: space.id,
|
spaceId: space.id,
|
||||||
parentPageId: page.id || null,
|
parentPageId: page.id || null,
|
||||||
title: title
|
title: title,
|
||||||
};
|
};
|
||||||
|
|
||||||
let createdPage: IPage;
|
let createdPage: IPage;
|
||||||
|
|
@ -249,11 +255,10 @@ const MentionList = forwardRef<any, MentionListProps>((props, ref) => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}, 50);
|
}, 50);
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new Error("Failed to create page");
|
throw new Error("Failed to create page");
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
viewportRef.current
|
viewportRef.current
|
||||||
|
|
@ -267,15 +272,19 @@ const MentionList = forwardRef<any, MentionListProps>((props, ref) => {
|
||||||
return (
|
return (
|
||||||
<Paper id="mention" shadow="md" py="xs" withBorder radius="md">
|
<Paper id="mention" shadow="md" py="xs" withBorder radius="md">
|
||||||
<Text c="dimmed" size="sm" px="sm">
|
<Text c="dimmed" size="sm" px="sm">
|
||||||
{ t("No results") }
|
{t("No results")}
|
||||||
</Text>
|
</Text>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasUsers = renderItems.some((item) => item.entityType === "user");
|
const hasUsers = renderItems.some((item) => item.entityType === "user");
|
||||||
const hasPages = renderItems.some((item) => item.entityType === "page" && item.id !== null);
|
const hasPages = renderItems.some(
|
||||||
const createPageItemData = renderItems.find((item) => item.entityType === "page" && item.id === null);
|
(item) => item.entityType === "page" && item.id !== null,
|
||||||
|
);
|
||||||
|
const createPageItemData = renderItems.find(
|
||||||
|
(item) => item.entityType === "page" && item.id === null,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper id="mention" shadow="md" withBorder radius="md" py={6}>
|
<Paper id="mention" shadow="md" withBorder radius="md" py={6}>
|
||||||
|
|
@ -283,7 +292,9 @@ const MentionList = forwardRef<any, MentionListProps>((props, ref) => {
|
||||||
viewportRef={viewportRef}
|
viewportRef={viewportRef}
|
||||||
mah={350}
|
mah={350}
|
||||||
w={popupWidth}
|
w={popupWidth}
|
||||||
|
scrollbars={"y"}
|
||||||
scrollbarSize={6}
|
scrollbarSize={6}
|
||||||
|
styles={{ content: { minWidth: 0 } }}
|
||||||
>
|
>
|
||||||
{renderItems?.map((item, index) => {
|
{renderItems?.map((item, index) => {
|
||||||
if (item.entityType === "header") {
|
if (item.entityType === "header") {
|
||||||
|
|
@ -299,6 +310,7 @@ const MentionList = forwardRef<any, MentionListProps>((props, ref) => {
|
||||||
pt={isFirst ? 2 : 4}
|
pt={isFirst ? 2 : 4}
|
||||||
pb={4}
|
pb={4}
|
||||||
tt="uppercase"
|
tt="uppercase"
|
||||||
|
style={{ userSelect: "none" }}
|
||||||
>
|
>
|
||||||
{item.label}
|
{item.label}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
@ -323,9 +335,9 @@ const MentionList = forwardRef<any, MentionListProps>((props, ref) => {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1 }}>
|
||||||
<Text size="sm" fw={500}>
|
<AutoTooltipText size="sm" fw={500}>
|
||||||
{item.label}
|
{item.label}
|
||||||
</Text>
|
</AutoTooltipText>
|
||||||
</div>
|
</div>
|
||||||
</Group>
|
</Group>
|
||||||
</UnstyledButton>
|
</UnstyledButton>
|
||||||
|
|
@ -355,9 +367,14 @@ const MentionList = forwardRef<any, MentionListProps>((props, ref) => {
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
|
|
||||||
<div style={{ flex: 1, minWidth: 0 }}>
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
<Text size="sm" fw={500} truncate>
|
<AutoTooltipText size="sm" fw={500} truncate>
|
||||||
{item.label}
|
{item.label}
|
||||||
|
</AutoTooltipText>
|
||||||
|
{item.spaceName && (
|
||||||
|
<Text size="xs" c="dimmed" truncate>
|
||||||
|
{item.spaceName}
|
||||||
</Text>
|
</Text>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Group>
|
</Group>
|
||||||
</UnstyledButton>
|
</UnstyledButton>
|
||||||
|
|
@ -372,9 +389,12 @@ const MentionList = forwardRef<any, MentionListProps>((props, ref) => {
|
||||||
{(hasUsers || hasPages) && <Divider my={6} />}
|
{(hasUsers || hasPages) && <Divider my={6} />}
|
||||||
<UnstyledButton
|
<UnstyledButton
|
||||||
data-item-index={renderItems.indexOf(createPageItemData)}
|
data-item-index={renderItems.indexOf(createPageItemData)}
|
||||||
onClick={() => selectItem(renderItems.indexOf(createPageItemData))}
|
onClick={() =>
|
||||||
|
selectItem(renderItems.indexOf(createPageItemData))
|
||||||
|
}
|
||||||
className={clsx(classes.menuBtn, {
|
className={clsx(classes.menuBtn, {
|
||||||
[classes.selectedItem]: renderItems.indexOf(createPageItemData) === selectedIndex,
|
[classes.selectedItem]:
|
||||||
|
renderItems.indexOf(createPageItemData) === selectedIndex,
|
||||||
})}
|
})}
|
||||||
px="sm"
|
px="sm"
|
||||||
>
|
>
|
||||||
|
|
@ -388,7 +408,7 @@ const MentionList = forwardRef<any, MentionListProps>((props, ref) => {
|
||||||
<IconPlus size={16} stroke={1.5} />
|
<IconPlus size={16} stroke={1.5} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
|
|
||||||
<div style={{ flex: 1, minWidth: 0 }}>
|
<div style={{ flex: 1, minWidth: 0, overflow: "hidden" }}>
|
||||||
<Text size="sm" fw={500} truncate>
|
<Text size="sm" fw={500} truncate>
|
||||||
{t("Create page")}: {createPageItemData.label}
|
{t("Create page")}: {createPageItemData.label}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
|
||||||
|
|
@ -106,7 +106,7 @@ const mentionRenderItems = () => {
|
||||||
left: `${x}px`,
|
left: `${x}px`,
|
||||||
top: `${y}px`,
|
top: `${y}px`,
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
zIndex: "9999",
|
zIndex: "100",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -54,12 +54,20 @@ export default function MentionView(props: NodeViewProps) {
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{entityType === "page" && (
|
{entityType === "page" && isError && (
|
||||||
|
<Text component="span" c="dimmed" size="sm">
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{entityType === "page" && !isError && (
|
||||||
<Anchor
|
<Anchor
|
||||||
component={Link}
|
component={Link}
|
||||||
fw={500}
|
fw={500}
|
||||||
to={
|
to={
|
||||||
isShareRoute ? shareSlugUrl : buildPageUrl(spaceSlug, slugId, label, anchorId)
|
isShareRoute
|
||||||
|
? shareSlugUrl
|
||||||
|
: buildPageUrl(page?.space?.slug || spaceSlug, slugId, page?.title || label, anchorId)
|
||||||
}
|
}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
underline="never"
|
underline="never"
|
||||||
|
|
|
||||||
|
|
@ -26,4 +26,6 @@ export type MentionSuggestionItem =
|
||||||
entityId: string;
|
entityId: string;
|
||||||
slugId: string;
|
slugId: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
|
spaceName?: string;
|
||||||
|
spaceSlug?: string;
|
||||||
};
|
};
|
||||||
|
|
@ -198,6 +198,7 @@ export class SearchService {
|
||||||
let pageSearch = this.db
|
let pageSearch = this.db
|
||||||
.selectFrom('pages')
|
.selectFrom('pages')
|
||||||
.select(['id', 'slugId', 'title', 'icon', 'spaceId'])
|
.select(['id', 'slugId', 'title', 'icon', 'spaceId'])
|
||||||
|
.select((eb) => this.pageRepo.withSpace(eb))
|
||||||
.where((eb) =>
|
.where((eb) =>
|
||||||
eb(
|
eb(
|
||||||
sql`LOWER(f_unaccent(pages.title))`,
|
sql`LOWER(f_unaccent(pages.title))`,
|
||||||
|
|
@ -209,17 +210,19 @@ export class SearchService {
|
||||||
.where('workspaceId', '=', workspaceId)
|
.where('workspaceId', '=', workspaceId)
|
||||||
.limit(limit);
|
.limit(limit);
|
||||||
|
|
||||||
// only search spaces the user has access to
|
// search all spaces the user has access to, prioritizing the current space
|
||||||
const userSpaceIds = await this.spaceMemberRepo.getUserSpaceIds(userId);
|
const userSpaceIds = await this.spaceMemberRepo.getUserSpaceIds(userId);
|
||||||
|
|
||||||
if (suggestion?.spaceId) {
|
if (userSpaceIds?.length > 0) {
|
||||||
if (userSpaceIds.includes(suggestion.spaceId)) {
|
|
||||||
pageSearch = pageSearch.where('spaceId', '=', suggestion.spaceId);
|
|
||||||
pages = await pageSearch.execute();
|
|
||||||
}
|
|
||||||
} else if (userSpaceIds?.length > 0) {
|
|
||||||
// we need this check or the query will throw an error if the userSpaceIds array is empty
|
|
||||||
pageSearch = pageSearch.where('spaceId', 'in', userSpaceIds);
|
pageSearch = pageSearch.where('spaceId', 'in', userSpaceIds);
|
||||||
|
|
||||||
|
if (suggestion?.spaceId) {
|
||||||
|
pageSearch = pageSearch.orderBy(
|
||||||
|
sql`CASE WHEN pages."space_id" = ${suggestion.spaceId} THEN 0 ELSE 1 END`,
|
||||||
|
'asc',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
pages = await pageSearch.execute();
|
pages = await pageSearch.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -230,7 +233,6 @@ export class SearchService {
|
||||||
await this.pagePermissionRepo.filterAccessiblePageIds({
|
await this.pagePermissionRepo.filterAccessiblePageIds({
|
||||||
pageIds,
|
pageIds,
|
||||||
userId,
|
userId,
|
||||||
spaceId: suggestion?.spaceId,
|
|
||||||
});
|
});
|
||||||
const accessibleSet = new Set(accessibleIds);
|
const accessibleSet = new Set(accessibleIds);
|
||||||
pages = pages.filter((p) => accessibleSet.has(p.id));
|
pages = pages.filter((p) => accessibleSet.has(p.id));
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,4 @@
|
||||||
import {
|
import { Global, Logger, Module, OnApplicationBootstrap } from '@nestjs/common';
|
||||||
Global,
|
|
||||||
Logger,
|
|
||||||
Module,
|
|
||||||
OnApplicationBootstrap,
|
|
||||||
BeforeApplicationShutdown,
|
|
||||||
} from '@nestjs/common';
|
|
||||||
import { InjectKysely, KyselyModule } from 'nestjs-kysely';
|
import { InjectKysely, KyselyModule } from 'nestjs-kysely';
|
||||||
import { EnvironmentService } from '../integrations/environment/environment.service';
|
import { EnvironmentService } from '../integrations/environment/environment.service';
|
||||||
import { CamelCasePlugin, LogEvent, sql } from 'kysely';
|
import { CamelCasePlugin, LogEvent, sql } from 'kysely';
|
||||||
|
|
@ -107,9 +101,7 @@ import { normalizePostgresUrl } from '../common/helpers';
|
||||||
WatcherRepo,
|
WatcherRepo,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class DatabaseModule
|
export class DatabaseModule implements OnApplicationBootstrap {
|
||||||
implements OnApplicationBootstrap, BeforeApplicationShutdown
|
|
||||||
{
|
|
||||||
private readonly logger = new Logger(DatabaseModule.name);
|
private readonly logger = new Logger(DatabaseModule.name);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
|
@ -126,12 +118,6 @@ export class DatabaseModule
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async beforeApplicationShutdown(): Promise<void> {
|
|
||||||
if (this.db) {
|
|
||||||
await this.db.destroy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async establishConnection() {
|
async establishConnection() {
|
||||||
const retryAttempts = 15;
|
const retryAttempts = 15;
|
||||||
const retryDelay = 3000;
|
const retryDelay = 3000;
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ import slugify = require('@sindresorhus/slugify');
|
||||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||||
const packageJson = require('../../../package.json');
|
const packageJson = require('../../../package.json');
|
||||||
import { EnvironmentService } from '../environment/environment.service';
|
import { EnvironmentService } from '../environment/environment.service';
|
||||||
|
import { DomainService } from '../environment/domain.service';
|
||||||
import {
|
import {
|
||||||
getAttachmentIds,
|
getAttachmentIds,
|
||||||
getProsemirrorContent,
|
getProsemirrorContent,
|
||||||
|
|
@ -49,6 +50,7 @@ export class ExportService {
|
||||||
@InjectKysely() private readonly db: KyselyDB,
|
@InjectKysely() private readonly db: KyselyDB,
|
||||||
private readonly storageService: StorageService,
|
private readonly storageService: StorageService,
|
||||||
private readonly environmentService: EnvironmentService,
|
private readonly environmentService: EnvironmentService,
|
||||||
|
private readonly domainService: DomainService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async exportPage(format: string, page: Page, singlePage?: boolean) {
|
async exportPage(format: string, page: Page, singlePage?: boolean) {
|
||||||
|
|
@ -61,9 +63,11 @@ export class ExportService {
|
||||||
let prosemirrorJson: any;
|
let prosemirrorJson: any;
|
||||||
|
|
||||||
if (singlePage) {
|
if (singlePage) {
|
||||||
|
const baseUrl = await this.getWorkspaceBaseUrl(page.workspaceId);
|
||||||
prosemirrorJson = await this.turnPageMentionsToLinks(
|
prosemirrorJson = await this.turnPageMentionsToLinks(
|
||||||
getProsemirrorContent(page.content),
|
getProsemirrorContent(page.content),
|
||||||
page.workspaceId,
|
page.workspaceId,
|
||||||
|
baseUrl,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// mentions is already turned to links during the zip process
|
// mentions is already turned to links during the zip process
|
||||||
|
|
@ -149,12 +153,14 @@ export class ExportService {
|
||||||
|
|
||||||
const tree = buildTree(pages as Page[]);
|
const tree = buildTree(pages as Page[]);
|
||||||
|
|
||||||
|
const baseUrl = await this.getWorkspaceBaseUrl(pages[0].workspaceId);
|
||||||
const zip = new JSZip();
|
const zip = new JSZip();
|
||||||
await this.zipPages(
|
await this.zipPages(
|
||||||
tree,
|
tree,
|
||||||
format,
|
format,
|
||||||
zip,
|
zip,
|
||||||
includeAttachments,
|
includeAttachments,
|
||||||
|
baseUrl,
|
||||||
userId,
|
userId,
|
||||||
ignorePermissions,
|
ignorePermissions,
|
||||||
);
|
);
|
||||||
|
|
@ -218,6 +224,7 @@ export class ExportService {
|
||||||
|
|
||||||
const tree = buildTree(pages as Page[]);
|
const tree = buildTree(pages as Page[]);
|
||||||
|
|
||||||
|
const baseUrl = await this.getWorkspaceBaseUrl(pages[0].workspaceId);
|
||||||
const zip = new JSZip();
|
const zip = new JSZip();
|
||||||
|
|
||||||
await this.zipPages(
|
await this.zipPages(
|
||||||
|
|
@ -225,6 +232,7 @@ export class ExportService {
|
||||||
format,
|
format,
|
||||||
zip,
|
zip,
|
||||||
includeAttachments,
|
includeAttachments,
|
||||||
|
baseUrl,
|
||||||
userId,
|
userId,
|
||||||
ignorePermissions,
|
ignorePermissions,
|
||||||
);
|
);
|
||||||
|
|
@ -248,6 +256,7 @@ export class ExportService {
|
||||||
format: string,
|
format: string,
|
||||||
zip: JSZip,
|
zip: JSZip,
|
||||||
includeAttachments: boolean,
|
includeAttachments: boolean,
|
||||||
|
baseUrl: string,
|
||||||
userId?: string,
|
userId?: string,
|
||||||
ignorePermissions = false,
|
ignorePermissions = false,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
|
@ -271,6 +280,7 @@ export class ExportService {
|
||||||
const prosemirrorJson = await this.turnPageMentionsToLinks(
|
const prosemirrorJson = await this.turnPageMentionsToLinks(
|
||||||
getProsemirrorContent(page.content),
|
getProsemirrorContent(page.content),
|
||||||
page.workspaceId,
|
page.workspaceId,
|
||||||
|
baseUrl,
|
||||||
userId,
|
userId,
|
||||||
ignorePermissions,
|
ignorePermissions,
|
||||||
);
|
);
|
||||||
|
|
@ -360,6 +370,7 @@ export class ExportService {
|
||||||
async turnPageMentionsToLinks(
|
async turnPageMentionsToLinks(
|
||||||
prosemirrorJson: any,
|
prosemirrorJson: any,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
|
baseUrl: string,
|
||||||
userId?: string,
|
userId?: string,
|
||||||
ignorePermissions = false,
|
ignorePermissions = false,
|
||||||
) {
|
) {
|
||||||
|
|
@ -429,8 +440,7 @@ export class ExportService {
|
||||||
const truncatedTitle = linkTitle?.substring(0, 70);
|
const truncatedTitle = linkTitle?.substring(0, 70);
|
||||||
const pageSlug = `${slugify(truncatedTitle)}-${slugId}`;
|
const pageSlug = `${slugify(truncatedTitle)}-${slugId}`;
|
||||||
|
|
||||||
// Create the link URL
|
const link = `${baseUrl}/s/${spaceSlug}/p/${pageSlug}`;
|
||||||
const link = `${this.environmentService.getAppUrl()}/s/${spaceSlug}/p/${pageSlug}`;
|
|
||||||
|
|
||||||
// Create a link mark and a text node with that mark
|
// Create a link mark and a text node with that mark
|
||||||
const linkMark = editorState.schema.marks.link.create({ href: link });
|
const linkMark = editorState.schema.marks.link.create({ href: link });
|
||||||
|
|
@ -476,6 +486,16 @@ export class ExportService {
|
||||||
return updatedDoc.toJSON();
|
return updatedDoc.toJSON();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async getWorkspaceBaseUrl(workspaceId: string): Promise<string> {
|
||||||
|
const workspace = await this.db
|
||||||
|
.selectFrom('workspaces')
|
||||||
|
.select('hostname')
|
||||||
|
.where('id', '=', workspaceId)
|
||||||
|
.executeTakeFirst();
|
||||||
|
|
||||||
|
return this.domainService.getUrl(workspace?.hostname);
|
||||||
|
}
|
||||||
|
|
||||||
private async filterPagesForExport(
|
private async filterPagesForExport(
|
||||||
pages: Page[],
|
pages: Page[],
|
||||||
rootPageId: string | null,
|
rootPageId: string | null,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue