feat(clickwhale): add ClickWhale link integration - #199
Conversation
Ports the ClickWhale write actions from Bit-Pi: create, update and delete link. The free side resolves the field map and fires bit_integrations_clickwhale_*; all table writes live in the Pro plugin. Bit-Pi's getLinkDetails is not ported — it is a lookup rather than an action. Links are identified by link_id rather than slug. Slug has no unique index and ClickWhale performs no uniqueness check on save, so a slug can match several rows; all four triggers emit link_id, so the id is already available to any flow that needs it. There are no fetched dropdowns here — every input is free text, the id of the link being acted on, or a fixed option set — so no refresh route was added.
The link owner is a per-flow config choice backed by a fetchable list, so it moves from the field map to a selectedAuthor dropdown, fed by a new refresh_clickwhale_authors endpoint. create_link now also passes the flow config to its Pro handler, so the hook takes four arguments rather than three. Authors are not filtered by capability: ClickWhale registers its own admin menus under 'read', so any logged-in user is a legitimate owner and narrowing the list would hide valid choices. Only ID, display_name and user_login are selected, so the payload stays small without a silent cap. The select is keyed on the option count. MultiSelect matches defaultValue against its options in an effect keyed on defaultValue alone, and the authors arrive after mount, so without remounting a saved author would render blank on the edit screen while conf still held the id. Also drops validateClickWhaleConf, which only restated a check checkMappedFields already performs, and both call sites with it.
There was a problem hiding this comment.
Code Review
This pull request introduces a new integration for ClickWhale, allowing users to create, update, and delete links. It includes backend controllers, routing, and API helpers, alongside frontend React components for authorization, field mapping, and configuration. The review feedback highlights three key areas for improvement: first, ensuring that non-persisted configuration fields like clickWhaleFields are rebuilt on mount when editing to prevent rendering issues; second, declaring the refreshAuthors controller method as static since it does not access instance state; and third, adding a defensive guard check when accessing the DOM element's scrollTop property to avoid potential runtime crashes.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| useEffect(() => { | ||
| if (listsForAction(mainAction).length > 0) { | ||
| refreshClickWhaleAuthors(setLists, setIsLoading) | ||
| } | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| }, [mainAction]) |
There was a problem hiding this comment.
When editing an integration, the static configuration field clickWhaleFields is not persisted in the database and needs to be rebuilt on mount. Without doing so, the field mapping section will not render, preventing users from viewing or editing their mapped fields. Rebuild clickWhaleFields inside the useEffect hook on mount/action change.
useEffect(() => {
if (mainAction) {
setClickWhaleConf(prevConf =>
create(prevConf, draftConf => {
if (!draftConf.clickWhaleFields) {
draftConf.clickWhaleFields = fieldsByAction[mainAction] || []
}
})
)
}
if (listsForAction(mainAction).length > 0) {
refreshClickWhaleAuthors(setLists, setIsLoading)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [mainAction])References
- When editing an integration, ensure that non-persisted configuration fields (like static fields) are rebuilt on mount from the saved action without modifying the saved field mappings, to prevent empty options and component crashes.
There was a problem hiding this comment.
Not applying — the premise does not hold here.
clickWhaleFields is persisted. It is written into conf when the action is chosen:
draftConf.clickWhaleFields = fieldsByAction[value] || []conf is serialized into btcbi_flow.flow_details on save, so the edit screen reads it back from the database and the field-map section renders. The render site is also guarded — {mainAction && clickWhaleConf.clickWhaleFields && ( … )} — so there is no crash path either.
The suggested guard only fires when the value is missing, which in practice does not happen.
The real issue nearby is staleness rather than absence: because the definitions are persisted, a release that changes the field list leaves flows saved before it holding the old copy — and the proposed check would not repair those, since the array is non-empty. Deriving the fields from mainAction at render time would fix that, but it changes the saved config shape across the whole component family, so it belongs in its own change.
| * would hide valid choices. Only the three display fields are selected, so the | ||
| * payload stays small even on a site with many users. | ||
| */ | ||
| public function refreshAuthors() |
There was a problem hiding this comment.
The refreshAuthors method is registered as a static route callback and does not access any instance state ($this). It should be explicitly declared as static to maintain consistency and adhere to coding standards.
public static function refreshAuthors()References
- In PHP, controller methods that do not access instance state ($this) and are registered as static route callbacks should be explicitly declared as static to maintain consistency and adhere to coding standards.
There was a problem hiding this comment.
Not applying — style preference rather than a defect, and consistent with the same call on #196, #197 and #198.
Route::action() dispatches through reflection and supports both forms explicitly:
$response = $reflectionMethod->invoke($reflectionMethod->isStatic() ? null : new $invokeable[0](), $data);A non-static callback is instantiated and invoked correctly. There are 30 non-static refresh* methods across backend/Actions/ today, and this controller follows FluentCartController, whose refresh methods are also non-static.
Static is the wider majority (64 vs 30), so it is worth standardising — but as one sweep across the directory rather than on whichever integration is newest, which would otherwise leave BadgeOS, LatePoint, ProfilePress and ClickWhale inconsistent with each other.
| setTimeout(() => { | ||
| document.getElementById('btcd-settings-wrp').scrollTop = 0 | ||
| }, 300) |
There was a problem hiding this comment.
Directly accessing scrollTop on the result of document.getElementById without a guard check can cause a runtime crash if the element is not found in the DOM. Add a defensive guard check before accessing the property.
setTimeout(() => {
const settingsWrp = document.getElementById('btcd-settings-wrp')
if (settingsWrp) {
settingsWrp.scrollTop = 0
}
}, 300)There was a problem hiding this comment.
Agreed — applied. The call fires 300ms after the click, so navigating away in that window unmounts the wrapper and the stray timer throws on a null deref.
setTimeout(() => {
const settingsWrp = document.getElementById('btcd-settings-wrp')
if (settingsWrp) {
settingsWrp.scrollTop = 0
}
}, 300)Worth flagging for a follow-up: the unguarded form appears in 268 files under AllIntegrations, with only 4 already guarding it — it gets copied along with each new integration. Fixing it repo-wide belongs in its own sweep rather than here.
✅ WordPress Plugin Check Report
📊 ReportAll checks passed! No errors or warnings found. 🤖 Generated by WordPress Plugin Check Action • Learn more about Plugin Check |
…ce before scrolling
Description
Adds ClickWhale as an action integration and registers it as a trigger source.
Three write actions — Create, Update and Delete Link — plus the UI for
configuring them.
The free plugin resolves the field map and fires
bit_integrations_clickwhale_*;all table writes live in bit-integrations-pro.
Motivation & Context
ClickWhale is a link manager and shortener that Bit-Pi already integrates with,
but Bit Integrations had no support for it. This closes that gap so link flows —
create a tracked link when a post publishes, retire one when a campaign ends —
can be built here.
Bit-Pi's
getLinkDetailsis deliberately not ported: it is a lookup, not anaction.
Related Links: (if applicable)
Type of Change
Key Changes
Backend
ClickWhaleControllerwith an activation check, an authorizeendpoint, and an author dropdown endpoint
RecordApiHelperdispatching the three actions to their Pro hooks,logging every path through
LogHandler::saveClickWhaletoAllTriggersNameandcustomFormIntegrationsFrontend
layout, Utilities section, create and edit wizards
per-run value, so it belongs in a select rather than the field map
bit-pi either hardcoded or ignored
Field mapping
link_id, not slug: slug has no unique index andClickWhale performs no uniqueness check, so a slug can match several rows. All
four triggers emit
link_id, so the id is already available to any flowChecklist
Notes for reviewers
install; verification was static (
php -l, php-cs-fixer, ESLint, Prettier) plushook-argument and field-key parity checks.
menus under
read, so any logged-in user is a legitimate owner and narrowing thelist would hide valid choices.
react-multiple-select-dropdown-litematches
defaultValueagainst options in an effect keyed ondefaultValuealone,so without remounting a saved author renders blank when editing.
Changelog