(core) Add new home page cards

Summary:
New cards on the home page link to useful resources like the welcome
video, tutorial, webinars, and the Help Center. They are shown by
default to new and exisiting users, and may be hidden via a toggle.

Test Plan: Browser tests.

Reviewers: jarek

Reviewed By: jarek

Differential Revision: https://phab.getgrist.com/D4340
This commit is contained in:
George Gevoian 2024-09-12 13:10:55 -04:00
parent 839bf63b9f
commit da6c39aa50
34 changed files with 1055 additions and 999 deletions

View File

@ -5,6 +5,7 @@ import {sanitizeHTML} from 'app/client/ui/sanitizeHTML';
import {dropdownWithSearch} from 'app/client/ui/searchDropdown';
import {isXSmallScreenObs} from 'app/client/ui2018/cssVars';
import {confirmModal} from 'app/client/ui2018/modals';
import {toggleSwitch} from 'app/client/ui2018/toggleSwitch';
import {CellValue} from 'app/plugin/GristData';
import {Disposable, dom, DomContents, makeTestId, MutableObsArray, obsArray, Observable} from 'grainjs';
import {marked} from 'marked';
@ -528,32 +529,21 @@ class BoolRenderer extends BaseFieldRenderer {
}
private _renderSwitchInput() {
return css.toggleSwitch(
dom('input',
dom.prop('checked', this.checked),
dom.prop('value', use => use(this.checked) ? '1' : '0'),
dom.on('change', (_e, elem) => this.checked.set(elem.checked)),
{
type: this.inputType,
name: this.name(),
required: this.field.options.formRequired,
},
return toggleSwitch(this. checked, {
label: this.field.question,
inputArgs: [
{name: this.name(), required: this.field.options.formRequired},
preventSubmitOnEnter(),
),
css.gristSwitch(
css.gristSwitchSlider(),
css.gristSwitchCircle(),
),
css.toggleLabel(
],
labelArgs: [
css.label.cls('-required', Boolean(this.field.options.formRequired)),
this.field.question,
),
);
],
});
}
private _renderCheckboxInput() {
return css.toggle(
dom('input',
css.checkboxInput(
dom.prop('checked', this.checked),
dom.prop('value', use => use(this.checked) ? '1' : '0'),
dom.on('change', (_e, elem) => this.checked.set(elem.checked)),
@ -613,7 +603,7 @@ class ChoiceListRenderer extends BaseFieldRenderer {
{name: this.name(), required},
dom.forEach(this.checkboxes, (checkbox) =>
css.checkbox(
dom('input',
css.checkboxInput(
dom.prop('checked', checkbox.checked),
dom.on('change', (_e, elem) => checkbox.checked.set(elem.value)),
{
@ -674,7 +664,7 @@ class RefListRenderer extends BaseFieldRenderer {
{name: this.name(), required},
dom.forEach(this.checkboxes, (checkbox) =>
css.checkbox(
dom('input',
css.checkboxInput(
dom.prop('checked', checkbox.checked),
dom.on('change', (_e, elem) => checkbox.checked.set(elem.value)),
{

View File

@ -102,73 +102,12 @@ export const submitButton = styled('div', `
}
`);
// TODO: break up into multiple variables, one for each field type.
export const field = styled('div', `
display: flex;
flex-direction: column;
height: 100%;
justify-content: space-between;
& input[type="checkbox"] {
-webkit-appearance: none;
-moz-appearance: none;
margin: 0;
padding: 0;
flex-shrink: 0;
display: inline-block;
width: 16px;
height: 16px;
--radius: 3px;
position: relative;
margin-right: 8px;
vertical-align: baseline;
}
& input[type="checkbox"]:focus {
outline-color: ${vars.primaryBgHover};
}
& input[type="checkbox"]:checked:enabled,
& input[type="checkbox"]:indeterminate:enabled {
--color: ${vars.primaryBg};
}
& input[type="checkbox"]:disabled {
--color: ${colors.darkGrey};
cursor: not-allowed;
}
& input[type="checkbox"]::before,
& input[type="checkbox"]::after {
content: '';
position: absolute;
top: 0;
left: 0;
height: 16px;
width: 16px;
box-sizing: border-box;
border: 1px solid var(--color, ${colors.darkGrey});
border-radius: var(--radius);
}
& input[type="checkbox"]:checked::before,
& input[type="checkbox"]:disabled::before,
& input[type="checkbox"]:indeterminate::before {
background-color: var(--color);
}
& input[type="checkbox"]:not(:checked):indeterminate::after {
-webkit-mask-image: var(--icon-Minus);
}
& input[type="checkbox"]:not(:disabled)::after {
background-color: ${colors.light};
}
& input[type="checkbox"]:checked::after,
& input[type="checkbox"]:indeterminate::after {
content: '';
position: absolute;
height: 16px;
width: 16px;
-webkit-mask-image: var(--icon-Tick);
-webkit-mask-size: contain;
-webkit-mask-position: center;
-webkit-mask-repeat: no-repeat;
background-color: ${colors.light};
}
& > .${label.className} {
color: ${colors.dark};
font-size: 13px;
@ -217,6 +156,63 @@ export const textarea = styled('textarea', `
resize: none;
`);
export const checkboxInput = styled('input', `
-webkit-appearance: none;
-moz-appearance: none;
margin: 0;
padding: 0;
flex-shrink: 0;
display: inline-block;
width: 16px;
height: 16px;
--radius: 3px;
position: relative;
margin-right: 8px;
vertical-align: baseline;
&:focus {
outline-color: ${vars.primaryBgHover};
}
&:checked:enabled, &:indeterminate:enabled {
--color: ${vars.primaryBg};
}
&:disabled {
--color: ${colors.darkGrey};
cursor: not-allowed;
}
&::before, &::after {
content: '';
position: absolute;
top: 0;
left: 0;
height: 16px;
width: 16px;
box-sizing: border-box;
border: 1px solid var(--color, ${colors.darkGrey});
border-radius: var(--radius);
}
&:checked::before, &:disabled::before, &:indeterminate::before {
background-color: var(--color);
}
&:not(:checked):indeterminate::after {
-webkit-mask-image: var(--icon-Minus);
}
&:not(:disabled)::after {
background-color: ${colors.light};
}
&:checked::after, &:indeterminate::after {
content: '';
position: absolute;
height: 16px;
width: 16px;
-webkit-mask-image: var(--icon-Tick);
-webkit-mask-size: contain;
-webkit-mask-position: center;
-webkit-mask-repeat: no-repeat;
background-color: ${colors.light};
}
`);
export const spinner = styled(numericSpinner, `
& input {
height: 29px;
@ -240,32 +236,6 @@ export const toggle = styled('label', `
}
`);
export const toggleSwitch = styled(toggle, `
cursor: pointer;
& input[type='checkbox'] {
margin: 0;
position: absolute;
top: 1px;
left: 4px;
}
& input[type='checkbox'],
& input[type='checkbox']::before,
& input[type='checkbox']::after {
height: 1px;
width: 1px;
}
& input[type='checkbox']:focus {
outline: none;
}
& input[type='checkbox']:focus {
outline: none;
}
& > span {
margin-left: 8px;
}
`);
export const toggleLabel = styled('span', `
font-size: 13px;
font-weight: 700;
@ -273,56 +243,6 @@ export const toggleLabel = styled('span', `
overflow-wrap: anywhere;
`);
export const gristSwitchSlider = styled('div', `
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
border-radius: 17px;
-webkit-transition: background-color .4s;
transition: background-color .4s;
`);
export const gristSwitchCircle = styled('div', `
position: absolute;
cursor: pointer;
content: "";
height: 13px;
width: 13px;
left: 2px;
bottom: 2px;
background-color: white;
border-radius: 17px;
-webkit-transition: transform .4s;
transition: transform .4s;
`);
export const gristSwitch = styled('div', `
position: relative;
width: 30px;
height: 17px;
display: inline-block;
flex: none;
input:focus + & > .${gristSwitchSlider.className} {
outline: 2px solid ${vars.primaryBgHover};
outline-offset: 1px;
}
input:checked + & > .${gristSwitchSlider.className} {
background-color: ${vars.primaryBg};
}
input:checked + & > .${gristSwitchCircle.className} {
-webkit-transform: translateX(13px);
-ms-transform: translateX(13px);
transform: translateX(13px);
}
`);
export const checkboxList = styled('div', `
display: inline-flex;
flex-direction: column;

View File

@ -9,7 +9,6 @@ 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} from 'app/client/ui/ProductUpgrades';
import {SupportGristNudge} from 'app/client/ui/SupportGristNudge';
import {gristThemePrefs} from 'app/client/ui2018/theme';
import {AsyncCreate} from 'app/common/AsyncCreate';
import {PlanSelection} from 'app/common/BillingAPI';
@ -130,8 +129,6 @@ export interface AppModel {
behavioralPromptsManager: BehavioralPromptsManager;
supportGristNudge: SupportGristNudge;
refreshOrgUsage(): Promise<void>;
showUpgradeModal(): Promise<void>;
showNewSiteModal(): Promise<void>;
@ -351,8 +348,6 @@ export class AppModelImpl extends Disposable implements AppModel {
public readonly behavioralPromptsManager: BehavioralPromptsManager =
BehavioralPromptsManager.create(this, this);
public readonly supportGristNudge: SupportGristNudge = SupportGristNudge.create(this, this);
constructor(
public readonly topAppModel: TopAppModel,
public readonly currentUser: ExtendedUser|null,

View File

@ -6,6 +6,7 @@ import {localStorageObs} from 'app/client/lib/localStorageObs';
import {AppModel, reportError} from 'app/client/models/AppModel';
import {reportMessage, UserError} from 'app/client/models/errors';
import {urlState} from 'app/client/models/gristUrlState';
import {getUserPrefObs} from 'app/client/models/UserPrefs';
import {ownerName} from 'app/client/models/WorkspaceInfo';
import {IHomePage, isFeatureEnabled} from 'app/common/gristUrls';
import {isLongerThan} from 'app/common/gutil';
@ -31,7 +32,7 @@ export interface HomeModel {
workspaces: Observable<Workspace[]>;
loading: Observable<boolean|"slow">; // Set to "slow" when loading for a while.
available: Observable<boolean>; // set if workspaces loaded correctly.
showIntro: Observable<boolean>; // set if no docs and we should show intro.
empty: Observable<boolean>; // set if no docs.
singleWorkspace: Observable<boolean>; // set if workspace name should be hidden.
trashWorkspaces: Observable<Workspace[]>; // only set when viewing trash
templateWorkspaces: Observable<Workspace[]>; // Only set when viewing templates or all documents.
@ -49,6 +50,8 @@ export interface HomeModel {
// the current org is a personal org, or the current org is view access only.
otherSites: Observable<Organization[]>;
onlyShowDocuments: Observable<boolean>;
currentSort: Observable<SortPref>;
currentView: Observable<ViewPref>;
importSources: Observable<ImportSourceElement[]>;
@ -123,21 +126,26 @@ export class HomeModelImpl extends Disposable implements HomeModel, ViewSettings
return orgs.filter(org => org.id !== currentOrg.id);
});
public readonly onlyShowDocuments = getUserPrefObs(this.app.userPrefsObs, 'onlyShowDocuments', {
defaultValue: false,
}) as Observable<boolean>;
public readonly currentSort: Observable<SortPref>;
public readonly currentView: Observable<ViewPref>;
// The workspace for new docs, or "unsaved" to only allow unsaved-doc creation, or null if the
// user isn't allowed to create a doc.
public readonly newDocWorkspace = Computed.create(this, this.currentPage, this.currentWS, (use, page, ws) => {
// Anonymous user can create docs, but in unsaved mode.
if (!this.app.currentValidUser) { return "unsaved"; }
if (!this.app.currentValidUser) {
// Anonymous user can create docs, but in unsaved mode and only when enabled.
return getGristConfig().enableAnonPlayground ? 'unsaved' : null;
}
if (page === 'trash') { return null; }
const destWS = (['all', 'templates'].includes(page)) ? (use(this.workspaces)[0] || null) : ws;
return destWS && roles.canEdit(destWS.access) ? destWS : null;
});
// Whether to show intro: no docs (other than examples).
public readonly showIntro = Computed.create(this, this.workspaces, (use, wss) => (
public readonly empty = Computed.create(this, this.workspaces, (use, wss) => (
wss.every((ws) => ws.isSupportWorkspace || ws.docs.length === 0)));
public readonly shouldShowAddNewTip = Observable.create(this,
@ -343,22 +351,16 @@ export class HomeModelImpl extends Disposable implements HomeModel, ViewSettings
}
/**
* Fetches templates if on the Templates or All Documents page.
*
* Only fetches featured (pinned) templates on the All Documents page.
* Fetches templates if on the Templates page.
*/
private async _maybeFetchTemplates(): Promise<Workspace[] | null> {
const {templateOrg} = getGristConfig();
if (!templateOrg) { return null; }
const currentPage = this.currentPage.get();
const shouldFetchTemplates = ['all', 'templates'].includes(currentPage);
if (!shouldFetchTemplates) { return null; }
if (!getGristConfig().templateOrg || this.currentPage.get() !== 'templates') {
return null;
}
let templateWss: Workspace[] = [];
try {
const onlyFeatured = currentPage === 'all';
templateWss = await this._app.api.getTemplates(onlyFeatured);
templateWss = await this._app.api.getTemplates();
} catch {
reportError('Failed to load templates');
}
@ -381,8 +383,7 @@ export class HomeModelImpl extends Disposable implements HomeModel, ViewSettings
if (
!isFeatureEnabled('tutorials') ||
!templateOrg ||
!onboardingTutorialDocId ||
this._app.dismissedPopups.get().includes('onboardingCards')
!onboardingTutorialDocId
) {
return;
}

View File

@ -14,8 +14,10 @@ function shouldShowAddNewTip(home: HomeModel): boolean {
home.app.isOwnerOrEditor() &&
// And the tip hasn't been shown before.
home.shouldShowAddNewTip.get() &&
// And the intro isn't being shown.
!home.showIntro.get() &&
// And the site isn't empty.
!home.empty.get() &&
// And home page cards aren't being shown.
!(home.currentPage.get() === 'all' && !home.onlyShowDocuments.get()) &&
// And the workspace loaded correctly.
home.available.get() &&
// And the current page isn't /p/trash; the Add New button is limited there.

View File

@ -1,4 +1,5 @@
import {autoFocus} from 'app/client/lib/domUtils';
import {makeT} from 'app/client/lib/localization';
import {ValidationGroup, Validator} from 'app/client/lib/Validator';
import {AppModel, getHomeUrl} from 'app/client/models/AppModel';
import {reportError, UserError} from 'app/client/models/errors';
@ -11,11 +12,7 @@ import {TEAM_PLAN} from 'app/common/Features';
import {checkSubdomainValidity} from 'app/common/orgNameUtils';
import {UserAPIImpl} from 'app/common/UserAPI';
import {PlanSelection} from 'app/common/BillingAPI';
import {
Disposable, dom, DomArg, DomContents, DomElementArg, IDisposableOwner, input, makeTestId,
Observable, styled
} from 'grainjs';
import { makeT } from '../lib/localization';
import {Disposable, dom, DomContents, DomElementArg, input, makeTestId, Observable, styled} from 'grainjs';
const t = makeT('CreateTeamModal');
const testId = makeTestId('test-create-team-');
@ -87,16 +84,12 @@ export function buildUpgradeModal(owner: Disposable, options: {
throw new UserError(t(`Billing is not supported in grist-core`));
}
export interface IUpgradeButton {
showUpgradeCard(...args: DomArg<HTMLElement>[]): DomContents;
showUpgradeButton(...args: DomArg<HTMLElement>[]): DomContents;
}
export class UpgradeButton extends Disposable {
constructor(_appModel: AppModel) {
super();
}
export function buildUpgradeButton(owner: IDisposableOwner, app: AppModel): IUpgradeButton {
return {
showUpgradeCard: () => null,
showUpgradeButton: () => null,
};
public buildDom() { return null; }
}
export function buildConfirm({

View File

@ -4,6 +4,7 @@
* Orgs, workspaces and docs are fetched asynchronously on build via the passed in API.
*/
import {loadUserManager} from 'app/client/lib/imports';
import {makeT} from 'app/client/lib/localization';
import {getTimeFromNow} from 'app/client/lib/timeUtils';
import {reportError} from 'app/client/models/AppModel';
import {docUrl, urlState} from 'app/client/models/gristUrlState';
@ -12,17 +13,18 @@ import {getWorkspaceInfo, workspaceName} from 'app/client/models/WorkspaceInfo';
import {attachAddNewTip} from 'app/client/ui/AddNewTip';
import * as css from 'app/client/ui/DocMenuCss';
import {buildHomeIntro, buildWorkspaceIntro} from 'app/client/ui/HomeIntro';
import {buildUpgradeButton} from 'app/client/ui/ProductUpgrades';
import {newDocMethods} from 'app/client/ui/NewDocMethods';
import {buildPinnedDoc, createPinnedDocs} from 'app/client/ui/PinnedDocs';
import {shadowScroll} from 'app/client/ui/shadowScroll';
import {makeShareDocUrl} from 'app/client/ui/ShareMenu';
import {buildTemplateDocs} from 'app/client/ui/TemplateDocs';
import {transition} from 'app/client/ui/transitions';
import {shouldShowWelcomeCoachingCall, showWelcomeCoachingCall} from 'app/client/ui/WelcomeCoachingCall';
import {createVideoTourTextButton} from 'app/client/ui/OpenVideoTour';
import {bigPrimaryButton} from 'app/client/ui2018/buttons';
import {buttonSelect, cssButtonSelect} from 'app/client/ui2018/buttonSelect';
import {isNarrowScreenObs, theme} from 'app/client/ui2018/cssVars';
import {buildOnboardingCards} from 'app/client/ui/OnboardingCards';
import {theme} 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 {menu, menuItem, menuText, select} from 'app/client/ui2018/menus';
import {confirmModal, saveModal} from 'app/client/ui2018/modals';
@ -30,12 +32,17 @@ import {IHomePage} from 'app/common/gristUrls';
import {SortPref, ViewPref} from 'app/common/Prefs';
import * as roles from 'app/common/roles';
import {Document, Workspace} from 'app/common/UserAPI';
import {computed, Computed, dom, DomArg, DomContents, DomElementArg, IDisposableOwner,
makeTestId, observable, Observable} from 'grainjs';
import {buildTemplateDocs} from 'app/client/ui/TemplateDocs';
import {makeT} from 'app/client/lib/localization';
import {localStorageBoolObs} from 'app/client/lib/localStorageObs';
import {bigBasicButton} from 'app/client/ui2018/buttons';
import {
computed,
Computed,
dom,
DomArg,
DomElementArg,
IDisposableOwner,
makeTestId,
observable,
Observable,
} from 'grainjs';
import sortBy = require('lodash/sortBy');
const t = makeT(`DocMenu`);
@ -70,10 +77,7 @@ function attachWelcomePopups(home: HomeModel): (el: Element) => void {
function createLoadedDocMenu(owner: IDisposableOwner, home: HomeModel) {
const flashDocId = observable<string|null>(null);
const upgradeButton = buildUpgradeButton(owner, home.app);
return css.docList( /* vbox */
/* first line */
dom.create(buildOnboardingCards, {homeModel: home}),
/* hbox */
css.docListContent(
/* left column - grow 1 */
@ -85,24 +89,16 @@ function createLoadedDocMenu(owner: IDisposableOwner, home: HomeModel) {
dom('span', t("(The organization needs a paid plan)")),
]),
// currentWS and showIntro observables change together. We capture both in one domComputed call.
dom.domComputed<[IHomePage, Workspace|undefined, boolean]>(
(use) => [use(home.currentPage), use(home.currentWS), use(home.showIntro)],
([page, workspace, showIntro]) => {
dom.domComputed<[IHomePage, Workspace|undefined]>(
(use) => [use(home.currentPage), use(home.currentWS)],
([page, workspace]) => {
const viewSettings: ViewSettings =
page === 'trash' ? makeLocalViewSettings(home, 'trash') :
page === 'templates' ? makeLocalViewSettings(home, 'templates') :
workspace ? makeLocalViewSettings(home, workspace.id) :
home;
return [
buildPrefs(
viewSettings,
// Hide the sort and view options when showing the intro.
{hideSort: showIntro, hideView: showIntro && page === 'all'},
['all', 'workspace'].includes(page)
? upgradeButton.showUpgradeButton(css.upgradeButton.cls(''))
: null,
),
page !== 'all' ? null : buildHomeIntro(home),
// Build the pinned docs dom. Builds nothing if the selectedOrg is unloaded.
// TODO: this is shown on all pages, but there is a hack in currentWSPinnedDocs that
@ -124,9 +120,8 @@ function createLoadedDocMenu(owner: IDisposableOwner, home: HomeModel) {
dom.maybe(home.available, () => [
buildOtherSites(home),
(showIntro && page === 'all' ?
null :
css.docListHeader(
page === 'all' && home.app.isPersonal && !home.app.currentValidUser ? null : css.docListHeaderWrap(
css.listHeader(
(
page === 'all' ? t("All Documents") :
page === 'templates' ?
@ -137,24 +132,20 @@ function createLoadedDocMenu(owner: IDisposableOwner, home: HomeModel) {
workspace && [css.docHeaderIcon('Folder'), workspaceName(home.app, workspace)]
),
testId('doc-header'),
)
),
buildPrefs(viewSettings),
),
(
(page === 'all') ?
dom('div',
showIntro ? buildHomeIntro(home) : null,
buildAllDocsBlock(home, home.workspaces, showIntro, flashDocId, viewSettings),
shouldShowTemplates(home, showIntro) ? buildAllDocsTemplates(home, viewSettings) : null,
) :
(page === 'trash') ?
page === 'all' ? buildAllDocumentsDocsBlock({home, flashDocId, viewSettings}) :
page === 'trash' ?
dom('div',
css.docBlock(t("Documents stay in Trash for 30 days, after which they get deleted permanently.")),
dom.maybe((use) => use(home.trashWorkspaces).length === 0, () =>
css.docBlock(t("Trash is empty."))
),
buildAllDocsBlock(home, home.trashWorkspaces, false, flashDocId, viewSettings),
buildAllDocsBlock(home, home.trashWorkspaces, flashDocId, viewSettings),
) :
(page === 'templates') ?
page === 'templates' ?
dom('div',
buildAllTemplates(home, home.templateWorkspaces, viewSettings)
) :
@ -172,30 +163,21 @@ function createLoadedDocMenu(owner: IDisposableOwner, home: HomeModel) {
}),
testId('doclist')
),
dom.maybe(use => !use(isNarrowScreenObs()) && ['all', 'workspace'].includes(use(home.currentPage)),
() => {
// 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.buildNudgeCard(),
];
}),
),
);
}
function buildAllDocsBlock(
home: HomeModel, workspaces: Observable<Workspace[]>,
showIntro: boolean, flashDocId: Observable<string|null>, viewSettings: ViewSettings,
home: HomeModel,
workspaces: Observable<Workspace[]>,
flashDocId: Observable<string|null>,
viewSettings: ViewSettings
) {
return dom.forEach(workspaces, (ws) => {
// Don't show the support workspace -- examples/templates are now retrieved from a special org.
// TODO: Remove once support workspaces are removed from the backend.
if (ws.isSupportWorkspace) { return null; }
// Show docs in regular workspaces. For empty orgs, we show the intro and skip
// the empty workspace headers. Workspaces are still listed in the left panel.
if (showIntro) { return null; }
return css.docBlock(
css.docBlockHeaderLink(
css.wsLeft(
@ -224,42 +206,50 @@ function buildAllDocsBlock(
});
}
/**
* Builds the collapsible examples and templates section at the bottom of
* the All Documents page.
*
* If there are no featured templates, builds nothing.
*/
function buildAllDocsTemplates(home: HomeModel, viewSettings: ViewSettings) {
return dom.domComputed(home.featuredTemplates, templates => {
if (templates.length === 0) { return null; }
function buildAllDocumentsDocsBlock(options: {
home: HomeModel,
flashDocId: Observable<string|null>,
viewSettings: ViewSettings
}) {
const {home, flashDocId, viewSettings} = options;
if (home.app.isPersonal && !home.app.currentValidUser) { return null; }
const hideTemplatesObs = localStorageBoolObs('hide-examples');
return css.allDocsTemplates(css.templatesDocBlock(
dom.autoDispose(hideTemplatesObs),
css.templatesHeaderWrap(
css.templatesHeader(
t("Examples & Templates"),
dom.domComputed(hideTemplatesObs, (collapsed) =>
collapsed ? css.templatesHeaderIcon('Expand') : css.templatesHeaderIcon('Collapse')
),
dom.on('click', () => hideTemplatesObs.set(!hideTemplatesObs.get())),
testId('all-docs-templates-header'),
),
createVideoTourTextButton(),
return dom('div',
dom.maybe(use => use(home.empty) && !home.app.isOwnerOrEditor(), () => css.docBlock(
css.introLine(
t("You have read-only access to this site. Currently there are no documents."),
dom('br'),
t("Any documents created in this site will appear here."),
testId('readonly-no-docs-message'),
),
dom.maybe((use) => !use(hideTemplatesObs), () => [
buildTemplateDocs(home, templates, viewSettings),
bigBasicButton(
t("Discover More Templates"),
urlState().setLinkUrl({homePage: 'templates'}),
testId('all-docs-templates-discover-more'),
)
]),
css.docBlock.cls((use) => '-' + use(home.currentView)),
testId('all-docs-templates'),
));
});
css.introLine(
t(
"Interested in using Grist outside of your team? Visit your free "
+ "{{personalSiteLink}}.",
{
personalSiteLink: dom.maybe(use =>
use(home.app.topAppModel.orgs).find(o => o.owner), org => cssLink(
urlState().setLinkUrl({org: org.domain ?? undefined}),
t("personal site"),
testId('readonly-personal-site-link')
)),
}
),
testId('readonly-personal-site-message'),
),
)),
dom.maybe(use => use(home.empty) && home.app.isOwnerOrEditor(), () => css.createFirstDocument(
css.createFirstDocumentImage({src: 'img/create-document.svg'}),
bigPrimaryButton(
t('Create my first document'),
dom.on('click', () => newDocMethods.createDocAndOpen(home)),
dom.boolAttr('disabled', use => !use(home.newDocWorkspace)),
),
)),
dom.maybe(use => !use(home.empty), () =>
buildAllDocsBlock(home, home.workspaces, flashDocId, viewSettings),
),
);
}
/**
@ -333,20 +323,11 @@ function buildOtherSites(home: HomeModel) {
/**
* Build the widget for selecting sort and view mode options.
*
* 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,
hideView: boolean,
},
...args: DomArg<HTMLElement>[]): DomContents {
function buildPrefs(viewSettings: ViewSettings, ...args: DomArg<HTMLElement>[]) {
return css.prefSelectors(
// The Sort selector.
options.hideSort ? null : dom.update(
dom.update(
select<SortPref>(viewSettings.currentSort, [
{value: 'name', label: t("By Name")},
{value: 'date', label: t("By Date Modified")},
@ -357,7 +338,7 @@ function buildPrefs(
),
// The View selector.
options.hideView ? null : buttonSelect<ViewPref>(viewSettings.currentView, [
buttonSelect<ViewPref>(viewSettings.currentView, [
{value: 'icons', icon: 'TypeTable'},
{value: 'list', icon: 'TypeCardList'},
],
@ -617,13 +598,3 @@ function scrollIntoViewIfNeeded(target: Element) {
target.scrollIntoView(true);
}
}
/**
* Returns true if templates should be shown in All Documents.
*/
function shouldShowTemplates(home: HomeModel, showIntro: boolean): boolean {
const org = home.app.currentOrg;
const isPersonalOrg = Boolean(org && org.owner);
// Show templates for all personal orgs, and for non-personal orgs when showing intro.
return isPersonalOrg || showIntro;
}

View File

@ -36,14 +36,24 @@ export const docList = styled('div', `
export const docListContent = styled('div', `
display: flex;
width: 100%;
max-width: 1340px;
margin: 0 auto;
`);
export const docMenu = styled('div', `
flex-grow: 1;
max-width: 100%;
width: 100%;
`);
const listHeader = styled('div', `
export const docListHeaderWrap = styled('div', `
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 16px;
margin-bottom: 24px;
`);
export const listHeader = styled('div', `
min-height: 32px;
line-height: 32px;
color: ${theme.text};
@ -358,3 +368,28 @@ export const upgradeButton = styled('div', `
export const upgradeCard = styled('div', `
margin-left: 64px;
`);
export const createFirstDocument = styled('div', `
margin: 32px 0px;
display: flex;
flex-direction: column;
row-gap: 16px;
align-items: center;
justify-content: center;
`);
export const createFirstDocumentImage = styled('img', `
display: flex;
align-items: center;
justify-content: center;
`);
export const paragraph = styled(docBlock, `
color: ${theme.text};
line-height: 1.6;
`);
export const introLine = styled(paragraph, `
font-size: ${vars.introFontSize};
margin-bottom: 8px;
`);

View File

@ -1,24 +1,22 @@
import {makeT} from 'app/client/lib/localization';
import {getLoginOrSignupUrl, getLoginUrl, getSignupUrl, urlState} from 'app/client/models/gristUrlState';
import {HomeModel} from 'app/client/models/HomeModel';
import {productPill} from 'app/client/ui/AppHeader';
import * as css from 'app/client/ui/DocMenuCss';
import {buildHomeIntroCards} from 'app/client/ui/HomeIntroCards';
import {newDocMethods} from 'app/client/ui/NewDocMethods';
import {manageTeamUsersApp} from 'app/client/ui/OpenUserManager';
import {bigBasicButton, cssButton} from 'app/client/ui2018/buttons';
import {bigBasicButton} from 'app/client/ui2018/buttons';
import {testId, theme, vars} from 'app/client/ui2018/cssVars';
import {icon} from 'app/client/ui2018/icons';
import {cssLink} from 'app/client/ui2018/links';
import {commonUrls, isFeatureEnabled} from 'app/common/gristUrls';
import {menu, menuCssClass} from 'app/client/ui2018/menus';
import {toggleSwitch} from 'app/client/ui2018/toggleSwitch';
import {FullUser} from 'app/common/LoginSessionAPI';
import * as roles from 'app/common/roles';
import {getGristConfig} from 'app/common/urlUtils';
import {Computed, dom, DomContents, styled} from 'grainjs';
import {dom, DomContents, styled} from 'grainjs';
import {defaultMenuOptions} from 'popweasel';
const t = makeT('HomeIntro');
export function buildHomeIntro(homeModel: HomeModel): DomContents {
const isViewer = homeModel.app.currentOrg?.access === roles.VIEWER;
const user = homeModel.app.currentValidUser;
const isAnonym = !user;
const isPersonal = !homeModel.app.isTeamSite;
@ -26,185 +24,104 @@ export function buildHomeIntro(homeModel: HomeModel): DomContents {
return makeAnonIntro(homeModel);
} else if (isPersonal) {
return makePersonalIntro(homeModel, user);
} else { // isTeamSite
if (isViewer) {
return makeViewerTeamSiteIntro(homeModel);
} else {
return makeTeamSiteIntro(homeModel);
}
} else {
return makeTeamSiteIntro(homeModel);
}
}
export function buildWorkspaceIntro(homeModel: HomeModel): DomContents {
const isViewer = homeModel.currentWS.get()?.access === roles.VIEWER;
const isAnonym = !homeModel.app.currentValidUser;
const emptyLine = cssIntroLine(testId('empty-workspace-info'), t("This workspace is empty."));
const emptyLine = css.introLine(testId('empty-workspace-info'), t("This workspace is empty."));
if (isAnonym || isViewer) {
return emptyLine;
} else {
return [
emptyLine,
buildButtons(homeModel, {
invite: false,
templates: false,
import: true,
empty: true
})
cssBtnGroup(
cssBtn(cssBtnIcon('Import'), t("Import Document"), testId('intro-import-doc'),
dom.on('click', () => newDocMethods.importDocAndOpen(homeModel)),
),
cssBtn(cssBtnIcon('Page'), t("Create Empty Document"), testId('intro-create-doc'),
dom.on('click', () => newDocMethods.createDocAndOpen(homeModel)),
),
),
];
}
}
function makeViewerTeamSiteIntro(homeModel: HomeModel) {
const personalOrg = Computed.create(null, use => use(homeModel.app.topAppModel.orgs).find(o => o.owner));
const docLink = (dom.maybe(personalOrg, org => {
return cssLink(
urlState().setLinkUrl({org: org.domain ?? undefined}),
t("personal site"),
testId('welcome-personal-url'));
}));
return [
css.docListHeader(
dom.autoDispose(personalOrg),
t("Welcome to {{- orgName}}", {orgName: homeModel.app.currentOrgName}),
productPill(homeModel.app.currentOrg, {large: true}),
testId('welcome-title')
),
cssIntroLine(
testId('welcome-info'),
t("You have read-only access to this site. Currently there are no documents."),
dom('br'),
t("Any documents created in this site will appear here."),
),
cssIntroLine(
t("Interested in using Grist outside of your team? Visit your free "), docLink, '.',
testId('welcome-text')
)
];
}
function makeTeamSiteIntro(homeModel: HomeModel) {
return [
css.docListHeader(
t("Welcome to {{- orgName}}", {orgName: homeModel.app.currentOrgName}),
productPill(homeModel.app.currentOrg, {large: true}),
testId('welcome-title')
css.docListHeaderWrap(
cssHeader(
t("Welcome to {{- orgName}}", {orgName: homeModel.app.currentOrgName}),
productPill(homeModel.app.currentOrg, {large: true}),
testId('welcome-title')
),
buildPreferencesMenu(homeModel),
),
cssIntroLine(t("Get started by inviting your team and creating your first Grist document.")),
(!isFeatureEnabled('helpCenter') ? null :
cssIntroLine(
t(
'Learn more in our {{helpCenterLink}}.',
{helpCenterLink: helpCenterLink()}
),
testId('welcome-text')
)
),
makeCreateButtons(homeModel)
dom.create(buildHomeIntroCards, {homeModel}),
];
}
function makePersonalIntro(homeModel: HomeModel, user: FullUser) {
return [
css.docListHeader(t("Welcome to Grist, {{- name}}!", {name: user.name}), testId('welcome-title')),
cssIntroLine(t("Get started by creating your first Grist document.")),
(!isFeatureEnabled('helpCenter') ? null :
cssIntroLine(t("Visit our {{link}} to learn more.", { link: helpCenterLink() }),
testId('welcome-text'))
css.docListHeaderWrap(
cssHeader(
t("Welcome to Grist, {{- name}}!", {name: user.name}),
testId('welcome-title'),
),
buildPreferencesMenu(homeModel),
),
makeCreateButtons(homeModel),
dom.create(buildHomeIntroCards, {homeModel}),
];
}
function makeAnonIntroWithoutPlayground(homeModel: HomeModel) {
return [
(!isFeatureEnabled('helpCenter') ? null : cssIntroLine(t("Visit our {{link}} to learn more about Grist.", {
link: helpCenterLink()
}), testId('welcome-text-no-playground'))),
cssIntroLine(t("To use Grist, please either sign up or sign in.")),
cssBtnGroup(
cssBtn(t("Sign up"), cssButton.cls('-primary'), testId('intro-sign-up'),
dom.on('click', () => location.href = getSignupUrl())
),
cssBtn(t("Sign in"), testId('intro-sign-in'),
dom.on('click', () => location.href = getLoginUrl())
)
)
];
}
function makeAnonIntro(homeModel: HomeModel) {
const welcomeToGrist = css.docListHeader(t("Welcome to Grist!"), testId('welcome-title'));
const welcomeToGrist = css.docListHeaderWrap(
cssHeader(
t("Welcome to Grist!"),
testId('welcome-title'),
),
);
if (!getGristConfig().enableAnonPlayground) {
return [
welcomeToGrist,
...makeAnonIntroWithoutPlayground(homeModel)
];
}
const signUp = cssLink({href: getLoginOrSignupUrl()}, t("Sign up"));
return [
return cssIntro(
welcomeToGrist,
cssIntroLine(t("Get started by exploring templates, or creating your first Grist document.")),
cssIntroLine(t("{{signUp}} to save your work. ", {signUp}),
(!isFeatureEnabled('helpCenter') ? null : t("Visit our {{link}} to learn more.", { link: helpCenterLink() })),
testId('welcome-text')),
makeCreateButtons(homeModel),
];
}
function helpCenterLink() {
return cssLink({href: commonUrls.help, target: '_blank'}, cssInlineIcon('Help'), t("Help Center"));
}
function buildButtons(homeModel: HomeModel, options: {
invite: boolean,
templates: boolean,
import: boolean,
empty: boolean,
}) {
return cssBtnGroup(
!options.invite ? null :
cssBtn(cssBtnIcon('Help'), t("Invite Team Members"), testId('intro-invite'),
cssButton.cls('-primary'),
dom.on('click', () => manageTeamUsersApp({app: homeModel.app})),
),
!options.templates ? null :
cssBtn(cssBtnIcon('FieldTable'), t("Browse Templates"), testId('intro-templates'),
cssButton.cls('-primary'),
dom.show(isFeatureEnabled("templates")),
urlState().setLinkUrl({homePage: 'templates'}),
),
!options.import ? null :
cssBtn(cssBtnIcon('Import'), t("Import Document"), testId('intro-import-doc'),
dom.on('click', () => newDocMethods.importDocAndOpen(homeModel)),
),
!options.empty ? null :
cssBtn(cssBtnIcon('Page'), t("Create Empty Document"), testId('intro-create-doc'),
dom.on('click', () => newDocMethods.createDocAndOpen(homeModel)),
),
dom.create(buildHomeIntroCards, {homeModel}),
);
}
function makeCreateButtons(homeModel: HomeModel) {
const canManageTeam = homeModel.app.isTeamSite &&
roles.canEditAccess(homeModel.app.currentOrg?.access || null);
return buildButtons(homeModel, {
invite: canManageTeam,
templates: !canManageTeam,
import: true,
empty: true
});
function buildPreferencesMenu(homeModel: HomeModel) {
const {onlyShowDocuments} = homeModel;
return cssDotsMenu(
cssDots(icon('Dots')),
menu(
() => [
toggleSwitch(onlyShowDocuments, {
label: t('Only show documents'),
args: [
testId('welcome-menu-only-show-documents'),
],
}),
],
{
...defaultMenuOptions,
menuCssClass: `${menuCssClass} ${cssPreferencesMenu.className}`,
placement: 'bottom-end',
}
),
testId('welcome-menu'),
);
}
const cssParagraph = styled(css.docBlock, `
color: ${theme.text};
line-height: 1.6;
const cssIntro = styled('div', `
margin-bottom: 24px;
`);
const cssIntroLine = styled(cssParagraph, `
font-size: ${vars.introFontSize};
margin-bottom: 8px;
const cssHeader = styled(css.listHeader, `
font-size: 24px;
line-height: 36px;
`);
const cssBtnGroup = styled('div', `
@ -225,6 +142,21 @@ const cssBtnIcon = styled(icon, `
margin-right: 8px;
`);
const cssInlineIcon = styled(icon, `
margin: -2px 4px 2px 4px;
const cssPreferencesMenu = styled('div', `
padding: 10px 16px;
`);
const cssDotsMenu = styled('div', `
display: flex;
cursor: pointer;
border-radius: ${vars.controlBorderRadius};
&:hover, &.weasel-popup-open {
background-color: ${theme.hover};
}
`);
const cssDots = styled('div', `
--icon-color: ${theme.lightText};
padding: 8px;
`);

View File

@ -0,0 +1,386 @@
import {makeT} from 'app/client/lib/localization';
import {urlState} from 'app/client/models/gristUrlState';
import {HomeModel} from 'app/client/models/HomeModel';
import {newDocMethods} from 'app/client/ui/NewDocMethods';
import {openVideoTour} from 'app/client/ui/OpenVideoTour';
import {basicButtonLink, bigPrimaryButton, primaryButtonLink} from 'app/client/ui2018/buttons';
import {colors, theme, vars} from 'app/client/ui2018/cssVars';
import {icon} from 'app/client/ui2018/icons';
import {commonUrls, isFeatureEnabled} from 'app/common/gristUrls';
import {getGristConfig} from 'app/common/urlUtils';
import {Computed, dom, IDisposableOwner, makeTestId, styled, subscribeElem} from 'grainjs';
interface BuildHomeIntroCardsOptions {
homeModel: HomeModel;
}
const t = makeT('HomeIntroCards');
const testId = makeTestId('test-intro-');
export function buildHomeIntroCards(
owner: IDisposableOwner,
{homeModel}: BuildHomeIntroCardsOptions
) {
const {onboardingTutorialDocId, templateOrg} = getGristConfig();
const percentComplete = Computed.create(owner, (use) => {
if (!homeModel.app.currentValidUser) { return 0; }
const tutorial = use(homeModel.onboardingTutorial);
if (!tutorial) { return undefined; }
return tutorial.forks?.[0]?.options?.tutorial?.percentComplete ?? 0;
});
let videoPlayButtonElement: HTMLElement;
return dom.maybe(use => !use(homeModel.onlyShowDocuments), () => cssHomeIntroCards(
cssVideoTour(
cssVideoTourThumbnail(
cssVideoTourThumbnailSpacer(),
videoPlayButtonElement = cssVideoTourPlayButton(
cssVideoTourPlayIcon('VideoPlay2'),
),
cssVideoTourThumbnailText(t('3 minute video tour')),
),
dom.on('click', () => openVideoTour(videoPlayButtonElement)),
testId('video-tour'),
),
cssTutorial(
dom.hide(() => !isFeatureEnabled('tutorials') || !templateOrg || !onboardingTutorialDocId),
cssTutorialHeader(t('Finish our basics tutorial')),
cssTutorialBody(
cssTutorialProgress(
cssTutorialProgressText(
cssTutorialProgressPercentage(
dom.domComputed(percentComplete, (percent) => percent !== undefined ? `${percent}%` : null),
testId('tutorial-percent-complete'),
),
),
cssTutorialProgressBar(
(elem) => subscribeElem(elem, percentComplete, (val) => {
elem.style.setProperty('--percent-complete', String(val ?? 0));
})
),
),
dom('div',
primaryButtonLink(
t('Tutorial'),
urlState().setLinkUrl({org: templateOrg!, doc: onboardingTutorialDocId}),
),
)
),
testId('tutorial'),
),
cssNewDocument(
cssNewDocumentHeader(t('Start a new document')),
cssNewDocumentBody(
cssNewDocumentButton(
cssNewDocumentButtonIcon('Page'),
t('Blank document'),
dom.on('click', () => newDocMethods.createDocAndOpen(homeModel)),
dom.boolAttr('disabled', use => !use(homeModel.newDocWorkspace)),
testId('create-doc'),
),
cssNewDocumentButton(
cssNewDocumentButtonIcon('Import'),
t('Import file'),
dom.on('click', () => newDocMethods.importDocAndOpen(homeModel)),
dom.boolAttr('disabled', use => !use(homeModel.newDocWorkspace)),
testId('import-doc'),
),
cssNewDocumentButton(
dom.show(isFeatureEnabled("templates") && Boolean(templateOrg)),
cssNewDocumentButtonIcon('FieldTable'),
t('Templates'),
urlState().setLinkUrl({homePage: 'templates'}),
testId('templates'),
),
),
),
cssWebinars(
dom.show(isFeatureEnabled('helpCenter')),
cssWebinarsImage({src: 'img/webinars.svg'}),
t('Learn more {{webinarsLinks}}', {
webinarsLinks: cssWebinarsButton(
t('Webinars'),
{href: commonUrls.webinars, target: '_blank'},
testId('webinars'),
),
}),
),
cssHelpCenter(
dom.show(isFeatureEnabled('helpCenter')),
cssHelpCenterImage({src: 'img/help-center.svg'}),
t('Find solutions and explore more resources {{helpCenterLink}}', {
helpCenterLink: cssHelpCenterButton(
t('Help center'),
{href: commonUrls.help, target: '_blank'},
testId('help-center'),
),
}),
),
testId('cards'),
));
}
// Cards are hidden at specific breakpoints; we use non-standard ones
// here, as they work better than the ones defined in `cssVars.ts`.
const mediaXLarge = `(max-width: ${1440 - 0.02}px)`;
const mediaLarge = `(max-width: ${1280 - 0.02}px)`;
const mediaMedium = `(max-width: ${1048 - 0.02}px)`;
const mediaSmall = `(max-width: ${828 - 0.02}px)`;
const cssHomeIntroCards = styled('div', `
display: grid;
gap: 24px;
margin-bottom: 24px;
display: grid;
grid-template-columns: 239px minmax(0, 437px) minmax(196px, 1fr) minmax(196px, 1fr);
grid-template-rows: repeat(2, 1fr);
@media ${mediaLarge} {
& {
grid-template-columns: 239px minmax(0, 437px) minmax(196px, 1fr);
}
}
@media ${mediaMedium} {
& {
grid-template-columns: 239px minmax(0, 437px);
}
}
@media ${mediaSmall} {
& {
display: flex;
flex-direction: column;
}
}
`);
const cssVideoTour = styled('div', `
grid-area: 1 / 1 / 2 / 2;
flex-shrink: 0;
width: 239px;
overflow: hidden;
cursor: pointer;
border-radius: 4px;
@media ${mediaSmall} {
& {
width: unset;
}
}
`);
const cssVideoTourThumbnail = styled('div', `
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 36px 32px;
background-image: url("img/youtube-screenshot.png");
background-color: rgba(0, 0, 0, 0.4);
background-blend-mode: multiply;
background-size: cover;
transform: scale(1.2);
width: 100%;
height: 100%;
`);
const cssVideoTourThumbnailSpacer = styled('div', ``);
const cssVideoTourPlayButton = styled('div', `
display: flex;
justify-content: center;
align-items: center;
align-self: center;
width: 32px;
height: 32px;
background-color: ${theme.controlPrimaryBg};
border-radius: 50%;
.${cssVideoTourThumbnail.className}:hover & {
background-color: ${theme.controlPrimaryHoverBg};
}
`);
const cssVideoTourPlayIcon = styled(icon, `
--icon-color: ${theme.controlPrimaryFg};
width: 24px;
height: 24px;
`);
const cssVideoTourThumbnailText = styled('div', `
color: ${colors.light};
font-weight: 700;
text-align: center;
`);
const cssTutorial = styled('div', `
grid-area: 1 / 2 / 2 / 3;
position: relative;
border-radius: 4px;
color: ${theme.announcementPopupFg};
background-color: ${theme.announcementPopupBg};
padding: 16px;
`);
const cssTutorialHeader = styled('div', `
display: flex;
align-items: flex-start;
justify-content: space-between;
font-size: 16px;
font-style: normal;
font-weight: 500;
margin-bottom: 8px;
`);
const cssTutorialBody = styled('div', `
display: flex;
flex-direction: column;
gap: 16px;
`);
const cssTutorialProgress = styled('div', `
display
flex: auto;
min-width: 120px;
`);
const cssTutorialProgressText = styled('div', `
display: flex;
justify-content: space-between;
`);
const cssTutorialProgressPercentage = styled('div', `
font-size: 18px;
font-style: normal;
font-weight: 700;
min-height: 21.5px;
`);
const cssTutorialProgressBar = styled('div', `
margin-top: 4px;
height: 10px;
border-radius: 8px;
background: ${theme.mainPanelBg};
--percent-complete: 0;
&::after {
content: '';
border-radius: 8px;
background: ${theme.progressBarFg};
display: block;
height: 100%;
width: calc((var(--percent-complete) / 100) * 100%);
}
`);
const cssNewDocument = styled('div', `
grid-area: 2 / 1 / 3 / 3;
grid-column: span 2;
display: flex;
flex-direction: column;
justify-content: space-between;
position: relative;
border-radius: 4px;
color: ${theme.announcementPopupFg};
background-color: ${theme.announcementPopupBg};
padding: 24px;
`);
const cssNewDocumentHeader = styled('div', `
font-weight: 500;
font-size: ${vars.xxlargeFontSize};
`);
const cssNewDocumentBody = styled('div', `
display: flex;
gap: 16px;
margin-top: 16px;
@media ${mediaSmall} {
& {
flex-direction: column;
}
}
`);
const cssNewDocumentButton = styled(bigPrimaryButton, `
display: flex;
align-items: center;
justify-content: center;
flex-grow: 1;
padding: 6px;
`);
const cssNewDocumentButtonIcon = styled(icon, `
flex-shrink: 0;
margin-right: 8px;
@media ${mediaXLarge} {
& {
display: none;
}
}
`);
const cssSecondaryCard = styled('div', `
font-weight: 500;
font-size: 14px;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
justify-content: center;
min-width: 196px;
color: ${theme.text};
background-color: ${theme.popupSecondaryBg};
position: relative;
border-radius: 4px;
padding: 16px;
`);
const cssSecondaryCardImage = styled('img', `
display: block;
height: auto;
`);
const cssSecondaryCardButton = styled(basicButtonLink, `
font-weight: 400;
font-size: ${vars.mediumFontSize};
margin-top: 8px;
`);
const cssWebinars = styled(cssSecondaryCard, `
grid-area: 2 / 3 / 3 / 4;
@media ${mediaMedium} {
& {
display: none;
}
}
`);
const cssWebinarsImage = styled(cssSecondaryCardImage, `
width: 105.78px;
margin-bottom: 8px;
`);
const cssWebinarsButton = cssSecondaryCardButton;
const cssHelpCenter = styled(cssSecondaryCard, `
grid-area: 2 / 4 / 3 / 5;
@media ${mediaLarge} {
& {
display: none;
}
}
`);
const cssHelpCenterImage = styled(cssSecondaryCardImage, `
width: 67.77px;
`);
const cssHelpCenterButton = cssSecondaryCardButton;

View File

@ -1,232 +0,0 @@
import {makeT} from 'app/client/lib/localization';
import {urlState} from 'app/client/models/gristUrlState';
import {HomeModel} from 'app/client/models/HomeModel';
import {openVideoTour} from 'app/client/ui/OpenVideoTour';
import {bigPrimaryButtonLink} from 'app/client/ui2018/buttons';
import {colors, theme} from 'app/client/ui2018/cssVars';
import {icon} from 'app/client/ui2018/icons';
import {isFeatureEnabled} from 'app/common/gristUrls';
import {getGristConfig} from 'app/common/urlUtils';
import {Computed, dom, IDisposableOwner, makeTestId, styled, subscribeElem} from 'grainjs';
interface BuildOnboardingCardsOptions {
homeModel: HomeModel;
}
const t = makeT('OnboardingCards');
const testId = makeTestId('test-onboarding-');
export function buildOnboardingCards(
owner: IDisposableOwner,
{homeModel}: BuildOnboardingCardsOptions
) {
const {templateOrg, onboardingTutorialDocId} = getGristConfig();
if (!isFeatureEnabled('tutorials') || !templateOrg || !onboardingTutorialDocId) { return null; }
const percentComplete = Computed.create(owner, (use) => {
if (!homeModel.app.currentValidUser) { return 0; }
const tutorial = use(homeModel.onboardingTutorial);
if (!tutorial) { return undefined; }
return tutorial.forks?.[0]?.options?.tutorial?.percentComplete ?? 0;
});
const shouldShowCards = Computed.create(owner, (use) =>
!use(homeModel.app.dismissedPopups).includes('onboardingCards'));
let videoPlayButtonElement: HTMLElement;
return dom.maybe(shouldShowCards, () =>
cssOnboardingCards(
cssTutorialCard(
cssDismissCardsButton(
icon('CrossBig'),
dom.on('click', () => homeModel.app.dismissPopup('onboardingCards', true)),
testId('dismiss-cards'),
),
cssTutorialCardHeader(
t('Complete our basics tutorial'),
),
cssTutorialCardSubHeader(
t('Learn the basics of reference columns, linked widgets, column types, & cards.')
),
cssTutorialCardBody(
cssTutorialProgress(
cssTutorialProgressText(
cssProgressPercentage(
dom.domComputed(percentComplete, (percent) => percent !== undefined ? `${percent}%` : null),
testId('tutorial-percent-complete'),
),
cssStarIcon('Star'),
),
cssTutorialProgressBar(
(elem) => subscribeElem(elem, percentComplete, (val) => {
elem.style.setProperty('--percent-complete', String(val ?? 0));
})
),
),
bigPrimaryButtonLink(
t('Complete the tutorial'),
urlState().setLinkUrl({org: templateOrg, doc: onboardingTutorialDocId}),
),
),
testId('tutorial-card'),
),
cssVideoCard(
cssVideoThumbnail(
cssVideoThumbnailSpacer(),
videoPlayButtonElement = cssVideoPlayButton(
cssPlayIcon('VideoPlay2'),
),
cssVideoThumbnailText(t('3 minute video tour')),
),
dom.on('click', () => openVideoTour(videoPlayButtonElement)),
),
)
);
}
const cssOnboardingCards = styled('div', `
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, max-content));
gap: 24px;
margin: 24px 0;
`);
const cssTutorialCard = styled('div', `
position: relative;
border-radius: 4px;
color: ${theme.announcementPopupFg};
background-color: ${theme.announcementPopupBg};
padding: 16px 24px;
`);
const cssTutorialCardHeader = styled('div', `
display: flex;
align-items: flex-start;
justify-content: space-between;
font-size: 18px;
font-style: normal;
font-weight: 700;
`);
const cssDismissCardsButton = styled('div', `
position: absolute;
top: 8px;
right: 8px;
padding: 4px;
border-radius: 4px;
cursor: pointer;
--icon-color: ${theme.popupCloseButtonFg};
&:hover {
background-color: ${theme.hover};
}
`);
const cssTutorialCardSubHeader = styled('div', `
font-size: 14px;
font-style: normal;
font-weight: 500;
margin: 8px 0;
`);
const cssTutorialCardBody = styled('div', `
display: flex;
flex-wrap: wrap;
gap: 24px;
margin: 16px 0;
align-items: end;
`);
const cssTutorialProgress = styled('div', `
flex: auto;
min-width: 120px;
`);
const cssTutorialProgressText = styled('div', `
display: flex;
justify-content: space-between;
`);
const cssProgressPercentage = styled('div', `
font-size: 20px;
font-style: normal;
font-weight: 700;
`);
const cssStarIcon = styled(icon, `
--icon-color: ${theme.accentIcon};
width: 24px;
height: 24px;
`);
const cssTutorialProgressBar = styled('div', `
margin-top: 4px;
height: 10px;
border-radius: 8px;
background: ${theme.mainPanelBg};
--percent-complete: 0;
&::after {
content: '';
border-radius: 8px;
background: ${theme.progressBarFg};
display: block;
height: 100%;
width: calc((var(--percent-complete) / 100) * 100%);
}
`);
const cssVideoCard = styled('div', `
width: 220px;
height: 158px;
overflow: hidden;
cursor: pointer;
border-radius: 4px;
`);
const cssVideoThumbnail = styled('div', `
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 36px 32px;
background-image: url("img/youtube-screenshot.png");
background-color: rgba(0, 0, 0, 0.4);
background-blend-mode: multiply;
background-size: cover;
transform: scale(1.2);
width: 100%;
height: 100%;
`);
const cssVideoThumbnailSpacer = styled('div', ``);
const cssVideoPlayButton = styled('div', `
display: flex;
justify-content: center;
align-items: center;
align-self: center;
width: 32px;
height: 32px;
background-color: ${theme.controlPrimaryBg};
border-radius: 50%;
.${cssVideoThumbnail.className}:hover & {
background-color: ${theme.controlPrimaryHoverBg};
}
`);
const cssPlayIcon = styled(icon, `
--icon-color: ${theme.controlPrimaryFg};
width: 24px;
height: 24px;
`);
const cssVideoThumbnailText = styled('div', `
color: ${colors.light};
font-weight: 700;
text-align: center;
`);

View File

@ -1,45 +1,32 @@
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 {basicButton, basicButtonLink, bigPrimaryButton} from 'app/client/ui2018/buttons';
import {colors, isNarrowScreenObs, testId, theme, vars} from 'app/client/ui2018/cssVars';
import {colors, testId, theme, vars} from 'app/client/ui2018/cssVars';
import {icon} from 'app/client/ui2018/icons';
import {cssLink} from 'app/client/ui2018/links';
import {modal} from 'app/client/ui2018/modals';
import {commonUrls, isFeatureEnabled} from 'app/common/gristUrls';
import {getGristConfig} from 'app/common/urlUtils';
import {Computed, Disposable, dom, DomContents, Observable, styled, UseCB} from 'grainjs';
import {Computed, Disposable, dom, DomContents, Observable, styled} from 'grainjs';
const t = makeT('SupportGristNudge');
type ButtonState =
| 'collapsed'
| 'expanded';
/**
* Nudges users to support Grist by opting in to telemetry or sponsoring on Github.
* Button that nudges users to support Grist by opting in to telemetry or sponsoring on Github.
*
* For installation admins, this includes a card with a nudge which collapses into a "Support
* For installation admins, this includes a modal with a nudge which collapses into a "Support
* Grist" button in the top bar. When that's not applicable, it is only a "Support Grist" button
* that links to the Github sponsorship page.
*
* Users can dismiss these nudges.
* Users can dismiss this button.
*/
export class SupportGristNudge extends Disposable {
export class SupportGristButton extends Disposable {
private readonly _showButton: Computed<null|'link'|'expand'>;
private readonly _telemetryModel: TelemetryModel = TelemetryModelImpl.create(this, this._appModel);
private readonly _buttonStateKey = `u=${this._appModel.currentValidUser?.id ?? 0};supportGristNudge`;
private _buttonState = localStorageObs(this._buttonStateKey, 'expanded') as Observable<ButtonState>;
// Whether the nudge just got accepted, and we should temporarily show the "Thanks" version.
private _justAccepted = Observable.create(this, false);
private _showButton: Computed<null|'link'|'expand'>;
private _showNudge: Computed<null|'normal'|'accepted'>;
constructor(private _appModel: AppModel) {
super();
const {deploymentType, telemetry} = getGristConfig();
@ -48,28 +35,16 @@ export class SupportGristNudge extends Disposable {
const isTelemetryOn = (telemetry && telemetry.telemetryLevel !== 'off');
const isAdminNudgeApplicable = isAdmin && !isTelemetryOn;
const generallyHide = (use: UseCB) => (
!isEnabled ||
use(_appModel.dismissedPopups).includes('supportGrist') ||
use(isNarrowScreenObs())
);
this._showButton = Computed.create(this, use => {
if (generallyHide(use)) { return null; }
if (!isAdminNudgeApplicable) { return 'link'; }
if (use(this._buttonState) !== 'expanded') { return 'expand'; }
return null;
});
if (!isEnabled || use(_appModel.dismissedPopups).includes('supportGrist')) {
return null;
}
this._showNudge = Computed.create(this, use => {
if (use(this._justAccepted)) { return 'accepted'; }
if (generallyHide(use)) { return null; }
if (isAdminNudgeApplicable && use(this._buttonState) === 'expanded') { return 'normal'; }
return null;
return isAdminNudgeApplicable ? 'expand' : 'link';
});
}
public buildTopBarButton(): DomContents {
public buildDom(): DomContents {
return dom.domComputed(this._showButton, (which) => {
if (!which) { return null; }
const elemType = (which === 'link') ? basicButtonLink : basicButton;
@ -77,7 +52,7 @@ export class SupportGristNudge extends Disposable {
elemType(cssHeartIcon('💛 '), t('Support Grist'),
(which === 'link' ?
{href: commonUrls.githubSponsorGristLabs, target: '_blank'} :
dom.on('click', () => this._buttonState.set('expanded'))
dom.on('click', () => this._buildNudgeModal())
),
cssContributeButtonCloseButton(
@ -85,7 +60,7 @@ export class SupportGristNudge extends Disposable {
dom.on('click', (ev) => {
ev.stopPropagation();
ev.preventDefault();
this._dismissAndClose();
this._markDismissed();
}),
testId('support-grist-button-dismiss'),
),
@ -95,26 +70,31 @@ export class SupportGristNudge extends Disposable {
});
}
public buildNudgeCard() {
return dom.domComputed(this._showNudge, nudge => {
if (!nudge) { return null; }
return cssCard(
(nudge === 'normal' ?
this._buildSupportGristCardContent() :
this._buildOptedInCardContent()
private _buildNudgeModal() {
return modal((ctl, owner) => {
const currentStep = Observable.create<'opt-in' | 'opted-in'>(owner, 'opt-in');
return [
cssModal.cls(''),
cssCloseButton(
icon('CrossBig'),
dom.on('click', () => ctl.close()),
testId('support-nudge-close'),
),
testId('support-nudge'),
);
});
dom.domComputed(currentStep, (step) => {
return step === 'opt-in'
? this._buildOptInScreen(async () => {
await this._optInToTelemetry();
currentStep.set('opted-in');
})
: this._buildOptedInScreen(() => ctl.close());
}),
];
}, {});
}
private _buildSupportGristCardContent() {
private _buildOptInScreen(onOptIn: () => Promise<void>) {
return [
cssCloseButton(
icon('CrossBig'),
dom.on('click', () => this._buttonState.set('collapsed')),
testId('support-nudge-close'),
),
cssLeftAlignedHeader(t('Support Grist')),
cssParagraph(t(
'Opt in to telemetry to help us understand how the product ' +
@ -132,19 +112,14 @@ export class SupportGristNudge extends Disposable {
),
cssFullWidthButton(
t('Opt in to Telemetry'),
dom.on('click', () => this._optInToTelemetry()),
dom.on('click', () => onOptIn()),
testId('support-nudge-opt-in'),
),
];
}
private _buildOptedInCardContent() {
private _buildOptedInScreen(onClose: () => void) {
return [
cssCloseButton(
icon('CrossBig'),
dom.on('click', () => this._justAccepted.set(false)),
testId('support-nudge-close'),
),
cssCenteredFlex(cssSparks()),
cssCenterAlignedHeader(t('Opted In')),
cssParagraph(
@ -157,7 +132,7 @@ export class SupportGristNudge extends Disposable {
cssCenteredFlex(
cssPrimaryButton(
t('Close'),
dom.on('click', () => this._justAccepted.set(false)),
dom.on('click', () => onClose()),
testId('support-nudge-close-button'),
),
),
@ -166,19 +141,11 @@ export class SupportGristNudge extends Disposable {
private _markDismissed() {
this._appModel.dismissPopup('supportGrist', true);
// Also cleanup the no-longer-needed button state from localStorage.
getStorage().removeItem(this._buttonStateKey);
}
private _dismissAndClose() {
this._markDismissed();
this._justAccepted.set(false);
}
private async _optInToTelemetry() {
await this._telemetryModel.updateTelemetryPrefs({telemetryLevel: 'limited'});
this._markDismissed();
this._justAccepted.set(true);
}
}
@ -202,10 +169,7 @@ const cssCenteredFlex = styled('div', `
align-items: center;
`);
const cssContributeButton = styled('div', `
margin-left: 8px;
margin-right: 8px;
`);
const cssContributeButton = styled('div', ``);
const cssContributeButtonCloseButton = styled(tokenFieldStyles.cssDeleteButton, `
margin-left: 4px;
@ -233,18 +197,6 @@ const cssContributeButtonCloseButton = styled(tokenFieldStyles.cssDeleteButton,
}
`);
const cssCard = styled('div', `
width: 297px;
padding: 24px;
color: ${theme.announcementPopupFg};
background: ${theme.announcementPopupBg};
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;
@ -303,3 +255,10 @@ const cssSparks = styled('div', `
const cssHeartIcon = styled('span', `
line-height: 1;
`);
const cssModal = styled('div', `
position: relative;
width: 100%;
max-width: 400px;
min-width: 0px;
`);

View File

@ -8,7 +8,9 @@ import {workspaceName} from 'app/client/models/WorkspaceInfo';
import {AccountWidget} from 'app/client/ui/AccountWidget';
import {buildNotifyMenuButton} from 'app/client/ui/NotifyUI';
import {manageTeamUsersApp} from 'app/client/ui/OpenUserManager';
import {UpgradeButton} from 'app/client/ui/ProductUpgrades';
import {buildShareMenuButton} from 'app/client/ui/ShareMenu';
import {SupportGristButton} from 'app/client/ui/SupportGristButton';
import {hoverTooltip} from 'app/client/ui/tooltips';
import {cssHoverCircle, cssTopBarBtn} from 'app/client/ui/TopBarCss';
import {buildLanguageMenu} from 'app/client/ui/LanguageMenu';
@ -28,20 +30,20 @@ export function createTopBarHome(appModel: AppModel, onSave?: (personal: boolean
return [
cssFlexSpace(),
(appModel.isTeamSite && roles.canEditAccess(appModel.currentOrg?.access || null) ?
[
basicButton(
t("Manage Team"),
dom.on('click', () => manageTeamUsersApp({app: appModel, onSave})),
testId('topbar-manage-team')
),
cssSpacer()
] :
null
cssButtons(
dom.create(UpgradeButton, appModel),
dom.create(SupportGristButton, appModel),
(appModel.isTeamSite && roles.canEditAccess(appModel.currentOrg?.access || null) ?
[
basicButton(
t("Manage team"),
dom.on('click', () => manageTeamUsersApp({app: appModel, onSave})),
testId('topbar-manage-team')
),
] :
null
),
),
appModel.supportGristNudge.buildTopBarButton(),
buildLanguageMenu(appModel),
isAnonymous ? null : buildNotifyMenuButton(appModel.notifier, appModel),
dom('div', dom.create(AccountWidget, appModel)),
@ -197,6 +199,12 @@ function topBarUndoBtn(iconName: IconName, ...domArgs: DomElementArg[]): Element
);
}
const cssButtons = styled('div', `
display: flex;
gap: 8px;
margin-right: 8px;
`);
const cssTopBarUndoBtn = styled(cssTopBarBtn, `
background-color: ${theme.topBarButtonSecondaryFg};

View File

@ -265,6 +265,7 @@ export const theme = {
/* Popups */
popupBg: new CustomProp('theme-popup-bg', undefined, 'white'),
popupSecondaryBg: new CustomProp('theme-popup-secondary-bg', undefined, colors.lightGrey),
popupInnerShadow: new CustomProp('theme-popup-shadow-inner', undefined,
'rgba(31, 37, 50, 0.31)'),
popupOuterShadow: new CustomProp('theme-popup-shadow-outer', undefined,

View File

@ -0,0 +1,115 @@
import {theme} from 'app/client/ui2018/cssVars';
import {dom, DomElementArg, Observable, styled} from 'grainjs';
interface ToggleSwitchOptions {
label?: string;
args?: DomElementArg[];
inputArgs?: DomElementArg[];
labelArgs?: DomElementArg[];
}
export function toggleSwitch(value: Observable<boolean>, options: ToggleSwitchOptions = {}) {
const {label, args = [], inputArgs = [], labelArgs = []} = options;
return cssToggleSwitch(
cssInput(
{type: 'checkbox'},
dom.prop('checked', value),
dom.prop('value', use => use(value) ? '1' : '0'),
dom.on('change', (_e, elem) => value.set(elem.checked)),
...inputArgs,
),
cssSwitch(
cssSwitchSlider(),
cssSwitchCircle(),
),
!label ? null : cssLabel(
label,
...labelArgs,
),
...args,
);
}
const cssToggleSwitch = styled('label', `
position: relative;
display: inline-flex;
margin-top: 8px;
cursor: pointer;
`);
const cssInput = styled('input', `
position: absolute;
height: 1px;
width: 1px;
top: 1px;
left: 4px;
margin: 0;
&::before, &::after {
height: 1px;
width: 1px;
}
&:focus {
outline: none;
}
`);
const cssSwitch = styled('div', `
position: relative;
width: 30px;
height: 17px;
display: inline-block;
flex: none;
`);
const cssSwitchSlider = styled('div', `
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: ${theme.switchSliderFg};
border-radius: 17px;
-webkit-transition: background-color .4s;
transition: background-color .4s;
.${cssInput.className}:focus + .${cssSwitch.className} > & {
outline: 2px solid ${theme.controlPrimaryHoverBg};
outline-offset: 1px;
}
.${cssInput.className}:checked + .${cssSwitch.className} > & {
background-color: ${theme.controlPrimaryBg};
}
`);
const cssSwitchCircle = styled('div', `
position: absolute;
cursor: pointer;
content: "";
height: 13px;
width: 13px;
left: 2px;
bottom: 2px;
background-color: ${theme.switchCircleFg};
border-radius: 17px;
-webkit-transition: transform .4s;
transition: transform .4s;
.${cssInput.className}:checked + .${cssSwitch.className} > & {
-webkit-transform: translateX(13px);
-ms-transform: translateX(13px);
transform: translateX(13px);
}
`);
const cssLabel = styled('span', `
color: ${theme.text};
margin-left: 8px;
font-size: 13px;
font-weight: 400;
line-height: 16px;
overflow-wrap: anywhere;
`);

View File

@ -33,6 +33,8 @@ export interface UserPrefs extends Prefs {
dismissedWelcomePopups?: DismissedReminder[];
// Localization support.
locale?: string;
// If only documents should be shown on the All Documents page.
onlyShowDocuments?: boolean;
}
// A collection of preferences related to a combination of user and org.
@ -112,9 +114,9 @@ export const DismissedPopup = StringUnion(
'supportGrist', // nudge to opt in to telemetry
'publishForm', // confirmation for publishing a form
'unpublishForm', // confirmation for unpublishing a form
'onboardingCards', // onboarding cards shown on the doc menu
/* Deprecated */
'onboardingCards', // onboarding cards shown on the doc menu
'tutorialFirstCard', // first card of the tutorial
);
export type DismissedPopup = typeof DismissedPopup.type;

View File

@ -90,6 +90,7 @@ export const ThemeColors = t.iface([], {
"modal-backdrop-close-button-fg": "string",
"modal-backdrop-close-button-hover-fg": "string",
"popup-bg": "string",
"popup-secondary-bg": "string",
"popup-shadow-inner": "string",
"popup-shadow-outer": "string",
"popup-close-button-fg": "string",

View File

@ -106,6 +106,7 @@ export interface ThemeColors {
/* Popups */
'popup-bg': string;
'popup-secondary-bg': string;
'popup-shadow-inner': string;
'popup-shadow-outer': string;
'popup-close-button-fg': string;

View File

@ -367,7 +367,7 @@ export interface UserAPI {
getOrg(orgId: number|string): Promise<Organization>;
getOrgWorkspaces(orgId: number|string, includeSupport?: boolean): Promise<Workspace[]>;
getOrgUsageSummary(orgId: number|string): Promise<OrgUsageSummary>;
getTemplates(onlyFeatured?: boolean): Promise<Workspace[]>;
getTemplates(): Promise<Workspace[]>;
getTemplate(docId: string): Promise<Document>;
getDoc(docId: string): Promise<Document>;
newOrg(props: Partial<OrganizationProperties>): Promise<number>;
@ -584,8 +584,8 @@ export class UserAPIImpl extends BaseAPI implements UserAPI {
return this.requestJson(`${this._url}/api/orgs/${orgId}/usage`, { method: 'GET' });
}
public async getTemplates(onlyFeatured: boolean = false): Promise<Workspace[]> {
return this.requestJson(`${this._url}/api/templates?onlyFeatured=${onlyFeatured ? 1 : 0}`, { method: 'GET' });
public async getTemplates(): Promise<Workspace[]> {
return this.requestJson(`${this._url}/api/templates`, { method: 'GET' });
}
public async getTemplate(docId: string): Promise<Document> {

View File

@ -96,6 +96,7 @@ export const commonUrls = {
plans: "https://www.getgrist.com/pricing",
contact: "https://www.getgrist.com/contact",
templates: 'https://www.getgrist.com/templates',
webinars: getWebinarsUrl(),
community: 'https://community.getgrist.com',
functions: 'https://support.getgrist.com/functions',
formulaSheet: 'https://support.getgrist.com/formula-cheat-sheet',
@ -703,6 +704,9 @@ export interface GristLoadConfig {
// Url for "contact support" button on Grist's "not found" error page
contactSupportUrl?: string;
// Url for webinars.
webinarsUrl?: string;
// When set, this directs the client to encode org information in path, not in domain.
pathOnly?: boolean;
@ -907,7 +911,7 @@ export function getKnownOrg(): string|null {
export function getHelpCenterUrl(): string {
const defaultUrl = "https://support.getgrist.com";
if(isClient()) {
if (isClient()) {
const gristConfig: GristLoadConfig = (window as any).gristConfig;
return gristConfig && gristConfig.helpCenterUrl || defaultUrl;
} else {
@ -916,7 +920,7 @@ export function getHelpCenterUrl(): string {
}
export function getTermsOfServiceUrl(): string|undefined {
if(isClient()) {
if (isClient()) {
const gristConfig: GristLoadConfig = (window as any).gristConfig;
return gristConfig && gristConfig.termsOfServiceUrl || undefined;
} else {
@ -926,7 +930,7 @@ export function getTermsOfServiceUrl(): string|undefined {
export function getFreeCoachingCallUrl(): string {
const defaultUrl = "https://calendly.com/grist-team/grist-free-coaching-call";
if(isClient()) {
if (isClient()) {
const gristConfig: GristLoadConfig = (window as any).gristConfig;
return gristConfig && gristConfig.freeCoachingCallUrl || defaultUrl;
} else {
@ -935,8 +939,8 @@ export function getFreeCoachingCallUrl(): string {
}
export function getContactSupportUrl(): string {
const defaultUrl = "https://www.getgrist.com/contact/";
if(isClient()) {
const defaultUrl = "https://www.getgrist.com/contact";
if (isClient()) {
const gristConfig: GristLoadConfig = (window as any).gristConfig;
return gristConfig && gristConfig.contactSupportUrl || defaultUrl;
} else {
@ -944,6 +948,16 @@ export function getContactSupportUrl(): string {
}
}
export function getWebinarsUrl(): string {
const defaultUrl = "https://www.getgrist.com/webinars";
if (isClient()) {
const gristConfig: GristLoadConfig = (window as any).gristConfig;
return gristConfig && gristConfig.webinarsUrl || defaultUrl;
} else {
return process.env.GRIST_WEBINARS_URL || defaultUrl;
}
}
/**
* Like getKnownOrg, but respects singleOrg/GRIST_SINGLE_ORG strictly.
* The main difference in behavior would be for orgs with custom domains

View File

@ -85,6 +85,7 @@ export const GristDark: ThemeColors = {
/* Popups */
'popup-bg': '#32323F',
'popup-secondary-bg': '#262633',
'popup-shadow-inner': '#000000',
'popup-shadow-outer': '#000000',
'popup-close-button-fg': '#A4A4B1',

View File

@ -85,6 +85,7 @@ export const GristLight: ThemeColors = {
/* Popups */
'popup-bg': 'white',
'popup-secondary-bg': '#F7F7F7',
'popup-shadow-inner': 'rgba(31, 37, 50, 0.31)',
'popup-shadow-outer': 'rgba(76, 86, 103, 0.24)',
'popup-close-button-fg': '#929299',

View File

@ -293,18 +293,14 @@ export class ApiServer {
}));
// GET /api/templates/
// Get all templates (or only featured templates if `onlyFeatured` is set).
// Get all templates.
this._app.get('/api/templates/', expressWrap(async (req, res) => {
const templateOrg = getTemplateOrg();
if (!templateOrg) {
throw new ApiError('Template org is not configured', 500);
throw new ApiError('Template org is not configured', 501);
}
const onlyFeatured = isParameterOn(req.query.onlyFeatured);
const query = await this._dbManager.getOrgWorkspaces(
{...getScope(req), showOnlyPinned: onlyFeatured},
templateOrg
);
const query = await this._dbManager.getOrgWorkspaces(getScope(req), templateOrg);
return sendReply(req, res, query);
}));

View File

@ -170,7 +170,6 @@ export interface Scope {
users?: AvailableUsers; // Set if available identities.
includeSupport?: boolean; // When set, include sample resources shared by support to scope.
showRemoved?: boolean; // When set, query is scoped to removed workspaces/docs.
showOnlyPinned?: boolean; // When set, query is scoped only to pinned docs.
showAll?: boolean; // When set, return both removed and regular resources.
specialPermit?: Permit; // When set, extra rights are granted on a specific resource.
}
@ -3399,8 +3398,6 @@ export class HomeDBManager extends EventEmitter {
const onDefault = 'docs.workspace_id = workspaces.id';
if (scope.showAll) {
return onDefault;
} else if (scope.showOnlyPinned) {
return `${onDefault} AND docs.is_pinned = TRUE AND (workspaces.removed_at IS NULL AND docs.removed_at IS NULL)`;
} else if (scope.showRemoved) {
return `${onDefault} AND (workspaces.removed_at IS NOT NULL OR docs.removed_at IS NOT NULL)`;
} else {

View File

@ -0,0 +1,32 @@
<svg width="150" height="140" viewBox="0 0 150 140" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.5883 140L15 139.989C15.2275 133.508 15.9255 127.052 17.0885 120.67C19.6326 107.007 23.863 98.034 29.6622 94L30 94.477C16.453 103.9 15.5952 139.64 15.5883 140Z" fill="#494949"/>
<path d="M22.6089 139L22 138.989C22.0131 138.346 22.3937 123.189 28.6503 119L29 119.475C22.9958 123.495 22.6119 138.845 22.6089 139Z" fill="#494949"/>
<path d="M33 94C34.6569 94 36 92.6568 36 91C36 89.3431 34.6569 88 33 88C31.3431 88 30 89.3431 30 91C30 92.6568 31.3431 94 33 94Z" fill="#16B378"/>
<path d="M31 119C32.6569 119 34 117.657 34 116C34 114.343 32.6569 113 31 113C29.3431 113 28 114.343 28 116C28 117.657 29.3431 119 31 119Z" fill="#16B378"/>
<path d="M20.8746 96.1993C21.4233 99.7892 19.9934 103 19.9934 103C19.9934 103 17.674 100.391 17.1254 96.8007C16.5767 93.2108 18.0066 90 18.0066 90C18.0066 90 20.326 92.6095 20.8746 96.1993Z" fill="#494949"/>
<path d="M30.6081 104.375C27.2943 105.576 24 104.676 24 104.676C24 104.676 26.0782 101.827 29.3919 100.625C32.7056 99.4235 36 100.324 36 100.324C36 100.324 33.9218 103.173 30.6081 104.375Z" fill="#494949"/>
<path d="M30.4054 126.629C28.9789 127.053 27.4598 127.115 26 126.809C26.9269 125.695 28.1752 124.857 29.5946 124.398C31.014 123.938 32.5439 123.875 34 124.218C33.0461 125.305 31.8065 126.137 30.4054 126.629Z" fill="#494949"/>
<path d="M107.999 136H103.934L102 121L108 121L107.999 136Z" fill="#FFF3DE"/>
<path d="M108 140L96 140V139.842C96.0001 138.558 96.4922 137.326 97.3681 136.418C98.244 135.51 99.432 135 100.671 135L108 135L108 140Z" fill="#494949"/>
<path d="M84.9994 136H80.9339L79 121L85 121L84.9994 136Z" fill="#FFF3DE"/>
<path d="M84.9998 140L73 140V139.842C73.0001 138.558 73.4922 137.326 74.3681 136.418C75.244 135.51 76.432 135 77.6707 135H77.671L85 135L84.9998 140Z" fill="#494949"/>
<path d="M74 53.8448L68.7607 53L67.068 55.8431L48.8528 60.6883L48.9021 60.9503C48.3869 60.389 47.7106 60.0182 46.9755 59.8938C46.2404 59.7694 45.4865 59.8983 44.8278 60.261C44.1691 60.6236 43.6416 61.2003 43.325 61.9037C43.0085 62.6072 42.9201 63.399 43.0733 64.1595C43.2265 64.9201 43.613 65.6078 44.1741 66.1187C44.7353 66.6295 45.4407 66.9357 46.1836 66.991C46.9264 67.0462 47.6663 66.8474 48.2912 66.4247C48.9161 66.0019 49.3921 65.3783 49.647 64.6481L71.7062 59.8473L74 53.8448Z" fill="#FFF3DE"/>
<path d="M127.561 50.0617C127.129 50.0621 126.7 50.1453 126.298 50.3069L126.403 50.1222L103.168 38L100 43.1689L124.157 53.9924C124.244 54.6529 124.519 55.274 124.947 55.7812C125.376 56.2885 125.94 56.6604 126.572 56.8526C127.204 57.0448 127.878 57.0491 128.513 56.865C129.147 56.6808 129.716 56.3161 130.15 55.8143C130.585 55.3126 130.867 54.6951 130.963 54.0357C131.06 53.3763 130.965 52.7029 130.692 52.096C130.419 51.4892 129.979 50.9746 129.423 50.6139C128.868 50.2531 128.222 50.0614 127.561 50.0617Z" fill="#FFF3DE"/>
<path d="M91.4035 18.0095C92.3294 12.2849 88.4401 6.88869 82.7165 5.95678C76.9929 5.02486 71.6024 8.91011 70.6765 14.6347C69.7506 20.3593 73.6399 25.7555 79.3635 26.6874C85.0872 27.6193 90.4776 23.734 91.4035 18.0095Z" fill="#494949"/>
<path d="M80.5 27C84.0899 27 87 24.0899 87 20.5C87 16.9101 84.0899 14 80.5 14C76.9102 14 74 16.9101 74 20.5C74 24.0899 76.9102 27 80.5 27Z" fill="#FFF3DE"/>
<path d="M80 19C83.866 19 87 16.9853 87 14.5C87 12.0147 83.866 10 80 10C76.134 10 73 12.0147 73 14.5C73 16.9853 76.134 19 80 19Z" fill="#494949"/>
<path d="M81.5 11C83.9853 11 86 8.98528 86 6.5C86 4.01472 83.9853 2 81.5 2C79.0147 2 77 4.01472 77 6.5C77 8.98528 79.0147 11 81.5 11Z" fill="#494949"/>
<path d="M75.9391 4.49998C75.9392 3.38767 76.3542 2.3148 77.1041 1.48831C77.854 0.66183 78.8856 0.140301 80 0.024332C79.8431 0.0081623 79.6855 4.01157e-05 79.5278 0C78.3265 0.00139078 77.1748 0.476108 76.3259 1.31987C75.4769 2.16363 75 3.30743 75 4.5C75 5.69257 75.4769 6.83636 76.3259 7.68013C77.1748 8.52389 78.3265 8.99861 79.5278 9C79.6855 8.99996 79.8431 8.99184 80 8.97567C78.8856 8.8597 77.854 8.33817 77.1041 7.51168C76.3542 6.68519 75.9392 5.6123 75.9391 4.49998Z" fill="#494949"/>
<path d="M76.6948 36.9326L77.2211 31.8285C77.2211 31.8285 84.3618 27.0813 86.7887 29.887L101.294 54.9226C101.294 54.9226 110.31 58.1564 109.992 70.5046L109.561 130.192L99.3364 131.323L93.1277 84.7871L87.5181 133L75.6148 132.624L76.6857 100.862L82.3616 70.0724L82.3073 59.8557L79.8075 55.7325C79.8075 55.7325 75.2283 53.8768 75.1003 48.6324L75 41.2606L76.6948 36.9326Z" fill="#494949"/>
<path d="M84 31.3536L84.14 29C84.14 29 105.607 34.648 103.904 38.6864C102.201 42.7247 99.0077 44 99.0077 44L86.6609 39.1115L84 31.3536Z" fill="#494949"/>
<path d="M78.1294 36.7761L76.5437 35C76.5437 35 63.8958 53.641 67.7205 55.7238C71.5452 57.8066 74.7569 56.7131 74.7569 56.7131L81 44.6897L78.1294 36.7761Z" fill="#494949"/>
<path d="M52.8642 97L135 80.5341L128.136 46L46 62.4659L52.8642 97Z" fill="white"/>
<path d="M135 81.4211L52.0609 98L45 62.5789L127.939 46L135 81.4211ZM52.9544 96.6589L133.661 80.5263L127.046 47.3411L46.3392 63.4737L52.9544 96.6589Z" fill="#D9D9D9"/>
<path d="M122.676 57.646L56 71L56.3903 72.9616L123.066 59.6076L122.676 57.646Z" fill="#D9D9D9"/>
<path d="M123.676 63.646L57 77L57.3903 78.9616L124.066 65.6076L123.676 63.646Z" fill="#D9D9D9"/>
<path d="M125.676 70.646L59 84L59.3903 85.9616L126.066 72.6076L125.676 70.646Z" fill="#D9D9D9"/>
<path d="M90.4449 67.5415L87.9873 67.988C87.8078 68.0204 87.6215 67.9867 87.4692 67.8942C87.3169 67.8018 87.2112 67.6581 87.1751 67.4948L86.0132 62.1976C85.9776 62.0342 86.0146 61.8646 86.1162 61.7261C86.2178 61.5875 86.3756 61.4913 86.5551 61.4585L89.0127 61.012C89.1922 60.9796 89.3786 61.0133 89.5308 61.1058C89.6831 61.1982 89.7888 61.3419 89.8249 61.5052L90.9868 66.8024C91.0224 66.9658 90.9854 67.1354 90.8838 67.2739C90.7822 67.4125 90.6244 67.5087 90.4449 67.5415Z" fill="#16B378"/>
<path d="M117.445 69.5415L114.987 69.988C114.808 70.0204 114.621 69.9867 114.469 69.8942C114.317 69.8018 114.211 69.6581 114.175 69.4948L113.013 64.1976C112.978 64.0342 113.015 63.8646 113.116 63.7261C113.218 63.5875 113.376 63.4913 113.555 63.4585L116.013 63.012C116.192 62.9796 116.379 63.0133 116.531 63.1058C116.683 63.1982 116.789 63.3419 116.825 63.5052L117.987 68.8024C118.022 68.9658 117.985 69.1354 117.884 69.2739C117.782 69.4125 117.624 69.5087 117.445 69.5415Z" fill="#16B378"/>
<path d="M102.445 79.5415L99.9873 79.988C99.8078 80.0204 99.6215 79.9867 99.4692 79.8942C99.3169 79.8018 99.2112 79.6581 99.1752 79.4948L98.0132 74.1976C97.9776 74.0342 98.0146 73.8646 98.1162 73.7261C98.2178 73.5875 98.3756 73.4913 98.5551 73.4585L101.013 73.012C101.192 72.9796 101.379 73.0133 101.531 73.1058C101.683 73.1982 101.789 73.3419 101.825 73.5052L102.987 78.8024C103.022 78.9658 102.985 79.1354 102.884 79.2739C102.782 79.4125 102.624 79.5087 102.445 79.5415Z" fill="#16B378"/>
<path d="M130.826 140H0.173971C0.127831 140 0.0835806 139.947 0.0509547 139.854C0.0183289 139.76 0 139.633 0 139.5C0 139.367 0.0183289 139.24 0.0509547 139.146C0.0835806 139.053 0.127831 139 0.173971 139H130.826C130.872 139 130.916 139.053 130.949 139.146C130.982 139.24 131 139.367 131 139.5C131 139.633 130.982 139.76 130.949 139.854C130.916 139.947 130.872 140 130.826 140Z" fill="#CACACA"/>
</svg>

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

@ -0,0 +1,10 @@
<svg width="68" height="40" viewBox="0 0 68 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M34.6532 0.5H64.3471C66.2978 0.5 67.8872 2.11518 67.8872 4.09745V24.2321C67.8872 26.2144 66.2978 27.8296 64.3471 27.8296H61.8364V34.8226L54.9548 27.8296H34.6532C32.7025 27.8296 31.113 26.2144 31.113 24.2321V4.09745C31.113 2.11518 32.7025 0.5 34.6532 0.5Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M60.6106 26.6038V31.8293L55.4683 26.6038H34.6532C33.3979 26.6038 32.3388 25.556 32.3388 24.2321V4.09745C32.3388 2.77355 33.3979 1.72581 34.6532 1.72581H64.3471C65.6023 1.72581 66.6614 2.77355 66.6614 4.09745V24.2321C66.6614 25.556 65.6023 26.6038 64.3471 26.6038H60.6106ZM61.8364 34.8226V27.8296H64.3471C66.2978 27.8296 67.8872 26.2144 67.8872 24.2321V4.09745C67.8872 2.11518 66.2978 0.5 64.3471 0.5H34.6532C32.7025 0.5 31.113 2.11518 31.113 4.09745V24.2321C31.113 26.2144 32.7025 27.8296 34.6532 27.8296H54.9548L61.8364 34.8226Z" fill="#D9D9D9"/>
<path d="M34.79 6.62935C34.79 5.27536 35.8877 4.17773 37.2417 4.17773H61.7578C63.1118 4.17773 64.2094 5.27536 64.2094 6.62935V21.339C64.2094 22.693 63.1118 23.7906 61.7578 23.7906H37.2417C35.8877 23.7906 34.79 22.693 34.79 21.339V6.62935Z" fill="#16B378" fill-opacity="0.15"/>
<path d="M50.2033 7.07422L49.9734 17.1453H47.7307L47.5079 7.07422H50.2033ZM48.8521 21.4914C48.4296 21.4914 48.0674 21.3428 47.7656 21.0456C47.4684 20.7485 47.3198 20.3863 47.3198 19.9591C47.3198 19.5412 47.4684 19.1837 47.7656 18.8865C48.0674 18.5894 48.4296 18.4408 48.8521 18.4408C49.2653 18.4408 49.6229 18.5894 49.9247 18.8865C50.2311 19.1837 50.3843 19.5412 50.3843 19.9591C50.3843 20.2424 50.3124 20.5001 50.1684 20.7322C50.0291 20.9644 49.8434 21.1501 49.6112 21.2894C49.3837 21.424 49.1307 21.4914 48.8521 21.4914Z" fill="#16B378"/>
<path d="M33.3468 5.17773H3.65294C1.70225 5.17773 0.112793 6.79291 0.112793 8.77518V28.9099C0.112793 30.8921 1.70225 32.5073 3.65294 32.5073H6.16356V39.5003L13.0452 32.5073H33.3468C35.2975 32.5073 36.887 30.8921 36.887 28.9099V8.77518C36.887 6.79291 35.2975 5.17773 33.3468 5.17773Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.38936 31.2815V36.507L12.5317 31.2815H33.3468C34.6021 31.2815 35.6612 30.2338 35.6612 28.9099V8.77518C35.6612 7.45128 34.6021 6.40354 33.3468 6.40354H3.65294C2.39772 6.40354 1.3386 7.45128 1.3386 8.77518V28.9099C1.3386 30.2338 2.39772 31.2815 3.65294 31.2815H7.38936ZM6.16356 39.5003V32.5073H3.65294C1.70225 32.5073 0.112793 30.8921 0.112793 28.9099V8.77518C0.112793 6.79291 1.70225 5.17773 3.65294 5.17773H33.3468C35.2975 5.17773 36.887 6.79291 36.887 8.77518V28.9099C36.887 30.8921 35.2975 32.5073 33.3468 32.5073H13.0452L6.16356 39.5003Z" fill="#D9D9D9"/>
<path d="M3.79004 11.3071C3.79004 9.95309 4.88766 8.85547 6.24165 8.85547H30.7578C32.1118 8.85547 33.2094 9.95309 33.2094 11.3071V26.0168C33.2094 27.3707 32.1118 28.4684 30.7578 28.4684H6.24165C4.88766 28.4684 3.79004 27.3707 3.79004 26.0168V11.3071Z" fill="#16B378" fill-opacity="0.15"/>
<path d="M17.3021 21.8228V21.6417C17.3067 20.8013 17.3856 20.1326 17.5389 19.6358C17.6967 19.139 17.9196 18.7397 18.2075 18.4379C18.4954 18.1314 18.8459 17.8505 19.2592 17.5951C19.5424 17.4187 19.7955 17.226 20.0183 17.0171C20.2459 16.8035 20.4246 16.5667 20.5546 16.3066C20.6846 16.042 20.7497 15.7471 20.7497 15.4221C20.7497 15.0553 20.6638 14.7372 20.492 14.4679C20.3202 14.1986 20.088 13.9897 19.7955 13.8411C19.5076 13.6925 19.1849 13.6182 18.8274 13.6182C18.4977 13.6182 18.1843 13.6902 17.8871 13.8341C17.5946 13.9734 17.3508 14.187 17.1558 14.4749C16.9654 14.7581 16.8586 15.118 16.8354 15.5544H14.356C14.3792 14.6722 14.5928 13.934 14.9967 13.3396C15.4053 12.7453 15.9439 12.2996 16.6126 12.0024C17.2858 11.7052 18.0287 11.5566 18.8413 11.5566C19.7281 11.5566 20.5082 11.7122 21.1815 12.0233C21.8594 12.3344 22.3864 12.7778 22.7625 13.3536C23.1432 13.9247 23.3336 14.6026 23.3336 15.3873C23.3336 15.9166 23.2477 16.3902 23.0759 16.8081C22.9087 17.226 22.6696 17.5975 22.3585 17.9225C22.0474 18.2475 21.6783 18.5377 21.2511 18.7931C20.875 19.0252 20.5662 19.2667 20.3248 19.5174C20.088 19.7682 19.9116 20.063 19.7955 20.4019C19.684 20.7363 19.626 21.1495 19.6214 21.6417V21.8228H17.3021ZM18.5139 26.1688C18.0961 26.1688 17.7362 26.0202 17.4344 25.7231C17.1326 25.4259 16.9817 25.0637 16.9817 24.6366C16.9817 24.2187 17.1326 23.8611 17.4344 23.564C17.7362 23.2668 18.0961 23.1182 18.5139 23.1182C18.9272 23.1182 19.2847 23.2668 19.5865 23.564C19.893 23.8611 20.0462 24.2187 20.0462 24.6366C20.0462 24.9198 19.9742 25.1775 19.8303 25.4096C19.691 25.6418 19.5053 25.8275 19.2731 25.9668C19.0456 26.1015 18.7925 26.1688 18.5139 26.1688Z" fill="#16B378"/>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

14
static/img/webinars.svg Normal file
View File

@ -0,0 +1,14 @@
<svg width="107" height="42" viewBox="0 0 107 42" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M45.1257 5.21875H2.64915C1.69143 5.21875 0.915039 5.99536 0.915039 6.95336V35.4722C0.915039 36.4302 1.69143 37.2068 2.64915 37.2068H45.1257C46.0834 37.2068 46.8598 36.4302 46.8598 35.4722V6.95336C46.8598 5.99536 46.0834 5.21875 45.1257 5.21875Z" fill="white"/>
<path d="M45.1223 5.77642C45.7722 5.77642 46.2995 6.30386 46.2995 6.95387V35.4727C46.2995 36.1227 45.7722 36.6502 45.1223 36.6502H2.64951C1.99968 36.6502 1.47239 36.1227 1.47239 35.4727V6.95387C1.47239 6.30386 1.99968 5.77642 2.64951 5.77642H45.1223ZM45.1223 4.66211H2.64951C1.38327 4.66211 0.358398 5.68727 0.358398 6.95387V35.4727C0.358398 36.7393 1.38327 37.7645 2.64951 37.7645H45.1223C46.3886 37.7645 47.4135 36.7393 47.4135 35.4727V6.95387C47.4135 5.68727 46.3886 4.66211 45.1223 4.66211Z" fill="#D9D9D9"/>
<path d="M41.2008 8.80078H6.93063C5.9442 8.80078 5.14453 9.60067 5.14453 10.5874V32.0564C5.14453 33.0431 5.9442 33.843 6.93063 33.843H41.2008C42.1872 33.843 42.9869 33.0431 42.9869 32.0564V10.5874C42.9869 9.60067 42.1872 8.80078 41.2008 8.80078Z" fill="#D6EEE5"/>
<path d="M29.0251 22.3705C30.0153 21.7985 30.0152 20.3688 29.0249 19.797L20.2727 14.7441C19.2825 14.1725 18.0449 14.8873 18.0449 16.0309V26.1392C18.0449 27.283 19.2828 27.9978 20.273 27.4258L29.0251 22.3705Z" fill="#16B378"/>
<path d="M103.854 5.21875H61.3772C60.4195 5.21875 59.6431 5.99536 59.6431 6.95336V35.4722C59.6431 36.4302 60.4195 37.2068 61.3772 37.2068H103.854C104.811 37.2068 105.588 36.4302 105.588 35.4722V6.95336C105.588 5.99536 104.811 5.21875 103.854 5.21875Z" fill="white"/>
<path d="M103.85 5.77642C104.5 5.77642 105.027 6.30386 105.027 6.95387V35.4727C105.027 36.1227 104.5 36.6502 103.85 36.6502H61.377C60.7272 36.6502 60.1999 36.1227 60.1999 35.4727V6.95387C60.1999 6.30386 60.7272 5.77642 61.377 5.77642H103.85ZM103.85 4.66211H61.377C60.1108 4.66211 59.0859 5.68727 59.0859 6.95387V35.4727C59.0859 36.7393 60.1108 37.7645 61.377 37.7645H103.85C105.116 37.7645 106.141 36.7393 106.141 35.4727V6.95387C106.141 5.68727 105.116 4.66211 103.85 4.66211Z" fill="#D9D9D9"/>
<path d="M99.9288 8.80078H65.6587C64.6722 8.80078 63.8726 9.60067 63.8726 10.5874V32.0564C63.8726 33.0431 64.6722 33.843 65.6587 33.843H99.9288C100.915 33.843 101.715 33.0431 101.715 32.0564V10.5874C101.715 9.60067 100.915 8.80078 99.9288 8.80078Z" fill="#D6EEE5"/>
<path d="M87.7527 22.3705C88.7429 21.7985 88.7427 20.3688 87.7524 19.797L79.0003 14.7441C78.0101 14.1725 76.7725 14.8873 76.7725 16.0309V26.1392C76.7725 27.283 78.0103 27.9978 79.0005 27.4258L87.7527 22.3705Z" fill="#16B378"/>
<path d="M80.4243 1.31445H28.1469C26.9682 1.31445 26.0127 2.27025 26.0127 3.44929V38.5484C26.0127 39.7275 26.9682 40.6833 28.1469 40.6833H80.4243C81.603 40.6833 82.5586 39.7275 82.5586 38.5484V3.44929C82.5586 2.27025 81.603 1.31445 80.4243 1.31445Z" fill="white"/>
<path d="M80.4197 2.00032C81.2195 2.00032 81.8684 2.64946 81.8684 3.44945V38.5486C81.8684 39.3486 81.2195 39.9977 80.4197 39.9977H28.1469C27.3471 39.9977 26.6982 39.3486 26.6982 38.5486V3.44945C26.6982 2.64946 27.3471 2.00032 28.1469 2.00032H80.4197ZM80.4197 0.628906H28.1469C26.5885 0.628906 25.3271 1.89061 25.3271 3.44945V38.5486C25.3271 40.1074 26.5885 41.3691 28.1469 41.3691H80.4197C81.9781 41.3691 83.2395 40.1074 83.2395 38.5486V3.44945C83.2395 1.89061 81.9781 0.628906 80.4197 0.628906Z" fill="#D9D9D9"/>
<path d="M75.5934 5.72266H33.416C32.2019 5.72266 31.2178 6.70711 31.2178 7.9215V34.3441C31.2178 35.5585 32.2019 36.543 33.416 36.543H75.5934C76.8075 36.543 77.7917 35.5585 77.7917 34.3441V7.9215C77.7917 6.70711 76.8075 5.72266 75.5934 5.72266Z" fill="#D6EEE5"/>
<path d="M60.608 22.4226C61.8267 21.7187 61.8265 19.9591 60.6076 19.2554L49.8361 13.0366C48.6174 12.3331 47.0942 13.2128 47.0942 14.6203V27.0609C47.0942 28.4686 48.6177 29.3484 49.8364 28.6445L60.608 22.4226Z" fill="#16B378"/>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -2301,15 +2301,6 @@ describe('ApiServer', function() {
assert.deepEqual(resp.data[0].docs.map((doc: any) => doc.name), ['Lightweight CRM']);
assert.deepEqual(resp.data[1].docs.map((doc: any) => doc.name), ['Expense Report', 'Timesheet']);
// Make a request to retrieve only the featured (pinned) templates.
const resp2 = await axios.get(`${homeUrl}/api/templates/?onlyFeatured=1`, nobody);
// Assert that the response includes only pinned documents.
assert.equal(resp2.status, 200);
assert.lengthOf(resp2.data, 2);
assert.deepEqual(resp.data.map((ws: any) => ws.name), ['CRM', 'Invoice']);
assert.deepEqual(resp2.data[0].docs.map((doc: any) => doc.name), ['Lightweight CRM']);
assert.deepEqual(resp2.data[1].docs, []);
// Add a new document to the CRM workspace, but don't share it with everyone.
await axios.post(`${homeUrl}/api/workspaces/${crmWsId}/docs`,
{name: 'Draft CRM Template', isPinned: true}, support);

View File

@ -148,15 +148,14 @@ describe('BehavioralPrompts', function() {
describe('for the Add New button', function() {
it('should not be shown if site is empty', async function() {
session = await gu.session().user('user4').login({showTips: true});
await gu.dismissCoachingCall();
await driver.navigate().refresh();
await gu.loadDocMenu('/');
await session.loadDocMenu('/');
await assertPromptTitle(null);
});
it('should be shown if site has documents', async function() {
await session.tempNewDoc(cleanup, 'BehavioralPromptsAddNew');
await session.loadDocMenu('/');
await driver.find('.test-bc-workspace').click();
await gu.waitForDocMenuToLoad();
await assertPromptTitle('Add New');
});
@ -166,16 +165,31 @@ describe('BehavioralPrompts', function() {
await assertPromptTitle(null);
});
it('should only be shown once each visit to the doc menu', async function() {
// Navigate to another page without reloading; the tip should now be shown.
await driver.find('.test-dm-all-docs').click();
await gu.waitForDocMenuToLoad();
it('should only be shown on the All Documents page if intro is hidden', async function() {
await session.loadDocMenu('/');
await assertPromptTitle(null);
await driver.find('.test-welcome-menu').click();
await driver.find('.test-welcome-menu-only-show-documents').click();
await gu.waitForServer();
await assertPromptTitle(null);
await gu.loadDocMenu('/');
await assertPromptTitle('Add New');
});
it('should only be shown once on each visit', async function() {
// Navigate to the home page for the first time; the tip should be shown.
await gu.loadDocMenu('/');
await assertPromptTitle('Add New');
// Navigate to another page; the tip should no longer be shown.
// Switch to a different page; the tip should no longer be shown.
await driver.findContent('.test-dm-workspace', /Home/).click();
await gu.waitForDocMenuToLoad();
await assertPromptTitle(null);
// Reload the page; the tip should be shown again.
await driver.navigate().refresh();
await gu.waitForDocMenuToLoad();
await assertPromptTitle('Add New');
});
});

View File

@ -49,16 +49,8 @@ describe('DocTutorial', function () {
it('shows a tutorial card', async function() {
await viewerSession.loadDocMenu('/');
assert.isTrue(await driver.find('.test-onboarding-tutorial-card').isDisplayed());
assert.equal(await driver.find('.test-onboarding-tutorial-percent-complete').getText(), '0%');
});
it('can dismiss tutorial card', async function() {
await driver.find('.test-onboarding-dismiss-cards').click();
assert.isFalse(await driver.find('.test-onboarding-tutorial-card').isPresent());
await driver.navigate().refresh();
await gu.waitForDocMenuToLoad();
assert.isFalse(await driver.find('.test-onboarding-tutorial-card').isPresent());
assert.isTrue(await driver.find('.test-intro-tutorial').isDisplayed());
assert.equal(await driver.find('.test-intro-tutorial-percent-complete').getText(), '0%');
});
it('shows a link to tutorial', async function() {
@ -84,9 +76,9 @@ describe('DocTutorial', function () {
afterEach(() => gu.checkForErrors());
it('shows a tutorial card', async function() {
assert.isTrue(await driver.find('.test-onboarding-tutorial-card').isDisplayed());
assert.isTrue(await driver.find('.test-intro-tutorial').isDisplayed());
await gu.waitToPass(async () =>
assert.equal(await driver.find('.test-onboarding-tutorial-percent-complete').getText(), '0%'),
assert.equal(await driver.find('.test-intro-tutorial-percent-complete').getText(), '0%'),
2000
);
});
@ -454,7 +446,7 @@ describe('DocTutorial', function () {
await gu.waitForServer();
await gu.waitForDocMenuToLoad();
await gu.waitToPass(async () =>
assert.equal(await driver.find('.test-onboarding-tutorial-percent-complete').getText(), '15%'),
assert.equal(await driver.find('.test-intro-tutorial-percent-complete').getText(), '15%'),
2000
);
await driver.find('.test-dm-basic-tutorial').click();
@ -567,12 +559,12 @@ describe('DocTutorial', function () {
await gu.waitForDocMenuToLoad();
assert.match(await driver.getCurrentUrl(), /o\/docs\/$/);
await gu.waitToPass(async () =>
assert.equal(await driver.find('.test-onboarding-tutorial-percent-complete').getText(), '0%'),
assert.equal(await driver.find('.test-intro-tutorial-percent-complete').getText(), '0%'),
2000
);
await ownerSession.loadDocMenu('/');
await gu.waitToPass(async () =>
assert.equal(await driver.find('.test-onboarding-tutorial-percent-complete').getText(), '100%'),
assert.equal(await driver.find('.test-intro-tutorial-percent-complete').getText(), '100%'),
2000
);
});

View File

@ -22,24 +22,12 @@ describe('HomeIntro', function() {
// Check message specific to anon
assert.equal(await driver.find('.test-welcome-title').getText(), 'Welcome to Grist!');
assert.match(await driver.find('.test-welcome-text').getText(), /Sign up.*Visit our Help Center/);
// Check the sign-up link.
const signUp = await driver.findContent('.test-welcome-text a', 'Sign up');
assert.include(await signUp.getAttribute('href'), '/signin');
// Check that the link takes us to a Grist login page.
await signUp.click();
await gu.checkLoginPage();
await driver.navigate().back();
await gu.waitForDocMenuToLoad();
});
it('should should intro screen for anon', () => testIntroScreen({team: false}));
it('should should intro screen for anon', () => testIntroScreen({anon: true, team: false}));
it('should not show Other Sites section', testOtherSitesSection);
it('should allow create/import from intro screen', testCreateImport.bind(null, false));
it('should allow collapsing examples and remember the state', testExamplesCollapsing);
it('should show examples workspace with the intro', testExamplesSection);
it('should link to examples page from the intro', testExamplesPage);
it('should render selected Examples workspace specially', testSelectedExamplesPage);
});
@ -61,15 +49,12 @@ describe('HomeIntro', function() {
// Check message specific to logged-in user
assert.match(await driver.find('.test-welcome-title').getText(), new RegExp(`Welcome.* ${session.name}`));
assert.match(await driver.find('.test-welcome-text').getText(), /Visit our Help Center/);
assert.notMatch(await driver.find('.test-welcome-text').getText(), /sign up/i);
});
it('should not show Other Sites section', testOtherSitesSection);
it('should show intro screen for empty org', () => testIntroScreen({team: false}));
it('should show intro screen for empty org', () => testIntroScreen({anon: false, team: false}));
it('should allow create/import from intro screen', testCreateImport.bind(null, true));
it('should allow collapsing examples and remember the state', testExamplesCollapsing);
it('should show examples workspace with the intro', testExamplesSection);
it('should link to examples page from the intro', testExamplesPage);
it('should allow copying examples', testCopyingExamples.bind(null, undefined));
it('should render selected Examples workspace specially', testSelectedExamplesPage);
it('should show empty workspace info', testEmptyWorkspace.bind(null, {buttons: true}));
@ -87,14 +72,12 @@ describe('HomeIntro', function() {
// Check message specific to logged-in user and an empty team site.
assert.match(await driver.find('.test-welcome-title').getText(), new RegExp(`Welcome.* ${session.orgName}`));
assert.match(await driver.find('.test-welcome-text').getText(), /Learn more/);
assert.notMatch(await driver.find('.test-welcome-text').getText(), /sign up/);
});
it('should not show Other Sites section', testOtherSitesSection);
it('should show intro screen for empty org', () => testIntroScreen({team: true}));
it('should show intro screen for empty org', () => testIntroScreen({anon: false, team: true}));
it('should allow create/import from intro screen', testCreateImport.bind(null, true));
it('should show examples workspace with the intro', testExamplesSection);
it('should link to examples page from the intro', testExamplesPage);
it('should allow copying examples', testCopyingExamples.bind(null, gu.session().teamSite.orgName));
it('should render selected Examples workspace specially', testSelectedExamplesPage);
it('should show empty workspace info', testEmptyWorkspace);
@ -105,22 +88,41 @@ describe('HomeIntro', function() {
assert.isFalse(await driver.find('.test-dm-other-sites-header').isPresent());
}
async function testIntroScreen(options: {team: boolean}) {
async function testIntroScreen(options: {anon: boolean; team: boolean}) {
// TODO There is no longer a thumbnail + video link on an empty site, but it's a good place to
// check for the presence and functionality of the planned links that open an intro video.
// Check link to Help Center
assert.include(await driver.findContent('.test-welcome-text a', /Help Center/).getAttribute('href'),
assert.isTrue(await driver.find('.test-intro-cards').isDisplayed());
assert.isTrue(await driver.find('.test-intro-video-tour').isDisplayed());
assert.isFalse(await driver.find('.test-intro-tutorial').isDisplayed());
assert.isTrue(await driver.find('.test-intro-create-doc').isDisplayed());
assert.isTrue(await driver.find('.test-intro-import-doc').isDisplayed());
assert.isTrue(await driver.find('.test-intro-templates').isDisplayed());
assert.include(await driver.find('.test-intro-webinars').getAttribute('href'),
'www.getgrist.com/webinars');
assert.include(await driver.find('.test-intro-help-center').getAttribute('href'),
'support.getgrist.com');
if (options.team) {
assert.equal(await driver.find('.test-intro-invite').getText(), 'Invite Team Members');
assert.equal(await driver.find('.test-topbar-manage-team').getText(), 'Manage Team');
assert.equal(await driver.find('.test-topbar-manage-team').getText(), 'Manage team');
} else {
assert.equal(await driver.find('.test-intro-invite').isPresent(), false);
assert.equal(await driver.find('.test-topbar-manage-team').isPresent(), false);
assert.equal(await driver.find('.test-intro-templates').getText(), 'Browse Templates');
assert.include(await driver.find('.test-intro-templates').getAttribute('href'), '/p/templates');
}
if (options.anon) {
assert.isFalse(await driver.find('.test-welcome-menu').isPresent());
} else {
await driver.find('.test-welcome-menu').click();
await driver.find('.test-welcome-menu-only-show-documents').click();
await gu.waitForServer();
assert.isFalse(await driver.find('.test-intro-cards').isPresent());
await driver.navigate().refresh();
await gu.waitForDocMenuToLoad();
assert.isFalse(await driver.find('.test-intro-cards').isPresent());
await driver.find('.test-welcome-menu').click();
await driver.find('.test-welcome-menu-only-show-documents').click();
await gu.waitForServer();
assert.isTrue(await driver.find('.test-intro-cards').isDisplayed());
}
}
@ -159,49 +161,16 @@ describe('HomeIntro', function() {
assert.isAbove(Number(await img.getAttribute('naturalWidth')), 0);
});
async function testExamplesCollapsing() {
assert.equal(await driver.find('.test-dm-pinned-doc-name').isDisplayed(), true);
// Collapse the templates section, check it's collapsed
await driver.find('.test-dm-all-docs-templates-header').click();
assert.equal(await driver.find('.test-dm-pinned-doc-name').isPresent(), false);
// Reload and check it's still collapsed.
await driver.navigate().refresh();
async function testExamplesPage() {
// Make sure we can get to the templates page from the home page.
await driver.find('.test-intro-templates').click();
await gu.waitForDocMenuToLoad();
assert.equal(await driver.find('.test-dm-pinned-doc-name').isPresent(), false);
// Expand back, and check.
await driver.find('.test-dm-all-docs-templates-header').click();
assert.equal(await driver.find('.test-dm-pinned-doc-name').isDisplayed(), true);
// Reload and check it's still expanded.
await driver.navigate().refresh();
await gu.waitForDocMenuToLoad();
assert.equal(await driver.find('.test-dm-pinned-doc-name').isDisplayed(), true);
}
async function testExamplesSection() {
// Check rendering and functionality of the examples and templates section
assert(gu.testCurrentUrl(/p\/templates/));
// Check titles.
assert.includeMembers(await driver.findAll('.test-dm-pinned-doc-name', (el) => el.getText()),
['Lightweight CRM']);
// Check the Discover More Templates button is shown.
const discoverMoreButton = await driver.find('.test-dm-all-docs-templates-discover-more');
assert(await discoverMoreButton.isPresent());
assert.include(await discoverMoreButton.getAttribute('href'), '/p/templates');
// Check that the button takes us to the templates page, then go back.
await discoverMoreButton.click();
await gu.waitForDocMenuToLoad();
assert(gu.testCurrentUrl(/p\/templates/));
await driver.navigate().back();
await gu.waitForDocMenuToLoad();
// Check images.
const docItem = await driver.findContent('.test-dm-pinned-doc', /Lightweight CRM/);
assert.equal(await docItem.find('img').isPresent(), true);
@ -223,8 +192,9 @@ describe('HomeIntro', function() {
}
async function testCopyingExamples(destination?: string) {
// Open the example to copy it. Note that we no longer support copying examples from doc menu.
// Make full copy of the example.
// Open the example and make a full copy of it.
await driver.find('.test-dm-templates-page').click();
await gu.waitForDocMenuToLoad();
await driver.findContent('.test-dm-pinned-doc-name', /Lightweight CRM/).click();
await gu.waitForDocToLoad();
await driver.findWait('.test-tb-share-action', 500).click();
@ -232,9 +202,11 @@ describe('HomeIntro', function() {
await checkDocAndRestore(true, async () => {
assert.match(await gu.getCell('Company', 1).getText(), /Sporer/);
assert.match(await driver.find('.test-bc-doc').value(), /LCRM Copy/);
}, 2);
}, 3);
// Make a template copy of the example.
await driver.find('.test-dm-templates-page').click();
await gu.waitForDocMenuToLoad();
await driver.findContent('.test-dm-pinned-doc-name', /Lightweight CRM/).click();
await gu.waitForDocToLoad();
await driver.findWait('.test-tb-share-action', 500).click();
@ -244,7 +216,7 @@ describe('HomeIntro', function() {
// No data, because the file was copied as a template.
assert.equal(await gu.getCell(0, 1).getText(), '');
assert.match(await driver.find('.test-bc-doc').value(), /LCRM Template Copy/);
}, 2);
}, 3);
}
async function testSelectedExamplesPage() {
@ -328,8 +300,6 @@ async function checkDocAndRestore(
await gu.waitForDocMenuToLoad();
// If not logged in, we create docs "unsaved" and don't see them in doc-menu.
if (isLoggedIn) {
// Freshly-created users will see a tip for the Add New button; dismiss it.
await gu.dismissBehavioralPrompts();
// Delete the first doc we find. We expect exactly one to exist.
await deleteFirstDoc();
}

View File

@ -3,50 +3,29 @@ import * as gu from 'test/nbrowser/gristUtils';
import {setupTestSuite} from 'test/nbrowser/testUtils';
describe('HomeIntroWithoutPlayground', function() {
this.timeout(40000);
this.timeout(20000);
setupTestSuite({samples: true});
gu.withEnvironmentSnapshot({'GRIST_ANON_PLAYGROUND': false});
describe("Anonymous on merged-org", function() {
it('should show welcome page with signin and signup buttons and "add new" button disabled', async function () {
// Sign out
it('should show welcome page', async function () {
const session = await gu.session().personalSite.anon.login();
// Open doc-menu
await session.loadDocMenu('/');
assert.equal(await driver.find('.test-welcome-title').getText(), 'Welcome to Grist!');
assert.match(
await driver.find('.test-welcome-text-no-playground').getText(),
/Visit our Help Center.*about Grist./
);
// Check the sign-up and sign-in buttons.
const getSignUp = async () => await driver.findContent('.test-intro-sign-up', 'Sign up');
const getSignIn = async () => await driver.findContent('.test-intro-sign-in', 'Sign in');
// Check that these buttons take us to a Grist login page.
for (const getButton of [getSignUp, getSignIn]) {
const button = await getButton();
await button.click();
await gu.checkLoginPage();
await driver.navigate().back();
await gu.waitForDocMenuToLoad();
}
});
it('should not allow creating new documents', async function () {
// Sign out
const session = await gu.session().personalSite.anon.login();
// Open doc-menu
await session.loadDocMenu('/');
// Check that add-new button is disabled
// Check that the Add New button is disabled.
assert.equal(await driver.find('.test-dm-add-new').matches('[class*=-disabled]'), true);
// Check that add-new menu is not displayed
await driver.find('.test-dm-add-new').doClick();
assert.equal(await driver.find('.test-dm-new-doc').isPresent(), false);
// Check that the intro buttons are also disabled.
assert.equal(await driver.find('.test-intro-create-doc').getAttribute('disabled'), 'true');
assert.equal(await driver.find('.test-intro-import-doc').getAttribute('disabled'), 'true');
});
});
});

View File

@ -14,7 +14,7 @@ async function openMainPage() {
await driver.get(`${server.getHost()}`);
while (true) { // eslint-disable-line no-constant-condition
try {
if (await driver.findContent('button', /Create Empty Document/).isPresent()) {
if (await driver.find('.test-intro-create-doc').isPresent()) {
return;
}
} catch (e) {
@ -31,7 +31,7 @@ describe("Smoke", function() {
it('can create, edit, and reopen a document', async function() {
this.timeout(20000);
await openMainPage();
await driver.findContent('button', /Create Empty Document/).click();
await driver.find('.test-intro-create-doc').click();
await gu.waitForDocToLoad(20000);
await gu.dismissWelcomeTourIfNeeded();
await gu.getCell('A', 1).click();

View File

@ -38,8 +38,7 @@ describe('SupportGrist', function() {
await session.loadDocMenu('/');
});
it('does not show a nudge on the doc menu', async function() {
await assertNudgeCardShown(false);
it('shows a button in the top bar', async function() {
await assertSupportButtonShown(true, {isSponsorLink: true});
});
@ -56,34 +55,15 @@ describe('SupportGrist', function() {
await session.loadDocMenu('/');
});
it('shows a nudge on the doc menu', async function() {
// Check that the nudge is expanded by default.
await assertSupportButtonShown(false);
await assertNudgeCardShown(true);
// Reload the doc menu and check that it's still expanded.
await session.loadDocMenu('/');
await assertSupportButtonShown(false);
await assertNudgeCardShown(true);
// Close the nudge and check that it's now collapsed.
await driver.find('.test-support-nudge-close').click();
it('shows a button in the top bar', async function() {
await assertSupportButtonShown(true, {isSponsorLink: false});
await assertNudgeCardShown(false);
// Reload again, and check that it's still collapsed.
await session.loadDocMenu('/');
await assertSupportButtonShown(true, {isSponsorLink: false});
await assertNudgeCardShown(false);
// Dismiss the contribute button and check that it's now gone, even after reloading.
// Dismiss the button and check that it's now gone, even after reloading.
await driver.find('.test-support-grist-button').mouseMove();
await driver.find('.test-support-grist-button-dismiss').click();
await assertSupportButtonShown(false);
await assertNudgeCardShown(false);
await session.loadDocMenu('/');
await assertSupportButtonShown(false);
await assertNudgeCardShown(false);
});
it('shows a link to Admin Panel and Support Grist in the user menu', async function() {
@ -92,35 +72,30 @@ describe('SupportGrist', function() {
await assertMenuHasSupportGrist(true);
});
it('supports opting in to telemetry from the nudge', async function() {
// Reset all dismissed popups, including the telemetry nudge.
it('supports opting in to telemetry', async function() {
// Reset all dismissed popups, which includes the telemetry button.
await driver.executeScript('resetDismissedPopups();');
await gu.waitForServer();
await session.loadDocMenu('/');
// Opt in to telemetry and reload the page.
await driver.find('.test-support-grist-button').click();
await driver.find('.test-support-nudge-opt-in').click();
await driver.findWait('.test-support-nudge-close-button', 1000).click();
await assertSupportButtonShown(false);
await assertNudgeCardShown(false);
await session.loadDocMenu('/');
// Check that the nudge is no longer shown and telemetry is set to "limited".
// Check that the button is no longer shown and telemetry is set to "limited".
await assertSupportButtonShown(false);
await assertNudgeCardShown(false);
await assertTelemetryLevel('limited');
});
it('does not show the nudge if telemetry is enabled', async function() {
// Reset all dismissed popups, including the telemetry nudge.
it('does not show the button if telemetry is enabled', async function() {
await driver.executeScript('resetDismissedPopups();');
await gu.waitForServer();
// Reload the doc menu and check that the nudge still isn't shown.
// Reload the doc menu and check that We show the "Support Grist" button linking to sponsorship page.
await session.loadDocMenu('/');
await assertNudgeCardShown(false);
// We still show the "Support Grist" button linking to sponsorship page.
await assertSupportButtonShown(true, {isSponsorLink: true});
// Disable telemetry from the Support Grist page.
@ -133,10 +108,9 @@ describe('SupportGrist', function() {
'.test-support-grist-page-telemetry-section button', /Opt out of Telemetry/, 2000).click();
await driver.findContentWait('.test-support-grist-page-telemetry-section button', /Opt in to Telemetry/, 2000);
// Reload the doc menu and check that the nudge is now shown.
// Reload the doc menu and check that the button is now shown.
await gu.loadDocMenu('/');
await assertSupportButtonShown(false);
await assertNudgeCardShown(true);
await assertSupportButtonShown(true, {isSponsorLink: false});
});
it('shows sponsorship link when no telemetry nudge, and allows dismissing it', async function() {
@ -156,16 +130,14 @@ describe('SupportGrist', function() {
// We still show the "Support Grist" button linking to sponsorship page.
await assertSupportButtonShown(true, {isSponsorLink: true});
await assertNudgeCardShown(false);
// we can dismiss it.
// We can dismiss it.
await driver.find('.test-support-grist-button').mouseMove();
await driver.find('.test-support-grist-button-dismiss').click();
await assertSupportButtonShown(false);
// And this will get remembered.
await session.loadDocMenu('/');
await assertNudgeCardShown(false);
await assertSupportButtonShown(false);
});
});
@ -185,9 +157,8 @@ describe('SupportGrist', function() {
oldEnv.restore();
});
it('does not show a nudge on the doc menu', async function() {
it('does not show a button in the top bar', async function() {
await assertSupportButtonShown(false);
await assertNudgeCardShown(false);
});
it('shows Admin Panel but not Support Grist in the user menu for admin', async function() {
@ -219,9 +190,8 @@ describe('SupportGrist', function() {
oldEnv.restore();
});
it('does not show a nudge on the doc menu', async function() {
it('does not show a button in the top bar', async function() {
await assertSupportButtonShown(false);
await assertNudgeCardShown(false);
});
it('shows Admin Panel but not Support Grist page in the user menu', async function() {
@ -242,11 +212,6 @@ async function assertSupportButtonShown(isShown: boolean, opts?: {isSponsorLink:
}
}
async function assertNudgeCardShown(isShown: boolean) {
const card = driver.find('.test-support-nudge');
assert.equal(await card.isPresent() && await card.isDisplayed(), isShown);
}
async function assertMenuHasAdminPanel(isShown: boolean) {
const elem = driver.find('.test-usermenu-admin-panel');
assert.equal(await elem.isPresent() && await elem.isDisplayed(), isShown);