yummy-guide-generic-administrate 0.1.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 (43) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +4 -0
  3. data/README.md +243 -0
  4. data/Rakefile +11 -0
  5. data/app/assets/javascripts/yummy_guide_administrate/filter_form.js +219 -0
  6. data/app/assets/javascripts/yummy_guide_administrate/sticky_left_columns.js +141 -0
  7. data/app/assets/stylesheets/yummy_guide_administrate/components.scss +199 -0
  8. data/app/controllers/concerns/yummy_guide/administrate/datetime_filter_parameters.rb +40 -0
  9. data/app/controllers/concerns/yummy_guide/administrate/default_sorting.rb +46 -0
  10. data/app/dashboards/yummy_guide/administrate/application_dashboard.rb +46 -0
  11. data/app/fields/yummy_guide/administrate/fields/area/picture_field.rb +106 -0
  12. data/app/fields/yummy_guide/administrate/fields/json_pretty_field.rb +29 -0
  13. data/app/fields/yummy_guide/administrate/fields/version_item_field.rb +62 -0
  14. data/app/fields/yummy_guide/administrate/fields/version_whodunnit_field.rb +65 -0
  15. data/app/helpers/yummy_guide/administrate/collection_helper.rb +62 -0
  16. data/app/helpers/yummy_guide/administrate/filter_form_helper.rb +80 -0
  17. data/app/views/fields/yummy_guide_administrate/area/picture/_form.html.erb +36 -0
  18. data/app/views/fields/yummy_guide_administrate/area/picture/_index.html.erb +2 -0
  19. data/app/views/fields/yummy_guide_administrate/area/picture/_show.html.erb +24 -0
  20. data/app/views/fields/yummy_guide_administrate/json_pretty_field/_index.html.erb +2 -0
  21. data/app/views/fields/yummy_guide_administrate/json_pretty_field/_show.html.erb +6 -0
  22. data/app/views/fields/yummy_guide_administrate/version_item_field/_index.html.erb +2 -0
  23. data/app/views/fields/yummy_guide_administrate/version_item_field/_show.html.erb +6 -0
  24. data/app/views/fields/yummy_guide_administrate/version_whodunnit_field/_index.html.erb +2 -0
  25. data/app/views/fields/yummy_guide_administrate/version_whodunnit_field/_show.html.erb +6 -0
  26. data/app/views/yummy_guide/administrate/administrate/application/_collection.html.erb +62 -0
  27. data/app/views/yummy_guide/administrate/filter_forms/_checkbox_group.html.erb +30 -0
  28. data/app/views/yummy_guide/administrate/filter_forms/_datetime_field.html.erb +30 -0
  29. data/app/views/yummy_guide/administrate/filter_forms/_frame.html.erb +28 -0
  30. data/lib/generic/administrate.rb +3 -0
  31. data/lib/yummy_guide/administrate/engine.rb +18 -0
  32. data/lib/yummy_guide/administrate/version.rb +8 -0
  33. data/lib/yummy_guide/administrate.rb +13 -0
  34. data/spec/spec_helper.rb +25 -0
  35. data/spec/yummy_guide/administrate/application_dashboard_spec.rb +39 -0
  36. data/spec/yummy_guide/administrate/collection_helper_spec.rb +25 -0
  37. data/spec/yummy_guide/administrate/datetime_filter_parameters_spec.rb +52 -0
  38. data/spec/yummy_guide/administrate/fields/json_pretty_field_spec.rb +22 -0
  39. data/spec/yummy_guide/administrate/fields/version_item_field_spec.rb +30 -0
  40. data/spec/yummy_guide/administrate/fields/version_whodunnit_field_spec.rb +31 -0
  41. data/spec/yummy_guide/administrate/filter_form_helper_spec.rb +40 -0
  42. data/yummy-guide-generic-administrate.gemspec +38 -0
  43. metadata +156 -0
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YummyGuide
4
+ module Administrate
5
+ module FilterFormHelper
6
+ def yummy_guide_administrate_filter_current_values(raw_values)
7
+ values =
8
+ if raw_values.respond_to?(:to_unsafe_h)
9
+ raw_values.to_unsafe_h
10
+ elsif raw_values.respond_to?(:to_h)
11
+ raw_values.to_h
12
+ else
13
+ raw_values || {}
14
+ end
15
+
16
+ values.deep_stringify_keys
17
+ end
18
+
19
+ def yummy_guide_administrate_filter_hour_options
20
+ @yummy_guide_administrate_filter_hour_options ||= [["", ""]] + (0..23).map { |hour| [format("%02d", hour), format("%02d", hour)] }
21
+ end
22
+
23
+ def yummy_guide_administrate_filter_minute_options
24
+ @yummy_guide_administrate_filter_minute_options ||= [["", ""], ["00", "00"], ["30", "30"], ["59", "59"]]
25
+ end
26
+
27
+ def yummy_guide_administrate_datetime_filter_parts(value, end_of_day: false)
28
+ raw_value = value.to_s.strip
29
+
30
+ if raw_value.blank?
31
+ { date: "", hour: "", minute: "" }
32
+ elsif raw_value.match?(/\A\d{4}-\d{2}-\d{2}\z/)
33
+ {
34
+ date: raw_value,
35
+ hour: (end_of_day ? "23" : "00"),
36
+ minute: (end_of_day ? "59" : "00")
37
+ }
38
+ else
39
+ parsed = Time.zone.parse(raw_value)
40
+
41
+ if parsed.present?
42
+ {
43
+ date: parsed.strftime("%Y-%m-%d"),
44
+ hour: parsed.strftime("%H"),
45
+ minute: parsed.strftime("%M")
46
+ }
47
+ else
48
+ { date: "", hour: "", minute: "" }
49
+ end
50
+ end
51
+ end
52
+
53
+ def yummy_guide_administrate_datetime_filter_combined_value(parts)
54
+ if parts[:date].blank?
55
+ ""
56
+ elsif parts[:hour].present? && parts[:minute].present?
57
+ "#{parts[:date]}T#{parts[:hour]}:#{parts[:minute]}"
58
+ else
59
+ parts[:date]
60
+ end
61
+ end
62
+
63
+ def yummy_guide_administrate_checkbox_group_options(options, label_method: nil, value_method: nil)
64
+ Array(options).map do |option|
65
+ if option.is_a?(Array) && option.size == 2
66
+ option
67
+ elsif label_method.present? || value_method.present?
68
+ [
69
+ option.public_send(label_method || :to_s),
70
+ option.public_send(value_method || :id)
71
+ ]
72
+ else
73
+ [option, option]
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
80
+
@@ -0,0 +1,36 @@
1
+ <div class="field-unit__label">
2
+ <%= f.label field.attribute %>
3
+ </div>
4
+ <div class="field-unit__field">
5
+ <% attachments = field.attachments %>
6
+ <% empty_slots = [field.max_uploads - attachments.size, 0].max %>
7
+
8
+ <div class="yummy-guide-administrate-picture-slots">
9
+ <% attachments.each do |attachment| %>
10
+ <% preview_url = field.preview_url(attachment, view_context: self) %>
11
+ <div class="yummy-guide-administrate-picture-slot">
12
+ <% if preview_url.present? %>
13
+ <div class="yummy-guide-administrate-picture-slot__preview">
14
+ <%= image_tag preview_url, class: "yummy-guide-administrate-picture-list__image" %>
15
+ </div>
16
+ <% else %>
17
+ <div class="yummy-guide-administrate-picture-slot__placeholder"><%= field.attachment_label(attachment) %></div>
18
+ <% end %>
19
+
20
+ <% if (attachment_id = field.attachment_identifier(attachment)).present? %>
21
+ <label class="yummy-guide-administrate-picture-slot__delete">
22
+ <input type="checkbox" name="<%= field.purge_input_name %>" value="<%= attachment_id %>">
23
+ Delete
24
+ </label>
25
+ <% end %>
26
+ </div>
27
+ <% end %>
28
+
29
+ <% empty_slots.times do %>
30
+ <div class="yummy-guide-administrate-picture-slot">
31
+ <%= file_field_tag field.input_name, accept: "image/*", class: "yummy-guide-administrate-picture-slot__input" %>
32
+ </div>
33
+ <% end %>
34
+ </div>
35
+ </div>
36
+
@@ -0,0 +1,2 @@
1
+ <%= render "fields/yummy_guide_administrate/area/picture/show", field: field %>
2
+
@@ -0,0 +1,24 @@
1
+ <% if field.attachments.any? %>
2
+ <div class="yummy-guide-administrate-picture-list">
3
+ <% field.attachments.each do |attachment| %>
4
+ <% preview_url = field.preview_url(attachment, view_context: self) %>
5
+ <% link_url = field.attachment_url(attachment, view_context: self) || preview_url %>
6
+ <div class="yummy-guide-administrate-picture-list__item">
7
+ <% if preview_url.present? %>
8
+ <% if link_url.present? %>
9
+ <%= link_to link_url, title: field.attachment_label(attachment) do %>
10
+ <%= image_tag preview_url, class: "yummy-guide-administrate-picture-list__image" %>
11
+ <% end %>
12
+ <% else %>
13
+ <%= image_tag preview_url, class: "yummy-guide-administrate-picture-list__image" %>
14
+ <% end %>
15
+ <% else %>
16
+ <span><%= field.attachment_label(attachment) %></span>
17
+ <% end %>
18
+ </div>
19
+ <% end %>
20
+ </div>
21
+ <% else %>
22
+ No attachment
23
+ <% end %>
24
+
@@ -0,0 +1,2 @@
1
+ <%= render "fields/yummy_guide_administrate/json_pretty_field/show", field: field %>
2
+
@@ -0,0 +1,6 @@
1
+ <% if field.data.present? %>
2
+ <pre class="json-pretty-field"><%= field.to_s %></pre>
3
+ <% else %>
4
+ -
5
+ <% end %>
6
+
@@ -0,0 +1,2 @@
1
+ <%= render "fields/yummy_guide_administrate/version_item_field/show", field: field %>
2
+
@@ -0,0 +1,6 @@
1
+ <% if field.linkable? %>
2
+ <%= link_to field.label, field.path, class: "action-show", onclick: "event.stopPropagation();" %>
3
+ <% else %>
4
+ <%= field.label %>
5
+ <% end %>
6
+
@@ -0,0 +1,2 @@
1
+ <%= render "fields/yummy_guide_administrate/version_whodunnit_field/show", field: field %>
2
+
@@ -0,0 +1,6 @@
1
+ <% if field.linkable? %>
2
+ <%= link_to field.label, field.path, class: "action-show", onclick: "event.stopPropagation();" %>
3
+ <% elsif field.data.present? %>
4
+ <%= field.label %>
5
+ <% end %>
6
+
@@ -0,0 +1,62 @@
1
+ <div class="scroll-table">
2
+ <table aria-labelledby="<%= table_title %>" data-fixed-columns-count="<%= yummy_guide_administrate_collection_table_fixed_columns_count(page: page, collection_presenter: collection_presenter) %>">
3
+ <thead>
4
+ <tr>
5
+ <% collection_presenter.attribute_types.each do |attr_name, attr_type| %>
6
+ <th
7
+ class="cell-label cell-label--<%= attr_type.html_class %> cell-label--<%= collection_presenter.ordered_html_class(attr_name) %> cell-label--<%= "#{collection_presenter.resource_name}_#{attr_name}" %>"
8
+ scope="col"
9
+ aria-sort="<%= sort_order(collection_presenter.ordered_html_class(attr_name)) %>"
10
+ >
11
+ <%= link_to(sanitized_order_params(page, collection_field_name).merge(
12
+ collection_presenter.order_params_for(attr_name, key: collection_field_name)
13
+ )) do %>
14
+ <%= t(
15
+ "helpers.label.#{collection_presenter.resource_name}.#{attr_name}",
16
+ default: resource_class.human_attribute_name(attr_name).titleize
17
+ ) %>
18
+ <% if collection_presenter.ordered_by?(attr_name) %>
19
+ <span class="cell-label__sort-indicator cell-label__sort-indicator--<%= collection_presenter.ordered_html_class(attr_name) %>">
20
+ <svg aria-hidden="true">
21
+ <use xlink:href="#icon-up-caret" />
22
+ </svg>
23
+ </span>
24
+ <% end %>
25
+ <% end %>
26
+ </th>
27
+ <% end %>
28
+ <%= render(
29
+ yummy_guide_administrate_collection_actions_partial("collection_header_actions"),
30
+ collection_presenter: collection_presenter,
31
+ page: page,
32
+ resources: resources,
33
+ table_title: "page-title"
34
+ ) %>
35
+ </tr>
36
+ </thead>
37
+
38
+ <tbody>
39
+ <% resources.each do |resource| %>
40
+ <tr class="js-table-row">
41
+ <% collection_presenter.attributes_for(resource).each do |attribute| %>
42
+ <% cell_href = yummy_guide_administrate_collection_attribute_path(attribute: attribute, resource: resource, namespace: namespace) %>
43
+ <td class="cell-data cell-data--<%= attribute.html_class %>">
44
+ <%= yummy_guide_administrate_collection_wrap(render_field(attribute), href: cell_href) %>
45
+ </td>
46
+ <% end %>
47
+
48
+ <%= render(
49
+ yummy_guide_administrate_collection_actions_partial("collection_item_actions"),
50
+ collection_presenter: collection_presenter,
51
+ collection_field_name: collection_field_name,
52
+ page: page,
53
+ namespace: namespace,
54
+ resource: resource,
55
+ table_title: "page-title"
56
+ ) %>
57
+ </tr>
58
+ <% end %>
59
+ </tbody>
60
+ </table>
61
+ </div>
62
+
@@ -0,0 +1,30 @@
1
+ <% normalized_options = yummy_guide_administrate_checkbox_group_options(options,
2
+ label_method: local_assigns[:label_method],
3
+ value_method: local_assigns[:value_method]) %>
4
+ <% selected = Array(selected_values).map(&:to_s) %>
5
+ <div class="filter-checkbox-group" data-checkbox-group="<%= group_name %>">
6
+ <div class="filter-checkbox-group__options">
7
+ <% normalized_options.each do |label, value| %>
8
+ <% value_string = value.to_s %>
9
+ <label style="display: flex; align-items: center; gap: 6px;">
10
+ <%= check_box_tag "#{form_scope}[#{field_name}][]",
11
+ value,
12
+ selected.include?(value_string),
13
+ id: "#{form_scope}_#{field_name}_#{value_string.parameterize(separator: "_")}",
14
+ data: { checkbox_group_item: group_name } %>
15
+ <span><%= label %></span>
16
+ </label>
17
+ <% end %>
18
+ </div>
19
+ <div class="filter-checkbox-group__actions">
20
+ <%= button_tag "Select all",
21
+ type: "button",
22
+ class: "button button--outline-primary filter-checkbox-group__action",
23
+ data: { behavior: "checkbox-group-select-all", target: group_name } %>
24
+ <%= button_tag "Clear all",
25
+ type: "button",
26
+ class: "button button--outline-primary filter-checkbox-group__action",
27
+ data: { behavior: "checkbox-group-clear-all", target: group_name } %>
28
+ </div>
29
+ </div>
30
+
@@ -0,0 +1,30 @@
1
+ <% parts = yummy_guide_administrate_datetime_filter_parts(current_value, end_of_day: local_assigns[:end_of_day]) %>
2
+ <div
3
+ class="filter-datetime-group"
4
+ data-datetime-filter="true"
5
+ data-datetime-field="<%= field_name %>"
6
+ <% if local_assigns[:end_target].present? %>
7
+ data-datetime-end-target="<%= end_target %>"
8
+ <% end %>
9
+ >
10
+ <%= hidden_field_tag("#{form_scope}[#{field_name}]",
11
+ yummy_guide_administrate_datetime_filter_combined_value(parts),
12
+ class: css_class,
13
+ data: { datetime_part: "combined" }) %>
14
+ <%= date_field_tag("#{form_scope}[#{field_name}_date]",
15
+ parts[:date],
16
+ class: "#{css_class} filter-datetime-group__date",
17
+ data: { datetime_part: "date" }) %>
18
+ <%= select_tag("#{form_scope}[#{field_name}_hour]",
19
+ options_for_select(yummy_guide_administrate_filter_hour_options, parts[:hour]),
20
+ class: "#{css_class} #{css_class}--hour filter-datetime-group__time",
21
+ data: { datetime_part: "hour" },
22
+ disabled: parts[:date].blank?) %>
23
+ <span class="filter-datetime-group__separator">:</span>
24
+ <%= select_tag("#{form_scope}[#{field_name}_minute]",
25
+ options_for_select(yummy_guide_administrate_filter_minute_options, parts[:minute]),
26
+ class: "#{css_class} #{css_class}--minute filter-datetime-group__time",
27
+ data: { datetime_part: "minute" },
28
+ disabled: parts[:date].blank?) %>
29
+ </div>
30
+
@@ -0,0 +1,28 @@
1
+ <% current_values = yummy_guide_administrate_filter_current_values(local_assigns[:current_values] || local_assigns[:search_options] || {}) %>
2
+ <% form_css_class = local_assigns[:form_css_class] || "yummy-guide-administrate-filter-form" %>
3
+ <% form_attributes = { class: form_css_class, data: { yummy_guide_administrate_filter_form: true } } %>
4
+ <% form_attributes[:id] = local_assigns[:form_id] if local_assigns[:form_id].present? %>
5
+ <% if local_assigns[:form_data].present? %>
6
+ <% form_attributes[:data].merge!(local_assigns[:form_data]) %>
7
+ <% end %>
8
+
9
+ <%= form_with url: path, scope: form, method: method, html: form_attributes do |f| %>
10
+ <% Array(local_assigns[:hidden_fields]).each do |(key, value)| %>
11
+ <%= hidden_field_tag "#{form}[#{key}]", value %>
12
+ <% end %>
13
+
14
+ <h2 class="<%= "#{form_css_class}__title" %>"><%= local_assigns[:title] || "Filter Options" %></h2>
15
+ <div class="<%= "#{form_css_class}__body" %>">
16
+ <table class="filter_table">
17
+ <%= yield f, current_values %>
18
+ </table>
19
+ </div>
20
+ <div class="<%= "#{form_css_class}__actions" %>">
21
+ <%= button_tag local_assigns[:clear_label] || "Clear",
22
+ type: "button",
23
+ class: "button button--outline-primary js-filter-form-clear",
24
+ data: { behavior: "filter-form-clear" } %>
25
+ <%= f.submit local_assigns[:submit_label] || "Filter", class: "submit_filter" %>
26
+ </div>
27
+ <% end %>
28
+
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../yummy_guide/administrate"
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YummyGuide
4
+ module Administrate
5
+ class Engine < ::Rails::Engine
6
+ initializer "yummy_guide.administrate.assets" do |app|
7
+ next unless app.config.respond_to?(:assets)
8
+
9
+ app.config.assets.precompile += %w[
10
+ yummy_guide_administrate/components.css
11
+ yummy_guide_administrate/filter_form.js
12
+ yummy_guide_administrate/sticky_left_columns.js
13
+ ]
14
+ end
15
+ end
16
+ end
17
+ end
18
+
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YummyGuide
4
+ module Administrate
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
8
+
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails"
4
+ require "administrate"
5
+
6
+ require_relative "administrate/version"
7
+ require_relative "administrate/engine"
8
+
9
+ module YummyGuide
10
+ module Administrate
11
+ end
12
+ end
13
+
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/setup"
4
+ require "rspec"
5
+ require "active_support"
6
+ require "active_support/core_ext"
7
+ require "active_model"
8
+ require "action_controller"
9
+ require "action_view"
10
+
11
+ require "generic/administrate"
12
+ Dir[File.expand_path("../app/**/*.rb", __dir__)].sort.each { |path| require path }
13
+
14
+ RSpec.configure do |config|
15
+ config.disable_monkey_patching!
16
+ config.expect_with :rspec do |expectations|
17
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
18
+ end
19
+
20
+ config.mock_with :rspec do |mocks|
21
+ mocks.verify_partial_doubles = true
22
+ end
23
+
24
+ config.shared_context_metadata_behavior = :apply_to_host_groups
25
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe YummyGuide::Administrate::ApplicationDashboard do
6
+ describe ".index_fixed_columns_count" do
7
+ it "returns the subclass constant when defined" do
8
+ subclass = Class.new(described_class)
9
+ subclass.const_set(:INDEX_FIXED_COLUMNS_COUNT, 3)
10
+
11
+ expect(subclass.index_fixed_columns_count).to eq(3)
12
+ end
13
+
14
+ it "falls back to the superclass value" do
15
+ subclass = Class.new(described_class)
16
+
17
+ expect(subclass.index_fixed_columns_count).to eq(1)
18
+ end
19
+ end
20
+
21
+ describe ".collection_attribute_sortable?" do
22
+ it "uses collection attributes when explicit sortable attributes are absent" do
23
+ subclass = Class.new(described_class)
24
+ subclass.const_set(:COLLECTION_ATTRIBUTES, %i[id created_at])
25
+
26
+ expect(subclass.collection_attribute_sortable?(:created_at)).to be(true)
27
+ expect(subclass.collection_attribute_sortable?(:name)).to be(false)
28
+ end
29
+
30
+ it "prefers explicit sortable attributes when present" do
31
+ subclass = Class.new(described_class)
32
+ subclass.const_set(:COLLECTION_ATTRIBUTES, %i[id created_at])
33
+ subclass.const_set(:COLLECTION_SORTABLE_ATTRIBUTES, %i[name])
34
+
35
+ expect(subclass.collection_attribute_sortable?(:name)).to be(true)
36
+ expect(subclass.collection_attribute_sortable?(:created_at)).to be(false)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe YummyGuide::Administrate::CollectionHelper do
6
+ subject(:helper_host) do
7
+ Class.new do
8
+ include YummyGuide::Administrate::CollectionHelper
9
+ end.new
10
+ end
11
+
12
+ it "caps the fixed column count by the number of visible attributes" do
13
+ dashboard_class = Class.new do
14
+ def self.index_fixed_columns_count
15
+ 4
16
+ end
17
+ end
18
+
19
+ page = Object.new
20
+ page.instance_variable_set(:@dashboard, dashboard_class.new)
21
+ collection_presenter = Struct.new(:attribute_types).new({ id: :integer, name: :string })
22
+
23
+ expect(helper_host.yummy_guide_administrate_collection_table_fixed_columns_count(page: page, collection_presenter: collection_presenter)).to eq(2)
24
+ end
25
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe YummyGuide::Administrate::DatetimeFilterParameters do
6
+ subject(:host) do
7
+ Class.new do
8
+ include YummyGuide::Administrate::DatetimeFilterParameters
9
+
10
+ def normalize(filters, keys:)
11
+ normalize_datetime_filter_params(filters, keys: keys)
12
+ end
13
+ end.new
14
+ end
15
+
16
+ it "combines date and time parts into a datetime-local string" do
17
+ filters = ActionController::Parameters.new(
18
+ start_date_date: "2026-05-07",
19
+ start_date_hour: "09",
20
+ start_date_minute: "30"
21
+ )
22
+
23
+ normalized = host.normalize(filters, keys: [:start_date])
24
+
25
+ expect(normalized[:start_date]).to eq("2026-05-07T09:30")
26
+ expect(normalized).not_to have_key(:start_date_date)
27
+ end
28
+
29
+ it "returns just the date when time parts are blank" do
30
+ filters = ActionController::Parameters.new(
31
+ end_date_date: "2026-05-07",
32
+ end_date_hour: "",
33
+ end_date_minute: ""
34
+ )
35
+
36
+ normalized = host.normalize(filters, keys: [:end_date])
37
+
38
+ expect(normalized[:end_date]).to eq("2026-05-07")
39
+ end
40
+
41
+ it "sets nil when the date is blank" do
42
+ filters = ActionController::Parameters.new(
43
+ start_date_date: "",
44
+ start_date_hour: "09",
45
+ start_date_minute: "30"
46
+ )
47
+
48
+ normalized = host.normalize(filters, keys: [:start_date])
49
+
50
+ expect(normalized[:start_date]).to be_nil
51
+ end
52
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe YummyGuide::Administrate::Fields::JsonPrettyField do
6
+ def build_field(data)
7
+ described_class.new(:payload, data, :show, resource: Object.new)
8
+ end
9
+
10
+ it "pretty prints JSON strings" do
11
+ field = build_field('{"a":1}')
12
+
13
+ expect(field.to_s).to include("\"a\": 1")
14
+ end
15
+
16
+ it "falls back to the raw string when parsing fails" do
17
+ field = build_field("not-json")
18
+
19
+ expect(field.to_s).to eq("not-json")
20
+ end
21
+ end
22
+
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe YummyGuide::Administrate::Fields::VersionItemField do
6
+ before do
7
+ stub_const("SpecVersionItemRecord", Class.new do
8
+ attr_reader :id
9
+
10
+ def initialize(id)
11
+ @id = id
12
+ end
13
+ end)
14
+ end
15
+
16
+ let(:record) { SpecVersionItemRecord.new(12) }
17
+ let(:resource) { Struct.new(:item_type, :item_id, :reify).new("Widget", 12, nil) }
18
+
19
+ def build_field(data = record)
20
+ described_class.new(:item, data, :show, resource: resource, namespace: :admin)
21
+ end
22
+
23
+ it "builds a readable label" do
24
+ expect(build_field.label).to eq("SpecVersionItemRecord #12")
25
+ end
26
+
27
+ it "uses a fallback label when the target resource is unavailable" do
28
+ expect(build_field(nil).label).to eq("Widget #12")
29
+ end
30
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe YummyGuide::Administrate::Fields::VersionWhodunnitField do
6
+ let(:user_class) do
7
+ Class.new do
8
+ def self.find_by(id:)
9
+ return unless id == 5
10
+
11
+ Struct.new(:id, :full_name) do
12
+ def self.model_name
13
+ ActiveModel::Name.new(self, nil, "User")
14
+ end
15
+ end.new(5, "Test User")
16
+ end
17
+ end
18
+ end
19
+
20
+ def build_field(data)
21
+ described_class.new(:whodunnit, data, :show, resource: Object.new, user_class: user_class)
22
+ end
23
+
24
+ it "uses the resolved user label when found" do
25
+ expect(build_field(5).label).to eq("Test User")
26
+ end
27
+
28
+ it "falls back to a generic label when the user is missing" do
29
+ expect(build_field(99).label).to eq("User #99")
30
+ end
31
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ RSpec.describe YummyGuide::Administrate::FilterFormHelper do
6
+ subject(:helper_host) do
7
+ Class.new do
8
+ include YummyGuide::Administrate::FilterFormHelper
9
+ end.new
10
+ end
11
+
12
+ before do
13
+ Time.zone = "Tokyo"
14
+ end
15
+
16
+ it "normalizes current values to string keys" do
17
+ values = ActionController::Parameters.new(start_date: "2026-05-07")
18
+
19
+ expect(helper_host.yummy_guide_administrate_filter_current_values(values)).to eq("start_date" => "2026-05-07")
20
+ end
21
+
22
+ it "builds end-of-day defaults for date-only inputs" do
23
+ parts = helper_host.yummy_guide_administrate_datetime_filter_parts("2026-05-07", end_of_day: true)
24
+
25
+ expect(parts).to eq(date: "2026-05-07", hour: "23", minute: "59")
26
+ end
27
+
28
+ it "combines parsed datetime parts back into a single value" do
29
+ combined = helper_host.yummy_guide_administrate_datetime_filter_combined_value(date: "2026-05-07", hour: "08", minute: "00")
30
+
31
+ expect(combined).to eq("2026-05-07T08:00")
32
+ end
33
+
34
+ it "maps checkbox group objects through label and value methods" do
35
+ option = Struct.new(:id, :name).new(10, "Japanese")
36
+
37
+ expect(helper_host.yummy_guide_administrate_checkbox_group_options([option], label_method: :name, value_method: :id)).to eq([["Japanese", 10]])
38
+ end
39
+ end
40
+