mirror of
https://github.com/outline/outline.git
synced 2026-06-13 03:14:59 +03:00
feat: collection mentions (#8529)
* feat: init collection mention * refactor: dedicated search helper function for collection mentions * feat: add test for collection search function helper * feat: parseCollectionSlug * feat: isCollectionUrl * feat: add collection mention to paste handler * fix: update translation of mention keyboard shortcut * fix: keyboard shortcut mention label * fix: missing teamId in search helper functioN * chore: update translations --------- Co-authored-by: Tom Moor <tom@getoutline.com>
This commit is contained in:
@@ -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}`;
|
||||
|
||||
@@ -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<MentionItem[]>([]);
|
||||
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 ? (
|
||||
<Icon
|
||||
value={collection.icon}
|
||||
color={collection.color ?? undefined}
|
||||
/>
|
||||
) : (
|
||||
<CollectionIcon />
|
||||
),
|
||||
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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -462,7 +462,7 @@ function KeyboardShortcuts() {
|
||||
items: [
|
||||
{
|
||||
shortcut: "@",
|
||||
label: t("Mention user or document"),
|
||||
label: t("Mention users and more"),
|
||||
},
|
||||
{
|
||||
shortcut: ":",
|
||||
|
||||
@@ -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:*");
|
||||
|
||||
@@ -203,6 +203,35 @@ export default class SearchHelper {
|
||||
});
|
||||
}
|
||||
|
||||
public static async searchCollectionsForUser(
|
||||
user: User,
|
||||
options: SearchOptions = {}
|
||||
): Promise<Collection[]> {
|
||||
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 = {}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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_(
|
||||
</Link>
|
||||
);
|
||||
});
|
||||
|
||||
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 (
|
||||
<Link
|
||||
{...attrs}
|
||||
className={cn(className, {
|
||||
"ProseMirror-selectednode": isSelected,
|
||||
})}
|
||||
to={collection?.path ?? `/collection/${node.attrs.modelId}`}
|
||||
>
|
||||
{collection?.icon ? (
|
||||
<Icon value={collection?.icon} color={collection?.color} size={18} />
|
||||
) : (
|
||||
<CollectionIcon size={18} />
|
||||
)}
|
||||
{collection?.title || node.attrs.label}
|
||||
</Link>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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 <MentionUser {...props} />;
|
||||
case MentionType.Document:
|
||||
return <MentionDocument {...props} />;
|
||||
case MentionType.Collection:
|
||||
return <MentionCollection {...props} />;
|
||||
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;
|
||||
|
||||
@@ -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í"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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": "در حال بارگذاری"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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": "アップロード中"
|
||||
}
|
||||
}
|
||||
@@ -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": "업로드 중"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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": "Завантажується"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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": "上传中"
|
||||
}
|
||||
}
|
||||
@@ -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": "正在上傳"
|
||||
}
|
||||
}
|
||||
@@ -57,6 +57,7 @@ export enum FileOperationState {
|
||||
export enum MentionType {
|
||||
User = "user",
|
||||
Document = "document",
|
||||
Collection = "collection",
|
||||
}
|
||||
|
||||
export type PublicEnv = {
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user