Skip to content

feat(settings): redesigned UI, new field types, decoded-value API#211

Merged
antosubash merged 25 commits into
mainfrom
settings-refactor-v2
May 23, 2026
Merged

feat(settings): redesigned UI, new field types, decoded-value API#211
antosubash merged 25 commits into
mainfrom
settings-refactor-v2

Conversation

@antosubash
Copy link
Copy Markdown
Owner

@antosubash antosubash commented May 22, 2026

Summary

  • New SettingTypes: Select, Color, Url, Email, Password, MultilineText, DateTime extend the existing 4. SettingDefinition gains AllowedValues, Min/Max, Pattern, Required, Sensitive, Order, Placeholder.
  • Decoded-value API: SettingValueDto/UserSettingValueDto carry decoded JsonElement values. New endpoints: PUT /api/settings/bulk, DELETE /api/settings/{key} (reset), GET /api/settings/{key}/resolved. Server-side validation against the new SettingDefinition rules. Sensitive values masked.
  • Per-type field components under components/fields/SettingField.tsx is now a thin dispatcher.
  • Redesigned pages: search/filter, scope tabs, scope-coded badges, inheritance indicator, reset-to-default per row, sticky bulk-save bar, design-system Card/Section/SearchInput/Badge/EmptyState primitives.
  • 8 bugs found by QA, all fixed in this PR:
    • Security: write endpoints (PUT/DELETE/bulk) now gated on Settings.Update permission (was bare .RequireAuthorization() — any authed user could write System settings).
    • GET/DELETE /api/settings/{key} return 400 instead of 500 when ?scope= missing.
    • PUT /api/settings rejects scope=User (prevents ghost UserId=null rows).
    • SetManyAsync throws SettingValidationException for User scope → endpoint converts to 400.
    • SettingValidator: Email/Url/Color/DateTime skip the user Pattern regex (no duplicate errors); URL requires http/https scheme (not file:// on Linux).
    • Frontend: handleSave/handleReset/handleBulkSave check res.ok, parse ValidationProblemDetails, show inline danger alert (was silent failure).
    • Frontend: Search input + Clear-search action stay mounted in the empty state (was unmounting → no recovery without page reload).

Verification

Admin Settings — System tab

admin system tab

Admin Settings — search + new ColorField

admin color field

User Settings — inheritance display + SelectField

user settings

Local CI

  • dotnet format --verify-no-changes
  • npm run check (biome + typecheck across 13 modules) ✓
  • dotnet build -warnaserror ✓ (0 errors, 0 warnings)
  • dotnet test ✓ — 1,107/1,108 tests pass. Settings module: 104/104.
  • npx playwright test (settings) ✓ — 66/66 settings e2e pass.

Known pre-existing issues on main

These fail on main too (verified by stashing and re-running); not introduced by this branch:

  • EventDurabilityE2ETests.PublishedEvent_Survives_The_Durable_Pipeline_End_To_End — durability E2E flake (also fails in this PR's CI build).
  • feature-flags-crud e2e, 2× filestorage-crud unauthenticated, 1× permissions.spec.ts › can access settings page (404 on /settings — wrong route), 1× permissions.spec.ts › admin API rejects unauthenticated request.

Test plan

  • Reviewer loads /settings/manage and confirms System/Application tabs, group sidebar, search filter, scope badges, and the new sample settings (Primary Color, Support Email, Welcome Message)
  • Reviewer loads /settings/me and confirms the inheritance line, Display Density Select, and Reset to default flow
  • Reviewer probes permissions by hitting PUT /api/settings as a non-admin user (expect 403)
  • Reviewer validates the silent-failure fix by submitting an invalid value and seeing the inline error
  • CI is green (modulo the pre-existing EventDurabilityE2ETests flake)

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 22, 2026

Deploying simplemodule-website with  Cloudflare Pages  Cloudflare Pages

Latest commit: ff709fe
Status: ✅  Deploy successful!
Preview URL: https://5e7b561f.simplemodule-website.pages.dev
Branch Preview URL: https://settings-refactor-v2.simplemodule-website.pages.dev

View logs

antosubash added 16 commits May 23, 2026 00:05
… has no results

BUG-7: handleSave, handleReset, and handleBulkSave in both AdminSettings and
UserSettings now check response.ok before updating local state. On failure,
an inline danger alert surfaces the error detail parsed from
ValidationProblemDetails (falling back to the HTTP status code). Local state
is only updated after a confirmed successful response.

BUG-8: AdminSettings.renderGroups() always renders SettingsLayout (toolbar +
sidebar nav) regardless of result count. The empty state (using EmptyState
from @simplemodule/ui with a "Clear search" action) is rendered inside the
content area so the search input and Bulk edit toggle are never unmounted.
UserSettings applies the same fix: the sticky toolbar already survived empty
results, but the sidebar nav was conditional; it now always renders, and the
empty state uses EmptyState with the same clear-search action.

New i18n keys: AdminSettings.NoResults, AdminSettings.SaveError,
AdminSettings.SaveErrorTitle, UserSettings.NoResults, UserSettings.SaveError,
UserSettings.SaveErrorTitle.
…r-scope write

- GAP-PERM: UpdateSetting, DeleteSetting, BulkUpdateSettings now require
  Settings.Update permission (403 for authenticated users without it)
- BUG-3: GET /api/settings/{key} without ?scope now returns 400 instead of 500
- BUG-4: DELETE /api/settings/{key} without ?scope now returns 400 instead of 500
- BUG-6: PUT /api/settings and PUT /api/settings/bulk reject scope=User with 400,
  directing callers to use /api/settings/me instead
- Tests: update existing write tests to use Settings.Update permission; add new
  tests covering 403, 400-missing-scope, and 400-user-scope cases
…an reject

BUG-1: Skip ValidatePattern for Email/Url/Color/DateTime types to prevent
duplicate validation errors when a Pattern is also configured.
BUG-2: URL validator now requires http/https scheme, rejecting file:// URIs
and relative paths like /foo/bar.
BUG-5: SetManyAsync now checks for User-scope entries upfront and throws
SettingValidationException (→ 400) instead of InvalidOperationException (→ 500).
Tests updated and new regression tests added for all three bugs.
Both relied on prior state and would flake under parallel execution. Now
each pre-cleans via API and the Bulk discard test picks a distinct value
that's guaranteed to differ from any persisted state.
@antosubash antosubash enabled auto-merge (squash) May 23, 2026 22:37
PR #211 gated /api/settings on Settings.Update permission and made the
value field a decoded JsonElement (not a JSON-encoded string). The
durability test needed both: an admin client with the permission claim,
and a plain 'hello' instead of '"hello"'.
@antosubash antosubash merged commit 846585d into main May 23, 2026
6 checks passed
@antosubash antosubash deleted the settings-refactor-v2 branch May 23, 2026 22:51
antosubash added a commit that referenced this pull request May 24, 2026
…er endpoint (#215)

* feat(core): add Form Request classes for typed binding + validation per endpoint (#163)

Introduces Laravel-style FormRequest<TSelf> base class that bundles parameter
binding, authorization, validation rules, and data normalization into a single
class. The endpoint handler receives an already-valid request object.

Pipeline: Bind → Authorize (403) → Prepare → Validate (422) → Handler

- FormRequest base + [FormRequest] attribute in SimpleModule.Core
- Source generator discovers [FormRequest] types, validates shape (SM0056/SM0057),
  emits TypeScript interfaces (same path as [Dto])
- FormRequestEndpointFilter auto-applied to all module route groups
- FluentValidation under the hood via RuleConfigurator<T>
- Validator cached per type for performance
- 422 Unprocessable Entity with RFC 7807 problem+json for validation failures
- Email module CreateTemplateEndpoint refactored as reference implementation
- 16 new xUnit tests covering binding, validation, authorization, and prepare hooks
- Constitution updated with FormRequest rules and SM0056/SM0057 diagnostics

* fix(core): address architecture review — filter coverage, shared Inertia errors, testability

- Fix C3: modules without RoutePrefix now get a group with AddFormRequestFilter()
  instead of mapping directly to app (no-prefix endpoints were silently skipping
  the FormRequest validation pipeline)
- Fix C1: ConfigureEndpoints escape hatch now wraps in a group with
  AddFormRequestFilter() so FormRequest types work in escape-hatch modules
- Fix M4: extract shared InertiaErrorResult utility used by both
  FormRequestEndpointFilter and GlobalExceptionHandler (removes duplication)
- Fix m2: add public ValidateRulesAsync() method on FormRequest so consumers
  can unit-test their validation rules without constructing filter context
- Update generator tests to match new generated code shape

* fix(core): address code review findings — dedup, auth, caching, API gaps

- Fix duplicate TS interfaces when both [Dto] and [FormRequest] on same class
  (DtoFinder now deduplicates FQNs before the [FormRequest] scan)
- Restore .RequirePermission() on CreateTemplateEndpoint for ASP.NET auth
  metadata visibility (OpenAPI/Swagger, policy-based audit); remove redundant
  FormRequest.Authorize() override from CreateTemplateFormRequest
- Revert escape-hatch ConfigureEndpoints to receive WebApplication (not
  RouteGroupBuilder) — avoids breaking contract for modules that cast to
  IApplicationBuilder
- ValidateRulesAsync now calls Prepare() before validation, consistent with
  the filter pipeline
- Add When/Unless forwarding to RuleConfigurator for grouped conditional rules
- Replace ConcurrentDictionary validator cache with volatile + Interlocked
  (single-entry-per-type, simpler, avoids redundant dictionary overhead)

* feat(settings): convert Settings endpoints to FormRequest pattern

Convert 4 Settings module endpoints to use FormRequest for input validation:
- UpdateSettingEndpoint (PUT /api/settings) — validates key format, scope enum
- UpdateMySettingEndpoint (PUT /api/settings/me) — reuses UpdateSettingFormRequest
- CreateMenuItemEndpoint (POST /api/settings/menus) — validates label, URL length
- UpdateMenuItemEndpoint (PUT /api/settings/menus/{id}) — validates same as create

Demonstrates dual-validation: FormRequest validates shape (key format, field
lengths, enum membership), service layer validates domain (setting type/range
against definitions). FormRequest returns 422, service exceptions return 400.

Symmetric validation: both Create and Update menu endpoints now share the same
rules; both admin and user setting endpoints use the same FormRequest.

Includes 16 new integration tests verifying 422 responses, Prepare normalization,
RFC 7807 shape, and auth-before-validation ordering.

* fix(settings): address code review — regex, scope, cascade, test semantics

- Fix regex to allow camelCase/PascalCase keys (email.defaultFromAddress,
  FileStorage.MaxFileSizeMb, etc.) and enforce segment-based validation that
  rejects trailing dots and empty segments
- Create dedicated UpdateMySettingFormRequest (Key + Value only) for
  PUT /api/settings/me — removes misleading Scope field that was validated
  but silently ignored
- Fix double error messages for empty key by splitting rules with .When()
  guard so regex only fires on non-empty keys
- Rename misleading test: NullValue "clears setting" → actually stores
  empty string (documents real behavior, not aspirational)
- Add tests for camelCase keys and trailing-dot rejection

* fix(settings): adapt FormRequest tests to upstream API changes

Upstream PR #211 changed UpdateSettingRequest.Value from string? to
JsonElement and added .RequirePermission(SettingsPermissions.Update).
Update FormRequest integration tests to:
- Use JsonSerializer.Deserialize<JsonElement>() for Value fields
- Pass SettingsPermissions.Update to CreateAuthenticatedClient
- Replace hyphenated test keys with camelCase (regex rejects hyphens)
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