katalyst-koi 4.0.0

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 (206) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +23 -0
  4. data/Upgrade.md +6 -0
  5. data/app/assets/builds/koi/admin.css +1 -0
  6. data/app/assets/builds/koi/nav_items.css +1 -0
  7. data/app/assets/config/koi.js +10 -0
  8. data/app/assets/images/koi/application/chevron-right.svg +10 -0
  9. data/app/assets/images/koi/application/glyphicons-halflings-white.png +0 -0
  10. data/app/assets/images/koi/application/glyphicons-halflings.png +0 -0
  11. data/app/assets/images/koi/application/icon-collapse-down.png +0 -0
  12. data/app/assets/images/koi/application/icon-collapse-up.png +0 -0
  13. data/app/assets/images/koi/application/icon-file-doc.png +0 -0
  14. data/app/assets/images/koi/application/icon-file-img.png +0 -0
  15. data/app/assets/images/koi/application/icon-file-pdf.png +0 -0
  16. data/app/assets/images/koi/application/icon-file-ppt.png +0 -0
  17. data/app/assets/images/koi/application/icon-file-unknown.png +0 -0
  18. data/app/assets/images/koi/application/icon-file-xls.png +0 -0
  19. data/app/assets/images/koi/application/icon-file-zip.png +0 -0
  20. data/app/assets/images/koi/application/icon-form-date-picker.png +0 -0
  21. data/app/assets/images/koi/application/icon-form-error.png +0 -0
  22. data/app/assets/images/koi/application/icon-index-sort-ascending.png +0 -0
  23. data/app/assets/images/koi/application/icon-index-sort-descending.png +0 -0
  24. data/app/assets/images/koi/application/icon-index-sort.png +0 -0
  25. data/app/assets/images/koi/application/icon-index-sortable.png +0 -0
  26. data/app/assets/images/koi/application/icon-menu-cursor.png +0 -0
  27. data/app/assets/images/koi/application/icon-overlay-add.png +0 -0
  28. data/app/assets/images/koi/application/icon-overlay-close.png +0 -0
  29. data/app/assets/images/koi/application/icon-sortable.png +0 -0
  30. data/app/assets/images/koi/application/jcrop.gif +0 -0
  31. data/app/assets/images/koi/application/loading.gif +0 -0
  32. data/app/assets/images/koi/application/select-arrow.svg +3 -0
  33. data/app/assets/images/koi/application/select_arrow.png +0 -0
  34. data/app/assets/images/koi/application/sort-ascending.png +0 -0
  35. data/app/assets/images/koi/application/sort-descending.png +0 -0
  36. data/app/assets/javascripts/koi/admin.js +4 -0
  37. data/app/assets/javascripts/koi/controllers/application.js +11 -0
  38. data/app/assets/javascripts/koi/controllers/document_field_controller.js +26 -0
  39. data/app/assets/javascripts/koi/controllers/file_field_controller.js +143 -0
  40. data/app/assets/javascripts/koi/controllers/flash_controller.js +12 -0
  41. data/app/assets/javascripts/koi/controllers/form_request_submit_controller.js +11 -0
  42. data/app/assets/javascripts/koi/controllers/image_field_controller.js +24 -0
  43. data/app/assets/javascripts/koi/controllers/index.js +6 -0
  44. data/app/assets/javascripts/koi/controllers/index_actions_controller.js +61 -0
  45. data/app/assets/javascripts/koi/controllers/keyboard_controller.js +149 -0
  46. data/app/assets/javascripts/koi/controllers/navigation_controller.js +84 -0
  47. data/app/assets/javascripts/koi/controllers/navigation_toggle_controller.js +7 -0
  48. data/app/assets/javascripts/koi/controllers/show_hide_controller.js +25 -0
  49. data/app/assets/javascripts/koi/controllers/sluggable_controller.js +30 -0
  50. data/app/assets/javascripts/koi/controllers/webauthn_authentication_controller.js +23 -0
  51. data/app/assets/javascripts/koi/controllers/webauthn_registration_controller.js +30 -0
  52. data/app/assets/javascripts/koi/utils/transition.js +220 -0
  53. data/app/assets/stylesheets/koi/admin.scss +27 -0
  54. data/app/assets/stylesheets/koi/base/_button.scss +122 -0
  55. data/app/assets/stylesheets/koi/base/_icon.scss +29 -0
  56. data/app/assets/stylesheets/koi/base/_index.scss +18 -0
  57. data/app/assets/stylesheets/koi/base/_input.scss +13 -0
  58. data/app/assets/stylesheets/koi/base/_link.scss +26 -0
  59. data/app/assets/stylesheets/koi/base/_list.scss +11 -0
  60. data/app/assets/stylesheets/koi/base/_typography.scss +160 -0
  61. data/app/assets/stylesheets/koi/components/_actions-group.scss +7 -0
  62. data/app/assets/stylesheets/koi/components/_image-field.scss +33 -0
  63. data/app/assets/stylesheets/koi/components/_index-actions.scss +69 -0
  64. data/app/assets/stylesheets/koi/components/_index-table.scss +91 -0
  65. data/app/assets/stylesheets/koi/components/_index.scss +6 -0
  66. data/app/assets/stylesheets/koi/components/_item-table.scss +33 -0
  67. data/app/assets/stylesheets/koi/components/_pagy.scss +41 -0
  68. data/app/assets/stylesheets/koi/layouts/_banner.scss +7 -0
  69. data/app/assets/stylesheets/koi/layouts/_content.scss +40 -0
  70. data/app/assets/stylesheets/koi/layouts/_flash.scss +41 -0
  71. data/app/assets/stylesheets/koi/layouts/_header.scss +62 -0
  72. data/app/assets/stylesheets/koi/layouts/_index.scss +48 -0
  73. data/app/assets/stylesheets/koi/layouts/_main.scss +23 -0
  74. data/app/assets/stylesheets/koi/layouts/_navigation.scss +156 -0
  75. data/app/assets/stylesheets/koi/layouts/_stack.scss +13 -0
  76. data/app/assets/stylesheets/koi/pages/_index.scss +1 -0
  77. data/app/assets/stylesheets/koi/pages/_login.scss +40 -0
  78. data/app/assets/stylesheets/koi/themes/_content.scss +5 -0
  79. data/app/assets/stylesheets/koi/themes/_govuk.scss +52 -0
  80. data/app/assets/stylesheets/koi/themes/_index.scss +5 -0
  81. data/app/assets/stylesheets/koi/themes/_kpop.scss +5 -0
  82. data/app/assets/stylesheets/koi/themes/_navigation.scss +5 -0
  83. data/app/assets/stylesheets/koi/themes/_trix.scss +32 -0
  84. data/app/assets/stylesheets/koi/utils/_breakpoints.scss +13 -0
  85. data/app/assets/stylesheets/koi/utils/_hide.scss +11 -0
  86. data/app/assets/stylesheets/koi/utils/_index.scss +2 -0
  87. data/app/assets/stylesheets/koi/utils/_typography.scss +24 -0
  88. data/app/components/koi/header/edit_component.rb +58 -0
  89. data/app/components/koi/header/index_component.rb +23 -0
  90. data/app/components/koi/header/new_component.rb +40 -0
  91. data/app/components/koi/header/show_component.rb +51 -0
  92. data/app/components/koi/header_component.html.erb +16 -0
  93. data/app/components/koi/header_component.rb +28 -0
  94. data/app/components/koi/index_table_component.rb +21 -0
  95. data/app/controllers/admin/admin_users_controller.rb +88 -0
  96. data/app/controllers/admin/application_controller.rb +9 -0
  97. data/app/controllers/admin/caches_controller.rb +11 -0
  98. data/app/controllers/admin/credentials_controller.rb +64 -0
  99. data/app/controllers/admin/dashboards_controller.rb +7 -0
  100. data/app/controllers/admin/sessions_controller.rb +78 -0
  101. data/app/controllers/admin/url_rewrites_controller.rb +87 -0
  102. data/app/controllers/concerns/koi/controller/has_admin_users.rb +49 -0
  103. data/app/controllers/concerns/koi/controller/has_webauthn.rb +45 -0
  104. data/app/controllers/concerns/koi/controller/is_admin_controller.rb +52 -0
  105. data/app/helpers/katalyst/content/editor/errors.rb +21 -0
  106. data/app/helpers/katalyst/navigation/editor/errors.rb +21 -0
  107. data/app/helpers/koi/application_helper.rb +7 -0
  108. data/app/helpers/koi/date_helper.rb +36 -0
  109. data/app/helpers/koi/definition_list_helper.rb +92 -0
  110. data/app/helpers/koi/index_actions_helper.rb +99 -0
  111. data/app/jobs/koi/application_job.rb +6 -0
  112. data/app/mailers/koi/application_mailer.rb +8 -0
  113. data/app/models/admin/credential.rb +14 -0
  114. data/app/models/admin/user.rb +51 -0
  115. data/app/models/application_record.rb +5 -0
  116. data/app/models/concerns/koi/model/archivable.rb +55 -0
  117. data/app/models/url_rewrite.rb +25 -0
  118. data/app/views/admin/admin_users/_admin.html+row.erb +4 -0
  119. data/app/views/admin/admin_users/_authentication.html.erb +15 -0
  120. data/app/views/admin/admin_users/_fields.html.erb +4 -0
  121. data/app/views/admin/admin_users/edit.html.erb +11 -0
  122. data/app/views/admin/admin_users/index.html.erb +9 -0
  123. data/app/views/admin/admin_users/new.html.erb +11 -0
  124. data/app/views/admin/admin_users/show.html.erb +22 -0
  125. data/app/views/admin/credentials/new.html.erb +14 -0
  126. data/app/views/admin/dashboards/show.html.erb +1 -0
  127. data/app/views/admin/sessions/new.html.erb +19 -0
  128. data/app/views/admin/shared/icons/_close.html.erb +8 -0
  129. data/app/views/admin/shared/icons/_cross.html.erb +3 -0
  130. data/app/views/admin/shared/icons/_menu.html.erb +3 -0
  131. data/app/views/admin/shared/icons/_refresh.html.erb +8 -0
  132. data/app/views/admin/url_rewrites/_form_fields.html.erb +3 -0
  133. data/app/views/admin/url_rewrites/_url_rewrite.html+row.erb +7 -0
  134. data/app/views/admin/url_rewrites/edit.html.erb +12 -0
  135. data/app/views/admin/url_rewrites/index.html.erb +10 -0
  136. data/app/views/admin/url_rewrites/new.html.erb +11 -0
  137. data/app/views/admin/url_rewrites/show.html.erb +16 -0
  138. data/app/views/katalyst/content/asides/_aside.html+form.erb +18 -0
  139. data/app/views/katalyst/content/columns/_column.html+form.erb +18 -0
  140. data/app/views/katalyst/content/contents/_content.html+form.erb +20 -0
  141. data/app/views/katalyst/content/figures/_figure.html+form.erb +17 -0
  142. data/app/views/katalyst/content/groups/_group.html+form.erb +18 -0
  143. data/app/views/katalyst/content/items/_item.html+form.erb +18 -0
  144. data/app/views/katalyst/content/sections/_section.html+form.erb +18 -0
  145. data/app/views/katalyst/navigation/items/_button.html.erb +15 -0
  146. data/app/views/katalyst/navigation/items/_heading.html.erb +11 -0
  147. data/app/views/katalyst/navigation/items/_link.html.erb +13 -0
  148. data/app/views/katalyst/navigation/menus/edit.html.erb +12 -0
  149. data/app/views/katalyst/navigation/menus/new.html.erb +9 -0
  150. data/app/views/katalyst/navigation/menus/show.html.erb +18 -0
  151. data/app/views/layouts/koi/_environment.html.erb +4 -0
  152. data/app/views/layouts/koi/_flash.html.erb +8 -0
  153. data/app/views/layouts/koi/_header.html.erb +11 -0
  154. data/app/views/layouts/koi/_navigation.html.erb +13 -0
  155. data/app/views/layouts/koi/_navigation_collapse.html.erb +3 -0
  156. data/app/views/layouts/koi/_navigation_header.html.erb +6 -0
  157. data/app/views/layouts/koi/_navigation_item.html.erb +12 -0
  158. data/app/views/layouts/koi/application.html.erb +59 -0
  159. data/app/views/layouts/koi/login.html.erb +29 -0
  160. data/config/importmap.rb +9 -0
  161. data/config/initializers/flipper.rb +13 -0
  162. data/config/initializers/pagy.rb +1 -0
  163. data/config/initializers/time_formats.rb +5 -0
  164. data/config/locales/koi.en.yml +18 -0
  165. data/config/locales/pagy.en.yml +6 -0
  166. data/config/routes.rb +25 -0
  167. data/db/migrate/20120220130849_devise_create_admins.rb +56 -0
  168. data/db/migrate/20130509235316_add_url_rewriter.rb +13 -0
  169. data/db/migrate/20230213053854_convert_devise_admins_to_rails.rb +7 -0
  170. data/db/migrate/20230412023411_create_admin_user_credentials.rb +20 -0
  171. data/db/migrate/20230531063707_update_admin_users.rb +37 -0
  172. data/db/migrate/20230602033610_add_archived_to_admin_users.rb +7 -0
  173. data/db/seeds.rb +9 -0
  174. data/lib/generators/koi/active_record/active_record_generator.rb +43 -0
  175. data/lib/generators/koi/admin/USAGE +8 -0
  176. data/lib/generators/koi/admin/admin_generator.rb +20 -0
  177. data/lib/generators/koi/admin_controller/USAGE +17 -0
  178. data/lib/generators/koi/admin_controller/admin_controller_generator.rb +51 -0
  179. data/lib/generators/koi/admin_controller/templates/controller.rb.tt +81 -0
  180. data/lib/generators/koi/admin_controller/templates/controller_spec.rb.tt +135 -0
  181. data/lib/generators/koi/admin_route/admin_route_generator.rb +62 -0
  182. data/lib/generators/koi/admin_views/USAGE +12 -0
  183. data/lib/generators/koi/admin_views/admin_views_generator.rb +54 -0
  184. data/lib/generators/koi/admin_views/templates/_fields.html.erb.tt +3 -0
  185. data/lib/generators/koi/admin_views/templates/_record.html+row.erb.tt +10 -0
  186. data/lib/generators/koi/admin_views/templates/edit.html.erb.tt +12 -0
  187. data/lib/generators/koi/admin_views/templates/index.html.erb.tt +7 -0
  188. data/lib/generators/koi/admin_views/templates/new.html.erb.tt +11 -0
  189. data/lib/generators/koi/admin_views/templates/show.html.erb.tt +18 -0
  190. data/lib/govuk_design_system_formbuilder/concerns/file_element.rb +115 -0
  191. data/lib/govuk_design_system_formbuilder/elements/document.rb +59 -0
  192. data/lib/govuk_design_system_formbuilder/elements/image.rb +86 -0
  193. data/lib/katalyst/koi.rb +3 -0
  194. data/lib/koi/caching.rb +15 -0
  195. data/lib/koi/config.rb +11 -0
  196. data/lib/koi/engine.rb +40 -0
  197. data/lib/koi/form_builder.rb +76 -0
  198. data/lib/koi/menu/builder.rb +68 -0
  199. data/lib/koi/menu.rb +46 -0
  200. data/lib/koi/middleware/url_redirect.rb +44 -0
  201. data/lib/koi/release.rb +52 -0
  202. data/lib/koi/version.rb +5 -0
  203. data/lib/koi.rb +37 -0
  204. data/spec/factories/admins.rb +9 -0
  205. data/spec/factories/url_rewrites.rb +9 -0
  206. metadata +430 -0
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/named_base"
4
+ require "rails/generators/resource_helpers"
5
+
6
+ module Koi
7
+ class AdminViewsGenerator < Rails::Generators::NamedBase
8
+ include Rails::Generators::ResourceHelpers
9
+
10
+ source_root File.expand_path("templates", __dir__)
11
+
12
+ argument :attributes, type: :array, default: [], banner: "field:type field:type"
13
+
14
+ def create_root_folder
15
+ empty_directory File.join("app/views/admin", controller_file_path)
16
+ end
17
+
18
+ def copy_view_files
19
+ available_views.each do |filename|
20
+ target = filename.gsub("record", singular_name)
21
+ template filename, File.join("app/views/admin", controller_file_path, target)
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def available_views
28
+ %w(index.html.erb edit.html.erb show.html.erb new.html.erb _fields.html.erb _record.html+row.erb)
29
+ end
30
+
31
+ def govuk_input_for(attribute)
32
+ case attribute.type
33
+ when :string
34
+ %(<%= form.govuk_text_field :#{attribute.name}, label: { size: "s" } %>)
35
+ when :integer
36
+ %(<%= form.govuk_number_field :#{attribute.name}, label: { size: "s" } %>)
37
+ when :boolean
38
+ %(<%= form.govuk_check_box_field :#{attribute.name}, small: true, label: { size: "s" } %>)
39
+ when :date
40
+ %(<%= form.govuk_date_field :#{attribute.name}, legend: { size: "s" } %>)
41
+ when :text
42
+ %(<%= form.govuk_rich_text_area :#{attribute.name}, label: { size: "s" } %>)
43
+ when :rich_text
44
+ %(<%= form.govuk_rich_text_area :#{attribute.name}, label: { size: "s" } %>)
45
+ else
46
+ ""
47
+ end
48
+ end
49
+
50
+ def index_attributes
51
+ attributes.select { |attribute| attribute.type == :string }.take(3)
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,3 @@
1
+ <%- attributes.each do |attribute| -%>
2
+ <%= govuk_input_for attribute %>
3
+ <%- end -%>
@@ -0,0 +1,10 @@
1
+ <%- index_attributes.each_with_index do |attribute, index| -%>
2
+ <%- if index.zero? -%>
3
+ <%% row.cell :<%= attribute.name %>, header: true do |cell| %>
4
+ <%%= link_to cell.value, url_for([:admin, <%= singular_name %>]) %>
5
+ <%% end %>
6
+
7
+ <%- else -%>
8
+ <%% row.cell :<%= attribute.name %> %>
9
+ <%- end -%>
10
+ <%- end -%>
@@ -0,0 +1,12 @@
1
+ <%% content_for :header do %>
2
+ <%%= render(Koi::Header::EditComponent.new(resource: <%= singular_name %>)) %>
3
+ <%% end %>
4
+
5
+ <%%= form_with(model: <%= singular_name %>, url: [:admin, <%= singular_name %>], builder: Koi::FormBuilder) do |form| %>
6
+ <%%= render "fields", form: %>
7
+
8
+ <div class="actions">
9
+ <%%= form.admin_save %>
10
+ <%%= form.admin_delete %>
11
+ </div>
12
+ <%% end %>
@@ -0,0 +1,7 @@
1
+ <%% content_for :header do %>
2
+ <%%= render(Koi::Header::IndexComponent.new(model: <%= class_name %>)) %>
3
+ <%% end %>
4
+
5
+ <%%= koi_index_actions create: true, search: true %>
6
+
7
+ <%%= render table %>
@@ -0,0 +1,11 @@
1
+ <%% content_for :header do %>
2
+ <%%= render(Koi::Header::NewComponent.new(model: <%= class_name %>)) %>
3
+ <%% end %>
4
+
5
+ <%%= form_with(model: <%= singular_name %>, url: [:admin, <%= class_name %>], builder: Koi::FormBuilder) do |form| %>
6
+ <%%= render "fields", form: %>
7
+
8
+ <div class="actions">
9
+ <%%= form.admin_save %>
10
+ </div>
11
+ <%% end %>
@@ -0,0 +1,18 @@
1
+ <%% content_for :header do %>
2
+ <%%= render(Koi::Header::ShowComponent.new(resource: <%= singular_name %>)) %>
3
+ <%% end %>
4
+
5
+ <h2>Summary</h2>
6
+
7
+ <%%= definition_list(class: "item-table") do |builder| %>
8
+ <%- attributes.each do |attribute| -%>
9
+ <%%= builder.item <%= singular_name %>, :<%= attribute.name %> %>
10
+ <%- end -%>
11
+ <%% end %>
12
+
13
+ <div class="actions">
14
+ <%%= button_to "Delete", [:admin, <%= singular_name %>],
15
+ class: "button button--secondary",
16
+ method: :delete,
17
+ form: { data: { turbo_confirm: "Are you sure?" } } %>
18
+ </div>
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "govuk_design_system_formbuilder"
4
+
5
+ module GOVUKDesignSystemFormBuilder
6
+ module Elements
7
+ module FileElement
8
+ extend ActiveSupport::Concern
9
+
10
+ using PrefixableArray
11
+
12
+ include Traits::Error
13
+ include Traits::Hint
14
+ include Traits::Label
15
+ include Traits::Supplemental
16
+ include Traits::HTMLAttributes
17
+ include Traits::HTMLClasses
18
+ include ActionDispatch::Routing::RouteSet::MountedHelpers
19
+
20
+ def html
21
+ Containers::FormGroup.new(*bound, **default_form_group_options(**@form_group)).html do
22
+ safe_join([label_element, preview, hint_element, error_element, file, destroy_element, supplemental_content])
23
+ end
24
+ end
25
+
26
+ protected
27
+
28
+ def stimulus_controller_actions
29
+ <<~ACTIONS.gsub(/\s+/, " ").freeze
30
+ dragover->#{stimulus_controller}#dragover
31
+ dragenter->#{stimulus_controller}#dragenter
32
+ dragleave->#{stimulus_controller}#dragleave
33
+ drop->#{stimulus_controller}#drop
34
+ ACTIONS
35
+ end
36
+
37
+ def stimulus_controller
38
+ nil
39
+ end
40
+
41
+ def file
42
+ @builder.file_field(@attribute_name, attributes(@html_attributes))
43
+ end
44
+
45
+ def destroy_element
46
+ return if @html_attributes[:optional].blank?
47
+
48
+ @builder.fields_for(:"#{@attribute_name}_attachment") do |form|
49
+ form.hidden_field :_destroy, value: false, data: { "#{stimulus_controller}_target" => "destroy" }
50
+ end
51
+ end
52
+
53
+ def destroy_element_trigger
54
+ return if @html_attributes[:optional].blank?
55
+
56
+ content_tag(:button, "", class: "file-destroy", data: { action: "#{stimulus_controller}#setDestroy" })
57
+ end
58
+
59
+ def preview
60
+ ""
61
+ end
62
+
63
+ def preview_url
64
+ preview? ? main_app.url_for(value) : ""
65
+ end
66
+
67
+ def preview?
68
+ value&.attached? && value&.persisted?
69
+ end
70
+
71
+ def value
72
+ @builder.object.send(@attribute_name)
73
+ end
74
+
75
+ def file_input_options
76
+ default_file_input_options = options
77
+
78
+ add_option(default_file_input_options, :accept, @mime_types.join(","))
79
+ add_option(default_file_input_options, :data, :action, "change->#{stimulus_controller}#onUpload")
80
+
81
+ default_file_input_options
82
+ end
83
+
84
+ def options
85
+ {
86
+ id: field_id(link_errors: true),
87
+ class: classes,
88
+ aria: { describedby: combine_references(hint_id, error_id, supplemental_id) },
89
+ }
90
+ end
91
+
92
+ def classes
93
+ build_classes(%(file-upload), %(file-upload--error) => has_errors?).prefix(brand)
94
+ end
95
+
96
+ def default_form_group_options(**form_group_options)
97
+ add_option(form_group_options, :class, "govuk-form-group govuk-image-field")
98
+ add_option(form_group_options, :data, :controller, stimulus_controller)
99
+ add_option(form_group_options, :data, :action, stimulus_controller_actions)
100
+ add_option(form_group_options, :data, :"#{stimulus_controller}_mime_types_value",
101
+ @mime_types.to_json)
102
+
103
+ form_group_options
104
+ end
105
+
106
+ def add_option(options, key, *path)
107
+ if path.length > 1
108
+ add_option(options[key] ||= {}, *path)
109
+ else
110
+ options[key] = [options[key], *path].compact.join(" ")
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "govuk_design_system_formbuilder"
4
+
5
+ module GOVUKDesignSystemFormBuilder
6
+ module Elements
7
+ class Document < Base
8
+ include FileElement
9
+
10
+ MIME_TYPES = %w[
11
+ image/png image/gif image/jpeg image/webp
12
+ application/pdf
13
+ ].freeze
14
+
15
+ def initialize(builder, object_name, attribute_name, hint:, label:, caption:, form_group:, **kwargs, &block)
16
+ super(builder, object_name, attribute_name, &block)
17
+
18
+ @mime_types = MIME_TYPES || kwargs[:mime_types]
19
+ @label = label
20
+ @caption = caption
21
+ @hint = hint
22
+ @html_attributes = kwargs.merge(file_input_options)
23
+ @form_group = form_group
24
+ end
25
+
26
+ def preview
27
+ options = {}
28
+ add_option(options, :data, "#{stimulus_controller}_target", "preview")
29
+ add_option(options, :class, "preview-file")
30
+ add_option(options, :class, "hidden") unless preview?
31
+
32
+ tag.div **options do
33
+ filename = @builder.object.send(@attribute_name).filename.to_s
34
+ tag.p(filename, class: "preview-filename") + destroy_element_trigger
35
+ end
36
+ end
37
+
38
+ def stimulus_controller
39
+ "document-field"
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ module GOVUKDesignSystemFormBuilder
46
+ module Builder
47
+ delegate :config, to: GOVUKDesignSystemFormBuilder
48
+
49
+ # Generates a +div+ element with an +input+ with +type=file+ with a label, optional hint.
50
+ #
51
+ # @example A upload field with label as a proc
52
+ # = f.govuk_file_field :data, label: -> { tag.h3('Upload your document') }
53
+ #
54
+ def govuk_document_field(attribute_name, label: {}, caption: {}, hint: {}, form_group: {}, **kwargs, &block)
55
+ Elements::Document.new(self, object_name, attribute_name, label:, caption:, hint:, form_group:, **kwargs,
56
+ &block).html
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "govuk_design_system_formbuilder"
4
+ require "govuk_design_system_formbuilder/elements/document"
5
+
6
+ module GOVUKDesignSystemFormBuilder
7
+ module Elements
8
+ class Image < Base
9
+ include FileElement
10
+
11
+ MIME_TYPES = %w[image/png image/gif image/jpeg image/webp].freeze
12
+
13
+ def initialize(builder, object_name, attribute_name, hint:, label:, caption:, form_group:, **kwargs, &block)
14
+ super(builder, object_name, attribute_name, &block)
15
+
16
+ @mime_types = kwargs[:mime_types] || MIME_TYPES
17
+ @label = label
18
+ @caption = caption
19
+ @hint = hint
20
+ @html_attributes = kwargs.merge(file_input_options)
21
+ @form_group = form_group
22
+ end
23
+
24
+ def preview
25
+ options = {}
26
+ add_option(options, :data, "#{stimulus_controller}_target", "preview")
27
+ add_option(options, :class, "preview-image")
28
+ add_option(options, :class, "hidden") unless preview?
29
+
30
+ tag.div **options do
31
+ tag.img(src: preview_url, class: "image-thumbnail") + destroy_element_trigger
32
+ end
33
+ end
34
+
35
+ def stimulus_controller
36
+ "image-field"
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ module GOVUKDesignSystemFormBuilder
43
+ module Builder
44
+ delegate :config, to: GOVUKDesignSystemFormBuilder
45
+
46
+ # Generates a +div+ element to preview uploaded images and an +input+ with +type=file+ with a label, optional hint.
47
+ #
48
+ # @param attribute_name [Symbol] The name of the attribute
49
+ # @param hint [Hash,Proc] The content of the hint. No hint will be added if 'text' is left +nil+. When a +Proc+ is
50
+ # supplied the hint will be wrapped in a +div+ instead of a +span+
51
+ # @option hint text [String] the hint text
52
+ # @option hint kwargs [Hash] additional arguments are applied as attributes to the hint
53
+ # @param label [Hash,Proc] configures or sets the associated label content
54
+ # @option label text [String] the label text
55
+ # @option label size [String] the size of the label font, can be +xl+, +l+, +m+, +s+ or nil
56
+ # @option label tag [Symbol,String] the label's wrapper tag, intended to allow labels to act as page headings
57
+ # @option label hidden [Boolean] control the visibility of the label. Hidden labels will still be read by screen
58
+ # readers
59
+ # @option label kwargs [Hash] additional arguments are applied as attributes on the +label+ element
60
+ # @param caption [Hash] configures or sets the caption content which is inserted above the label
61
+ # @option caption text [String] the caption text
62
+ # @option caption size [String] the size of the caption, can be +xl+, +l+ or +m+. Defaults to +m+
63
+ # @option caption kwargs [Hash] additional arguments are applied as attributes on the caption +span+ element
64
+ # @option kwargs [Hash] kwargs additional arguments are applied as attributes to the +input+ element.
65
+ # @param form_group [Hash] configures the form group
66
+ # @option form_group classes [Array,String] sets the form group's classes
67
+ # @option form_group kwargs [Hash] additional attributes added to the form group
68
+ # @param block [Block] arbitrary HTML that will be rendered between the hint and the input
69
+ # @return [ActiveSupport::SafeBuffer] HTML output
70
+ #
71
+ # @example An image field with injected content
72
+ # = f.govuk_image_field :incident_image,
73
+ # label: { text: 'Attach a picture of the incident' } do
74
+ #
75
+ # p.govuk-inset-text
76
+ # | If you don't know exactly leave this section blank
77
+ #
78
+ # @example A image upload field with label as a proc
79
+ # = f.govuk_image_field :image, label: -> { tag.h3('Upload your image') }
80
+ #
81
+ def govuk_image_field(attribute_name, label: {}, caption: {}, hint: {}, form_group: {}, **kwargs, &block)
82
+ Elements::Image.new(self, object_name, attribute_name,
83
+ label:, caption:, hint:, form_group:, **kwargs, &block).html
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "koi"
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/integer/time"
4
+
5
+ module Koi
6
+ module Caching
7
+ # Caching Enabled
8
+ mattr_accessor :enabled
9
+ @@enabled = true
10
+
11
+ # Cache Expires in
12
+ mattr_accessor :expires_in
13
+ @@expires_in = 60.minutes
14
+ end
15
+ end
data/lib/koi/config.rb ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/configurable"
4
+
5
+ module Koi
6
+ class Config
7
+ include ActiveSupport::Configurable
8
+
9
+ config_accessor(:resource_name_candidates) { %i[title name] }
10
+ end
11
+ end
data/lib/koi/engine.rb ADDED
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "middleware/url_redirect"
4
+
5
+ module Koi
6
+ class Engine < ::Rails::Engine
7
+ engine_name "koi"
8
+
9
+ initializer "koi.assets" do |app|
10
+ app.middleware.use ::ActionDispatch::Static, "#{root}/public"
11
+ app.middleware.insert_before Rack::Sendfile, Koi::UrlRedirect
12
+
13
+ config.after_initialize do |a|
14
+ if a.config.respond_to?(:assets)
15
+ a.config.assets.precompile += %w(koi.js)
16
+ end
17
+ end
18
+ end
19
+
20
+ initializer "koi.importmap", before: "importmap" do |app|
21
+ app.config.importmap.paths << root.join("config/importmap.rb")
22
+ app.config.importmap.cache_sweepers << root.join("app/assets/javascripts")
23
+ end
24
+
25
+ initializer "koi.factories", after: "factory_bot.set_factory_paths" do
26
+ FactoryBot.definition_file_paths << Engine.root.join("spec/factories") if defined?(FactoryBot)
27
+ end
28
+
29
+ config.generators do |g|
30
+ g.test_framework :rspec
31
+ g.fixture_replacement :factory_bot
32
+ g.factory_bot dir: "spec/factories"
33
+ end
34
+
35
+ config.to_prepare do
36
+ Katalyst::Navigation::BaseController.include Koi::Controller::IsAdminController
37
+ Katalyst::Navigation::BaseController.helper Koi::Engine.helpers
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Koi
4
+ class FormBuilder < ActionView::Helpers::FormBuilder
5
+ delegate_missing_to :@template
6
+
7
+ include GOVUKDesignSystemFormBuilder::Builder
8
+
9
+ # Generates a submit button for saving admin resources.
10
+ def admin_save(text = "Save", name: :commit, value: :save, class: "button button--primary", **kwargs)
11
+ button(text, name:, value:, class:, **kwargs)
12
+ end
13
+
14
+ # Generates a delete link formatted as a button that will perform a turbo
15
+ # delete with a confirmation.
16
+ def admin_delete(text = "Delete", url: nil, confirm: "Are you sure?", data: {}, **kwargs)
17
+ return unless object.persisted?
18
+
19
+ link_to(text, url || url_for(action: :destroy),
20
+ class: "button button--secondary",
21
+ data: data.reverse_merge(turbo_method: :delete, turbo_confirm: confirm),
22
+ **kwargs)
23
+ end
24
+
25
+ # Generates an archive link formatted as a button that will perform a turbo
26
+ # delete with a confirmation.
27
+ def admin_archive(text = "Archive", **kwargs)
28
+ admin_delete(text, **kwargs)
29
+ end
30
+
31
+ # Generates a discard changes link formatted as a text button that navigates
32
+ # the user back to the previous page.
33
+ def admin_discard(text = "Discard", url: :back, **kwargs)
34
+ link_to(text, url, class: "button button--text", **kwargs)
35
+ end
36
+
37
+ # @api internal
38
+ # @see GOVUKDesignSystemFormBuilder::Builder#govuk_document_field
39
+ def govuk_document_field(attribute_name, hint: {}, **kwargs, &block)
40
+ max_size = hint[:max_size] || App::PERMITTED_IMAGE_SIZE
41
+ super(attribute_name, hint:, **kwargs) do
42
+ if block
43
+ concat(yield)
44
+ else
45
+ concat(t("helpers.hint.default.document", max_size: @template.number_to_human_size(max_size)))
46
+ end
47
+ end
48
+ end
49
+
50
+ # @api internal
51
+ # @see GOVUKDesignSystemFormBuilder::Builder#govuk_image_field
52
+ def govuk_image_field(attribute_name, hint: {}, **kwargs, &block)
53
+ max_size = hint[:max_size] || App::PERMITTED_IMAGE_SIZE
54
+ super(attribute_name, hint:, **kwargs) do
55
+ if block
56
+ concat(yield)
57
+ else
58
+ concat(t("helpers.hint.default.image", max_size: @template.number_to_human_size(max_size)))
59
+ end
60
+ end
61
+ end
62
+
63
+ # Use content editor trix setup by default.
64
+ #
65
+ # @api internal
66
+ # @see GOVUKDesignSystemFormBuilder::Builder#govuk_rich_text_area
67
+ def govuk_rich_text_area(attribute_name, data: {}, **kwargs, &block)
68
+ data = data.reverse_merge(
69
+ direct_upload_url: @template.katalyst_content.direct_uploads_url,
70
+ controller: "content--editor--trix",
71
+ action: "trix-initialize->content--editor--trix#trixInitialize",
72
+ )
73
+ super(attribute_name, data:, **kwargs, &block)
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Koi
4
+ module Menu
5
+ class Builder
6
+ def initialize
7
+ @menu = Katalyst::Navigation::Menu.new
8
+ @index = 0
9
+ @depth = 0
10
+ end
11
+
12
+ def add_items(items)
13
+ items.each do |k, v|
14
+ add_item(k, v)
15
+ end
16
+ end
17
+
18
+ def add_item(title, value)
19
+ if value.is_a?(Hash)
20
+ add_menu(title:) do |b|
21
+ value.each do |k, v|
22
+ b.add_item(k, v)
23
+ end
24
+ end
25
+ else
26
+ add_link(title:, url: value)
27
+ end
28
+ end
29
+
30
+ def add_menu(title:, **options, &)
31
+ @menu.items.build(type: Katalyst::Navigation::Heading.name,
32
+ title:,
33
+ **options,
34
+ index: @index,
35
+ depth: @depth)
36
+ @index += 1
37
+ @depth += 1
38
+ yield(self)
39
+ @depth -= 1
40
+ end
41
+
42
+ def add_link(title:, url:, **options)
43
+ @menu.items.build(type: Katalyst::Navigation::Link.name,
44
+ title:,
45
+ url:,
46
+ **options,
47
+ index: @index,
48
+ depth: @depth)
49
+ @index += 1
50
+ end
51
+
52
+ def add_button(title:, url:, **options)
53
+ @menu.items.build(type: Katalyst::Navigation::Button.name,
54
+ title:,
55
+ url:,
56
+ **options,
57
+ index: @index,
58
+ depth: @depth)
59
+ @index += 1
60
+ end
61
+
62
+ def render
63
+ @menu.published_version = @menu.build_draft_version
64
+ @menu
65
+ end
66
+ end
67
+ end
68
+ end
data/lib/koi/menu.rb ADDED
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "koi/menu/builder"
4
+
5
+ module Koi
6
+ module Menu
7
+ mattr_accessor :priority
8
+ @@priority = {}
9
+
10
+ mattr_accessor :modules
11
+ @@modules = {}
12
+
13
+ mattr_accessor :advanced
14
+ @@advanced = {}
15
+
16
+ def admin_menu(context)
17
+ builder = Builder.new
18
+ builder.add_menu(title: "Priority") do |b|
19
+ b.add_link(title: "View site", url: "/", target: :blank)
20
+ b.add_link(title: "Dashboard", url: context.main_app.admin_dashboard_path)
21
+ b.add_items(priority)
22
+ b.add_button(title: "Logout", url: context.main_app.admin_session_path, http_method: :delete)
23
+ end
24
+ builder.add_menu(title: "Modules") do |b|
25
+ b.add_items(modules)
26
+ end
27
+ builder.add_menu(title: "Advanced") do |b|
28
+ b.add_items(advanced)
29
+
30
+ if Object.const_defined?("Flipper::UI")
31
+ b.add_link(title: "Flipper", url: context.main_app.admin_root_path.concat("/flipper"),
32
+ target: :blank)
33
+ end
34
+ if Object.const_defined?("Sidekiq::Web")
35
+ b.add_link(title: "Sidekiq", url: context.main_app.admin_root_path.concat("sidekiq"),
36
+ target: :blank)
37
+ end
38
+ b.add_button(title: "Clear cache", url: context.main_app.admin_cache_path,
39
+ http_method: :delete)
40
+ end
41
+ builder.render
42
+ end
43
+
44
+ module_function(:admin_menu)
45
+ end
46
+ end