Skip to content

feat(google-sheet): add ten Pro actions and fix the free row insert - #218

Open
RishadAlam wants to merge 16 commits into
mainfrom
feat/google-sheet-pro-actions
Open

feat(google-sheet): add ten Pro actions and fix the free row insert#218
RishadAlam wants to merge 16 commits into
mainfrom
feat/google-sheet-pro-actions

Conversation

@RishadAlam

Copy link
Copy Markdown
Member

Description

Adds ten Google Sheet write actions on top of the existing row insert, selected from a new action dropdown that shows only the configuration each action needs. Also fixes three bugs in the integration as it shipped: swallowed API errors, worksheet titles containing spaces, and a silently truncated spreadsheet list.

All new actions are Pro-gated and fire bit_integrations_google_sheet_* filters for the Pro plugin to answer. The existing row insert stays free and behaves exactly as before — flows saved without a mainAction still route through the original code path.

Motivation & Context

The Google Sheet action could only append a row. Everything Bit-Pi offers — updating and deleting rows, adding columns, and managing worksheets and spreadsheets — had no equivalent here, so any flow needing more than an append had nowhere to go.

Separately, the three refresh endpoints tested for an error at the wrong path ($response->response->error, while Google returns error at the top level). A 401 from an expired connection passed the success guard, ->files was read off an error body, and the user saw an empty Spreadsheets dropdown with PHP warnings in the log and no explanation in the UI.

Related Links: (if applicable)

Type of Change

  • 🐛 Bug fix
  • ✨ New feature
  • 💥 Breaking change
  • 📚 Documentation update
  • ⚡ Improvement
  • 🔄 Code refactor

Key Changes

Integrations

  • Added ten Pro actions: append or update row, update row, delete row, create column, create/copy/clear/delete worksheet, and create/delete spreadsheet
  • Kept the existing row insert free and unchanged — it is now simply the action whose requirements happen to include every section

Frontend

  • Added an action selector, Pro-gated via checkIsPro/getProLabel, as the first thing a new integration shows
  • Added per-action show/hide for every part of the config: an action that targets no worksheet never renders a worksheet select, and a row action offers no field map until its headers are known
  • Updated the field map to take its targets from the action — its own fields, the worksheet headers, or both — with required rows pre-filled and locked so they cannot be retargeted or deleted
  • Updated the destructive actions (clear/delete worksheet, delete spreadsheet) to identify their target through the field map rather than a dropdown, so a flow is no longer pinned to the one sheet picked at build time
  • Added the spreadsheet list fetch on action selection instead of waiting for the user to find the refresh button
  • Updated the utilities section to the standard TableCheckBox presentation used elsewhere

Backend

  • Added RecordApiHelper dispatch on mainAction, with one literal hook string per action and LogHandler::save on every path
  • Fixed the error handling in all three refresh endpoints, which now report Google's own message
  • Fixed worksheet titles containing spaces, which produced a malformed A1 range and an unencoded URL on the row insert
  • Fixed the spreadsheet listing, which stopped at Drive's default of 100 files; it now requests 1000, sorted by name, excluding trashed files
  • Fixed the wizard refusing to advance on Update Row, where a spare blank field-map row was permanently counted as unmapped

Code Refactor

  • Removed the duplicate ProRecordApiHelper, GoogleSheetProLayout and GoogleSheetProFieldMap, folding each back into the component it sat beside — field-map resolution, header lookup and value formatting existed twice and had to be fixed twice

Testing

Every action was exercised end to end against a live Google account: the run creates its own spreadsheet, performs each action on it, reads the sheet back to confirm the effect, then deletes the spreadsheet. The whole suite passes twice — once with a plain worksheet name and once with a name containing a space. A second harness covers the same actions with HTTP mocked, including the append-vs-update branch, a header row starting away from column A, and a missing required field.

Checklist

  • Code follows project style guidelines
  • Self-review completed
  • Tests added/updated
  • Documentation updated if needed
  • README updated if needed

Changelog

  • New Actions: Google Sheet: 10 new actions added (Pro) — append or update row, update row, delete row, create column, create/copy/clear/delete worksheet, create/delete spreadsheet.
  • Fix: Google Sheet: connection and API errors are now shown when refreshing spreadsheets, worksheets or headers, instead of an empty dropdown.
  • Fix: Google Sheet: worksheets whose name contains a space now work.
  • Improvement: Google Sheet: the spreadsheet list is no longer limited to 100 items and is sorted by name.

The three refresh endpoints tested for an error at the wrong path.
refreshSpreadsheetsAjaxHelper checked `$response->response->error`, but
Google returns `error` at the top level, so a 401 body passed the success
guard and `->files` was read off an error object: PHP warnings, a
`foreach` over null, and an empty Spreadsheets dropdown with no reason
shown to the user. The error branch dereferenced the same wrong path, so
even when it did fire it emitted another warning instead of a message.

refreshWorksheetsAjaxHelper and refreshWorksheetHeadersAjaxHelper had the
same shape problem, testing `->status === 'error'` and `->message`, which
the Sheets API never sends, and reading `->sheets` unguarded.

Route all three through hasApiError()/apiErrorMessage(), which understand
both the Sheets/Drive error object and the OAuth `error`/`error_description`
pair. Success responses carry no `error` key, so the behaviour there is
unchanged.
Adds ten write actions on top of the existing row insert: append or update
row, update row, delete row, create column, and create, copy, clear and
delete worksheet, plus create and delete spreadsheet. Ported from Bit-Pi's
GoogleSheet services; read-only operations (find spreadsheets, find
worksheet, export sheet, get rows) and the polling triggers are out of
scope.

The existing insert stays free and untouched: execute() falls through to
the original RecordApiHelper whenever mainAction is absent or 'insertRow',
so flows saved before this change keep their exact behaviour.

Everything else fires bit_integrations_google_sheet_* for the Pro plugin to
answer, and logs the outcome through LogHandler so the Task Log has a
diagnosis either way. ProRecordApiHelper resolves smart tags on the
per-action inputs and maps header-mapped values onto their column offsets
before handing them over, so Pro receives plain data and never re-reads the
flow config.

resolveTokenDetails() reuses the existing refresh and persist helpers,
giving the Pro path a valid access token without changing how the legacy
path obtains its own.
Puts an action dropdown at the top of the integration layout, Pro-gated
through checkIsPro/getProLabel. Picking anything other than the existing
row insert renders GoogleSheetProLayout; picking the insert renders the
original layout unchanged, so saved integrations look and behave exactly
as before.

GoogleSheetProLayout reuses the existing spreadsheet, worksheet and header
refresh endpoints, and renders only what each action needs: the header
field map for the row actions, a column-to-match select for append or
update, smart-tag inputs for free text and row numbers, and a keep-headers
option for clear worksheet.

isActionConfigured() replaces the hardcoded spreadsheet + worksheet +
field map check in the wizard and the edit screen, since actions such as
create spreadsheet have neither a spreadsheet nor a field map. It resolves
to the previous condition for the row insert.

handleMainAction ignores a no-op selection so a saved field map cannot be
cleared by a re-render.
Row number, column name, worksheet title, spreadsheet title and
destination spreadsheet id were smart-tag text inputs stored on the
config. That made each one a value the user types once, when they are
inputs that should come from the trigger on every run — a row number
picked at build time targets one row forever.

They are now target fields in the field map, the same shape FluentCart
uses: staticData declares the fields an action accepts, the layout offers
them as mapping targets alongside the worksheet headers where an action
uses both, and selecting an action pre-fills one row per declared field so
the required ones are visible rather than discovered.

ProRecordApiHelper resolves the whole field map once, then splits it: rows
naming one of the action's own fields become that input, the rest are
header values mapped onto their column offsets. The Pro handlers read the
same $fieldData keys as before and are unchanged.

isActionConfigured now checks that each required field is actually mapped
instead of that a config property is non-empty, and the spreadsheet and
worksheet change handlers rebuild the field map for the current action
rather than resetting it to a single blank row. The row insert resolves to
one blank row exactly as before.
A new integration now opens with only the action dropdown. Each section
below it appears once the chosen action actually calls for it and its
prerequisites are met: the spreadsheet select for actions that target one,
the worksheet select once a spreadsheet is picked, the header row and the
field map once a worksheet is picked, the column-to-match select once the
headers are known, and the utilities checkbox only for clear worksheet.
Delete spreadsheet no longer asks for a worksheet, and create spreadsheet
asks for neither.

mainAction starts empty for a new integration and is read with `??` rather
than `||`, which separates "not chosen yet" from a saved flow that predates
the field: those have no mainAction key at all and still resolve to the row
insert, so editing one is unchanged. Saving is blocked until an action is
selected.
…hared ones

GoogleSheetProLayout and GoogleSheetProFieldMap were near-copies of the
components they sat beside: the same spreadsheet and worksheet selects, the
same header row input, the same form-field/custom/smart-tag column. Keeping
two of each meant every fix had to be made twice.

There is now one layout and one field map, driven entirely by the needsX
lists in staticData. The row insert is simply the action whose lists happen
to include every section, so it renders the same controls it always did:
spreadsheet, worksheet, header row with its explanatory note, and a
header-driven field map.

The field map takes its options as a targetFields prop instead of reading
worksheet headers out of the config itself, which is what let the two
versions collapse into one — the layout decides whether an action's targets
are its own fields, the worksheet headers, or both.

Net 280 lines removed.
Removes the block comments describing what the readiness flags, the field
map generator and the field-map row resolver do, since the code says it.

Keeps four one-liners that carry something the code cannot: why the action
falls back with `??` rather than `||`, why column offsets are shifted by the
header row's column, why worksheet titles are quoted for A1 notation, and
why a Google error body has to be ruled out before reading its payload key.
Each is a change a reader would otherwise undo.
…elper

The two helpers resolved the field map with near-identical loops and both
looked up the worksheet header list, one inline and one through a method.
Every fix to either had to be repeated.

There is now one RecordApiHelper taking the flow details and dispatching on
mainAction: the row insert builds and posts the append request as before,
anything else fires its bit_integrations_google_sheet_* filter. Field-map
resolution, the header lookup and the array formatting are shared.

Array values now go through formatArrayObject on both paths, so a flat
array still joins with commas rather than turning into JSON — the Pro path
picked up the row insert's behaviour, not the other way round. Log entries
keep their existing shapes: the row insert still records type_name
"insert" and returns the raw API response, the Pro actions record the
action slug and return the response envelope.

The controller collapses to one construction and one call. Its token
handling now runs through resolveTokenDetails for both paths, and the
unreachable `empty($tokenDetails)` guard is gone — normalizeConnectionToken
always returns an object, which empty() never reports as empty.
Selecting an action left the Spreadsheets dropdown empty until the user
noticed the refresh button. It now loads on selection, for any action that
targets a spreadsheet, and only when the list is not already populated —
switching between actions reuses what was fetched, and the refresh button
still forces a reload.

The new config is built before the state update rather than inside a
setState callback, because refreshSpreadsheets writes back the conf it is
handed: passing the previous one would have landed a response that still
carried the old action.
Drops the last explanatory comments from the Google Sheet action: the
token-resolution and API-error doc blocks in the controller, the record
helper's file header, the column-offset and A1-quoting notes in the Pro
helper, and the action-change note in the layout.

The only comment left in the touched code is the `// translators:` line,
which the i18n tooling reads.
Selecting an action pre-fills a row for each field it requires. Those rows
now keep their target: the Google field select is disabled, so the user
only chooses what to map into it, and the delete button is disabled too —
otherwise the row could just be removed and the lock would mean nothing.

The "(required)" suffix on those options is gone; a disabled select already
says the target is fixed.
Clear worksheet, delete worksheet and delete spreadsheet identified their
target through the spreadsheet and worksheet dropdowns, which fixes each
flow to the one sheet the admin picked while building it. The id now comes
from the field map instead, so the target can be supplied by the trigger:
delete spreadsheet maps a Spreadsheet Id, and the two worksheet actions map
a Spreadsheet Id and a Worksheet Name.

The dropdowns no longer render for these three, so there is one place the
target can come from. Clear worksheet keeps its keep-headers utility.

Their handlers read the ids out of $fieldData, so the two delete filters
gain that argument and clear sheet now takes four: response, field data,
utilities, config.
The keep-headers option was a bare checkbox with an inline label, unlike
every other integration. It now follows the FluentCart shape: a dedicated
GoogleSheetActions component rendering a TableCheckBox, placed under a
"Utilities" heading with the usual rule above it.

The value still lands in conf.utilities.selected_first_row_headers, so the
clear-worksheet handler is unchanged.
…exercising every action

Ran all ten actions plus the row insert end to end through the free
dispatcher into the Pro handlers with outbound HTTP mocked, and reviewed
both sides. Fixes:

Next was permanently blocked on update row. generateMappedField added a
spare blank row next to the pre-filled required ones, and checkMappedFields
counts a fully blank row as an unmapped field, so the wizard refused to
advance no matter what the user mapped. It now pre-fills only the action's
own fields, falling back to a single blank row when it declares none —
matching FluentCart, and leaving the row insert with the one blank row it
always had.

A worksheet whose title contains a space produced a malformed append range
and an unencoded URL. The title is now quoted for A1 notation and the range
percent-encoded, the same way the Pro handlers already build theirs.

The spreadsheet dropdown silently stopped at Drive's default of 100 files.
The listing now asks for 1000, sorted by name, excludes trashed files, and
requests only the two fields it reads.

Also: the worksheet-headers refresh bailed only when every parameter was
missing rather than any of them; the worksheet and header refreshes could
throw on a config with no `default` object; the append falls back to ROWS
and A1 rather than emitting a notice when those are absent; and the
utilities checkbox no longer regenerates its DOM id on every render.
Copilot AI lite review requested due to automatic review settings September 5, 2026 08:44

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown

🔍 WordPress Plugin Check Report

⚠️ Status: Passed with warnings

📊 Report

🎯 Total Issues ❌ Errors ⚠️ Warnings
1 0 1

⚠️ Warnings (1)

📁 readme.txt (1 warning)
📍 Line 🔖 Check 💬 Message
0 mismatched_plugin_name Plugin name "Bit integrations - Form Integration, Webhook, Spreadsheets, CRM, LMS & Email Automation" is different from the name declared in plugin header "Bit Integrations".

🤖 Generated by WordPress Plugin Check Action • Learn more about Plugin Check

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants