Skip to content
Merged
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
19 changes: 19 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Repository Guidelines

## Project Structure & Module Organization
Engine code lives under `app/`, mirroring a standard Rails engine: controllers, components, and views compose admin UI, while `app/assets` and `app/javascript/koi` hold the source for bundles shipped via Rollup. Shared Ruby modules and tasks sit in `lib/`, including generators and Rake extensions. Specs reside in `spec/`, with the `spec/dummy` Rails app providing an integration harness; treat it as disposable scaffolding, not a source of production logic.

## Build, Test, and Development Commands
Run `bin/setup` once to install gem and JavaScript dependencies and to scaffold the dummy app. Use `bundle exec rake build` to compile assets (`yarn build`) and prepare the engine. `bundle exec rspec` exercises the full suite; pair it with `bundle exec rake lint` to run RuboCop, ERB lint, and Prettier checks. For interactive demos, launch the sandboxed host app with `bin/dev`, which proxies to `spec/dummy/bin/dev`.

## Coding Style & Naming Conventions
Ruby follows the `rubocop-katalyst` profile: two-space indentation, snake_case for methods and variables, and CamelCase for classes/modules. Prefer service objects and view components that live alongside peers in `app/components`. JavaScript and stylesheet changes should pass Prettier's default formatting; keep ES modules colocated with their Rails counterpart under `app/javascript/koi`. When extending generators or Rake tasks, namespaced constants should sit under `Katalyst::Koi` to avoid collisions.

## Testing Guidelines
Author specs with RSpec; name files `*_spec.rb` within the matching directory (e.g., `spec/components`). Exercise factories via `spec/factories` and update fixtures only when assertions demand it. System specs should target the dummy app flows; reset state with provided helpers in `spec/support`. Before raising a PR, ensure `bundle exec rspec` passes locally and add coverage for each new behaviour or regression fix.

## Commit & Pull Request Guidelines
Commits in this repo favour short, descriptive subjects in sentence case (e.g., `Fix input size for select elements` or `Churn: update dependencies`). Group unrelated changes into separate commits to keep rollbacks trivial. Pull requests should summarise intent, reference any Katalyst issue IDs, and include screenshots or GIFs for admin UI tweaks. Mention required follow-up migrations or configuration changes explicitly so downstream applications can plan upgrades.

## Security & Maintenance
Run `bundle exec rake security` before releases to scan the engine with Brakeman. Keep Rollup outputs clean by running `yarn clean` when removing assets, and verify the dummy app still boots after dependency updates to catch breaking Rails changes early.
173 changes: 173 additions & 0 deletions docs/admin-module-workflow.md

Large diffs are not rendered by default.

131 changes: 131 additions & 0 deletions docs/archive/koi-basic-module-guide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# Building a Basic Koi Module

Use this guide when you want the lightest-possible CRUD surface in Koi: no ordering, no archiving, no associations—just a Rails model backed by standard columns and the admin scaffolding that Koi generates for you. It is written for both humans and coding LLMs so the process can be repeated reliably.

## What a Koi Module Provides

After generation you get:

- An ActiveRecord model wired with an `admin_search` scope and optional defaults from `koi:model` (`lib/generators/koi/model/model_generator.rb`).
- An admin controller, requests spec, GOV.UK-flavoured views, and fully wired routes from `koi:admin` (fan-out to controller, views, and route generators under `lib/generators/koi`).
- Automatic menu registration in `config/initializers/koi.rb` so the module appears in the admin UI once the server restarts.
- Form, table, and show layouts that follow Koi’s conventions, including breadcrumb/header wiring and Turbo-ready responses.

## Before You Start

1. Run `bin/setup` once per repository clone to install dependencies and prepare `spec/dummy` if you have not already.
2. Ensure `config/routes/admin.rb` and `config/initializers/koi.rb` exist. If not, run `bin/rails g koi:admin_route` and restart the server.
3. Decide on a model name and the columns you need. For the “basic module” pattern, stick to primitive columns (string, text, integer, boolean, date, datetime). Skip `archived_at`, `ordinal`, attachments, and associations so the generated UI stays minimal.

## Step-by-step Workflow

### 1. Generate the model and migration

Use the `koi:model` generator so the model receives the `admin_search` scope.

```sh
bin/rails g koi:model Article title:string summary:text published_on:date featured:boolean
```

- The generator accepts the same options as `rails g model` (for example `--skip-fixtures`, `--timestamps=false`).
- `koi:model` inserts either a `pg_search_scope` or a SQL fallback depending on whether `PgSearch::Model` is loaded (`lib/generators/koi/model/model_generator.rb`).

### 2. Run the migration

```sh
bin/rails db:migrate
```

Running the migration is essential before invoking `koi:admin`. If you skip this step, `koi:admin` cannot introspect the schema and you will have to pass every column to the generator manually.

### 3. Generate the admin surface

```sh
bin/rails g koi:admin Article
```

Key behaviours to understand:

- With no attribute arguments, `koi:admin` inspects the model you just migrated and collects columns, rich text associations, enums, and attachments (`lib/generators/koi/helpers/attribute_helpers.rb`). This keeps the form/view templates in sync with the database.
- To control field order explicitly, pass attributes in the desired sequence: `bin/rails g koi:admin Article title:string summary:text published_on:date featured:boolean`. The generator uses the attribute array as-is when rendering `_form.html.erb` (`lib/generators/koi/admin_views/templates/_form.html.erb.tt`).
- The admin generator fans out to:
- `koi:admin_controller` → `app/controllers/admin/articles_controller.rb` and `spec/requests/admin/articles_controller_spec.rb`.
- `koi:admin_views` → form, index, new/edit, and show templates under `app/views/admin/articles`.
- `koi:admin_route` → adds `resources :articles` to `config/routes/admin.rb` and updates the menu initializer (`lib/generators/koi/admin_route/admin_route_generator.rb`).
- All three generators run with `--force=true` by default, so files are overwritten without a prompt. Commit or stash before regenerating.

### 4. Restart the server

The navigation entry lives in `config/initializers/koi.rb`, so restart Spring/your Rails server to see the new module in the admin menu. Without the restart, the initializer is not reloaded and the menu will not change.

### 5. Verify CRUD behaviour

1. Sign in at `/admin`.
2. Confirm the module appears in the menu (alphabetically unless you reorder the initializer).
3. Create a record and save it—forms use GOV.UK inputs wired in `_form.html.erb`.
4. Visit the show page. The `<h1>` renders `record.to_s` (`lib/generators/koi/admin_views/templates/show.html.erb.tt:6`), so define `def to_s = title` (or another identifying attribute) in your model to display the correct heading.
5. Edit and delete to confirm the controller wiring and flash notices work as expected.

## Generator Reference for Basic Modules

### `koi:model`

- Inherits every option from `rails g model` (timestamps, parent class, fixtures, etc.).
- Adds `admin_search` automatically. If your schema lacks any string columns, the fallback scope becomes a simple `where` on whatever columns exist.
- Adds a default scope when an `ordinal` column is present; avoid that column for the “basic module” path.

### `koi:admin`

- Signature: `bin/rails g koi:admin NAME [field:type ...]`.
- Options: `--force=false` (preview without overwriting), `--skip-admin_controller`, `--skip-admin_views`, `--skip-admin_route` (skip individual hooks if you need to customise manually).
- Generates helpers for index/new/edit/show routes based on the namespace logic in `lib/generators/koi/helpers/resource_helpers.rb`.

### `koi:admin_controller`, `koi:admin_views`, `koi:admin_route`

You rarely run these directly, but they are available for targeted regeneration (for example, `bin/rails g koi:admin_views Article` if you only changed columns). They share the same attribute parsing and `--force` option.

## Column Types and UI Output

The attribute-introspection layer maps common Rails column types to Koi’s form and table helpers (`lib/generators/koi/helpers/attribute_types.rb`). For basic modules, expect the following defaults:

| Column type | Form helper | Index cell | Show cell |
| --- | --- | --- | --- |
| `string`, `text` | `form.govuk_text_field` | `row.text` | `row.text` |
| `integer` | `form.govuk_number_field` | `row.number` | `row.number` |
| `boolean` | `form.govuk_check_box_field` | `row.boolean` | `row.boolean` |
| `date` | `form.govuk_date_field` | `row.date` | `row.date` |
| `datetime` | _no input generated_; add one manually if you need it | `row.datetime` | `row.datetime` |
| `enum` | `form.govuk_enum_select` | `row.enum` | `row.enum` |

Types not listed fall back to the string helpers. If you introduce attachments, rich text, or associations later, rerun the generator and update the form manually as needed.

## Field Ordering Rules

- `koi:admin` renders fields in the order of the `attributes` array it receives. When introspecting a migrated model, that order matches how columns appear in the database schema. Passing explicit attributes lets you override it.
- The first column becomes the clickable link on the index table (`lib/generators/koi/admin_views/templates/index.html.erb.tt:22`). Choose an identifying field (for example, `title`) if you want a particular column to link to the show page.
- To reorder after generation, edit `_form.html.erb`, `index.html.erb`, and `show.html.erb` manually or rerun the generator with the desired order.

## Regeneration Strategy

- When the schema changes (new column, renamed field), rerun `koi:admin` for that model. The generator rewrites controllers, views, routes, and menu entries to match the new schema.
- Because `--force` defaults to true, regeneration overwrites local edits. Either:
- keep custom logic in separate partials/components that you reapply after regeneration, or
- use `--force=false` to generate side-by-side files and copy changes across.
- You can regenerate just the controller, views, or routes if that is all that changed.

## Putting It All Together (Worked Example)

1. **Generate model:** `bin/rails g koi:model Announcement title:string body:text published_on:date featured:boolean`
2. **Migrate:** `bin/rails db:migrate`
3. **Generate admin:** `bin/rails g koi:admin Announcement`
4. **Set the display name:**
```ruby
# app/models/announcement.rb
class Announcement < ApplicationRecord
def to_s = title
end
```
5. **Restart:** `bin/dev` (or restart your existing server process).
6. **Verify in the UI:** create, edit, show, and delete announcements from `/admin/announcements`.

Following these steps yields a fully functional, minimal CRUD module that plays nicely with the rest of Koi. When you are ready for richer behaviour (archiving, ordering, attachments), jump to `docs/admin-module-workflow.md` for the advanced patterns.
67 changes: 67 additions & 0 deletions docs/archiving.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Archiving Modules in Koi

Use this guide whenever you scaffold or retrofit a module that supports soft deletion via `archived_at`. It summarises the conventions baked into Koi, the gaps left by generators, and the post-generation steps required to give editors a predictable UI.

## What the Generators Provide

When the schema contains an `archived_at` column _and_ the model mixes in `Koi::Model::Archivable` (add the concern before running `koi:admin`, or rerun the generator afterward so it picks up the capability):

- `koi:model` keeps `archived_at` in the migration but, as of Koi 5.0.3, does **not** insert `include Koi::Model::Archivable`; add the concern manually (before running `koi:admin`) so the admin generator can detect the capability.
- `koi:admin` generates:
- Bulk archive tooling on the index (`table_selection_with` + `Archive` button) and an `Archived` tab.
- Controller actions `archive`, `restore`, and `archived`, plus the `destroy` behaviour that archives first, then hard-deletes once already archived.
- Routes and menu entries for the additional actions.
- Default scopes become `not_archived`, so new records stay visible unless explicitly archived.

## What You Still Need to Wire Manually

1. **Permit the toggle** – add `:archived` to strong parameters in the admin controller so the form checkbox can post back:
```ruby
def page_params
params.expect(page: [
:title,
:published_on,
:archived_at,
:archived,
{ items_attributes: [%i[id index depth]] },
])
end
```

2. **Expose the checkbox** – surface the boolean on both new/edit forms so editors can archive or restore a single record without navigating away:
```erb
<%= form.govuk_check_box_field :archived %>
```
The concern maps the boolean to the soft-delete helper (`true` ⇒ archive, `false` ⇒ restore). Because new records default to unchecked, they start unarchived.

3. **Surface state in the UI** – include `row.boolean :archived` (or `:archived?`) in summary tables so admins can see the flag on the show screen. This is optional but recommended for clarity.

4. **Tests** – extend the request spec to cover:
- Bulk archive & restore actions (`PUT /archive`, `PUT /restore`).
- Destroy behaviour (archives first, then deletes when already archived).
- Form-driven archiving via the checkbox on create/update.
Use `Page.archived` / `Page.not_archived` to assert state changes.

5. **Content status bar links** – if the module uses Katalyst::Content, make sure you expose matching public routes so the editor’s status bar can link to the live and preview versions. Add something like:
```ruby
resources :pages, only: :show do
get :preview, on: :member
end
```
Then render the published/draft versions in a controller (`render_content(page.published_version)` / `page.draft_version`) so `url_for(container)` resolves correctly. Without these routes, the status bar raises “undefined method `page_path`” and its links break.
If the site needs `/slug` URLs without the `/pages` prefix, layer on the constraint workflow in [`root-level-page-routing.md`](./root-level-page-routing.md).

## Typical Admin Workflow

1. **Archive from the index** – select rows using the checkboxes, click `Archive`, confirm the toast, and verify they disappear from the active index.
2. **Restore from the archived tab** – open `Archived`, select records, click `Restore`.
3. **Archive from the form** – open a record, tick “Archived”, save. The index removes it (default scope). Editing again and unticking restores it.
4. **Permanent delete** – once a record is archived, hitting the destroy action (via index bulk delete or form archive again) removes it entirely.

## Recommended Follow-up Checks

- Ensure the menu entry still surfaces the module under `Koi::Menu.modules`.
- If the module exposes public routes, verify archived records fall out of default public scopes (because of the default scope).
- Run `bundle exec rspec` for the request specs touching archive flows.

Keep this guide close when adding new archivable modules so future changes stay consistent with Koi’s soft-delete semantics.
Loading
Loading