mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
5dc4706dc7
This adds some remaining parts of the boot page to the admin panel, and then removes the boot page.
587 lines
21 KiB
TypeScript
587 lines
21 KiB
TypeScript
import {BehavioralPromptsManager} from 'app/client/components/BehavioralPromptsManager';
|
|
import {hooks} from 'app/client/Hooks';
|
|
import {get as getBrowserGlobals} from 'app/client/lib/browserGlobals';
|
|
import {makeT} from 'app/client/lib/localization';
|
|
import {sessionStorageObs} from 'app/client/lib/localStorageObs';
|
|
import {error} from 'app/client/lib/log';
|
|
import {reportError, setErrorNotifier} from 'app/client/models/errors';
|
|
import {urlState} from 'app/client/models/gristUrlState';
|
|
import {Notifier} from 'app/client/models/NotifyModel';
|
|
import {getFlavor, ProductFlavor} from 'app/client/ui/CustomThemes';
|
|
import {buildNewSiteModal, buildUpgradeModal} from 'app/client/ui/ProductUpgrades';
|
|
import {SupportGristNudge} from 'app/client/ui/SupportGristNudge';
|
|
import {gristThemePrefs} from 'app/client/ui2018/theme';
|
|
import {AsyncCreate} from 'app/common/AsyncCreate';
|
|
import {PlanSelection} from 'app/common/BillingAPI';
|
|
import {ICustomWidget} from 'app/common/CustomWidget';
|
|
import {OrgUsageSummary} from 'app/common/DocUsage';
|
|
import {Features, isFreePlan, isLegacyPlan, mergedFeatures, Product} from 'app/common/Features';
|
|
import {GristLoadConfig, IGristUrlState} from 'app/common/gristUrls';
|
|
import {FullUser} from 'app/common/LoginSessionAPI';
|
|
import {LocalPlugin} from 'app/common/plugin';
|
|
import {DismissedPopup, DismissedReminder, UserPrefs} from 'app/common/Prefs';
|
|
import {isOwner, isOwnerOrEditor} from 'app/common/roles';
|
|
import {getTagManagerScript} from 'app/common/tagManager';
|
|
import {getDefaultThemePrefs, ThemePrefs, ThemePrefsChecker} from 'app/common/ThemePrefs';
|
|
import {getGristConfig} from 'app/common/urlUtils';
|
|
import {ExtendedUser} from 'app/common/UserAPI';
|
|
import {getOrgName, isTemplatesOrg, Organization, OrgError, UserAPI, UserAPIImpl} from 'app/common/UserAPI';
|
|
import {getUserPrefObs, getUserPrefsObs, markAsSeen} from 'app/client/models/UserPrefs';
|
|
import {bundleChanges, Computed, Disposable, Observable, subscribe} from 'grainjs';
|
|
|
|
const t = makeT('AppModel');
|
|
|
|
// Reexported for convenience.
|
|
export {reportError} from 'app/client/models/errors';
|
|
|
|
export type PageType =
|
|
| "doc"
|
|
| "home"
|
|
| "billing"
|
|
| "welcome"
|
|
| "account"
|
|
| "admin"
|
|
| "activation";
|
|
|
|
const G = getBrowserGlobals('document', 'window');
|
|
|
|
// TopAppModel is the part of the app model that persists across org and user switches.
|
|
export interface TopAppModel {
|
|
api: UserAPI;
|
|
isSingleOrg: boolean;
|
|
productFlavor: ProductFlavor;
|
|
currentSubdomain: Observable<string|undefined>;
|
|
|
|
notifier: Notifier;
|
|
plugins: LocalPlugin[];
|
|
|
|
// Everything else gets fully rebuilt when the org/user changes. This is to ensure that
|
|
// different parts of the code aren't using different users/orgs while the switch is pending.
|
|
appObs: Observable<AppModel|null>;
|
|
|
|
orgs: Observable<Organization[]>;
|
|
users: Observable<FullUser[]>;
|
|
|
|
customWidgets: Observable<ICustomWidget[]|null>;
|
|
|
|
// Reinitialize the app. This is called when org or user changes.
|
|
initialize(): void;
|
|
|
|
// Rebuilds the AppModel and consequently the AppUI, without changing the user or the org.
|
|
reload(): void;
|
|
|
|
/**
|
|
* Returns the UntrustedContentOrigin use settings. Throws if not defined.
|
|
*/
|
|
getUntrustedContentOrigin(): string;
|
|
/**
|
|
* Reloads orgs and accounts for current user.
|
|
*/
|
|
fetchUsersAndOrgs(): Promise<void>;
|
|
|
|
/**
|
|
* Enumerate the widgets in the WidgetRepository for this installation
|
|
* of Grist.
|
|
*/
|
|
getWidgets(): Promise<ICustomWidget[]>;
|
|
|
|
/**
|
|
* Reload cached list of widgets, for testing purposes.
|
|
*/
|
|
testReloadWidgets(): Promise<void>;
|
|
}
|
|
|
|
/**
|
|
* AppModel is specific to the currently loaded organization and active user. It gets rebuilt when
|
|
* we switch the current organization or the current user.
|
|
*/
|
|
export interface AppModel {
|
|
topAppModel: TopAppModel;
|
|
api: UserAPI;
|
|
|
|
currentUser: ExtendedUser|null;
|
|
currentValidUser: ExtendedUser|null; // Like currentUser, but null when anonymous
|
|
|
|
currentOrg: Organization|null; // null if no access to currentSubdomain
|
|
currentOrgName: string; // Our best guess for human-friendly name.
|
|
currentOrgUsage: Observable<OrgUsageSummary|null>;
|
|
isPersonal: boolean; // Is it a personal site?
|
|
isTeamSite: boolean; // Is it a team site?
|
|
isLegacySite: boolean; // Is it a legacy site?
|
|
isTemplatesSite: boolean; // Is it the templates site?
|
|
orgError?: OrgError; // If currentOrg is null, the error that caused it.
|
|
lastVisitedOrgDomain: Observable<string|null>;
|
|
|
|
currentProduct: Product|null; // The current org's product.
|
|
currentPriceId: string|null; // The current org's stripe plan id.
|
|
currentFeatures: Features|null; // Features of the current org's product.
|
|
|
|
userPrefsObs: Observable<UserPrefs>;
|
|
themePrefs: Observable<ThemePrefs>;
|
|
/**
|
|
* Popups that user has seen.
|
|
*/
|
|
dismissedPopups: Observable<DismissedPopup[]>;
|
|
dismissedWelcomePopups: Observable<DismissedReminder[]>;
|
|
|
|
pageType: Observable<PageType>;
|
|
needsOrg: Observable<boolean>;
|
|
|
|
notifier: Notifier;
|
|
planName: string|null;
|
|
|
|
behavioralPromptsManager: BehavioralPromptsManager;
|
|
|
|
supportGristNudge: SupportGristNudge;
|
|
|
|
refreshOrgUsage(): Promise<void>;
|
|
showUpgradeModal(): Promise<void>;
|
|
showNewSiteModal(): Promise<void>;
|
|
isBillingManager(): boolean; // If user is a billing manager for this org
|
|
isSupport(): boolean; // If user is a Support user
|
|
isOwner(): boolean; // If user is an owner of this org
|
|
isOwnerOrEditor(): boolean; // If user is an owner or editor of this org
|
|
isInstallAdmin(): boolean; // Is user an admin of this installation
|
|
dismissPopup(name: DismissedPopup, isSeen: boolean): void; // Mark popup as dismissed or not.
|
|
switchUser(user: FullUser, org?: string): Promise<void>;
|
|
isFreePlan(): boolean;
|
|
}
|
|
|
|
export interface TopAppModelOptions {
|
|
/** Defaults to true. */
|
|
useApi?: boolean;
|
|
}
|
|
|
|
export class TopAppModelImpl extends Disposable implements TopAppModel {
|
|
public readonly isSingleOrg: boolean;
|
|
public readonly productFlavor: ProductFlavor;
|
|
|
|
public readonly currentSubdomain = Computed.create(this, urlState().state, (use, s) => s.org);
|
|
public readonly notifier = Notifier.create(this);
|
|
public readonly appObs = Observable.create<AppModel|null>(this, null);
|
|
public readonly orgs = Observable.create<Organization[]>(this, []);
|
|
public readonly users = Observable.create<FullUser[]>(this, []);
|
|
public readonly plugins: LocalPlugin[] = [];
|
|
public readonly customWidgets = Observable.create<ICustomWidget[]|null>(this, null);
|
|
private readonly _gristConfig?: GristLoadConfig;
|
|
// Keep a list of available widgets, once requested, so we don't have to
|
|
// keep reloading it. Downside: browser page will need reloading to pick
|
|
// up new widgets - that seems ok.
|
|
private readonly _widgets: AsyncCreate<ICustomWidget[]>;
|
|
|
|
constructor(window: {gristConfig?: GristLoadConfig},
|
|
public readonly api: UserAPI = newUserAPIImpl(),
|
|
public readonly options: TopAppModelOptions = {}
|
|
) {
|
|
super();
|
|
setErrorNotifier(this.notifier);
|
|
this.isSingleOrg = Boolean(window.gristConfig && window.gristConfig.singleOrg);
|
|
this.productFlavor = getFlavor(window.gristConfig && window.gristConfig.org);
|
|
this._gristConfig = window.gristConfig;
|
|
this._widgets = new AsyncCreate<ICustomWidget[]>(async () => {
|
|
const widgets = this.options.useApi === false ? [] : await this.api.getWidgets();
|
|
this.customWidgets.set(widgets);
|
|
return widgets;
|
|
});
|
|
|
|
// Initially, and on any change to subdomain, call initialize() to get the full Organization
|
|
// and the FullUser to use for it (the user may change when switching orgs).
|
|
this.autoDispose(subscribe(this.currentSubdomain, (use) => this.initialize()));
|
|
this.plugins = this._gristConfig?.plugins || [];
|
|
|
|
if (this.options.useApi !== false) {
|
|
this.fetchUsersAndOrgs().catch(reportError);
|
|
}
|
|
}
|
|
|
|
public initialize(): void {
|
|
this._doInitialize().catch(reportError);
|
|
}
|
|
|
|
// Rebuilds the AppModel and consequently the AppUI, etc, without changing the user or the org.
|
|
public reload(): void {
|
|
const app = this.appObs.get();
|
|
if (app) {
|
|
const {currentUser, currentOrg, orgError} = app;
|
|
AppModelImpl.create(this.appObs, this, currentUser, currentOrg, orgError);
|
|
}
|
|
}
|
|
|
|
public async getWidgets(): Promise<ICustomWidget[]> {
|
|
return this._widgets.get();
|
|
}
|
|
|
|
public async testReloadWidgets() {
|
|
console.log("testReloadWidgets");
|
|
this._widgets.clear();
|
|
this.customWidgets.set(null);
|
|
console.log("testReloadWidgets cleared and nulled");
|
|
const result = await this.getWidgets();
|
|
console.log("testReloadWidgets got", {result});
|
|
}
|
|
|
|
public getUntrustedContentOrigin() {
|
|
if (G.window.isRunningUnderElectron) {
|
|
// when loaded within webviews it is safe to serve plugin's content from the same domain
|
|
return "";
|
|
}
|
|
|
|
const origin = this._gristConfig?.pluginUrl;
|
|
if (!origin) {
|
|
throw new Error("Missing untrustedContentOrigin configuration");
|
|
}
|
|
if (origin.match(/:[0-9]+$/)) {
|
|
// Port number already specified, no need to add.
|
|
return origin;
|
|
}
|
|
return origin + ":" + G.window.location.port;
|
|
}
|
|
|
|
public async fetchUsersAndOrgs() {
|
|
const data = await this.api.getSessionAll();
|
|
if (this.isDisposed()) { return; }
|
|
bundleChanges(() => {
|
|
this.users.set(data.users);
|
|
this.orgs.set(data.orgs);
|
|
});
|
|
}
|
|
|
|
private async _doInitialize() {
|
|
this.appObs.set(null);
|
|
if (this.options.useApi === false) {
|
|
AppModelImpl.create(this.appObs, this, null, null, {error: 'no-api', status: 500});
|
|
return;
|
|
}
|
|
try {
|
|
const {user, org, orgError} = await this.api.getSessionActive();
|
|
if (this.isDisposed()) { return; }
|
|
if (org) {
|
|
// Check that our domain matches what the api returns.
|
|
const state = urlState().state.get();
|
|
if (state.org !== org.domain && org.domain !== null) {
|
|
// If not, redirect. This is to allow vanity domains
|
|
// to "stick" only if paid for.
|
|
await urlState().pushUrl({...state, org: org.domain});
|
|
}
|
|
if (org.billingAccount && org.billingAccount.product &&
|
|
org.billingAccount.product.name === 'suspended') {
|
|
this.notifier.createUserMessage(
|
|
t("This team site is suspended. Documents can be read, but not modified."),
|
|
{actions: ['renew', 'personal']}
|
|
);
|
|
}
|
|
}
|
|
AppModelImpl.create(this.appObs, this, user, org, orgError);
|
|
} catch (err) {
|
|
// tslint:disable-next-line:no-console
|
|
console.log(`getSessionActive() failed: ${err}`);
|
|
if (this.isDisposed()) { return; }
|
|
AppModelImpl.create(this.appObs, this, null, null, {error: err.message, status: err.status || 500});
|
|
}
|
|
}
|
|
}
|
|
|
|
export class AppModelImpl extends Disposable implements AppModel {
|
|
public readonly api: UserAPI = this.topAppModel.api;
|
|
|
|
// Compute currentValidUser, turning anonymous into null.
|
|
public readonly currentValidUser: ExtendedUser|null =
|
|
this.currentUser && !this.currentUser.anonymous ? this.currentUser : null;
|
|
|
|
// Figure out the org name, or blank if details are unavailable.
|
|
public readonly currentOrgName = getOrgNameOrGuest(this.currentOrg, this.currentUser);
|
|
|
|
public readonly currentOrgUsage: Observable<OrgUsageSummary|null> = Observable.create(this, null);
|
|
|
|
public readonly lastVisitedOrgDomain = this.autoDispose(sessionStorageObs('grist-last-visited-org-domain'));
|
|
|
|
public readonly currentProduct = this.currentOrg?.billingAccount?.product ?? null;
|
|
public readonly currentPriceId = this.currentOrg?.billingAccount?.stripePlanId ?? null;
|
|
public readonly currentFeatures = mergedFeatures(
|
|
this.currentProduct?.features ?? null,
|
|
this.currentOrg?.billingAccount?.features ?? null
|
|
);
|
|
|
|
public readonly isPersonal = Boolean(this.currentOrg?.owner);
|
|
public readonly isTeamSite = Boolean(this.currentOrg) && !this.isPersonal;
|
|
public readonly isLegacySite = Boolean(this.currentProduct && isLegacyPlan(this.currentProduct.name));
|
|
public readonly isTemplatesSite = isTemplatesOrg(this.currentOrg);
|
|
|
|
public readonly userPrefsObs = getUserPrefsObs(this);
|
|
public readonly themePrefs = getUserPrefObs(this.userPrefsObs, 'theme', {
|
|
defaultValue: getDefaultThemePrefs(),
|
|
checker: ThemePrefsChecker,
|
|
}) as Observable<ThemePrefs>;
|
|
|
|
public readonly dismissedPopups = getUserPrefObs(this.userPrefsObs, 'dismissedPopups',
|
|
{ defaultValue: [] }) as Observable<DismissedPopup[]>;
|
|
public readonly dismissedWelcomePopups = getUserPrefObs(this.userPrefsObs, 'dismissedWelcomePopups',
|
|
{ defaultValue: [] }) as Observable<DismissedReminder[]>;
|
|
|
|
// Get the current PageType from the URL.
|
|
public readonly pageType: Observable<PageType> = Computed.create(this, urlState().state,
|
|
(_use, state) => {
|
|
if (state.doc) {
|
|
return 'doc';
|
|
} else if (state.billing) {
|
|
return 'billing';
|
|
} else if (state.welcome) {
|
|
return 'welcome';
|
|
} else if (state.account) {
|
|
return 'account';
|
|
} else if (state.adminPanel) {
|
|
return 'admin';
|
|
} else if (state.activation) {
|
|
return 'activation';
|
|
} else {
|
|
return 'home';
|
|
}
|
|
});
|
|
|
|
public readonly needsOrg: Observable<boolean> = Computed.create(
|
|
this, urlState().state, (use, state) => {
|
|
return !(
|
|
Boolean(state.welcome) ||
|
|
state.billing === 'scheduled' ||
|
|
Boolean(state.account) ||
|
|
Boolean(state.activation) ||
|
|
Boolean(state.adminPanel)
|
|
);
|
|
});
|
|
|
|
public readonly notifier = this.topAppModel.notifier;
|
|
|
|
public readonly behavioralPromptsManager: BehavioralPromptsManager =
|
|
BehavioralPromptsManager.create(this, this);
|
|
|
|
public readonly supportGristNudge: SupportGristNudge = SupportGristNudge.create(this, this);
|
|
|
|
constructor(
|
|
public readonly topAppModel: TopAppModel,
|
|
public readonly currentUser: ExtendedUser|null,
|
|
public readonly currentOrg: Organization|null,
|
|
public readonly orgError?: OrgError,
|
|
) {
|
|
super();
|
|
|
|
// Whenever theme preferences change, update the global `gristThemePrefs` observable; this triggers
|
|
// an automatic update to the global `gristThemeObs` computed observable.
|
|
this.autoDispose(subscribe(this.themePrefs, (_use, themePrefs) => gristThemePrefs.set(themePrefs)));
|
|
|
|
this._recordSignUpIfIsNewUser();
|
|
|
|
const state = urlState().state.get();
|
|
if (state.createTeam) {
|
|
// Remove params from the URL.
|
|
urlState().pushUrl({createTeam: false, params: {}}, {avoidReload: true, replace: true}).catch(() => {});
|
|
this.showNewSiteModal({
|
|
priceId: state.params?.billingPlan,
|
|
product: state.params?.planType,
|
|
}).catch(reportError);
|
|
} else if (state.upgradeTeam) {
|
|
// Remove params from the URL.
|
|
urlState().pushUrl({upgradeTeam: false, params: {}}, {avoidReload: true, replace: true}).catch(() => {});
|
|
this.showUpgradeModal({
|
|
priceId: state.params?.billingPlan,
|
|
product: state.params?.planType,
|
|
}).catch(reportError);
|
|
}
|
|
|
|
G.window.resetDismissedPopups = (seen = false) => {
|
|
this.dismissedPopups.set(seen ? DismissedPopup.values : []);
|
|
this.behavioralPromptsManager.reset();
|
|
};
|
|
|
|
this.autoDispose(subscribe(urlState().state, this.topAppModel.orgs, async (_use, s, orgs) => {
|
|
this._updateLastVisitedOrgDomain(s, orgs);
|
|
}));
|
|
}
|
|
|
|
public get planName() {
|
|
return this.currentProduct?.name ?? null;
|
|
}
|
|
|
|
public async showUpgradeModal(plan?: PlanSelection) {
|
|
if (this.planName && this.currentOrg) {
|
|
if (this.isPersonal) {
|
|
await this.showNewSiteModal(plan);
|
|
} else if (this.isTeamSite) {
|
|
await buildUpgradeModal(this, {
|
|
appModel: this,
|
|
pickPlan: plan,
|
|
reason: 'upgrade'
|
|
});
|
|
} else {
|
|
throw new Error("Unexpected state");
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
public async showNewSiteModal(plan?: PlanSelection) {
|
|
if (this.planName) {
|
|
await buildNewSiteModal(this, {
|
|
appModel: this,
|
|
plan,
|
|
onCreate: () => this.topAppModel.fetchUsersAndOrgs().catch(reportError)
|
|
});
|
|
}
|
|
}
|
|
|
|
public isSupport() {
|
|
return Boolean(this.currentValidUser?.isSupport);
|
|
}
|
|
|
|
public isBillingManager() {
|
|
return Boolean(this.currentOrg?.billingAccount?.isManager);
|
|
}
|
|
|
|
public isOwner() {
|
|
return Boolean(this.currentOrg && isOwner(this.currentOrg));
|
|
}
|
|
|
|
public isOwnerOrEditor() {
|
|
return Boolean(this.currentOrg && isOwnerOrEditor(this.currentOrg));
|
|
}
|
|
|
|
public isInstallAdmin(): boolean {
|
|
return Boolean(this.currentUser?.isInstallAdmin);
|
|
}
|
|
|
|
/**
|
|
* Fetch and update the current org's usage.
|
|
*/
|
|
public async refreshOrgUsage() {
|
|
if (!this.isOwner()) {
|
|
// Note: getOrgUsageSummary already checks for owner access; we do an early return
|
|
// here to skip making unnecessary API calls.
|
|
return;
|
|
}
|
|
|
|
const usage = await this.api.getOrgUsageSummary(this.currentOrg!.id);
|
|
if (!this.isDisposed()) {
|
|
this.currentOrgUsage.set(usage);
|
|
}
|
|
}
|
|
|
|
public dismissPopup(name: DismissedPopup, isSeen: boolean): void {
|
|
markAsSeen(this.dismissedPopups, name, isSeen);
|
|
}
|
|
|
|
public async switchUser(user: FullUser, org?: string) {
|
|
await this.api.setSessionActive(user.email, org);
|
|
this.lastVisitedOrgDomain.set(null);
|
|
}
|
|
|
|
public isFreePlan() {
|
|
return isFreePlan(this.planName || '');
|
|
}
|
|
|
|
private _updateLastVisitedOrgDomain({doc, org}: IGristUrlState, availableOrgs: Organization[]) {
|
|
if (
|
|
!org ||
|
|
// Invalid or inaccessible sites shouldn't be counted as visited.
|
|
!this.currentOrg ||
|
|
// Visits to a document shouldn't be counted either.
|
|
doc
|
|
) {
|
|
return;
|
|
}
|
|
|
|
// Only count sites that a user has access to (i.e. those listed in the Site Switcher).
|
|
if (!availableOrgs.some(({domain}) => domain === org)) { return; }
|
|
|
|
this.lastVisitedOrgDomain.set(org);
|
|
}
|
|
|
|
/**
|
|
* If the current user is a new user, record a sign-up event via Google Tag Manager.
|
|
*/
|
|
private _recordSignUpIfIsNewUser() {
|
|
const isNewUser = this.userPrefsObs.get().recordSignUpEvent;
|
|
if (!isNewUser) { return; }
|
|
|
|
// If Google Tag Manager isn't configured, don't record anything.
|
|
const {tagManagerId} = getGristConfig();
|
|
if (!tagManagerId) { return; }
|
|
|
|
let dataLayer = (window as any).dataLayer;
|
|
if (!dataLayer) {
|
|
// Load the Google Tag Manager script into the document.
|
|
const script = document.createElement('script');
|
|
script.innerHTML = getTagManagerScript(tagManagerId);
|
|
document.head.appendChild(script);
|
|
dataLayer = (window as any).dataLayer;
|
|
if (!dataLayer) {
|
|
error(`_recordSignUpIfIsNewUser() failed to load Google Tag Manager`);
|
|
}
|
|
}
|
|
|
|
// Send the sign-up event, and remove the recordSignUpEvent flag from preferences.
|
|
dataLayer.push({event: 'new-sign-up'});
|
|
getUserPrefObs(this.userPrefsObs, 'recordSignUpEvent').set(undefined);
|
|
}
|
|
}
|
|
|
|
export function getOrgNameOrGuest(org: Organization|null, user: FullUser|null) {
|
|
if (!org) { return ''; }
|
|
if (user && user.anonymous && org.owner && org.owner.id === user.id) {
|
|
return "@Guest";
|
|
}
|
|
return getOrgName(org);
|
|
}
|
|
|
|
/**
|
|
* If we don't know what the home URL is, the top level of the site
|
|
* we are on may work. This should always work for single-server installs
|
|
* that don't encode organization information in domains. Even for other
|
|
* cases, this should be a good enough home URL for many purposes, it
|
|
* just may still have some organization information encoded in it from
|
|
* the domain that could influence results that might be supposed to be
|
|
* organization-neutral.
|
|
*/
|
|
export function getFallbackHomeUrl(): string {
|
|
const {host, protocol} = window.location;
|
|
return `${protocol}//${host}`;
|
|
}
|
|
|
|
/**
|
|
* Get the official home URL sent to us from the back end.
|
|
*/
|
|
export function getConfiguredHomeUrl(): string {
|
|
const gristConfig: any = (window as any).gristConfig;
|
|
return (gristConfig && gristConfig.homeUrl) || getFallbackHomeUrl();
|
|
}
|
|
|
|
/**
|
|
* Get the home URL, using fallback if on admin page rather
|
|
* than trusting back end configuration.
|
|
*/
|
|
export function getPreferredHomeUrl(): string|undefined {
|
|
const gristUrl = urlState().state.get();
|
|
if (gristUrl.adminPanel) {
|
|
// On the admin panel, we should not trust configuration much,
|
|
// since we want the user to be able to access it to diagnose
|
|
// problems with configuration. So we access the API via the
|
|
// site we happen to be on rather than anything configured on
|
|
// the back end. Couldn't we just always do this? Maybe!
|
|
// It could require adjustments for calls that are meant
|
|
// to be site-neutral if the domain has an org encoded in it.
|
|
// But that's a small price to pay. Grist Labs uses a setup
|
|
// where api calls go to a dedicated domain distinct from all
|
|
// other sites, but there's no particular advantage to it.
|
|
return getFallbackHomeUrl();
|
|
}
|
|
return getConfiguredHomeUrl();
|
|
}
|
|
|
|
export function getHomeUrl(): string {
|
|
return getPreferredHomeUrl() || getConfiguredHomeUrl();
|
|
}
|
|
|
|
export function newUserAPIImpl(): UserAPIImpl {
|
|
return new UserAPIImpl(getHomeUrl(), {
|
|
fetch: hooks.fetch,
|
|
});
|
|
}
|