uchi 0.1.7 → 0.1.8

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: de2b1d1a3dd3e56ab24bce7e64663414c0dbfe4dfc4ac782ca47fcd967e4b048
4
- data.tar.gz: b9e0322ebafd58e4369f8b1e0087c8e4ec3bb637226a02b37b890d16be9986ad
3
+ metadata.gz: 56f0102a6440897f16c6a2cd432d2ce52a7c018c0546fd7029a012ce937d5511
4
+ data.tar.gz: 9c3fc85ba5021deec675ed623ff72e3c529aedcfcf75797f1d4dcc36d624ad69
5
5
  SHA512:
6
- metadata.gz: b2ff7837db46a22eb39adc6cbd07077d9dd288032e6d77468a596c8042e3ab8476bc92ee57a3161ab177523701c0f7e4e4e8f8f49a45fc2691e8eaf0c4c2a0a2
7
- data.tar.gz: 01accad1e6c453af98f1cccde568c46eb6b6eb9e0474f5454f3b9b59e215df636b00a599909b506f0c878a44d40d2f4b936329d0dab8aea82f422cf0709586ff
6
+ metadata.gz: 5c8c4ad2edc2e428b6b5c58f1c8a007d4fd60d6b84484eecadac47d0104fd8f021f1fedb3d422867d014e4672f29f1df6fbe1edbb09ee7b35570bc078a1a29b7
7
+ data.tar.gz: df693cb8af530b74deaaa10059a3a9162bf7ddc10cb77b64d01dfb79d620826e3f885935ef4b2ccd9fd3fbe0b449eb6f59556bfd09738a6870f3f6fe1c46731a
@@ -2,7 +2,7 @@
2
2
  class="relative"
3
3
  data-action="keydown.esc->belongs-to#closeDropdown"
4
4
  data-controller="belongs-to"
5
- data-belongs-to-backend-url-value="<%= helpers.belongs_to_associated_records_path(field: field.name, model: repository.model, record_id: record&.id) %>"
5
+ data-belongs-to-backend-url-value="<%= helpers.uchi.belongs_to_associated_records_path(field: field.name, model: repository.model, record_id: record&.id) %>"
6
6
  >
7
7
  <%= render(Uchi::Flowbite::Input::Label.new(attribute: field.name, form: form, options: {for: dom_id_for_toggle})) { label } %>
8
8
 
@@ -19,7 +19,14 @@ module Uchi
19
19
  "No association named #{field.name.inspect} found on #{record.class}"
20
20
  end
21
21
 
22
- model = reflection.klass
22
+ model = if reflection.polymorphic?
23
+ associated_record&.class
24
+ else
25
+ reflection.klass
26
+ end
27
+
28
+ return nil if model.nil?
29
+
23
30
  repository_class = Uchi::Repository.for_model(model)
24
31
  repository_class.new
25
32
  end
@@ -34,7 +41,14 @@ module Uchi
34
41
 
35
42
  def associated_repository
36
43
  @associated_repository ||= begin
37
- model = reflection.klass
44
+ model = if reflection.polymorphic?
45
+ associated_record&.class
46
+ else
47
+ reflection.klass
48
+ end
49
+
50
+ return nil if model.nil?
51
+
38
52
  repository_class = Uchi::Repository.for_model(model)
39
53
  repository_class.new
40
54
  end
@@ -125,11 +139,31 @@ module Uchi
125
139
  :attributes
126
140
  end
127
141
 
142
+ # Returns the actions this field should appear on.
143
+ #
144
+ # For polymorphic associations, excludes :edit and :new to prevent showing
145
+ # the field on forms where the type cannot be determined.
146
+ def on(*actions)
147
+ on = super
148
+ return on - [:edit, :new] if polymorphic? && actions.empty?
149
+
150
+ on
151
+ end
152
+
128
153
  def param_key
129
154
  # TODO: This is too naive. We need to match this to the actual foreign
130
155
  # key of the model.
131
156
  :"#{name}_id"
132
157
  end
158
+
159
+ private
160
+
161
+ def polymorphic?
162
+ return false unless repository
163
+
164
+ reflection = repository.model.reflect_on_association(name)
165
+ reflection&.polymorphic? || false
166
+ end
133
167
  end
134
168
  end
135
169
  end
@@ -2,7 +2,7 @@
2
2
  class="relative"
3
3
  data-action="keydown.esc->has-many#closeDropdown"
4
4
  data-controller="has-many"
5
- data-has-many-backend-url-value="<%= helpers.has_many_associated_records_path(field: field.name, model: repository.model, record_id: record&.id) %>"
5
+ data-has-many-backend-url-value="<%= helpers.uchi.has_many_associated_records_path(field: field.name, model: repository.model, record_id: record&.id) %>"
6
6
  data-has-many-field-name-value="<%= field_name_for_input %>"
7
7
  >
8
8
  <%= render(Uchi::Flowbite::Input::Label.new(attribute: field.name, form: form, options: {for: dom_id_for_toggle})) { label } %>
@@ -20,7 +20,7 @@ module Uchi::Flowbite
20
20
 
21
21
  class << self
22
22
  def classes(size: :default, state: :default, style: :default)
23
- style = styles.fetch(style) or raise "wut"
23
+ style = styles.fetch(style)
24
24
  classes = style.fetch(state)
25
25
  classes + sizes.fetch(size)
26
26
  end
@@ -93,7 +93,13 @@ module Uchi::Flowbite
93
93
  !!@disabled
94
94
  end
95
95
 
96
+ # Returns true if the object has errors. Returns false if there is no
97
+ # object.
98
+ #
99
+ # @return [Boolean] true if there are errors, false otherwise.
96
100
  def errors?
101
+ return false unless @object
102
+
97
103
  @object.errors.include?(@attribute.intern)
98
104
  end
99
105
 
@@ -36,6 +36,8 @@ module Uchi::Flowbite
36
36
  end
37
37
 
38
38
  def errors?
39
+ return false unless @object
40
+
39
41
  @object.errors.include?(@attribute.intern)
40
42
  end
41
43
 
@@ -67,7 +67,11 @@ module Uchi::Flowbite
67
67
  renders_one :label
68
68
 
69
69
  # Returns the errors for attribute
70
+ #
71
+ # @return [Array<String>] An array of error messages for the attribute.
70
72
  def errors
73
+ return [] unless @object
74
+
71
75
  @object.errors[@attribute] || []
72
76
  end
73
77
 
@@ -185,7 +189,13 @@ module Uchi::Flowbite
185
189
  end
186
190
 
187
191
  def id_for_hint_element
188
- "#{@form.object_name}_#{@attribute}_hint"
192
+ [
193
+ @form.object_name,
194
+ @attribute,
195
+ "hint"
196
+ ]
197
+ .compact_blank
198
+ .join("_")
189
199
  end
190
200
 
191
201
  # @return [Hash] The keyword arguments for the input component.
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Uchi::Flowbite
4
+ class Styles
5
+ class StyleNotFoundError < ::KeyError; end
6
+
7
+ class << self
8
+ def from_hash(styles_hash)
9
+ styles = Styles.new
10
+ styles_hash.each do |style_name, states_hash|
11
+ styles.add_style(style_name, states_hash)
12
+ end
13
+ styles
14
+ end
15
+ end
16
+
17
+ def add_style(style_name, states_hash)
18
+ @styles[style_name] = Uchi::Flowbite::Style.new(states_hash)
19
+ end
20
+
21
+ def fetch(style_name)
22
+ return @styles[style_name] if @styles.key?(style_name)
23
+
24
+ raise \
25
+ StyleNotFoundError,
26
+ "Style not found: #{style_name}. Available styles: " \
27
+ "#{@styles.keys.sort.join(", ")}"
28
+ end
29
+
30
+ def initialize
31
+ @styles = {}
32
+ end
33
+ end
34
+ end
@@ -42,7 +42,7 @@
42
42
  >
43
43
  <% actions.each do |action| %>
44
44
  <li role="menuitem">
45
- <%= form_with url: helpers.actions_executions_path, method: :post, class: "block" do %>
45
+ <%= form_with url: helpers.uchi.actions_executions_path, method: :post, class: "block" do %>
46
46
  <%= hidden_field_tag :model, repository.model.name %>
47
47
  <%= hidden_field_tag :action_name, action.class.name %>
48
48
  <%= hidden_field_tag :id, record.id %>
@@ -9,7 +9,7 @@ module Uchi
9
9
  before_action :set_repository
10
10
 
11
11
  def create
12
- @record = build_record
12
+ @record = build_record_for_new
13
13
  if @record.save
14
14
  flash[:success] = @repository.translate.successful_create
15
15
  redirect_to(@repository.routes.path_for(:show, id: @record.id), status: :see_other)
@@ -54,7 +54,7 @@ module Uchi
54
54
  end
55
55
 
56
56
  def new
57
- @record = build_record
57
+ @record = build_record_for_new
58
58
  end
59
59
 
60
60
  def show
@@ -63,7 +63,7 @@ module Uchi
63
63
 
64
64
  def update
65
65
  @record = find_record
66
- if @record.update(record_params)
66
+ if @record.update(record_params_for_update)
67
67
  flash[:success] = @repository.translate.successful_update
68
68
  redirect_to(@repository.routes.path_for(:show, id: @record.id, uniq: rand), status: :see_other)
69
69
  else
@@ -73,8 +73,8 @@ module Uchi
73
73
 
74
74
  private
75
75
 
76
- def build_record
77
- @repository.build(record_params)
76
+ def build_record_for_new
77
+ @repository.build(record_params_for_new)
78
78
  end
79
79
 
80
80
  # Returns the path to use for the cancel link
@@ -128,10 +128,26 @@ module Uchi
128
128
  25
129
129
  end
130
130
 
131
- def record_params
132
- permitted_params = @repository.fields_for_edit.map(&:permitted_param)
131
+ def permitted_params_for_edit
132
+ @repository.fields_for_edit.map(&:permitted_param)
133
+ end
134
+
135
+ def permitted_params_for_new
136
+ @repository.fields_for_new.map(&:permitted_param)
137
+ end
138
+
139
+ def record_params_for_edit
140
+ (params[@repository.model_param_key] || ActionController::Parameters.new)
141
+ .permit(*permitted_params_for_edit)
142
+ end
143
+
144
+ def record_params_for_update
145
+ record_params_for_edit
146
+ end
147
+
148
+ def record_params_for_new
133
149
  (params[@repository.model_param_key] || ActionController::Parameters.new)
134
- .permit(*permitted_params)
150
+ .permit(*permitted_params_for_new)
135
151
  end
136
152
 
137
153
  # Returns the repository class associated with this controller.
@@ -16,7 +16,7 @@
16
16
  </head>
17
17
 
18
18
  <body class="antialiased bg-neutral-secondary-medium h-full p-2">
19
- <div class="md:flex">
19
+ <div class="md:flex h-full">
20
20
  <%= render(partial: "uchi/navigation/main") %>
21
21
 
22
22
  <main class="py-6 overflow-x-hidden grow md:px-6"><%= yield %></main>
@@ -15,10 +15,10 @@
15
15
  <%= render(Uchi::Flowbite::Card.new) do %>
16
16
  <%= form_with model: @record, url: @repository.routes.path_for(:create) do |form| %>
17
17
  <div class="space-y-6">
18
- <% @repository.fields_for_edit.each do |field| %>
18
+ <% @repository.fields_for_new.each do |field| %>
19
19
  <div>
20
20
  <%= render(
21
- field.edit_component(
21
+ field.new_component(
22
22
  form: form,
23
23
  repository: @repository,
24
24
  label: @repository.translate.field_label(field),
@@ -127,7 +127,7 @@ module Uchi
127
127
  protected
128
128
 
129
129
  def default_on
130
- [:edit, :index, :show]
130
+ [:edit, :index, :new, :show]
131
131
  end
132
132
 
133
133
  def default_searchable?
data/lib/uchi/field.rb CHANGED
@@ -51,6 +51,17 @@ module Uchi
51
51
  @name = name.to_sym
52
52
  end
53
53
 
54
+ def new_component(form:, repository:, label: nil, hint: nil)
55
+ # For now all components use the same component for Edit and New. Override
56
+ # this method in a subclass to provide a different component.
57
+ edit_component(
58
+ form: form,
59
+ hint: hint,
60
+ label: label,
61
+ repository: repository
62
+ )
63
+ end
64
+
54
65
  # Returns the key that this field is expected to use in params
55
66
  def param_key
56
67
  name.to_sym
@@ -0,0 +1,21 @@
1
+ module Uchi
2
+ class Plugins
3
+ attr_reader :registered
4
+
5
+ def hook(hook_name, **kwargs)
6
+ registered.each do |plugin_class|
7
+ next unless plugin_class.hooks_into?(hook_name)
8
+
9
+ plugin_class.hook(hook_name, **kwargs)
10
+ end
11
+ end
12
+
13
+ def initialize
14
+ @registered = []
15
+ end
16
+
17
+ def register(plugin_class)
18
+ @registered << plugin_class
19
+ end
20
+ end
21
+ end
@@ -22,7 +22,7 @@ module Uchi
22
22
  end
23
23
 
24
24
  def root_path
25
- "/#{uchi_namespace}/"
25
+ "/#{uchi_path}/"
26
26
  end
27
27
 
28
28
  private
@@ -34,7 +34,7 @@ module Uchi
34
34
 
35
35
  def plural_path_for(_action, **options)
36
36
  parts = [
37
- uchi_namespace,
37
+ uchi_as,
38
38
  plural,
39
39
  "path"
40
40
  ].compact
@@ -47,15 +47,19 @@ module Uchi
47
47
  action = nil if action == :destroy
48
48
  parts = [
49
49
  action,
50
- uchi_namespace,
50
+ uchi_as,
51
51
  singular,
52
52
  "path"
53
53
  ].compact
54
54
  call_url_helper_in_main_app(parts, **options)
55
55
  end
56
56
 
57
- def uchi_namespace
58
- :uchi
57
+ def uchi_as
58
+ Uchi.routes.mount_as
59
+ end
60
+
61
+ def uchi_path
62
+ Uchi.routes.mount_at
59
63
  end
60
64
  end
61
65
  end
@@ -51,18 +51,31 @@ module Uchi
51
51
  end
52
52
 
53
53
  # Returns an array of fields to show on the edit page.
54
+ #
55
+ # @return [Array<Uchi::Field>]
54
56
  def fields_for_edit
55
- fields.select { |field| field.on.include?(:edit) }.each { |field| field.repository = self }
57
+ fields_for(:edit)
56
58
  end
57
59
 
58
60
  # Returns an array of fields to show on the index page.
61
+ #
62
+ # @return [Array<Uchi::Field>]
59
63
  def fields_for_index
60
- fields.select { |field| field.on.include?(:index) }.each { |field| field.repository = self }
64
+ fields_for(:index)
65
+ end
66
+
67
+ # Returns an array of fields to show on the new page.
68
+ #
69
+ # @return [Array<Uchi::Field>]
70
+ def fields_for_new
71
+ fields_for(:new)
61
72
  end
62
73
 
63
74
  # Returns an array of fields to show on the show page.
75
+ #
76
+ # @return [Array<Uchi::Field>]
64
77
  def fields_for_show
65
- fields.select { |field| field.on.include?(:show) }.each { |field| field.repository = self }
78
+ fields_for(:show)
66
79
  end
67
80
 
68
81
  def find_all(search: nil, scope: model.all, sort_order: default_sort_order)
@@ -185,6 +198,20 @@ module Uchi
185
198
  end
186
199
  end
187
200
 
201
+ # Returns an array of fields to show for the given action.
202
+ #
203
+ # @param action [Symbol] The action to get fields for. One of :index, :show,
204
+ # :new, :edit.
205
+ #
206
+ # @return [Array<Uchi::Field>]
207
+ def fields_for(action)
208
+ fields_with_repository.select { |field| field.on.include?(action) }
209
+ end
210
+
211
+ def fields_with_repository
212
+ fields.each { |field| field.repository = self }
213
+ end
214
+
188
215
  def searchable_fields
189
216
  @searchable_fields ||= fields.select { |field| field.searchable? }
190
217
  end
data/lib/uchi/routes.rb CHANGED
@@ -7,19 +7,26 @@ module Uchi
7
7
  # Rails.application.routes.draw do
8
8
  # Uchi.routes.mount(self)
9
9
  # end
10
+ #
11
+ # @param host_routes [ActionDispatch::Routing::Mapper] The host
12
+ # application's routes mapper.
13
+ #
14
+ # @param at [Symbol] The path segment where Uchi should be mounted.
10
15
  def mount(host_routes, at: default_at)
16
+ @mount_at = (at || default_at).to_sym
11
17
  host_routes.mount(
12
18
  Uchi::Engine,
13
- at: at
19
+ as: mount_as,
20
+ at: mount_at
14
21
  )
15
22
 
16
- draw_repository_routes(host_routes, at: at)
23
+ draw_repository_routes(host_routes, at: mount_at)
17
24
  end
18
25
 
19
26
  def draw_root_route(routes, repository:, at: default_at)
20
27
  return unless repository
21
28
 
22
- routes.namespace(at) do
29
+ routes.namespace(at, as: mount_as) do
23
30
  routes.root to: "#{repository.controller_name}#index"
24
31
  end
25
32
  end
@@ -28,7 +35,7 @@ module Uchi
28
35
  repositories = Uchi::Repository.all
29
36
  repositories.each do |repository_class|
30
37
  resources_name = repository_class.controller_name
31
- routes.namespace(at) do
38
+ routes.namespace(at, as: mount_as) do
32
39
  routes.resources(resources_name)
33
40
  end
34
41
  end
@@ -36,10 +43,25 @@ module Uchi
36
43
  draw_root_route(routes, at: at, repository: repositories.first)
37
44
  end
38
45
 
46
+ # Returns the name to use when generating routing helper method names
47
+ def mount_as
48
+ :uchi
49
+ end
50
+
51
+ # Returns the path prefix for the routes, i.e. the first URL segment where
52
+ # Uchi can be requested.
53
+ def mount_at
54
+ @mount_at ||= default_at
55
+ end
56
+
39
57
  private
40
58
 
41
59
  def default_at
42
- "uchi"
60
+ :uchi
61
+ end
62
+
63
+ def mount_path
64
+ mount_at
43
65
  end
44
66
  end
45
67
  end
data/lib/uchi/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Uchi
4
- VERSION = "0.1.7"
4
+ VERSION = "0.1.8"
5
5
  end
data/lib/uchi.rb CHANGED
@@ -11,12 +11,17 @@ require "uchi/action"
11
11
  require "uchi/action_response"
12
12
  require "uchi/field"
13
13
  require "uchi/i18n"
14
+ require "uchi/plugins"
14
15
  require "uchi/repository"
15
16
  require "uchi/routes"
16
17
  require "uchi/sort_order"
17
18
  require "uchi/repository/translate"
18
19
 
19
20
  module Uchi
21
+ def self.plugins
22
+ @plugins ||= Plugins.new
23
+ end
24
+
20
25
  def self.routes
21
26
  @routes ||= Routes.new
22
27
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: uchi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.7
4
+ version: 0.1.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jakob Skjerning
@@ -169,6 +169,7 @@ files:
169
169
  - app/components/uchi/flowbite/input_field/url.rb
170
170
  - app/components/uchi/flowbite/link.rb
171
171
  - app/components/uchi/flowbite/style.rb
172
+ - app/components/uchi/flowbite/styles.rb
172
173
  - app/components/uchi/flowbite/toast.rb
173
174
  - app/components/uchi/flowbite/toast/icon.html.erb
174
175
  - app/components/uchi/flowbite/toast/icon.rb
@@ -262,6 +263,7 @@ files:
262
263
  - lib/uchi/pagy/toolbox/helpers/page_url.rb
263
264
  - lib/uchi/pagy/toolbox/paginators/method.rb
264
265
  - lib/uchi/pagy/toolbox/paginators/offset.rb
266
+ - lib/uchi/plugins.rb
265
267
  - lib/uchi/repository.rb
266
268
  - lib/uchi/repository/routes.rb
267
269
  - lib/uchi/repository/translate.rb