gristlabs_grist-core/app/client/ui/AppUI.ts
George Gevoian 24fc3a2d00 (core) Redesign examples and templates UI
Summary:
The old Examples and Templates workspace is now
a page that pulls templates from a new public Grist Templates org.
The All Documents view will pull featured templates from that org, where
featured templates are simply pinned documents in Grist Templates. The
Examples and Templates page will also show the featured templates, as
well as the rest of the available templates organized by category. The
categories are equivalent to workspaces in Grist Templates, and are
generated dynamically.

Test Plan: Browser tests.

Reviewers: paulfitz, dsagal

Reviewed By: paulfitz, dsagal

Subscribers: dsagal, paulfitz, jarek

Differential Revision: https://phab.getgrist.com/D2930
2021-07-28 12:29:03 -07:00

155 lines
7.0 KiB
TypeScript

import {domAsync} from 'app/client/lib/domAsync';
import {loadBillingPage} from 'app/client/lib/imports';
import {createSessionObs, isBoolean, isNumber} from 'app/client/lib/sessionObs';
import {AppModel, TopAppModel} from 'app/client/models/AppModel';
import {DocPageModelImpl} from 'app/client/models/DocPageModel';
import {HomeModelImpl} from 'app/client/models/HomeModel';
import {App} from 'app/client/ui/App';
import {appHeader} from 'app/client/ui/AppHeader';
import {createBottomBarDoc} from 'app/client/ui/BottomBar';
import {createDocMenu} from 'app/client/ui/DocMenu';
import {createForbiddenPage, createNotFoundPage, createOtherErrorPage} from 'app/client/ui/errorPages';
import {createHomeLeftPane} from 'app/client/ui/HomeLeftPane';
import {buildSnackbarDom} from 'app/client/ui/NotifyUI';
import {pagePanels} from 'app/client/ui/PagePanels';
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 {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
// content. This function allows disposing the created content by old-style code.
// TODO once #newui is gone, we don't need to worry about this being disposable.
// appObj is the App object from app/client/ui/App.ts
export function createAppUI(topAppModel: TopAppModel, appObj: App): IDisposable {
const content = dom.maybe(topAppModel.appObs, (appModel) => [
createMainPage(appModel, appObj),
buildSnackbarDom(appModel.notifier, appModel),
]);
dom.update(document.body, content, {
// Cancel out bootstrap's overrides.
style: 'font-family: inherit; font-size: inherit; line-height: inherit;'
});
function dispose() {
// Return value of dom.maybe() / dom.domComputed() is a pair of markers with a function that
// replaces content between them when an observable changes. It's uncommon to dispose the set
// with the markers, and grainjs doesn't provide a helper, but we can accomplish it by
// disposing the markers. They will automatically trigger the disposal of the included
// content. This avoids the need to wrap the contents in another layer of a dom element.
const [beginMarker, endMarker] = content;
replaceContent(beginMarker, endMarker, null);
dom.domDispose(beginMarker);
dom.domDispose(endMarker);
document.body.removeChild(beginMarker);
document.body.removeChild(endMarker);
}
return {dispose};
}
function createMainPage(appModel: AppModel, appObj: App) {
if (!appModel.currentOrg && appModel.pageType.get() !== 'welcome') {
const err = appModel.orgError;
if (err && err.status === 404) {
return createNotFoundPage(appModel);
} else if (err && (err.status === 401 || err.status === 403)) {
// Generally give access denied error.
// The exception is for document pages, where we want to allow access to documents
// shared publically without being shared specifically with the current user.
if (appModel.pageType.get() !== 'doc') {
return createForbiddenPage(appModel);
}
} else {
return createOtherErrorPage(appModel, err && err.error);
}
}
return dom.domComputed(appModel.pageType, (pageType) => {
if (pageType === 'home') {
return dom.create(pagePanelsHome, appModel);
} else if (pageType === 'billing') {
return domAsync(loadBillingPage().then(bp => dom.create(bp.BillingPage, appModel)));
} else if (pageType === 'welcome') {
return dom.create(WelcomePage, appModel);
} else {
return dom.create(pagePanelsDoc, appModel, appObj);
}
});
}
function pagePanelsHome(owner: IDisposableOwner, appModel: AppModel) {
const pageModel = HomeModelImpl.create(owner, appModel);
const leftPanelOpen = Observable.create(owner, true);
// Set document title to strings like "Home - Grist" or "Org Name - Grist".
owner.autoDispose(subscribe(pageModel.currentPage, pageModel.currentWS, (use, page, ws) => {
const name = (
page === 'trash' ? 'Trash' :
page === 'templates' ? 'Examples & Templates' :
ws ? ws.name : appModel.currentOrgName
);
document.title = `${name} - Grist`;
}));
return pagePanels({
leftPanel: {
panelWidth: Observable.create(owner, 240),
panelOpen: leftPanelOpen,
hideOpener: true,
header: appHeader(appModel.currentOrgName, appModel.topAppModel.productFlavor),
content: createHomeLeftPane(leftPanelOpen, pageModel),
},
headerMain: createTopBarHome(appModel),
contentMain: createDocMenu(pageModel),
});
}
function pagePanelsDoc(owner: IDisposableOwner, appModel: AppModel, appObj: App) {
const pageModel = DocPageModelImpl.create(owner, appObj, appModel);
// To simplify manual inspection in the common case, keep the most recently created
// DocPageModel available as a global variable.
(window as any).gristDocPageModel = pageModel;
const leftPanelOpen = createSessionObs<boolean>(owner, "leftPanelOpen", true, isBoolean);
const rightPanelOpen = createSessionObs<boolean>(owner, "rightPanelOpen", false, isBoolean);
const leftPanelWidth = createSessionObs<number>(owner, "leftPanelWidth", 240, isNumber);
const rightPanelWidth = createSessionObs<number>(owner, "rightPanelWidth", 240, isNumber);
// The RightPanel component gets created only when an instance of GristDoc is set in pageModel.
// use.owner is a feature of grainjs to make the new RightPanel owned by the computed itself:
// each time the gristDoc observable changes (and triggers the callback), the previously-created
// instance of RightPanel will get disposed.
const rightPanel = Computed.create(owner, pageModel.gristDoc, (use, gristDoc) =>
gristDoc ? RightPanel.create(use.owner, gristDoc, rightPanelOpen) : null);
// Set document title to strings like "DocName - Grist"
owner.autoDispose(subscribe(pageModel.currentDocTitle, (use, docName) => {
document.title = `${docName} - Grist`;
}));
// Called after either panel is closed, opened, or resized.
function onResize() {
const gristDoc = pageModel.gristDoc.get();
if (gristDoc) { gristDoc.resizeEmitter.emit(); }
}
return pagePanels({
leftPanel: {
panelWidth: leftPanelWidth,
panelOpen: leftPanelOpen,
header: appHeader(appModel.currentOrgName || pageModel.currentOrgName, appModel.topAppModel.productFlavor),
content: pageModel.createLeftPane(leftPanelOpen),
},
rightPanel: {
panelWidth: rightPanelWidth,
panelOpen: rightPanelOpen,
header: dom.maybe(rightPanel, (panel) => panel.header),
content: dom.maybe(rightPanel, (panel) => panel.content),
},
headerMain: dom.create(createTopBarDoc, appModel, pageModel, appObj.allCommands),
contentMain: dom.maybe(pageModel.gristDoc, (gristDoc) => gristDoc.buildDom()),
onResize,
testId,
contentBottom: dom.create(createBottomBarDoc, pageModel, leftPanelOpen, rightPanelOpen)
});
}