2022-04-21 17:57:33 +00:00
|
|
|
import {DocUsageBanner} from 'app/client/components/DocUsageBanner';
|
2022-05-26 06:47:26 +00:00
|
|
|
import {SiteUsageBanner} from 'app/client/components/SiteUsageBanner';
|
2020-10-02 15:10:00 +00:00
|
|
|
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';
|
2021-08-18 17:49:34 +00:00
|
|
|
import {AppHeader} from 'app/client/ui/AppHeader';
|
2021-01-14 11:20:13 +00:00
|
|
|
import {createBottomBarDoc} from 'app/client/ui/BottomBar';
|
2020-10-02 15:10:00 +00:00
|
|
|
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';
|
2021-02-08 19:00:02 +00:00
|
|
|
import {testId} from 'app/client/ui2018/cssVars';
|
2022-05-27 11:03:56 +00:00
|
|
|
import {getPageTitleSuffix} from 'app/common/gristUrls';
|
|
|
|
import {getGristConfig} from 'app/common/urlUtils';
|
2020-10-02 15:10:00 +00:00
|
|
|
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;'
|
2022-02-08 09:26:29 +00:00
|
|
|
});
|
2020-10-02 15:10:00 +00:00
|
|
|
|
|
|
|
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
|
2022-02-19 09:46:49 +00:00
|
|
|
// shared publicly without being shared specifically with the current user.
|
2020-10-02 15:10:00 +00:00
|
|
|
if (appModel.pageType.get() !== 'doc') {
|
|
|
|
return createForbiddenPage(appModel);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return createOtherErrorPage(appModel, err && err.error);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return dom.domComputed(appModel.pageType, (pageType) => {
|
|
|
|
if (pageType === 'home') {
|
2021-08-05 15:12:46 +00:00
|
|
|
return dom.create(pagePanelsHome, appModel, appObj);
|
2020-10-02 15:10:00 +00:00
|
|
|
} 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);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-08-05 15:12:46 +00:00
|
|
|
function pagePanelsHome(owner: IDisposableOwner, appModel: AppModel, app: App) {
|
|
|
|
const pageModel = HomeModelImpl.create(owner, appModel, app.clientScope);
|
2020-10-02 15:10:00 +00:00
|
|
|
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' :
|
2021-07-28 19:02:06 +00:00
|
|
|
page === 'templates' ? 'Examples & Templates' :
|
2020-10-02 15:10:00 +00:00
|
|
|
ws ? ws.name : appModel.currentOrgName
|
|
|
|
);
|
2022-05-27 11:03:56 +00:00
|
|
|
document.title = `${name}${getPageTitleSuffix(getGristConfig())}`;
|
2020-10-02 15:10:00 +00:00
|
|
|
}));
|
|
|
|
|
|
|
|
return pagePanels({
|
|
|
|
leftPanel: {
|
|
|
|
panelWidth: Observable.create(owner, 240),
|
|
|
|
panelOpen: leftPanelOpen,
|
|
|
|
hideOpener: true,
|
2021-08-18 17:49:34 +00:00
|
|
|
header: dom.create(AppHeader, appModel.currentOrgName, appModel),
|
2020-10-02 15:10:00 +00:00
|
|
|
content: createHomeLeftPane(leftPanelOpen, pageModel),
|
|
|
|
},
|
|
|
|
headerMain: createTopBarHome(appModel),
|
|
|
|
contentMain: createDocMenu(pageModel),
|
2022-06-06 16:21:26 +00:00
|
|
|
contentTop: dom.create(SiteUsageBanner, appModel),
|
2020-10-02 15:10:00 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
2022-05-18 16:05:37 +00:00
|
|
|
appObj.setDocPageModel(pageModel);
|
2021-02-08 19:00:02 +00:00
|
|
|
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);
|
2020-10-02 15:10:00 +00:00
|
|
|
|
|
|
|
// 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) => {
|
2022-05-27 11:03:56 +00:00
|
|
|
document.title = `${docName}${getPageTitleSuffix(getGristConfig())}`;
|
2020-10-02 15:10:00 +00:00
|
|
|
}));
|
|
|
|
|
|
|
|
// 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,
|
2021-11-05 14:47:17 +00:00
|
|
|
header: dom.create(AppHeader, appModel.currentOrgName || pageModel.currentOrgName, appModel, pageModel),
|
2021-02-08 19:00:02 +00:00
|
|
|
content: pageModel.createLeftPane(leftPanelOpen),
|
2020-10-02 15:10:00 +00:00
|
|
|
},
|
|
|
|
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,
|
2022-04-21 17:57:33 +00:00
|
|
|
contentTop: dom.create(DocUsageBanner, pageModel),
|
|
|
|
contentBottom: dom.create(createBottomBarDoc, pageModel, leftPanelOpen, rightPanelOpen),
|
2020-10-02 15:10:00 +00:00
|
|
|
});
|
|
|
|
}
|