Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/views/admin/sessions/new.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
<%= form.hidden_field :response, data: { webauthn_authentication_target: "response" } %>
<%= hidden_field_tag(:redirect, redirect) %>
<div class="actions">
<%= form.admin_save "Next" %>
<%= form.button("Next", type: :submit, class: "button") %>
<%= form.button(type: :button, class: "button button--secondary", data: { action: "webauthn-authentication#authenticate" }) do %>
<icon class="icon" data-icon="fingerprint" aria-label="Passkey">&nbsp;</icon>
<% end %>
Expand Down
2 changes: 1 addition & 1 deletion app/views/admin/sessions/otp.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@
<%# note, autocomplete off is ignored by browsers but required by PCI-DSS %>
<%= form.govuk_text_field :token, autofocus: true, autocomplete: "off" %>
<%= hidden_field_tag(:redirect, params[:redirect]) %>
<%= form.admin_save "Next" %>
<%= form.button("Next", type: :submit, class: "button") %>
<% end %>
2 changes: 1 addition & 1 deletion app/views/admin/sessions/password.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<%# note, autocomplete off is ignored by browsers but required by PCI-DSS %>
<%= form.govuk_password_field :password, autofocus: true, autocomplete: "off" %>
<%= hidden_field_tag(:redirect, params[:redirect]) %>
<%= form.admin_save "Next" %>
<%= form.button("Next", type: :submit, class: "button") %>

<%# init govuk js to provide the show/hide button %>
<%= govuk_formbuilder_init %>
Expand Down
2 changes: 1 addition & 1 deletion app/views/admin/tokens/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@
<%= row.text :email %>
<% end %>
<div class="actions">
<%= form.admin_save "Sign in" %>
<%= form.button("Sign in", type: :submit, class: "button") %>
</div>
<% end %>
11 changes: 7 additions & 4 deletions docs/admin-module-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@ Picking a stable attribute (title, name, slug) keeps UI labels consistent across

- Form templates iterate through the discovered attributes in order and render `govuk_*` helpers (`lib/generators/koi/admin_views/templates/_form.html.erb.tt:3`). Reorder or group fields by rearranging the generated ERB.
- If you pass attribute arguments to `koi:admin`, the generated forms respect the order you provide. When you omit arguments, discovery follows the migration order for columns—i.e., the order you specified when running `koi:model`—then appends attachments, rich text, and `belongs_to` associations in declaration order.
- `Koi::FormBuilder` injects admin-friendly buttons (`form.admin_save`, `form.admin_delete`) and smart defaults for file hints and ActionText direct uploads (`lib/koi/form/builder.rb:13`).
- Keep form actions focused on saving form content (plain submit buttons in form sections).
- Put non-form actions (archive/delete/view/preview) in page header actions via `actions_list`, `link_to_delete`, and `link_to_archive_or_delete`.
- `Koi::FormBuilder` provides smart defaults for file hints and ActionText direct uploads (`lib/koi/form/builder.rb:13`).
- The first attribute becomes the default index link via `row.link`, so choose something human-readable (title/name) or update the generated `index.html.erb`.
- For structured content components, mix in helpers from `Koi::Form::Content` to reuse heading/style selectors inside custom forms (`lib/koi/form/content.rb:8`).

Expand All @@ -98,7 +100,7 @@ Picking a stable attribute (title, name, slug) keeps UI labels consistent across
### Table Layout

- Lists render inside `table_with`, and the first attribute becomes the linked column. Subsequent attributes follow in declaration order (`lib/generators/koi/admin_views/templates/index.html.erb.tt:33`).
- When the module is archivable, a selection column and bulk archive button appear automatically (`lib/generators/koi/admin_views/templates/index.html.erb.tt:19`).
- When the module is archivable, selection columns and bulk archive/restore actions should be available on active/archived indexes (`lib/generators/koi/admin_views/templates/index.html.erb.tt:19`).
- Summary pages render every “show attribute” using `row.text`, `row.enum`, etc. (`lib/generators/koi/admin_views/templates/show.html.erb.tt:10`).

### Collections & Filters
Expand All @@ -122,7 +124,7 @@ Each controller defines an inner `Collection` class extending `Admin::Collection
Including `Koi::Model::Archivable` in the model adds the `archived_at` scope and helpers (`app/models/concerns/koi/model/archivable.rb:17`). When the generator spots an `archived_at` column:

- Extra routes (`archive`, `restore`, `archived`) are added (`lib/generators/koi/admin_route/admin_route_generator.rb:33`).
- The index view adds a bulk archive action and a link to the archived list (`lib/generators/koi/admin_views/templates/index.html.erb.tt:21`).
- Index/archived views include bulk archive/restore via selection controls plus a link between active and archived lists (`lib/generators/koi/admin_views/templates/index.html.erb.tt:21`).
- Destroy actions archive first, then delete once already archived (`lib/generators/koi/admin_controller/templates/controller.rb.tt:64`).

Finish the setup by following the dedicated archiving guide (`archiving.md`) for form wiring, strong parameters, UI surfacing, and testing expectations. That guide captures the manual steps discovered while building the Pages module.
Expand All @@ -138,7 +140,7 @@ Every admin module is added to `Koi::Menu.modules` with a label derived from the
## Common Customisations

- **Add custom filters** by extending the inner `Collection` and declaring attributes manually; anything you add shows up in `table_query_with` automatically.
- **Override form layouts** using ViewComponents or partials if you need multi-column layouts—just ensure the submit buttons continue to call `form.admin_save` so styles remain consistent.
- **Override form layouts** using ViewComponents or partials if you need multi-column layouts—keep form controls save-focused and move non-form actions to header actions.
- **Additional actions** go inside the controller and can be surfaced in the header via `actions_list`.
- **Non-standard inputs** (e.g., slug sync, toggles) can hook into existing Stimulus controllers such as `sluggable` or `show-hide`.
- **Front-end routes** – when marketing pages should appear at `/slug` instead of `/pages/slug`, use the [`root-level-page-routing.md`](./root-level-page-routing.md) constraint pattern after scaffolding the public controller.
Expand Down Expand Up @@ -166,6 +168,7 @@ Every admin module is added to `Koi::Menu.modules` with a label derived from the
- Generators overwrite files without prompting. Keep commits small so you can regenerate confidently.
- `belongs_to` relationships only appear on the show page; add your own form fields/filters if you need to edit them in the UI.
- When handling attachments, always call `save_attachments!` before rendering to avoid losing uploads.
- In request specs, prefer combined redirect assertions (`have_http_status(:see_other).and(redirect_to(...))`).
- Pagination is disabled for orderable lists. If you have a large dataset, consider a dedicated reorder screen instead of relying on the default drag-and-drop.
- Navigation entries are stored in code, not the database. Remember to adjust `config/initializers/koi.rb` when you rename modules.
- After scaffolding, step through the CRUD screens (create → show → edit → delete) to confirm helpers, validations, and menu wiring behave as expected.
Expand Down
133 changes: 133 additions & 0 deletions docs/koi-5-admin-upgrade-guide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# Koi 5 Admin Upgrade Guide

## Purpose

Use this guide to upgrade legacy admin modules to Koi 5 while preserving product behavior and reducing drift.

## Preflight checklist

- Ensure the app and admin shell both load (`https://localhost`, `https://localhost/admin`).
- Install required engine migrations and migrate before feature work.
- Confirm admin CSS is pure CSS (Koi 5), not Sass-based admin entrypoints.
- Create a baseline checkpoint commit before module-by-module uplift.

## Module archetype: decide first

Pick lifecycle semantics before touching controller/view code.

- `Simple CRUD` (eg project types, industries): no lifecycle state; optional ordering.
- `Archivable` (eg staff, clients): boolean lifecycle with `archive/restore`; optional archived index.
- `Content stateful` (eg articles, pages, case studies): `publish/unpublish` via content state (`draft_version_id` / `published_version_id`).

Do not mix archetypes inside one module unless there is a strong product reason.

## Canonical module workflow

1. Regenerate with `bin/rails generate koi:admin <model> --force`.
2. Fix routes first (dedupe generator inserts, correct collection/member actions, confirm helpers with `rails routes`).
3. Align controller shape to Koi 5:
- locals-based rendering
- `params.expect`
- explicit collection actions
- `:see_other` redirect semantics
4. Move defaults/business invariants to model + migration (not controller setup).
5. Rework views to Koi 5 patterns (header/actions, concise table rows, summary table where useful).
6. Update request specs to match intended behavior.
7. Run module specs, admin request specs, then full suite.
8. Checkpoint commit.

## Strong params: `expect` rules

`expect` is stricter than legacy `require(...).permit(...)` for repeated nested structures.

- Scalar arrays: `author_ids: []`
- Repeated nested hashes: `items_attributes: [[:id, :index, :depth]]`
- Single nested hash: `seo_metadatum_attributes: %i[id title description keywords _destroy]`

Do not translate repeated nested structures from `permit` without checking shape.

## View conventions

- Keep index pages concise; only high-signal fields for triage/navigation.
- Avoid images on index/archived unless operationally required.
- State-scoped pages should not show the state column.
- Prefer explicit path helpers and avoid polymorphic path indirection where readability suffers.
- Use plain submit buttons; avoid `form.admin_save` / `form.admin_delete` in new Koi 5 work.
- Keep `form_with` in `new`/`edit` and use shared fields partials (`_metadata_fields`) for common inputs.
- Keep custom sections (eg SEO) explicit in `edit`, not hidden behind option locals.
- In ERB, use parentheses for helper calls with arguments, especially helper calls that also take a block.

### Header action helpers

- Use header action links for lifecycle/destructive actions rather than form action buttons.
- `link_to_delete(record)` always issues a delete request and confirms by default.
- `link_to_archive_or_delete(record)` archives active records and only shows delete when already archived.
- For non-standard routes, pass an explicit `url:` to avoid polymorphic mismatch.

### Archivable table UX

- For Koi 5 archivable modules, include selection checkboxes on active and archived indexes.
- Provide bulk `archive` and bulk `restore` actions via table selection controls.

## Ordering rules

- Keep ordering on the active/current index only.
- Do not paginate orderable index pages.
- Paginate non-orderable archived pages by default.
- Keep ordering concerns separate from lifecycle actions.

## Content module strategy

Use a phased approach:

- `Phase A (top/tail)`: index/new/edit first.
- `Phase B (editor)`: show/update content editor workflow.

For show/update in content modules:

- Build editor with `Katalyst::Content::EditorComponent.new(container: record)`.
- Preserve commit semantics (`publish`, `save`, `revert`).
- Invalid update from show should return turbo editor errors with `:unprocessable_content`.

## Lifecycle migration safety

When converting legacy lifecycle models (eg `active` -> content state):

- Migrate data first to preserve intent (eg inactive -> unpublished).
- Then remove legacy columns/scopes/UI paths.
- Update frontend selectors/scopes so hidden records do not reappear.

## Admin CSS + content icons

- Koi 5 admin stylesheet is CSS (`@import url("koi/index.css")`).
- Content 3 icon overrides use `[data-icon="..."]`; old `value` / `data-item-type` selectors no longer work.
- Custom content icons should use `viewBox="0 0 16 16"` and `stroke-width="2"`.

## Testing guidance

- Use tight request-spec loops while upgrading each module.
- Keep tests focused on app behavior, not Katalyst Tables internals.
- Avoid query-specific table tests like "finds needle" / "hides chaff".
- Use combined redirect assertions: `have_http_status(:see_other).and(redirect_to(...))`.

## Done criteria for one module

- Routes/helpers are correct and non-duplicated.
- Lifecycle behavior matches selected archetype.
- Index/show/edit UX follows Koi 5 conventions.
- Strong params `expect` shape is correct (especially repeated nested attributes).
- Module request specs pass.
- Admin request suite passes.
- Full suite passes (excluding known pending tests).

## References

- Koi controller patterns: `koi/app/controllers/admin/admin_users_controller.rb`, `koi/app/controllers/admin/url_rewrites_controller.rb`
- Koi views: `koi/app/views/admin/admin_users/index.html.erb`, `koi/app/views/admin/admin_users/archived.html.erb`
- Real-world archive/order pattern: `frg/app/controllers/admin/merchandise_controller.rb`, `frg/app/views/admin/merchandise/index.html.erb`
- Content editor contract: `koi/docs/skills/content-admin.md`
- Local upgrade examples in this repo:
- `kat/app/controllers/admin/staff_controller.rb`
- `kat/app/controllers/admin/articles_controller.rb`
- `kat/app/controllers/admin/pages_controller.rb`
- `kat/app/controllers/admin/case_studies_controller.rb`
6 changes: 4 additions & 2 deletions docs/koi-user-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,15 +174,17 @@ In views, use the provided helpers:
- `row.link` outputs a link to the record’s show (or a custom URL) (`app/components/concerns/koi/tables/cells.rb:18`).
- `row.attachment` displays ActiveStorage attachments as downloads/thumbnails (`app/components/concerns/koi/tables/cells.rb:44`).
- Use `table_selection_with` for bulk actions, as shown in `app/views/admin/admin_users/index.html.erb`.
- For archivable resources, include selection controls on active and archived tables so users can bulk archive and bulk restore.

### Forms

`Koi::FormBuilder` combines `GOVUKDesignSystemFormBuilder` with helper shortcuts (`lib/koi/form_builder.rb`). Key helpers include:
`Koi::FormBuilder` combines `GOVUKDesignSystemFormBuilder` with helper shortcuts (`lib/koi/form_builder.rb`).

- `form.admin_save` / `form.admin_delete` / `form.admin_archive` / `form.admin_discard` for consistent action buttons (`lib/koi/form/builder.rb:13`).
- Automatic admin routes in `form_with` via `Koi::FormHelper` (`app/helpers/koi/form_helper.rb`).
- File field helpers use size limits from configuration.
- `Koi::Form::Content` provides macros for content block editors (heading fields, target selectors, etc.) used with `Katalyst::Content`.
- In module forms, keep submit controls focused on saving form content (plain submit buttons).
- Put non-form lifecycle actions in page header actions (`actions_list`) with `link_to_delete(record)` or `link_to_archive_or_delete(record)`.

Remember to call `govuk_formbuilder_init` once when you render password fields so the GOV.UK show/hide toggle initialises (`app/views/admin/sessions/password.html.erb:12`).

Expand Down
Loading