(core) Add Support Grist page and nudge

Summary:
Adds a new Support Grist page (accessible only in grist-core), containing
options to opt in to telemetry and sponsor Grist Labs on GitHub.

A nudge is also shown in the doc menu, which can be collapsed or permanently
dismissed.

Test Plan: Browser and server tests.

Reviewers: paulfitz, dsagal

Reviewed By: paulfitz

Subscribers: jarek, dsagal

Differential Revision: https://phab.getgrist.com/D3926
This commit is contained in:
George Gevoian
2023-07-04 17:21:34 -04:00
parent 051c6d52fe
commit 35237a5835
47 changed files with 1743 additions and 365 deletions

View File

@@ -65,7 +65,7 @@ export class AccountWidget extends Disposable {
t("Toggle Mobile Mode"),
cssCheckmark('Tick', dom.show(viewport.viewportEnabled)),
testId('usermenu-toggle-mobile'),
);
);
if (!user) {
return [
@@ -100,6 +100,7 @@ export class AccountWidget extends Disposable {
this._maybeBuildBillingPageMenuItem(),
this._maybeBuildActivationPageMenuItem(),
this._maybeBuildSupportGristPageMenuItem(),
mobileModeToggle,
@@ -155,10 +156,10 @@ export class AccountWidget extends Disposable {
// For links, disabling with just a class is hard; easier to just not make it a link.
// TODO weasel menus should support disabling menuItemLink.
(isBillingManager ?
menuItemLink(urlState().setLinkUrl({billing: 'billing'}), 'Billing Account') :
menuItem(() => null, 'Billing Account', dom.cls('disabled', true))
menuItemLink(urlState().setLinkUrl({billing: 'billing'}), t('Billing Account')) :
menuItem(() => null, t('Billing Account'), dom.cls('disabled', true))
) :
menuItem(() => this._appModel.showUpgradeModal(), 'Upgrade Plan');
menuItem(() => this._appModel.showUpgradeModal(), t('Upgrade Plan'));
}
private _maybeBuildActivationPageMenuItem() {
@@ -167,7 +168,21 @@ export class AccountWidget extends Disposable {
return null;
}
return menuItemLink('Activation', urlState().setLinkUrl({activation: 'activation'}));
return menuItemLink(t('Activation'), urlState().setLinkUrl({activation: 'activation'}));
}
private _maybeBuildSupportGristPageMenuItem() {
const {deploymentType} = getGristConfig();
if (deploymentType !== 'core') {
return null;
}
return menuItemLink(
t('Support Grist'),
cssHeartIcon('💛'),
urlState().setLinkUrl({supportGrist: 'support-grist'}),
testId('usermenu-support-grist'),
);
}
}
@@ -183,6 +198,10 @@ export const cssUserIcon = styled('div', `
cursor: pointer;
`);
const cssHeartIcon = styled('span', `
margin-left: 8px;
`);
const cssUserInfo = styled('div', `
padding: 12px 24px 12px 16px;
min-width: 200px;

View File

@@ -1,7 +1,7 @@
import {buildDocumentBanners, buildHomeBanners} from 'app/client/components/Banners';
import {ViewAsBanner} from 'app/client/components/ViewAsBanner';
import {domAsync} from 'app/client/lib/domAsync';
import {loadBillingPage} from 'app/client/lib/imports';
import {loadAccountPage, loadActivationPage, loadBillingPage, loadSupportGristPage} from 'app/client/lib/imports';
import {createSessionObs, isBoolean, isNumber} from 'app/client/lib/sessionObs';
import {AppModel, TopAppModel} from 'app/client/models/AppModel';
import {DocPageModelImpl} from 'app/client/models/DocPageModel';
@@ -75,6 +75,12 @@ function createMainPage(appModel: AppModel, appObj: App) {
return domAsync(loadBillingPage().then(bp => dom.create(bp.BillingPage, appModel)));
} else if (pageType === 'welcome') {
return dom.create(WelcomePage, appModel);
} else if (pageType === 'account') {
return domAsync(loadAccountPage().then(ap => dom.create(ap.AccountPage, appModel)));
} else if (pageType === 'support-grist') {
return domAsync(loadSupportGristPage().then(sgp => dom.create(sgp.SupportGristPage, appModel)));
} else if (pageType === 'activation') {
return domAsync(loadActivationPage().then(ap => dom.create(ap.ActivationPage, appModel)));
} else {
return dom.create(pagePanelsDoc, appModel, appObj);
}

View File

@@ -174,7 +174,14 @@ function createLoadedDocMenu(owner: IDisposableOwner, home: HomeModel) {
testId('doclist')
),
dom.maybe(use => !use(isNarrowScreenObs()) && ['all', 'workspace'].includes(use(home.currentPage)),
() => upgradeButton.showUpgradeCard(css.upgradeCard.cls(''))),
() => {
// TODO: These don't currently clash (grist-core stubs the upgradeButton), but a way to
// manage card popups will be needed if more are added later.
return [
upgradeButton.showUpgradeCard(css.upgradeCard.cls('')),
home.app.supportGristNudge.showCard(),
];
}),
));
}

View File

@@ -1,4 +1,5 @@
import {GristDoc} from 'app/client/components/GristDoc';
import {logTelemetryEvent} from 'app/client/lib/telemetry';
import {getWelcomeHomeUrl, urlState} from 'app/client/models/gristUrlState';
import {renderer} from 'app/client/ui/DocTutorialRenderer';
import {cssPopupBody, FloatingPopup} from 'app/client/ui/FloatingPopup';
@@ -30,12 +31,12 @@ const TOOLTIP_KEY = 'docTutorialTooltip';
export class DocTutorial extends FloatingPopup {
private _appModel = this._gristDoc.docPageModel.appModel;
private _currentDoc = this._gristDoc.docPageModel.currentDoc.get();
private _currentFork = this._currentDoc?.forks?.[0];
private _docComm = this._gristDoc.docComm;
private _docData = this._gristDoc.docData;
private _docId = this._gristDoc.docId();
private _slides: Observable<DocTutorialSlide[] | null> = Observable.create(this, null);
private _currentSlideIndex = Observable.create(this,
this._currentDoc?.forks?.[0]?.options?.tutorial?.lastSlideIndex ?? 0);
private _currentSlideIndex = Observable.create(this, this._currentFork?.options?.tutorial?.lastSlideIndex ?? 0);
private _saveCurrentSlidePositionDebounced = debounce(this._saveCurrentSlidePosition, 1000, {
@@ -231,14 +232,30 @@ export class DocTutorial extends FloatingPopup {
private async _saveCurrentSlidePosition() {
const currentOptions = this._currentDoc?.options ?? {};
const currentSlideIndex = this._currentSlideIndex.get();
const numSlides = this._slides.get()?.length;
await this._appModel.api.updateDoc(this._docId, {
options: {
...currentOptions,
tutorial: {
lastSlideIndex: this._currentSlideIndex.get(),
lastSlideIndex: currentSlideIndex,
}
}
});
let percentComplete: number | undefined = undefined;
if (numSlides !== undefined && numSlides > 0) {
percentComplete = Math.floor(((currentSlideIndex + 1) / numSlides) * 100);
}
logTelemetryEvent('tutorialProgressChanged', {
full: {
tutorialForkIdDigest: this._currentFork?.id,
tutorialTrunkIdDigest: this._currentFork?.trunkId,
lastSlideIndex: currentSlideIndex,
numSlides,
percentComplete,
},
});
}
private async _changeSlide(slideIndex: number) {

View File

@@ -3,7 +3,7 @@ import {makeT} from 'app/client/lib/localization';
import {ShortcutKey, ShortcutKeyContent} from 'app/client/ui/ShortcutKey';
import {icon} from 'app/client/ui2018/icons';
import {cssLink} from 'app/client/ui2018/links';
import {commonUrls} from 'app/common/gristUrls';
import {commonUrls, GristDeploymentType} from 'app/common/gristUrls';
import {BehavioralPrompt} from 'app/common/Prefs';
import {dom, DomContents, DomElementArg, styled} from 'grainjs';
@@ -104,6 +104,7 @@ export const GristTooltips: Record<Tooltip, TooltipContentFunc> = {
export interface BehavioralPromptContent {
title: () => string;
content: (...domArgs: DomElementArg[]) => DomContents;
deploymentTypes: GristDeploymentType[] | '*';
}
export const GristBehavioralPrompts: Record<BehavioralPrompt, BehavioralPromptContent> = {
@@ -119,6 +120,7 @@ export const GristBehavioralPrompts: Record<BehavioralPrompt, BehavioralPromptCo
),
...args,
),
deploymentTypes: ['saas'],
},
referenceColumnsConfig: {
title: () => t('Reference Columns'),
@@ -133,6 +135,7 @@ export const GristBehavioralPrompts: Record<BehavioralPrompt, BehavioralPromptCo
),
...args,
),
deploymentTypes: ['saas'],
},
rawDataPage: {
title: () => t('Raw Data page'),
@@ -142,6 +145,7 @@ export const GristBehavioralPrompts: Record<BehavioralPrompt, BehavioralPromptCo
dom('div', cssLink({href: commonUrls.helpRawData, target: '_blank'}, t('Learn more.'))),
...args,
),
deploymentTypes: ['saas'],
},
accessRules: {
title: () => t('Access Rules'),
@@ -151,6 +155,7 @@ export const GristBehavioralPrompts: Record<BehavioralPrompt, BehavioralPromptCo
dom('div', cssLink({href: commonUrls.helpAccessRules, target: '_blank'}, t('Learn more.'))),
...args,
),
deploymentTypes: ['saas'],
},
filterButtons: {
title: () => t('Pinning Filters'),
@@ -160,6 +165,7 @@ export const GristBehavioralPrompts: Record<BehavioralPrompt, BehavioralPromptCo
dom('div', cssLink({href: commonUrls.helpFilterButtons, target: '_blank'}, t('Learn more.'))),
...args,
),
deploymentTypes: ['saas'],
},
nestedFiltering: {
title: () => t('Nested Filtering'),
@@ -168,6 +174,7 @@ export const GristBehavioralPrompts: Record<BehavioralPrompt, BehavioralPromptCo
dom('div', t('Only those rows will appear which match all of the filters.')),
...args,
),
deploymentTypes: ['saas'],
},
pageWidgetPicker: {
title: () => t('Selecting Data'),
@@ -176,6 +183,7 @@ export const GristBehavioralPrompts: Record<BehavioralPrompt, BehavioralPromptCo
dom('div', t('Use the 𝚺 icon to create summary (or pivot) tables, for totals or subtotals.')),
...args,
),
deploymentTypes: ['saas'],
},
pageWidgetPickerSelectBy: {
title: () => t('Linking Widgets'),
@@ -185,6 +193,7 @@ export const GristBehavioralPrompts: Record<BehavioralPrompt, BehavioralPromptCo
dom('div', cssLink({href: commonUrls.helpLinkingWidgets, target: '_blank'}, t('Learn more.'))),
...args,
),
deploymentTypes: ['saas'],
},
editCardLayout: {
title: () => t('Editing Card Layout'),
@@ -195,6 +204,7 @@ export const GristBehavioralPrompts: Record<BehavioralPrompt, BehavioralPromptCo
})),
...args,
),
deploymentTypes: ['saas'],
},
addNew: {
title: () => t('Add New'),
@@ -203,6 +213,7 @@ export const GristBehavioralPrompts: Record<BehavioralPrompt, BehavioralPromptCo
+ 'or import data.')),
...args,
),
deploymentTypes: ['saas'],
},
rickRow: {
title: () => t('Anchor Links'),
@@ -217,6 +228,7 @@ export const GristBehavioralPrompts: Record<BehavioralPrompt, BehavioralPromptCo
),
...args,
),
deploymentTypes: '*',
},
customURL: {
title: () => t('Custom Widgets'),
@@ -230,5 +242,6 @@ export const GristBehavioralPrompts: Record<BehavioralPrompt, BehavioralPromptCo
dom('div', cssLink({href: commonUrls.helpCustomWidgets, target: '_blank'}, t('Learn more.'))),
...args,
),
deploymentTypes: ['saas'],
},
};

View File

@@ -0,0 +1,326 @@
import {makeT} from 'app/client/lib/localization';
import {localStorageObs} from 'app/client/lib/localStorageObs';
import {getStorage} from 'app/client/lib/storage';
import {tokenFieldStyles} from 'app/client/lib/TokenField';
import {AppModel} from 'app/client/models/AppModel';
import {urlState} from 'app/client/models/gristUrlState';
import {TelemetryModel, TelemetryModelImpl} from 'app/client/models/TelemetryModel';
import {bigPrimaryButton} from 'app/client/ui2018/buttons';
import {colors, isNarrowScreenObs, theme, vars} from 'app/client/ui2018/cssVars';
import {icon} from 'app/client/ui2018/icons';
import {cssLink} from 'app/client/ui2018/links';
import {commonUrls} from 'app/common/gristUrls';
import {getGristConfig} from 'app/common/urlUtils';
import {Disposable, dom, makeTestId, Observable, styled} from 'grainjs';
const testId = makeTestId('test-support-grist-nudge-');
const t = makeT('SupportGristNudge');
type ButtonState =
| 'collapsed'
| 'expanded';
type CardPage =
| 'support-grist'
| 'opted-in';
/**
* Nudges users to support Grist by opting in to telemetry.
*
* This currently includes a button that opens a card with the nudge.
* The button is hidden when the card is visible, and vice versa.
*/
export class SupportGristNudge extends Disposable {
private readonly _telemetryModel: TelemetryModel = new TelemetryModelImpl(this._appModel);
private readonly _buttonState: Observable<ButtonState>;
private readonly _currentPage: Observable<CardPage>;
private readonly _isClosed: Observable<boolean>;
constructor(private _appModel: AppModel) {
super();
if (!this._shouldShowCardOrButton()) { return; }
this._buttonState = localStorageObs(
`u=${this._appModel.currentValidUser?.id ?? 0};supportGristNudge`, 'expanded'
) as Observable<ButtonState>;
this._currentPage = Observable.create(null, 'support-grist');
this._isClosed = Observable.create(this, false);
}
public showButton() {
if (!this._shouldShowCardOrButton()) { return null; }
return dom.maybe(
use => !use(isNarrowScreenObs()) && (use(this._buttonState) === 'collapsed' && !use(this._isClosed)),
() => this._buildButton()
);
}
public showCard() {
if (!this._shouldShowCardOrButton()) { return null; }
return dom.maybe(
use => !use(isNarrowScreenObs()) && (use(this._buttonState) === 'expanded' && !use(this._isClosed)),
() => this._buildCard()
);
}
private _markAsDismissed() {
this._appModel.dismissedPopup('supportGrist').set(true);
getStorage().removeItem(
`u=${this._appModel.currentValidUser?.id ?? 0};supportGristNudge`);
}
private _close() {
this._isClosed.set(true);
}
private _dismissAndClose() {
this._markAsDismissed();
this._close();
}
private _shouldShowCardOrButton() {
if (this._appModel.dismissedPopups.get().includes('supportGrist')) {
return false;
}
const {activation, deploymentType, telemetry} = getGristConfig();
if (deploymentType !== 'core' || !activation?.isManager) {
return false;
}
if (telemetry && telemetry.telemetryLevel !== 'off') {
return false;
}
return true;
}
private _buildButton() {
return cssContributeButton(
cssButtonIconAndText(
icon('Fireworks'),
t('Contribute'),
),
cssContributeButtonCloseButton(
icon('CrossSmall'),
dom.on('click', (ev) => {
ev.stopPropagation();
this._dismissAndClose();
}),
testId('contribute-button-close'),
),
dom.on('click', () => { this._buttonState.set('expanded'); }),
testId('contribute-button'),
);
}
private _buildCard() {
return cssCard(
dom.domComputed(this._currentPage, page => {
if (page === 'support-grist') {
return this._buildSupportGristCardContent();
} else {
return this._buildOptedInCardContent();
}
}),
testId('card'),
);
}
private _buildSupportGristCardContent() {
return [
cssCloseButton(
icon('CrossBig'),
dom.on('click', () => this._buttonState.set('collapsed')),
testId('card-close'),
),
cssLeftAlignedHeader(t('Support Grist')),
cssParagraph(t(
'Opt in to telemetry to help us understand how the product ' +
'is used, so that we can prioritize future improvements.'
)),
cssParagraph(
t(
'We only collect usage statistics, as detailed in our {{helpCenterLink}}, never ' +
'document contents. Opt out any time from the {{supportGristLink}} in the user menu.',
{
helpCenterLink: helpCenterLink(),
supportGristLink: supportGristLink(),
},
),
),
cssFullWidthButton(
t('Opt in to Telemetry'),
dom.on('click', () => this._optInToTelemetry()),
testId('card-opt-in'),
),
];
}
private _buildOptedInCardContent() {
return [
cssCloseButton(
icon('CrossBig'),
dom.on('click', () => this._close()),
testId('card-close-icon-button'),
),
cssCenteredFlex(cssSparks()),
cssCenterAlignedHeader(t('Opted In')),
cssParagraph(
t(
'Thank you! Your trust and support is greatly appreciated. ' +
'Opt out any time from the {{link}} in the user menu.',
{link: supportGristLink()},
),
),
cssCenteredFlex(
cssPrimaryButton(
t('Close'),
dom.on('click', () => this._close()),
testId('card-close-button'),
),
),
];
}
private async _optInToTelemetry() {
await this._telemetryModel.updateTelemetryPrefs({telemetryLevel: 'limited'});
this._currentPage.set('opted-in');
this._markAsDismissed();
}
}
function helpCenterLink() {
return cssLink(
t('Help Center'),
{href: commonUrls.helpTelemetryLimited, target: '_blank'},
);
}
function supportGristLink() {
return cssLink(
t('Support Grist page'),
{href: urlState().makeUrl({supportGrist: 'support-grist'}), target: '_blank'},
);
}
const cssCenteredFlex = styled('div', `
display: flex;
justify-content: center;
align-items: center;
`);
const cssContributeButton = styled('div', `
position: relative;
background: ${theme.controlPrimaryBg};
color: ${theme.controlPrimaryFg};
border-radius: 25px;
padding: 4px 12px 4px 8px;
font-style: normal;
font-weight: medium;
font-size: 13px;
line-height: 16px;
cursor: pointer;
--icon-color: ${theme.controlPrimaryFg};
&:hover {
background: ${theme.controlPrimaryHoverBg};
}
`);
const cssButtonIconAndText = styled('div', `
display: flex;
gap: 8px;
`);
const cssContributeButtonCloseButton = styled(tokenFieldStyles.cssDeleteButton, `
margin-left: 4px;
vertical-align: bottom;
line-height: 1;
position: absolute;
top: -4px;
right: -8px;
border-radius: 16px;
background-color: ${colors.dark};
width: 18px;
height: 18px;
cursor: pointer;
z-index: 1;
display: none;
align-items: center;
justify-content: center;
.${cssContributeButton.className}:hover & {
display: flex;
}
`);
const cssCard = styled('div', `
width: 297px;
padding: 24px;
background: #DCF4EB;
border-radius: 4px;
align-self: flex-start;
position: sticky;
flex-shrink: 0;
top: 0px;
`);
const cssHeader = styled('div', `
font-size: ${vars.xxxlargeFontSize};
font-weight: 600;
margin-bottom: 16px;
`);
const cssLeftAlignedHeader = styled(cssHeader, `
text-align: left;
`);
const cssCenterAlignedHeader = styled(cssHeader, `
text-align: center;
`);
const cssParagraph = styled('div', `
font-size: 13px;
line-height: 18px;
margin-bottom: 12px;
`);
const cssPrimaryButton = styled(bigPrimaryButton, `
display: flex;
justify-content: center;
align-items: center;
margin-top: 32px;
text-align: center;
`);
const cssFullWidthButton = styled(cssPrimaryButton, `
width: 100%;
`);
const cssCloseButton = styled('div', `
position: absolute;
top: 8px;
right: 8px;
padding: 4px;
border-radius: 4px;
cursor: pointer;
--icon-color: ${colors.slate};
&:hover {
background-color: ${colors.mediumGreyOpaque};
}
`);
const cssSparks = styled('div', `
height: 48px;
width: 48px;
background-image: var(--icon-Sparks);
display: inline-block;
background-repeat: no-repeat;
`);

View File

@@ -0,0 +1,289 @@
import {buildHomeBanners} from 'app/client/components/Banners';
import {makeT} from 'app/client/lib/localization';
import {AppModel} from 'app/client/models/AppModel';
import {urlState} from 'app/client/models/gristUrlState';
import {TelemetryModel, TelemetryModelImpl} from 'app/client/models/TelemetryModel';
import {AppHeader} from 'app/client/ui/AppHeader';
import {leftPanelBasic} from 'app/client/ui/LeftPanelCommon';
import {pagePanels} from 'app/client/ui/PagePanels';
import {createTopBarHome} from 'app/client/ui/TopBar';
import {cssBreadcrumbs, separator} from 'app/client/ui2018/breadcrumbs';
import {bigBasicButton, bigBasicButtonLink, bigPrimaryButton} from 'app/client/ui2018/buttons';
import {mediaSmall, theme, vars} 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 {commonUrls} from 'app/common/gristUrls';
import {TelemetryPrefsWithSources} from 'app/common/InstallAPI';
import {getGristConfig} from 'app/common/urlUtils';
import {Computed, Disposable, dom, makeTestId, Observable, styled} from 'grainjs';
const testId = makeTestId('test-support-grist-page-');
const t = makeT('SupportGristPage');
export class SupportGristPage extends Disposable {
private readonly _model: TelemetryModel = new TelemetryModelImpl(this._appModel);
private readonly _optInToTelemetry = Computed.create(this, this._model.prefs,
(_use, prefs) => {
if (!prefs) { return null; }
return prefs.telemetryLevel.value !== 'off';
})
.onWrite(async (optIn) => {
const telemetryLevel = optIn ? 'limited' : 'off';
await this._model.updateTelemetryPrefs({telemetryLevel});
});
constructor(private _appModel: AppModel) {
super();
this._model.fetchTelemetryPrefs().catch(reportError);
}
public buildDom() {
const panelOpen = Observable.create(this, false);
return pagePanels({
leftPanel: {
panelWidth: Observable.create(this, 240),
panelOpen,
hideOpener: true,
header: dom.create(AppHeader, this._appModel.currentOrgName, this._appModel),
content: leftPanelBasic(this._appModel, panelOpen),
},
headerMain: this._buildMainHeader(),
contentTop: buildHomeBanners(this._appModel),
contentMain: this._buildMainContent(),
});
}
private _buildMainHeader() {
return dom.frag(
cssBreadcrumbs({style: 'margin-left: 16px;'},
cssLink(
urlState().setLinkUrl({}),
t('Home'),
),
separator(' / '),
dom('span', t('Support Grist')),
),
createTopBarHome(this._appModel),
);
}
private _buildMainContent() {
return cssPageContainer(
cssPage(
dom('div',
cssPageTitle(t('Support Grist')),
this._buildTelemetrySection(),
this._buildSponsorshipSection(),
),
),
);
}
private _buildTelemetrySection() {
return cssSection(
cssSectionTitle(t('Telemetry')),
dom.domComputed(this._model.prefs, prefs => {
if (prefs === null) {
return cssSpinnerBox(loadingSpinner());
}
const {activation} = getGristConfig();
if (!activation?.isManager) {
if (prefs.telemetryLevel.value === 'limited') {
return [
cssParagraph(t(
'This instance is opted in to telemetry. Only the site administrator has permission to change this.',
))
];
} else {
return [
cssParagraph(t(
'This instance is opted out of telemetry. Only the site administrator has permission to change this.',
))
];
}
} else {
return [
cssParagraph(t(
'Support Grist by opting in to telemetry, which helps us understand how the product ' +
'is used, so that we can prioritize future improvements.'
)),
cssParagraph(
t('We only collect usage statistics, as detailed in our {{link}}, never document contents.', {
link: telemetryHelpCenterLink(),
}),
),
cssParagraph(t('You can opt out of telemetry at any time from this page.')),
this._buildTelemetrySectionButtons(prefs),
];
}
}),
testId('telemetry-section'),
);
}
private _buildTelemetrySectionButtons(prefs: TelemetryPrefsWithSources) {
const {telemetryLevel: {value, source}} = prefs;
if (source === 'preferences') {
return dom.domComputed(this._optInToTelemetry, (optedIn) => {
if (optedIn) {
return [
cssOptInOutMessage(
t('You have opted in to telemetry. Thank you!'), ' 🙏',
testId('telemetry-section-message'),
),
cssOptOutButton(t('Opt out of Telemetry'),
dom.on('click', () => this._optInToTelemetry.set(false)),
),
];
} else {
return [
cssOptInButton(t('Opt in to Telemetry'),
dom.on('click', () => this._optInToTelemetry.set(true)),
),
];
}
});
} else {
return cssOptInOutMessage(
value !== 'off'
? [t('You have opted in to telemetry. Thank you!'), ' 🙏']
: t('You have opted out of telemetry.'),
testId('telemetry-section-message'),
);
}
}
private _buildSponsorshipSection() {
return cssSection(
cssSectionTitle(t('Sponsor Grist Labs on GitHub')),
cssParagraph(
t(
'Grist software is developed by Grist Labs, which offers free and paid ' +
'hosted plans. We also make Grist code available under a standard free ' +
'and open OSS license (Apache 2.0) on {{link}}.',
{link: gristCoreLink()},
),
),
cssParagraph(
t(
'You can support Grist open-source development by sponsoring ' +
'us on our {{link}}.',
{link: sponsorGristLink()},
),
),
cssParagraph(t(
'We are a small and determined team. Your support matters a lot to us. ' +
'It also shows to others that there is a determined community behind this product.'
)),
cssSponsorButton(
cssButtonIconAndText(icon('Heart'), cssButtonText(t('Manage Sponsorship'))),
{href: commonUrls.githubSponsorGristLabs, target: '_blank'},
),
testId('sponsorship-section'),
);
}
}
function telemetryHelpCenterLink() {
return cssLink(
t('Help Center'),
{href: commonUrls.helpTelemetryLimited, target: '_blank'},
);
}
function sponsorGristLink() {
return cssLink(
t('GitHub Sponsors page'),
{href: commonUrls.githubSponsorGristLabs, target: '_blank'},
);
}
function gristCoreLink() {
return cssLink(
t('GitHub'),
{href: commonUrls.githubGristCore, target: '_blank'},
);
}
const cssPageContainer = styled('div', `
overflow: auto;
padding: 64px 80px;
@media ${mediaSmall} {
& {
padding: 0px;
}
}
`);
const cssPage = styled('div', `
padding: 16px;
max-width: 600px;
width: 100%;
`);
const cssPageTitle = styled('div', `
height: 32px;
line-height: 32px;
margin-bottom: 24px;
color: ${theme.text};
font-size: 24px;
font-weight: ${vars.headerControlTextWeight};
`);
const cssSectionTitle = styled('div', `
height: 24px;
line-height: 24px;
margin-bottom: 24px;
color: ${theme.text};
font-size: ${vars.xlargeFontSize};
font-weight: ${vars.headerControlTextWeight};
`);
const cssSection = styled('div', `
margin-bottom: 60px;
`);
const cssParagraph = styled('div', `
color: ${theme.text};
font-size: 14px;
line-height: 20px;
margin-bottom: 12px;
`);
const cssOptInOutMessage = styled(cssParagraph, `
line-height: 40px;
font-weight: 600;
margin-top: 24px;
margin-bottom: 0px;
`);
const cssOptInButton = styled(bigPrimaryButton, `
margin-top: 24px;
`);
const cssOptOutButton = styled(bigBasicButton, `
margin-top: 24px;
`);
const cssSponsorButton = styled(bigBasicButtonLink, `
margin-top: 24px;
`);
const cssButtonIconAndText = styled('div', `
display: flex;
align-items: center;
`);
const cssButtonText = styled('span', `
margin-left: 8px;
`);
const cssSpinnerBox = styled('div', `
margin-top: 24px;
text-align: center;
`);

View File

@@ -26,7 +26,7 @@ const t = makeT('TopBar');
export function createTopBarHome(appModel: AppModel) {
return [
cssFlexSpace(),
appModel.supportGristNudge.showButton(),
(appModel.isTeamSite && roles.canEditAccess(appModel.currentOrg?.access || null) ?
[
basicButton(