From d8fbe35455fb36582befba34fb4ae3b567d886b3 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Sat, 30 Nov 2024 10:13:44 -0500 Subject: [PATCH] fix: Template variables are not applied on client (#8044) * fix: Template variables are not applied on client * test --- app/menus/TemplatesMenu.tsx | 4 +-- app/scenes/Document/components/Document.tsx | 14 +++++--- app/utils/date.ts | 29 +---------------- server/commands/documentCreator.ts | 4 +-- server/models/helpers/ProsemirrorHelper.tsx | 25 --------------- server/models/helpers/TextHelper.ts | 26 --------------- shared/utils/ProsemirrorHelper.ts | 29 +++++++++++++++++ .../utils}/TextHelper.test.ts | 12 ++++--- shared/utils/TextHelper.ts | 32 +++++++++++++++++++ 9 files changed, 83 insertions(+), 92 deletions(-) rename {server/models/helpers => shared/utils}/TextHelper.test.ts (73%) create mode 100644 shared/utils/TextHelper.ts diff --git a/app/menus/TemplatesMenu.tsx b/app/menus/TemplatesMenu.tsx index c1a07a23c8..60582cda9a 100644 --- a/app/menus/TemplatesMenu.tsx +++ b/app/menus/TemplatesMenu.tsx @@ -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 ? ( ) : ( diff --git a/app/scenes/Document/components/Document.tsx b/app/scenes/Document/components/Document.tsx index c6c940905d..f8a32ab6a1 100644 --- a/app/scenes/Document/components/Document.tsx +++ b/app/scenes/Document/components/Document.tsx @@ -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 { } 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 { } 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; diff --git a/app/utils/date.ts b/app/utils/date.ts index e38113f92f..bb686a8d4d 100644 --- a/app/utils/date.ts +++ b/app/utils/date.ts @@ -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))); -} diff --git a/server/commands/documentCreator.ts b/server/commands/documentCreator.ts index 2cf96e9849..e90f91f5c8 100644 --- a/server/commands/documentCreator.ts +++ b/server/commands/documentCreator.ts @@ -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< diff --git a/server/models/helpers/ProsemirrorHelper.tsx b/server/models/helpers/ProsemirrorHelper.tsx index 9ac99647fc..a7b6dd1d9f 100644 --- a/server/models/helpers/ProsemirrorHelper.tsx +++ b/server/models/helpers/ProsemirrorHelper.tsx @@ -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 diff --git a/server/models/helpers/TextHelper.ts b/server/models/helpers/TextHelper.ts index e91ade6337..8f321d0a9f 100644 --- a/server/models/helpers/TextHelper.ts +++ b/server/models/helpers/TextHelper.ts @@ -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 diff --git a/shared/utils/ProsemirrorHelper.ts b/shared/utils/ProsemirrorHelper.ts index 595757d775..be1b16a1c4 100644 --- a/shared/utils/ProsemirrorHelper.ts +++ b/shared/utils/ProsemirrorHelper.ts @@ -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=(?[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); + } } diff --git a/server/models/helpers/TextHelper.test.ts b/shared/utils/TextHelper.test.ts similarity index 73% rename from server/models/helpers/TextHelper.test.ts rename to shared/utils/TextHelper.test.ts index 601b93bab3..e1e2ddca97 100644 --- a/server/models/helpers/TextHelper.test.ts +++ b/shared/utils/TextHelper.test.ts @@ -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"); }); }); }); diff --git a/shared/utils/TextHelper.ts b/shared/utils/TextHelper.ts new file mode 100644 index 0000000000..39987f7c16 --- /dev/null +++ b/shared/utils/TextHelper.ts @@ -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); + } +}