Skip to content

feat(slug): keep slug in sync with title, lockable slug field, reject-on-collision option#8

Open
openrijal wants to merge 3 commits into
awecode:mainfrom
openrijal:feat/slug-sync-and-lock
Open

feat(slug): keep slug in sync with title, lockable slug field, reject-on-collision option#8
openrijal wants to merge 3 commits into
awecode:mainfrom
openrijal:feat/slug-sync-and-lock

Conversation

@openrijal

Copy link
Copy Markdown

Closes #7

What changed

  • Slug stays in sync with the title on update too, not only on create. The AutoForm.vue watcher used to be gated on props.mode === 'create'; it now runs in both modes, but only while the slug field is locked (see next point).
  • Slug field is readonly by default with a lock toggle. When a field is listed in slugFields, AutoFormField.vue renders a lock icon in the trailing slot. Click it to unlock the field for manual editing — the auto-sync pauses so the editor's slug isn't overwritten when the title changes again.
  • New slugCollision option ('suffix' | 'reject', default 'suffix'). The existing silent -1/-2 suffix behavior is unchanged. With 'reject', the server pre-checks uniqueness and throws a 422 with data.errors keyed by the slug field, so the form surfaces a per-field validation error.
  • Update formspec route now passes slugFields through to the client (was only sent by the create route).
  • Lock state is passed from AutoForm to AutoFormField via provide/inject, so non-slug forms have zero change in behavior.

Files

  • server/utils/registry.ts — added slugCollision to AdminModelOptions and AdminModelConfig.
  • server/utils/slug.ts — added assertUniqueSlugs(cfg, data, excludeLookupValue?) that throws a 422 validation error when any slug collides.
  • server/services/create.ts, server/services/update.ts — call assertUniqueSlugs pre-insert when slugCollision === 'reject'; re-assert on DB unique violation to handle races.
  • server/api/autoadmin/formspec/[modelKey]/update/[lookupValue].ts — surface cfg.slugFields to the client.
  • components/AutoForm.vue — reactive slugLocks map, watcher gated on locked, provided to children. Update mode skips clearing the slug when source is briefly empty.
  • components/AutoFormField.vue — injects lock state; for text inputs that are slug fields, binds :readonly and renders a lock/unlock toggle in the trailing slot.

Backwards compat

  • slugCollision defaults to 'suffix' — every existing registered model behaves the same.
  • Models without slugFields are unaffected (the lock UI, watcher, and validation only run when slugFields is set).
  • The lock UI only triggers on type: 'text' fields, which is what slug fields are in practice.

How to test

  1. Register a model with slugFields: { slug: ['title'] }.
  2. Create a record — slug auto-fills from the title, field is readonly with a lock icon.
  3. Click the lock — field becomes editable; type a custom slug.
  4. Open an existing record, change the title — slug follows it (if still locked).
  5. Add slugCollision: 'reject', try to save a duplicate slug — see a validation error on the slug field instead of a silent -2 suffix.

openrijal and others added 3 commits May 20, 2026 19:50
…-on-collision option

- Move slug auto-sync watcher out of create-only gate so the slug also
  follows the title on update — but only while the field is locked.
- Add lock state per slug field (provided to AutoFormField via inject);
  slug fields render readonly with a lock toggle. Unlocking pauses the
  auto-sync so manual edits stick.
- New `slugCollision: 'suffix' | 'reject'` option on registered models
  (default `'suffix'` keeps existing behavior). With `'reject'` the
  server pre-checks uniqueness and returns a validation error keyed by
  the slug field, surfaced on the form.
- Pass `slugFields` through the update formspec route (was missing).

Closes awecode#7

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New per-resource option `slugLockedByDefault?: boolean` (default `true`).
When `false`, slug fields start unlocked so editors can type a custom
slug immediately on create — the lock toggle is still available to
re-enable auto-sync from the source field.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the same three options to `useJsonResourceRegistry().register(...)`
for `kind: 'array'` resources: `slugFields`, `slugCollision`,
`slugLockedByDefault`. Behavior matches the DB-registered models so
editors can use one mental model regardless of storage backend.

- `jsonResourceRegistry.ts`: new options on input + config; carried
  through `defaultArrayConfig`.
- `jsonFormSpec.ts`: passes `slugFields` and `slugLockedByDefault`
  through both create and update form specs so the existing
  AutoForm/AutoFormField client wiring picks them up.
- `slug.ts`: new in-memory helpers `ensureUniqueSlugsInRows` and
  `assertUniqueSlugsInRows` for array resources (mirroring the DB-side
  helpers, but pure functions over the in-memory rows).
- `jsonResourceCrud.ts`: `createJsonArrayRecord` and
  `updateJsonArrayRecord` now run the uniqueness check (suffix or
  reject) inside the existing `writeArrayWithRetry` closure, before
  writing back to storage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

feat: keep slug in sync with title, with a readonly-but-editable slug field and reject-on-collision option

1 participant