Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: added

Forms: add a per-form control for syncing responses to Google Sheets.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: added

Forms: add a Google Sheets lookup helper for the sync setup flow.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: added

Forms: add an endpoint to set up Google Sheets response sync.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: added

Forms: carry integration destinations in the response payload.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: changed

Forms: improve the layout of the Google Sheets sync controls.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: fixed

Forms: use the correct field labels as Google Sheets column headings.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: fixed

Forms: fix the Google Sheets sync setup not saving its result.
4 changes: 4 additions & 0 deletions projects/packages/forms/src/blocks/contact-form/attributes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ export default {
organizationId: '',
},
},
googleSheetsData: {
type: 'object',
default: {},
},
mailpoet: {
type: 'object',
default: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import apiFetch from '@wordpress/api-fetch';
import {
Button,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use the Button from @wordpress/ui

CheckboxControl,
RadioControl,
TextControl,
__experimentalVStack as VStack, // eslint-disable-line @wordpress/no-unsafe-wp-apis
} from '@wordpress/components';
Comment on lines +2 to +8

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As of the latest @wordpress/ui package version bump in Jetpack, you can now use components for all of these from the @wordpress/ui package instead.

Please also rebase onto the latest trunk; you'll see the new linter rule complain about using the old components until you update. If you must use this older component, you can include it in the linter exclusion list for now. Just lemme know!

import { useState, useCallback } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { Link, Notice } from '@wordpress/ui';

export type GoogleSheetsData = {
enabled?: boolean;
spreadsheetId?: string;
spreadsheetUrl?: string;
columns?: string[];
};

type Props = {
googleSheetsData: GoogleSheetsData;
setGoogleSheetsData: ( data: GoogleSheetsData ) => void;
formPostId: number;
formTitle: string;
};

type SetupResponse = {
spreadsheetId: string;
spreadsheetUrl: string;
columns: string[];
};

/**
* Whether a string looks like a Google Sheets document reference.
*
* Mirrors Google_Drive::extract_sheet_id() on the server, which is what actually
* decides. This is only here to keep the user from submitting something the
* server will certainly reject.
*
* @param reference - A pasted spreadsheet URL or bare ID.
* @return Whether it looks like a spreadsheet reference.
*/
export function looksLikeSpreadsheetReference( reference: string ): boolean {
const trimmed = ( reference || '' ).trim();

if ( ! trimmed ) {
return false;
}

return (
/\/spreadsheets\/d\/[a-zA-Z0-9_-]+/.test( trimmed ) || /^[a-zA-Z0-9_-]{10,}$/.test( trimmed )
);
}

/**
* Per-form controls for syncing responses to a Google Spreadsheet.
*
* Rendered only once the site has a Google Drive connection; before that the
* card shows its connect button instead.
*
* @param props - Component props.
* @param props.googleSheetsData - The form's current sync configuration.
* @param props.setGoogleSheetsData - Persists a change to that configuration.
* @param props.formPostId - The post the form is edited in, used to scope backfill and read field labels.
* @param props.formTitle - The form's title, used to name a new spreadsheet.
* @return The sync controls.
*/
export default function GoogleSheetsSyncControls( {
googleSheetsData,
setGoogleSheetsData,
formPostId,
formTitle,
}: Props ) {
const [ mode, setMode ] = useState( 'create' );
const [ spreadsheetUrl, setSpreadsheetUrl ] = useState( '' );
const [ backfill, setBackfill ] = useState( true );
const [ isSettingUp, setIsSettingUp ] = useState( false );
const [ error, setError ] = useState( '' );

const isConfigured = !! googleSheetsData?.spreadsheetId;

const handleSetup = useCallback( () => {
setError( '' );
setIsSettingUp( true );

apiFetch< SetupResponse >( {
path: '/wp/v2/feedback/integrations/google-sheets/setup',
method: 'POST',
data: {
form_post_id: formPostId,
title: formTitle,
mode,
spreadsheet_url: mode === 'existing' ? spreadsheetUrl : '',
backfill,
},
} )
.then( ( result: SetupResponse ) => {
setGoogleSheetsData( {
enabled: true,
spreadsheetId: result.spreadsheetId,
spreadsheetUrl: result.spreadsheetUrl,
columns: result.columns,
} );
} )
.catch( ( requestError: { message?: string } ) => {
setError(
requestError?.message ||
__( 'The spreadsheet could not be set up. Please try again.', 'jetpack-forms' )
);
} )
.finally( () => {
setIsSettingUp( false );
} );
}, [ formPostId, formTitle, mode, spreadsheetUrl, backfill, setGoogleSheetsData ] );

if ( isConfigured ) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once spreadsheetId is set there's no way back into the setup controls — no change spreadsheet, no refresh columns, no disconnect. The header toggle only flips enabled; it never touches columns.

Meanwhile the form stays editable. Rename "Phone" to "Mobile" and build_row() looks up the frozen "Phone", misses, and writes '' for every row from then on; the numbers are just dropped. Add a field and its values never reach the sheet at all. Delete one and there's a permanently blank column the user can't remove without hand-editing block attributes. The only recovery is deleting and re-adding the form block, which orphans the first spreadsheet.

At minimum, could we compare the stored columns against the form's current labels on render and show a "this form has changed since setup" notice with a re-run action? Even just keeping the setup controls reachable would help.

return (
<VStack spacing="3" className="jp-forms__gsheets-sync">
<p className="integration-card__description">

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can likely use Text instead of custom styles.

{ __(
'New responses to this form are added to your spreadsheet as they arrive.',
'jetpack-forms'
) }
Comment on lines +127 to +130
</p>
{ googleSheetsData.spreadsheetUrl && (
<div>
<Link openInNewTab href={ googleSheetsData.spreadsheetUrl }>
{ __( 'Open spreadsheet', 'jetpack-forms' ) }
</Link>
</div>
) }
</VStack>
);
}

const canSubmit =
! isSettingUp && ( mode === 'create' || looksLikeSpreadsheetReference( spreadsheetUrl ) );

return (
<VStack spacing="4" className="jp-forms__gsheets-sync">

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use the newer Stack instead of Vstack/HStack.

<p className="integration-card__description">
{ __(
'Send each new response to a Google Spreadsheet, as well as keeping it here.',
'jetpack-forms'
) }
</p>

<VStack spacing="3">
<RadioControl
label={ __( 'Spreadsheet', 'jetpack-forms' ) }
selected={ mode }
options={ [
{ label: __( 'Create a new spreadsheet', 'jetpack-forms' ), value: 'create' },
{ label: __( 'Use an existing spreadsheet', 'jetpack-forms' ), value: 'existing' },
] }
onChange={ setMode }
/>

{ /* Indented so it reads as belonging to the option above it rather
than as a further choice of its own. */ }
{ mode === 'existing' && (
<div className="jp-forms__gsheets-sync__suboption">
<TextControl
label={ __( 'Spreadsheet link', 'jetpack-forms' ) }
help={ __(
'Paste the link to a Google Sheet you own. Its existing content is left untouched — new responses are added below it.',
'jetpack-forms'
) }
value={ spreadsheetUrl }
onChange={ setSpreadsheetUrl }
__nextHasNoMarginBottom={ true }
__next40pxDefaultSize={ true }
/>
</div>
) }
</VStack>

{ /* Separated from the radio group: this applies whichever
spreadsheet the response goes to, so it must not read as a
third option in that group. */ }
<CheckboxControl
label={ __( 'Include responses you already have', 'jetpack-forms' ) }
help={ __(
'Copies the responses collected so far into the spreadsheet before syncing begins.',
'jetpack-forms'
) }
checked={ backfill }
onChange={ setBackfill }
__nextHasNoMarginBottom={ true }
/>

{ error && (
<Notice.Root intent="error">
<Notice.Description>{ error }</Notice.Description>
</Notice.Root>
) }

<div>
<Button
variant="primary"
onClick={ handleSetup }
disabled={ ! canSubmit }
isBusy={ isSettingUp }
__next40pxDefaultSize={ true }
>
{ __( 'Set up syncing', 'jetpack-forms' ) }
</Button>
</div>
</VStack>
);
}
Loading
Loading