Skip to content

fix(forms): stop placeholders reading as values a blank field doesn't submit - #1887

Merged
dawsontoth merged 5 commits into
stagefrom
claude/1294-placeholder-usability
Oct 12, 2026
Merged

dawsontoth merged 5 commits into
stagefrom
claude/1294-placeholder-usability

Conversation

@dawsontoth

@dawsontoth dawsontoth commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

A rabbit marking example text as an example on a stump form

Folded in

This PR now also carries the form length-limit stack, rebased in a straight line on stage, so one Rebase and merge lands all three:

Decided in review: database, table, file and application names keep their length cap, which says so when it cuts a paste. Only email and password fields have no cap.

Each of those PRs keeps its own verification and cross-model review record. On the combined tree, the full suite passes (505 files, 4,734 tests), and tsc -b, oxlint and dprint check all exit 0.

⊙ Problem

Some Fabric placeholders show a real-looking value that a blank field does not submit (#1294). Users read the field as already filled and leave it blank, or retype the grey text by hand. The worst case is OAuth Scope: a blank Scope saved an empty string, and that empty string replaces the provider's preset scope, so sign-in ran with no scope at all.

❓ Your call: every row in the table below is a UX wording call. Each row is a one-line text change except Scope, so any of them can be reverted on its own.

💡 Solution

I went through all ~70 placeholders in src and sorted each one with a single rule. A placeholder that shows a value must either be what a blank field submits, or read as an example. Instructions ("Search…", "Choose Role") and placeholders that already match what a blank field submits are left alone (see Left unchanged below).

Where Field Before After Why
Org → Settings → OAuth (add + edit) Scope placeholder openid email profile, but a blank field sent '' placeholder unchanged; a blank field now sends openid email profile Makes the placeholder true. See the evidence below
Org → Settings → OAuth (add + edit) Domain company.okta.com e.g. company.okta.com Required field that looked filled in
New cluster (self-managed) Optional Cluster Load Balancer Host Name example.your-company.com e.g. cluster.your-company.com Optional field: a blank one sets no host name, but it looked set
Applications → Import, and Redeploy Install Command npm install Leave blank to install dependencies automatically A blank field already gets Harper's automatic install. Typing npm install is not the same thing: it opts out of the automatic install. Evidence below
Schema editor → field → Index options Index type HNSW e.g. HNSW A blank type means a standard index, not HNSW, so users thought they had a vector index
API explorer → Authorize → Bearer Token eyJhbGciOi… e.g. eyJhbGciOi… A cut-off JWT looks like a token that is already stored

❓ Your call: on edit, a provider saved with an empty scope gets openid email profile written the next time anyone saves it, even if the admin only changed the domain. Today that config signs in with an empty scope, so this repairs it, but it is a write the admin didn't type.

❓ Your call: HNSW is Harper's only custom index type, so e.g. slightly implies there are others. The fuller fix the issue mentions is a dropdown (Standard / HNSW). I didn't do that because the schema editor's text fields round-trip any directive argument as-is, and a dropdown would drop unknown values.

Evidence for the behavioral claims (traced, not assumed):

  • Scope. Studio sends '' for a blank Scope. Central manager passes the stored value to @harperfast/oauth unchanged. That library's buildProviderConfig spreads it over the preset: config.ts:79-82 @ v1.6.0, the version central manager pins. v2.9.0 still keeps an explicit '' (config.ts:548-552). The authorize request then sends scope: this.config.scope || '' (OAuthProvider.ts:76 @ v1.6.0).
  • Install Command. Harper runs a custom command only when one is set (Application.ts:4402). Otherwise it uses the package manager from package.json devEngines.packageManager (:4457), and falls back to npm with --omit=dev (:4540). A redeploy with a blank command writes no install block (operations.js:793). Harper 5.0.0 and 4.7 use the same order: custom command, then devEngines, then npm.
  • Index type. CUSTOM_INDEXES holds only HNSW (customIndexes.ts:4), and a blank type returns early as a standard index (databases.ts:3482). Distance left blank is cosine (HierarchicalNavigableSmallWorld.ts:543-548), so its cosine placeholder stays.

Left unchanged

  • Already what a blank field submits: new-org Name and Subdomain, cluster Host Name and Operations API Port, Create Table's Primary Key id and Database data, Import Data's Database data, the schema editor's Database data, table/endpoint name overrides and Distance cosine, and the new application's Name.
  • Instructions, not values: search and filter boxes, Choose … selects, Your password, Leave blank to keep existing, Okta Client ID/Secret, Optional, Page.
  • Already examples: e.g. us-east-1, e.g. 100OFF, e.g. example.com, *.example.com, e.g. https, operations, wss, e.g. 280ms, medium distribution.
  • Conventional, or deliberately value-shaped: you@company.com on the auth pages reads as a template. The secret editor's ******** deliberately signals a hidden existing value, next to Reveal. The cluster instance protocol select's https:// placeholder never shows, because secure is always set. In the admin-only notification composer, the link sample lists two alternatives joined by "or", and a blank message is rejected when you submit.

Skipped (would conflict)

🔧 Changes

✅ Verification

Closes #1294
Closes #1290
Closes #1883

🤖 Generated with Claude Code

Related PRs: none found
Complexity: easy

Review-Coverage: authored=claude; ran=gemini,codex; blocked=cursor-grok(no-receipt),cursor-composer(no-receipt),domain(auth); declined=cursor-kimi,cursor-muse; rounds=1; full=1 @ 53c9df4

Review-Attention: study ~11m (critical: FieldRow.tsx; raised: degraded review) @ 53c9df4

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request improves user experience and form validation by updating several input placeholders for clarity, adding accessibility labels, and ensuring that empty or whitespace-only OAuth scope inputs default to a standard fallback value. Additionally, a new test suite has been added to verify the OAuth scope behavior under various conditions. There are no review comments, and the changes look correct and well-tested.

@github-actions

github-actions Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 76.65% 12894 / 16820
🔵 Statements 76.64% 13708 / 17884
🔵 Functions 71.73% 3434 / 4787
🔵 Branches 71.53% 10065 / 14070
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
src/components/ui/form/FormControl.tsx 100% 100% 100% 100%
src/components/ui/form/LengthLimitedInput.tsx 100% 100% 100% 100%
src/components/ui/form/measurePasteCut.ts 100% 100% 100% 100%
src/components/ui/form/useFormField.tsx 87.5% 50% 100% 87.5% 15
src/components/ui/form/useLengthLimit.tsx 100% 94.73% 100% 100%
src/features/auth/AuthLayout.css 0% 0% 0% 0%
src/features/auth/ResetPassword.tsx 80% 50% 77.77% 80% 35, 59-66
src/features/auth/SignUp.tsx 85.71% 69.23% 86.66% 87.8% 134-138
src/features/auth/components/AuthInput.tsx 100% 100% 100% 100%
src/features/clusters/upsert/fields/ClusterAbbreviatedName.tsx 100% 100% 100% 100%
src/features/clusters/upsert/fields/ClusterFQDN.tsx 0% 100% 0% 0% 18-37
src/features/clusters/upsert/fields/ClusterName.tsx 100% 100% 100% 100%
src/features/instance/apis/explorer/SettingsPanel.tsx 94.02% 82.19% 93.33% 93.93% 42, 206, 371-377, 394
src/features/instance/applications/components/NewApplication/CommonInstructions.tsx 100% 100% 100% 100%
src/features/instance/applications/components/NewApplication/ImportInstructions.tsx 60% 75% 42.85% 60% 41, 99-136, 170-180
src/features/instance/applications/components/SchemaEditorView/FieldRow.tsx 42.55% 66.66% 22.85% 38.09% 58, 60, 62, 64, 84-91, 127-268
src/features/instance/applications/modals/AddDirectoryOrFileModal.tsx 35% 4.54% 44.44% 34.21% 34-35, 67-110, 130-131
src/features/instance/applications/modals/RedeployApplicationModal.tsx 74.77% 49.31% 91.66% 74.31% 96, 119-134, 154-161, 165, 171, 189, 203-205, 214-223, 235, 323-333
src/features/instance/applications/modals/RenameFileModal.tsx 37.73% 15.15% 50% 37.25% 38, 45-46, 80-139, 144-145
src/features/instance/config/roles/modals/AddRoleModal.tsx 81.25% 85.71% 75% 81.25% 60, 72-73
src/features/instance/databases/modals/CreateNewTableModal.tsx 55% 33.33% 66.66% 55% 65-83
src/features/instance/databases/modals/ImportDataModal.tsx 54.7% 48.21% 68% 54.86% 73, 76-78, 82, 92, 210-236, 240-244, 258-269, 271-275, 277-285, 295-296, 302-307, 333-334, 434-447
src/features/organization/roles/modals/AddOrganizationRoleModal.tsx 45.16% 27.27% 33.33% 45.16% 72, 79-100, 149, 167-195
src/features/organization/settings/index.tsx 73.61% 69.76% 64.7% 75.71% 83, 121, 122, 134-138, 145-146, 151-152, 157-160, 199, 263-369, 404-405, 479-480
src/features/organizations/components/NewOrgForm.tsx 81.81% 50% 71.42% 81.81% 61-68
src/features/profile/index.tsx 60% 57.14% 75% 60% 48-64
src/features/profile/mutations/updateUserSchema.ts 100% 100% 100% 100%
src/lib/zod/password.ts 100% 100% 100% 100%
Generated in workflow #2309 for commit b8b4519 by the Vitest Coverage Report Action

dawsontoth and others added 5 commits October 12, 2026 19:21
… submit

A placeholder that shows a real-looking value reads as already filled in, so
users either leave the field blank expecting that value or retype it by hand.
Each one now either is what a blank field submits, or reads as an example:

- OAuth Scope: a blank field now submits the default its placeholder shows.
  Central manager stores the scope verbatim and it overrides the provider
  preset, so a blank Scope signed in with an empty scope.
- Install Command (import + redeploy): the "npm install" placeholder said
  what to type, but a blank field already gets Harper's automatic install,
  and typing "npm install" opts out of it. The placeholder now says so.
- Okta domain, load balancer host name, HNSW index type and the API
  explorer bearer token read as examples ("e.g. ...").

Also gives the OAuth provider card's icon-only edit button an accessible
name ("Edit provider").

Closes #1294

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ing it on submit

Text inputs whose zod schema caps a length now carry a matching `maxLength`, read from the
schema itself (`Schema.shape.<field>.maxLength`) so the attribute and the validation cannot
come apart. That covers the organization role name, the add/invite user email, the instance
role name, sign-up first/last name and email, profile first/last name, the new application
name, the add/rename file names, and the import modal's database and table names.

Password inputs are deliberately left without `maxLength`: a clamped paste would save a
password in the password manager that never worked, so they keep showing the validation
error instead.

The profile form's name limits (50/50) disagreed with central manager's `validateUpdateUser`
(first name 40, last name 80), so a 41-50 character first name passed the form and was
refused by the server, and a 51-80 character last name that sign-up accepted could never be
saved again. They now match the server and the sign-up form.

Closes #1290

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…was cut

A hard maxLength stops typing at the limit, but it also truncates a longer
paste without a word, so a password-manager or clipboard value can land cut
short with nothing on screen to say so.

Every field #1888 limits now says so under the input, in amber rather than
the red of a validation error, since the value is valid:

- "Maximum N characters reached." once the value reaches the limit, cleared
  again once it drops back under. A value that starts over the limit, such as
  a Profile name saved under the old 50-character cap, says how long it is.
- "... Pasted text was P characters; only the first K fit." (or "none of it
  fit") when a paste was cut, measured once the browser has inserted it: from
  the selection where the input exposes one, and, where type="email" hides
  it, by reading the new value as the old one with the paste spliced in, so
  text already in the field never counts as pasted. It counts what actually
  landed, which is one short of the room when the browser won't split a
  surrogate pair, and it stays until the value changes.

A schema without .max() gives zod's maxLength as null, which leaves the field
alone rather than showing "Maximum null characters".

The hint is an always-rendered aria-live="polite" region tied to the input by
aria-describedby (FormControl now appends a caller's id rather than dropping
it), and shows only the paste message after a cut paste rather than announcing
the limit first. Password fields get neither a maxLength nor a hint.

LengthLimitedInput wraps FormControl + Input for the plain fields; AuthInput
uses the same useLengthLimit hook, so sign-up's names and email get it too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d fields hit their limit

A cut email address can still be a valid one: an 81-character
...@example.com pasted into the organization invite or sign-up became
...@example.co, passed z.email(), and the invite would have gone to that
mailbox. So the two email fields now work like the password fields: no
maxLength and no length hint, and the schema's .max(80) refuses an over-long
address on submit ("Email cannot be longer than 80 characters.").

That leaves no capped field with type="email", the one input type that hides
its selection, so measurePasteCut keeps only the selection-known path. The
hook makes no paste claim when the selection is hidden.

NewOrgForm (name, subdomain), ClusterName, ClusterAbbreviatedName and
CreateNewTableModal (table name, primary key, database name) already had a
maxLength that cut silently. They now go through LengthLimitedInput, so they
say when the limit is reached and when a paste was cut, like the rest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reset Password refused anything over 50 characters, while sign-up and Profile set no limit at
all. None of those matched central manager: its only password cap is
`VALIDATION_CONSTRAINTS.USER.PASSWORD_MAX_LENGTH` (256), applied by `validateAddUser` to sign-up,
while `validateResetPassword` and `validateUpdateUser` take any length. So a password manager's
64-character password worked for sign-up and Profile but could not be reset to, and a sign-up
password over 256 passed the form only to be refused by the server.

All three forms now cap the password at one shared `ACCOUNT_PASSWORD_MAX_LENGTH` (256) with the
same message. Password inputs still have no `maxLength`, so an over-long paste is shown in full
with the error rather than silently clamped.

Closes #1883

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dawsontoth
dawsontoth force-pushed the claude/1294-placeholder-usability branch from 53c9df4 to 413bf83 Compare October 12, 2026 23:23
@dawsontoth
dawsontoth marked this pull request as ready for review October 12, 2026 23:23
@dawsontoth
dawsontoth requested a review from a team as a code owner October 12, 2026 23:23
@dawsontoth
dawsontoth merged commit 259f3fe into stage Oct 12, 2026
12 checks passed
@dawsontoth
dawsontoth deleted the claude/1294-placeholder-usability branch October 12, 2026 23:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant