From 47cc243959001c26b6ddad28015ba5d5adee46c8 Mon Sep 17 00:00:00 2001 From: Stephen Nelson Date: Fri, 31 Jul 2026 23:08:15 +0930 Subject: [PATCH] Integrate new govuk_attachment_field inputs Requires an upgrade to katalyst-govuk-formbuilder 2. --- .github/workflows/release.yml | 4 + Gemfile.lock | 23 +- app/assets/stylesheets/koi/blocks/button.css | 6 +- .../stylesheets/koi/forms/document-field.css | 84 ------ .../stylesheets/koi/forms/file-upload.css | 259 ++++++++++++++++++ .../stylesheets/koi/forms/image-field.css | 96 ------- app/assets/stylesheets/koi/forms/index.css | 2 - app/javascript/koi/application.js | 14 - app/javascript/koi/controllers/index.js | 4 +- app/views/admin/sessions/password.html.erb | 3 - docs/koi-user-guide.md | 2 - katalyst-koi.gemspec | 4 +- .../koi/helpers/attribute_helpers.rb | 6 + lib/koi/form/builder.rb | 18 +- lib/tasks/dummy.thor | 2 +- package.json | 2 +- .../koi/admin_controller_generator_spec.rb | 27 ++ .../koi/admin_views_generator_spec.rb | 1 + spec/helpers/koi/form_builder_spec.rb | 38 +++ spec/support/capybara.rb | 1 + spec/system/admin/authentication_spec.rb | 19 ++ spec/templates/app/models/banner.rb | 2 + yarn.lock | 2 +- 23 files changed, 394 insertions(+), 225 deletions(-) delete mode 100644 app/assets/stylesheets/koi/forms/document-field.css delete mode 100644 app/assets/stylesheets/koi/forms/image-field.css create mode 100644 spec/helpers/koi/form_builder_spec.rb diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 05f9cae32..8696dfcef 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,6 +15,10 @@ jobs: contents: write id-token: write steps: + - name: Install dependencies + env: + DEPENDENCIES: libvips + run: sudo apt-get install -y $DEPENDENCIES || (sudo apt-get update && sudo apt-get install -y $DEPENDENCIES) - uses: actions/checkout@v6 - uses: ruby/setup-ruby@v1 with: diff --git a/Gemfile.lock b/Gemfile.lock index 63e22ca75..52d84b08b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -9,12 +9,12 @@ GIT PATH remote: . specs: - katalyst-koi (5.9.0) + katalyst-koi (5.10.0) bcrypt importmap-rails jwt (>= 3.0) katalyst-content (>= 3.2) - katalyst-govuk-formbuilder (>= 1.28.0, < 2) + katalyst-govuk-formbuilder (>= 2) katalyst-html-attributes katalyst-navigation (>= 2) katalyst-tables (>= 3.5) @@ -78,11 +78,11 @@ GEM erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - active_storage_validations (3.0.6) - activejob (>= 6.1.4) - activemodel (>= 6.1.4) - activestorage (>= 6.1.4) - activesupport (>= 6.1.4) + active_storage_validations (4.0.0) + activejob (>= 7.0.1) + activemodel (>= 7.0.1) + activestorage (>= 7.0.1) + activesupport (>= 7.0.1) marcel (>= 1.0.3) activejob (8.1.3.1) activesupport (= 8.1.3.1) @@ -229,13 +229,14 @@ GEM jwt (3.2.0) base64 katalyst-basic-auth (1.0.0) - katalyst-content (3.2.1) + katalyst-content (3.2.2) active_storage_validations activerecord katalyst-govuk-formbuilder katalyst-html-attributes view_component - katalyst-govuk-formbuilder (1.30.1) + katalyst-govuk-formbuilder (2.0.0) + activestorage (>= 8.0.0) govuk_design_system_formbuilder (>= 6.2.0) katalyst-html-attributes (1.1.2) activesupport @@ -364,7 +365,7 @@ GEM zeitwerk (~> 2.6) rainbow (3.1.1) rake (13.4.2) - rbs (4.1.1) + rbs (4.1.2) logger prism (>= 1.6.0) tsort @@ -486,7 +487,7 @@ GEM railties (>= 6.0.0) thor (1.5.0) timeout (0.6.1) - tpm-key_attestation (0.14.1) + tpm-key_attestation (0.14.2) bindata (~> 2.4) openssl (> 2.0) openssl-signature_algorithm (~> 1.0) diff --git a/app/assets/stylesheets/koi/blocks/button.css b/app/assets/stylesheets/koi/blocks/button.css index ad194501d..b9bb6db6b 100644 --- a/app/assets/stylesheets/koi/blocks/button.css +++ b/app/assets/stylesheets/koi/blocks/button.css @@ -1,4 +1,5 @@ -.button { +.button, +:where(.govuk-button) { /* Configuration */ --button-padding: 0.5em 1em; --button-gap: 0.5em; @@ -72,7 +73,8 @@ --button-padding: 0.5em; } - &[data-ghost-button] { + &[data-ghost-button], + &:where(.govuk-button--secondary) { --button-bg: transparent; --button-border-color: currentColor; --button-color: currentColor; diff --git a/app/assets/stylesheets/koi/forms/document-field.css b/app/assets/stylesheets/koi/forms/document-field.css deleted file mode 100644 index 2948c3e59..000000000 --- a/app/assets/stylesheets/koi/forms/document-field.css +++ /dev/null @@ -1,84 +0,0 @@ -.govuk-document-field { - display: grid; - grid-template-areas: - "label" - "hint" - "error" - "preview" - "file" - "supplemental"; - grid-template-columns: auto; - grid-auto-rows: auto; - - label { - grid-area: label; - } - - .govuk-hint { - grid-area: hint; - } - - .govuk-error-message { - grid-area: error; - } - - input { - grid-area: file; - - &::file-selector-button { - color: var(--color-dark); - background: var(--color-tint); - border: var(--stroke-input); - border-radius: var(--input-radius); - } - } - - .preview-file { - grid-area: preview; - position: relative; - overflow: visible; - background-color: var(--color-tint); - border: var(--stroke-input); - border-radius: var(--input-radius); - margin-bottom: var(--space-2xs); - padding: var(--space-3xs); - line-height: var(--leading-standard); - align-items: start; - margin-right: auto; - - &:not([hidden]) { - display: inline-flex; - } - - .preview-filename { - line-height: unset; - } - - .file-destroy { - appearance: none; - color: black; - border: none; - background: none; - display: inline-block; - margin-left: 0.2rem; - padding: 0; - - &::after { - content: "×"; - display: block; - position: relative; - width: 21px; - line-height: 21px; - font-size: 21px; - } - } - } - - > div:last-child { - grid-area: supplemental; - - > * + * { - margin-top: var(--flow-space, var(--space-2xs)); - } - } -} diff --git a/app/assets/stylesheets/koi/forms/file-upload.css b/app/assets/stylesheets/koi/forms/file-upload.css index e34fd2eff..2c3d45bbd 100644 --- a/app/assets/stylesheets/koi/forms/file-upload.css +++ b/app/assets/stylesheets/koi/forms/file-upload.css @@ -1,3 +1,5 @@ +/* HTML File inputs */ + .govuk-file-upload { max-width: 100%; } @@ -18,3 +20,260 @@ opacity: 0.5; cursor: not-allowed; } + +/* Drop zone (JS uploads) */ + +.govuk-file-upload-wrapper { + display: block; + position: relative; + z-index: 0; + border: var(--stroke-input); + border-radius: var(--input-radius); + padding: var(--space-xs-s); + + .govuk-form-group--error & { + border-color: var(--govuk-error-color); + } + + &:has(.govuk-file-upload-button:hover) { + border-color: var(--color-dark-glare); + } +} + +.govuk-file-upload-wrapper--disabled { + /* the button disables pointer events, so the cursor lives on the wrapper */ + cursor: not-allowed; +} + +.govuk-file-upload-button { + --button-font-weight: 400; + + width: 100%; + background: transparent; + border: none; + padding: 0; + cursor: pointer; + border-radius: var(--radius-s); + + &:disabled { + pointer-events: none; + opacity: 0.5; + } + + &:focus-visible { + outline: none; + } + + &:focus-visible .govuk-button { + outline: var(--focus-outline); + outline-offset: 2px; + } + + .govuk-attachment + & { + margin-block-start: var(--flow-space, var(--space-s)); + } +} + +.govuk-file-upload-wrapper:has(.govuk-file-upload-button--dragging) { + background-color: var(--color-tint); +} + +.govuk-file-upload-button__status { + /* visually hidden */ + border: 0; + clip: rect(0 0 0 0); + height: auto; + margin: 0; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; + white-space: nowrap; +} + +.govuk-file-upload-button__pseudo-button-container { + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: var(--space-2xs); +} + +.govuk-file-upload-button__pseudo-button { + flex-shrink: 0; +} + +.govuk-file-upload-button__instruction { + margin: 0; + text-align: left; +} + +/* Attachments */ + +.govuk-attachment { + display: grid; + grid-template-areas: "preview caption actions"; + grid-template-columns: auto 1fr auto; + margin: 0 0 var(--flow-space) 0; + padding: var(--space-xs); + grid-gap: var(--space-s); + border: 1px solid var(--color-mid); + border-radius: var(--input-radius); + + .preview { + grid-area: preview; + max-width: 4rem; + aspect-ratio: 1/1; + object-fit: cover; + border-radius: var(--radius-s); + } + + .caption { + display: flex; + flex-direction: column; + grid-area: caption; + font-family: var(--font-base); + font-size: var(--size-step-0); + padding-block-start: unset; + } + + .filename { + font-weight: var(--font-bold); + } + + .size { + color: var(--govuk-hint-color); + } + + .govuk-attachment-progress { + inline-size: auto; + max-inline-size: 16em; + margin-block-start: var(--space-xs); + block-size: var(--space-2xs); + appearance: none; + border: none; + border-radius: var(--radius-s); + overflow: hidden; + + background-color: var(--color-mid); + + &::-webkit-progress-bar { + background-color: var(--color-mid); + } + + &::-webkit-progress-value { + background-color: var(--color-dark); + transition: width var(--transition-movement); + } + + &::-moz-progress-bar { + background-color: var(--color-dark); + } + } + + .actions { + grid-area: actions; + flex-direction: column; + align-items: stretch; + justify-content: center; + } + + .actions button { + display: none; /* Buttons require JavaScript */ + } + + &[data-state] .status { + margin-block-start: auto; + } + + &[data-state="upload-successful"] { + .status { + color: var(--color-success); + } + } + + &[data-state="upload-failed"] { + border-color: var(--govuk-error-color); + + .status { + color: var(--govuk-error-color); + } + } +} + +/* With JavaScript running the button is the figure's only interactive +control; the select still carries the submitted value but leaves the +display, tab order, and accessibility tree. */ +.govuk-frontend-supported .govuk-attachment { + .actions select { + display: none; + } + + .actions button { + display: revert; + } +} + +/* + * Hide duplicate inputs when multiple is not enabled, this allows reverting + * but the value from the last select will overwrite the others in save. + */ +.govuk-file-upload-wrapper:has(input[type="file"]:not([multiple])) { + .govuk-attachment:has(+ .govuk-attachment) { + display: none; + } +} + +/* Movement */ + +.govuk-attachment { + .status { + align-self: flex-start; + } + + &[data-state="upload-successful"] { + .status { + animation: govuk-attachment-state-pop var(--transition-bounce); + } + } + + &[data-state="upload-failed"] { + .status { + animation: govuk-attachment-state-pop var(--transition-bounce); + } + } +} + +@keyframes govuk-attachment-state-pop { + 50% { + scale: 1.05; + } +} + +.govuk-attachment { + &:has(.govuk-attachment-progress):has(.preview) { + &::before { + content: ""; + position: absolute; + display: block; + margin: var(--space-xs); + width: calc(64px - 2 * var(--space-xs)); + padding: 4px; /* spinner width */ + aspect-ratio: 1; + border-radius: 50%; + background: var(--color-light); + --mask: + conic-gradient(#0000 10%, #000), linear-gradient(#000 0 0) content-box; + -webkit-mask: var(--mask); + mask: var(--mask); + -webkit-mask-composite: source-out; + mask-composite: subtract; + animation: govuk-attachment-upload-spinner 1s infinite linear; + } + } +} + +@keyframes govuk-attachment-upload-spinner { + to { + transform: rotate(1turn); + } +} diff --git a/app/assets/stylesheets/koi/forms/image-field.css b/app/assets/stylesheets/koi/forms/image-field.css deleted file mode 100644 index a1afbc204..000000000 --- a/app/assets/stylesheets/koi/forms/image-field.css +++ /dev/null @@ -1,96 +0,0 @@ -.govuk-image-field { - display: grid; - grid-template-areas: - "label preview" - "hint preview" - "error preview" - "file preview" - "supplemental supplemental"; - grid-template-columns: 1fr auto; - grid-template-rows: auto auto auto 1fr auto; - gap: 0 1rem; - - label { - grid-area: label; - } - - .govuk-hint { - grid-area: hint; - } - - .govuk-error-message { - grid-area: error; - } - - input { - grid-area: file; - align-self: start; - - &::file-selector-button { - color: var(--color-dark); - background: var(--color-tint); - border: var(--stroke-input); - border-radius: var(--input-radius); - } - } - - .preview-image { - grid-area: preview; - position: relative; - display: grid; - overflow: visible; - align-self: start; - align-items: center; - justify-items: center; - background-color: var(--color-tint); - border: var(--stroke-input); - border-radius: var(--input-radius); - - min-width: calc(8em + 2 * var(--stroke-input-width)); - aspect-ratio: 1; - - .image-thumbnail { - max-width: 8em; - max-height: 8em; - } - } - - .file-destroy { - appearance: none; - position: absolute; - background-color: var(--color-tint); - color: var(--color-dark); - border-radius: var(--input-radius); - border: var(--stroke-input); - display: block; - padding: 0; - top: calc(-1 * var(--stroke-input-width)); - right: calc(-1 * var(--stroke-input-width)); - - &::after { - content: "×"; - display: block; - position: relative; - width: 21px; - line-height: 21px; - font-size: 21px; - top: -1px; - } - } - - > div:last-child { - grid-area: supplemental; - - > * + * { - margin-top: var(--flow-space, var(--space-2xs)); - } - } - - .preview-image[hidden] { - display: block !important; - - .file-destroy { - display: none; - } - } -} diff --git a/app/assets/stylesheets/koi/forms/index.css b/app/assets/stylesheets/koi/forms/index.css index 3c8a88032..3f31b09d4 100644 --- a/app/assets/stylesheets/koi/forms/index.css +++ b/app/assets/stylesheets/koi/forms/index.css @@ -14,8 +14,6 @@ @import url("combobox.css"); @import url("date-input.css"); @import url("file-upload.css"); -@import url("image-field.css"); -@import url("document-field.css"); @import url("lexxy.css"); @import url("radios.css"); @import url("password.css"); diff --git a/app/javascript/koi/application.js b/app/javascript/koi/application.js index ec35ac262..8989d26b6 100644 --- a/app/javascript/koi/application.js +++ b/app/javascript/koi/application.js @@ -1,5 +1,4 @@ import "@hotwired/turbo-rails"; -import { initAll } from "@katalyst/govuk-formbuilder"; import "@rails/actiontext"; import Lexxy from "./utils/lexxy"; import Trix from "./utils/trix"; @@ -7,17 +6,4 @@ import Trix from "./utils/trix"; import "./controllers"; import "./elements"; -/** Initialize GOVUK */ -function initGOVUK() { - document.body.classList.toggle("js-enabled", true); - document.body.classList.toggle( - "govuk-frontend-supported", - "noModule" in HTMLScriptElement.prototype, - ); - initAll(); -} - -window.addEventListener("turbo:load", initGOVUK); -if (window.Turbo) initGOVUK(); - export default { Lexxy, Trix }; diff --git a/app/javascript/koi/controllers/index.js b/app/javascript/koi/controllers/index.js index 8538f03c4..53d10ea89 100644 --- a/app/javascript/koi/controllers/index.js +++ b/app/javascript/koi/controllers/index.js @@ -3,8 +3,8 @@ import { application } from "./application"; import content from "@katalyst/content"; application.load(content); -import govuk from "@katalyst/govuk-formbuilder"; -application.load(govuk); +import GOVUK from "@katalyst/govuk-formbuilder"; +GOVUK.start(application); import navigation from "@katalyst/navigation"; application.load(navigation); diff --git a/app/views/admin/sessions/password.html.erb b/app/views/admin/sessions/password.html.erb index f6ef7a939..b8136a734 100644 --- a/app/views/admin/sessions/password.html.erb +++ b/app/views/admin/sessions/password.html.erb @@ -13,8 +13,5 @@ <%= form.govuk_password_field :password, autofocus: true, autocomplete: "off" %> <%= hidden_field_tag(:redirect, params[:redirect]) %> <%= form.button("Next", type: :submit, class: "button") %> - - <%# init govuk js to provide the show/hide button %> - <%= govuk_formbuilder_init %> <% end %> <% end %> diff --git a/docs/koi-user-guide.md b/docs/koi-user-guide.md index 809b2e3f9..a151298fb 100644 --- a/docs/koi-user-guide.md +++ b/docs/koi-user-guide.md @@ -197,8 +197,6 @@ In views, use the provided helpers: - 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`). - ### Model Concerns - `Koi::Model::Archivable` adds an `archived_at` flag, default scopes, and helper methods such as `archive!`/`restore!` (see `app/models/concerns/koi/model/archivable.rb`). Use it for content that can be soft-deleted from the interface. diff --git a/katalyst-koi.gemspec b/katalyst-koi.gemspec index a5f09ded3..b19fa9b23 100644 --- a/katalyst-koi.gemspec +++ b/katalyst-koi.gemspec @@ -3,7 +3,7 @@ # Describe your gem and declare its dependencies: Gem::Specification.new do |s| s.name = "katalyst-koi" - s.version = "5.9.0" + s.version = "5.10.0" s.authors = ["Katalyst Interactive"] s.email = ["developers@katalyst.com.au"] s.summary = "Koi CMS admin framework" @@ -41,7 +41,7 @@ Gem::Specification.new do |s| # Katalyst libraries s.add_dependency "katalyst-content", ">= 3.2" - s.add_dependency "katalyst-govuk-formbuilder", ">= 1.28.0", "< 2" + s.add_dependency "katalyst-govuk-formbuilder", ">= 2" s.add_dependency "katalyst-html-attributes" s.add_dependency "katalyst-navigation", ">= 2" s.add_dependency "katalyst-tables", ">= 3.5" diff --git a/lib/generators/koi/helpers/attribute_helpers.rb b/lib/generators/koi/helpers/attribute_helpers.rb index 3a9a24079..24734b609 100644 --- a/lib/generators/koi/helpers/attribute_helpers.rb +++ b/lib/generators/koi/helpers/attribute_helpers.rb @@ -40,6 +40,12 @@ def attachment? @attachment.present? end + # Introspected attachments are all built with type :attachment; + # whether the field submits an array follows the reflection's macro. + def attachments? + @attachment.present? && @attachment.macro == :has_many_attached + end + def enum? @enum.present? end diff --git a/lib/koi/form/builder.rb b/lib/koi/form/builder.rb index 877dee458..38a280b04 100644 --- a/lib/koi/form/builder.rb +++ b/lib/koi/form/builder.rb @@ -70,8 +70,8 @@ def govuk_image_field(attribute_name, hint: {}, **, &) def lexxy_rich_textarea(attribute_name, **attributes, &) attributes = { class: "lexxy-content", - data: { direct_upload_url: @template.main_app.admin_direct_uploads_url }, - }.merge_html(**attributes) + data: { direct_upload_url: }, + }.merge_html(attributes) super end @@ -83,11 +83,21 @@ def lexxy_rich_textarea(attribute_name, **attributes, &) def trix_rich_textarea(attribute_name, **attributes, &) attributes = { class: "trix-content", - data: { direct_upload_url: @template.main_app.admin_direct_uploads_url }, - }.merge_html(**attributes) + data: { direct_upload_url: }, + }.merge_html(attributes) super end + + private + + def direct_upload_url + if @template.respond_to?(:admin_direct_uploads_url) + @template.admin_direct_uploads_url + elsif @template.respond_to?(:main_app) && @template.main_app.respond_to?(:admin_direct_uploads_url) + @template.main_app.admin_direct_uploads_url + end + end end end end diff --git a/lib/tasks/dummy.thor b/lib/tasks/dummy.thor index 923e29bc5..bfbbde40c 100644 --- a/lib/tasks/dummy.thor +++ b/lib/tasks/dummy.thor @@ -108,7 +108,7 @@ class Dummy < Thor inside("spec/dummy") do run <<~SH rails g koi:model Announcement name:string title:string content:rich_text published_on:date archived_at:datetime - rails g koi:model Banner name:string image:attachment ordinal:integer status:integer + rails g koi:model Banner name:string image:attachment gallery:attachments ordinal:integer status:integer SH end diff --git a/package.json b/package.json index 50fee5f8a..8d606de6b 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "devDependencies": { "@rollup/plugin-node-resolve": "^16.0.0", "@rollup/plugin-terser": "^1.0.0", - "prettier": "^3.9.4", + "prettier": "^3.9.6", "rollup": "^4.62.2" } } diff --git a/spec/generators/koi/admin_controller_generator_spec.rb b/spec/generators/koi/admin_controller_generator_spec.rb index 423e3a32f..49a9f1587 100644 --- a/spec/generators/koi/admin_controller_generator_spec.rb +++ b/spec/generators/koi/admin_controller_generator_spec.rb @@ -29,4 +29,31 @@ expect(Pathname.new(file("app/controllers/admin/announcements_controller.rb"))).to exist expect(Pathname.new(file("spec/requests/admin/announcements_controller_spec.rb"))).to exist end + + # Introspected attachments follow their reflection's macro: a has_one + # submits a scalar signed id, a has_many an array (the attachment + # field's `name[]` inputs), so the permit shapes must differ. + context "with a model with attachments" do + subject(:output) do + gen = generator(%w(banner)) + Ammeter::OutputCapturer.capture(:stdout) { gen.invoke_all } + end + + let(:controller) { File.read(file("app/controllers/admin/banners_controller.rb")) } + + it "permits a has_many attachment as an array" do + output + expect(controller).to include("gallery: []") + end + + it "permits a has_one attachment as a scalar" do + output + expect(controller).to include(":image") + end + + it "does not permit the has_one attachment as an array" do + output + expect(controller).not_to include("image: []") + end + end end diff --git a/spec/generators/koi/admin_views_generator_spec.rb b/spec/generators/koi/admin_views_generator_spec.rb index 99ceb14e1..e2d38d39b 100644 --- a/spec/generators/koi/admin_views_generator_spec.rb +++ b/spec/generators/koi/admin_views_generator_spec.rb @@ -150,6 +150,7 @@ <% row.text :name %> <% row.enum :status %> <% row.attachment :image %> + <% row.attachment :gallery %> <% end %> ERB end diff --git a/spec/helpers/koi/form_builder_spec.rb b/spec/helpers/koi/form_builder_spec.rb new file mode 100644 index 000000000..fc7fa8349 --- /dev/null +++ b/spec/helpers/koi/form_builder_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe Koi::FormBuilder do + subject(:builder) { described_class.new(:banner, Banner.new, helper, {}) } + + describe "#govuk_image_field" do + it "posts direct uploads to Koi's authorised admin endpoint" do + expect(builder.govuk_image_field(:image)).to have_css( + "input[type=file][data-direct-upload-url='#{helper.main_app.admin_direct_uploads_url}']", + visible: :all, + ) + end + end + + context "with a rich text attribute" do + subject(:builder) { described_class.new(:announcement, Announcement.new, helper, {}) } + + describe "#lexxy_rich_textarea" do + it "posts direct uploads to Koi's authorised admin endpoint" do + expect(builder.lexxy_rich_textarea(:content)).to have_css( + "lexxy-editor[data-direct-upload-url='#{helper.main_app.admin_direct_uploads_url}']", + visible: :all, + ) + end + end + + describe "#trix_rich_textarea" do + it "posts direct uploads to Koi's authorised admin endpoint" do + expect(builder.trix_rich_textarea(:content)).to have_css( + "trix-editor[data-direct-upload-url='#{helper.main_app.admin_direct_uploads_url}']", + visible: :all, + ) + end + end + end +end diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index de9d64252..bc9aec5a9 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -3,5 +3,6 @@ require "capybara/rspec" RSpec.configure do |config| + config.include Capybara::RSpecMatchers, type: :helper config.include Capybara::RSpecMatchers, type: :request end diff --git a/spec/system/admin/authentication_spec.rb b/spec/system/admin/authentication_spec.rb index 6896c839f..ec175cab6 100644 --- a/spec/system/admin/authentication_spec.rb +++ b/spec/system/admin/authentication_spec.rb @@ -32,6 +32,25 @@ expect(page).to have_current_path("/admin/dashboard") end + it "reveals and hides the password with the show/hide toggle" do + admin = create(:admin) + visit "/admin" + + fill_in "Email", with: admin.email + click_on "Next" + + expect(page).to have_field("Password", type: "password") + + # The toggle is rendered hidden and only becomes visible when the GOV.UK + # password input enhancement runs, so finding it proves the login page + # enhances without any per-view initialisation. + find(".govuk-password-input__toggle").click + expect(page).to have_field("Password", type: "text") + + find(".govuk-password-input__toggle").click + expect(page).to have_field("Password", type: "password") + end + it "supports redirect after login" do admin = create(:admin) visit "/admin/admin_users" diff --git a/spec/templates/app/models/banner.rb b/spec/templates/app/models/banner.rb index 207d73bd4..3bc1f2349 100644 --- a/spec/templates/app/models/banner.rb +++ b/spec/templates/app/models/banner.rb @@ -7,6 +7,8 @@ class Banner < ApplicationRecord image.variant :thumb, resize_to_fill: [100, 100] end + has_many_attached :gallery + scope :admin_search, ->(query) do where("name LIKE :query", query: "%#{query}%") end diff --git a/yarn.lock b/yarn.lock index 397cc2550..a7dbb315c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -274,7 +274,7 @@ picomatch@^4.0.2: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.5.tgz#51ea57a17d86f605f81039595fbc40ed06a55fab" integrity sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A== -prettier@^3.9.4: +prettier@^3.9.6: version "3.9.6" resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.9.6.tgz#b3ea5146515d40fc53f18aa63f74dfab1e10dbf6" integrity sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==