(core) Forms post-release fixes and improvements

Summary:
Fixes misc. bugs with forms, updates Grist URLs on static form pages to link
to the new forms marketing page, and adds a forms announcement popup that's
shown next to the Add New button within a document.

Test Plan: Browser tests.

Reviewers: jarek

Reviewed By: jarek

Differential Revision: https://phab.getgrist.com/D4185
This commit is contained in:
George Gevoian
2024-02-14 16:18:09 -05:00
parent b8f32d1784
commit cd339ce7cb
43 changed files with 957 additions and 302 deletions

View File

@@ -39,7 +39,7 @@ function showAddNewTip(home: HomeModel): void {
return;
}
home.app.behavioralPromptsManager.showTip(addNewButton, 'addNew', {
home.app.behavioralPromptsManager.showPopup(addNewButton, 'addNew', {
popupOptions: {
placement: 'right-start',
},

View File

@@ -349,7 +349,7 @@ export function columnFilterMenu(owner: IDisposableOwner, opts: IFilterMenuOptio
icon('PinTilted'),
cssPinButton.cls('-pinned', model.filterInfo.isPinned),
dom.on('click', () => filterInfo.pinned(!filterInfo.pinned())),
gristDoc.behavioralPromptsManager.attachTip('filterButtons', {
gristDoc.behavioralPromptsManager.attachPopup('filterButtons', {
popupOptions: {
attach: null,
placement: 'right',

View File

@@ -373,7 +373,7 @@ class CustomSectionConfigurationConfig extends Disposable{
switch (widgetUrl) {
// TODO: come up with a way to attach tips without hardcoding widget URLs.
case 'https://gristlabs.github.io/grist-widget/calendar/index.html': {
return this._gristDoc.behavioralPromptsManager.attachTip('calendarConfig', {
return this._gristDoc.behavioralPromptsManager.attachPopup('calendarConfig', {
popupOptions: {placement: 'left-start'},
});
}
@@ -600,7 +600,7 @@ export class CustomSectionConfig extends Disposable {
dom.attr('placeholder', t("Enter Custom URL")),
testId('url')
),
this._gristDoc.behavioralPromptsManager.attachTip('customURL', {
this._gristDoc.behavioralPromptsManager.attachPopup('customURL', {
popupOptions: {
placement: 'left-start',
},

View File

@@ -24,7 +24,7 @@ export function filterBar(
dom.forEach(viewSection.activeFilters, (filterInfo) => makeFilterField(filterInfo, popupControls)),
dom.maybe(viewSection.showNestedFilteringPopup, () => {
return dom('div',
gristDoc.behavioralPromptsManager.attachTip('nestedFiltering', {
gristDoc.behavioralPromptsManager.attachPopup('nestedFiltering', {
onDispose: () => viewSection.showNestedFilteringPopup.set(false),
}),
);

View File

@@ -108,7 +108,7 @@ function buildAddNewColumMenuSection(gridView: GridView, index?: number): DomEle
},
menuIcon(colType.icon as IconName),
colType.displayName === 'Reference'?
gridView.gristDoc.behavioralPromptsManager.attachTip('referenceColumns', {
gridView.gristDoc.behavioralPromptsManager.attachPopup('referenceColumns', {
popupOptions: {
attach: `.${menuCssClass}`,
placement: 'left-start',

View File

@@ -1,6 +1,7 @@
import * as commands from 'app/client/components/commands';
import {makeT} from 'app/client/lib/localization';
import {ShortcutKey, ShortcutKeyContent} from 'app/client/ui/ShortcutKey';
import {basicButtonLink} from 'app/client/ui2018/buttons';
import {icon} from 'app/client/ui2018/icons';
import {cssLink} from 'app/client/ui2018/links';
import {commonUrls, GristDeploymentType} from 'app/common/gristUrls';
@@ -28,6 +29,17 @@ const cssIcon = styled(icon, `
width: 18px;
`);
const cssNewsPopupLearnMoreButton = styled(basicButtonLink, `
color: white;
border: 1px solid white;
padding: 3px;
&:hover, &:focus, &:visited {
color: white;
border-color: white;
}
`);
export type Tooltip =
| 'dataSize'
| 'setTriggerFormula'
@@ -126,11 +138,14 @@ see or edit which parts of your document.')
};
export interface BehavioralPromptContent {
popupType: 'tip' | 'news';
title: () => string;
content: (...domArgs: DomElementArg[]) => DomContents;
showDeploymentTypes: GristDeploymentType[] | '*';
deploymentTypes: GristDeploymentType[] | 'all';
/** Defaults to `everyone`. */
audience?: 'signed-in-users' | 'anonymous-users' | 'everyone';
/** Defaults to `desktop`. */
showContext?: 'mobile' | 'desktop' | '*';
deviceType?: 'mobile' | 'desktop' | 'all';
/** Defaults to `false`. */
hideDontShowTips?: boolean;
/** Defaults to `false`. */
@@ -141,6 +156,7 @@ export interface BehavioralPromptContent {
export const GristBehavioralPrompts: Record<BehavioralPrompt, BehavioralPromptContent> = {
referenceColumns: {
popupType: 'tip',
title: () => t('Reference Columns'),
content: (...args: DomElementArg[]) => cssTooltipContent(
dom('div', t('Reference columns are the key to {{relational}} data in Grist.', {
@@ -152,9 +168,10 @@ export const GristBehavioralPrompts: Record<BehavioralPrompt, BehavioralPromptCo
),
...args,
),
showDeploymentTypes: ['saas', 'core', 'enterprise', 'electron'],
deploymentTypes: ['saas', 'core', 'enterprise', 'electron'],
},
referenceColumnsConfig: {
popupType: 'tip',
title: () => t('Reference Columns'),
content: (...args: DomElementArg[]) => cssTooltipContent(
dom('div', t('Select the table to link to.')),
@@ -167,9 +184,10 @@ record in that table, but you may select which column from that record to show.'
),
...args,
),
showDeploymentTypes: ['saas', 'core', 'enterprise', 'electron'],
deploymentTypes: ['saas', 'core', 'enterprise', 'electron'],
},
rawDataPage: {
popupType: 'tip',
title: () => t('Raw Data page'),
content: (...args: DomElementArg[]) => cssTooltipContent(
dom('div', t('The Raw Data page lists all data tables in your document, \
@@ -177,9 +195,10 @@ including summary tables and tables not included in page layouts.')),
dom('div', cssLink({href: commonUrls.helpRawData, target: '_blank'}, t('Learn more.'))),
...args,
),
showDeploymentTypes: ['saas', 'core', 'enterprise', 'electron'],
deploymentTypes: ['saas', 'core', 'enterprise', 'electron'],
},
accessRules: {
popupType: 'tip',
title: () => t('Access Rules'),
content: (...args: DomElementArg[]) => cssTooltipContent(
dom('div', t('Access rules give you the power to create nuanced rules \
@@ -187,9 +206,10 @@ to determine who can see or edit which parts of your document.')),
dom('div', cssLink({href: commonUrls.helpAccessRules, target: '_blank'}, t('Learn more.'))),
...args,
),
showDeploymentTypes: ['saas', 'core', 'enterprise', 'electron'],
deploymentTypes: ['saas', 'core', 'enterprise', 'electron'],
},
filterButtons: {
popupType: 'tip',
title: () => t('Pinning Filters'),
content: (...args: DomElementArg[]) => cssTooltipContent(
dom('div', t('Pinned filters are displayed as buttons above the widget.')),
@@ -197,27 +217,30 @@ to determine who can see or edit which parts of your document.')),
dom('div', cssLink({href: commonUrls.helpFilterButtons, target: '_blank'}, t('Learn more.'))),
...args,
),
showDeploymentTypes: ['saas', 'core', 'enterprise', 'electron'],
deploymentTypes: ['saas', 'core', 'enterprise', 'electron'],
},
nestedFiltering: {
popupType: 'tip',
title: () => t('Nested Filtering'),
content: (...args: DomElementArg[]) => cssTooltipContent(
dom('div', t('You can filter by more than one column.')),
dom('div', t('Only those rows will appear which match all of the filters.')),
...args,
),
showDeploymentTypes: ['saas', 'core', 'enterprise', 'electron'],
deploymentTypes: ['saas', 'core', 'enterprise', 'electron'],
},
pageWidgetPicker: {
popupType: 'tip',
title: () => t('Selecting Data'),
content: (...args: DomElementArg[]) => cssTooltipContent(
dom('div', t('Select the table containing the data to show.')),
dom('div', t('Use the 𝚺 icon to create summary (or pivot) tables, for totals or subtotals.')),
...args,
),
showDeploymentTypes: ['saas', 'core', 'enterprise', 'electron'],
deploymentTypes: ['saas', 'core', 'enterprise', 'electron'],
},
pageWidgetPickerSelectBy: {
popupType: 'tip',
title: () => t('Linking Widgets'),
content: (...args: DomElementArg[]) => cssTooltipContent(
dom('div', t('Link your new widget to an existing widget on this page.')),
@@ -225,9 +248,10 @@ to determine who can see or edit which parts of your document.')),
dom('div', cssLink({href: commonUrls.helpLinkingWidgets, target: '_blank'}, t('Learn more.'))),
...args,
),
showDeploymentTypes: ['saas', 'core', 'enterprise', 'electron'],
deploymentTypes: ['saas', 'core', 'enterprise', 'electron'],
},
editCardLayout: {
popupType: 'tip',
title: () => t('Editing Card Layout'),
content: (...args: DomElementArg[]) => cssTooltipContent(
dom('div', t('Rearrange the fields in your card by dragging and resizing cells.')),
@@ -236,17 +260,19 @@ to determine who can see or edit which parts of your document.')),
})),
...args,
),
showDeploymentTypes: ['saas', 'core', 'enterprise', 'electron'],
deploymentTypes: ['saas', 'core', 'enterprise', 'electron'],
},
addNew: {
popupType: 'tip',
title: () => t('Add New'),
content: (...args: DomElementArg[]) => cssTooltipContent(
dom('div', t('Click the Add New button to create new documents or workspaces, or import data.')),
...args,
),
showDeploymentTypes: ['saas', 'core', 'enterprise', 'electron'],
deploymentTypes: ['saas', 'core', 'enterprise', 'electron'],
},
rickRow: {
popupType: 'tip',
title: () => t('Anchor Links'),
content: (...args: DomElementArg[]) => cssTooltipContent(
dom('div',
@@ -258,13 +284,14 @@ to determine who can see or edit which parts of your document.')),
),
...args,
),
showDeploymentTypes: '*',
showContext: '*',
deploymentTypes: 'all',
deviceType: 'all',
hideDontShowTips: true,
forceShow: true,
markAsSeen: false,
},
customURL: {
popupType: 'tip',
title: () => t('Custom Widgets'),
content: (...args: DomElementArg[]) => cssTooltipContent(
dom('div',
@@ -275,9 +302,10 @@ to determine who can see or edit which parts of your document.')),
dom('div', cssLink({href: commonUrls.helpCustomWidgets, target: '_blank'}, t('Learn more.'))),
...args,
),
showDeploymentTypes: ['saas', 'core', 'enterprise', 'electron'],
deploymentTypes: ['saas', 'core', 'enterprise', 'electron'],
},
calendarConfig: {
popupType: 'tip',
title: () => t('Calendar'),
content: (...args: DomElementArg[]) => cssTooltipContent(
dom('div', t("To configure your calendar, select columns for start/end dates and event titles. \
@@ -287,6 +315,21 @@ data.")),
dom('div', cssLink({href: commonUrls.helpCalendarWidget, target: '_blank'}, t('Learn more.'))),
...args,
),
showDeploymentTypes: ['saas', 'core', 'enterprise', 'electron'],
deploymentTypes: ['saas', 'core', 'enterprise', 'electron'],
},
formsAreHere: {
popupType: 'news',
audience: 'signed-in-users',
title: () => t('Forms are here!'),
content: (...args: DomElementArg[]) => cssTooltipContent(
dom('div', t('Build simple forms right in Grist and share in a click with our new widget. {{learnMoreButton}}', {
learnMoreButton: cssNewsPopupLearnMoreButton(t('Learn more'), {
href: commonUrls.forms,
target: '_blank',
}),
})),
...args,
),
deploymentTypes: ['saas', 'core', 'enterprise'],
},
};

View File

@@ -22,7 +22,7 @@
* the caller. Pass an `onFinishCB` to handle when a user dimiss the popups.
*/
import { Disposable, dom, DomElementArg, Holder, makeTestId, styled, svg } from "grainjs";
import { Disposable, dom, DomElementArg, Holder, makeTestId, Observable, styled, svg } from "grainjs";
import { createPopper, Placement } from '@popperjs/core';
import { FocusLayer } from 'app/client/lib/FocusLayer';
import {makeT} from 'app/client/lib/localization';
@@ -74,18 +74,34 @@ export interface IOnBoardingMsg {
urlState?: IGristUrlState;
}
let _isTourActiveObs: Observable<boolean>|undefined;
// Returns a singleton observable for whether some tour is currently active.
//
// GristDoc subscribes to this observable in order to temporarily disable tips and other
// in-product popups from being shown while a tour is active.
export function isTourActiveObs(): Observable<boolean> {
if (!_isTourActiveObs) {
const obs = Observable.create<boolean>(null, false);
_isTourActiveObs = obs;
}
return _isTourActiveObs;
}
// There should only be one tour at a time. Use a holder to dispose the previous tour when
// starting a new one.
const tourSingleton = Holder.create<OnBoardingPopupsCtl>(null);
export function startOnBoarding(messages: IOnBoardingMsg[], onFinishCB: (lastMessageIndex: number) => void) {
const ctl = OnBoardingPopupsCtl.create(tourSingleton, messages, onFinishCB);
ctl.onDispose(() => isTourActiveObs().set(false));
ctl.start().catch(reportError);
isTourActiveObs().set(true);
}
// Returns whether some tour is currently active.
export function isTourActive(): boolean {
return !tourSingleton.isEmpty();
return isTourActiveObs().get();
}
class OnBoardingError extends Error {

View File

@@ -337,7 +337,7 @@ export class PageWidgetSelect extends Disposable {
cssIcon('TypeTable'), 'New Table',
// prevent the selection of 'New Table' if it is disabled
dom.on('click', (ev) => !this._isNewTableDisabled.get() && this._selectTable('New Table')),
this._behavioralPromptsManager.attachTip('pageWidgetPicker', {
this._behavioralPromptsManager.attachPopup('pageWidgetPicker', {
popupOptions: {
attach: null,
placement: 'right-start',
@@ -395,7 +395,7 @@ export class PageWidgetSelect extends Disposable {
),
'selectBy',
{popupOptions: {attach: null}, domArgs: [
this._behavioralPromptsManager.attachTip('pageWidgetPickerSelectBy', {
this._behavioralPromptsManager.attachPopup('pageWidgetPickerSelectBy', {
popupOptions: {
attach: null,
placement: 'bottom',

View File

@@ -1013,16 +1013,7 @@ export class RightPanel extends Disposable {
return domAsync(imports.loadViewPane().then(() => buildConfigContainer(cssSection(
// Field config.
dom.maybeOwned(selectedField, (scope, field) => {
const requiredField = field.widgetOptionsJson.prop('formRequired');
// V2 thing.
// const hiddenField = field.widgetOptionsJson.prop('formHidden');
const defaultField = field.widgetOptionsJson.prop('formDefault');
const toComputed = (obs: typeof defaultField) => {
const result = Computed.create(scope, (use) => use(obs));
result.onWrite(val => obs.setAndSave(val));
return result;
};
dom.maybe(selectedField, (field) => {
const fieldTitle = field.widgetOptionsJson.prop('question');
return [
@@ -1063,21 +1054,10 @@ export class RightPanel extends Disposable {
// cssSection(
// builder.buildSelectWidgetDom(),
// ),
dom.maybe(use => ['Choice', 'ChoiceList', 'Ref', 'RefList'].includes(use(builder.field.pureType)), () => [
cssSection(
builder.buildConfigDom(),
),
]),
cssSection(
builder.buildFormConfigDom(),
),
]),
cssSeparator(),
cssLabel(t("Field rules")),
cssRow(labeledSquareCheckbox(
toComputed(requiredField),
t("Required field"),
testId('field-required'),
)),
// V2 thing
// cssRow(labeledSquareCheckbox(toComputed(hiddenField), t("Hidden field")),),
];
}),

View File

@@ -1,3 +1,4 @@
import {makeT} from 'app/client/lib/localization';
import {logTelemetryEvent} from 'app/client/lib/telemetry';
import {AppModel} from 'app/client/models/AppModel';
import {bigBasicButton, bigPrimaryButtonLink} from 'app/client/ui2018/buttons';
@@ -7,7 +8,6 @@ import {cardPopup, cssPopupBody, cssPopupButtons, cssPopupCloseButton,
import {icon} from 'app/client/ui2018/icons';
import {getGristConfig} from 'app/common/urlUtils';
import {dom, styled} from 'grainjs';
import { makeT } from '../lib/localization';
const t = makeT('WelcomeCoachingCall');
@@ -17,7 +17,7 @@ export function shouldShowWelcomeCoachingCall(appModel: AppModel) {
// Defer showing coaching call until Add New tip is dismissed.
const {behavioralPromptsManager, dismissedWelcomePopups} = appModel;
if (behavioralPromptsManager.shouldShowTip('addNew')) { return false; }
if (behavioralPromptsManager.shouldShowPopup('addNew')) { return false; }
const popup = dismissedWelcomePopups.get().find(p => p.id === 'coachingCall');
return (

View File

@@ -7,9 +7,9 @@ import {pagePanels} from 'app/client/ui/PagePanels';
import {setUpPage} from 'app/client/ui/setUpPage';
import {createTopBarHome} from 'app/client/ui/TopBar';
import {bigBasicButtonLink, bigPrimaryButtonLink} from 'app/client/ui2018/buttons';
import {colors, theme, vars} from 'app/client/ui2018/cssVars';
import {colors, mediaSmall, theme, vars} from 'app/client/ui2018/cssVars';
import {icon} from 'app/client/ui2018/icons';
import {getPageTitleSuffix} from 'app/common/gristUrls';
import {commonUrls, getPageTitleSuffix} from 'app/common/gristUrls';
import {getGristConfig} from 'app/common/urlUtils';
import {dom, DomElementArg, makeTestId, observable, styled} from 'grainjs';
@@ -128,14 +128,14 @@ export function createFormNotFoundPage(message?: string) {
cssFormErrorFooter(
cssFormPoweredByGrist(
cssFormPoweredByGristLink(
{href: 'https://www.getgrist.com', target: '_blank'},
{href: commonUrls.forms, target: '_blank'},
t('Powered by'),
cssGristLogo(),
)
),
cssFormBuildForm(
cssFormBuildFormLink(
{href: 'https://www.getgrist.com', target: '_blank'},
{href: commonUrls.forms, target: '_blank'},
t('Build your own form'),
icon('Expand'),
),
@@ -227,10 +227,17 @@ const cssButtonWrap = styled('div', `
`);
const cssFormErrorPage = styled('div', `
--grist-form-padding: 48px;
min-height: 100%;
background-color: ${colors.lightGrey};
height: 100%;
width: 100%;
padding-top: 52px;
padding: 52px 0px 52px 0px;
overflow: auto;
@media ${mediaSmall} {
& {
padding: 20px 0px 20px 0px;
}
}
`);
const cssFormErrorContainer = styled('div', `
@@ -243,6 +250,7 @@ const cssFormError = styled('div', `
text-align: center;
flex-direction: column;
align-items: center;
background-color: white;
border: 1px solid ${colors.darkGrey};
border-radius: 3px;
max-width: 600px;
@@ -254,8 +262,10 @@ const cssFormErrorBody = styled('div', `
`);
const cssFormErrorImage = styled('img', `
width: 250px;
height: 281px;
width: 100%;
height: 100%;
max-width: 250px;
max-height: 281px;
`);
const cssFormErrorText = styled('div', `
@@ -282,8 +292,6 @@ const cssFormPoweredByGrist = styled('div', `
align-items: center;
justify-content: center;
padding: 0px 10px;
margin-left: calc(-1 * var(--grist-form-padding));
margin-right: calc(-1 * var(--grist-form-padding));
`);
const cssFormPoweredByGristLink = styled('a', `