mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) Add product for new personal plan
Summary: Adds the new personal plan as a product that will be available in the future. Can be enabled along with other plan-related via an environment variable. Test Plan: Browser tests and existing tests. Reviewers: jarek Reviewed By: jarek Subscribers: dsagal Differential Revision: https://phab.getgrist.com/D3533
This commit is contained in:
@@ -139,7 +139,7 @@ const cssButton = styled(icon, `
|
||||
|
||||
const cssExpandButton = styled(cssButton, `
|
||||
&-expanded {
|
||||
-webkit-mask-image: var(--icon-DropdownUp);
|
||||
-webkit-mask-image: var(--icon-DropdownUp) !important;
|
||||
}
|
||||
`);
|
||||
|
||||
|
||||
@@ -4,11 +4,9 @@ import {docListHeader} from 'app/client/ui/DocMenuCss';
|
||||
import {infoTooltip} from 'app/client/ui/tooltips';
|
||||
import {colors, mediaXSmall} from 'app/client/ui2018/cssVars';
|
||||
import {icon} from 'app/client/ui2018/icons';
|
||||
import {cssLink} from 'app/client/ui2018/links';
|
||||
import {loadingSpinner} from 'app/client/ui2018/loaders';
|
||||
import {APPROACHING_LIMIT_RATIO, DataLimitStatus} from 'app/common/DocUsage';
|
||||
import {Features, isFreeProduct} from 'app/common/Features';
|
||||
import {commonUrls} from 'app/common/gristUrls';
|
||||
import {Features, isFreePlan} from 'app/common/Features';
|
||||
import {capitalizeFirstWord} from 'app/common/gutil';
|
||||
import {canUpgradeOrg} from 'app/common/roles';
|
||||
import {Computed, Disposable, dom, DomContents, DomElementArg, makeTestId, styled} from 'grainjs';
|
||||
@@ -168,8 +166,12 @@ export class DocumentUsage extends Disposable {
|
||||
buildLimitStatusMessage(status, product?.features, {
|
||||
disableRawDataLink: true
|
||||
}),
|
||||
(product && isFreeProduct(product)
|
||||
? [' ', buildUpgradeMessage(canUpgradeOrg(org))]
|
||||
(product && isFreePlan(product.name)
|
||||
? [' ', buildUpgradeMessage(
|
||||
canUpgradeOrg(org),
|
||||
'long',
|
||||
() => this._docPageModel.appModel.showUpgradeModal()
|
||||
)]
|
||||
: null
|
||||
),
|
||||
]);
|
||||
@@ -236,21 +238,25 @@ export function buildLimitStatusMessage(
|
||||
}
|
||||
}
|
||||
|
||||
export function buildUpgradeMessage(canUpgrade: boolean, variant: 'short' | 'long' = 'long') {
|
||||
export function buildUpgradeMessage(
|
||||
canUpgrade: boolean,
|
||||
variant: 'short' | 'long',
|
||||
onUpgrade: () => void,
|
||||
) {
|
||||
if (!canUpgrade) { return 'Contact the site owner to upgrade the plan to raise limits.'; }
|
||||
|
||||
const upgradeLinkText = 'start your 30-day free trial of the Pro plan.';
|
||||
return [
|
||||
variant === 'short' ? null : 'For higher limits, ',
|
||||
buildUpgradeLink(variant === 'short' ? capitalizeFirstWord(upgradeLinkText) : upgradeLinkText),
|
||||
buildUpgradeLink(
|
||||
variant === 'short' ? capitalizeFirstWord(upgradeLinkText) : upgradeLinkText,
|
||||
() => onUpgrade(),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
function buildUpgradeLink(linkText: string) {
|
||||
return cssUnderlinedLink(linkText, {
|
||||
href: commonUrls.plans,
|
||||
target: '_blank',
|
||||
});
|
||||
function buildUpgradeLink(linkText: string, onClick: () => void) {
|
||||
return cssUnderlinedLink(linkText, dom.on('click', () => onClick()));
|
||||
}
|
||||
|
||||
function buildRawDataPageLink(linkText: string) {
|
||||
@@ -356,7 +362,8 @@ const cssHeader = styled(docListHeader, `
|
||||
margin-bottom: 0px;
|
||||
`);
|
||||
|
||||
const cssUnderlinedLink = styled(cssLink, `
|
||||
const cssUnderlinedLink = styled('span', `
|
||||
cursor: pointer;
|
||||
color: unset;
|
||||
text-decoration: underline;
|
||||
|
||||
|
||||
@@ -4,8 +4,9 @@ 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, NEW_DEAL} from 'app/client/ui/ProductUpgrades';
|
||||
import {OrgUsageSummary} from 'app/common/DocUsage';
|
||||
import {Features} from 'app/common/Features';
|
||||
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';
|
||||
@@ -16,7 +17,6 @@ import {getGristConfig} from 'app/common/urlUtils';
|
||||
import {getOrgName, Organization, OrgError, SUPPORT_EMAIL, UserAPI, UserAPIImpl} from 'app/common/UserAPI';
|
||||
import {getUserPrefObs, getUserPrefsObs} from 'app/client/models/UserPrefs';
|
||||
import {bundleChanges, Computed, Disposable, Observable, subscribe} from 'grainjs';
|
||||
import {buildNewSiteModal, buildUpgradeModal} from 'app/client/ui/ProductUpgrades';
|
||||
|
||||
export {reportError} from 'app/client/models/errors';
|
||||
|
||||
@@ -70,9 +70,11 @@ export interface AppModel {
|
||||
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?
|
||||
orgError?: OrgError; // If currentOrg is null, the error that caused it.
|
||||
|
||||
currentFeatures: Features; // features of the current org's product.
|
||||
currentProduct: Product|null; // The current org's product.
|
||||
currentFeatures: Features; // Features of the current org's product.
|
||||
userPrefsObs: Observable<UserPrefs>;
|
||||
|
||||
pageType: Observable<PageType>;
|
||||
@@ -199,11 +201,14 @@ export class AppModelImpl extends Disposable implements AppModel {
|
||||
|
||||
public readonly currentOrgUsage: Observable<OrgUsageSummary|null> = Observable.create(this, null);
|
||||
|
||||
public readonly currentProduct = this.currentOrg?.billingAccount?.product ?? null;
|
||||
public readonly currentFeatures = this.currentProduct?.features ?? {};
|
||||
|
||||
public readonly isPersonal = Boolean(this.currentOrg?.owner);
|
||||
public readonly isTeamSite = Boolean(this.currentOrg) && !this.isPersonal;
|
||||
|
||||
public readonly currentFeatures = (this.currentOrg && this.currentOrg.billingAccount) ?
|
||||
this.currentOrg.billingAccount.product.features : {};
|
||||
// TODO: the `NEW_DEAL` observable can be removed after new deal is released.
|
||||
public readonly isLegacySite = Boolean(
|
||||
NEW_DEAL().get() && this.currentProduct && isLegacyPlan(this.currentProduct.name));
|
||||
|
||||
public readonly userPrefsObs = getUserPrefsObs(this);
|
||||
|
||||
@@ -224,7 +229,7 @@ export class AppModelImpl extends Disposable implements AppModel {
|
||||
}
|
||||
|
||||
public get planName() {
|
||||
return this.currentOrg?.billingAccount?.product.name ?? null;
|
||||
return this.currentProduct?.name ?? null;
|
||||
}
|
||||
|
||||
public async showUpgradeModal() {
|
||||
|
||||
@@ -171,8 +171,8 @@ export class UserManagerModelImpl extends Disposable implements UserManagerModel
|
||||
) {
|
||||
super();
|
||||
if (this._options.appModel) {
|
||||
const features = this._options.appModel.currentFeatures;
|
||||
this._shareAnnotator = new ShareAnnotator(features, initData);
|
||||
const product = this._options.appModel.currentProduct;
|
||||
this._shareAnnotator = new ShareAnnotator(product, initData);
|
||||
}
|
||||
this.annotate();
|
||||
}
|
||||
|
||||
@@ -51,7 +51,11 @@ export class AppHeader extends Disposable {
|
||||
productPill(currentOrg),
|
||||
this._orgName && cssDropdownIcon('Dropdown'),
|
||||
menu(() => [
|
||||
menuSubHeader(`${this._appModel.isTeamSite ? 'Team' : 'Personal'} Site`, testId('orgmenu-title')),
|
||||
menuSubHeader(
|
||||
`${this._appModel.isTeamSite ? 'Team' : 'Personal'} Site`
|
||||
+ (this._appModel.isLegacySite ? ' (Legacy)' : ''),
|
||||
testId('orgmenu-title'),
|
||||
),
|
||||
menuItemLink(urlState().setLinkUrl({}), 'Home Page', testId('orgmenu-home-page')),
|
||||
|
||||
// Show 'Organization Settings' when on a home page of a valid org.
|
||||
|
||||
@@ -10,7 +10,7 @@ import {BillingPlanManagers} from 'app/client/ui/BillingPlanManagers';
|
||||
import {createForbiddenPage} from 'app/client/ui/errorPages';
|
||||
import {leftPanelBasic} from 'app/client/ui/LeftPanelCommon';
|
||||
import {pagePanels} from 'app/client/ui/PagePanels';
|
||||
import {NEW_DEAL, showTeamUpgradeConfirmation} from 'app/client/ui/ProductUpgrades';
|
||||
import {showTeamUpgradeConfirmation} from 'app/client/ui/ProductUpgrades';
|
||||
import {createTopBarHome} from 'app/client/ui/TopBar';
|
||||
import {cssBreadcrumbs, cssBreadcrumbsLink, separator} from 'app/client/ui2018/breadcrumbs';
|
||||
import {bigBasicButton, bigBasicButtonLink, bigPrimaryButton} from 'app/client/ui2018/buttons';
|
||||
@@ -45,9 +45,6 @@ export class BillingPage extends Disposable {
|
||||
|
||||
constructor(private _appModel: AppModel) {
|
||||
super();
|
||||
|
||||
// TODO: remove once NEW_DEAL is there. Execute for side effect
|
||||
void NEW_DEAL();
|
||||
this._appModel.refreshOrgUsage().catch(reportError);
|
||||
}
|
||||
|
||||
|
||||
@@ -71,10 +71,11 @@ function createLoadedDocMenu(owner: IDisposableOwner, home: HomeModel) {
|
||||
workspace ? makeLocalViewSettings(home, workspace.id) :
|
||||
home;
|
||||
return [
|
||||
// Hide the sort option only when showing intro.
|
||||
((showIntro && page === 'all') ? css.prefSelectors(upgradeButton.showUpgradeButton()) :
|
||||
// This is float:right element
|
||||
buildPrefs(viewSettings, {hideSort: showIntro}, upgradeButton.showUpgradeButton())
|
||||
buildPrefs(
|
||||
viewSettings,
|
||||
// Hide the sort and view options when showing the intro.
|
||||
{hideSort: showIntro, hideView: showIntro && page === 'all'},
|
||||
['all', 'workspace'].includes(page) ? upgradeButton.showUpgradeButton() : null,
|
||||
),
|
||||
|
||||
// Build the pinned docs dom. Builds nothing if the selectedOrg is unloaded.
|
||||
@@ -297,12 +298,16 @@ function buildOtherSites(home: HomeModel) {
|
||||
|
||||
/**
|
||||
* Build the widget for selecting sort and view mode options.
|
||||
* If hideSort is true, will hide the sort dropdown: it has no effect on the list of examples, so
|
||||
* best to hide when those are the only docs shown.
|
||||
*
|
||||
* Options hideSort and hideView control which options are shown; they should have no effect
|
||||
* on the list of examples, so best to hide when those are the only docs shown.
|
||||
*/
|
||||
function buildPrefs(
|
||||
viewSettings: ViewSettings,
|
||||
options: {hideSort: boolean},
|
||||
options: {
|
||||
hideSort: boolean,
|
||||
hideView: boolean,
|
||||
},
|
||||
...args: DomArg<HTMLElement>[]): DomContents {
|
||||
return css.prefSelectors(
|
||||
// The Sort selector.
|
||||
@@ -317,7 +322,7 @@ function buildPrefs(
|
||||
),
|
||||
|
||||
// The View selector.
|
||||
buttonSelect<ViewPref>(viewSettings.currentView, [
|
||||
options.hideView ? null : buttonSelect<ViewPref>(viewSettings.currentView, [
|
||||
{value: 'icons', icon: 'TypeTable'},
|
||||
{value: 'list', icon: 'TypeCardList'},
|
||||
],
|
||||
|
||||
@@ -191,7 +191,7 @@ function addMenu(home: HomeModel, creating: Observable<boolean>): DomElementArg[
|
||||
dom.cls('disabled', (use) => !roles.canEdit(orgAccess) || !use(home.available)),
|
||||
testId("dm-new-workspace")
|
||||
),
|
||||
upgradeText(needUpgrade),
|
||||
upgradeText(needUpgrade, () => home.app.showUpgradeModal()),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -225,8 +225,12 @@ function workspaceMenu(home: HomeModel, ws: Workspace, renaming: Observable<Work
|
||||
testId('dm-delete-workspace')),
|
||||
upgradableMenuItem(needUpgrade, manageWorkspaceUsers,
|
||||
roles.canEditAccess(ws.access) ? "Manage Users" : "Access Details",
|
||||
// TODO: Personal plans can't currently share workspaces, but that restriction
|
||||
// should formally be documented and defined in `Features`, with this check updated
|
||||
// to look there instead.
|
||||
dom.cls('disabled', () => home.app.isPersonal),
|
||||
testId('dm-workspace-access')),
|
||||
upgradeText(needUpgrade),
|
||||
upgradeText(needUpgrade, () => home.app.showUpgradeModal()),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -19,9 +19,13 @@ function buildAction(action: NotifyAction, item: Notification, options: IBeaconO
|
||||
const appModel = options.appModel;
|
||||
switch (action) {
|
||||
case 'upgrade':
|
||||
return dom('a', cssToastAction.cls(''), 'Upgrade Plan', {target: '_blank'},
|
||||
{href: commonUrls.plans});
|
||||
|
||||
if (appModel) {
|
||||
return cssToastAction('Upgrade Plan', dom.on('click', () =>
|
||||
appModel.showUpgradeModal()));
|
||||
} else {
|
||||
return dom('a', cssToastAction.cls(''), 'Upgrade Plan', {target: '_blank'},
|
||||
{href: commonUrls.plans});
|
||||
}
|
||||
case 'renew':
|
||||
// If already on the billing page, nothing to return.
|
||||
if (urlState().state.get().billing === 'billing') { return null; }
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Command } from 'app/client/components/commands';
|
||||
import { NeedUpgradeError, reportError } from 'app/client/models/errors';
|
||||
import { textButton } from 'app/client/ui2018/buttons';
|
||||
import { cssCheckboxSquare, cssLabel, cssLabelText } from 'app/client/ui2018/checkbox';
|
||||
import { colors, testId, vars } from 'app/client/ui2018/cssVars';
|
||||
import { IconName } from 'app/client/ui2018/IconList';
|
||||
import { icon } from 'app/client/ui2018/icons';
|
||||
import { cssSelectBtn } from 'app/client/ui2018/select';
|
||||
import { commonUrls } from 'app/common/gristUrls';
|
||||
import { BindableValue, Computed, dom, DomElementArg, DomElementMethod, IDomArgs,
|
||||
MaybeObsArray, MutableObsArray, Observable, styled } from 'grainjs';
|
||||
import * as weasel from 'popweasel';
|
||||
@@ -299,10 +299,10 @@ export function upgradableMenuItem(needUpgrade: boolean, action: () => void, ...
|
||||
}
|
||||
}
|
||||
|
||||
export function upgradeText(needUpgrade: boolean) {
|
||||
export function upgradeText(needUpgrade: boolean, onClick: () => void) {
|
||||
if (!needUpgrade) { return null; }
|
||||
return menuText(dom('span', '* Workspaces are available on team plans. ',
|
||||
dom('a', {href: commonUrls.plans}, 'Upgrade now')));
|
||||
cssUpgradeTextButton('Upgrade now', dom.on('click', () => onClick()))));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -584,3 +584,7 @@ const cssCheckboxText = styled(cssLabelText, `
|
||||
color: ${colors.dark};
|
||||
white-space: pre;
|
||||
`);
|
||||
|
||||
const cssUpgradeTextButton = styled(textButton, `
|
||||
font-size: ${vars.smallFontSize};
|
||||
`);
|
||||
|
||||
Reference in New Issue
Block a user