From c54dde3dbabbdf5e6cefa7d8d111fb472b467313 Mon Sep 17 00:00:00 2001 From: George Gevoian Date: Wed, 27 Jul 2022 13:20:14 -0700 Subject: [PATCH] (core) Populate doc title, description and thumbnail in app.html Summary: Fills in the title and description/thumbnail (for templates) in app.html if the page being requested is for a document. Test Plan: Tested manually. Reviewers: paulfitz Reviewed By: paulfitz Subscribers: dsagal Differential Revision: https://phab.getgrist.com/D3544 --- app/client/ui/AppUI.ts | 4 +++ app/gen-server/ApiServer.ts | 2 +- app/server/lib/sendAppPage.ts | 64 +++++++++++++++++++++++++++++++++-- package.json | 3 ++ static/app.html | 3 +- yarn.lock | 36 +++++++++++++++++++- 6 files changed, 106 insertions(+), 6 deletions(-) diff --git a/app/client/ui/AppUI.ts b/app/client/ui/AppUI.ts index 9526ec68..2f5081b3 100644 --- a/app/client/ui/AppUI.ts +++ b/app/client/ui/AppUI.ts @@ -128,6 +128,10 @@ function pagePanelsDoc(owner: IDisposableOwner, appModel: AppModel, appObj: App) // Set document title to strings like "DocName - Grist" owner.autoDispose(subscribe(pageModel.currentDocTitle, (use, docName) => { + // If the document hasn't loaded yet, don't update the title; since the HTML document already has + // a title element with the document's name, there's no need for further action. + if (!pageModel.currentDoc.get()) { return; } + document.title = `${docName}${getPageTitleSuffix(getGristConfig())}`; })); diff --git a/app/gen-server/ApiServer.ts b/app/gen-server/ApiServer.ts index 0e87a790..ee3eee3a 100644 --- a/app/gen-server/ApiServer.ts +++ b/app/gen-server/ApiServer.ts @@ -19,7 +19,7 @@ import {User} from './entity/User'; import {HomeDBManager} from './lib/HomeDBManager'; // Special public organization that contains examples and templates. -const TEMPLATES_ORG_DOMAIN = process.env.GRIST_ID_PREFIX ? +export const TEMPLATES_ORG_DOMAIN = process.env.GRIST_ID_PREFIX ? `templates-${process.env.GRIST_ID_PREFIX}` : 'templates'; diff --git a/app/server/lib/sendAppPage.ts b/app/server/lib/sendAppPage.ts index 85dcebdd..43297911 100644 --- a/app/server/lib/sendAppPage.ts +++ b/app/server/lib/sendAppPage.ts @@ -1,11 +1,14 @@ import {getPageTitleSuffix, GristLoadConfig, HideableUiElements, IHideableUiElement} from 'app/common/gristUrls'; import {getTagManagerSnippet} from 'app/common/tagManager'; +import {Document} from 'app/common/UserAPI'; import {isAnonymousUser, RequestWithLogin} from 'app/server/lib/Authorizer'; import {RequestWithOrg} from 'app/server/lib/extractOrg'; import {GristServer} from 'app/server/lib/GristServer'; import {getSupportedEngineChoices} from 'app/server/lib/serverUtils'; import * as express from 'express'; import * as fse from 'fs-extra'; +import jsesc from 'jsesc'; +import * as handlebars from 'handlebars'; import * as path from 'path'; export interface ISendAppPageOptions { @@ -65,8 +68,10 @@ export function makeGristConfig(homeUrl: string|null, extra: Partial { const fileContent = await fse.readFile(path.join(staticDir, "message.html"), 'utf8'); - const content = fileContent - .replace("", ``); + const content = fileContent.replace( + "", + `` + ); resp.status(200).type('html').send(content); }; } @@ -98,10 +103,15 @@ export function makeSendAppPage(opts: { const warning = testLogin ? "
Authentication is not enforced
" : ""; const content = fileContent .replace("", warning) + .replace("", getPageTitle(config)) + .replace("", getPageMetadataHtmlSnippet(config)) .replace("", getPageTitleSuffix(server?.getGristConfig())) .replace("", `` + tagManagerSnippet) .replace("", customHeadHtmlSnippet) - .replace("", ``); + .replace( + "", + `` + ); resp.status(options.status).type('html').send(content); }; } @@ -123,3 +133,51 @@ function configuredPageTitleSuffix() { const result = process.env.GRIST_PAGE_TITLE_SUFFIX; return result === "_blank" ? "" : result; } + +/** + * Returns a page title suitable for inserting into an HTML title element. + * + * Currently returns the document name if the page being requested is for a document, or + * a placeholder, "Loading...", that's updated in the client once the page has loaded. + * + * Note: The string returned is escaped and safe to insert into HTML. + */ +function getPageTitle(config: GristLoadConfig): string { + const maybeDoc = getDocFromConfig(config); + if (!maybeDoc) { return 'Loading...'; } + + return handlebars.Utils.escapeExpression(maybeDoc.name); +} + +/** + * Returns a string representation of 0 or more HTML metadata elements. + * + * Currently includes the document description and thumbnail if the requested page is + * for a document and the document has one set. + * + * Note: The string returned is escaped and safe to insert into HTML. + */ +function getPageMetadataHtmlSnippet(config: GristLoadConfig): string { + const metadataElements: string[] = []; + const maybeDoc = getDocFromConfig(config); + + const description = maybeDoc?.options?.description; + if (description) { + const content = handlebars.Utils.escapeExpression(description); + metadataElements.push(``); + } + + const thumbnail = maybeDoc?.options?.icon; + if (thumbnail) { + const content = handlebars.Utils.escapeExpression(thumbnail); + metadataElements.push(``); + } + + return metadataElements.join('\n'); +} + +function getDocFromConfig(config: GristLoadConfig): Document | null { + if (!config.getDoc || !config.assignmentId) { return null; } + + return config.getDoc[config.assignmentId] ?? null; +} diff --git a/package.json b/package.json index e49dbade..915aa222 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "@types/fs-extra": "5.0.4", "@types/image-size": "0.0.29", "@types/js-yaml": "3.11.2", + "@types/jsesc": "3.0.1", "@types/jsonwebtoken": "7.2.8", "@types/lodash": "4.14.117", "@types/lru-cache": "5.1.1", @@ -113,6 +114,7 @@ "fs-extra": "7.0.0", "grain-rpc": "0.1.7", "grainjs": "1.0.2", + "handlebars": "4.4.5", "highlight.js": "9.13.1", "http-proxy-agent": "5.0.0", "https-proxy-agent": "5.0.1", @@ -120,6 +122,7 @@ "image-size": "0.6.3", "jquery": "2.2.1", "js-yaml": "3.12.0", + "jsesc": "3.0.2", "jsonwebtoken": "8.3.0", "knockout": "3.5.0", "locale-currency": "0.0.2", diff --git a/static/app.html b/static/app.html index 44d01090..0d1f95a7 100644 --- a/static/app.html +++ b/static/app.html @@ -2,6 +2,7 @@ + @@ -15,7 +16,7 @@ -Loading...<!-- INSERT TITLE SUFFIX --> +<!-- INSERT TITLE --><!-- INSERT TITLE SUFFIX --> diff --git a/yarn.lock b/yarn.lock index 5541bb14..464bcbc1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3230,6 +3230,17 @@ gtoken@^5.0.4: google-p12-pem "^3.0.3" jws "^4.0.0" +handlebars@4.4.5: + version "4.4.5" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.4.5.tgz#1b1f94f9bfe7379adda86a8b73fb570265a0dddd" + integrity sha512-0Ce31oWVB7YidkaTq33ZxEbN+UDxMMgThvCe8ptgQViymL5DPis9uLdTA13MiRPhgvqyxIegugrP97iK3JeBHg== + dependencies: + neo-async "^2.6.0" + optimist "^0.6.1" + source-map "^0.6.1" + optionalDependencies: + uglify-js "^3.1.4" + har-schema@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" @@ -4352,6 +4363,11 @@ minimist@^1.1.0, minimist@^1.2.0, minimist@^1.2.5: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== +minimist@~0.0.1: + version "0.0.10" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" + integrity sha512-iotkTvxc+TwOm5Ieim8VnSNvCDjCK9S8G3scJ50ZthspSxa7jx50jkhYduuAtAjvfDUwSgOwf8+If99AlOEhyw== + minimist@~1.1.1: version "1.1.3" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.1.3.tgz#3bedfd91a92d39016fcfaa1c681e8faa1a1efda8" @@ -4575,7 +4591,7 @@ negotiator@0.6.2: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== -neo-async@^2.6.2: +neo-async@^2.6.0, neo-async@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== @@ -4820,6 +4836,14 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0: dependencies: wrappy "1" +optimist@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" + integrity sha512-snN4O4TkigujZphWLN0E//nQmm7790RYaE53DdL7ZYwee2D8DDo9/EyYiKUfN3rneWUjhJnueija3G9I2i0h3g== + dependencies: + minimist "~0.0.1" + wordwrap "~0.0.2" + os-browserify@~0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" @@ -6510,6 +6534,11 @@ typescript@4.7.4: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235" integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ== +uglify-js@^3.1.4: + version "3.16.3" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.16.3.tgz#94c7a63337ee31227a18d03b8a3041c210fd1f1d" + integrity sha512-uVbFqx9vvLhQg0iBaau9Z75AxWJ8tqM9AV890dIZCLApF4rTcyHwmAvLeEdYRs+BzYWu8Iw81F79ah0EfTXbaw== + uid-safe@2.1.5, uid-safe@~2.1.5: version "2.1.5" resolved "https://registry.yarnpkg.com/uid-safe/-/uid-safe-2.1.5.tgz#2b3d5c7240e8fc2e58f8aa269e5ee49c0857bd3a" @@ -6864,6 +6893,11 @@ winston@2.4.5: isstream "0.1.x" stack-trace "0.0.x" +wordwrap@~0.0.2: + version "0.0.3" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" + integrity sha512-1tMA907+V4QmxV7dbRvb4/8MaRALK6q9Abid3ndMYnbyo8piisCmeONVqVSXqQA3KaP4SLt5b7ud6E2sqP8TFw== + wrap-ansi@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09"