mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) Tweak navbar, breadcrumbs, and sign-in buttons
Summary: The changes are intended to smooth over some sharp edges when a signed-out user is using Grist (particularly while on the templates site). Test Plan: Browser tests. Reviewers: paulfitz Reviewed By: paulfitz Differential Revision: https://phab.getgrist.com/D3957
This commit is contained in:
@@ -13,7 +13,7 @@ import {SupportGristNudge} from 'app/client/ui/SupportGristNudge';
|
||||
import {attachCssThemeVars, prefersDarkModeObs} from 'app/client/ui2018/cssVars';
|
||||
import {OrgUsageSummary} from 'app/common/DocUsage';
|
||||
import {Features, isLegacyPlan, Product} from 'app/common/Features';
|
||||
import {GristLoadConfig} from 'app/common/gristUrls';
|
||||
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';
|
||||
@@ -23,7 +23,7 @@ import {getDefaultThemePrefs, Theme, ThemeAppearance, ThemeColors, ThemePrefs,
|
||||
ThemePrefsChecker} from 'app/common/ThemePrefs';
|
||||
import {getThemeColors} from 'app/common/Themes';
|
||||
import {getGristConfig} from 'app/common/urlUtils';
|
||||
import {getOrgName, Organization, OrgError, UserAPI, UserAPIImpl} from 'app/common/UserAPI';
|
||||
import {getOrgName, isTemplatesOrg, Organization, OrgError, UserAPI, UserAPIImpl} from 'app/common/UserAPI';
|
||||
import {getUserPrefObs, getUserPrefsObs, markAsSeen, markAsUnSeen} from 'app/client/models/UserPrefs';
|
||||
import {bundleChanges, Computed, Disposable, Observable, subscribe} from 'grainjs';
|
||||
|
||||
@@ -93,6 +93,7 @@ export interface AppModel {
|
||||
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>;
|
||||
|
||||
@@ -249,6 +250,7 @@ export class AppModelImpl extends Disposable implements AppModel {
|
||||
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', {
|
||||
@@ -325,12 +327,8 @@ export class AppModelImpl extends Disposable implements AppModel {
|
||||
this.behavioralPromptsManager.reset();
|
||||
};
|
||||
|
||||
this.autoDispose(subscribe(urlState().state, async (_use, {doc, org}) => {
|
||||
// Keep track of the last valid org domain the user visited, ignoring those
|
||||
// with a document id in the URL.
|
||||
if (!this.currentOrg || doc) { return; }
|
||||
|
||||
this.lastVisitedOrgDomain.set(org ?? null);
|
||||
this.autoDispose(subscribe(urlState().state, this.topAppModel.orgs, async (_use, s, orgs) => {
|
||||
this._updateLastVisitedOrgDomain(s, orgs);
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -404,6 +402,23 @@ export class AppModelImpl extends Disposable implements AppModel {
|
||||
return computed;
|
||||
}
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
@@ -42,6 +42,7 @@ export interface DocInfo extends Document {
|
||||
isSnapshot: boolean;
|
||||
isTutorialTrunk: boolean;
|
||||
isTutorialFork: boolean;
|
||||
isTemplate: boolean;
|
||||
idParts: UrlIdParts;
|
||||
openMode: OpenDocMode;
|
||||
}
|
||||
@@ -76,6 +77,7 @@ export interface DocPageModel {
|
||||
isSnapshot: Observable<boolean>;
|
||||
isTutorialTrunk: Observable<boolean>;
|
||||
isTutorialFork: Observable<boolean>;
|
||||
isTemplate: Observable<boolean>;
|
||||
|
||||
importSources: ImportSource[];
|
||||
|
||||
@@ -131,6 +133,8 @@ export class DocPageModelImpl extends Disposable implements DocPageModel {
|
||||
(use, doc) => doc ? doc.isTutorialTrunk : false);
|
||||
public readonly isTutorialFork = Computed.create(this, this.currentDoc,
|
||||
(use, doc) => doc ? doc.isTutorialFork : false);
|
||||
public readonly isTemplate = Computed.create(this, this.currentDoc,
|
||||
(use, doc) => doc ? doc.isTemplate : false);
|
||||
|
||||
public readonly importSources: ImportSource[] = [];
|
||||
|
||||
@@ -431,24 +435,33 @@ function addMenu(importSources: ImportSource[], gristDoc: GristDoc, isReadonly:
|
||||
function buildDocInfo(doc: Document, mode: OpenDocMode | undefined): DocInfo {
|
||||
const idParts = parseUrlId(doc.urlId || doc.id);
|
||||
const isFork = Boolean(idParts.forkId || idParts.snapshotId);
|
||||
const isBareFork = isFork && idParts.trunkId === NEW_DOCUMENT_CODE;
|
||||
const isSnapshot = Boolean(idParts.snapshotId);
|
||||
const isTutorial = doc.type === 'tutorial';
|
||||
const isTutorialTrunk = isTutorial && !isFork && mode !== 'default';
|
||||
const isTutorialFork = isTutorial && isFork;
|
||||
|
||||
let openMode = mode;
|
||||
if (!openMode) {
|
||||
if (isFork) {
|
||||
// Ignore the document 'openMode' setting if the doc is an unsaved fork.
|
||||
if (isFork || isTutorialTrunk || isTutorialFork) {
|
||||
// Tutorials (if no explicit /m/default mode is set) automatically get or
|
||||
// create a fork on load, which then behaves as a document that is in default
|
||||
// mode. Since the document's 'openMode' has no effect, don't bother trying
|
||||
// to set it here, as it'll potentially be confusing for other code reading it.
|
||||
openMode = 'default';
|
||||
} else if (!isFork && doc.type === 'template') {
|
||||
// Templates should always open in fork mode by default.
|
||||
openMode = 'fork';
|
||||
} else {
|
||||
// Try to use the document's 'openMode' if it's set.
|
||||
openMode = doc.options?.openMode ?? 'default';
|
||||
}
|
||||
}
|
||||
|
||||
const isPreFork = (openMode === 'fork');
|
||||
const isBareFork = isFork && idParts.trunkId === NEW_DOCUMENT_CODE;
|
||||
const isSnapshot = Boolean(idParts.snapshotId);
|
||||
const isTutorialTrunk = !isFork && doc.type === 'tutorial' && mode !== 'default';
|
||||
const isTutorialFork = isFork && doc.type === 'tutorial';
|
||||
const isPreFork = openMode === 'fork';
|
||||
const isTemplate = doc.type === 'template' && (isFork || isPreFork);
|
||||
const isEditable = !isSnapshot && (canEdit(doc.access) || isPreFork);
|
||||
|
||||
return {
|
||||
...doc,
|
||||
isFork,
|
||||
@@ -459,6 +472,7 @@ function buildDocInfo(doc: Document, mode: OpenDocMode | undefined): DocInfo {
|
||||
isSnapshot,
|
||||
isTutorialTrunk,
|
||||
isTutorialFork,
|
||||
isTemplate,
|
||||
isReadonly: !isEditable,
|
||||
idParts,
|
||||
openMode,
|
||||
|
||||
@@ -11,6 +11,7 @@ import {IHomePage} from 'app/common/gristUrls';
|
||||
import {isLongerThan} from 'app/common/gutil';
|
||||
import {SortPref, UserOrgPrefs, ViewPref} from 'app/common/Prefs';
|
||||
import * as roles from 'app/common/roles';
|
||||
import {getGristConfig} from 'app/common/urlUtils';
|
||||
import {Document, Organization, Workspace} from 'app/common/UserAPI';
|
||||
import {bundleChanges, Computed, Disposable, Observable, subscribe} from 'grainjs';
|
||||
import moment from 'moment';
|
||||
@@ -311,7 +312,9 @@ export class HomeModelImpl extends Disposable implements HomeModel, ViewSettings
|
||||
// now, but it is good to show names to highlight the possibility of adding more.
|
||||
const nonSupportWss = Array.isArray(wss) ? wss.filter(ws => !ws.isSupportWorkspace) : null;
|
||||
this.singleWorkspace.set(
|
||||
!!nonSupportWss && nonSupportWss.length === 1 && _isSingleWorkspaceMode(this._app)
|
||||
// The anon personal site always has 0 non-support workspaces.
|
||||
nonSupportWss?.length === 0 ||
|
||||
nonSupportWss?.length === 1 && _isSingleWorkspaceMode(this._app)
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -357,6 +360,9 @@ export class HomeModelImpl extends Disposable implements HomeModel, ViewSettings
|
||||
* Only fetches featured (pinned) templates on the All Documents page.
|
||||
*/
|
||||
private async _maybeFetchTemplates(): Promise<Workspace[] | null> {
|
||||
const {templateOrg} = getGristConfig();
|
||||
if (!templateOrg) { return null; }
|
||||
|
||||
const currentPage = this.currentPage.get();
|
||||
const shouldFetchTemplates = ['all', 'templates'].includes(currentPage);
|
||||
if (!shouldFetchTemplates) { return null; }
|
||||
@@ -366,10 +372,10 @@ export class HomeModelImpl extends Disposable implements HomeModel, ViewSettings
|
||||
const onlyFeatured = currentPage === 'all';
|
||||
templateWss = await this._app.api.getTemplates(onlyFeatured);
|
||||
} catch {
|
||||
// If the org doesn't exist (404), return nothing and don't report error to user.
|
||||
return null;
|
||||
reportError('Failed to load templates');
|
||||
}
|
||||
if (this.isDisposed()) { return null; }
|
||||
|
||||
for (const ws of templateWss) {
|
||||
for (const doc of ws.docs) {
|
||||
// Populate doc.workspace, which is used by DocMenu/PinnedDocs and
|
||||
|
||||
Reference in New Issue
Block a user