(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:
George Gevoian
2023-07-26 15:31:02 -07:00
parent bc54a6646e
commit a77170c4bd
25 changed files with 380 additions and 127 deletions

View File

@@ -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.
*/

View File

@@ -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,

View File

@@ -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