From 6b372fa6cdab2905e1a240b00907d5491eb15733 Mon Sep 17 00:00:00 2001 From: Alex Hall Date: Fri, 27 May 2022 13:03:56 +0200 Subject: [PATCH] (core) Allow configuring (mostly hiding) various little bits of UI Summary: Adds two new env vars GRIST_HIDE_UI_ELEMENTS and GRIST_PAGE_TITLE_SUFFIX which translate to values in GristLoadConfig that the server sends the client when loading. For checkin task https://gristlabs.getgrist.com/doc/check-ins/p/5#a1.s9.r1882.c19 Test Plan: Tested manually Reviewers: paulfitz Reviewed By: paulfitz Differential Revision: https://phab.getgrist.com/D3449 --- README.md | 2 ++ app/client/ui/AccountWidget.ts | 5 +++-- app/client/ui/AppHeader.ts | 3 ++- app/client/ui/AppUI.ts | 6 ++++-- app/client/ui/HomeLeftPane.ts | 2 ++ app/client/ui/LeftPanelCommon.ts | 5 ++++- app/client/ui/NotifyUI.ts | 3 ++- app/client/ui/SiteSwitcher.ts | 4 ++-- app/common/StringUnion.ts | 6 +++++- app/common/gristUrls.ts | 18 ++++++++++++++++++ app/server/lib/sendAppPage.ts | 18 +++++++++++++++++- static/account.html | 2 +- static/app.html | 2 +- static/error.html | 2 +- 14 files changed, 64 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 552f9cf3..4da13602 100644 --- a/README.md +++ b/README.md @@ -202,6 +202,7 @@ GRIST_DEFAULT_EMAIL | if set, login as this user if no other credentials present GRIST_DEFAULT_PRODUCT | if set, this controls enabled features and limits of new sites. See names of PRODUCTS in Product.ts. GRIST_DOMAIN | in hosted Grist, Grist is served from subdomains of this domain. Defaults to "getgrist.com". GRIST_EXPERIMENTAL_PLUGINS | enables experimental plugins +GRIST_HIDE_UI_ELEMENTS | comma-separated list of parts of the UI to hide. Allowed names of parts: `helpCenter,billing,templates,multiSite,multiAccounts` GRIST_HOME_INCLUDE_STATIC | if set, home server also serves static resources GRIST_HOST | hostname to use when listening on a port. GRIST_ID_PREFIX | for subdomains of form o-*, expect or produce o-${GRIST_ID_PREFIX}*. @@ -210,6 +211,7 @@ GRIST_MANAGED_WORKERS | if set, Grist can assume that if a url targeted at a doc GRIST_MAX_UPLOAD_ATTACHMENT_MB | max allowed size for attachments (0 or empty for unlimited). GRIST_MAX_UPLOAD_IMPORT_MB | max allowed size for imports (except .grist files) (0 or empty for unlimited). GRIST_ORG_IN_PATH | if true, encode org in path rather than domain +GRIST_PAGE_TITLE_SUFFIX | a string to append to the end of the `` in HTML documents. Defaults to `" - Grist"`. Set to `_blank` for no suffix at all. GRIST_PROXY_AUTH_HEADER | header which will be set by a (reverse) proxy webserver with an authorized users' email. This can be used as an alternative to a SAML service. GRIST_ROUTER_URL | optional url for an api that allows servers to be (un)registered with a load balancer GRIST_SERVE_SAME_ORIGIN | set to "true" to access home server and doc workers on the same protocol-host-port as the top-level page, same as for custom domains (careful, host header should be trustworthy) diff --git a/app/client/ui/AccountWidget.ts b/app/client/ui/AccountWidget.ts index 9bffce15..a42f9524 100644 --- a/app/client/ui/AccountWidget.ts +++ b/app/client/ui/AccountWidget.ts @@ -8,7 +8,7 @@ import {primaryButton} from 'app/client/ui2018/buttons'; import {colors, mediaDeviceNotSmall, testId, vars} from 'app/client/ui2018/cssVars'; import {icon} from 'app/client/ui2018/icons'; import {menu, menuDivider, menuItem, menuItemLink, menuSubHeader} from 'app/client/ui2018/menus'; -import {commonUrls} from 'app/common/gristUrls'; +import {commonUrls, shouldHideUiElement} from 'app/common/gristUrls'; import {FullUser} from 'app/common/LoginSessionAPI'; import * as roles from 'app/common/roles'; import {Organization, SUPPORT_EMAIL} from 'app/common/UserAPI'; @@ -111,6 +111,7 @@ export class AccountWidget extends Disposable { // Don't show on doc pages, or for personal orgs. null), + shouldHideUiElement("billing") ? null : // Show link to billing pages. currentOrg && !currentOrg.owner ? // For links, disabling with just a class is hard; easier to just not make it a link. @@ -128,7 +129,7 @@ export class AccountWidget extends Disposable { // In case of a single-org setup, skip all the account-switching UI. We'll also skip the // org-listing UI below. - this._appModel.topAppModel.isSingleOrg ? [] : [ + this._appModel.topAppModel.isSingleOrg || shouldHideUiElement("multiAccounts") ? [] : [ menuDivider(), menuSubHeader(dom.text((use) => use(users).length > 1 ? 'Switch Accounts' : 'Accounts')), dom.forEach(users, (_user) => { diff --git a/app/client/ui/AppHeader.ts b/app/client/ui/AppHeader.ts index 07d07a8b..494e8e80 100644 --- a/app/client/ui/AppHeader.ts +++ b/app/client/ui/AppHeader.ts @@ -2,6 +2,7 @@ import {urlState} from 'app/client/models/gristUrlState'; import {getTheme} from 'app/client/ui/CustomThemes'; import {cssLeftPane} from 'app/client/ui/PagePanels'; import {colors, testId, vars} from 'app/client/ui2018/cssVars'; +import {shouldHideUiElement} from 'app/common/gristUrls'; import * as version from 'app/common/version'; import {BindableValue, Disposable, dom, styled} from "grainjs"; import {menu, menuItem, menuItemLink, menuSubHeader} from 'app/client/ui2018/menus'; @@ -64,7 +65,7 @@ export class AppHeader extends Disposable { null), // Show link to billing pages. - currentOrg && !currentOrg.owner ? + currentOrg && !currentOrg.owner && !shouldHideUiElement("billing") ? // For links, disabling with just a class is hard; easier to just not make it a link. // TODO weasel menus should support disabling menuItemLink. (isBillingManager ? diff --git a/app/client/ui/AppUI.ts b/app/client/ui/AppUI.ts index 8b588a4b..faa7d153 100644 --- a/app/client/ui/AppUI.ts +++ b/app/client/ui/AppUI.ts @@ -18,6 +18,8 @@ import {RightPanel} from 'app/client/ui/RightPanel'; import {createTopBarDoc, createTopBarHome} from 'app/client/ui/TopBar'; import {WelcomePage} from 'app/client/ui/WelcomePage'; import {testId} from 'app/client/ui2018/cssVars'; +import {getPageTitleSuffix} from 'app/common/gristUrls'; +import {getGristConfig} from 'app/common/urlUtils'; import {Computed, dom, IDisposable, IDisposableOwner, Observable, replaceContent, subscribe} from 'grainjs'; // When integrating into the old app, we might in theory switch between new-style and old-style @@ -90,7 +92,7 @@ function pagePanelsHome(owner: IDisposableOwner, appModel: AppModel, app: App) { page === 'templates' ? 'Examples & Templates' : ws ? ws.name : appModel.currentOrgName ); - document.title = `${name} - Grist`; + document.title = `${name}${getPageTitleSuffix(getGristConfig())}`; })); return pagePanels({ @@ -127,7 +129,7 @@ function pagePanelsDoc(owner: IDisposableOwner, appModel: AppModel, appObj: App) // Set document title to strings like "DocName - Grist" owner.autoDispose(subscribe(pageModel.currentDocTitle, (use, docName) => { - document.title = `${docName} - Grist`; + document.title = `${docName}${getPageTitleSuffix(getGristConfig())}`; })); // Called after either panel is closed, opened, or resized. diff --git a/app/client/ui/HomeLeftPane.ts b/app/client/ui/HomeLeftPane.ts index d13326cf..7eb5df0b 100644 --- a/app/client/ui/HomeLeftPane.ts +++ b/app/client/ui/HomeLeftPane.ts @@ -12,6 +12,7 @@ import {colors, testId} from 'app/client/ui2018/cssVars'; import {icon} from 'app/client/ui2018/icons'; import {menu, menuIcon, menuItem, upgradableMenuItem, upgradeText} from 'app/client/ui2018/menus'; import {confirmModal} from 'app/client/ui2018/modals'; +import {shouldHideUiElement} from 'app/common/gristUrls'; import * as roles from 'app/common/roles'; import {Workspace} from 'app/common/UserAPI'; import {computed, dom, domComputed, DomElementArg, observable, Observable, styled} from 'grainjs'; @@ -96,6 +97,7 @@ export function createHomeLeftPane(leftPanelOpen: Observable<boolean>, home: Hom )), cssTools( cssPageEntry( + dom.hide(shouldHideUiElement("templates")), cssPageEntry.cls('-selected', (use) => use(home.currentPage) === "templates"), cssPageLink(cssPageIcon('FieldTable'), cssLinkText("Examples & Templates"), urlState().setLinkUrl({homePage: "templates"}), diff --git a/app/client/ui/LeftPanelCommon.ts b/app/client/ui/LeftPanelCommon.ts index d9f2b49c..d0f5a13b 100644 --- a/app/client/ui/LeftPanelCommon.ts +++ b/app/client/ui/LeftPanelCommon.ts @@ -17,7 +17,7 @@ import {beaconOpenMessage} from 'app/client/lib/helpScout'; import {AppModel} from 'app/client/models/AppModel'; import {colors, testId, vars} from 'app/client/ui2018/cssVars'; import {icon} from 'app/client/ui2018/icons'; -import {commonUrls} from 'app/common/gristUrls'; +import {commonUrls, shouldHideUiElement} from 'app/common/gristUrls'; import {dom, DomContents, Observable, styled} from 'grainjs'; /** @@ -25,6 +25,9 @@ import {dom, DomContents, Observable, styled} from 'grainjs'; * HelpCenter in a new tab. */ export function createHelpTools(appModel: AppModel, spacer = true): DomContents { + if (shouldHideUiElement("helpCenter")) { + return []; + } return [ spacer ? cssSpacer() : null, cssSplitPageEntry( diff --git a/app/client/ui/NotifyUI.ts b/app/client/ui/NotifyUI.ts index ca3fcbca..b1bb6428 100644 --- a/app/client/ui/NotifyUI.ts +++ b/app/client/ui/NotifyUI.ts @@ -8,7 +8,7 @@ import {colors, vars} from 'app/client/ui2018/cssVars'; import {icon} from 'app/client/ui2018/icons'; import {IconName} from "app/client/ui2018/IconList"; import {menuCssClass} from 'app/client/ui2018/menus'; -import {commonUrls} from 'app/common/gristUrls'; +import {commonUrls, shouldHideUiElement} from 'app/common/gristUrls'; import {dom, makeTestId, styled} from 'grainjs'; import {cssMenu, defaultMenuOptions, IOpenController, setPopupToCreateDom} from 'popweasel'; @@ -142,6 +142,7 @@ function buildNotifyDropdown(ctl: IOpenController, notifier: Notifier, appModel: cssDropdownContent( cssDropdownHeader( cssDropdownHeaderTitle('Notifications'), + shouldHideUiElement("helpCenter") ? null : cssDropdownFeedbackLink( cssDropdownFeedbackIcon('Feedback'), 'Give feedback', diff --git a/app/client/ui/SiteSwitcher.ts b/app/client/ui/SiteSwitcher.ts index 62454f61..40494619 100644 --- a/app/client/ui/SiteSwitcher.ts +++ b/app/client/ui/SiteSwitcher.ts @@ -1,4 +1,4 @@ -import {commonUrls, getSingleOrg} from 'app/common/gristUrls'; +import {commonUrls, getSingleOrg, shouldHideUiElement} from 'app/common/gristUrls'; import {getOrgName} from 'app/common/UserAPI'; import {dom, makeTestId, styled} from 'grainjs'; import {AppModel} from 'app/client/models/AppModel'; @@ -14,7 +14,7 @@ const testId = makeTestId('test-site-switcher-'); */ export function maybeAddSiteSwitcherSection(appModel: AppModel) { const orgs = appModel.topAppModel.orgs; - return dom.maybe((use) => use(orgs).length > 0 && !getSingleOrg(), () => [ + return dom.maybe((use) => use(orgs).length > 0 && !getSingleOrg() && !shouldHideUiElement("multiSite"), () => [ menuDivider(), buildSiteSwitcher(appModel), ]); diff --git a/app/common/StringUnion.ts b/app/common/StringUnion.ts index 3abccb14..c61eecd9 100644 --- a/app/common/StringUnion.ts +++ b/app/common/StringUnion.ts @@ -33,6 +33,10 @@ export const StringUnion = <UnionType extends string>(...values: UnionType[]) => return value; }; + const checkAll = (arr: string[]): UnionType[] => { + return arr.map(check); + }; + /** * StringUnion.parse(value) returns value when it's valid, and undefined otherwise. */ @@ -40,6 +44,6 @@ export const StringUnion = <UnionType extends string>(...values: UnionType[]) => return value != null && guard(value) ? value : undefined; }; - const unionNamespace = {guard, check, parse, values}; + const unionNamespace = {guard, check, parse, values, checkAll}; return Object.freeze(unionNamespace as typeof unionNamespace & {type: UnionType}); }; diff --git a/app/common/gristUrls.ts b/app/common/gristUrls.ts index 72287abf..0164ff86 100644 --- a/app/common/gristUrls.ts +++ b/app/common/gristUrls.ts @@ -5,6 +5,7 @@ import {encodeQueryParams, isAffirmative} from 'app/common/gutil'; import {LocalPlugin} from 'app/common/plugin'; import {StringUnion} from 'app/common/StringUnion'; import {UIRowId} from 'app/common/UIRowId'; +import {getGristConfig} from 'app/common/urlUtils'; import {Document} from 'app/common/UserAPI'; import clone = require('lodash/clone'); import pickBy = require('lodash/pickBy'); @@ -520,6 +521,23 @@ export interface GristLoadConfig { tagManagerId?: string; activation?: ActivationState; + + // Parts of the UI to hide + hideUiElements?: IHideableUiElement[]; + + // String to append to the end of the HTML document.title + pageTitleSuffix?: string; +} + +export const HideableUiElements = StringUnion("helpCenter", "billing", "templates", "multiSite", "multiAccounts"); +export type IHideableUiElement = typeof HideableUiElements.type; + +export function shouldHideUiElement(elem: IHideableUiElement): boolean { + return (getGristConfig().hideUiElements || []).includes(elem); +} + +export function getPageTitleSuffix(config?: GristLoadConfig) { + return config?.pageTitleSuffix ?? " - Grist"; } /** diff --git a/app/server/lib/sendAppPage.ts b/app/server/lib/sendAppPage.ts index b773868a..61e8bd37 100644 --- a/app/server/lib/sendAppPage.ts +++ b/app/server/lib/sendAppPage.ts @@ -1,4 +1,4 @@ -import {GristLoadConfig} from 'app/common/gristUrls'; +import {getPageTitleSuffix, GristLoadConfig, HideableUiElements, IHideableUiElement} from 'app/common/gristUrls'; import {getTagManagerSnippet} from 'app/common/tagManager'; import {isAnonymousUser, RequestWithLogin} from 'app/server/lib/Authorizer'; import {RequestWithOrg} from 'app/server/lib/extractOrg'; @@ -38,6 +38,8 @@ export function makeGristConfig(homeUrl: string|null, extra: Partial<GristLoadCo pathOnly, supportAnon: shouldSupportAnon(), supportEngines: getSupportedEngineChoices(), + hideUiElements: getHiddenUiElements(), + pageTitleSuffix: configuredPageTitleSuffix(), pluginUrl, stripeAPIKey: process.env.STRIPE_PUBLIC_API_KEY, googleClientId: process.env.GOOGLE_CLIENT_ID, @@ -110,6 +112,7 @@ export function makeSendAppPage(opts: { // Temporary changes end. const content = fileContent .replace("<!-- INSERT WARNING -->", warning) + .replace("<!-- INSERT TITLE SUFFIX -->", getPageTitleSuffix(server?.getGristConfig())) .replace("<!-- INSERT BASE -->", `<base href="${staticBaseUrl}">` + tagManagerSnippet) .replace("<!-- INSERT CUSTOM -->", customHeadHtmlSnippet) .replace("<!-- INSERT CONFIG -->", `<script>window.gristConfig = ${JSON.stringify(config)};</script>`); @@ -121,3 +124,16 @@ function shouldSupportAnon() { // Enable UI for anonymous access if a flag is explicitly set in the environment return process.env.GRIST_SUPPORT_ANON === "true"; } + +function getHiddenUiElements(): IHideableUiElement[] { + const str = process.env.GRIST_HIDE_UI_ELEMENTS; + if (!str) { + return []; + } + return HideableUiElements.checkAll(str.split(",")); +} + +function configuredPageTitleSuffix() { + const result = process.env.GRIST_PAGE_TITLE_SUFFIX; + return result === "_blank" ? "" : result; +} diff --git a/static/account.html b/static/account.html index d0520b78..67509f26 100644 --- a/static/account.html +++ b/static/account.html @@ -6,7 +6,7 @@ <link rel="icon" type="image/x-icon" href="icons/favicon.png" /> <link rel="stylesheet" href="icons/icons.css"> <!-- INSERT CUSTOM --> - <title>Grist + Account<!-- INSERT TITLE SUFFIX --> diff --git a/static/app.html b/static/app.html index 54a75830..44d01090 100644 --- a/static/app.html +++ b/static/app.html @@ -15,7 +15,7 @@ -Grist +Loading...<!-- INSERT TITLE SUFFIX --> diff --git a/static/error.html b/static/error.html index 08a81286..501ffd2e 100644 --- a/static/error.html +++ b/static/error.html @@ -6,7 +6,7 @@ - Grist + Error<!-- INSERT TITLE SUFFIX -->