diff --git a/app/actions/sections.ts b/app/actions/sections.ts index 3a5369b884..2046eb3d52 100644 --- a/app/actions/sections.ts +++ b/app/actions/sections.ts @@ -2,6 +2,8 @@ import { ActionContext } from "~/types"; export const CollectionSection = ({ t }: ActionContext) => t("Collection"); +export const CollectionsSection = ({ t }: ActionContext) => t("Collections"); + export const ActiveCollectionSection = ({ t, stores }: ActionContext) => { const activeCollection = stores.collections.active; return `${t("Collection")} · ${activeCollection?.name}`; diff --git a/app/editor/components/MentionMenu.tsx b/app/editor/components/MentionMenu.tsx index 661b6418f4..192668bbe4 100644 --- a/app/editor/components/MentionMenu.tsx +++ b/app/editor/components/MentionMenu.tsx @@ -1,6 +1,6 @@ import { isEmail } from "class-validator"; import { observer } from "mobx-react"; -import { DocumentIcon, PlusIcon } from "outline-icons"; +import { DocumentIcon, PlusIcon, CollectionIcon } from "outline-icons"; import * as React from "react"; import { useTranslation } from "react-i18next"; import { useLocation } from "react-router-dom"; @@ -12,7 +12,11 @@ import { MentionType } from "@shared/types"; import parseDocumentSlug from "@shared/utils/parseDocumentSlug"; import { Avatar, AvatarSize } from "~/components/Avatar"; import Flex from "~/components/Flex"; -import { DocumentsSection, UserSection } from "~/actions/sections"; +import { + DocumentsSection, + UserSection, + CollectionsSection, +} from "~/actions/sections"; import useRequest from "~/hooks/useRequest"; import useStores from "~/hooks/useStores"; import { client } from "~/utils/ApiClient"; @@ -40,7 +44,7 @@ function MentionMenu({ search, isActive, ...rest }: Props) { const [loaded, setLoaded] = React.useState(false); const [items, setItems] = React.useState([]); const { t } = useTranslation(); - const { auth, documents, users } = useStores(); + const { auth, documents, users, collections } = useStores(); const actorId = auth.currentUserId; const location = useLocation(); const documentId = parseDocumentSlug(location.pathname); @@ -49,8 +53,10 @@ function MentionMenu({ search, isActive, ...rest }: Props) { const { loading, request } = useRequest( React.useCallback(async () => { const res = await client.post("/suggestions.mention", { query: search }); + res.data.documents.map(documents.add); res.data.users.map(users.add); + res.data.collections.map(collections.add); }, [search, documents, users]) ); @@ -119,6 +125,34 @@ function MentionMenu({ search, isActive, ...rest }: Props) { } as MentionItem) ) ) + .concat( + collections + .findByQuery(search, { maxResults: maxResultsInSection }) + .map( + (collection) => + ({ + name: "mention", + icon: collection.icon ? ( + + ) : ( + + ), + title: collection.name, + section: CollectionsSection, + appendSpace: true, + attrs: { + id: v4(), + type: MentionType.Collection, + modelId: collection.id, + actorId, + label: collection.name, + }, + } as MentionItem) + ) + ) .concat([ { name: "link", @@ -146,7 +180,10 @@ function MentionMenu({ search, isActive, ...rest }: Props) { const handleSelect = React.useCallback( async (item: MentionItem) => { - if (item.attrs.type === MentionType.Document) { + if ( + item.attrs.type === MentionType.Document || + item.attrs.type === MentionType.Collection + ) { return; } if (!documentId) { diff --git a/app/editor/extensions/PasteHandler.tsx b/app/editor/extensions/PasteHandler.tsx index 73c1c63400..6982f74c93 100644 --- a/app/editor/extensions/PasteHandler.tsx +++ b/app/editor/extensions/PasteHandler.tsx @@ -20,8 +20,9 @@ import { isInCode } from "@shared/editor/queries/isInCode"; import { MenuItem } from "@shared/editor/types"; import { IconType, MentionType } from "@shared/types"; import { determineIconType } from "@shared/utils/icon"; +import parseCollectionSlug from "@shared/utils/parseCollectionSlug"; import parseDocumentSlug from "@shared/utils/parseDocumentSlug"; -import { isDocumentUrl, isUrl } from "@shared/utils/urls"; +import { isCollectionUrl, isDocumentUrl, isUrl } from "@shared/utils/urls"; import stores from "~/stores"; import PasteMenu from "../components/PasteMenu"; @@ -166,6 +167,51 @@ export default class PasteHandler extends Extension { this.insertLink(text); }); } + } else if (isCollectionUrl(text)) { + const slug = parseCollectionSlug(text); + + if (slug) { + stores.collections + .fetch(slug) + .then((collection) => { + if (view.isDestroyed) { + return; + } + if (collection) { + if (state.schema.nodes.mention) { + view.dispatch( + view.state.tr.replaceWith( + state.selection.from, + state.selection.to, + state.schema.nodes.mention.create({ + type: MentionType.Collection, + modelId: collection.id, + label: collection.name, + id: v4(), + }) + ) + ); + } else { + const { hash } = new URL(text); + const hasEmoji = + determineIconType(collection.icon) === + IconType.Emoji; + + const title = `${ + hasEmoji ? collection.icon + " " : "" + }${collection.name}`; + + this.insertLink(`${collection.path}${hash}`, title); + } + } + }) + .catch(() => { + if (view.isDestroyed) { + return; + } + this.insertLink(text); + }); + } } else { this.insertLink(text); } diff --git a/app/models/Collection.ts b/app/models/Collection.ts index f97bfcac43..a0c36f8f62 100644 --- a/app/models/Collection.ts +++ b/app/models/Collection.ts @@ -92,6 +92,11 @@ export default class Collection extends ParanoidModel { @observable archivedBy?: User; + @computed + get searchContent(): string { + return this.name; + } + /** Returns whether the collection is empty, or undefined if not loaded. */ @computed get isEmpty(): boolean | undefined { diff --git a/app/scenes/KeyboardShortcuts.tsx b/app/scenes/KeyboardShortcuts.tsx index 848be3cc8c..e29a1a304d 100644 --- a/app/scenes/KeyboardShortcuts.tsx +++ b/app/scenes/KeyboardShortcuts.tsx @@ -462,7 +462,7 @@ function KeyboardShortcuts() { items: [ { shortcut: "@", - label: t("Mention user or document"), + label: t("Mention users and more"), }, { shortcut: ":", diff --git a/server/models/helpers/SearchHelper.test.ts b/server/models/helpers/SearchHelper.test.ts index 03dc311a3a..e5f21f1441 100644 --- a/server/models/helpers/SearchHelper.test.ts +++ b/server/models/helpers/SearchHelper.test.ts @@ -861,6 +861,51 @@ describe("SearchHelper", () => { }); }); + describe("#searchCollectionsForUser", () => { + test("should return search results from collections", async () => { + const team = await buildTeam(); + const user = await buildUser({ teamId: team.id }); + const collection1 = await buildCollection({ + teamId: team.id, + userId: user.id, + name: "Test Collection", + }); + await buildCollection({ + teamId: team.id, + userId: user.id, + name: "Other Collection", + }); + + const results = await SearchHelper.searchCollectionsForUser(user, { + query: "test", + }); + + expect(results.length).toBe(1); + expect(results[0].id).toBe(collection1.id); + }); + + test("should return all collections when no query provided", async () => { + const team = await buildTeam(); + const user = await buildUser({ teamId: team.id }); + const collection1 = await buildCollection({ + teamId: team.id, + userId: user.id, + name: "Alpha", + }); + const collection2 = await buildCollection({ + teamId: team.id, + userId: user.id, + name: "Beta", + }); + + const results = await SearchHelper.searchCollectionsForUser(user); + + expect(results.length).toBe(2); + expect(results[0].id).toBe(collection1.id); + expect(results[1].id).toBe(collection2.id); + }); + }); + describe("webSearchQuery", () => { test("should correctly sanitize query", () => { expect(SearchHelper.webSearchQuery("one/two")).toBe("one/two:*"); diff --git a/server/models/helpers/SearchHelper.ts b/server/models/helpers/SearchHelper.ts index a2179fea9b..e8a85d83b0 100644 --- a/server/models/helpers/SearchHelper.ts +++ b/server/models/helpers/SearchHelper.ts @@ -203,6 +203,35 @@ export default class SearchHelper { }); } + public static async searchCollectionsForUser( + user: User, + options: SearchOptions = {} + ): Promise { + const { limit = 15, offset = 0, query } = options; + + const collectionIds = await user.collectionIds(); + + return Collection.findAll({ + where: { + [Op.and]: query + ? { + [Op.or]: [ + Sequelize.literal( + `unaccent(LOWER(name)) like unaccent(LOWER(:query))` + ), + ], + } + : {}, + id: collectionIds, + teamId: user.teamId, + }, + order: [["name", "ASC"]], + replacements: { query: `%${query}%` }, + limit, + offset, + }); + } + public static async searchForUser( user: User, options: SearchOptions = {} diff --git a/server/routes/api/suggestions/suggestions.ts b/server/routes/api/suggestions/suggestions.ts index eac32db6db..5263f3bd9e 100644 --- a/server/routes/api/suggestions/suggestions.ts +++ b/server/routes/api/suggestions/suggestions.ts @@ -23,7 +23,7 @@ router.post( const { offset, limit } = ctx.state.pagination; const actor = ctx.state.auth.user; - const [documents, users] = await Promise.all([ + const [documents, users, collections] = await Promise.all([ SearchHelper.searchTitlesForUser(actor, { query, offset, @@ -53,6 +53,7 @@ router.post( offset, limit, }), + SearchHelper.searchCollectionsForUser(actor, { query, offset, limit }), ]); ctx.body = { @@ -67,6 +68,7 @@ router.post( includeDetails: !!can(actor, "readDetails", user), }) ), + collections, }, }; } diff --git a/shared/editor/components/Mentions.tsx b/shared/editor/components/Mentions.tsx index 07737c5e8e..6c85c4880c 100644 --- a/shared/editor/components/Mentions.tsx +++ b/shared/editor/components/Mentions.tsx @@ -1,5 +1,5 @@ import { observer } from "mobx-react"; -import { DocumentIcon, EmailIcon } from "outline-icons"; +import { DocumentIcon, EmailIcon, CollectionIcon } from "outline-icons"; import { Node } from "prosemirror-model"; import * as React from "react"; import { Link } from "react-router-dom"; @@ -67,3 +67,36 @@ export const MentionDocument = observer(function MentionDocument_( ); }); + +export const MentionCollection = observer(function MentionCollection_( + props: ComponentProps +) { + const { isSelected, node } = props; + const { collections } = useStores(); + const collection = collections.get(node.attrs.modelId); + const modelId = node.attrs.modelId; + const { className, ...attrs } = getAttributesFromNode(node); + + React.useEffect(() => { + if (modelId) { + void collections.fetch(modelId); + } + }, [modelId, collections]); + + return ( + + {collection?.icon ? ( + + ) : ( + + )} + {collection?.title || node.attrs.label} + + ); +}); diff --git a/shared/editor/nodes/Mention.tsx b/shared/editor/nodes/Mention.tsx index 24aedfaa4a..fc200016de 100644 --- a/shared/editor/nodes/Mention.tsx +++ b/shared/editor/nodes/Mention.tsx @@ -16,7 +16,11 @@ import { Primitive } from "utility-types"; import { v4 as uuidv4 } from "uuid"; import env from "../../env"; import { MentionType } from "../../types"; -import { MentionDocument, MentionUser } from "../components/Mentions"; +import { + MentionCollection, + MentionDocument, + MentionUser, +} from "../components/Mentions"; import { MarkdownSerializerState } from "../lib/markdown/serializer"; import mentionRule from "../rules/mention"; import { ComponentProps } from "../types"; @@ -76,7 +80,9 @@ export default class Mention extends Node { href: node.attrs.type === MentionType.User ? undefined - : `${env.URL}/doc/${node.attrs.modelId}`, + : node.attrs.type === MentionType.Document + ? `${env.URL}/doc/${node.attrs.modelId}` + : `${env.URL}/collection/${node.attrs.modelId}`, "data-type": node.attrs.type, "data-id": node.attrs.modelId, "data-actorid": node.attrs.actorId, @@ -97,6 +103,8 @@ export default class Mention extends Node { return ; case MentionType.Document: return ; + case MentionType.Collection: + return ; default: return null; } @@ -145,10 +153,23 @@ export default class Mention extends Node { if ( selection instanceof NodeSelection && selection.node.type.name === this.name && - selection.node.attrs.type === MentionType.Document + (selection.node.attrs.type === MentionType.Document || + selection.node.attrs.type === MentionType.Collection) ) { const { modelId } = selection.node.attrs; - this.editor.props.onClickLink?.(`/doc/${modelId}`); + + const linkType = + selection.node.attrs.type === MentionType.Document + ? "doc" + : selection.node.attrs.type === MentionType.Collection + ? "collection" + : undefined; + + if (!linkType) { + return false; + } + + this.editor.props.onClickLink?.(`/${linkType}/${modelId}`); return true; } return false; diff --git a/shared/i18n/locales/cs_CZ/translation.json b/shared/i18n/locales/cs_CZ/translation.json index 117cc5b4a7..0ff9001886 100644 --- a/shared/i18n/locales/cs_CZ/translation.json +++ b/shared/i18n/locales/cs_CZ/translation.json @@ -769,7 +769,7 @@ "Inline code": "Vložený kód", "Inline LaTeX": "Vložený LaTeX", "Triggers": "Triggers", - "Mention user or document": "Mention user or document", + "Mention users and more": "Mention users and more", "Emoji": "Emoji", "Insert block": "Insert block", "Sign In": "Přihlásit se", @@ -1143,4 +1143,4 @@ "You created {{ timeAgo }}": "Vytvořili jste před {{ timeAgo }}", "{{ user }} created {{ timeAgo }}": "{{ user }} vytvořil před {{ timeAgo }}", "Uploading": "Nahrávání" -} +} \ No newline at end of file diff --git a/shared/i18n/locales/da_DK/translation.json b/shared/i18n/locales/da_DK/translation.json index abe70c6784..630efa8649 100644 --- a/shared/i18n/locales/da_DK/translation.json +++ b/shared/i18n/locales/da_DK/translation.json @@ -769,7 +769,7 @@ "Inline code": "Inline code", "Inline LaTeX": "Inline LaTeX", "Triggers": "Triggers", - "Mention user or document": "Mention user or document", + "Mention users and more": "Mention users and more", "Emoji": "Emoji", "Insert block": "Insert block", "Sign In": "Sign In", @@ -1143,4 +1143,4 @@ "You created {{ timeAgo }}": "You created {{ timeAgo }}", "{{ user }} created {{ timeAgo }}": "{{ user }} created {{ timeAgo }}", "Uploading": "Uploading" -} +} \ No newline at end of file diff --git a/shared/i18n/locales/de_DE/translation.json b/shared/i18n/locales/de_DE/translation.json index 5894957ad7..c4b98d1bd4 100644 --- a/shared/i18n/locales/de_DE/translation.json +++ b/shared/i18n/locales/de_DE/translation.json @@ -769,7 +769,7 @@ "Inline code": "Inline-Code", "Inline LaTeX": "Inline-LaTeX", "Triggers": "Triggers", - "Mention user or document": "Mention user or document", + "Mention users and more": "Mention users and more", "Emoji": "Emoji", "Insert block": "Insert block", "Sign In": "Anmelden", @@ -1143,4 +1143,4 @@ "You created {{ timeAgo }}": "Du hast vor {{ timeAgo }} erstellt", "{{ user }} created {{ timeAgo }}": "{{ user }} erstellte vor {{ timeAgo }}", "Uploading": "Wird hochgeladen" -} +} \ No newline at end of file diff --git a/shared/i18n/locales/en_US/translation.json b/shared/i18n/locales/en_US/translation.json index 3ac0b50298..f6f8e1a878 100644 --- a/shared/i18n/locales/en_US/translation.json +++ b/shared/i18n/locales/en_US/translation.json @@ -137,6 +137,7 @@ "Update role": "Update role", "Delete user": "Delete user", "Collection": "Collection", + "Collections": "Collections", "Debug": "Debug", "Document": "Document", "Documents": "Documents", @@ -368,7 +369,6 @@ "Archived collections": "Archived collections", "New doc": "New doc", "Empty": "Empty", - "Collections": "Collections", "Collapse": "Collapse", "Expand": "Expand", "Document not supported – try Markdown, Plain text, HTML, or Word": "Document not supported – try Markdown, Plain text, HTML, or Word", @@ -770,7 +770,7 @@ "Inline code": "Inline code", "Inline LaTeX": "Inline LaTeX", "Triggers": "Triggers", - "Mention user or document": "Mention user or document", + "Mention users and more": "Mention users and more", "Emoji": "Emoji", "Insert block": "Insert block", "Sign In": "Sign In", diff --git a/shared/i18n/locales/es_ES/translation.json b/shared/i18n/locales/es_ES/translation.json index 0eb85e7f7e..7b42420c0a 100644 --- a/shared/i18n/locales/es_ES/translation.json +++ b/shared/i18n/locales/es_ES/translation.json @@ -769,7 +769,7 @@ "Inline code": "Código en línea", "Inline LaTeX": "Línea de LaTeX", "Triggers": "Triggers", - "Mention user or document": "Mention user or document", + "Mention users and more": "Mention users and more", "Emoji": "Emoji", "Insert block": "Insert block", "Sign In": "Iniciar sesión", @@ -1143,4 +1143,4 @@ "You created {{ timeAgo }}": "Tú lo creaste {{ timeAgo }}", "{{ user }} created {{ timeAgo }}": "{{ user }} lo creó {{ timeAgo }}", "Uploading": "Subiendo" -} +} \ No newline at end of file diff --git a/shared/i18n/locales/fa_IR/translation.json b/shared/i18n/locales/fa_IR/translation.json index c01419fb5a..4ba0b7a26b 100644 --- a/shared/i18n/locales/fa_IR/translation.json +++ b/shared/i18n/locales/fa_IR/translation.json @@ -769,7 +769,7 @@ "Inline code": "کد درون خطی", "Inline LaTeX": "Inline LaTeX", "Triggers": "Triggers", - "Mention user or document": "Mention user or document", + "Mention users and more": "Mention users and more", "Emoji": "Emoji", "Insert block": "Insert block", "Sign In": "ورود", @@ -1143,4 +1143,4 @@ "You created {{ timeAgo }}": "You created {{ timeAgo }}", "{{ user }} created {{ timeAgo }}": "{{ user }} created {{ timeAgo }}", "Uploading": "در حال بارگذاری" -} +} \ No newline at end of file diff --git a/shared/i18n/locales/fr_FR/translation.json b/shared/i18n/locales/fr_FR/translation.json index 547f2f716b..53f7b39215 100644 --- a/shared/i18n/locales/fr_FR/translation.json +++ b/shared/i18n/locales/fr_FR/translation.json @@ -769,7 +769,7 @@ "Inline code": "Ligne de Code", "Inline LaTeX": "LaTeX en ligne", "Triggers": "Triggers", - "Mention user or document": "Mention user or document", + "Mention users and more": "Mention users and more", "Emoji": "Emoji", "Insert block": "Insert block", "Sign In": "Se connecter", @@ -1143,4 +1143,4 @@ "You created {{ timeAgo }}": "Créé par vous il y a {{ timeAgo }}", "{{ user }} created {{ timeAgo }}": "Créé par {{ user }} il y a {{ timeAgo }}", "Uploading": "Transfert en cours" -} +} \ No newline at end of file diff --git a/shared/i18n/locales/he_IL/translation.json b/shared/i18n/locales/he_IL/translation.json index c0b5c1fd0b..526306cb55 100644 --- a/shared/i18n/locales/he_IL/translation.json +++ b/shared/i18n/locales/he_IL/translation.json @@ -769,7 +769,7 @@ "Inline code": "Inline code", "Inline LaTeX": "Inline LaTeX", "Triggers": "Triggers", - "Mention user or document": "Mention user or document", + "Mention users and more": "Mention users and more", "Emoji": "Emoji", "Insert block": "Insert block", "Sign In": "Sign In", @@ -1143,4 +1143,4 @@ "You created {{ timeAgo }}": "You created {{ timeAgo }}", "{{ user }} created {{ timeAgo }}": "{{ user }} created {{ timeAgo }}", "Uploading": "Uploading" -} +} \ No newline at end of file diff --git a/shared/i18n/locales/hu_HU/translation.json b/shared/i18n/locales/hu_HU/translation.json index 5c7a135027..44d347da86 100644 --- a/shared/i18n/locales/hu_HU/translation.json +++ b/shared/i18n/locales/hu_HU/translation.json @@ -769,7 +769,7 @@ "Inline code": "Beágyazott kód", "Inline LaTeX": "Beágyazott LaTeX", "Triggers": "Triggers", - "Mention user or document": "Mention user or document", + "Mention users and more": "Mention users and more", "Emoji": "Emoji", "Insert block": "Insert block", "Sign In": "Bejelentkezés", @@ -1143,4 +1143,4 @@ "You created {{ timeAgo }}": "You created {{ timeAgo }}", "{{ user }} created {{ timeAgo }}": "{{ user }} created {{ timeAgo }}", "Uploading": "Uploading" -} +} \ No newline at end of file diff --git a/shared/i18n/locales/id_ID/translation.json b/shared/i18n/locales/id_ID/translation.json index a47e0e0c25..5554aee9a1 100644 --- a/shared/i18n/locales/id_ID/translation.json +++ b/shared/i18n/locales/id_ID/translation.json @@ -769,7 +769,7 @@ "Inline code": "Kode inline", "Inline LaTeX": "LaTeX Inline", "Triggers": "Triggers", - "Mention user or document": "Mention user or document", + "Mention users and more": "Mention users and more", "Emoji": "Emoji", "Insert block": "Insert block", "Sign In": "Masuk", @@ -1143,4 +1143,4 @@ "You created {{ timeAgo }}": "You created {{ timeAgo }}", "{{ user }} created {{ timeAgo }}": "{{ user }} created {{ timeAgo }}", "Uploading": "Mengunggah" -} +} \ No newline at end of file diff --git a/shared/i18n/locales/it_IT/translation.json b/shared/i18n/locales/it_IT/translation.json index 3e879da1d2..cbaeec5736 100644 --- a/shared/i18n/locales/it_IT/translation.json +++ b/shared/i18n/locales/it_IT/translation.json @@ -769,7 +769,7 @@ "Inline code": "Codice inline", "Inline LaTeX": "LaTeX in linea", "Triggers": "Triggers", - "Mention user or document": "Mention user or document", + "Mention users and more": "Mention users and more", "Emoji": "Emoji", "Insert block": "Insert block", "Sign In": "Accedi", @@ -1143,4 +1143,4 @@ "You created {{ timeAgo }}": "You created {{ timeAgo }}", "{{ user }} created {{ timeAgo }}": "{{ user }} created {{ timeAgo }}", "Uploading": "Caricamento" -} +} \ No newline at end of file diff --git a/shared/i18n/locales/ja_JP/translation.json b/shared/i18n/locales/ja_JP/translation.json index 54b5ff56bc..6d524fed0e 100644 --- a/shared/i18n/locales/ja_JP/translation.json +++ b/shared/i18n/locales/ja_JP/translation.json @@ -769,7 +769,7 @@ "Inline code": "インラインコード", "Inline LaTeX": "インライン LaTeX", "Triggers": "トリガー", - "Mention user or document": "ユーザーまたはドキュメントにメンション", + "Mention users and more": "ユーザーまたはドキュメントにメンション", "Emoji": "絵文字", "Insert block": "ブロックの挿入", "Sign In": "ログイン", @@ -1143,4 +1143,4 @@ "You created {{ timeAgo }}": "あなたが {{ timeAgo }} に作成しました", "{{ user }} created {{ timeAgo }}": "{{ user }} が {{ timeAgo }} に作成しました", "Uploading": "アップロード中" -} +} \ No newline at end of file diff --git a/shared/i18n/locales/ko_KR/translation.json b/shared/i18n/locales/ko_KR/translation.json index 248d73e2c3..7b0c2532db 100644 --- a/shared/i18n/locales/ko_KR/translation.json +++ b/shared/i18n/locales/ko_KR/translation.json @@ -769,7 +769,7 @@ "Inline code": "인라인 코드", "Inline LaTeX": "인라인 LaTeX", "Triggers": "Triggers", - "Mention user or document": "Mention user or document", + "Mention users and more": "Mention users and more", "Emoji": "Emoji", "Insert block": "Insert block", "Sign In": "로그인", @@ -1143,4 +1143,4 @@ "You created {{ timeAgo }}": "{{ timeAgo }} 전에 내가 생성함", "{{ user }} created {{ timeAgo }}": "{{ user }} 이(가) {{ timeAgo }} 전에 생성", "Uploading": "업로드 중" -} +} \ No newline at end of file diff --git a/shared/i18n/locales/nb_NO/translation.json b/shared/i18n/locales/nb_NO/translation.json index 7000f32c57..3bc7a8c65d 100644 --- a/shared/i18n/locales/nb_NO/translation.json +++ b/shared/i18n/locales/nb_NO/translation.json @@ -769,7 +769,7 @@ "Inline code": "Innebygd kode", "Inline LaTeX": "Innebygd LaTeX", "Triggers": "Utløsere", - "Mention user or document": "Nevn bruker eller dokument", + "Mention users and more": "Nevn bruker eller dokument", "Emoji": "Emoji", "Insert block": "Sett inn blokk", "Sign In": "Logg inn", @@ -1143,4 +1143,4 @@ "You created {{ timeAgo }}": "Du opprettet {{ timeAgo }}", "{{ user }} created {{ timeAgo }}": "{{ user }} opprettet {{ timeAgo }}", "Uploading": "Laster opp" -} +} \ No newline at end of file diff --git a/shared/i18n/locales/nl_NL/translation.json b/shared/i18n/locales/nl_NL/translation.json index c4061b6270..f0f55065c5 100644 --- a/shared/i18n/locales/nl_NL/translation.json +++ b/shared/i18n/locales/nl_NL/translation.json @@ -769,7 +769,7 @@ "Inline code": "Inline code", "Inline LaTeX": "Inline LaTeX", "Triggers": "Triggers", - "Mention user or document": "Mention user or document", + "Mention users and more": "Mention users and more", "Emoji": "Emoji", "Insert block": "Voeg blok in", "Sign In": "Aanmelden", @@ -1143,4 +1143,4 @@ "You created {{ timeAgo }}": "{{ timeAgo }} door jou aangemaakt", "{{ user }} created {{ timeAgo }}": "{{ timeAgo }} door {{ user }} aangemaakt", "Uploading": "Bezig met uploaden" -} +} \ No newline at end of file diff --git a/shared/i18n/locales/pl_PL/translation.json b/shared/i18n/locales/pl_PL/translation.json index c430ac9b4b..d4d42aaf47 100644 --- a/shared/i18n/locales/pl_PL/translation.json +++ b/shared/i18n/locales/pl_PL/translation.json @@ -769,7 +769,7 @@ "Inline code": "Kod w linii", "Inline LaTeX": "LaTeX w linii", "Triggers": "Triggers", - "Mention user or document": "Mention user or document", + "Mention users and more": "Mention users and more", "Emoji": "Emoji", "Insert block": "Insert block", "Sign In": "Zaloguj się", @@ -1143,4 +1143,4 @@ "You created {{ timeAgo }}": "Utworzyłeś {{ timeAgo }}", "{{ user }} created {{ timeAgo }}": "{{ user }} utworzył {{ timeAgo }}", "Uploading": "Wysyłanie" -} +} \ No newline at end of file diff --git a/shared/i18n/locales/pt_BR/translation.json b/shared/i18n/locales/pt_BR/translation.json index 2450e6633a..6543677117 100644 --- a/shared/i18n/locales/pt_BR/translation.json +++ b/shared/i18n/locales/pt_BR/translation.json @@ -769,7 +769,7 @@ "Inline code": "Código embutido", "Inline LaTeX": "LaTeX em linha", "Triggers": "Gatilhos", - "Mention user or document": "Mencionar usuário ou documento", + "Mention users and more": "Mencionar usuário ou documento", "Emoji": "Emoji", "Insert block": "Inserir bloco", "Sign In": "Entrar", @@ -1143,4 +1143,4 @@ "You created {{ timeAgo }}": "Você criou {{ timeAgo }}", "{{ user }} created {{ timeAgo }}": "{{ user }} criou {{ timeAgo }}", "Uploading": "Enviando" -} +} \ No newline at end of file diff --git a/shared/i18n/locales/pt_PT/translation.json b/shared/i18n/locales/pt_PT/translation.json index 35b4e1f713..8e57e16cbd 100644 --- a/shared/i18n/locales/pt_PT/translation.json +++ b/shared/i18n/locales/pt_PT/translation.json @@ -769,7 +769,7 @@ "Inline code": "Código em linha", "Inline LaTeX": "LaTeX Embutido", "Triggers": "Triggers", - "Mention user or document": "Mention user or document", + "Mention users and more": "Mention users and more", "Emoji": "Emoji", "Insert block": "Insert block", "Sign In": "Iniciar Sessão", @@ -1143,4 +1143,4 @@ "You created {{ timeAgo }}": "Criou à {{ timeAgo }}", "{{ user }} created {{ timeAgo }}": "{{ user }} criado à {{ timeAgo }}", "Uploading": "A carregar" -} +} \ No newline at end of file diff --git a/shared/i18n/locales/sv_SE/translation.json b/shared/i18n/locales/sv_SE/translation.json index 408416e349..033d94acc7 100644 --- a/shared/i18n/locales/sv_SE/translation.json +++ b/shared/i18n/locales/sv_SE/translation.json @@ -769,7 +769,7 @@ "Inline code": "Inline-kod", "Inline LaTeX": "Inline LaTeX", "Triggers": "Triggers", - "Mention user or document": "Mention user or document", + "Mention users and more": "Mention users and more", "Emoji": "Emoji", "Insert block": "Insert block", "Sign In": "Logga in", @@ -1143,4 +1143,4 @@ "You created {{ timeAgo }}": "Du skapade {{ timeAgo }}", "{{ user }} created {{ timeAgo }}": "{{ user }} skapade {{ timeAgo }}", "Uploading": "Laddar upp" -} +} \ No newline at end of file diff --git a/shared/i18n/locales/th_TH/translation.json b/shared/i18n/locales/th_TH/translation.json index 9eb33b6004..452c2ec0eb 100644 --- a/shared/i18n/locales/th_TH/translation.json +++ b/shared/i18n/locales/th_TH/translation.json @@ -769,7 +769,7 @@ "Inline code": "Inline code", "Inline LaTeX": "Inline LaTeX", "Triggers": "Triggers", - "Mention user or document": "Mention user or document", + "Mention users and more": "Mention users and more", "Emoji": "Emoji", "Insert block": "Insert block", "Sign In": "Sign In", @@ -1143,4 +1143,4 @@ "You created {{ timeAgo }}": "You created {{ timeAgo }}", "{{ user }} created {{ timeAgo }}": "{{ user }} created {{ timeAgo }}", "Uploading": "Uploading" -} +} \ No newline at end of file diff --git a/shared/i18n/locales/tr_TR/translation.json b/shared/i18n/locales/tr_TR/translation.json index c36a0831ed..f2f0fe8c5a 100644 --- a/shared/i18n/locales/tr_TR/translation.json +++ b/shared/i18n/locales/tr_TR/translation.json @@ -769,7 +769,7 @@ "Inline code": "Satır içi kod", "Inline LaTeX": "Inline LaTeX", "Triggers": "Triggers", - "Mention user or document": "Mention user or document", + "Mention users and more": "Mention users and more", "Emoji": "Emoji", "Insert block": "Insert block", "Sign In": "Kayıt ol", @@ -1143,4 +1143,4 @@ "You created {{ timeAgo }}": "You created {{ timeAgo }}", "{{ user }} created {{ timeAgo }}": "{{ user }} created {{ timeAgo }}", "Uploading": "Yükleniyor" -} +} \ No newline at end of file diff --git a/shared/i18n/locales/uk_UA/translation.json b/shared/i18n/locales/uk_UA/translation.json index fd1e9eb16d..a49b8a28fc 100644 --- a/shared/i18n/locales/uk_UA/translation.json +++ b/shared/i18n/locales/uk_UA/translation.json @@ -769,7 +769,7 @@ "Inline code": "Вбудований код", "Inline LaTeX": "Вбудований LaTeX", "Triggers": "Triggers", - "Mention user or document": "Mention user or document", + "Mention users and more": "Mention users and more", "Emoji": "Emoji", "Insert block": "Insert block", "Sign In": "Увійти", @@ -1143,4 +1143,4 @@ "You created {{ timeAgo }}": "Ви створили {{ timeAgo }}", "{{ user }} created {{ timeAgo }}": "{{ user }} створив {{ timeAgo }}", "Uploading": "Завантажується" -} +} \ No newline at end of file diff --git a/shared/i18n/locales/vi_VN/translation.json b/shared/i18n/locales/vi_VN/translation.json index db8285d295..0849d86546 100644 --- a/shared/i18n/locales/vi_VN/translation.json +++ b/shared/i18n/locales/vi_VN/translation.json @@ -769,7 +769,7 @@ "Inline code": "Mã nội tuyến", "Inline LaTeX": "LaTeX nội tuyến", "Triggers": "Triggers", - "Mention user or document": "Mention user or document", + "Mention users and more": "Mention users and more", "Emoji": "Emoji", "Insert block": "Insert block", "Sign In": "Đăng nhập", @@ -1143,4 +1143,4 @@ "You created {{ timeAgo }}": "You created {{ timeAgo }}", "{{ user }} created {{ timeAgo }}": "{{ user }} created {{ timeAgo }}", "Uploading": "Đang tải lên" -} +} \ No newline at end of file diff --git a/shared/i18n/locales/zh_CN/translation.json b/shared/i18n/locales/zh_CN/translation.json index 75557b0bf7..7c88fa7b47 100644 --- a/shared/i18n/locales/zh_CN/translation.json +++ b/shared/i18n/locales/zh_CN/translation.json @@ -769,7 +769,7 @@ "Inline code": "行内代码", "Inline LaTeX": "行内公式", "Triggers": "Triggers", - "Mention user or document": "提及用户或文档", + "Mention users and more": "提及用户或文档", "Emoji": "表情", "Insert block": "Insert block", "Sign In": "登录", @@ -1143,4 +1143,4 @@ "You created {{ timeAgo }}": "{{ timeAgo }} 由你创建", "{{ user }} created {{ timeAgo }}": "{{ timeAgo }} 由 {{ user }} 创建", "Uploading": "上传中" -} +} \ No newline at end of file diff --git a/shared/i18n/locales/zh_TW/translation.json b/shared/i18n/locales/zh_TW/translation.json index 0a0e768361..2f7040b3af 100644 --- a/shared/i18n/locales/zh_TW/translation.json +++ b/shared/i18n/locales/zh_TW/translation.json @@ -769,7 +769,7 @@ "Inline code": "行內程式碼", "Inline LaTeX": "行內 LaTeX", "Triggers": "Triggers", - "Mention user or document": "Mention user or document", + "Mention users and more": "Mention users and more", "Emoji": "Emoji", "Insert block": "Insert block", "Sign In": "登入", @@ -1143,4 +1143,4 @@ "You created {{ timeAgo }}": "{{ timeAgo }} 由您新增", "{{ user }} created {{ timeAgo }}": "{{ timeAgo }} 由 {{ user }} 新增", "Uploading": "正在上傳" -} +} \ No newline at end of file diff --git a/shared/types.ts b/shared/types.ts index 32904533cf..dc45116750 100644 --- a/shared/types.ts +++ b/shared/types.ts @@ -57,6 +57,7 @@ export enum FileOperationState { export enum MentionType { User = "user", Document = "document", + Collection = "collection", } export type PublicEnv = { diff --git a/shared/utils/parseCollectionSlug.test.ts b/shared/utils/parseCollectionSlug.test.ts new file mode 100644 index 0000000000..15eefd3ead --- /dev/null +++ b/shared/utils/parseCollectionSlug.test.ts @@ -0,0 +1,48 @@ +import sharedEnv from "../env"; +import parseCollectionSlug from "./parseCollectionSlug"; + +sharedEnv.URL = "https://app.outline.dev"; + +describe("#parseCollectionSlug", () => { + it("should work with fully qualified url", () => { + expect( + parseCollectionSlug("http://example.com/collection/test-ANzZwgv2RG") + ).toEqual("test-ANzZwgv2RG"); + }); + + it("should work with paths after document slug", () => { + expect( + parseCollectionSlug( + "http://mywiki.getoutline.com/collection/test-ANzZwgv2RG/recent" + ) + ).toEqual("test-ANzZwgv2RG"); + }); + + it("should work with hash", () => { + expect( + parseCollectionSlug( + "http://mywiki.getoutline.com/collection/test-ANzZwgv2RG#state" + ) + ).toEqual("test-ANzZwgv2RG"); + }); + + it("should work with subdomain qualified url", () => { + expect( + parseCollectionSlug( + "http://mywiki.getoutline.com/collection/test-ANzZwgv2RG" + ) + ).toEqual("test-ANzZwgv2RG"); + }); + + it("should work with path", () => { + expect(parseCollectionSlug("/collection/test-ANzZwgv2RG")).toEqual( + "test-ANzZwgv2RG" + ); + }); + + it("should work with path and hash", () => { + expect(parseCollectionSlug("/collection/test-ANzZwgv2RG#somehash")).toEqual( + "test-ANzZwgv2RG" + ); + }); +}); diff --git a/shared/utils/parseCollectionSlug.ts b/shared/utils/parseCollectionSlug.ts new file mode 100644 index 0000000000..9e3f484779 --- /dev/null +++ b/shared/utils/parseCollectionSlug.ts @@ -0,0 +1,25 @@ +import sharedEnv from "../env"; + +/** + * Parse the likely collection identifier from a given url. + * + * @param url The url to parse. + * @returns A collection identifier or undefined if not found. + */ +export default function parseCollectionSlug(url: string) { + let parsed; + + if (url[0] === "/") { + url = `${sharedEnv.URL}${url}`; + } + + try { + parsed = new URL(url).pathname; + } catch (err) { + return; + } + + const split = parsed.split("/"); + const indexOfCollection = split.indexOf("collection"); + return split[indexOfCollection + 1] ?? undefined; +} diff --git a/shared/utils/urls.ts b/shared/utils/urls.ts index fa700888ed..96d0d256c8 100644 --- a/shared/utils/urls.ts +++ b/shared/utils/urls.ts @@ -76,6 +76,21 @@ export function isDocumentUrl(url: string) { } } +/** + * Returns true if the given string is a link to a collection. + * + * @param options Parsing options. + * @returns True if a collection, false otherwise. + */ +export function isCollectionUrl(url: string) { + try { + const parsed = new URL(url, env.URL); + return isInternalUrl(url) && parsed.pathname.startsWith("/collection/"); + } catch (err) { + return false; + } +} + /** * Returns true if the given string is a url. *