2023-07-04 21:21:34 +00:00
|
|
|
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';
|
2023-09-21 16:57:58 +00:00
|
|
|
import {commonUrls, getPageTitleSuffix} from 'app/common/gristUrls';
|
2023-07-04 21:21:34 +00:00
|
|
|
import {TelemetryPrefsWithSources} from 'app/common/InstallAPI';
|
|
|
|
import {getGristConfig} from 'app/common/urlUtils';
|
2023-09-21 16:57:58 +00:00
|
|
|
import {Computed, Disposable, dom, makeTestId, Observable, styled, subscribe} from 'grainjs';
|
2023-07-04 21:21:34 +00:00
|
|
|
|
|
|
|
const testId = makeTestId('test-support-grist-page-');
|
|
|
|
|
|
|
|
const t = makeT('SupportGristPage');
|
|
|
|
|
|
|
|
export class SupportGristPage extends Disposable {
|
2023-09-21 16:57:58 +00:00
|
|
|
private readonly _currentPage = Computed.create(this, urlState().state, (_use, s) => s.supportGrist);
|
2023-07-04 21:21:34 +00:00
|
|
|
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();
|
2023-09-21 16:57:58 +00:00
|
|
|
this._setPageTitle();
|
2023-07-04 21:21:34 +00:00
|
|
|
this._model.fetchTelemetryPrefs().catch(reportError);
|
|
|
|
}
|
|
|
|
|
|
|
|
public buildDom() {
|
|
|
|
const panelOpen = Observable.create(this, false);
|
|
|
|
return pagePanels({
|
|
|
|
leftPanel: {
|
|
|
|
panelWidth: Observable.create(this, 240),
|
|
|
|
panelOpen,
|
|
|
|
hideOpener: true,
|
2023-07-26 22:31:02 +00:00
|
|
|
header: dom.create(AppHeader, this._appModel),
|
2023-07-04 21:21:34 +00:00
|
|
|
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'),
|
|
|
|
);
|
|
|
|
}
|
2023-09-21 16:57:58 +00:00
|
|
|
|
|
|
|
private _setPageTitle() {
|
|
|
|
this.autoDispose(subscribe(this._currentPage, (_use, page): string => {
|
|
|
|
const suffix = getPageTitleSuffix(getGristConfig());
|
|
|
|
switch (page) {
|
|
|
|
case undefined:
|
2023-11-29 20:13:29 +00:00
|
|
|
case 'support': {
|
2023-09-21 16:57:58 +00:00
|
|
|
return document.title = `Support Grist${suffix}`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}));
|
|
|
|
}
|
2023-07-04 21:21:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
`);
|