Summary: Also fixes a minor CSS regression in UserManager where the link to add a team member wasn't shown on a separate row. Test Plan: Browser tests. Reviewers: jarek Reviewed By: jarek Differential Revision: https://phab.getgrist.com/D3444pull/214/head
parent
2f3cf59fc3
commit
74ec9358da
@ -0,0 +1,165 @@
|
||||
import {colors, isNarrowScreenObs} from 'app/client/ui2018/cssVars';
|
||||
import {icon} from 'app/client/ui2018/icons';
|
||||
import {Disposable, dom, DomArg, DomElementArg, makeTestId, Observable, styled} from 'grainjs';
|
||||
|
||||
const testId = makeTestId('test-banner-');
|
||||
|
||||
export interface BannerOptions {
|
||||
/**
|
||||
* Content to display in the banner.
|
||||
*/
|
||||
content: DomArg;
|
||||
|
||||
/**
|
||||
* The banner style.
|
||||
*
|
||||
* Warning banners have a yellow background. Error banners have a red
|
||||
* background.
|
||||
*/
|
||||
style: 'warning' | 'error';
|
||||
|
||||
/**
|
||||
* Optional variant of `content` to display when screen width becomes narrow.
|
||||
*/
|
||||
contentSmall?: DomArg;
|
||||
|
||||
/**
|
||||
* Whether a button to close the banner should be shown.
|
||||
*
|
||||
* If true, `onClose` should also be specified; it will be called when the close
|
||||
* button is clicked.
|
||||
*
|
||||
* Defaults to false.
|
||||
*/
|
||||
showCloseButton?: boolean;
|
||||
|
||||
/**
|
||||
* Whether a button to collapse/expand the banner should be shown on narrow screens.
|
||||
*
|
||||
* Defaults to false.
|
||||
*/
|
||||
showExpandButton?: boolean;
|
||||
|
||||
/**
|
||||
* Function that is called when the banner close button is clicked.
|
||||
*
|
||||
* Should be used to handle disposal of the Banner.
|
||||
*/
|
||||
onClose?(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A customizable banner for displaying at the top of a page.
|
||||
*/
|
||||
export class Banner extends Disposable {
|
||||
private readonly _isExpanded = Observable.create(this, true);
|
||||
|
||||
constructor(private _options: BannerOptions) {
|
||||
super();
|
||||
}
|
||||
|
||||
public buildDom() {
|
||||
return cssBanner(
|
||||
cssBanner.cls(`-${this._options.style}`),
|
||||
this._buildContent(),
|
||||
this._buildButtons(),
|
||||
);
|
||||
}
|
||||
|
||||
private _buildContent() {
|
||||
const {content, contentSmall} = this._options;
|
||||
return dom.domComputed(use => {
|
||||
if (contentSmall === undefined) { return [content]; }
|
||||
|
||||
const isExpanded = use(this._isExpanded);
|
||||
const isNarrowScreen = use(isNarrowScreenObs());
|
||||
return [isNarrowScreen && !isExpanded ? contentSmall : content];
|
||||
});
|
||||
}
|
||||
|
||||
private _buildButtons() {
|
||||
return cssButtons(
|
||||
this._options.showExpandButton ? this._buildExpandButton() : null,
|
||||
this._options.showCloseButton ? this._buildCloseButton() : null,
|
||||
);
|
||||
}
|
||||
|
||||
private _buildCloseButton() {
|
||||
return cssButton('CrossBig',
|
||||
dom.on('click', () => this._options.onClose?.()),
|
||||
testId('close'),
|
||||
);
|
||||
}
|
||||
|
||||
private _buildExpandButton() {
|
||||
return dom.maybe(isNarrowScreenObs(), () => {
|
||||
return cssExpandButton('Dropdown',
|
||||
cssExpandButton.cls('-expanded', this._isExpanded),
|
||||
dom.on('click', () => this._isExpanded.set(!this._isExpanded.get())),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function buildBannerMessage(...domArgs: DomElementArg[]) {
|
||||
return cssBannerMessage(
|
||||
cssIcon('Idea'),
|
||||
cssLightlyBoldedText(domArgs),
|
||||
);
|
||||
}
|
||||
|
||||
const cssBanner = styled('div', `
|
||||
display: flex;
|
||||
padding: 10px;
|
||||
gap: 16px;
|
||||
color: white;
|
||||
|
||||
&-warning {
|
||||
background: #E6A117;
|
||||
}
|
||||
|
||||
&-error {
|
||||
background: ${colors.error};
|
||||
}
|
||||
`);
|
||||
|
||||
const cssButtons = styled('div', `
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
`);
|
||||
|
||||
const cssButton = styled(icon, `
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
background-color: white;
|
||||
`);
|
||||
|
||||
const cssExpandButton = styled(cssButton, `
|
||||
&-expanded {
|
||||
-webkit-mask-image: var(--icon-DropdownUp);
|
||||
}
|
||||
`);
|
||||
|
||||
const cssLightlyBoldedText = styled('div', `
|
||||
font-weight: 500;
|
||||
`);
|
||||
|
||||
const cssIconAndText = styled('div', `
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
`);
|
||||
|
||||
const cssBannerMessage = styled(cssIconAndText, `
|
||||
flex-grow: 1;
|
||||
justify-content: center;
|
||||
`);
|
||||
|
||||
const cssIcon = styled(icon, `
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-color: white;
|
||||
`);
|
@ -1,171 +0,0 @@
|
||||
import {buildLimitStatusMessage, buildUpgradeMessage} from 'app/client/components/DocumentUsage';
|
||||
import {sessionStorageBoolObs} from 'app/client/lib/localStorageObs';
|
||||
import {DocPageModel} from 'app/client/models/DocPageModel';
|
||||
import {colors, isNarrowScreenObs} from 'app/client/ui2018/cssVars';
|
||||
import {icon} from 'app/client/ui2018/icons';
|
||||
import {Computed, Disposable, dom, DomComputed, makeTestId, Observable, styled} from 'grainjs';
|
||||
|
||||
const testId = makeTestId('test-doc-usage-banner-');
|
||||
|
||||
export class DocUsageBanner extends Disposable {
|
||||
// Whether the banner is vertically expanded on narrow screens.
|
||||
private readonly _isExpanded = Observable.create(this, true);
|
||||
|
||||
private readonly _currentDocId = this._docPageModel.currentDocId;
|
||||
private readonly _currentDocUsage = this._docPageModel.currentDocUsage;
|
||||
private readonly _currentOrg = this._docPageModel.currentOrg;
|
||||
|
||||
private readonly _dataLimitStatus = Computed.create(this, this._currentDocUsage, (_use, usage) => {
|
||||
return usage?.dataLimitStatus ?? null;
|
||||
});
|
||||
|
||||
private readonly _shouldShowBanner: Computed<boolean> =
|
||||
Computed.create(this, this._currentOrg, (_use, org) => {
|
||||
return org?.access !== 'guests' && org?.access !== null;
|
||||
});
|
||||
|
||||
// Session storage observable. Set to false to dismiss the banner for the session.
|
||||
private _showApproachingLimitBannerPref: Observable<boolean>;
|
||||
|
||||
constructor(private _docPageModel: DocPageModel) {
|
||||
super();
|
||||
this.autoDispose(this._currentDocId.addListener((docId) => {
|
||||
if (this._showApproachingLimitBannerPref?.isDisposed() === false) {
|
||||
this._showApproachingLimitBannerPref.dispose();
|
||||
}
|
||||
const userId = this._docPageModel.appModel.currentUser?.id ?? 0;
|
||||
this._showApproachingLimitBannerPref = sessionStorageBoolObs(
|
||||
`u=${userId}:doc=${docId}:showApproachingLimitBanner`,
|
||||
true,
|
||||
);
|
||||
}));
|
||||
}
|
||||
|
||||
public buildDom() {
|
||||
return dom.maybe(this._dataLimitStatus, (status): DomComputed => {
|
||||
switch (status) {
|
||||
case 'approachingLimit': { return this._buildApproachingLimitBanner(); }
|
||||
case 'gracePeriod':
|
||||
case 'deleteOnly': { return this._buildExceedingLimitBanner(status === 'deleteOnly'); }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _buildApproachingLimitBanner() {
|
||||
return dom.maybe(this._shouldShowBanner, () => {
|
||||
return dom.domComputed(use => {
|
||||
if (!use(this._showApproachingLimitBannerPref)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const org = use(this._currentOrg);
|
||||
if (!org) { return null; }
|
||||
|
||||
const features = org.billingAccount?.product.features;
|
||||
return cssApproachingLimitBanner(
|
||||
cssBannerMessage(
|
||||
cssWhiteIcon('Idea'),
|
||||
cssLightlyBoldedText(
|
||||
buildLimitStatusMessage('approachingLimit', features),
|
||||
' ',
|
||||
buildUpgradeMessage(org.access === 'owners'),
|
||||
testId('text'),
|
||||
),
|
||||
),
|
||||
cssCloseButton('CrossBig',
|
||||
dom.on('click', () => this._showApproachingLimitBannerPref.set(false)),
|
||||
testId('close'),
|
||||
),
|
||||
testId('container'),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private _buildExceedingLimitBanner(isDeleteOnly: boolean) {
|
||||
return dom.maybe(this._shouldShowBanner, () => {
|
||||
return dom.maybe(this._currentOrg, org => {
|
||||
const features = org.billingAccount?.product.features;
|
||||
return cssExceedingLimitBanner(
|
||||
cssBannerMessage(
|
||||
cssWhiteIcon('Idea'),
|
||||
cssLightlyBoldedText(
|
||||
dom.domComputed(use => {
|
||||
const isExpanded = use(this._isExpanded);
|
||||
const isNarrowScreen = use(isNarrowScreenObs());
|
||||
const isOwner = org.access === 'owners';
|
||||
if (isNarrowScreen && !isExpanded) {
|
||||
return buildUpgradeMessage(isOwner, 'short');
|
||||
}
|
||||
|
||||
return [
|
||||
buildLimitStatusMessage(isDeleteOnly ? 'deleteOnly' : 'gracePeriod', features),
|
||||
' ',
|
||||
buildUpgradeMessage(isOwner),
|
||||
];
|
||||
}),
|
||||
testId('text'),
|
||||
),
|
||||
),
|
||||
dom.maybe(isNarrowScreenObs(), () => {
|
||||
return dom.domComputed(this._isExpanded, isExpanded =>
|
||||
cssExpandButton(
|
||||
isExpanded ? 'DropdownUp' : 'Dropdown',
|
||||
dom.on('click', () => this._isExpanded.set(!isExpanded)),
|
||||
),
|
||||
);
|
||||
}),
|
||||
testId('container'),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const cssLightlyBoldedText = styled('div', `
|
||||
font-weight: 500;
|
||||
`);
|
||||
|
||||
const cssUsageBanner = styled('div', `
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 10px;
|
||||
color: white;
|
||||
gap: 16px;
|
||||
`);
|
||||
|
||||
const cssApproachingLimitBanner = styled(cssUsageBanner, `
|
||||
background: #E6A117;
|
||||
`);
|
||||
|
||||
const cssExceedingLimitBanner = styled(cssUsageBanner, `
|
||||
background: ${colors.error};
|
||||
`);
|
||||
|
||||
const cssIconAndText = styled('div', `
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
`);
|
||||
|
||||
const cssBannerMessage = styled(cssIconAndText, `
|
||||
flex-grow: 1;
|
||||
justify-content: center;
|
||||
`);
|
||||
|
||||
const cssIcon = styled(icon, `
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
`);
|
||||
|
||||
const cssWhiteIcon = styled(cssIcon, `
|
||||
background-color: white;
|
||||
`);
|
||||
|
||||
const cssCloseButton = styled(cssIcon, `
|
||||
flex-shrink: 0;
|
||||
cursor: pointer;
|
||||
background-color: white;
|
||||
`);
|
||||
|
||||
const cssExpandButton = cssCloseButton;
|
@ -0,0 +1,93 @@
|
||||
import {Banner, buildBannerMessage} from 'app/client/components/Banner';
|
||||
import {buildUpgradeMessage} from 'app/client/components/DocumentUsage';
|
||||
import {sessionStorageBoolObs} from 'app/client/lib/localStorageObs';
|
||||
import {HomeModel} from 'app/client/models/HomeModel';
|
||||
import {isFreeProduct} from 'app/common/Features';
|
||||
import {isOwner} from 'app/common/roles';
|
||||
import {Disposable, dom, makeTestId, Observable} from 'grainjs';
|
||||
|
||||
const testId = makeTestId('test-site-usage-banner-');
|
||||
|
||||
export class SiteUsageBanner extends Disposable {
|
||||
private readonly _currentOrg = this._homeModel.app.currentOrg;
|
||||
private readonly _currentOrgUsage = this._homeModel.currentOrgUsage;
|
||||
private readonly _product = this._currentOrg?.billingAccount?.product;
|
||||
private readonly _currentUser = this._homeModel.app.currentValidUser;
|
||||
|
||||
// Session storage observable. Set to false to dismiss the banner for the session.
|
||||
private _showApproachingLimitBannerPref?: Observable<boolean>;
|
||||
|
||||
constructor(private _homeModel: HomeModel) {
|
||||
super();
|
||||
|
||||
if (this._currentUser && isOwner(this._currentOrg)) {
|
||||
this._showApproachingLimitBannerPref = this.autoDispose(sessionStorageBoolObs(
|
||||
`u=${this._currentUser.id}:org=${this._currentOrg.id}:showApproachingLimitBanner`,
|
||||
true,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
public buildDom() {
|
||||
return dom.maybe(this._currentOrgUsage, (usage) => {
|
||||
const {approachingLimit, gracePeriod, deleteOnly} = usage;
|
||||
if (deleteOnly > 0 || gracePeriod > 0) {
|
||||
return this._buildExceedingLimitsBanner(deleteOnly + gracePeriod);
|
||||
} else if (approachingLimit > 0) {
|
||||
return this._buildApproachingLimitsBanner(approachingLimit);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _buildApproachingLimitsBanner(numDocs: number) {
|
||||
return dom.domComputed(use => {
|
||||
if (this._showApproachingLimitBannerPref && !use(this._showApproachingLimitBannerPref)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const limitsMessage = numDocs > 1
|
||||
? `${numDocs} documents are approaching their limits.`
|
||||
: `${numDocs} document is approaching its limits.`;
|
||||
return dom.create(Banner, {
|
||||
content: buildBannerMessage(
|
||||
limitsMessage,
|
||||
(this._product && isFreeProduct(this._product)
|
||||
? [' ', buildUpgradeMessage(true)]
|
||||
: null
|
||||
),
|
||||
testId('text'),
|
||||
),
|
||||
style: 'warning',
|
||||
showCloseButton: true,
|
||||
onClose: () => this._showApproachingLimitBannerPref?.set(false),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private _buildExceedingLimitsBanner(numDocs: number) {
|
||||
const limitsMessage = numDocs > 1
|
||||
? `${numDocs} documents have exceeded their limits.`
|
||||
: `${numDocs} document has exceeded its limits.`;
|
||||
return dom.create(Banner, {
|
||||
content: buildBannerMessage(
|
||||
limitsMessage,
|
||||
(this._product && isFreeProduct(this._product)
|
||||
? [' ', buildUpgradeMessage(true)]
|
||||
: null
|
||||
),
|
||||
testId('text'),
|
||||
),
|
||||
contentSmall: buildBannerMessage(
|
||||
(this._product && isFreeProduct(this._product)
|
||||
? buildUpgradeMessage(true, 'short')
|
||||
: limitsMessage
|
||||
),
|
||||
),
|
||||
style: 'error',
|
||||
showCloseButton: false,
|
||||
showExpandButton: true,
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
import {DocPageModel} from 'app/client/models/DocPageModel';
|
||||
import {Disposable} from 'grainjs';
|
||||
|
||||
export class DocUsageBanner extends Disposable {
|
||||
constructor(_docPageModel: DocPageModel) { super(); }
|
||||
|
||||
public buildDom() {
|
||||
return null;
|
||||
}
|
||||
}
|
Loading…
Reference in new issue