katalyst-koi 4.18.1 → 5.0.0.alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/images/koi/icons/add.svg +3 -0
  3. data/app/assets/images/koi/icons/close.svg +1 -0
  4. data/app/assets/images/koi/koi.png +0 -0
  5. data/app/assets/javascripts/koi/controllers/file_field_controller.js +2 -2
  6. data/app/assets/javascripts/koi/controllers/index.js +0 -3
  7. data/app/assets/javascripts/koi/controllers/koi/modal_controller.js +40 -0
  8. data/app/assets/javascripts/koi/controllers/navigation_controller.js +14 -21
  9. data/app/assets/javascripts/koi/controllers/webauthn_registration_controller.js +4 -1
  10. data/app/assets/stylesheets/koi/blocks/actions.css +8 -0
  11. data/app/assets/stylesheets/koi/blocks/application-header.css +15 -0
  12. data/app/assets/stylesheets/koi/blocks/application-navigation.css +54 -0
  13. data/app/assets/stylesheets/koi/blocks/button.css +90 -0
  14. data/app/assets/stylesheets/koi/blocks/flash.css +19 -0
  15. data/app/assets/stylesheets/koi/blocks/icon.css +15 -0
  16. data/app/assets/stylesheets/koi/blocks/index.css +13 -0
  17. data/app/assets/stylesheets/koi/blocks/modal.css +26 -0
  18. data/app/assets/stylesheets/koi/blocks/navigation.css +23 -0
  19. data/app/assets/stylesheets/koi/blocks/page-header.css +31 -0
  20. data/app/assets/stylesheets/koi/blocks/pagy.css +82 -0
  21. data/app/assets/stylesheets/koi/blocks/prose.css +37 -0
  22. data/app/assets/stylesheets/koi/blocks/tables/index.css +4 -0
  23. data/app/assets/stylesheets/koi/{components/_query.scss → blocks/tables/query.css} +13 -13
  24. data/app/assets/stylesheets/koi/{base/_tables.scss → blocks/tables/table.css} +11 -59
  25. data/app/assets/stylesheets/koi/compositions/cover.css +17 -0
  26. data/app/assets/stylesheets/koi/{base/_flow.scss → compositions/flow.css} +1 -1
  27. data/app/assets/stylesheets/koi/compositions/index.css +4 -0
  28. data/app/assets/stylesheets/koi/compositions/wrapper.css +11 -0
  29. data/app/assets/stylesheets/koi/forms/caption.css +22 -0
  30. data/app/assets/stylesheets/koi/forms/checkboxes.css +153 -0
  31. data/app/assets/stylesheets/koi/forms/date-input.css +12 -0
  32. data/app/assets/stylesheets/koi/{components/_document-field.scss → forms/document-field.css} +20 -15
  33. data/app/assets/stylesheets/koi/forms/errors.css +38 -0
  34. data/app/assets/stylesheets/koi/forms/fieldset.css +73 -0
  35. data/app/assets/stylesheets/koi/forms/file-upload.css +20 -0
  36. data/app/assets/stylesheets/koi/forms/form-group.css +19 -0
  37. data/app/assets/stylesheets/koi/forms/hint.css +11 -0
  38. data/app/assets/stylesheets/koi/forms/image-field.css +96 -0
  39. data/app/assets/stylesheets/koi/forms/index.css +44 -0
  40. data/app/assets/stylesheets/koi/forms/input.css +194 -0
  41. data/app/assets/stylesheets/koi/forms/label.css +43 -0
  42. data/app/assets/stylesheets/koi/forms/password.css +18 -0
  43. data/app/assets/stylesheets/koi/forms/radios.css +162 -0
  44. data/app/assets/stylesheets/koi/forms/select.css +18 -0
  45. data/app/assets/stylesheets/koi/forms/textarea.css +3 -0
  46. data/app/assets/stylesheets/koi/forms/trix.css +33 -0
  47. data/app/assets/stylesheets/koi/global/fonts.css +22 -0
  48. data/app/assets/stylesheets/koi/global/global-styles.css +297 -0
  49. data/app/assets/stylesheets/koi/global/reset.css +98 -0
  50. data/app/assets/stylesheets/koi/global/variables.css +97 -0
  51. data/app/assets/stylesheets/koi/icons.css +14 -0
  52. data/app/assets/stylesheets/koi/{admin.scss → index.css} +16 -7
  53. data/app/assets/stylesheets/koi/login.css +26 -0
  54. data/app/assets/stylesheets/koi/themes/_index.scss +0 -1
  55. data/app/assets/stylesheets/koi/utilities/index.css +1 -0
  56. data/app/assets/stylesheets/koi/utilities/visually-hidden.css +18 -0
  57. data/app/components/concerns/koi/tables/cells.rb +3 -3
  58. data/app/components/koi/header_component.html.erb +12 -11
  59. data/app/components/koi/header_component.rb +2 -0
  60. data/app/components/koi/table_component.rb +8 -0
  61. data/app/controllers/admin/admin_users_controller.rb +24 -18
  62. data/app/controllers/admin/application_controller.rb +1 -3
  63. data/app/controllers/admin/credentials_controller.rb +18 -14
  64. data/app/controllers/admin/otps_controller.rb +15 -13
  65. data/app/controllers/admin/sessions_controller.rb +12 -1
  66. data/app/controllers/admin/url_rewrites_controller.rb +19 -17
  67. data/app/controllers/admin/well_knowns_controller.rb +20 -18
  68. data/app/controllers/concerns/koi/controller.rb +37 -0
  69. data/app/helpers/koi/form_helper.rb +18 -0
  70. data/app/helpers/koi/header_helper.rb +122 -0
  71. data/app/helpers/koi/index_actions_helper.rb +3 -2
  72. data/app/helpers/koi/modal_helper.rb +71 -0
  73. data/app/models/admin/user.rb +7 -1
  74. data/app/models/url_rewrite.rb +1 -9
  75. data/app/views/admin/admin_users/_form.html+self.erb +8 -0
  76. data/app/views/admin/admin_users/_form.html.erb +8 -0
  77. data/app/views/admin/admin_users/archived.html.erb +7 -4
  78. data/app/views/admin/admin_users/edit.html+self.erb +12 -0
  79. data/app/views/admin/admin_users/edit.html.erb +13 -8
  80. data/app/views/admin/admin_users/index.html.erb +10 -5
  81. data/app/views/admin/admin_users/new.html.erb +8 -8
  82. data/app/views/admin/admin_users/show.html+self.erb +26 -14
  83. data/app/views/admin/admin_users/show.html.erb +22 -20
  84. data/app/views/admin/credentials/_credentials.html+self.erb +8 -6
  85. data/app/views/admin/credentials/_credentials.html.erb +3 -1
  86. data/app/views/admin/credentials/create.turbo_stream.erb +4 -3
  87. data/app/views/admin/credentials/destroy.turbo_stream.erb +4 -2
  88. data/app/views/admin/credentials/new.html.erb +42 -36
  89. data/app/views/admin/dashboards/show.html.erb +13 -1
  90. data/app/views/admin/otps/_form.html.erb +7 -7
  91. data/app/views/admin/otps/create.turbo_stream.erb +3 -3
  92. data/app/views/admin/otps/new.html.erb +5 -3
  93. data/app/views/admin/sessions/new.html.erb +2 -3
  94. data/app/views/admin/sessions/otp.html.erb +1 -3
  95. data/app/views/admin/sessions/password.html.erb +1 -3
  96. data/app/views/admin/tokens/show.html.erb +4 -6
  97. data/app/views/admin/url_rewrites/_form.html.erb +9 -0
  98. data/app/views/admin/url_rewrites/edit.html.erb +13 -9
  99. data/app/views/admin/url_rewrites/index.html.erb +10 -7
  100. data/app/views/admin/url_rewrites/new.html.erb +8 -8
  101. data/app/views/admin/url_rewrites/show.html.erb +17 -12
  102. data/app/views/admin/well_knowns/_form.html.erb +9 -0
  103. data/app/views/admin/well_knowns/edit.html.erb +13 -9
  104. data/app/views/admin/well_knowns/index.html.erb +8 -5
  105. data/app/views/admin/well_knowns/new.html.erb +8 -8
  106. data/app/views/admin/well_knowns/show.html.erb +14 -13
  107. data/app/views/katalyst/content/asides/_aside.html+form.erb +6 -4
  108. data/app/views/katalyst/content/columns/_column.html+form.erb +5 -3
  109. data/app/views/katalyst/content/contents/_content.html+form.erb +8 -6
  110. data/app/views/katalyst/content/figures/_figure.html+form.erb +8 -5
  111. data/app/views/katalyst/content/groups/_group.html+form.erb +5 -3
  112. data/app/views/katalyst/content/items/_item.html+form.erb +5 -3
  113. data/app/views/katalyst/content/sections/_section.html+form.erb +5 -3
  114. data/app/views/katalyst/content/tables/_table.html+form.erb +16 -11
  115. data/app/views/katalyst/navigation/items/_button.html.erb +6 -12
  116. data/app/views/katalyst/navigation/items/_heading.html.erb +3 -10
  117. data/app/views/katalyst/navigation/items/_link.html.erb +6 -11
  118. data/app/views/katalyst/navigation/menus/edit.html.erb +10 -6
  119. data/app/views/katalyst/navigation/menus/index.html.erb +4 -2
  120. data/app/views/katalyst/navigation/menus/new.html.erb +5 -3
  121. data/app/views/katalyst/navigation/menus/show.html.erb +8 -7
  122. data/app/views/layouts/koi/_application_header.html.erb +20 -0
  123. data/app/views/layouts/koi/_application_navigation.html.erb +34 -0
  124. data/app/views/layouts/koi/_flash.html.erb +6 -3
  125. data/app/views/layouts/koi/_navigation_header.html.erb +0 -2
  126. data/app/views/layouts/koi/application.html.erb +22 -27
  127. data/app/views/layouts/koi/frame.html.erb +1 -3
  128. data/app/views/layouts/koi/login.html.erb +12 -5
  129. data/config/locales/koi.en.yml +9 -1
  130. data/config/routes.rb +1 -1
  131. data/lib/generators/koi/admin/admin_generator.rb +3 -12
  132. data/lib/generators/koi/admin_controller/admin_controller_generator.rb +6 -16
  133. data/lib/generators/koi/admin_controller/templates/controller.rb.tt +82 -18
  134. data/lib/generators/koi/admin_controller/templates/controller_spec.rb.tt +113 -47
  135. data/lib/generators/koi/admin_route/admin_route_generator.rb +60 -6
  136. data/lib/generators/koi/admin_views/USAGE +18 -7
  137. data/lib/generators/koi/admin_views/admin_views_generator.rb +19 -11
  138. data/lib/generators/koi/admin_views/templates/_form.html.erb.tt +8 -0
  139. data/lib/generators/koi/admin_views/templates/archived.html.erb.tt +33 -0
  140. data/lib/generators/koi/admin_views/templates/edit.html.erb.tt +17 -9
  141. data/lib/generators/koi/admin_views/templates/index.html.erb.tt +31 -3
  142. data/lib/generators/koi/admin_views/templates/new.html.erb.tt +8 -8
  143. data/lib/generators/koi/admin_views/templates/show.html.erb.tt +15 -18
  144. data/lib/generators/koi/helpers/attribute_helpers.rb +147 -0
  145. data/lib/generators/koi/helpers/attribute_types.rb +218 -0
  146. data/lib/generators/koi/helpers/resource_helpers.rb +121 -0
  147. data/lib/generators/koi/{active_record/active_record_generator.rb → model/model_generator.rb} +1 -1
  148. data/lib/koi/config.rb +3 -1
  149. data/lib/koi/engine.rb +0 -9
  150. data/lib/koi/form/builder.rb +4 -4
  151. data/lib/koi/form/content.rb +55 -0
  152. data/lib/koi/form/elements/document.rb +1 -1
  153. data/lib/koi/form/elements/image.rb +1 -1
  154. data/lib/koi/form_builder.rb +1 -0
  155. data/lib/koi/menu.rb +14 -1
  156. data/spec/factories/admins.rb +1 -1
  157. metadata +90 -99
  158. data/app/assets/builds/koi/admin.css +0 -1
  159. data/app/assets/stylesheets/koi/base/_button.scss +0 -122
  160. data/app/assets/stylesheets/koi/base/_icon.scss +0 -29
  161. data/app/assets/stylesheets/koi/base/_index.scss +0 -21
  162. data/app/assets/stylesheets/koi/base/_input.scss +0 -19
  163. data/app/assets/stylesheets/koi/base/_link.scss +0 -26
  164. data/app/assets/stylesheets/koi/base/_list.scss +0 -11
  165. data/app/assets/stylesheets/koi/base/_typography.scss +0 -160
  166. data/app/assets/stylesheets/koi/components/_actions-group.scss +0 -7
  167. data/app/assets/stylesheets/koi/components/_image-field.scss +0 -95
  168. data/app/assets/stylesheets/koi/components/_index.scss +0 -9
  169. data/app/assets/stylesheets/koi/components/_pagy.scss +0 -29
  170. data/app/assets/stylesheets/koi/components/_summary-list.scss +0 -40
  171. data/app/assets/stylesheets/koi/layouts/_banner.scss +0 -7
  172. data/app/assets/stylesheets/koi/layouts/_content.scss +0 -40
  173. data/app/assets/stylesheets/koi/layouts/_flash.scss +0 -41
  174. data/app/assets/stylesheets/koi/layouts/_header.scss +0 -61
  175. data/app/assets/stylesheets/koi/layouts/_index.scss +0 -48
  176. data/app/assets/stylesheets/koi/layouts/_main.scss +0 -23
  177. data/app/assets/stylesheets/koi/layouts/_navigation.scss +0 -180
  178. data/app/assets/stylesheets/koi/layouts/_stack.scss +0 -13
  179. data/app/assets/stylesheets/koi/pages/_index.scss +0 -1
  180. data/app/assets/stylesheets/koi/pages/_login.scss +0 -46
  181. data/app/assets/stylesheets/koi/themes/_govuk.scss +0 -56
  182. data/app/assets/stylesheets/koi/themes/_kpop.scss +0 -5
  183. data/app/assets/stylesheets/koi/utils/_breakpoints.scss +0 -13
  184. data/app/assets/stylesheets/koi/utils/_hide.scss +0 -11
  185. data/app/assets/stylesheets/koi/utils/_index.scss +0 -2
  186. data/app/assets/stylesheets/koi/utils/_typography.scss +0 -42
  187. data/app/components/koi/content/editor/item_form_component.html.erb +0 -11
  188. data/app/components/koi/content/editor/item_form_component.rb +0 -94
  189. data/app/components/koi/summary_list/attachment_component.rb +0 -47
  190. data/app/components/koi/summary_list/base.rb +0 -59
  191. data/app/components/koi/summary_list/boolean_component.rb +0 -15
  192. data/app/components/koi/summary_list/date_component.rb +0 -17
  193. data/app/components/koi/summary_list/datetime_component.rb +0 -17
  194. data/app/components/koi/summary_list/item_component.rb +0 -26
  195. data/app/components/koi/summary_list/number_component.rb +0 -21
  196. data/app/components/koi/summary_list/rich_text_component.rb +0 -8
  197. data/app/components/koi/summary_list/text_component.rb +0 -8
  198. data/app/components/koi/summary_list_component.html.erb +0 -5
  199. data/app/components/koi/summary_list_component.rb +0 -75
  200. data/app/controllers/concerns/koi/controller/is_admin_controller.rb +0 -66
  201. data/app/helpers/koi/application_helper.rb +0 -7
  202. data/app/helpers/koi/date_helper.rb +0 -26
  203. data/app/helpers/koi/definition_list_helper.rb +0 -10
  204. data/app/views/admin/admin_users/_fields.html+self.erb +0 -3
  205. data/app/views/admin/admin_users/_fields.html.erb +0 -3
  206. data/app/views/admin/url_rewrites/_fields.html.erb +0 -4
  207. data/app/views/admin/well_knowns/_fields.html.erb +0 -6
  208. data/app/views/layouts/koi/_environment.html.erb +0 -4
  209. data/app/views/layouts/koi/_header.html.erb +0 -11
  210. data/app/views/layouts/koi/_navigation.html.erb +0 -23
  211. data/app/views/layouts/koi/_navigation_collapse.html.erb +0 -3
  212. data/lib/generators/koi/admin_views/templates/_fields.html.erb.tt +0 -3
  213. data/lib/generators/koi/helpers/admin_generator_attributes.rb +0 -66
  214. data/lib/koi/extensions/dartsass.rb +0 -23
  215. /data/app/assets/stylesheets/koi/{components/_clipboard.scss → blocks/clipboard.css} +0 -0
  216. /data/app/assets/stylesheets/koi/{components/_index-actions.scss → blocks/index-actions.css} +0 -0
  217. /data/app/assets/stylesheets/koi/{components/_toolbar.scss → blocks/toolbar.css} +0 -0
  218. /data/app/assets/stylesheets/koi/{base/_repel.scss → compositions/repel.css} +0 -0
@@ -2,9 +2,9 @@
2
2
 
3
3
  module Admin
4
4
  class AdminUsersController < ApplicationController
5
- before_action :set_admin, only: %i[show edit update destroy]
5
+ before_action :set_admin_user, only: %i[show edit update destroy]
6
6
 
7
- attr_reader :admin
7
+ attr_reader :admin_user
8
8
 
9
9
  def index
10
10
  collection = Collection.new.with_params(params).apply(Admin::User.strict_loading)
@@ -19,34 +19,34 @@ module Admin
19
19
  end
20
20
 
21
21
  def show
22
- render :show, locals: { admin: }
22
+ render locals: { admin_user: }
23
23
  end
24
24
 
25
25
  def new
26
- @admin = Admin::User.new
26
+ @admin_user = Admin::User.new
27
27
 
28
- render :new, locals: { admin: }
28
+ render locals: { admin_user: }
29
29
  end
30
30
 
31
31
  def edit
32
- render :edit, locals: { admin: }
32
+ render :edit, locals: { admin_user: }
33
33
  end
34
34
 
35
35
  def create
36
- admin = Admin::User.new(admin_user_params)
36
+ admin_user = Admin::User.new(admin_user_params)
37
37
 
38
- if admin.save
39
- redirect_to admin_admin_user_path(admin)
38
+ if admin_user.save
39
+ redirect_to admin_admin_user_path(admin_user)
40
40
  else
41
- render :new, locals: { admin: }, status: :unprocessable_content
41
+ render :new, locals: { admin_user: }, status: :unprocessable_content
42
42
  end
43
43
  end
44
44
 
45
45
  def update
46
- if admin.update(admin_user_params)
46
+ if admin_user.update(admin_user_params)
47
47
  redirect_to action: :show
48
48
  else
49
- render :edit, locals: { admin: }, status: :unprocessable_content
49
+ render :edit, locals: { admin_user: }, status: :unprocessable_content
50
50
  end
51
51
  end
52
52
 
@@ -63,21 +63,27 @@ module Admin
63
63
  end
64
64
 
65
65
  def destroy
66
- admin.destroy
66
+ if admin_user.archived?
67
+ admin_user.destroy!
67
68
 
68
- redirect_to admin_admin_users_path
69
+ redirect_to admin_admin_users_path
70
+ else
71
+ admin_user.archive!
72
+
73
+ redirect_back_or_to(admin_admin_user_path(admin_user), status: :see_other)
74
+ end
69
75
  end
70
76
 
71
77
  private
72
78
 
73
- def set_admin
74
- @admin = Admin::User.with_archived.find(params[:id])
79
+ def set_admin_user
80
+ @admin_user = Admin::User.with_archived.find(params[:id])
75
81
 
76
- request.variant << :self if @admin == current_admin_user
82
+ request.variant << :self if admin_user == current_admin_user
77
83
  end
78
84
 
79
85
  def admin_user_params
80
- params.expect(admin: %i[name email password archived])
86
+ params.expect(admin_user: %i[name email password archived])
81
87
  end
82
88
 
83
89
  class Collection < Admin::Collection
@@ -2,8 +2,6 @@
2
2
 
3
3
  module Admin
4
4
  class ApplicationController < ActionController::Base
5
- include Koi::Controller::IsAdminController
6
-
7
- protect_from_forgery with: :exception
5
+ include Koi::Controller
8
6
  end
9
7
  end
@@ -6,25 +6,29 @@ module Admin
6
6
 
7
7
  before_action :set_admin_user
8
8
 
9
+ attr_reader :admin_user
10
+
9
11
  def new
10
- unless @admin_user.webauthn_id
11
- @admin_user.update!(webauthn_id: WebAuthn.generate_user_id)
12
+ unless admin_user.webauthn_id
13
+ admin_user.update!(webauthn_id: WebAuthn.generate_user_id)
12
14
  end
13
15
 
14
16
  options = webauthn_relying_party.options_for_registration(
15
17
  user: {
16
- id: @admin_user.webauthn_id,
17
- name: @admin_user.email,
18
- display_name: @admin_user.to_s,
18
+ id: admin_user.webauthn_id,
19
+ name: admin_user.email,
20
+ display_name: admin_user.name,
19
21
  },
20
- exclude: @admin_user.credentials.map(&:external_id),
22
+ exclude: admin_user.credentials.map(&:external_id),
21
23
  )
22
24
 
23
25
  # Store the newly generated challenge somewhere so you can have it
24
26
  # for the verification phase.
25
27
  session[:creation_challenge] = options.challenge
26
28
 
27
- render :new, locals: { admin: @admin_user, options: }
29
+ credential = admin_user.credentials.new
30
+
31
+ render locals: { admin_user:, credential:, options: }
28
32
  end
29
33
 
30
34
  def create
@@ -35,7 +39,7 @@ module Admin
35
39
  session.delete(:creation_challenge),
36
40
  )
37
41
 
38
- credential = @admin_user.credentials.find_or_initialize_by(
42
+ credential = admin_user.credentials.find_or_initialize_by(
39
43
  external_id: webauthn_credential.id,
40
44
  )
41
45
 
@@ -46,18 +50,18 @@ module Admin
46
50
  )
47
51
 
48
52
  respond_to do |format|
49
- format.html { redirect_to admin_admin_user_path(@admin_user), status: :see_other }
50
- format.turbo_stream { render locals: { admin: @admin_user } }
53
+ format.html { redirect_to admin_admin_user_path(admin_user), status: :see_other }
54
+ format.turbo_stream { render locals: { admin_user: } }
51
55
  end
52
56
  end
53
57
 
54
58
  def destroy
55
- credential = @admin_user.credentials.find(params[:id])
59
+ credential = admin_user.credentials.find(params[:id])
56
60
  credential.destroy!
57
61
 
58
62
  respond_to do |format|
59
- format.html { redirect_to admin_admin_user_path(@admin_user), status: :see_other }
60
- format.turbo_stream { render locals: { admin: @admin_user } }
63
+ format.html { redirect_to admin_admin_user_path(admin_user), status: :see_other }
64
+ format.turbo_stream { render locals: { admin_user: } }
61
65
  end
62
66
  end
63
67
 
@@ -70,7 +74,7 @@ module Admin
70
74
  def set_admin_user
71
75
  @admin_user = Admin::User.find(params[:admin_user_id])
72
76
 
73
- if current_admin == @admin_user
77
+ if current_admin == admin_user
74
78
  request.variant = :self
75
79
  else
76
80
  head(:forbidden)
@@ -4,45 +4,47 @@ module Admin
4
4
  class OtpsController < ApplicationController
5
5
  before_action :set_admin_user
6
6
 
7
+ attr_reader :admin_user
8
+
7
9
  def new
8
- @admin_user.otp_secret = ROTP::Base32.random
10
+ admin_user.otp_secret = ROTP::Base32.random
9
11
 
10
- render :new, locals: { admin: @admin_user }
12
+ render :new, locals: { admin_user: }
11
13
  end
12
14
 
13
15
  def create
14
- @admin_user.otp_secret = otp_params[:otp_secret]
16
+ admin_user.otp_secret = otp_params[:otp_secret]
15
17
 
16
- if @admin_user.otp.verify(otp_params[:token])
17
- @admin_user.save
18
+ if admin_user.otp.verify(otp_params[:token])
19
+ admin_user.save
18
20
 
19
- redirect_to admin_admin_user_path(@admin_user), status: :see_other
21
+ redirect_to admin_admin_user_path(admin_user), status: :see_other
20
22
  else
21
- @admin_user.errors.add(:token, :invalid)
23
+ admin_user.errors.add(:token, :invalid)
22
24
 
23
25
  respond_to do |format|
24
- format.html { redirect_to admin_admin_user_path(@admin_user), status: :see_other }
25
- format.turbo_stream { render locals: { admin: @admin_user } }
26
+ format.html { redirect_to admin_admin_user_path(admin_user), status: :see_other }
27
+ format.turbo_stream { render locals: { admin_user: }, status: :unprocessable_entity }
26
28
  end
27
29
  end
28
30
  end
29
31
 
30
32
  def destroy
31
- @admin_user.update!(otp_secret: nil)
33
+ admin_user.update!(otp_secret: nil)
32
34
 
33
- redirect_to admin_admin_user_path(@admin_user), status: :see_other
35
+ redirect_to admin_admin_user_path(admin_user), status: :see_other
34
36
  end
35
37
 
36
38
  private
37
39
 
38
40
  def otp_params
39
- params.expect(admin: %i[otp_secret token])
41
+ params.expect(admin_user: %i[otp_secret token])
40
42
  end
41
43
 
42
44
  def set_admin_user
43
45
  @admin_user = Admin::User.find(params[:admin_user_id])
44
46
 
45
- if current_admin == @admin_user
47
+ if current_admin == admin_user
46
48
  request.variant = :self
47
49
  else
48
50
  head(:forbidden)
@@ -6,7 +6,7 @@ module Admin
6
6
  include Koi::Controller::RecordsAuthentication
7
7
 
8
8
  before_action :redirect_authenticated, only: %i[new], if: :admin_signed_in?
9
- before_action :authenticate_local_admin, only: %i[new], if: :authenticate_local_admins?
9
+ before_action :authenticate_local_admin, only: %i[new], if: -> { Koi.config.authenticate_local_admins? }
10
10
 
11
11
  layout "koi/login"
12
12
 
@@ -94,6 +94,17 @@ module Admin
94
94
  redirect_to(admin_dashboard_path, status: :see_other)
95
95
  end
96
96
 
97
+ def authenticate_local_admin
98
+ return if admin_signed_in? || !Rails.env.development?
99
+
100
+ session[:admin_user_id] =
101
+ Admin::User.where(email: %W[#{ENV.fetch('USER', nil)}@katalyst.com.au admin@katalyst.com.au]).first&.id
102
+
103
+ flash.delete(:redirect) if (redirect = flash[:redirect])
104
+
105
+ redirect_to(redirect || admin_dashboard_path, status: :see_other)
106
+ end
107
+
97
108
  def admin_sign_in(admin_user)
98
109
  record_sign_in!(admin_user)
99
110
 
@@ -4,68 +4,70 @@ module Admin
4
4
  class UrlRewritesController < ApplicationController
5
5
  before_action :set_url_rewrite, only: %i[show edit update destroy]
6
6
 
7
+ attr_reader :collection, :url_rewrite
8
+
7
9
  def index
8
- collection = Collection.new.with_params(params).apply(UrlRewrite.strict_loading)
10
+ @collection = Collection.new.with_params(params).apply(UrlRewrite.strict_loading.all)
9
11
 
10
12
  render locals: { collection: }
11
13
  end
12
14
 
13
15
  def show
14
- render :show, locals: { url_rewrite: @url_rewrite }
16
+ render locals: { url_rewrite: }
15
17
  end
16
18
 
17
19
  def new
18
20
  @url_rewrite = UrlRewrite.new
19
- render :new, locals: { url_rewrite: @url_rewrite }
21
+
22
+ render locals: { url_rewrite: }
20
23
  end
21
24
 
22
25
  def edit
23
- render :edit, locals: { url_rewrite: @url_rewrite }
26
+ render locals: { url_rewrite: }
24
27
  end
25
28
 
26
29
  def create
27
30
  @url_rewrite = UrlRewrite.new(url_rewrite_params)
28
31
 
29
32
  if @url_rewrite.save
30
- redirect_to admin_url_rewrite_path(@url_rewrite)
33
+ redirect_to admin_url_rewrite_path(url_rewrite), status: :see_other
31
34
  else
32
- render :new, status: :unprocessable_content, locals: { url_rewrite: @url_rewrite }
35
+ render :new, locals: { url_rewrite: }, status: :unprocessable_content
33
36
  end
34
37
  end
35
38
 
36
39
  def update
37
- @url_rewrite.attributes = url_rewrite_params
38
-
39
- if @url_rewrite.save
40
- redirect_to admin_url_rewrite_path(@url_rewrite)
40
+ if url_rewrite.update(url_rewrite_params)
41
+ redirect_to admin_url_rewrite_path(url_rewrite), status: :see_other
41
42
  else
42
- render :edit, status: :unprocessable_content, locals: { url_rewrite: @url_rewrite }
43
+ render :edit, locals: { url_rewrite: }, status: :unprocessable_content
43
44
  end
44
45
  end
45
46
 
46
47
  def destroy
47
48
  @url_rewrite.destroy!
48
49
 
49
- redirect_to admin_url_rewrites_path
50
+ redirect_to admin_url_rewrites_path, status: :see_other
50
51
  end
51
52
 
52
53
  private
53
54
 
54
- def url_rewrite_params
55
- params.expect(url_rewrite: %i[from to status_code active])
55
+ def set_url_rewrite
56
+ @url_rewrite = ::UrlRewrite.find(params[:id])
56
57
  end
57
58
 
58
- def set_url_rewrite
59
- @url_rewrite = UrlRewrite.find(params[:id])
59
+ def url_rewrite_params
60
+ params.expect(url_rewrite: %i[from to active status_code])
60
61
  end
61
62
 
62
63
  class Collection < Admin::Collection
63
- config.sorting = "from"
64
+ config.sorting = :from
64
65
  config.paginate = true
65
66
 
66
67
  attribute :from, :string
67
68
  attribute :to, :string
68
69
  attribute :active, :boolean
70
+ attribute :status_code, :enum
69
71
  end
70
72
  end
71
73
  end
@@ -4,67 +4,69 @@ module Admin
4
4
  class WellKnownsController < ApplicationController
5
5
  before_action :set_well_known, only: %i[show edit update destroy]
6
6
 
7
+ attr_reader :collection, :well_known
8
+
7
9
  def index
8
- collection = Collection.new.with_params(params).apply(::WellKnown.strict_loading.all)
10
+ @collection = Collection.new.with_params(params).apply(WellKnown.strict_loading.all)
9
11
 
10
12
  render locals: { collection: }
11
13
  end
12
14
 
13
15
  def show
14
- render locals: { well_known: @well_known }
16
+ render locals: { well_known: }
15
17
  end
16
18
 
17
19
  def new
18
- render locals: { well_known: ::WellKnown.new }
20
+ @well_known = WellKnown.new
21
+
22
+ render locals: { well_known: }
19
23
  end
20
24
 
21
25
  def edit
22
- render locals: { well_known: @well_known }
26
+ render locals: { well_known: }
23
27
  end
24
28
 
25
29
  def create
26
- @well_known = ::WellKnown.new(well_known_params)
30
+ @well_known = WellKnown.new(well_known_params)
27
31
 
28
32
  if @well_known.save
29
- redirect_to [:admin, @well_known], status: :see_other
33
+ redirect_to admin_well_known_path(well_known), status: :see_other
30
34
  else
31
- render :new, locals: { well_known: @well_known }, status: :unprocessable_content
35
+ render :new, locals: { well_known: }, status: :unprocessable_content
32
36
  end
33
37
  end
34
38
 
35
39
  def update
36
- if @well_known.update(well_known_params)
37
- redirect_to action: :show, status: :see_other
40
+ if well_known.update(well_known_params)
41
+ redirect_to admin_well_known_path(well_known), status: :see_other
38
42
  else
39
- render :edit, locals: { well_known: @well_known }, status: :unprocessable_content
43
+ render :edit, locals: { well_known: }, status: :unprocessable_content
40
44
  end
41
45
  end
42
46
 
43
47
  def destroy
44
48
  @well_known.destroy!
45
49
 
46
- redirect_to action: :index, status: :see_other
50
+ redirect_to admin_well_knowns_path, status: :see_other
47
51
  end
48
52
 
49
53
  private
50
54
 
51
- # Only allow a list of trusted parameters through.
52
- def well_known_params
53
- params.expect(well_known: %i[name purpose content_type content])
54
- end
55
-
56
- # Use callbacks to share common setup or constraints between actions.
57
55
  def set_well_known
58
56
  @well_known = ::WellKnown.find(params[:id])
59
57
  end
60
58
 
59
+ def well_known_params
60
+ params.expect(well_known: %i[name purpose content_type content])
61
+ end
62
+
61
63
  class Collection < Admin::Collection
62
64
  config.sorting = :name
63
65
  config.paginate = true
64
66
 
65
67
  attribute :name, :string
66
68
  attribute :purpose, :string
67
- attribute :content_type, :string
69
+ attribute :content_type, :enum
68
70
  attribute :content, :string
69
71
  end
70
72
  end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Koi
4
+ module Controller
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ include HasAdminUsers
9
+ include HasAttachments
10
+ include Katalyst::Tables::Backend
11
+ include ::Pagy::Backend
12
+
13
+ default_form_builder "Koi::FormBuilder"
14
+ default_table_component "Koi::TableComponent"
15
+ default_table_query_component "Koi::TableQueryComponent"
16
+ default_summary_table_component "Koi::SummaryTableComponent"
17
+
18
+ # Dependency helpers
19
+ helper Katalyst::GOVUK::Formbuilder::Frontend
20
+ helper Katalyst::Navigation::FrontendHelper
21
+ helper Katalyst::Tables::Frontend
22
+ helper ::Pagy::Frontend
23
+
24
+ # Koi Helpers
25
+ helper FormHelper
26
+ helper HeaderHelper
27
+ helper Pagy::Frontend
28
+
29
+ # @deprecated to be removed in Koi 5
30
+ helper IndexActionsHelper
31
+
32
+ layout -> { turbo_frame_request? ? "koi/frame" : "koi/application" }
33
+
34
+ protect_from_forgery with: :exception
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Koi
4
+ module FormHelper
5
+ # Ensure that admin forms use `admin_` path helpers.
6
+ def form_with(model: false, url: nil, format: nil, **, &)
7
+ if model && (url != false)
8
+ url ||= if format.nil?
9
+ polymorphic_path([:admin, model], {})
10
+ else
11
+ polymorphic_path([:admin, model], format: format)
12
+ end
13
+ end
14
+
15
+ super
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Koi
4
+ module HeaderHelper
5
+ # This helper generates an accessible breadcrumb navigation structure with
6
+ # proper ARIA attributes for screen readers. The breadcrumb items should be
7
+ # provided as content within the block.
8
+ #
9
+ # @param ** Additional HTML attributes to merge with the default breadcrumb attributes
10
+ # @yield [Block] The block should contain the breadcrumb items (typically `<li>` elements)
11
+ # @return [String, nil] Returns the HTML `<nav>` element with breadcrumbs if content is present, nil otherwise
12
+ #
13
+ # @example Basic usage with list items
14
+ # <%= breadcrumb_list do %>
15
+ # <li><%= link_to "Home", root_path %></li>
16
+ # <li><%= link_to "Products", products_path %></li>
17
+ # <% end %>
18
+ #
19
+ # @see https://www.w3.org/WAI/ARIA/apg/patterns/breadcrumb/ ARIA breadcrumb pattern
20
+ def breadcrumb_list(**, &)
21
+ return if (content = capture(&)).blank?
22
+
23
+ tag.nav(**_koi_breadcrumbs_attributes(**)) do
24
+ tag.ol(content, class: "breadcrumbs-list", role: "list")
25
+ end
26
+ end
27
+
28
+ # This helper generates an accessible actions navigation structure with
29
+ # proper ARIA attributes for screen readers. The action items should be
30
+ # provided as content within the block.
31
+ #
32
+ # @param ** Additional HTML attributes to merge with the default actions attributes
33
+ # @yield [Block] The block should contain the action items (typically `<li>` elements)
34
+ # @return [String, nil] Returns the HTML `<nav>` element with actions if content is present, nil otherwise
35
+ #
36
+ # @example Basic usage with CRUD and contextual actions
37
+ # <%= actions_list do %>
38
+ # <li><%= link_to "Edit", edit_article_path(article) %></li>
39
+ # <li><%= link_to_archive_or_delete(article) %></li>
40
+ # <li><%= link_to "Config", articles_config_path(article) %></li>
41
+ # <% end %>
42
+ #
43
+ # @see https://www.w3.org/WAI/ARIA/apg/practices/names-and-descriptions/ ARIA naming and descriptions
44
+ def actions_list(**, &)
45
+ return if (content = capture(&)).blank?
46
+
47
+ tag.nav(**_koi_actions_attributes(**)) do
48
+ tag.ul(content, class: "actions-list", role: "list")
49
+ end
50
+ end
51
+
52
+ # Creates a delete link with confirmation.
53
+ #
54
+ # @param model [ActiveRecord::Base] The record to create a delete link for
55
+ # @param text [String] Text to display for delete link (default: "Delete")
56
+ # @param confirm [String] Confirmation message for delete action (default: "Are you sure?")
57
+ # @param url [String] Target url for delete actions (defaults to admin_<record>_path)
58
+ # @param ** Additional HTML attributes to pass to the link
59
+ # @return [String, nil] Returns the HTML link element, or nil if record is not persisted
60
+ #
61
+ # @example Basic usage
62
+ # <%= link_to_delete(user) %>
63
+ def link_to_delete(model,
64
+ text = "Delete",
65
+ confirm: "Are you sure?",
66
+ url: polymorphic_path([:admin, model]),
67
+ **)
68
+ return unless model.persisted?
69
+
70
+ link_to(text, url, data: { turbo_method: :delete, turbo_confirm: confirm }, **)
71
+ end
72
+
73
+ # Conditionally creates an archive or delete link based on the record's status.
74
+ #
75
+ # @param model [ActiveRecord::Base] The record to create an archive/delete link for
76
+ # @param archive_text [String] Text to display for archive link (default: "Archive")
77
+ # @param delete_text [String] Text to display for delete link (default: "Delete")
78
+ # @param confirm [String] Confirmation message for delete action (default: "Are you sure?")
79
+ # @param url [String] Target url for delete actions (defaults to admin_<record>_path)
80
+ # @param ** Additional HTML attributes to pass to the link
81
+ # @return [String, nil] Returns the HTML link element, or nil if record is not persisted
82
+ #
83
+ # @example Basic usage
84
+ # <%= link_to_archive_or_delete(user) %>
85
+ #
86
+ # @see Koi::Model::Archivable For more information about the archivable concern
87
+ def link_to_archive_or_delete(model,
88
+ archive_text: "Archive",
89
+ delete_text: "Delete",
90
+ confirm: "Are you sure?",
91
+ url: polymorphic_path([:admin, model]),
92
+ **)
93
+ raise ArgumentError unless model.respond_to?(:archived?)
94
+
95
+ return unless model.persisted?
96
+
97
+ if model.archived?
98
+ link_to(delete_text, url, data: { turbo_method: :delete, turbo_confirm: confirm }, **)
99
+ else
100
+ link_to(archive_text, url, data: { turbo_method: :delete }, **)
101
+ end
102
+ end
103
+
104
+ private
105
+
106
+ using Katalyst::HtmlAttributes::HasHtmlAttributes
107
+
108
+ def _koi_breadcrumbs_attributes(**attributes)
109
+ {
110
+ aria: { label: "Breadcrumb" },
111
+ class: "breadcrumb",
112
+ }.merge_html(attributes)
113
+ end
114
+
115
+ def _koi_actions_attributes(**attributes)
116
+ {
117
+ aria: { label: "Actions" },
118
+ class: "actions",
119
+ }.merge_html(attributes)
120
+ end
121
+ end
122
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Koi
4
4
  module IndexActionsHelper
5
+ # @deprecated to be removed in Koi 5
5
6
  def koi_index_actions(search: false, create: false, &)
6
7
  IndexActionsBuilder.new(self, search:, create:).render(&)
7
8
  end
@@ -43,7 +44,7 @@ module Koi
43
44
  end
44
45
 
45
46
  def search(form)
46
- tag.div class: "actions-group" do
47
+ tag.div class: "actions" do
47
48
  concat(search_button(form)) if search?
48
49
  concat(create_button(form)) if create?
49
50
  concat(search_input(form)) if search?
@@ -75,7 +76,7 @@ module Koi
75
76
  end
76
77
 
77
78
  def links(form, &)
78
- tag.div(class: "actions-group") do
79
+ tag.div(class: "actions") do
79
80
  yield(form) if block_given?
80
81
  end
81
82
  end