diff --git a/apps/client/public/locales/en-US/translation.json b/apps/client/public/locales/en-US/translation.json index 3f366e4d..7b87e3eb 100644 --- a/apps/client/public/locales/en-US/translation.json +++ b/apps/client/public/locales/en-US/translation.json @@ -130,6 +130,7 @@ "pages": "pages", "Password": "Password", "Password changed successfully": "Password changed successfully", + "People": "People", "Pending": "Pending", "Please confirm your action": "Please confirm your action", "Preferences": "Preferences", diff --git a/apps/client/src/features/editor/components/common/editor-paste-handler.tsx b/apps/client/src/features/editor/components/common/editor-paste-handler.tsx index a7a91749..dea1f73c 100644 --- a/apps/client/src/features/editor/components/common/editor-paste-handler.tsx +++ b/apps/client/src/features/editor/components/common/editor-paste-handler.tsx @@ -33,7 +33,6 @@ export const handlePaste = ( const url = clipboardData.trim(); const { from: pos, empty } = editor.state.selection; 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 if (!empty || match[2] !== window.location.host) { @@ -41,12 +40,6 @@ export const handlePaste = ( 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 urlWithoutAnchor = anchorId ? url.substring(0, url.indexOf("#")) diff --git a/apps/client/src/features/editor/components/mention/mention-list.tsx b/apps/client/src/features/editor/components/mention/mention-list.tsx index f71e39eb..f086df49 100644 --- a/apps/client/src/features/editor/components/mention/mention-list.tsx +++ b/apps/client/src/features/editor/components/mention/mention-list.tsx @@ -31,13 +31,17 @@ import { MentionSuggestionItem, } from "@/features/editor/components/mention/mention.type.ts"; 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 { SimpleTree } from "react-arborist"; import { SpaceTreeNode } from "@/features/page/tree/types"; import { useTranslation } from "react-i18next"; import { useQueryEmit } from "@/features/websocket/use-query-emit"; import { extractPageSlugId } from "@/lib"; +import { AutoTooltipText } from "@/components/ui/auto-tooltip-text.tsx"; const MentionList = forwardRef((props, ref) => { const [selectedIndex, setSelectedIndex] = useState(1); @@ -59,11 +63,11 @@ const MentionList = forwardRef((props, ref) => { includeUsers: true, includePages: true, spaceId: space.id, - limit: 10, + limit: props.query ? 10 : 5, preload: true, }); - const createPageItem = (label: string) : MentionSuggestionItem => { + const createPageItem = (label: string): MentionSuggestionItem => { return { id: null, label: label, @@ -71,15 +75,15 @@ const MentionList = forwardRef((props, ref) => { entityId: null, slugId: null, icon: null, - } - } + }; + }; useEffect(() => { if (suggestion && !isLoading) { let items: MentionSuggestionItem[] = []; if (suggestion?.users?.length > 0) { - items.push({ entityType: "header", label: t("Users") }); + items.push({ entityType: "header", label: t("People") }); items = items.concat( suggestion.users.map((user) => ({ @@ -97,11 +101,13 @@ const MentionList = forwardRef((props, ref) => { items = items.concat( suggestion.pages.map((page) => ({ id: uuid7(), - label: page.title || "Untitled", + label: page.title || t("Untitled"), entityType: "page", entityId: page.id, slugId: page.slugId, icon: page.icon, + spaceName: page.space?.name, + spaceSlug: page.space?.slug, })), ); } @@ -129,17 +135,17 @@ const MentionList = forwardRef((props, ref) => { creatorId: currentUser?.user.id, }); } - if (item.entityType === "page" && item.id!==null) { + if (item.entityType === "page" && item.id !== null) { props.command({ id: item.id, - label: item.label || "Untitled", + label: item.label || t("Untitled"), entityType: "page", entityId: item.entityId, slugId: item.slugId, creatorId: currentUser?.user.id, }); } - if (item.entityType === "page" && item.id===null) { + if (item.entityType === "page" && item.id === null) { createPage(item.label); } } @@ -207,7 +213,7 @@ const MentionList = forwardRef((props, ref) => { const payload: { spaceId: string; parentPageId?: string; title: string } = { spaceId: space.id, parentPageId: page.id || null, - title: title + title: title, }; let createdPage: IPage; @@ -231,7 +237,7 @@ const MentionList = forwardRef((props, ref) => { props.command({ id: uuid7(), - label: createdPage.title || "Untitled", + label: createdPage.title || "Untitled", entityType: "page", entityId: createdPage.id, slugId: createdPage.slugId, @@ -239,21 +245,20 @@ const MentionList = forwardRef((props, ref) => { }); setTimeout(() => { - emit({ - operation: "addTreeNode", - spaceId: space.id, - payload: { - parentId, - index: lastIndex, - data, - }, - }); - }, 50); - + emit({ + operation: "addTreeNode", + spaceId: space.id, + payload: { + parentId, + index: lastIndex, + data, + }, + }); + }, 50); } catch (err) { throw new Error("Failed to create page"); } - } + }; useEffect(() => { viewportRef.current @@ -267,15 +272,19 @@ const MentionList = forwardRef((props, ref) => { return ( - { t("No results") } + {t("No results")} ); } const hasUsers = renderItems.some((item) => item.entityType === "user"); - const hasPages = renderItems.some((item) => item.entityType === "page" && item.id !== null); - const createPageItemData = renderItems.find((item) => item.entityType === "page" && item.id === null); + const hasPages = renderItems.some( + (item) => item.entityType === "page" && item.id !== null, + ); + const createPageItemData = renderItems.find( + (item) => item.entityType === "page" && item.id === null, + ); return ( @@ -283,7 +292,9 @@ const MentionList = forwardRef((props, ref) => { viewportRef={viewportRef} mah={350} w={popupWidth} + scrollbars={"y"} scrollbarSize={6} + styles={{ content: { minWidth: 0 } }} > {renderItems?.map((item, index) => { if (item.entityType === "header") { @@ -299,6 +310,7 @@ const MentionList = forwardRef((props, ref) => { pt={isFirst ? 2 : 4} pb={4} tt="uppercase" + style={{ userSelect: "none" }} > {item.label} @@ -323,9 +335,9 @@ const MentionList = forwardRef((props, ref) => { />
- + {item.label} - +
@@ -355,9 +367,14 @@ const MentionList = forwardRef((props, ref) => {
- + {item.label} - + + {item.spaceName && ( + + {item.spaceName} + + )}
@@ -372,9 +389,12 @@ const MentionList = forwardRef((props, ref) => { {(hasUsers || hasPages) && } selectItem(renderItems.indexOf(createPageItemData))} + onClick={() => + selectItem(renderItems.indexOf(createPageItemData)) + } className={clsx(classes.menuBtn, { - [classes.selectedItem]: renderItems.indexOf(createPageItemData) === selectedIndex, + [classes.selectedItem]: + renderItems.indexOf(createPageItemData) === selectedIndex, })} px="sm" > @@ -388,7 +408,7 @@ const MentionList = forwardRef((props, ref) => { -
+
{t("Create page")}: {createPageItemData.label} diff --git a/apps/client/src/features/editor/components/mention/mention-suggestion.ts b/apps/client/src/features/editor/components/mention/mention-suggestion.ts index 2a4fec1f..01a4ffad 100644 --- a/apps/client/src/features/editor/components/mention/mention-suggestion.ts +++ b/apps/client/src/features/editor/components/mention/mention-suggestion.ts @@ -106,7 +106,7 @@ const mentionRenderItems = () => { left: `${x}px`, top: `${y}px`, position: "absolute", - zIndex: "9999", + zIndex: "100", }); }); }, diff --git a/apps/client/src/features/editor/components/mention/mention-view.tsx b/apps/client/src/features/editor/components/mention/mention-view.tsx index 22cb8be6..a874cdf4 100644 --- a/apps/client/src/features/editor/components/mention/mention-view.tsx +++ b/apps/client/src/features/editor/components/mention/mention-view.tsx @@ -54,12 +54,20 @@ export default function MentionView(props: NodeViewProps) { )} - {entityType === "page" && ( + {entityType === "page" && isError && ( + + {label} + + )} + + {entityType === "page" && !isError && ( this.pageRepo.withSpace(eb)) .where((eb) => eb( sql`LOWER(f_unaccent(pages.title))`, @@ -209,17 +210,19 @@ export class SearchService { .where('workspaceId', '=', workspaceId) .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); - if (suggestion?.spaceId) { - 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 + if (userSpaceIds?.length > 0) { 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(); } @@ -230,7 +233,6 @@ export class SearchService { await this.pagePermissionRepo.filterAccessiblePageIds({ pageIds, userId, - spaceId: suggestion?.spaceId, }); const accessibleSet = new Set(accessibleIds); pages = pages.filter((p) => accessibleSet.has(p.id)); diff --git a/apps/server/src/database/database.module.ts b/apps/server/src/database/database.module.ts index 765fee4f..3503e4ea 100644 --- a/apps/server/src/database/database.module.ts +++ b/apps/server/src/database/database.module.ts @@ -1,10 +1,4 @@ -import { - Global, - Logger, - Module, - OnApplicationBootstrap, - BeforeApplicationShutdown, -} from '@nestjs/common'; +import { Global, Logger, Module, OnApplicationBootstrap } from '@nestjs/common'; import { InjectKysely, KyselyModule } from 'nestjs-kysely'; import { EnvironmentService } from '../integrations/environment/environment.service'; import { CamelCasePlugin, LogEvent, sql } from 'kysely'; @@ -107,9 +101,7 @@ import { normalizePostgresUrl } from '../common/helpers'; WatcherRepo, ], }) -export class DatabaseModule - implements OnApplicationBootstrap, BeforeApplicationShutdown -{ +export class DatabaseModule implements OnApplicationBootstrap { private readonly logger = new Logger(DatabaseModule.name); constructor( @@ -126,12 +118,6 @@ export class DatabaseModule } } - async beforeApplicationShutdown(): Promise { - if (this.db) { - await this.db.destroy(); - } - } - async establishConnection() { const retryAttempts = 15; const retryDelay = 3000; diff --git a/apps/server/src/integrations/export/export.service.ts b/apps/server/src/integrations/export/export.service.ts index 48be81ba..c8cea483 100644 --- a/apps/server/src/integrations/export/export.service.ts +++ b/apps/server/src/integrations/export/export.service.ts @@ -33,6 +33,7 @@ import slugify = require('@sindresorhus/slugify'); // eslint-disable-next-line @typescript-eslint/no-require-imports const packageJson = require('../../../package.json'); import { EnvironmentService } from '../environment/environment.service'; +import { DomainService } from '../environment/domain.service'; import { getAttachmentIds, getProsemirrorContent, @@ -49,6 +50,7 @@ export class ExportService { @InjectKysely() private readonly db: KyselyDB, private readonly storageService: StorageService, private readonly environmentService: EnvironmentService, + private readonly domainService: DomainService, ) {} async exportPage(format: string, page: Page, singlePage?: boolean) { @@ -61,9 +63,11 @@ export class ExportService { let prosemirrorJson: any; if (singlePage) { + const baseUrl = await this.getWorkspaceBaseUrl(page.workspaceId); prosemirrorJson = await this.turnPageMentionsToLinks( getProsemirrorContent(page.content), page.workspaceId, + baseUrl, ); } else { // mentions is already turned to links during the zip process @@ -149,12 +153,14 @@ export class ExportService { const tree = buildTree(pages as Page[]); + const baseUrl = await this.getWorkspaceBaseUrl(pages[0].workspaceId); const zip = new JSZip(); await this.zipPages( tree, format, zip, includeAttachments, + baseUrl, userId, ignorePermissions, ); @@ -218,6 +224,7 @@ export class ExportService { const tree = buildTree(pages as Page[]); + const baseUrl = await this.getWorkspaceBaseUrl(pages[0].workspaceId); const zip = new JSZip(); await this.zipPages( @@ -225,6 +232,7 @@ export class ExportService { format, zip, includeAttachments, + baseUrl, userId, ignorePermissions, ); @@ -248,6 +256,7 @@ export class ExportService { format: string, zip: JSZip, includeAttachments: boolean, + baseUrl: string, userId?: string, ignorePermissions = false, ): Promise { @@ -271,6 +280,7 @@ export class ExportService { const prosemirrorJson = await this.turnPageMentionsToLinks( getProsemirrorContent(page.content), page.workspaceId, + baseUrl, userId, ignorePermissions, ); @@ -360,6 +370,7 @@ export class ExportService { async turnPageMentionsToLinks( prosemirrorJson: any, workspaceId: string, + baseUrl: string, userId?: string, ignorePermissions = false, ) { @@ -429,8 +440,7 @@ export class ExportService { const truncatedTitle = linkTitle?.substring(0, 70); const pageSlug = `${slugify(truncatedTitle)}-${slugId}`; - // Create the link URL - const link = `${this.environmentService.getAppUrl()}/s/${spaceSlug}/p/${pageSlug}`; + const link = `${baseUrl}/s/${spaceSlug}/p/${pageSlug}`; // Create a link mark and a text node with that mark const linkMark = editorState.schema.marks.link.create({ href: link }); @@ -476,6 +486,16 @@ export class ExportService { return updatedDoc.toJSON(); } + private async getWorkspaceBaseUrl(workspaceId: string): Promise { + const workspace = await this.db + .selectFrom('workspaces') + .select('hostname') + .where('id', '=', workspaceId) + .executeTakeFirst(); + + return this.domainService.getUrl(workspace?.hostname); + } + private async filterPagesForExport( pages: Page[], rootPageId: string | null,