mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) Add behavioral and coaching call popups
Summary: Adds a new category of popups that are shown dynamically when certain parts of the UI are first rendered, and a free coaching call popup that's shown to users on their site home page. Test Plan: Browser tests. Reviewers: jarek Reviewed By: jarek Differential Revision: https://phab.getgrist.com/D3706
This commit is contained in:
@@ -12,7 +12,8 @@ import {Features, isLegacyPlan, Product} from 'app/common/Features';
|
||||
import {GristLoadConfig} from 'app/common/gristUrls';
|
||||
import {FullUser} from 'app/common/LoginSessionAPI';
|
||||
import {LocalPlugin} from 'app/common/plugin';
|
||||
import {DeprecationWarning, DismissedPopup, UserPrefs} from 'app/common/Prefs';
|
||||
import {BehavioralPromptPrefs, DeprecationWarning, DismissedPopup, DismissedReminder,
|
||||
UserPrefs} from 'app/common/Prefs';
|
||||
import {isOwner} from 'app/common/roles';
|
||||
import {getTagManagerScript} from 'app/common/tagManager';
|
||||
import {getDefaultThemePrefs, Theme, ThemeAppearance, ThemeColors, ThemePrefs,
|
||||
@@ -23,7 +24,7 @@ import {getOrgName, Organization, OrgError, UserAPI, UserAPIImpl} from 'app/comm
|
||||
import {getUserPrefObs, getUserPrefsObs} from 'app/client/models/UserPrefs';
|
||||
import {bundleChanges, Computed, Disposable, Observable, subscribe} from 'grainjs';
|
||||
|
||||
const t = makeT('models.AppModel')
|
||||
const t = makeT('models.AppModel');
|
||||
|
||||
// Reexported for convenience.
|
||||
export {reportError} from 'app/client/models/errors';
|
||||
@@ -97,6 +98,8 @@ export interface AppModel {
|
||||
* Deprecation messages that user has seen.
|
||||
*/
|
||||
deprecatedWarnings: Observable<DeprecationWarning[]>;
|
||||
dismissedWelcomePopups: Observable<DismissedReminder[]>;
|
||||
behavioralPrompts: Observable<BehavioralPromptPrefs>;
|
||||
|
||||
pageType: Observable<PageType>;
|
||||
|
||||
@@ -108,6 +111,7 @@ export interface AppModel {
|
||||
showNewSiteModal(): 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
|
||||
}
|
||||
|
||||
export class TopAppModelImpl extends Disposable implements TopAppModel {
|
||||
@@ -236,11 +240,14 @@ export class AppModelImpl extends Disposable implements AppModel {
|
||||
}) as Observable<ThemePrefs>;
|
||||
public readonly currentTheme = this._getCurrentThemeObs();
|
||||
|
||||
public readonly dismissedPopups =
|
||||
getUserPrefObs(this.userPrefsObs, 'dismissedPopups', { defaultValue: [] }) as Observable<DismissedPopup[]>;
|
||||
public readonly dismissedPopups = getUserPrefObs(this.userPrefsObs, 'dismissedPopups',
|
||||
{ defaultValue: [] }) as Observable<DismissedPopup[]>;
|
||||
public readonly deprecatedWarnings = getUserPrefObs(this.userPrefsObs, 'seenDeprecatedWarnings',
|
||||
{ defaultValue: []}) as Observable<DeprecationWarning[]>;
|
||||
|
||||
{ defaultValue: [] }) as Observable<DeprecationWarning[]>;
|
||||
public readonly dismissedWelcomePopups = getUserPrefObs(this.userPrefsObs, 'dismissedWelcomePopups',
|
||||
{ defaultValue: [] }) as Observable<DismissedReminder[]>;
|
||||
public readonly behavioralPrompts = getUserPrefObs(this.userPrefsObs, 'behavioralPrompts',
|
||||
{ defaultValue: { dontShowTips: false, dismissedTips: [] } }) as Observable<BehavioralPromptPrefs>;
|
||||
|
||||
// Get the current PageType from the URL.
|
||||
public readonly pageType: Observable<PageType> = Computed.create(this, urlState().state,
|
||||
@@ -303,18 +310,21 @@ export class AppModelImpl extends Disposable implements AppModel {
|
||||
return Boolean(this.currentOrg?.billingAccount?.isManager);
|
||||
}
|
||||
|
||||
public isOwner() {
|
||||
return Boolean(this.currentOrg && isOwner(this.currentOrg));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch and update the current org's usage.
|
||||
*/
|
||||
public async refreshOrgUsage() {
|
||||
const currentOrg = this.currentOrg;
|
||||
if (!isOwner(currentOrg)) {
|
||||
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(currentOrg.id);
|
||||
const usage = await this.api.getOrgUsageSummary(this.currentOrg!.id);
|
||||
if (!this.isDisposed()) {
|
||||
this.currentOrgUsage.set(usage);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { GristDoc } from "app/client/components/GristDoc";
|
||||
import { ColumnFilter } from "app/client/models/ColumnFilter";
|
||||
import { FilterInfo } from "app/client/models/entities/ViewSectionRec";
|
||||
import { CellValue } from "app/plugin/GristData";
|
||||
@@ -29,6 +30,7 @@ interface ColumnFilterMenuModelParams {
|
||||
columnFilter: ColumnFilter;
|
||||
filterInfo: FilterInfo;
|
||||
valueCount: Array<[CellValue, IFilterCount]>;
|
||||
gristDoc: GristDoc;
|
||||
limitShow?: number;
|
||||
}
|
||||
|
||||
@@ -37,6 +39,8 @@ export class ColumnFilterMenuModel extends Disposable {
|
||||
|
||||
public readonly filterInfo = this._params.filterInfo;
|
||||
|
||||
public readonly gristDoc = this._params.gristDoc;
|
||||
|
||||
public readonly initialPinned = this.filterInfo.isPinned.peek();
|
||||
|
||||
public readonly limitShown = this._params.limitShow ?? MAXIMUM_SHOWN_FILTER_ITEMS;
|
||||
|
||||
@@ -336,13 +336,13 @@ function addMenu(importSources: ImportSource[], gristDoc: GristDoc, isReadonly:
|
||||
const selectBy = gristDoc.selectBy.bind(gristDoc);
|
||||
return [
|
||||
menuItem(
|
||||
(elem) => openPageWidgetPicker(elem, gristDoc.docModel, (val) => gristDoc.addNewPage(val).catch(reportError),
|
||||
(elem) => openPageWidgetPicker(elem, gristDoc, (val) => gristDoc.addNewPage(val).catch(reportError),
|
||||
{isNewPage: true, buttonLabel: 'Add Page'}),
|
||||
menuIcon("Page"), t("AddPage"), testId('dp-add-new-page'),
|
||||
dom.cls('disabled', isReadonly)
|
||||
),
|
||||
menuItem(
|
||||
(elem) => openPageWidgetPicker(elem, gristDoc.docModel, (val) => gristDoc.addWidgetToPage(val).catch(reportError),
|
||||
(elem) => openPageWidgetPicker(elem, gristDoc, (val) => gristDoc.addWidgetToPage(val).catch(reportError),
|
||||
{isNewPage: false, selectBy}),
|
||||
menuIcon("Widget"), t("AddWidgetToPage"), testId('dp-add-widget-to-page'),
|
||||
// disable for readonly doc and all special views
|
||||
|
||||
@@ -94,6 +94,10 @@ export interface ViewSectionRec extends IRowModel<"_grist_Views_section">, RuleO
|
||||
// changes to their filters. (True indicates unsaved changes)
|
||||
filterSpecChanged: Computed<boolean>;
|
||||
|
||||
// Set to true when a second pinned filter is added, to trigger a behavioral prompt. Note that
|
||||
// the popup is only shown once, even if this observable is set to true again in the future.
|
||||
showNestedFilteringPopup: Observable<boolean>;
|
||||
|
||||
// Customizable version of the JSON-stringified sort spec. It may diverge from the saved one.
|
||||
activeSortJson: modelUtil.CustomComputed<string>;
|
||||
|
||||
@@ -432,6 +436,8 @@ export function createViewSectionRec(this: ViewSectionRec, docModel: DocModel):
|
||||
return use(this.filters).some(col => !use(col.filter.isSaved) || !use(col.pinned.isSaved));
|
||||
});
|
||||
|
||||
this.showNestedFilteringPopup = Observable.create(this, false);
|
||||
|
||||
// Save all filters of fields/columns in the section.
|
||||
this.saveFilters = () => {
|
||||
return docModel.docData.bundleActions(`Save all filters in ${this.titleDef()}`,
|
||||
|
||||
Reference in New Issue
Block a user