Skip to content

feat(clickwhale): add ClickWhale link integration - #199

Open
RishadAlam wants to merge 3 commits into
mainfrom
feat/clickwhale
Open

feat(clickwhale): add ClickWhale link integration#199
RishadAlam wants to merge 3 commits into
mainfrom
feat/clickwhale

Conversation

@RishadAlam

@RishadAlam RishadAlam commented Jul 19, 2026

Copy link
Copy Markdown
Member

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 getLinkDetails is deliberately not ported: it is a lookup, not an
action.

Related Links: (if applicable)

  • Pro counterpart: Bit-Apps-Pro/bit-integrations-pro#137 — neither half is functional alone, so the two should merge together

Type of Change

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

Key Changes

Backend

  • Added ClickWhaleController with an activation check, an authorize
    endpoint, and an author dropdown endpoint
  • Added RecordApiHelper dispatching the three actions to their Pro hooks,
    logging every path through LogHandler::save
  • Added ClickWhale to AllTriggersName and customFormIntegrations

Frontend

  • Added the ClickWhale integration UI — authorization, field map, integration
    layout, Utilities section, create and edit wizards
  • Added a fetched Author dropdown. The owner is a per-flow choice, not a
    per-run value, so it belongs in a select rather than the field map
  • Added Utilities options for redirect type, nofollow and sponsored, which
    bit-pi either hardcoded or ignored

Field mapping

  • Links are identified by link_id, not slug: slug has no unique index and
    ClickWhale 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 flow

Checklist

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

Notes for reviewers

  • Not runtime-tested. No link was created, updated or deleted against a live
    install; verification was static (php -l, php-cs-fixer, ESLint, Prettier) plus
    hook-argument and field-key parity checks.
  • 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.
  • The Author select is keyed on its option count. react-multiple-select-dropdown-lite
    matches defaultValue against options in an effect keyed on defaultValue alone,
    so without remounting a saved author renders blank when editing.

Changelog

  • New Actions
  • ClickWhale: 3 new actions added (Pro).
  • New Triggers
  • ClickWhale: 4 new events added (Pro).

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.
Copilot AI review requested due to automatic review settings July 19, 2026 06:55

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.

@gemini-code-assist gemini-code-assist Bot 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.

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.

Comment on lines +49 to +54
useEffect(() => {
if (listsForAction(mainAction).length > 0) {
refreshClickWhaleAuthors(setLists, setIsLoading)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [mainAction])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

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
  1. 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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

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
  1. 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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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.

Comment on lines +28 to +30
setTimeout(() => {
document.getElementById('btcd-settings-wrp').scrollTop = 0
}, 300)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

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)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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.

@github-actions

Copy link
Copy Markdown

✅ WordPress Plugin Check Report

✅ Status: Passed

📊 Report

All checks passed! No errors or warnings found.


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

Copilot AI review requested due to automatic review settings July 19, 2026 07:04

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.

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