mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) Direct users to last visited site when possible
Summary: When clicking the logo in the top-left corner, or finishing a tutorial, we now direct users to the site they last visited, if possible. If unknown, a new redirect endpoint, /welcome/home, is used instead, which directs users to a sensible location based on the number of sites they have. Test Plan: Browser tests. Reviewers: paulfitz Reviewed By: paulfitz Differential Revision: https://phab.getgrist.com/D3878
This commit is contained in:
@@ -26,14 +26,24 @@ export function sessionStorageBoolObs(key: string, defValue = false): Observable
|
||||
return getStorageBoolObs(getSessionStorage(), key, defValue);
|
||||
}
|
||||
|
||||
function getStorageObs(store: Storage, key: string, defaultValue?: string) {
|
||||
const obs = Observable.create<string|null>(null, store.getItem(key) ?? defaultValue ?? null);
|
||||
obs.addListener((val) => (val === null) ? store.removeItem(key) : store.setItem(key, val));
|
||||
return obs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to create a string observable whose state is stored in localStorage.
|
||||
*/
|
||||
export function localStorageObs(key: string, defaultValue?: string): Observable<string|null> {
|
||||
const store = getStorage();
|
||||
const obs = Observable.create<string|null>(null, store.getItem(key) ?? defaultValue ?? null);
|
||||
obs.addListener((val) => (val === null) ? store.removeItem(key) : store.setItem(key, val));
|
||||
return obs;
|
||||
return getStorageObs(getStorage(), key, defaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Similar to `localStorageObs`, but always uses sessionStorage (or an in-memory equivalent).
|
||||
*/
|
||||
export function sessionStorageObs(key: string, defaultValue?: string): Observable<string|null> {
|
||||
return getStorageObs(getSessionStorage(), key, defaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {BehavioralPromptsManager} from 'app/client/components/BehavioralPromptsManager';
|
||||
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';
|
||||
@@ -83,6 +84,7 @@ export interface AppModel {
|
||||
isTeamSite: boolean; // Is it a team site?
|
||||
isLegacySite: boolean; // Is it a legacy site?
|
||||
orgError?: OrgError; // If currentOrg is null, the error that caused it.
|
||||
lastVisitedOrgDomain: Observable<string|null>;
|
||||
|
||||
currentProduct: Product|null; // The current org's product.
|
||||
currentFeatures: Features; // Features of the current org's product.
|
||||
@@ -227,6 +229,8 @@ export class AppModelImpl extends Disposable implements AppModel {
|
||||
|
||||
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 currentFeatures = this.currentProduct?.features ?? {};
|
||||
|
||||
@@ -284,6 +288,14 @@ export class AppModelImpl extends Disposable implements AppModel {
|
||||
this.dismissedPopups.set(seen ? DismissedPopup.values : []);
|
||||
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);
|
||||
}));
|
||||
}
|
||||
|
||||
public get planName() {
|
||||
|
||||
@@ -85,6 +85,10 @@ export function getLoginOrSignupUrl(nextUrl: string = _getCurrentUrl()): string
|
||||
return _getLoginLogoutUrl('signin', nextUrl);
|
||||
}
|
||||
|
||||
export function getWelcomeHomeUrl() {
|
||||
return _buildUrl('welcome/home').href;
|
||||
}
|
||||
|
||||
// Returns the relative URL (i.e. path) of the current page, except when it's the
|
||||
// "/signed-out" page, in which case it returns the home page ("/").
|
||||
// This is a good URL to use for a post-login redirect.
|
||||
@@ -97,14 +101,19 @@ function _getCurrentUrl(): string {
|
||||
|
||||
// Returns the URL for the given login page, with 'next' param optionally set.
|
||||
function _getLoginLogoutUrl(page: 'login'|'logout'|'signin'|'signup', nextUrl?: string | null): string {
|
||||
const startUrl = new URL(window.location.href);
|
||||
startUrl.pathname = addOrgToPath('', window.location.href, true) + '/' + page;
|
||||
startUrl.search = '';
|
||||
startUrl.hash = '';
|
||||
const startUrl = _buildUrl(page);
|
||||
if (nextUrl) { startUrl.searchParams.set('next', nextUrl); }
|
||||
return startUrl.href;
|
||||
}
|
||||
|
||||
function _buildUrl(page?: string): URL {
|
||||
const startUrl = new URL(window.location.href);
|
||||
startUrl.pathname = addOrgToPath('', window.location.href, true) + '/' + (page ?? '');
|
||||
startUrl.search = '';
|
||||
startUrl.hash = '';
|
||||
return startUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the interface expected by UrlState. It is only exported for the sake of tests; the
|
||||
* only public interface is the urlState() accessor.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {urlState} from 'app/client/models/gristUrlState';
|
||||
import {getWelcomeHomeUrl, urlState} from 'app/client/models/gristUrlState';
|
||||
import {buildAppMenuBillingItem} from 'app/client/ui/BillingButtons';
|
||||
import {getTheme} from 'app/client/ui/CustomThemes';
|
||||
import {cssLeftPane} from 'app/client/ui/PagePanels';
|
||||
@@ -48,7 +48,7 @@ export class AppHeader extends Disposable {
|
||||
cssAppLogo(
|
||||
{title: `Version ${version.version}` +
|
||||
((version.gitcommit as string) !== 'unknown' ? ` (${version.gitcommit})` : '')},
|
||||
urlState().setLinkUrl({}),
|
||||
this._setHomePageUrl(),
|
||||
testId('dm-logo')
|
||||
),
|
||||
cssOrg(
|
||||
@@ -79,6 +79,15 @@ export class AppHeader extends Disposable {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private _setHomePageUrl() {
|
||||
const lastVisitedOrg = this._appModel.lastVisitedOrgDomain.get();
|
||||
if (lastVisitedOrg) {
|
||||
return urlState().setLinkUrl({org: lastVisitedOrg});
|
||||
} else {
|
||||
return {href: getWelcomeHomeUrl()};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function productPill(org: Organization|null, options: {large?: boolean} = {}): DomContents {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {GristDoc} from 'app/client/components/GristDoc';
|
||||
import {urlState} from 'app/client/models/gristUrlState';
|
||||
import {getWelcomeHomeUrl, urlState} from 'app/client/models/gristUrlState';
|
||||
import {renderer} from 'app/client/ui/DocTutorialRenderer';
|
||||
import {cssPopupBody, FloatingPopup} from 'app/client/ui/FloatingPopup';
|
||||
import {sanitizeHTML} from 'app/client/ui/sanitizeHTML';
|
||||
@@ -242,7 +242,12 @@ export class DocTutorial extends FloatingPopup {
|
||||
private async _finishTutorial() {
|
||||
this._saveCurrentSlidePositionDebounced.cancel();
|
||||
await this._saveCurrentSlidePosition();
|
||||
await urlState().pushUrl({});
|
||||
const lastVisitedOrg = this._appModel.lastVisitedOrgDomain.get();
|
||||
if (lastVisitedOrg) {
|
||||
await urlState().pushUrl({org: lastVisitedOrg});
|
||||
} else {
|
||||
window.location.assign(getWelcomeHomeUrl());
|
||||
}
|
||||
}
|
||||
|
||||
private async _restartTutorial() {
|
||||
|
||||
Reference in New Issue
Block a user