mirror of
https://github.com/outline/outline.git
synced 2026-06-13 03:14:59 +03:00
fix: Template variables are not applied on client (#8044)
* fix: Template variables are not applied on client * test
This commit is contained in:
@@ -3,6 +3,7 @@ import { DocumentIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { MenuButton, useMenuState } from "reakit/Menu";
|
||||
import { TextHelper } from "@shared/utils/TextHelper";
|
||||
import Document from "~/models/Document";
|
||||
import Button from "~/components/Button";
|
||||
import ContextMenu from "~/components/ContextMenu";
|
||||
@@ -11,7 +12,6 @@ import Icon from "~/components/Icon";
|
||||
import useCurrentUser from "~/hooks/useCurrentUser";
|
||||
import useStores from "~/hooks/useStores";
|
||||
import { MenuItem } from "~/types";
|
||||
import { replaceTitleVariables } from "~/utils/date";
|
||||
|
||||
type Props = {
|
||||
document: Document;
|
||||
@@ -29,7 +29,7 @@ function TemplatesMenu({ onSelectTemplate, document }: Props) {
|
||||
const templateToMenuItem = React.useCallback(
|
||||
(tmpl: Document): MenuItem => ({
|
||||
type: "button",
|
||||
title: replaceTitleVariables(tmpl.titleWithDefault, user),
|
||||
title: TextHelper.replaceTemplateVariables(tmpl.titleWithDefault, user),
|
||||
icon: tmpl.icon ? (
|
||||
<Icon value={tmpl.icon} color={tmpl.color ?? undefined} />
|
||||
) : (
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
TeamPreference,
|
||||
} from "@shared/types";
|
||||
import { ProsemirrorHelper } from "@shared/utils/ProsemirrorHelper";
|
||||
import { TextHelper } from "@shared/utils/TextHelper";
|
||||
import { parseDomain } from "@shared/utils/domains";
|
||||
import { determineIconType } from "@shared/utils/icon";
|
||||
import RootStore from "~/stores/RootStore";
|
||||
@@ -44,7 +45,6 @@ import withStores from "~/components/withStores";
|
||||
import type { Editor as TEditor } from "~/editor";
|
||||
import { SearchResult } from "~/editor/components/LinkEditor";
|
||||
import { client } from "~/utils/ApiClient";
|
||||
import { replaceTitleVariables } from "~/utils/date";
|
||||
import { emojiToUrl } from "~/utils/emoji";
|
||||
import { isModKey } from "~/utils/keyboard";
|
||||
|
||||
@@ -151,7 +151,13 @@ class DocumentScene extends React.Component<Props> {
|
||||
}
|
||||
|
||||
const { view, schema } = editorRef;
|
||||
const doc = Node.fromJSON(schema, template.data);
|
||||
const doc = Node.fromJSON(
|
||||
schema,
|
||||
ProsemirrorHelper.replaceTemplateVariables(
|
||||
template.data,
|
||||
this.props.auth.user!
|
||||
)
|
||||
);
|
||||
|
||||
if (doc) {
|
||||
view.dispatch(
|
||||
@@ -168,9 +174,9 @@ class DocumentScene extends React.Component<Props> {
|
||||
}
|
||||
|
||||
if (!this.title) {
|
||||
const title = replaceTitleVariables(
|
||||
const title = TextHelper.replaceTemplateVariables(
|
||||
template.title,
|
||||
this.props.auth.user || undefined
|
||||
this.props.auth.user!
|
||||
);
|
||||
this.title = title;
|
||||
this.props.document.title = title;
|
||||
|
||||
+1
-28
@@ -10,16 +10,7 @@ import {
|
||||
isPast,
|
||||
} from "date-fns";
|
||||
import { TFunction } from "i18next";
|
||||
import startCase from "lodash/startCase";
|
||||
import {
|
||||
getCurrentDateAsString,
|
||||
getCurrentDateTimeAsString,
|
||||
getCurrentTimeAsString,
|
||||
unicodeCLDRtoBCP47,
|
||||
dateLocale,
|
||||
locales,
|
||||
} from "@shared/utils/date";
|
||||
import User from "~/models/User";
|
||||
import { dateLocale, locales } from "@shared/utils/date";
|
||||
|
||||
export function dateToHeading(
|
||||
dateTime: string,
|
||||
@@ -121,21 +112,3 @@ export function dateToExpiry(
|
||||
date: formatDate(date, "MMM dd, yyyy", { locale }),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces template variables in the given text with the current date and time.
|
||||
*
|
||||
* @param text The text to replace the variables in
|
||||
* @param user The user to get the language/locale from
|
||||
* @returns The text with the variables replaced
|
||||
*/
|
||||
export function replaceTitleVariables(text: string, user?: User) {
|
||||
const locales = user?.language
|
||||
? unicodeCLDRtoBCP47(user.language)
|
||||
: undefined;
|
||||
|
||||
return text
|
||||
.replace("{date}", startCase(getCurrentDateAsString(locales)))
|
||||
.replace("{time}", startCase(getCurrentTimeAsString(locales)))
|
||||
.replace("{datetime}", startCase(getCurrentDateTimeAsString(locales)));
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Optional } from "utility-types";
|
||||
import { ProsemirrorHelper } from "@shared/utils/ProsemirrorHelper";
|
||||
import { TextHelper } from "@shared/utils/TextHelper";
|
||||
import { Document, Event, User } from "@server/models";
|
||||
import { DocumentHelper } from "@server/models/helpers/DocumentHelper";
|
||||
import { ProsemirrorHelper } from "@server/models/helpers/ProsemirrorHelper";
|
||||
import { TextHelper } from "@server/models/helpers/TextHelper";
|
||||
import { APIContext } from "@server/types";
|
||||
|
||||
type Props = Optional<
|
||||
|
||||
@@ -21,9 +21,7 @@ import { schema, parser } from "@server/editor";
|
||||
import Logger from "@server/logging/Logger";
|
||||
import { trace } from "@server/logging/tracing";
|
||||
import Attachment from "@server/models/Attachment";
|
||||
import User from "@server/models/User";
|
||||
import FileStorage from "@server/storage/files";
|
||||
import { TextHelper } from "./TextHelper";
|
||||
|
||||
export type HTMLOptions = {
|
||||
/** A title, if it should be included */
|
||||
@@ -264,29 +262,6 @@ export class ProsemirrorHelper {
|
||||
return removeMarksInner(json);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces all template variables in the node.
|
||||
*
|
||||
* @param data The ProsemirrorData object to replace variables in
|
||||
* @param user The user to use for replacing variables
|
||||
* @returns The content with variables replaced
|
||||
*/
|
||||
static replaceTemplateVariables(data: ProsemirrorData, user: User) {
|
||||
function replace(node: ProsemirrorData) {
|
||||
if (node.type === "text" && node.text) {
|
||||
node.text = TextHelper.replaceTemplateVariables(node.text, user);
|
||||
}
|
||||
|
||||
if (node.content) {
|
||||
node.content.forEach(replace);
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
return replace(data);
|
||||
}
|
||||
|
||||
static async replaceInternalUrls(
|
||||
doc: Node | ProsemirrorData,
|
||||
basePath: string
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
import chunk from "lodash/chunk";
|
||||
import escapeRegExp from "lodash/escapeRegExp";
|
||||
import startCase from "lodash/startCase";
|
||||
import { AttachmentPreset } from "@shared/types";
|
||||
import {
|
||||
getCurrentDateAsString,
|
||||
getCurrentDateTimeAsString,
|
||||
getCurrentTimeAsString,
|
||||
unicodeCLDRtoBCP47,
|
||||
} from "@shared/utils/date";
|
||||
import attachmentCreator from "@server/commands/attachmentCreator";
|
||||
import env from "@server/env";
|
||||
import { trace } from "@server/logging/tracing";
|
||||
@@ -19,25 +12,6 @@ import parseImages from "@server/utils/parseImages";
|
||||
|
||||
@trace()
|
||||
export class TextHelper {
|
||||
/**
|
||||
* Replaces template variables in the given text with the current date and time.
|
||||
*
|
||||
* @param text The text to replace the variables in
|
||||
* @param user The user to get the language/locale from
|
||||
* @returns The text with the variables replaced
|
||||
*/
|
||||
static replaceTemplateVariables(text: string, user: User) {
|
||||
const locales = user.language
|
||||
? unicodeCLDRtoBCP47(user.language)
|
||||
: undefined;
|
||||
|
||||
return text
|
||||
.replace(/{date}/g, startCase(getCurrentDateAsString(locales)))
|
||||
.replace(/{time}/g, startCase(getCurrentTimeAsString(locales)))
|
||||
.replace(/{datetime}/g, startCase(getCurrentDateTimeAsString(locales)))
|
||||
.replace(/{author}/g, user.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts attachment urls in documents to signed equivalents that allow
|
||||
* direct access without a session cookie
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Node, Schema } from "prosemirror-model";
|
||||
import headingToSlug from "../editor/lib/headingToSlug";
|
||||
import textBetween from "../editor/lib/textBetween";
|
||||
import { ProsemirrorData } from "../types";
|
||||
import { TextHelper } from "./TextHelper";
|
||||
|
||||
export type Heading = {
|
||||
/* The heading in plain text */
|
||||
@@ -28,6 +29,11 @@ export type Task = {
|
||||
completed: boolean;
|
||||
};
|
||||
|
||||
interface User {
|
||||
name: string;
|
||||
language: string | null;
|
||||
}
|
||||
|
||||
export const attachmentRedirectRegex =
|
||||
/\/api\/attachments\.redirect\?id=(?<id>[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/gi;
|
||||
|
||||
@@ -307,4 +313,27 @@ export class ProsemirrorHelper {
|
||||
});
|
||||
return headings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces all template variables in the node.
|
||||
*
|
||||
* @param data The ProsemirrorData object to replace variables in
|
||||
* @param user The user to use for replacing variables
|
||||
* @returns The content with variables replaced
|
||||
*/
|
||||
static replaceTemplateVariables(data: ProsemirrorData, user: User) {
|
||||
function replace(node: ProsemirrorData) {
|
||||
if (node.type === "text" && node.text) {
|
||||
node.text = TextHelper.replaceTemplateVariables(node.text, user);
|
||||
}
|
||||
|
||||
if (node.content) {
|
||||
node.content.forEach(replace);
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
return replace(data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { buildUser } from "@server/test/factories";
|
||||
import { TextHelper } from "./TextHelper";
|
||||
|
||||
describe("TextHelper", () => {
|
||||
@@ -12,18 +11,21 @@ describe("TextHelper", () => {
|
||||
});
|
||||
|
||||
describe("replaceTemplateVariables", () => {
|
||||
const user = {
|
||||
name: "John Doe",
|
||||
language: "en",
|
||||
};
|
||||
|
||||
it("should replace {time} with current time", async () => {
|
||||
const user = await buildUser();
|
||||
const result = TextHelper.replaceTemplateVariables("Hello {time}", user);
|
||||
|
||||
expect(result).toBe("Hello 12 00 AM");
|
||||
expect(result).toBe("Hello 12:00 AM");
|
||||
});
|
||||
|
||||
it("should replace {date} with current date", async () => {
|
||||
const user = await buildUser();
|
||||
const result = TextHelper.replaceTemplateVariables("Hello {date}", user);
|
||||
|
||||
expect(result).toBe("Hello January 1 2021");
|
||||
expect(result).toBe("Hello January 1, 2021");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
getCurrentDateAsString,
|
||||
getCurrentDateTimeAsString,
|
||||
getCurrentTimeAsString,
|
||||
unicodeCLDRtoBCP47,
|
||||
} from "./date";
|
||||
|
||||
interface User {
|
||||
name: string;
|
||||
language: string | null;
|
||||
}
|
||||
|
||||
export class TextHelper {
|
||||
/**
|
||||
* Replaces template variables in the given text with the current date and time.
|
||||
*
|
||||
* @param text The text to replace the variables in
|
||||
* @param user The user to get the language/locale from
|
||||
* @returns The text with the variables replaced
|
||||
*/
|
||||
static replaceTemplateVariables(text: string, user: User) {
|
||||
const locales = user.language
|
||||
? unicodeCLDRtoBCP47(user.language)
|
||||
: undefined;
|
||||
|
||||
return text
|
||||
.replace(/{date}/g, getCurrentDateAsString(locales))
|
||||
.replace(/{time}/g, getCurrentTimeAsString(locales))
|
||||
.replace(/{datetime}/g, getCurrentDateTimeAsString(locales))
|
||||
.replace(/{author}/g, user.name);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user