Skip to content

feat(tower): UX improvements across pages and modals - #95

Merged
mrtaikandi merged 27 commits into
mainfrom
ux-improvements
May 9, 2026
Merged

feat(tower): UX improvements across pages and modals#95
mrtaikandi merged 27 commits into
mainfrom
ux-improvements

Conversation

@mrtaikandi

@mrtaikandi mrtaikandi commented May 9, 2026

Copy link
Copy Markdown
Owner

Summary

Iterates on Tower UX across most surfaces. Highlights:

  • Pages redesigned — Projects, Clients, Scopes, Variables, and Templates moved to a consistent stacked-card / divided-list layout with shared filter chrome.
  • Variables UX split — global Variables now lives on its own page, project-scoped variables live on a per-project tab, persisted via URL search params so the active tab/tier survives a refresh.
  • Sensitive-value reveal flow — both VariableEditorModal and EntryModal now mask *** values, gate every editable field, and require an explicit "Reveal Sensitive Values" round-trip before save. While masked, the form (including type and description on entries) is fully read-only.
  • Shared form pieces — extracted FilterButton, SearchFilterPopover, RevealButton, and the multi-row ScopedValuesField so config entries and variables share one editor surface.
  • Smaller fixes — Activity row alignment on the project Overview page; scoped-value Selects display correctly on first paint, including for sensitive entries that haven't been revealed.
  • APIUpdateClient now accepts and validates a scope-context update so the Edit Client dialog can mutate scopes; added handler tests.
  • Docs — new docs/guide/variables.md reference page and a Concepts cross-link.

mrtaikandi added 27 commits May 9, 2026 12:26
- Switch default button radius from rounded-full (pill) to rounded-lg
  (6px) for the Button component and four custom-styled call sites
- Update radius token descriptions: lg is now the canonical button
  radius; pill is reserved for chips, badges, dots, avatars, and
  segmented controls
- Convert the Projects page Filter button to an icon-only circle with
  a corner count badge
- Convert the single project Edit button to an icon-only circle
- Add a Plus icon to the New project button
- Pair del/add hunks into rows with placeholders so left/right line up
  index-for-index instead of relying on independent filter passes
- Wrap long lines (whitespace-pre-wrap break-all) and let the parent
  scroll container handle vertical overflow, so the bounce effect
  applies to both columns at once instead of only the active side
- Drop the inner panel borders and per-column padding in favor of a
  single divide-x rule between sides
- Move overflow-y-auto out of JsonDiff onto callers that actually want
  internal scrolling; PublishModal opts in via its className. On the
  snapshot page sticky now walks up to the page scroll container so
  the Before/After headers stay pinned while reviewing the diff
- FilterButton: icon-only circular trigger with optional active-count badge
- SearchFilterPopover: generic search-by-text filter popover that uses
  FilterButton as the trigger
- Refactor ProjectsFilterPopover to delegate to SearchFilterPopover so
  the existing tests and call sites keep working without changes
- Add optional Scopes property to UpdateClientRequest; when provided,
  it replaces the existing scope context
- New UpdateClientValidator validates dimensions and values against the
  configured scopes (mirrors CreateClient validation)
- UpdateClientHandler applies scope changes by mutating the existing
  dictionary in place and emits one audit FieldChange per dimension
  that was added, removed, or had its value changed
- Add tests for the happy path and the unknown-dimension rejection
- Regenerate OpenApi.json and Tower TypeScript types
- Move projectId from useCreateClient hook param into the mutation body
  so the same hook serves both per-project and global Create flows
- Add useUpdateClient mutation hook (with conflict handling)
- NewClientModal accepts an optional projectId; on the global Clients
  page where it is omitted, the dialog renders a project picker and
  validates the selection
- New EditClientModal: edits the client name and scope context;
  the owning project is shown read-only since clients cannot move
  between projects
- Clients route header now shows the SearchFilterPopover next to a
  New client button, matching the Projects page layout
- Replace the Manage column action with Edit and Open-project icon
  buttons; clicking the client name also opens the edit dialog
- Open-project links to the project's Clients tab and uses a native
  title tooltip to describe the destination
…ine delete

- Header actions: SearchFilterPopover next to the New scope button
  (Plus icon, matching the Projects page pattern); search filters by
  dimension or description
- Card layout flattened to a stacked design: dimension name (mono) and
  Edit button on the top row, description below, allowed-value pills
  underneath
- Allowed values render as bordered monospace tags both on the cards
  and inside the edit modal so the visual language stays consistent;
  modal pills are clickable buttons with an inline X to remove
- Delete moved into the edit modal as a destructive action with a
  type-to-confirm AlertDialog (mirrors the EditClientModal pattern);
  removed the standalone DeleteScopeDialog now that the flow lives
  inside the edit dialog
…pages

- Replace per-item bordered cards with a single bordered container of
  divided rows, mirroring the Projects page layout
- Each row is a role=button with click and Enter/Space handlers that
  open the edit dialog directly; per-row Edit buttons are gone
- Clients row: name in <project> [active] on top, scope tags below,
  Created/Updated/Expires (when set) in a small caption row; right
  column shows relative Last used time and the navigate-to-project
  icon button (with stopPropagation so clicking it doesn't trigger
  the row's edit handler)
- Scopes row: dimension on top, allowed-value tags directly below,
  description last; tags now use the same bg-bg-selected /
  text-fg-on-selected colours as the client scope tags for visual
  consistency, and the editable remove-pills inside the edit modal
  follow the same scheme
Adopts the projects-style divided list pattern on the Variables page and
introduces a shared RevealButton extracted from EntryValue. Each row now
shows the variable name, sensitive chip, owner pill (green 'GLOBAL' for
ungrouped/unscoped vars), description, and a config-style value panel
that supports masking, on-demand decryption, and copy. The edit modal
gains a reveal button on the value input and decrypts automatically when
unchecking 'Sensitive value' so the user never sees the literal '***'.
Consolidates variable structure, ownership tiers, two-tier resolution,
group/system-wide visibility, sensitivity, uniqueness rules, sharp
edges, and worked examples into a single guide. Cross-links from the
concepts page.
Splits the Templates route into a list page and per-template detail page
following the same divided-list pattern used by Clients/Scopes/Variables,
and reuses the project Configuration tab's flat/tree/JSON views to manage
template entries.

Generalises ConfigFlatView, ConfigTreeView and ConfigJsonView to accept a
ConfigOwner ({ kind: 'project' | 'template'; id }) instead of a projectId.
useOwnedEntries dispatches to useEffectiveEntries for projects (with
inheritance/overrides) and to a flat ConfigEntry list for templates with
the new 'template-self' source kind. JSON view drives off snapshot preview
for projects and raw entries for templates; entriesToResolvedDocument
accepts the broader ResolvableEntry shape so the same builder works for
both. useConfigEntries now disables the underlying query for empty owner
ids so the wrapper can call both hooks unconditionally.

The EntryModal renames its title to 'New/Edit Configuration', drops the
boxed background on the sensitive-value checkbox to match other modals,
and is keyed by entry id at the consumer so RHF/useFieldArray re-mount
with fresh defaultValues — fixes scope dimension/value selects rendering
empty when reopening an entry that has scoped overrides.

ConfigTreeView aligns the bordered tree container's height with the
detail panel via items-stretch + self-start, but only grows the left side
to match the right (not the other way around). EntryValue's empty-state
adds min-h-7 so the panel keeps the same height as the populated state.
Restrict /variables to globals (system-wide and group-owned) and move
project-tier variables to a new Variables tab on each project. Project
variables can no longer be created from the global page; the project
tab also surfaces inherited globals and shadow indicators.

Add ScopedValuesField, a shared scoped-overrides editor consumed by
both EntryModal and the new VariableEditorModal, so saving a variable
no longer overwrites scoped variants with a single unscoped value.

Extend Badge with an icon prop, mono tone, and selected variant; the
mono tone uses leading-none to align uppercase text with the icon.
Sweep page titles, modal/alert dialog titles, sidebar nav, section
headings, table column headers, and the Variable flow button labels so
every visible title follows the design language's Title Case rule
(e.g. "Edit Configuration", not "Edit configuration").

Body text, form field labels, and dynamic identifiers are unchanged.
The scoped-value dimension and value selects passed `{stringValue}` as
children to `<SelectValue>`. When the field's value transitioned from
empty to a real selection on the first pick, the manually-rendered
children stayed stale until the next render, so the trigger kept
showing the placeholder until a second selection. Letting Radix render
the matching item automatically via the controlled `value` prop fixes
the race.

Also remove the read-only Tier field from the Variable editor modal;
the modal description already conveys which tier is being edited.
…riggerKey logic and improving SelectValue rendering
The Scopes property is documented as replacing the existing scope context
when provided. The handler was clearing scopes unconditionally before the
null check, so any caller that omitted the field would silently lose all
scope bindings. Move the Clear() call inside the null check so null means
"no change" and an empty dictionary clears.

Add tests pinning both behaviours.
The Edit Client modal was filling every configured scope dimension with
its first allowed value when the modal opened, then sending the whole
dictionary on save. Editing only the name on a client that was not bound
to a dimension (or on a client created before the dimension existed)
would silently add bindings the user never picked.

Render an explicit "Unset" segment for each dimension and drop unset
entries from the request body before calling UpdateClient.
…luesField generic

The valuesAreMasked memo depended on the scopedValues array returned by
form.watch, which is a fresh reference each render, so the memo never
actually cached. Inline it as a plain expression. The decryptValues.isSuccess
short-circuit was redundant because a successful reveal also resets the
form fields away from '***'.

Constrain ScopedValuesField's generic to forms that actually contain a
scopedValues field array, and replace the per-call `as never` casts with
typed Path/ArrayPath casts so misuse fails at compile time instead of
relying on runtime hand-waving.
Add a test that opens the edit modal for a sensitive entry whose value
comes back masked, asserts the form is locked and the reveal banner is
visible, then clicks Reveal Sensitive Values and asserts the form
unlocks with the decrypted value populated.
@mrtaikandi
mrtaikandi merged commit 4199053 into main May 9, 2026
1 of 2 checks passed
@mrtaikandi
mrtaikandi deleted the ux-improvements branch May 9, 2026 20:16
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.

1 participant