katalyst-koi 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
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