Skip to content

B3 — DataForm settings UI#5

Merged
manzoorwanijk merged 10 commits into
trunkfrom
rsm-3024-dataform-settings
May 15, 2026
Merged

B3 — DataForm settings UI#5
manzoorwanijk merged 10 commits into
trunkfrom
rsm-3024-dataform-settings

Conversation

@manzoorwanijk
Copy link
Copy Markdown
Member

@manzoorwanijk manzoorwanijk commented May 14, 2026

Completes RSM-3024

Summary

Builds the React/DataForm settings UI for the plugin (RSM-3024).

  • DataForm-driven form for the telegram_auth_settings option, grouped into four card sections: bot credentials, new user accounts, optional permissions, sign-in button. Renders synchronously from a window.telegramAuthData global Bootstrap injects at page render — no fetch on mount. Save batches only the dirty fields to /wp/v2/settings.
  • BotFather setup walkthrough rendered above the form, with copyable Redirect URI + Trusted Origin chips so admins can paste them straight into BotFather's mini app.
  • Constant pinning: credential fields are auto-disabled with a "Managed via TELEGRAM_AUTH_CLIENT_ID constant in PHP" affordance when the wp-config constants are defined. Source flags ride along in the inline payload.
  • Bot-token-shape warning: if a value matching the Bot API token format gets pasted into Client Secret, an inline warning links back to the instructions above.
  • Settings::get_all() returns the effective settings with constant overrides applied, so the inline payload is the single source of truth on first render.
  • Sanitize merges over the stored option rather than defaults, so partial saves keep untouched fields.

Look & feel

  • Content capped at 880px max-width with a subtitle under the title.
  • Save button anchored bottom-start; save success/error notices render in a SnackbarList fixed bottom-end so the button doesn't shift when a notice appears.
  • All inline-axis CSS uses RTL-aware logical properties (inset-inline-end, padding-inline-start, padding-inline, padding-block).
  • Styles live in a route-level style.scss that wp-build's route bundler inlines as a runtime style tag.

Test plan

  • npm run build
  • Settings page loads at Settings → Telegram Auth without console errors.
  • All fields show current option values; toggling, editing, and saving persists round-trip.
  • With TELEGRAM_AUTH_CLIENT_ID / TELEGRAM_AUTH_CLIENT_SECRET defined in wp-config, both credential fields are disabled with the "Managed via … constant in PHP" affordance.
  • Pasting a Bot API token (e.g. 123456789:ABC…) into Client Secret shows the inline warning.
  • Copy buttons next to Redirect URI / Trusted Origin copy to clipboard.
  • Saving with bad data surfaces a Snackbar error; saving cleanly surfaces a Snackbar success — both at bottom-end, neither shifts the Save button.
  • CI green: composer phpcs, composer phpunit, npm run lint:js, npm run lint:css, npm run typecheck, npm test, npm run build.

🤖 Generated with Claude Code

telegram-auth jurassic tube_wp-admin_options-general php_page=telegram-auth-wp-admin telegram-auth jurassic tube_wp-admin_options-general php_page=telegram-auth-wp-admin (1)

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 14, 2026

🔍 WordPress Plugin Check Report

⚠️ Status: Passed with warnings

📊 Report

🎯 Total Issues ❌ Errors ⚠️ Warnings
24 0 24

⚠️ Warnings (24)

📁 composer.json (1 warning)
📍 Line 🔖 Check 💬 Message
0 missing_composer_json_file The "/vendor" directory using composer exists, but "composer.json" file is missing.
📁 build/routes.php (12 warnings)
📍 Line 🔖 Check 💬 Message
14 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound Global variables defined by a theme/plugin should start with the theme/plugin prefix. Found: "$routes_file".
19 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound Global variables defined by a theme/plugin should start with the theme/plugin prefix. Found: "$routes".
22 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound Global variables defined by a theme/plugin should start with the theme/plugin prefix. Found: "$routes_by_page".
23 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound Global variables defined by a theme/plugin should start with the theme/plugin prefix. Found: "$route".
24 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound Global variables defined by a theme/plugin should start with the theme/plugin prefix. Found: "$page_slug".
26 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound Global variables defined by a theme/plugin should start with the theme/plugin prefix. Found: "$routes_by_page".
28 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound Global variables defined by a theme/plugin should start with the theme/plugin prefix. Found: "$routes_by_page".
32 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound Global variables defined by a theme/plugin should start with the theme/plugin prefix. Found: "$page_slug".
32 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound Global variables defined by a theme/plugin should start with the theme/plugin prefix. Found: "$page_routes".
33 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound Global variables defined by a theme/plugin should start with the theme/plugin prefix. Found: "$page_slug_underscore".
34 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound Global variables defined by a theme/plugin should start with the theme/plugin prefix. Found: "$global_name".
35 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound Global variables defined by a theme/plugin should start with the theme/plugin prefix. Found: "$$global_name".
📁 build/build.php (5 warnings)
📍 Line 🔖 Check 💬 Message
13 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound Global variables defined by a theme/plugin should start with the theme/plugin prefix. Found: "$modules_file".
19 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound Global variables defined by a theme/plugin should start with the theme/plugin prefix. Found: "$scripts_file".
25 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound Global variables defined by a theme/plugin should start with the theme/plugin prefix. Found: "$styles_file".
31 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound Global variables defined by a theme/plugin should start with the theme/plugin prefix. Found: "$routes_file".
37 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound Global variables defined by a theme/plugin should start with the theme/plugin prefix. Found: "$pages_file".
📁 build/pages/telegram-auth/page-wp-admin.php (1 warning)
📍 Line 🔖 Check 💬 Message
141 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound Hook names invoked by a theme/plugin should start with the theme/plugin prefix. Found: "telegram-auth-wp-admin_init".
📁 build/pages/telegram-auth/page.php (5 warnings)
📍 Line 🔖 Check 💬 Message
141 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound Hook names invoked by a theme/plugin should start with the theme/plugin prefix. Found: "telegram-auth_init".
263 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound Hook names invoked by a theme/plugin should start with the theme/plugin prefix. Found: "admin_head-{$hook_suffix}".
270 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound Hook names invoked by a theme/plugin should start with the theme/plugin prefix. Found: "admin_head".
284 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound Hook names invoked by a theme/plugin should start with the theme/plugin prefix. Found: "admin_footer".
298 WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound Hook names invoked by a theme/plugin should start with the theme/plugin prefix. Found: "admin_footer-{$hook_suffix}".

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

manzoorwanijk added a commit that referenced this pull request May 14, 2026
- Preserve constant-pinned credential values across save:
  preserveConstants() re-overlays the original (constant-resolved)
  client_id / client_secret on the REST response, which only returns
  stored values. Without this, a save flips the disabled credential
  field from the constant value to the (empty) DB value until reload.
- Include the port in the site origin emitted via window.telegramAuthData
  so Trusted Origins on non-default-port setups (e.g. wp-env's
  :8888) match what Telegram expects.
- Drop the dead Snack.status field — it was set but never threaded
  through to SnackbarList.
- Flatten the "Get it from @Botfather. Read the instructions above."
  field description into a single translatable string.
- Switch the save-error snack to sprintf with a single template, so
  translators get one string instead of a colon-prefix fragment.
- Export diff / preserveConstants / errorMessage and add Jest unit
  tests covering them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@manzoorwanijk manzoorwanijk force-pushed the rsm-3024-dataform-settings branch from 3d7875b to 584890f Compare May 14, 2026 12:52
Copy link
Copy Markdown
Contributor

@gmjuhasz gmjuhasz left a comment

Choose a reason for hiding this comment

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

This works well, just had some questions/suggestions

Comment thread routes/telegram-auth/fields.tsx
Comment thread routes/telegram-auth/instructions.tsx
manzoorwanijk and others added 10 commits May 15, 2026 17:26
A partial REST PATCH from the settings UI should be able to omit a
field without blanking the stored value. Merging the incoming payload
over defaults — the previous behavior — meant every save had to
re-send every field, including the client secret, or it would be
silently overwritten.

Merge over the currently stored option instead (falling back to
defaults when nothing is stored yet). A field omitted from the
payload keeps its existing value; an explicit `""` still clears the
field.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the placeholder route at routes/telegram-auth/ with a working
settings form. The plugin's `register_setting()` already exposes the
option through WP core's `/wp/v2/settings`, so saves go through that —
no parallel `/telegram-auth/v1/settings` controller.

Initial state comes from a `window.telegramAuthData` global Bootstrap
inlines at page render. The settings UI reads it synchronously at mount;
there's no fetch on load, no spinner, no race window. Save still uses
apiFetch since that's a real round-trip, and the response carries the
sanitized values back so we can keep the diff baseline accurate.

Pieces:

- `routes/telegram-auth/types.ts` — TelegramAuthSettings, SettingsMeta,
  TelegramAuthData, WpSettingsResponse, and the `window.telegramAuthData`
  global declaration.
- `routes/telegram-auth/fields.tsx` — DataForm field set + form layout +
  the BOT_TOKEN_SHAPE regex. `buildFields(meta)` toggles `isDisabled` on
  the credential fields when their source is a wp-config constant.
- `routes/telegram-auth/settings-app.tsx` — outer guard against missing
  data global, inner component holds `original` / `draft` state, diffs
  on every render to drive the Save button, surfaces save/error notices,
  and shows an inline warning when the entered client_secret matches the
  Bot API token shape (common BotFather mix-up).
- `routes/telegram-auth/stage.tsx` — page chrome that hosts SettingsApp.
- `src/Admin/class-settings.php` — splits the source-resolver helper
  out so `get_client_id_source` (new) and `get_client_secret_source`
  (renamed from `get_secret_source` for symmetry) share it. Adds
  `get_all()` returning the full effective settings array with
  constant overrides applied, so Bootstrap can hand the UI a single
  source of truth. Drops `'require'` from the email-mode enum —
  `allow_signups=false` already covers "disable sign-up entirely".
- `src/class-bootstrap.php` — `admin_print_scripts-<page>` hook emits
  the inline data global. wp_json_encode with HEX flags so the inline
  script is safe regardless of where it lands in the page.

Tests: PHPUnit gains coverage for `get_all` (defaults / stored / constant
overrides), `get_client_id_source`, and `get_client_secret_source`. Jest
smoke covers the fields builder (declaration order, isDisabled toggling
per source, BOT_TOKEN_SHAPE match/no-match).

Test runner adds a routes/**/__tests__/ glob, picks up `react` as a
direct devDep so Jest can resolve `react/jsx-runtime` under the
project's `install-strategy=linked` npm setup, and silences ts-jest's
node10-moduleResolution deprecation warning (ts-jest forces
module=commonjs internally for the Jest runtime).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Walks admins through the OIDC setup with copy-able Redirect URI
and Trusted Origin values, so the existing bot-token-shape warning
("Read the instructions above") has something concrete to point at.
The two values are computed server-side and shipped in the inline
window.telegramAuthData payload.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Splits the DataForm into four card sections (credentials, accounts,
permissions, presentation) using the DataForm card layout, wraps the
instructions block in its own Card, and adds a subtitle plus a
content-width cap so the page no longer reads as a single wall of
fields.

Moves styling out of inline style props into a route-level SCSS
stylesheet picked up by wp-build's route bundler. All inline-axis
properties use RTL-aware logical properties (inset-inline-end,
padding-inline-start, padding-inline) so the page works in RTL
locales unchanged.

Save success/error notices now render in a SnackbarList anchored
fixed bottom-end, so the Save button (now bottom-start) no longer
shifts when a notice appears.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Preserve constant-pinned credential values across save:
  preserveConstants() re-overlays the original (constant-resolved)
  client_id / client_secret on the REST response, which only returns
  stored values. Without this, a save flips the disabled credential
  field from the constant value to the (empty) DB value until reload.
- Include the port in the site origin emitted via window.telegramAuthData
  so Trusted Origins on non-default-port setups (e.g. wp-env's
  :8888) match what Telegram expects.
- Drop the dead Snack.status field — it was set but never threaded
  through to SnackbarList.
- Flatten the "Get it from @Botfather. Read the instructions above."
  field description into a single translatable string.
- Switch the save-error snack to sprintf with a single template, so
  translators get one string instead of a colon-prefix fragment.
- Export diff / preserveConstants / errorMessage and add Jest unit
  tests covering them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses review feedback on #5 (r3246650016): the OIDC client secret
shouldn't be readable in window.telegramAuthData, the GET
/wp/v2/settings response, or the POST-save echo response, and a save
with a blank input or a constant-pinned credential shouldn't be able
to overwrite the stored value.

Read surfaces:
- Settings::get_all() always blanks client_secret regardless of source.
- rest_request_after_callbacks filter blanks client_secret on every
  /wp/v2/settings response (handles both the raw-array shape the
  settings controller emits and a wrapped WP_REST_Response).
- Client Secret field gets a 54-asterisk placeholder so the empty
  input reads as "something is stored, leave blank to keep".

Write guards in Settings::sanitize:
- Drop client_secret from the input when it's an empty string —
  "leave blank to keep" instead of "clear the stored secret".
- Drop client_id / client_secret from the input when the matching
  wp-config constant is defined — managed-elsewhere values must
  never be persisted to the option row.

The auth pipeline still reads the real secret via
Settings::get_client_secret() (server-side), so only the UI surfaces
are redacted.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Since the secret is redacted on render, leaving the input editable
silently invites admins to overwrite the stored value with their
own keystrokes. Render the field disabled by default with an "Edit"
suffix; clicking Edit enables the input, focuses it, and swaps the
suffix for the same eye show/hide toggle DataForm's built-in
password control uses (seen/unseen from @wordpress/icons).

Also sets autoComplete="new-password" + spellCheck=false so browsers
don't autofill or suggest corrections, and updates the non-constant
description copy to "Redacted for security. Leave blank to keep the
stored value, or click Edit to set a new one."

Reorganizes package.json so runtime @wordpress/* and react land
under dependencies rather than devDependencies; @wordpress/icons
added for the eye toggle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…anagers

After a successful save, bump a savedTick counter that's wired to
DataForm's `key` so the form remounts. ClientSecretEdit's internal
`editable` state resets to false, restoring the disabled-with-Edit
suffix — admins don't see a mysteriously-editable field hanging
around after the value has already been stored.

Switch autoComplete to "off" and add the well-known per-password-
manager opt-out attributes (data-1p-ignore, data-lpignore,
data-bwignore, data-form-type="other") so 1Password / LastPass /
Bitwarden stop offering autofill on the secret input. Password
managers ignore autoComplete="off" on type=password by default,
so the data attributes are what actually does the work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Swaps the Card wrapper for Panel + PanelBody so the BotFather setup
walkthrough collapses out of the way once the bot is configured.
PanelBody's initialOpen drives the default: open when client_id is
empty (fresh install needing setup), closed when it's already
populated (admin coming back to tweak unrelated fields).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@manzoorwanijk manzoorwanijk force-pushed the rsm-3024-dataform-settings branch from 584890f to de5450f Compare May 15, 2026 12:03
Copy link
Copy Markdown
Contributor

@gmjuhasz gmjuhasz left a comment

Choose a reason for hiding this comment

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

Looks and works great now, thanks.

Image

@manzoorwanijk manzoorwanijk merged commit 1e36c4e into trunk May 15, 2026
5 checks passed
@manzoorwanijk manzoorwanijk deleted the rsm-3024-dataform-settings branch May 15, 2026 12:24
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