(core) Polish forms

Summary:
  - Updates styling of form submitted page.
  - Tweaks styling of checkboxes, labels, and questions on form page.
  - Adds new form 404 page.
  - Adds checkbox to not show warning again when publishing or un-publishing a form.
  - Excludes formula, hidden, and attachment columns in submitted form data.
  - Adds placeholder text to form configuration inputs.
  - Improves dark mode styling in Form widget.
  - Updates default title and description of new forms.
  - Updates styling of Form widget buttons.
  - Fixes form success text input handling.

Test Plan: Browser tests.

Reviewers: jarek

Reviewed By: jarek

Differential Revision: https://phab.getgrist.com/D4170
pull/833/head
George Gevoian 4 months ago
parent b77c762358
commit 6cb8614017

@ -1,5 +1,5 @@
import {BillingPage} from 'app/client/ui/BillingPage';
import {setupPage} from 'app/client/ui/setupPage';
import {setUpPage} from 'app/client/ui/setUpPage';
import {dom} from 'grainjs';
setupPage((appModel) => dom.create(BillingPage, appModel));
setUpPage((appModel) => dom.create(BillingPage, appModel));

@ -369,11 +369,8 @@ class RefListModel extends Question {
public renderInput() {
return dom('div',
dom.prop('name', this.model.colId),
dom.forEach(this.choices, (choice) => css.cssLabel(
dom('input',
dom.prop('name', this.model.colId),
{type: 'checkbox', value: String(choice[0]), style: 'margin-right: 5px;'}
),
dom.forEach(this.choices, (choice) => css.cssCheckboxLabel(
squareCheckbox(observable(false)),
String(choice[1] ?? '')
)),
dom.maybe(use => use(this.choices).length === 0, () => [

@ -361,7 +361,7 @@ export class FormView extends Disposable {
}
public buildDom() {
return style.cssFormView(
return style.cssFormView(
testId('editor'),
style.cssFormEditBody(
style.cssFormContainer(
@ -427,120 +427,145 @@ export class FormView extends Disposable {
}
}
private async _publish() {
confirmModal(t('Publish your form?'),
t('Publish'),
async () => {
const page = this.viewSection.view().page();
if (!page) {
throw new Error('Unable to publish form: undefined page');
}
let validShare = page.shareRef() !== 0;
// If page is shared, make sure home server is aware of it.
if (validShare) {
try {
const pageShare = page.share();
const serverShare = await this.gristDoc.docComm.getShare(pageShare.linkId());
validShare = !!serverShare;
} catch(ex) {
// TODO: for now ignore the error, but the UI should be updated to not show editor
if (ex.code === 'AUTH_NO_OWNER') {
return;
}
throw ex;
private async _handleClickPublish() {
if (this.gristDoc.appModel.dismissedPopups.get().includes('publishForm')) {
await this._publishForm();
} else {
confirmModal(t('Publish your form?'),
t('Publish'),
async (dontShowAgain) => {
await this._publishForm();
if (dontShowAgain) {
this.gristDoc.appModel.dismissedPopup('publishForm').set(true);
}
},
{
explanation: (
dom('div',
style.cssParagraph(
t(
'Publishing your form will generate a share link. Anyone with the link can ' +
'see the empty form and submit a response.'
),
),
style.cssParagraph(
t(
'Users are limited to submitting ' +
'entries (records in your table) and reading pre-set values in designated ' +
'fields, such as reference and choice columns.'
),
),
)
),
hideDontShowAgain: false,
},
);
}
}
private async _publishForm() {
const page = this.viewSection.view().page();
if (!page) {
throw new Error('Unable to publish form: undefined page');
}
let validShare = page.shareRef() !== 0;
// If page is shared, make sure home server is aware of it.
if (validShare) {
try {
const pageShare = page.share();
const serverShare = await this.gristDoc.docComm.getShare(pageShare.linkId());
validShare = !!serverShare;
} catch(ex) {
// TODO: for now ignore the error, but the UI should be updated to not show editor
if (ex.code === 'AUTH_NO_OWNER') {
return;
}
await this.gristDoc.docModel.docData.bundleActions('Publish form', async () => {
if (!validShare) {
const shareRef = await this.gristDoc.docModel.docData.sendAction([
'AddRecord',
'_grist_Shares',
null,
{
linkId: uuidv4(),
options: JSON.stringify({
publish: true,
}),
}
]);
await this.gristDoc.docModel.docData.sendAction(['UpdateRecord', '_grist_Pages', page.id(), {shareRef}]);
} else {
const share = page.share();
share.optionsObj.update({publish: true});
await share.optionsObj.save();
throw ex;
}
}
await this.gristDoc.docModel.docData.bundleActions('Publish form', async () => {
if (!validShare) {
const shareRef = await this.gristDoc.docModel.docData.sendAction([
'AddRecord',
'_grist_Shares',
null,
{
linkId: uuidv4(),
options: JSON.stringify({
publish: true,
}),
}
]);
await this.gristDoc.docModel.docData.sendAction(['UpdateRecord', '_grist_Pages', page.id(), {shareRef}]);
} else {
const share = page.share();
share.optionsObj.update({publish: true});
await share.optionsObj.save();
}
await this.save();
this.viewSection.shareOptionsObj.update({
form: true,
publish: true,
});
await this.viewSection.shareOptionsObj.save();
});
},
{
explanation: (
dom('div',
style.cssParagraph(
t(
'Publishing your form will generate a share link. Anyone with the link can ' +
'see the empty form and submit a response.'
),
),
style.cssParagraph(
t(
'Users are limited to submitting ' +
'entries (records in your table) and reading pre-set values in designated ' +
'fields, such as reference and choice columns.'
),
),
)
),
},
);
await this.save();
this.viewSection.shareOptionsObj.update({
form: true,
publish: true,
});
await this.viewSection.shareOptionsObj.save();
});
}
private async _unpublish() {
confirmModal(t('Unpublish your form?'),
t('Unpublish'),
async () => {
await this.gristDoc.docModel.docData.bundleActions('Unpublish form', async () => {
this.viewSection.shareOptionsObj.update({
publish: false,
});
await this.viewSection.shareOptionsObj.save();
const view = this.viewSection.view();
if (view.viewSections().peek().every(vs => !vs.shareOptionsObj.prop('publish')())) {
const share = this._pageShare.get();
if (!share) { return; }
share.optionsObj.update({
publish: false,
});
await share.optionsObj.save();
private async _handleClickUnpublish() {
if (this.gristDoc.appModel.dismissedPopups.get().includes('unpublishForm')) {
await this._unpublishForm();
} else {
confirmModal(t('Unpublish your form?'),
t('Unpublish'),
async (dontShowAgain) => {
await this._unpublishForm();
if (dontShowAgain) {
this.gristDoc.appModel.dismissedPopup('unpublishForm').set(true);
}
});
},
{
explanation: (
dom('div',
style.cssParagraph(
t(
'Unpublishing the form will disable the share link so that users accessing ' +
'your form via that link will see an error.'
},
{
explanation: (
dom('div',
style.cssParagraph(
t(
'Unpublishing the form will disable the share link so that users accessing ' +
'your form via that link will see an error.'
),
),
),
)
),
},
);
)
),
hideDontShowAgain: false,
},
);
}
}
private async _unpublishForm() {
await this.gristDoc.docModel.docData.bundleActions('Unpublish form', async () => {
this.viewSection.shareOptionsObj.update({
publish: false,
});
await this.viewSection.shareOptionsObj.save();
const view = this.viewSection.view();
if (view.viewSections().peek().every(vs => !vs.shareOptionsObj.prop('publish')())) {
const share = this._pageShare.get();
if (!share) { return; }
share.optionsObj.update({
publish: false,
});
await share.optionsObj.save();
}
});
}
private _buildPublisher() {
return style.cssSwitcher(
this._buildSwitcherMessage(),
style.cssButtonGroup(
style.cssIconButton(
style.cssSmallIconButton(
style.cssIconButton.cls('-frameless'),
icon('Revert'),
testId('reset'),
@ -608,14 +633,14 @@ export class FormView extends Disposable {
dom('div', 'Unpublish'),
dom.show(this.gristDoc.appModel.isOwner()),
style.cssIconButton.cls('-warning'),
dom.on('click', () => this._unpublish()),
dom.on('click', () => this._handleClickUnpublish()),
testId('unpublish'),
)
: style.cssIconButton(
dom('div', 'Publish'),
dom.show(this.gristDoc.appModel.isOwner()),
cssButton.cls('-primary'),
dom.on('click', () => this._publish()),
dom.on('click', () => this._handleClickPublish()),
testId('publish'),
);
}),
@ -685,6 +710,9 @@ export class FormView extends Disposable {
// If formula column, no.
if (c.isFormula() && c.formula()) { return false; }
// Attachments are currently unsupported in forms.
if (c.pureType() === 'Attachments') { return false; }
return true;
});
toAdd.sort((a, b) => a.parentPos() - b.parentPos());
@ -714,9 +742,8 @@ defaults(FormView.prototype, BaseView.prototype);
Object.assign(FormView.prototype, BackboneEvents);
// Default values when form is reset.
const FORM_TITLE = "## **My Super Form**";
const FORM_DESC = "This is the UI design work in progress on Grist Forms. We are working hard to " +
"give you the best possible experience with this feature";
const FORM_TITLE = "## **Form Title**";
const FORM_DESC = "Your form description goes here.";
const SECTION_TITLE = '### **Header**';
const SECTION_DESC = 'Description';

@ -1,7 +1,7 @@
import {textarea} from 'app/client/ui/inputs';
import {sanitizeHTML} from 'app/client/ui/sanitizeHTML';
import {basicButton, basicButtonLink} from 'app/client/ui2018/buttons';
import {colors, theme, vars} from 'app/client/ui2018/cssVars';
import {basicButton, bigBasicButton, bigBasicButtonLink} from 'app/client/ui2018/buttons';
import {colors, theme} from 'app/client/ui2018/cssVars';
import {icon} from 'app/client/ui2018/icons';
import {BindableValue, dom, DomElementArg, IDomArgs, Observable, styled, subscribeBindable} from 'grainjs';
import {marked} from 'marked';
@ -37,8 +37,6 @@ export const cssFormContainer = styled('div', `
gap: 8px;
`);
export const cssFieldEditor = styled('div.hover_border.field_editor', `
position: relative;
cursor: pointer;
@ -51,18 +49,18 @@ export const cssFieldEditor = styled('div.hover_border.field_editor', `
transition: transform 0.2s ease-in-out;
&:hover:not(:has(.hover_border:hover),&-cut) {
--hover-visible: visible;
outline: 1px solid ${colors.lightGreen};
outline: 1px solid ${theme.controlPrimaryBg};
}
&-selected:not(&-cut) {
background: #F7F7F7;
outline: 1px solid ${colors.lightGreen};
background: ${theme.lightHover};
outline: 1px solid ${theme.controlPrimaryBg};
--selected-block: block;
}
&:active:not(:has(&:active)) {
outline: 1px solid ${colors.darkGreen};
outline: 1px solid ${theme.controlPrimaryHoverBg};
}
&-drag-hover {
outline: 2px dashed ${colors.lightGreen};
outline: 2px dashed ${theme.controlPrimaryBg};
outline-offset: 2px;
}
&-cut {
@ -100,14 +98,6 @@ export const cssSection = styled('div', `
}
`);
export const cssLabel = styled('label', `
font-size: 15px;
font-weight: normal;
user-select: none;
display: block;
margin: 0px;
`);
export const cssCheckboxLabel = styled('label', `
font-size: 15px;
font-weight: normal;
@ -139,7 +129,7 @@ export const cssEditableLabel = styled(textarea, `
cursor: pointer;
min-height: 1.5rem;
color: ${colors.darkText};
color: ${theme.mediumText};
font-size: 12px;
font-weight: 700;
@ -149,12 +139,12 @@ export const cssEditableLabel = styled(textarea, `
&-edit {
cursor: auto;
background: ${theme.inputBg};
outline: 2px solid black;
outline: 2px solid ${theme.accessRulesFormulaEditorFocus};
outline-offset: 1px;
border-radius: 2px;
}
&-normal {
color: ${colors.darkText};
color: ${theme.mediumText};
font-size: 15px;
font-weight: normal;
}
@ -172,15 +162,16 @@ export const cssDesc = styled('div', `
`);
export const cssInput = styled('input', `
background-color: ${theme.inputDisabledBg};
font-size: inherit;
padding: 4px 8px;
border: 1px solid #D9D9D9;
border: 1px solid ${theme.inputBorder};
border-radius: 3px;
outline: none;
cursor-events: none;
pointer-events: none;
&-invalid {
color: red;
color: ${theme.inputInvalid};
}
&[type="number"], &[type="date"], &[type="datetime-local"], &[type="text"] {
width: 100%;
@ -190,19 +181,19 @@ export const cssInput = styled('input', `
export const cssSelect = styled('select', `
flex: auto;
width: 100%;
background-color: ${theme.inputDisabledBg};
font-size: inherit;
padding: 4px 8px;
border: 1px solid #D9D9D9;
border: 1px solid ${theme.inputBorder};
border-radius: 3px;
outline: none;
cursor-events: none;
pointer-events: none;
&-invalid {
color: red;
color: ${theme.inputInvalid};
}
`);
export const cssFieldEditorContent = styled('div', `
`);
@ -221,14 +212,6 @@ export const cssSelectedOverlay = styled('div._cssSelectedOverlay', `
}
`);
export const cssControlsLabel = styled('div', `
background: ${colors.lightGreen};
color: ${colors.light};
padding: 1px 2px;
min-width: 24px;
`);
export const cssPlusButton = styled('div', `
position: relative;
min-height: 32px;
@ -242,32 +225,18 @@ export const cssCircle = styled('div', `
border-radius: 50%;
width: 24px;
height: 24px;
background-color: ${colors.lightGreen};
color: ${colors.light};
background-color: ${theme.addNewCircleSmallBg};
color: ${theme.addNewCircleSmallFg};
display: flex;
justify-content: center;
align-items: center;
.${cssPlusButton.className}:hover & {
background: ${colors.darkGreen};
background: ${theme.addNewCircleSmallHoverBg};
}
`);
export const cssPlusIcon = styled(icon, `
--icon-color: ${colors.light};
`);
export const cssAddText = styled('div', `
color: ${colors.slate};
border-radius: 4px;
padding: 2px 4px;
font-size: 12px;
z-index: 1;
&:before {
content: "Add a field";
}
.${cssPlusButton.className}-hover &:before {
content: "Drop here";
}
--icon-color: ${theme.controlPrimaryFg};
`);
export const cssPadding = styled('div', `
@ -300,27 +269,27 @@ export const cssColumn = styled('div', `
justify-content: center;
align-items: center;
padding-right: 8px;
--icon-color: ${colors.slate};
--icon-color: ${theme.lightText};
align-self: stretch;
transition: height 0.2s ease-in-out;
border: 2px dashed ${colors.darkGrey};
background: ${colors.lightGrey};
color: ${colors.slate};
border: 2px dashed ${theme.inputBorder};
background: ${theme.lightHover};
color: ${theme.lightText};
border-radius: 4px;
padding: 2px 4px;
font-size: 12px;
}
&-selected {
border: 2px dashed ${colors.slate};
border: 2px dashed ${theme.lightText};
}
&-empty:hover, &-add-button:hover {
border: 2px dashed ${colors.slate};
border: 2px dashed ${theme.lightText};
}
&-drag-over {
outline: 2px dashed ${colors.lightGreen};
outline: 2px dashed ${theme.controlPrimaryBg};
}
&-add-button {
@ -352,13 +321,10 @@ export const cssButtonGroup = styled('div', `
`);
export const cssIconLink = styled(basicButtonLink, `
padding: 3px 8px;
font-size: ${vars.smallFontSize};
export const cssIconLink = styled(bigBasicButtonLink, `
display: flex;
align-items: center;
gap: 4px;
min-height: 24px;
&-standard {
background-color: ${theme.leftPanelBg};
@ -379,13 +345,21 @@ export const cssIconLink = styled(basicButtonLink, `
}
`);
export const cssIconButton = styled(basicButton, `
padding: 3px 8px;
font-size: ${vars.smallFontSize};
export const cssSmallIconButton = styled(basicButton, `
display: flex;
align-items: center;
gap: 4px;
&-frameless {
background-color: transparent;
border: none;
}
`);
export const cssIconButton = styled(bigBasicButton, `
display: flex;
align-items: center;
gap: 4px;
min-height: 24px;
&-standard {
background-color: ${theme.leftPanelBg};
@ -412,6 +386,9 @@ export const cssMarkdownRendered = styled('div', `
& textarea {
font-size: 15px;
}
&-edit textarea {
outline: 2px solid ${theme.accessRulesFormulaEditorFocus};
}
& strong {
font-weight: 600;
}
@ -425,7 +402,7 @@ export const cssMarkdownRendered = styled('div', `
text-align: right;
}
& hr {
border-color: ${colors.darkGrey};
border-color: ${theme.inputBorder};
margin: 8px 0px;
}
&-separator {
@ -506,7 +483,7 @@ export const cssDrag = styled(icon, `
top: calc(50% - 16px / 2);
width: 16px;
height: 16px;
--icon-color: ${colors.lightGreen};
--icon-color: ${theme.controlPrimaryBg};
&-top {
top: 16px;
}
@ -569,7 +546,7 @@ export const cssRemoveButton = styled('div', `
right: 11px;
top: 11px;
border-radius: 3px;
background: ${colors.darkGrey};
background: ${theme.attachmentsEditorButtonHoverBg};
display: none;
height: 16px;
width: 16px;
@ -582,7 +559,7 @@ export const cssRemoveButton = styled('div', `
width: 13px;
}
&:hover {
background: ${colors.mediumGreyOpaque};
background: ${theme.controlSecondaryHoverBg};
cursor: pointer;
}
.${cssFieldEditor.className}-selected > &,

@ -1,4 +1,3 @@
import {createErrPage} from 'app/client/ui/errorPages';
import {setupPage} from 'app/client/ui/setupPage';
import {setUpErrPage} from 'app/client/ui/errorPages';
setupPage((appModel) => createErrPage(appModel));
setUpErrPage();

@ -48,6 +48,7 @@ const G = getBrowserGlobals('document', 'window');
// TopAppModel is the part of the app model that persists across org and user switches.
export interface TopAppModel {
options: TopAppModelOptions;
api: UserAPI;
isSingleOrg: boolean;
productFlavor: ProductFlavor;
@ -147,6 +148,11 @@ export interface AppModel {
switchUser(user: FullUser, org?: string): Promise<void>;
}
export interface TopAppModelOptions {
/** Defaults to true. */
attachTheme?: boolean;
}
export class TopAppModelImpl extends Disposable implements TopAppModel {
public readonly isSingleOrg: boolean;
public readonly productFlavor: ProductFlavor;
@ -167,6 +173,7 @@ export class TopAppModelImpl extends Disposable implements TopAppModel {
constructor(
window: {gristConfig?: GristLoadConfig},
public readonly api: UserAPI = newUserAPIImpl(),
public readonly options: TopAppModelOptions = {}
) {
super();
setErrorNotifier(this.notifier);
@ -350,7 +357,7 @@ export class AppModelImpl extends Disposable implements AppModel {
) {
super();
this._setTheme();
this._setUpTheme();
this._recordSignUpIfIsNewUser();
const state = urlState().state.get();
@ -525,9 +532,14 @@ export class AppModelImpl extends Disposable implements AppModel {
);
}
private _setTheme() {
// Custom CSS is incompatible with custom themes.
if (getGristConfig().enableCustomCss) { return; }
private _setUpTheme() {
if (
this.topAppModel.options.attachTheme === false ||
// Custom CSS is incompatible with custom themes.
getGristConfig().enableCustomCss
) {
return;
}
attachCssThemeVars(this.currentTheme.get());
this.autoDispose(this.currentTheme.addListener((newTheme, oldTheme) => {

@ -31,7 +31,6 @@ import {ColumnRec, ViewSectionRec} from 'app/client/models/DocModel';
import {CustomSectionConfig} from 'app/client/ui/CustomSectionConfig';
import {buildDescriptionConfig} from 'app/client/ui/DescriptionConfig';
import {BuildEditorOptions} from 'app/client/ui/FieldConfig';
import {autoGrow} from 'app/client/ui/forms';
import {GridOptions} from 'app/client/ui/GridOptions';
import {textarea} from 'app/client/ui/inputs';
import {attachPageWidgetPicker, IPageWidget, toPageWidget} from 'app/client/ui/PageWidgetPicker';
@ -927,11 +926,15 @@ export class RightPanel extends Disposable {
return [
cssLabel(t("Submit button label")),
cssRow(
cssTextInput(submitButton, (val) => submitButton.set(val)),
cssTextInput(submitButton, (val) => submitButton.set(val), {placeholder: 'Submit'}),
),
cssLabel(t("Success text")),
cssRow(
cssTextArea(successText, {onInput: true}, autoGrow(successText)),
cssTextArea(
successText,
{autoGrow: true, save: (val) => successText.set(val)},
{placeholder: 'Thank you! Your response has been recorded.'}
),
),
cssLabel(t("Submit another response")),
cssRow(
@ -944,7 +947,7 @@ export class RightPanel extends Disposable {
labeledSquareCheckbox(redirection, t('Redirect automatically after submission')),
),
cssRow(
cssTextInput(successURL, (val) => successURL.set(val)),
cssTextInput(successURL, (val) => successURL.set(val), {placeholder: t('Enter redirect URL')}),
dom.show(redirection),
),
];

@ -4,10 +4,12 @@ import {getLoginUrl, getMainOrgUrl, getSignupUrl, urlState} from 'app/client/mod
import {AppHeader} from 'app/client/ui/AppHeader';
import {leftPanelBasic} from 'app/client/ui/LeftPanelCommon';
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 {theme, vars} from 'app/client/ui2018/cssVars';
import {getPageTitleSuffix, GristLoadConfig} from 'app/common/gristUrls';
import {colors, theme, vars} from 'app/client/ui2018/cssVars';
import {icon} from 'app/client/ui2018/icons';
import {getPageTitleSuffix} from 'app/common/gristUrls';
import {getGristConfig} from 'app/common/urlUtils';
import {dom, DomElementArg, makeTestId, observable, styled} from 'grainjs';
@ -15,14 +17,22 @@ const testId = makeTestId('test-');
const t = makeT('errorPages');
export function setUpErrPage() {
const {errPage} = getGristConfig();
const attachTheme = errPage !== 'form-not-found';
setUpPage((appModel) => {
return createErrPage(appModel);
}, {attachTheme});
}
export function createErrPage(appModel: AppModel) {
const gristConfig: GristLoadConfig = (window as any).gristConfig || {};
const message = gristConfig.errMessage;
return gristConfig.errPage === 'signed-out' ? createSignedOutPage(appModel) :
gristConfig.errPage === 'not-found' ? createNotFoundPage(appModel, message) :
gristConfig.errPage === 'access-denied' ? createForbiddenPage(appModel, message) :
gristConfig.errPage === 'account-deleted' ? createAccountDeletedPage(appModel) :
createOtherErrorPage(appModel, message);
const {errMessage, errPage} = getGristConfig();
return errPage === 'signed-out' ? createSignedOutPage(appModel) :
errPage === 'not-found' ? createNotFoundPage(appModel, errMessage) :
errPage === 'access-denied' ? createForbiddenPage(appModel, errMessage) :
errPage === 'account-deleted' ? createAccountDeletedPage(appModel) :
errPage === 'form-not-found' ? createFormNotFoundPage(errMessage) :
createOtherErrorPage(appModel, errMessage);
}
/**
@ -99,6 +109,43 @@ export function createNotFoundPage(appModel: AppModel, message?: string) {
]);
}
/**
* Creates a form-specific "Not Found" page.
*/
export function createFormNotFoundPage(message?: string) {
document.title = t("Form not found");
return cssFormErrorPage(
cssFormErrorContainer(
cssFormError(
cssFormErrorBody(
cssFormErrorImage({src: 'forms/form-not-found.svg'}),
cssFormErrorText(
message ?? t('An unknown error occurred.'),
testId('error-text'),
),
),
cssFormErrorFooter(
cssFormPoweredByGrist(
cssFormPoweredByGristLink(
{href: 'https://www.getgrist.com', target: '_blank'},
t('Powered by'),
cssGristLogo(),
)
),
cssFormBuildForm(
cssFormBuildFormLink(
{href: 'https://www.getgrist.com', target: '_blank'},
t('Build your own form'),
icon('Expand'),
),
),
),
),
),
);
}
/**
* Creates a generic error page with the given message.
*/
@ -178,3 +225,102 @@ const cssErrorText = styled('div', `
const cssButtonWrap = styled('div', `
margin-bottom: 8px;
`);
const cssFormErrorPage = styled('div', `
--grist-form-padding: 48px;
min-height: 100%;
width: 100%;
padding-top: 52px;
`);
const cssFormErrorContainer = styled('div', `
padding-left: 16px;
padding-right: 16px;
`);
const cssFormError = styled('div', `
display: flex;
text-align: center;
flex-direction: column;
align-items: center;
border: 1px solid ${colors.darkGrey};
border-radius: 3px;
max-width: 600px;
margin: 0px auto;
`);
const cssFormErrorBody = styled('div', `
padding: 48px 16px 0px 16px;
`);
const cssFormErrorImage = styled('img', `
width: 250px;
height: 281px;
`);
const cssFormErrorText = styled('div', `
font-weight: 600;
font-size: 16px;
line-height: 24px;
margin-top: 32px;
margin-bottom: 24px;
`);
const cssFormErrorFooter = styled('div', `
border-top: 1px solid ${colors.darkGrey};
padding: 8px 16px;
width: 100%;
`);
const cssFormPoweredByGrist = styled('div', `
color: ${colors.darkText};
font-size: 13px;
font-style: normal;
font-weight: 600;
line-height: 16px;
display: flex;
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', `
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
color: ${colors.darkText};
text-decoration: none;
`);
const cssFormBuildForm = styled('div', `
display: flex;
align-items: center;
justify-content: center;
margin-top: 8px;
`);
const cssFormBuildFormLink = styled('a', `
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
line-height: 16px;
text-decoration-line: underline;
color: ${colors.darkGreen};
--icon-color: ${colors.darkGreen};
`);
const cssGristLogo = styled('div', `
width: 58px;
height: 20.416px;
flex-shrink: 0;
background: url(forms/logo.png);
background-position: 0 0;
background-size: contain;
background-color: transparent;
background-repeat: no-repeat;
margin-top: 3px;
`);

@ -1,6 +1,6 @@
import {get as getBrowserGlobals} from 'app/client/lib/browserGlobals';
import {setupLocale} from 'app/client/lib/localization';
import {AppModel, TopAppModelImpl} from 'app/client/models/AppModel';
import {AppModel, newUserAPIImpl, TopAppModelImpl} from 'app/client/models/AppModel';
import {setUpErrorHandling} from 'app/client/models/errors';
import {buildSnackbarDom} from 'app/client/ui/NotifyUI';
import {addViewportTag} from 'app/client/ui/viewport';
@ -10,13 +10,22 @@ import {dom, DomContents} from 'grainjs';
const G = getBrowserGlobals('document', 'window');
export interface SetUpPageOptions {
/** Defaults to true. */
attachTheme?: boolean;
}
/**
* Sets up error handling and global styles, and replaces the DOM body with
* the result of calling `buildPage`.
*/
export function setupPage(buildPage: (appModel: AppModel) => DomContents) {
export function setUpPage(
buildPage: (appModel: AppModel) => DomContents,
options: SetUpPageOptions = {}
) {
const {attachTheme = true} = options;
setUpErrorHandling();
const topAppModel = TopAppModelImpl.create(null, {});
const topAppModel = TopAppModelImpl.create(null, {}, newUserAPIImpl(), {attachTheme});
attachCssRootVars(topAppModel.productFlavor);
addViewportTag();

@ -167,6 +167,7 @@ export const theme = {
/* Text */
text: new CustomProp('theme-text', undefined, colors.dark),
lightText: new CustomProp('theme-text-light', undefined, colors.slate),
mediumText: new CustomProp('theme-text-medium', undefined, colors.darkText),
darkText: new CustomProp('theme-text-dark', undefined, 'black'),
errorText: new CustomProp('theme-text-error', undefined, colors.error),
errorTextHover: new CustomProp('theme-text-error-hover', undefined, '#BF0A31'),

@ -4,13 +4,14 @@ import {reportError} from 'app/client/models/errors';
import {cssInput} from 'app/client/ui/cssInput';
import {prepareForTransition, TransitionWatcher} from 'app/client/ui/transitions';
import {bigBasicButton, bigPrimaryButton, cssButton} from 'app/client/ui2018/buttons';
import {labeledSquareCheckbox} from 'app/client/ui2018/checkbox';
import {mediaSmall, testId, theme, vars} from 'app/client/ui2018/cssVars';
import {loadingSpinner} from 'app/client/ui2018/loaders';
import {cssMenuElem} from 'app/client/ui2018/menus';
import {waitGrainObs} from 'app/common/gutil';
import {IOpenController, IPopupOptions, PopupControl, popupOpen} from 'popweasel';
import {Computed, Disposable, dom, DomContents, DomElementArg, input, keyframes,
MultiHolder, Observable, styled} from 'grainjs';
import {cssMenuElem} from 'app/client/ui2018/menus';
import {IOpenController, IPopupOptions, PopupControl, popupOpen} from 'popweasel';
const t = makeT('modals');
@ -339,6 +340,8 @@ export function saveModal(
export interface ConfirmModalOptions {
explanation?: DomElementArg,
hideCancel?: boolean;
/** Defaults to true. */
hideDontShowAgain?: boolean;
extraButtons?: DomContents;
modalOptions?: IModalOptions;
saveDisabled?: Observable<boolean>;
@ -353,22 +356,42 @@ export interface ConfirmModalOptions {
export function confirmModal(
title: DomElementArg,
btnText: DomElementArg,
onConfirm: () => Promise<void>,
{explanation, hideCancel, extraButtons, modalOptions, saveDisabled, width}: ConfirmModalOptions = {},
onConfirm: (dontShowAgain?: boolean) => Promise<void>,
options: ConfirmModalOptions = {},
): void {
return saveModal((ctl, owner): ISaveModalOptions => ({
title,
body: explanation || null,
saveLabel: btnText,
saveFunc: onConfirm,
const {
explanation,
hideCancel,
width: width ?? 'normal',
hideDontShowAgain = true,
extraButtons,
modalOptions,
saveDisabled,
}), modalOptions);
width
} = options;
return saveModal((_ctl, owner): ISaveModalOptions => {
const dontShowAgain = Observable.create(owner, false);
return {
title,
body: [
explanation || null,
hideDontShowAgain ? null : dom('div',
cssDontShowAgainCheckbox(
dontShowAgain,
cssDontShowAgainCheckboxLabel(t("Don't show again")),
testId('modal-dont-show-again'),
),
),
],
saveLabel: btnText,
saveFunc: () => onConfirm(hideDontShowAgain ? undefined : dontShowAgain.get()),
hideCancel,
width: width ?? 'normal',
extraButtons,
saveDisabled,
};
}, modalOptions);
}
/**
* Creates a simple prompt modal (replacement for the native one).
* Closed via clicking anywhere outside the modal or Cancel button.
@ -669,3 +692,11 @@ export const cssAnimatedModal = styled('div', `
animation-duration: 0.4s;
position: relative;
`);
const cssDontShowAgainCheckbox = styled(labeledSquareCheckbox, `
line-height: normal;
`);
const cssDontShowAgainCheckboxLabel = styled('span', `
color: ${theme.lightText};
`);

@ -37,7 +37,8 @@ export interface ApiErrorDetails {
}
export type ApiErrorCode =
| 'UserNotConfirmed';
| 'UserNotConfirmed'
| 'FormNotFound';
/**
* An error with an http status code.

@ -1,3 +1,4 @@
import {isHiddenCol} from 'app/common/gristTypes';
import {CellValue, GristType} from 'app/plugin/GristData';
import {MaybePromise} from 'app/plugin/gutil';
import _ from 'lodash';
@ -70,6 +71,7 @@ export interface FieldModel {
description: string;
colId: string;
type: string;
isFormula: boolean;
options: FieldOptions;
values(): MaybePromise<[number, CellValue][]>;
}
@ -202,10 +204,19 @@ abstract class BaseQuestion implements Question {
`;
}
public name(field: FieldModel): string {
const excludeFromFormData = (
field.isFormula ||
field.type === 'Attachments' ||
isHiddenCol(field.colId)
);
return `${excludeFromFormData ? '_' : ''}${field.colId}`;
}
public label(field: FieldModel): string {
// This might be HTML.
const label = field.question;
const name = field.colId;
const name = this.name(field);
return `
<label class='grist-label' for='${name}'>${label}</label>
`;
@ -218,7 +229,7 @@ class Text extends BaseQuestion {
public input(field: FieldModel, context: RenderContext): string {
const required = field.options.formRequired ? 'required' : '';
return `
<input type='text' name='${field.colId}' ${required}/>
<input type='text' name='${this.name(field)}' ${required}/>
`;
}
}
@ -227,7 +238,7 @@ class Date extends BaseQuestion {
public input(field: FieldModel, context: RenderContext): string {
const required = field.options.formRequired ? 'required' : '';
return `
<input type='date' name='${field.colId}' ${required}/>
<input type='date' name='${this.name(field)}' ${required}/>
`;
}
}
@ -236,7 +247,7 @@ class DateTime extends BaseQuestion {
public input(field: FieldModel, context: RenderContext): string {
const required = field.options.formRequired ? 'required' : '';
return `
<input type='datetime-local' name='${field.colId}' ${required}/>
<input type='datetime-local' name='${this.name(field)}' ${required}/>
`;
}
}
@ -248,7 +259,7 @@ class Choice extends BaseQuestion {
// Insert empty option.
choices.unshift('');
return `
<select name='${field.colId}' ${required} >
<select name='${this.name(field)}' ${required} >
${choices.map((choice) => `<option value='${choice}'>${choice}</option>`).join('')}
</select>
`;
@ -271,7 +282,7 @@ class Bool extends BaseQuestion {
const label = field.question ? field.question : field.colId;
return `
<label class='grist-switch'>
<input type='checkbox' name='${field.colId}' value="1" ${required} />
<input type='checkbox' name='${this.name(field)}' value="1" ${required} />
<div class="grist-widget_switch grist-switch_transition">
<div class="grist-switch_slider"></div>
<div class="grist-switch_circle"></div>
@ -287,10 +298,10 @@ class ChoiceList extends BaseQuestion {
const required = field.options.formRequired ? 'required' : '';
const choices: string[] = field.options.choices || [];
return `
<div name='${field.colId}' class='grist-choice-list grist-checkbox-list ${required}'>
<div name='${this.name(field)}' class='grist-checkbox-list ${required}'>
${choices.map((choice) => `
<label>
<input type='checkbox' name='${field.colId}[]' value='${choice}' />
<label class='grist-checkbox'>
<input type='checkbox' name='${this.name(field)}[]' value='${choice}' />
<span>
${choice}
</span>
@ -310,12 +321,12 @@ class RefList extends BaseQuestion {
// Support for 30 choices, TODO: make it dynamic.
choices.splice(30);
return `
<div name='${field.colId}' class='grist-ref-list grist-checkbox-list ${required}'>
<div name='${this.name(field)}' class='grist-checkbox-list ${required}'>
${choices.map((choice) => `
<label class='grist-checkbox'>
<input type='checkbox'
data-grist-type='${field.type}'
name='${field.colId}[]'
name='${this.name(field)}[]'
value='${String(choice[0])}' />
<span>
${String(choice[1] ?? '')}
@ -339,7 +350,7 @@ class Ref extends BaseQuestion {
// <option type='number' is not standard, we parse it ourselves.
const required = field.options.formRequired ? 'required' : '';
return `
<select name='${field.colId}' class='grist-ref' data-grist-type='${field.type}' ${required}>
<select name='${this.name(field)}' class='grist-ref' data-grist-type='${field.type}' ${required}>
${choices.map((choice) => `<option value='${String(choice[0])}'>${String(choice[1] ?? '')}</option>`).join('')}
</select>
`;

@ -102,12 +102,14 @@ export interface BehavioralPromptPrefs {
* List of all popups that user can see and dismiss
*/
export const DismissedPopup = StringUnion(
'deleteRecords', // confirmation for deleting records keyboard shortcut,
'deleteFields', // confirmation for deleting columns keyboard shortcut,
'tutorialFirstCard', // first card of the tutorial,
'formulaHelpInfo', // formula help info shown in the popup editor,
'formulaAssistantInfo', // formula assistant info shown in the popup editor,
'supportGrist', // nudge to opt in to telemetry,
'deleteRecords', // confirmation for deleting records keyboard shortcut
'deleteFields', // confirmation for deleting columns keyboard shortcut
'tutorialFirstCard', // first card of the tutorial
'formulaHelpInfo', // formula help info shown in the popup editor
'formulaAssistantInfo', // formula assistant info shown in the popup editor
'supportGrist', // nudge to opt in to telemetry
'publishForm', // confirmation for publishing a form
'unpublishForm', // confirmation for unpublishing a form
);
export type DismissedPopup = typeof DismissedPopup.type;

@ -27,6 +27,7 @@ export const Theme = t.iface([], {
export const ThemeColors = t.iface([], {
"text": "string",
"text-light": "string",
"text-medium": "string",
"text-dark": "string",
"text-error": "string",
"text-error-hover": "string",

@ -25,6 +25,7 @@ export interface ThemeColors {
/* Text */
'text': string;
'text-light': string;
'text-medium': string;
'text-dark': string;
'text-error': string;
'text-error-hover': string;

@ -4,6 +4,7 @@ export const GristDark: ThemeColors = {
/* Text */
'text': '#EFEFEF',
'text-light': '#A4A4B1',
'text-medium': '#D5D5D5',
'text-dark': '#FFFFFF',
'text-error': '#E63946',
'text-error-hover': '#FF5C5C',

@ -4,6 +4,7 @@ export const GristLight: ThemeColors = {
/* Text */
'text': '#262633',
'text-light': '#929299',
'text-medium': '#494949',
'text-dark': 'black',
'text-error': '#D0021B',
'text-error-hover': '#A10000',

@ -237,7 +237,7 @@ export function attachAppEndpoint(options: AttachOptions): void {
res.send(html);
} else {
const error = await response.json();
throw new ApiError(error?.error ?? 'Failed to fetch form', response.status);
throw new ApiError(error?.error ?? 'An unknown error occurred.', response.status, error?.details);
}
}));
}

@ -1493,6 +1493,7 @@ export class DocWorkerApi {
question: options.question || col.label || colId,
options,
type,
isFormula: Boolean(col.isFormula && col.formula),
// If this is reference field, we will need to fetch the referenced table.
values: refValues(col)
};
@ -1529,7 +1530,7 @@ export class DocWorkerApi {
ANOTHER_RESPONSE: Boolean(box.anotherResponse),
// Not trusted content entered by user.
CONTENT: html,
SUCCESS_TEXT: box.successText || `Thank you! Your response has been recorded.`,
SUCCESS_TEXT: box.successText || 'Thank you! Your response has been recorded.',
SUCCESS_URL: redirectUrl,
});
res.status(200).send(renderedHtml);
@ -1550,40 +1551,37 @@ export class DocWorkerApi {
throw new ApiError('DocData not available', 500);
}
const notFoundError = () => {
throw new ApiError("Oops! The form you're looking for doesn't exist.", 404, {
code: 'FormNotFound',
});
};
// Check that the request is for a valid section in the document.
const sections = docData.getMetaTable('_grist_Views_section');
const section = sections.getRecord(sectionId);
if (!section) {
throw new ApiError('Form not found', 404);
}
if (!section) { return notFoundError(); }
// Check that the section is for a form.
const sectionShareOptions = safeJsonParse(section.shareOptions, {});
if (!sectionShareOptions.form) {
throw new ApiError('Form not found', 400);
}
if (!sectionShareOptions.form) { return notFoundError(); }
// Check that the form is associated with a share.
const viewId = section.parentId;
const pages = docData.getMetaTable('_grist_Pages');
const page = pages.getRecords().find(p => p.viewRef === viewId);
if (!page) {
throw new ApiError('Form not found', 404);
}
if (!page) { return notFoundError(); }
const shares = docData.getMetaTable('_grist_Shares');
const share = shares.getRecord(page.shareRef);
if (!share) {
throw new ApiError('Form not found', 404);
}
if (!share) { return notFoundError(); }
// Check that the share's link id matches the expected link id.
if (share.linkId !== linkId) {
throw new ApiError('Form not found', 404);
}
if (share.linkId !== linkId) { return notFoundError(); }
// Finally, check that both the section and share are published.
if (!sectionShareOptions.publish || !safeJsonParse(share.options, {})?.publish) {
throw new ApiError('Form not published', 400);
throw new ApiError('Oops! This form is no longer published.', 404, {code: 'FormNotFound'});
}
}

@ -1510,6 +1510,7 @@ export class FlexServer implements GristServer {
if (resp.headersSent || !this._sendAppPage) { return next(err); }
try {
const errPage = (
err.details?.code === 'FormNotFound' ? 'form-not-found' :
err.status === 403 ? 'access-denied' :
err.status === 404 ? 'not-found' :
'other-error'

@ -0,0 +1,40 @@
<svg width="250" height="281" viewBox="0 0 250 281" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_848_6612)">
<path d="M141.5 217C201.423 217 250 168.423 250 108.5C250 48.5771 201.423 0 141.5 0C81.5771 0 33 48.5771 33 108.5C33 168.423 81.5771 217 141.5 217Z" fill="#F7F7F7"/>
<path d="M212.246 49H13.7544C13.3288 48.9882 12.9911 48.5309 13.0002 47.9786C13.009 47.443 13.3416 47.0115 13.7544 47H212.246C212.671 47.0118 213.009 47.4691 213 48.0214C212.991 48.557 212.658 48.9885 212.246 49Z" fill="#D9D9D9"/>
<path d="M145.516 74H90.4843C86.3508 74 83 77.3579 83 81.5V81.5C83 85.6421 86.3508 89 90.4843 89H145.516C149.649 89 153 85.6421 153 81.5C153 77.3579 149.649 74 145.516 74Z" fill="white"/>
<path d="M54.479 104H180.521C184.652 104 188 107.358 188 111.5C188 115.642 184.652 119 180.521 119H54.479C50.3484 119 47 115.642 47 111.5C47 107.358 50.3484 104 54.479 104Z" fill="white"/>
<path d="M54.479 134H180.521C184.652 134 188 137.358 188 141.5C188 145.642 184.652 149 180.521 149H54.479C50.3484 149 47 145.642 47 141.5C47 137.358 50.3484 134 54.479 134Z" fill="white"/>
<path d="M73.0013 276L77.7435 276L80 258L73 258L73.0013 276Z" fill="#FFF3DE"/>
<path d="M73.0002 281L87 280.999V280.812C86.9998 277.631 84.5602 275.051 81.5509 275.051L78.9933 273L74.2221 275.051L73 275.052L73.0002 281Z" fill="#262633"/>
<path d="M36 263.232L39.6614 266L52 253.085L46.5955 249L36 263.232Z" fill="#FFF3DE"/>
<path d="M33 266.535L44.7449 275L44.8563 274.857C46.7502 272.432 46.2391 268.99 43.7146 267.17L42.7901 264.06L37.5662 262.739L36.5409 262L33 266.535Z" fill="#262633"/>
<path d="M51.3552 192.296C51.3552 192.296 51.6075 199.243 51.9257 204.814C51.9779 205.728 49.8574 206.721 49.9123 207.715C49.9487 208.374 50.3473 209.015 50.3843 209.703C50.4234 210.429 49.8731 211.071 49.9123 211.822C49.9506 212.555 50.5784 213.396 50.6162 214.146C51.0238 222.223 52.5011 231.893 51.0218 234.596C50.2927 235.928 41 254.383 41 254.383C41 254.383 46.1776 262.223 47.6569 259.61C49.6108 256.158 64.669 240.009 64.669 236.925C64.669 233.845 68.1527 210.761 68.1527 210.761L70.5819 224.422L71.6957 227.129L71.3457 228.718L72.0655 230.862L72.1044 232.984L72.8052 236.925C72.8052 236.925 70.6972 267.629 71.8843 268.484C73.0752 269.343 78.8408 270.885 79.5213 269.343C80.1981 267.801 84.6033 237.299 84.6033 237.299C84.6033 237.299 85.3133 223.321 86.0826 210.328C86.127 209.574 86.6522 208.663 86.6928 207.92C86.7409 207.058 86.4081 205.938 86.4524 205.101C86.5005 204.142 86.9221 203.466 86.9628 202.548C87.2771 195.45 85.5125 186.808 84.9504 185.957C83.2528 183.388 81.7254 181.335 81.7254 181.335C81.7254 181.335 58.8618 173.751 52.7523 181.457L51.3552 192.296Z" fill="#262633"/>
<path d="M74.962 120.819L65.7068 119L61.3877 124.76C53.3285 132.546 53.2071 139.418 54.6458 150.433V165.893L53.6045 176.257C53.6045 176.257 49.9184 183.833 53.7203 185.597C57.5222 187.361 82.4953 187.211 84.8796 186.507C87.2639 185.803 85.1972 184.938 84.5711 181.656C83.395 175.492 84.1817 178.466 84.2626 176.503C85.4231 148.35 82.7201 136.792 82.4456 133.754L77.7387 125.366L74.962 120.819Z" fill="#929299"/>
<path d="M122.554 143.416C121.591 145.202 119.331 145.886 117.507 144.943C117.312 144.843 117.127 144.726 116.954 144.593L97 157L97.0575 150.245L116.441 139.23C117.822 137.714 120.196 137.581 121.744 138.933C123.039 140.064 123.374 141.918 122.554 143.416Z" fill="#FFF3DE"/>
<path d="M66.7149 129.273L64.4887 129.032C62.4317 128.814 60.4135 129.714 59.1714 131.402C58.6956 132.043 58.3539 132.776 58.1675 133.557L58.1662 133.563C57.6078 135.913 58.4836 138.377 60.3887 139.814L68.0777 145.602C73.4736 153.048 83.6146 157.987 95.5101 162L114 150.274L107.46 142.186L94.7777 149.126L75.9262 134.225L75.9153 134.216L68.7403 129.495L66.7149 129.273Z" fill="#929299"/>
<path d="M71.5 116C77.299 116 82 111.299 82 105.5C82 99.701 77.299 95 71.5 95C65.701 95 61 99.701 61 105.5C61 111.299 65.701 116 71.5 116Z" fill="#FFF3DE"/>
<path d="M71.3801 117C71.2942 117.003 71.2082 117.005 71.1223 117.007C71.0855 117.102 71.0458 117.197 71 117.29L71.3801 117Z" fill="#2F2E41"/>
<path d="M74.4839 106.254C74.5006 106.359 74.5261 106.462 74.5602 106.563C74.5462 106.457 74.5206 106.354 74.4839 106.254Z" fill="#2F2E41"/>
<path d="M82.0995 95.767C81.6612 97.1278 81.27 95.4017 79.8282 95.7122C78.0938 96.0856 76.0754 95.9574 74.655 94.8944C72.5387 93.3418 69.796 92.9238 67.3132 93.7755C64.8828 94.6352 60.811 95.2414 60.1778 97.74C59.9576 98.6086 59.8702 99.5512 59.3366 100.271C58.8701 100.9 58.1338 101.259 57.5181 101.744C55.4391 103.379 57.0281 108.026 58.0121 110.481C58.9961 112.936 61.2533 114.725 63.7349 115.641C66.136 116.528 68.7421 116.684 71.3035 116.614C71.7491 115.459 71.5566 114.125 71.189 112.927C70.791 111.63 70.2018 110.379 70.0355 109.033C69.8693 107.687 70.2343 106.158 71.3631 105.406C72.4006 104.715 74.089 105.136 74.4837 106.254C74.2534 104.856 75.669 103.504 77.1287 103.266C78.6959 103.011 80.2631 103.577 81.8258 103.858C83.3885 104.14 82.8338 97.5619 82.0995 95.767Z" fill="#262633"/>
<path d="M141.652 123.57C142.645 111.578 133.727 101.052 121.733 100.059C109.739 99.0666 99.2114 107.984 98.2187 119.976C97.2261 131.968 106.144 142.494 118.138 143.487C130.132 144.479 140.66 135.562 141.652 123.57Z" fill="#F9AE41"/>
<path d="M125.66 112.695L119.936 118.418L114.212 112.695C113.285 111.768 111.783 111.768 110.856 112.695C109.929 113.621 109.929 115.123 110.856 116.05L116.58 121.773L110.856 127.496C109.93 128.424 109.932 129.926 110.859 130.851C111.785 131.776 113.285 131.776 114.212 130.851L119.936 125.128L125.66 130.851C126.587 131.777 128.09 131.776 129.015 130.848C129.94 129.922 129.94 128.423 129.015 127.496L123.291 121.773L129.015 116.05C129.942 115.123 129.942 113.621 129.015 112.695C128.089 111.768 126.586 111.768 125.66 112.695Z" fill="white"/>
<path d="M111.349 103.46C111.991 105.395 110.943 107.484 109.008 108.126C108.802 108.194 108.59 108.245 108.375 108.276L103.758 131.171L98.8779 126.411L104.118 104.895C103.959 102.851 105.486 101.066 107.53 100.906C109.24 100.773 110.819 101.829 111.349 103.46Z" fill="#FFF3DE"/>
<path d="M62.2893 133.074L60.5634 134.529C58.9721 135.876 58.2041 137.96 58.5401 140.018C58.6643 140.802 58.9484 141.552 59.3747 142.221L59.3782 142.226C60.6633 144.239 63.0302 145.286 65.3843 144.882L74.8777 143.245C83.9508 144.408 94.5509 140.406 105.716 134.494L110.268 113.025L99.9382 112.254L96.0301 126.227L72.2491 129.756L72.2351 129.758L63.8618 131.752L62.2893 133.074Z" fill="#929299"/>
<path d="M116.346 280.87L0.505916 281.001C0.225604 281 -0.000905241 280.772 2.72016e-06 280.491C0.000902156 280.212 0.226878 279.986 0.505916 279.985L116.346 279.854C116.626 279.855 116.852 280.083 116.852 280.364C116.851 280.643 116.625 280.869 116.346 280.87Z" fill="#D9D9D9"/>
<path d="M24.2025 38.6588C26.1934 38.6588 27.8073 37.0091 27.8073 34.9742C27.8073 32.9392 26.1934 31.2896 24.2025 31.2896C22.2116 31.2896 20.5977 32.9392 20.5977 34.9742C20.5977 37.0091 22.2116 38.6588 24.2025 38.6588Z" fill="#494949"/>
<path d="M36.6556 38.6588C38.6465 38.6588 40.2604 37.0091 40.2604 34.9742C40.2604 32.9392 38.6465 31.2896 36.6556 31.2896C34.6647 31.2896 33.0508 32.9392 33.0508 34.9742C33.0508 37.0091 34.6647 38.6588 36.6556 38.6588Z" fill="#494949"/>
<path d="M49.1087 38.6588C51.0996 38.6588 52.7136 37.0091 52.7136 34.9742C52.7136 32.9392 51.0996 31.2896 49.1087 31.2896C47.1178 31.2896 45.5039 32.9392 45.5039 34.9742C45.5039 37.0091 47.1178 38.6588 49.1087 38.6588Z" fill="#494949"/>
<path d="M63.3084 37.8875C63.1976 37.8875 63.0871 37.8447 63.0037 37.7593L60.5713 35.2729C60.4092 35.1072 60.4092 34.8424 60.5713 34.6768L63.0037 32.1905C63.1687 32.0223 63.4389 32.0196 63.6064 32.1838C63.7748 32.3485 63.7777 32.6184 63.6131 32.7866L61.4723 34.9748L63.6131 37.1632C63.7777 37.3314 63.7748 37.6013 63.6064 37.766C63.5236 37.8472 63.416 37.8875 63.3084 37.8875Z" fill="#494949"/>
<path d="M67.3955 37.8874C67.2879 37.8874 67.1803 37.847 67.0975 37.7658C66.9291 37.6012 66.9262 37.3314 67.0908 37.163L69.2313 34.9746L67.0908 32.7864C66.9262 32.6183 66.9291 32.3483 67.0975 32.1837C67.2654 32.0188 67.5356 32.0219 67.7003 32.1903L70.1324 34.6766C70.2945 34.8423 70.2945 35.107 70.1324 35.2727L67.7003 37.7591C67.6168 37.8445 67.5063 37.8874 67.3955 37.8874Z" fill="#494949"/>
<path d="M194.642 31.7163H190.12C189.604 31.7163 189.186 32.134 189.186 32.6498V37.1769C189.186 37.6927 189.604 38.1104 190.12 38.1104H194.642C195.158 38.1104 195.58 37.6927 195.58 37.1769V32.6498C195.58 32.134 195.158 31.7163 194.642 31.7163Z" fill="#494949"/>
<path d="M183.559 31.7163H179.037C178.521 31.7163 178.103 32.134 178.103 32.6498V37.1769C178.103 37.6927 178.521 38.1104 179.037 38.1104H183.559C184.075 38.1104 184.497 37.6927 184.497 37.1769V32.6498C184.497 32.134 184.075 31.7163 183.559 31.7163Z" fill="#494949"/>
<path d="M205.086 31.9292H200.563C200.048 31.9292 199.63 32.3469 199.63 32.8627V37.3898C199.63 37.9056 200.048 38.3233 200.563 38.3233H205.086C205.602 38.3233 206.024 37.9056 206.024 37.3898V32.8627C206.024 32.3469 205.602 31.9292 205.086 31.9292Z" fill="#494949"/>
<path d="M136.916 33.6514H100.764C100.133 33.6514 99.6255 34.1629 99.6255 34.7895C99.6255 35.4162 100.133 35.9277 100.764 35.9277H136.916C137.542 35.9277 138.054 35.4161 138.054 34.7895C138.054 34.1629 137.542 33.6514 136.916 33.6514Z" fill="#494949"/>
</g>
<defs>
<clipPath id="clip0_848_6612">
<rect width="250" height="281" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 9.2 KiB

@ -0,0 +1,38 @@
<svg width="250" height="215" viewBox="0 0 250 215" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_664_12841)">
<path d="M18.9655 197.496C20.4488 198.003 22.0414 198.13 23.5898 197.865C25.1383 197.599 26.5904 196.95 27.8063 195.979C30.9028 193.448 31.8738 189.28 32.6635 185.419L35 174L30.1084 177.28C26.5904 179.638 22.9933 182.073 20.5577 185.489C18.1221 188.905 17.0595 193.569 19.0159 197.266" fill="#D9D9D9"/>
<path d="M19.892 212.685C19.3193 208.27 18.73 203.797 19.1327 199.335C19.4896 195.371 20.6324 191.501 22.9589 188.326C24.1938 186.645 25.6964 185.204 27.3992 184.068C27.8432 183.772 28.2518 184.516 27.8098 184.812C24.8634 186.782 22.5844 189.691 21.3014 193.121C19.8844 196.934 19.6569 201.091 19.901 205.142C20.0485 207.591 20.362 210.025 20.6768 212.456C20.7017 212.566 20.6862 212.681 20.6334 212.78C20.5807 212.878 20.4946 212.951 20.3926 212.985C20.2886 213.015 20.1778 213 20.0841 212.944C19.9904 212.887 19.9215 212.794 19.8923 212.684L19.892 212.685Z" fill="#F7F7F7"/>
<path d="M24 205.062C24.6313 205.998 25.4991 206.759 26.5198 207.273C27.5406 207.787 28.6798 208.036 29.8279 207.996C32.7788 207.859 35.238 205.852 37.4515 203.945L44 198.306L39.6661 198.104C36.5494 197.958 33.3522 197.822 30.3836 198.76C27.415 199.699 24.6773 201.954 24.1345 204.95" fill="#D9D9D9"/>
<path d="M18.0568 214.416C20.9825 209.218 24.3758 203.44 30.4397 201.593C32.1256 201.082 33.8933 200.898 35.6478 201.053C36.2008 201.101 36.0627 201.957 35.5108 201.91C32.5699 201.664 29.6346 202.444 27.1992 204.118C24.8555 205.72 23.0307 207.947 21.4864 210.309C20.5403 211.755 19.6929 213.263 18.8455 214.769C18.5747 215.251 17.7828 214.903 18.0568 214.416Z" fill="#F7F7F7"/>
<path d="M248.16 151H89.8401C89.3523 150.999 88.8846 150.805 88.5397 150.459C88.1947 150.114 88.0006 149.645 88 149.156V1.84371C88.0006 1.35492 88.1947 0.886339 88.5397 0.540715C88.8846 0.195092 89.3523 0.000638965 89.8401 0H248.16C248.648 0.000638965 249.115 0.195092 249.46 0.540715C249.805 0.886339 249.999 1.35492 250 1.84371V149.156C249.999 149.645 249.805 150.113 249.46 150.459C249.115 150.805 248.648 150.999 248.16 151Z" fill="white"/>
<path d="M248.16 151H89.8401C89.3523 150.999 88.8846 150.805 88.5397 150.459C88.1947 150.114 88.0006 149.645 88 149.156V1.84371C88.0006 1.35492 88.1947 0.886339 88.5397 0.540715C88.8846 0.195092 89.3523 0.000638965 89.8401 0H248.16C248.648 0.000638965 249.115 0.195092 249.46 0.540715C249.805 0.886339 249.999 1.35492 250 1.84371V149.156C249.999 149.645 249.805 150.113 249.46 150.459C249.115 150.805 248.648 150.999 248.16 151ZM89.8401 0.736031C89.5478 0.736667 89.2676 0.85332 89.0608 1.06046C88.8541 1.2676 88.7377 1.54835 88.737 1.84129V149.156C88.7377 149.449 88.8541 149.73 89.0608 149.937C89.2676 150.144 89.5478 150.261 89.8401 150.261H248.16C248.452 150.261 248.733 150.144 248.939 149.937C249.146 149.73 249.262 149.449 249.263 149.156V1.84371C249.262 1.55077 249.146 1.27001 248.939 1.06288C248.732 0.85574 248.452 0.739086 248.16 0.73845L89.8401 0.736031Z" fill="#D9D9D9"/>
<path d="M164.246 38C163.916 38.0007 163.599 38.159 163.365 38.4402C163.131 38.7215 163 39.1026 163 39.5C163 39.8974 163.131 40.2785 163.365 40.5598C163.599 40.841 163.916 40.9993 164.246 41H232.754C233.084 40.9993 233.401 40.841 233.635 40.5598C233.869 40.2785 234 39.8974 234 39.5C234 39.1026 233.869 38.7215 233.635 38.4402C233.401 38.159 233.084 38.0007 232.754 38H164.246Z" fill="#D9D9D9"/>
<path d="M164.263 46C163.928 46.001 163.607 46.1594 163.37 46.4406C163.133 46.7218 163 47.1028 163 47.5C163 47.8972 163.133 48.2782 163.37 48.5594C163.607 48.8406 163.928 48.999 164.263 49H199.737C200.072 48.999 200.393 48.8406 200.63 48.5594C200.867 48.2782 201 47.8972 201 47.5C201 47.1028 200.867 46.7218 200.63 46.4406C200.393 46.1594 200.072 46.001 199.737 46H164.263Z" fill="#D9D9D9"/>
<path d="M104.246 86C103.915 86.001 103.598 86.1594 103.365 86.4406C103.131 86.7218 103 87.1028 103 87.5C103 87.8972 103.131 88.2782 103.365 88.5594C103.598 88.8406 103.915 88.999 104.246 89H232.754C233.085 88.999 233.402 88.8406 233.635 88.5594C233.869 88.2782 234 87.8972 234 87.5C234 87.1028 233.869 86.7218 233.635 86.4406C233.402 86.1594 233.085 86.001 232.754 86H104.246Z" fill="#D9D9D9"/>
<path d="M104.253 94C103.92 94.001 103.602 94.1594 103.367 94.4406C103.132 94.7218 103 95.1028 103 95.5C103 95.8972 103.132 96.2782 103.367 96.5594C103.602 96.8406 103.92 96.999 104.253 97H199.747C200.08 96.999 200.398 96.8406 200.633 96.5594C200.868 96.2782 201 95.8972 201 95.5C201 95.1028 200.868 94.7218 200.633 94.4406C200.398 94.1594 200.08 94.001 199.747 94H104.253Z" fill="#D9D9D9"/>
<path d="M104.246 101C103.915 101.001 103.598 101.159 103.365 101.441C103.131 101.722 103 102.103 103 102.5C103 102.897 103.131 103.278 103.365 103.559C103.598 103.841 103.915 103.999 104.246 104H232.754C233.085 103.999 233.402 103.841 233.635 103.559C233.869 103.278 234 102.897 234 102.5C234 102.103 233.869 101.722 233.635 101.441C233.402 101.159 233.085 101.001 232.754 101H104.246Z" fill="#D9D9D9"/>
<path d="M104.253 109C103.92 109.001 103.602 109.159 103.367 109.441C103.132 109.722 103 110.103 103 110.5C103 110.897 103.132 111.278 103.367 111.559C103.602 111.841 103.92 111.999 104.253 112H199.747C200.08 111.999 200.398 111.841 200.633 111.559C200.868 111.278 201 110.897 201 110.5C201 110.103 200.868 109.722 200.633 109.441C200.398 109.159 200.08 109.001 199.747 109H104.253Z" fill="#D9D9D9"/>
<path d="M104.246 116C103.915 116.001 103.598 116.159 103.365 116.441C103.131 116.722 103 117.103 103 117.5C103 117.897 103.131 118.278 103.365 118.559C103.598 118.841 103.915 118.999 104.246 119H232.754C233.085 118.999 233.402 118.841 233.635 118.559C233.869 118.278 234 117.897 234 117.5C234 117.103 233.869 116.722 233.635 116.441C233.402 116.159 233.085 116.001 232.754 116H104.246Z" fill="#D9D9D9"/>
<path d="M104.253 124C103.92 124.001 103.602 124.159 103.367 124.441C103.132 124.722 103 125.103 103 125.5C103 125.897 103.132 126.278 103.367 126.559C103.602 126.841 103.92 126.999 104.253 127H199.747C200.08 126.999 200.398 126.841 200.633 126.559C200.868 126.278 201 125.897 201 125.5C201 125.103 200.868 124.722 200.633 124.441C200.398 124.159 200.08 124.001 199.747 124H104.253Z" fill="#D9D9D9"/>
<path d="M125 61C121.44 61 117.96 59.9444 115 57.9665C112.04 55.9887 109.733 53.1775 108.37 49.8884C107.008 46.5994 106.651 42.9802 107.346 39.4885C108.04 35.9968 109.755 32.7895 112.272 30.2721C114.789 27.7548 117.997 26.0404 121.488 25.3459C124.98 24.6513 128.599 25.0078 131.888 26.3702C135.177 27.7325 137.989 30.0396 139.966 32.9997C141.944 35.9598 143 39.4399 143 43C142.995 47.7722 141.096 52.3474 137.722 55.7219C134.348 59.0964 129.772 60.9945 125 61Z" fill="#16B378"/>
<path d="M69.9936 129.323C69.9633 128.678 69.7927 128.048 69.4941 127.477C69.1954 126.906 68.7758 126.407 68.265 126.017C67.7541 125.626 67.1643 125.353 66.5371 125.217C65.91 125.08 65.2607 125.084 64.6351 125.227L58.831 117L53 119.328L61.3588 130.831C61.6677 131.872 62.3457 132.763 63.2642 133.335C64.1827 133.907 65.2778 134.12 66.3422 133.935C67.4066 133.749 68.3661 133.176 69.0391 132.327C69.7121 131.477 70.0517 130.408 69.9936 129.323Z" fill="#FFF3DE"/>
<path d="M58.9468 129L41 105.431L47.7245 84.036C48.2173 78.6571 51.5422 77.1551 51.6836 77.0937L51.8994 77L57.751 92.797L53.4546 104.395L64 122.35L58.9468 129Z" fill="#262633"/>
<path d="M120.809 57.0385C120.147 57.1194 119.511 57.341 118.946 57.6875C118.381 58.034 117.9 58.4971 117.539 59.0441C117.177 59.5911 116.943 60.2087 116.853 60.8534C116.763 61.4982 116.819 62.1544 117.017 62.7758L109 69.2858L111.87 75L123.059 65.6517C124.107 65.2581 124.972 64.5031 125.488 63.5299C126.004 62.5566 126.135 61.4326 125.858 60.3709C125.581 59.3092 124.914 58.3836 123.983 57.7694C123.052 57.1552 121.923 56.8951 120.809 57.0385Z" fill="#FFF3DE"/>
<path d="M122 68.4885L100.032 88L78.2998 82.9764C72.9292 82.8967 71.1816 79.726 71.1097 79.5908L71 79.3847L86.2044 72.4091L98.0365 75.7755L115.014 64L122 68.4885Z" fill="#262633"/>
<path d="M83.0011 209L89.0993 209L92 186L83 186L83.0011 209Z" fill="#FFF3DE"/>
<path d="M101 215L83 215.001L82.9997 208.001L95.9497 208C97.2891 208 98.5736 208.536 99.5206 209.489C100.468 210.442 101 211.735 101 213.083L101 215Z" fill="#494949"/>
<path d="M60.0008 209L66.0989 209L69 186L60 186L60.0008 209Z" fill="#FFF3DE"/>
<path d="M77 215L59 215.001L58.9997 208.001L71.9497 208C72.6129 208 73.2696 208.132 73.8823 208.387C74.495 208.642 75.0517 209.017 75.5207 209.489C75.9896 209.961 76.3616 210.521 76.6154 211.138C76.8692 211.755 76.9999 212.415 76.9999 213.083L77 215Z" fill="#494949"/>
<path d="M58 126L58.4729 159.704L58.9462 204L69.3513 203.037L74.5542 141.889L81.1758 203.037H91.918L93 141.408L89.2162 127.926L58 126Z" fill="#494949"/>
<path d="M78.2425 131C66.6814 131.001 56.0396 125.775 55.8959 125.703L55.7766 125.644L54.8064 102.391C54.5251 101.569 48.9852 85.3551 48.047 80.2015C47.0963 74.9801 60.8738 70.3977 62.5469 69.8619L62.9266 65.6615L78.3659 64L80.3228 69.3745L85.8618 71.4488C86.4897 71.6841 87.0094 72.1413 87.3224 72.7337C87.6353 73.3262 87.7198 74.0127 87.5598 74.6632L84.4813 87.166L92 128.531L90.3813 128.601C86.5013 130.351 82.3138 131 78.2425 131Z" fill="#262633"/>
<path d="M77.1817 60.0285C82.5097 57.1096 84.4706 50.4197 81.5615 45.0864C78.6524 39.7531 71.975 37.7958 66.6471 40.7148C61.3191 43.6337 59.3583 50.3235 62.2673 55.6569C65.1764 60.9902 71.8538 62.9475 77.1817 60.0285Z" fill="#FFF3DE"/>
<path d="M62.3141 60.8596C64.3894 63.0713 68.2436 61.884 68.514 58.8626C68.5352 58.628 68.5337 58.392 68.5094 58.1577C68.3698 56.8193 67.5969 55.6043 67.782 54.1911C67.8239 53.8394 67.9549 53.5043 68.1625 53.2174C69.8163 51.0019 73.6984 54.2083 75.2591 52.2027C76.2161 50.9729 75.0912 49.0366 75.8256 47.6621C76.7949 45.8479 79.6659 46.7429 81.4663 45.7493C83.4695 44.6439 83.3497 41.5691 82.0311 39.6988C80.4229 37.4179 77.6034 36.2009 74.8189 36.0254C72.0345 35.85 69.2692 36.6031 66.6697 37.6166C63.7161 38.7682 60.7872 40.3596 58.9696 42.9579C56.7592 46.1177 56.5465 50.3657 57.6519 54.0604C58.3244 56.308 60.6194 59.0536 62.3141 60.8596Z" fill="#494949"/>
<path d="M140.632 215H0.368146C0.270508 215 0.176862 214.947 0.107822 214.854C0.0387808 214.76 0 214.633 0 214.5C0 214.367 0.0387808 214.24 0.107822 214.146C0.176862 214.053 0.270508 214 0.368146 214H140.632C140.729 214 140.823 214.053 140.892 214.146C140.961 214.24 141 214.367 141 214.5C141 214.633 140.961 214.76 140.892 214.854C140.823 214.947 140.729 215 140.632 215Z" fill="#D9D9D9"/>
<path d="M124.096 50C123.777 50.0004 123.47 49.8812 123.236 49.6665L117.403 44.3171C117.158 44.0919 117.013 43.7797 117.001 43.4493C116.989 43.1188 117.11 42.7972 117.338 42.5549C117.566 42.3126 117.882 42.1696 118.216 42.1572C118.551 42.1448 118.877 42.2641 119.123 42.4889L124.064 47.0201L133.839 37.3688C133.957 37.2522 134.096 37.1596 134.249 37.0963C134.403 37.033 134.568 37.0003 134.734 37C134.9 36.9997 135.065 37.0318 135.219 37.0945C135.373 37.1573 135.512 37.2493 135.63 37.3655C135.748 37.4816 135.841 37.6196 135.904 37.7714C135.968 37.9232 136 38.0859 136 38.2501C136 38.4144 135.966 38.5769 135.902 38.7285C135.838 38.88 135.744 39.0176 135.626 39.1333L124.99 49.6347C124.872 49.7507 124.733 49.8426 124.58 49.9053C124.426 49.968 124.262 50.0002 124.096 50Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0_664_12841">
<rect width="250" height="215" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

@ -1,8 +1,8 @@
html,
body {
background-color: #f7f7f7;
padding: 0px;
margin: 0px;
background-color: #f7f7f7;
line-height: 1.42857143;
}
@ -13,6 +13,7 @@ body {
.grist-form-container {
--icon-Tick: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTExLjYxODMwNjksNC42NzcwMjg0NyBDMTEuNzk2Njc4OSw0LjQ2NjIyNTE3IDEyLjExMjE2NzgsNC40Mzk5MzQ0MyAxMi4zMjI5NzExLDQuNjE4MzA2NDUgQzEyLjUzMzc3NDQsNC43OTY2Nzg0OCAxMi41NjAwNjUyLDUuMTEyMTY3NDEgMTIuMzgxNjkzMSw1LjMyMjk3MDcxIEw2LjUzMDY4ODI3LDEyLjIzNzc5NDYgTDMuNjQ2NDQ2NjEsOS4zNTM1NTI5OCBDMy40NTExODQ0Niw5LjE1ODI5MDg0IDMuNDUxMTg0NDYsOC44NDE3MDgzNSAzLjY0NjQ0NjYxLDguNjQ2NDQ2MiBDMy44NDE3MDg3Niw4LjQ1MTE4NDA2IDQuMTU4MjkxMjQsOC40NTExODQwNiA0LjM1MzU1MzM5LDguNjQ2NDQ2MiBMNi40NjkzMTE3MywxMC43NjIyMDQ1IEwxMS42MTgzMDY5LDQuNjc3MDI4NDcgWiIgZmlsbD0iIzAwMCIgZmlsbC1ydWxlPSJub256ZXJvIi8+PC9zdmc+);
--icon-Minus: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHJlY3QgZmlsbD0iIzAwMCIgZmlsbC1ydWxlPSJub256ZXJvIiB4PSIyIiB5PSI3LjUiIHdpZHRoPSIxMiIgaGVpZ2h0PSIxIiByeD0iLjUiLz48L3N2Zz4=);
--icon-Expand: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTgsOS4xNzQ2MzA1MiBMMTAuOTIxODI3Myw2LjE4OTAyMzIgQzExLjE2ODQ3NDIsNS45MzY5OTIyNyAxMS41NjgzNjc5LDUuOTM2OTkyMjcgMTEuODE1MDE0OCw2LjE4OTAyMzIgQzEyLjA2MTY2MTcsNi40NDEwNTQxMyAxMi4wNjE2NjE3LDYuODQ5Njc3MDEgMTEuODE1MDE0OCw3LjEwMTcwNzk0IEw4LDExIEw0LjE4NDk4NTE5LDcuMTAxNzA3OTQgQzMuOTM4MzM4MjcsNi44NDk2NzcwMSAzLjkzODMzODI3LDYuNDQxMDU0MTMgNC4xODQ5ODUxOSw2LjE4OTAyMzIgQzQuNDMxNjMyMTEsNS45MzY5OTIyNyA0LjgzMTUyNTc4LDUuOTM2OTkyMjcgNS4wNzgxNzI3LDYuMTg5MDIzMiBMOCw5LjE3NDYzMDUyIFoiIGZpbGw9IiMwMDAiIGZpbGwtcnVsZT0ibm9uemVybyIgdHJhbnNmb3JtPSJyb3RhdGUoLTkwIDggOC41KSIvPjwvc3ZnPg==');
--primary: #16b378;
--primary-dark: #009058;
--dark-gray: #D9D9D9;
@ -29,20 +30,23 @@ body {
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
}
.grist-form-container .grist-form-confirm {
background-color: white;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
border: 1px solid var(--dark-gray);
border-radius: 3px;
max-width: 600px;
margin: 0px auto;
}
.grist-form {
margin: 0px auto;
background-color: white;
border: 1px solid #E8E8E8;
border: 1px solid var(--dark-gray);
width: 600px;
border-radius: 8px;
display: flex;
@ -71,7 +75,7 @@ body {
.grist-form .grist-section {
border-radius: 3px;
border: 1px solid #D9D9D9;
border: 1px solid var(--dark-gray);
padding: 16px 24px;
padding: 24px;
margin-top: 24px;
@ -86,7 +90,7 @@ body {
.grist-form input[type="datetime-local"],
.grist-form input[type="number"] {
padding: 4px 8px;
border: 1px solid #D9D9D9;
border: 1px solid var(--dark-gray);
border-radius: 3px;
outline: none;
}
@ -110,9 +114,9 @@ body {
.grist-form .grist-field input[type="text"] {
padding: 4px 8px;
border-radius: 3px;
border: 1px solid #D9D9D9;
border: 1px solid var(--dark-gray);
font-size: 13px;
outline-color: #16b378;
outline-color: var(--primary);
outline-width: 1px;
line-height: inherit;
width: 100%;
@ -125,8 +129,8 @@ body {
}
.grist-form input[type="submit"], .grist-form-container button {
background-color: #16b378;
border: 1px solid #16b378;
background-color: var(--primary);
border: 1px solid var(--primary);
color: white;
padding: 10px 24px;
border-radius: 4px;
@ -145,11 +149,6 @@ body {
line-height: inherit;
}
.grist-form input[type="checkbox"] {
margin: 0px;
}
.grist-form .grist-columns {
display: grid;
grid-template-columns: repeat(var(--grist-columns-count), 1fr);
@ -159,9 +158,9 @@ body {
.grist-form select {
padding: 4px 8px;
border-radius: 3px;
border: 1px solid #D9D9D9;
border: 1px solid var(--dark-gray);
font-size: 13px;
outline-color: #16b378;
outline-color: var(--primary);
outline-width: 1px;
background: white;
line-height: inherit;
@ -169,7 +168,7 @@ body {
width: 100%;
}
.grist-form .grist-choice-list {
.grist-form .grist-checkbox-list {
display: flex;
flex-direction: column;
gap: 4px;
@ -177,9 +176,6 @@ body {
.grist-form .grist-checkbox {
display: flex;
align-items: center;
gap: 4px;
--color: var(--dark-gray);
}
.grist-form .grist-checkbox:hover {
--color: var(--light-gray);
@ -193,11 +189,9 @@ body {
display: inline-block;
width: 16px;
height: 16px;
outline: none !important;
--radius: 3px;
position: relative;
margin: 0;
margin-right: 4px;
margin-right: 8px;
vertical-align: baseline;
}
@ -249,7 +243,6 @@ body {
background-color: var(--light);
}
.grist-form .grist-submit input[type="submit"]:hover, .grist-form-container button:hover {
border-color: var(--primary-dark);
background-color: var(--primary-dark);
@ -257,7 +250,7 @@ body {
.grist-power-by {
margin-top: 24px;
color: var(--dark-text, #494949);
color: #494949;
font-size: 13px;
font-style: normal;
font-weight: 600;
@ -276,7 +269,7 @@ body {
align-items: center;
justify-content: center;
gap: 8px;
color: var(--dark-text, #494949);
color: #494949;
text-decoration: none;
}
@ -294,10 +287,11 @@ body {
.grist-question > .grist-label {
color: var(--dark, #262633);
font-size: 12px;
font-size: 13px;
font-style: normal;
font-weight: 700;
line-height: 16px; /* 145.455% */
margin-top: 8px;
margin-bottom: 8px;
display: block;
}
@ -432,3 +426,100 @@ input:checked + .grist-switch_transition > .grist-switch_circle {
-webkit-transition: .4s;
transition: .4s;
}
.grist-form-confirm-container {
padding-left: 16px;
padding-right: 16px;
}
.grist-form-confirm-body {
padding: 48px 16px 16px 16px;
}
.grist-form-confirm-image {
width: 250px;
height: 215px;
}
.grist-form-confirm-text {
font-weight: 600;
font-size: 16px;
line-height: 24px;
margin-top: 32px;
white-space: prewrap;
}
.grist-form-confirm-buttons {
display: flex;
justify-content: center;
align-items: center;
margin-top: 24px;
}
.grist-form-confirm-new-response-button {
position: relative;
outline: none;
border-style: none;
line-height: normal;
user-select: none;
display: flex;
justify-content: center;
align-items: center;
padding: 12px 24px;
min-height: 40px;
background: var(--primary, #16B378);
border-radius: 3px;
color: #FFFFFF;
}
.grist-form-confirm-new-response-button:hover {
background: var(--primary-dark);
cursor: pointer;
}
.grist-form-confirm-footer {
border-top: 1px solid var(--dark-gray);
padding: 8px 16px;
width: 100%;
}
.grist-form-confirm-footer .grist-power-by {
margin-top: 0px;
padding-top: 0px;
padding-bottom: 0px;
border-top: none;
}
.grist-form-confirm-build-form {
display: flex;
align-items: center;
justify-content: center;
margin-top: 8px;
}
.grist-form-confirm-build-form-link {
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
line-height: 16px;
text-decoration-line: underline;
color: var(--primary-dark);
}
.grist-form-icon {
position: relative;
display: inline-block;
vertical-align: middle;
-webkit-mask-repeat: no-repeat;
-webkit-mask-position: center;
-webkit-mask-size: contain;
width: 16px;
height: 16px;
background-color: black;
}
.grist-form-icon-expand {
-webkit-mask-image: var(--icon-Expand);
background-color: var(--primary-dark);
}

@ -38,13 +38,40 @@
</a>
</div>
</form>
<div class='grist-form-confirm' style='display: none'>
<div>
{{ SUCCESS_TEXT }}
<div class="grist-form-confirm-container">
<div class='grist-form-confirm' style='display: none'>
<div class="grist-form-confirm-body">
<img class='grist-form-confirm-image' src="forms/form-submitted.svg">
<div class='grist-form-confirm-text'>
{{ SUCCESS_TEXT }}
</div>
{{#if ANOTHER_RESPONSE }}
<div class='grist-form-confirm-buttons'>
<button
class='grist-form-confirm-new-response-button'
onclick='window.location.reload()'
>
Submit new response
</button>
</div>
{{/if}}
</div>
<div class='grist-form-confirm-footer'>
<div class="grist-power-by">
<a href="https://www.getgrist.com" target="_blank">
<div>Powered by</div>
<div class="grist-logo"></div>
</a>
</div>
<div class='grist-form-confirm-build-form'>
<a class='grist-form-confirm-build-form-link' href="https://www.getgrist.com/forms/" target="_blank">
Build your own form
<div class="grist-form-icon grist-form-icon-expand"></div>
</a>
</div>
</div>
</div>
{{#if ANOTHER_RESPONSE }}
<button onclick="window.location.reload()">Submit another response</button>
{{/if}}
</div>
</main>
<script>

@ -75,9 +75,12 @@ describe('FormView', function() {
assert.deepEqual(await readLabels(), ['A', 'B', 'C', 'D']);
await driver.find('.test-forms-publish').click();
await driver.find('.test-modal-confirm').click();
if (await driver.find('.test-modal-confirm').isPresent()) {
await driver.find('.test-modal-confirm').click();
}
await gu.waitForServer();
// Now open the form in external window.
await clipboard.lockAndPerform(async (cb) => {
await driver.find(`.test-forms-link`).click();
@ -387,6 +390,39 @@ describe('FormView', function() {
await removeForm();
});
it('can submit a form with a formula field', async function() {
const formUrl = await createFormWith('Text');
// Temporarily make A a formula column.
await gu.sendActions([
['AddRecord', 'Table1', null, {A: 'Foo'}],
['UpdateRecord', '_grist_Tables_column', 2, {formula: '"hello"', isFormula: true}],
]);
assert.deepEqual(await api.getTable(docId, 'Table1').then(t => t.A), ['hello']);
await gu.onNewTab(async () => {
await driver.get(formUrl);
await driver.findWait('input[name="D"]', 1000).click();
await gu.sendKeys('Hello World');
await driver.find('input[name="_A"]').click();
await gu.sendKeys('goodbye');
await driver.find('input[type="submit"]').click();
await waitForConfirm();
});
// Make sure we see the new record.
await expectInD(['', 'Hello World']);
// And check that A was not modified.
assert.deepEqual(await api.getTable(docId, 'Table1').then(t => t.A), ['hello', 'hello']);
await gu.sendActions([
['RemoveRecord', 'Table1', 1],
['UpdateRecord', '_grist_Tables_column', 2, {formula: '', isFormula: false}],
]);
await removeForm();
});
it('can unpublish forms', async function() {
const formUrl = await createFormWith('Text');
await driver.find('.test-forms-unpublish').click();
@ -395,12 +431,8 @@ describe('FormView', function() {
await gu.onNewTab(async () => {
await driver.get(formUrl);
assert.match(
await driver.findWait('.test-error-header', 2000).getText(),
/Something went wrong/
);
assert.match(
await driver.findWait('.test-error-content', 2000).getText(),
/There was an error: Form not published\./
await driver.findWait('.test-error-text', 2000).getText(),
/Oops! This form is no longer published\./
);
});
@ -414,6 +446,26 @@ describe('FormView', function() {
});
});
it('can stop showing warning when publishing or unpublishing', async function() {
// Click "Don't show again" in both modals and confirm.
await driver.find('.test-forms-unpublish').click();
await driver.find('.test-modal-dont-show-again').click();
await driver.find('.test-modal-confirm').click();
await gu.waitForServer();
await driver.find('.test-forms-publish').click();
await driver.find('.test-modal-dont-show-again').click();
await driver.find('.test-modal-confirm').click();
await gu.waitForServer();
// Check that the modals are no longer shown when publishing or unpublishing.
await driver.find('.test-forms-unpublish').click();
await gu.waitForServer();
assert.isFalse(await driver.find('.test-modal-title').isPresent());
await driver.find('.test-forms-publish').click();
await gu.waitForServer();
assert.isFalse(await driver.find('.test-modal-title').isPresent());
});
it('can create a form for a blank table', async function() {
// Add new page and select form.
@ -431,7 +483,7 @@ describe('FormView', function() {
assert.isTrue(await driver.findContent('.test-forms-submit', gu.exactMatch('Submit')).isDisplayed());
});
it('doesnt generates fields when they are added', async function() {
it("doesn't generate fields when they are added", async function() {
await gu.sendActions([
['AddVisibleColumn', 'Form', 'Choice',
{type: 'Choice', widgetOption: JSON.stringify({choices: ['A', 'B', 'C']})}],

Loading…
Cancel
Save