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, 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 = (...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 = (...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", warning)
+ .replace("", getPageTitleSuffix(server?.getGristConfig()))
.replace("", `` + tagManagerSnippet)
.replace("", customHeadHtmlSnippet)
.replace("", ``);
@@ -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 @@
- Grist
+ Account
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...
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