habarico 7.3.0.pre.beta.7 → 7.3.0.pre.beta.9

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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/lib/habarico/contrib/preferences/base_preferences.rb +0 -1
  4. data/lib/habarico/contrib/preferences/config.rb +2 -3
  5. data/lib/habarico/contrib/preferences/registry.rb +35 -16
  6. data/lib/habarico/contrib/preferences/schemas.rb +0 -80
  7. data/lib/habarico/definition/action_reference.rb +2 -1
  8. data/lib/habarico/definition/boxes/box.rb +1 -1
  9. data/lib/habarico/definition/concerns/definition_model.rb +17 -5
  10. data/lib/habarico/definition/default_actions_reference.rb +18 -0
  11. data/lib/habarico/definition/definition.rb +1 -2
  12. data/lib/habarico/generators/action/resource_action_endpoint_parameters_generator.rb +13 -3
  13. data/lib/habarico/generators/action/trigger_action_schema_generator.rb +7 -1
  14. data/lib/habarico/generators/action/upload_files_action_parameters_generator.rb +1 -1
  15. data/lib/habarico/generators/action/view_action_parameters_generator.rb +23 -3
  16. data/lib/habarico/generators/endpoint/field_schema_generator.rb +2 -2
  17. data/lib/habarico/generators/endpoint/filter_schema_generator.rb +1 -1
  18. data/lib/habarico/generators/menu/menu_view_endpoint_parameters_generator.rb +3 -3
  19. data/lib/habarico/generators/view/action_descriptor_generator.rb +42 -1
  20. data/lib/habarico/generators/view/box_schema_generator.rb +21 -3
  21. data/lib/habarico/generators/view/detail_view_parameters_generator.rb +9 -5
  22. data/lib/habarico/generators/view/gallery_box_parameters_generator.rb +4 -7
  23. data/lib/habarico/generators/view/list_box_parameters_generator.rb +33 -33
  24. data/lib/habarico/generators/view/list_view_parameters_generator.rb +19 -12
  25. data/lib/habarico/generators/view/view_endpoint_parameters_generator.rb +13 -2
  26. data/lib/habarico/helpers/box_helper.rb +27 -0
  27. data/lib/habarico/schema/action/resource_action_endpoint_parameters.rb +2 -8
  28. data/lib/habarico/schema/action/upload_files_action_parameters.rb +1 -1
  29. data/lib/habarico/schema/action/view_action_parameters.rb +3 -2
  30. data/lib/habarico/schema/endpoint/field_schema.rb +1 -1
  31. data/lib/habarico/schema/endpoint/filter_schema.rb +1 -1
  32. data/lib/habarico/schema/resource_reference.rb +72 -0
  33. data/lib/habarico/schema/types.rb +3 -0
  34. data/lib/habarico/schema/view/action_descriptor.rb +21 -0
  35. data/lib/habarico/schema/view/box_schema.rb +3 -3
  36. data/lib/habarico/schema/view/default_actions_descriptor.rb +30 -0
  37. data/lib/habarico/schema/view/detail_view_parameters.rb +2 -2
  38. data/lib/habarico/schema/view/gallery_box_parameters.rb +2 -5
  39. data/lib/habarico/schema/view/list_box_parameters.rb +5 -12
  40. data/lib/habarico/schema/view/list_view_parameters.rb +3 -3
  41. data/lib/habarico/schema/view/view_endpoint_parameters.rb +2 -5
  42. data/lib/habarico.rb +5 -2
  43. metadata +4 -3
  44. data/lib/habarico/generators/action/view_action_endpoint_parameters_generator.rb +0 -42
  45. data/lib/habarico/schema/action/view_action_endpoint_parameters.rb +0 -16
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ccab4855ae575799d8c7b8dc724f8b56d5115d77f1aa3fe8a69f8c57d39b002a
4
- data.tar.gz: 89d5e992dc3aa9121a26d25c72f1151b34c0136d01b05edc9e98d3e4f8de8a6d
3
+ metadata.gz: d4d4ae92bdfc004471375643191a1ca845fb27d8e5186906c185e683705e2277
4
+ data.tar.gz: da980d711e305d3cf7b3fb892845068945d308bf0bda223d9ae8f8b5e48e803b
5
5
  SHA512:
6
- metadata.gz: 98b13911cb62726a2816019e305479afc1ee08e138afb608999902ad7ef95dca786642114a03fc96cc89c51d017574d66fe2eca46af29cda7ab4a4b07bf84b37
7
- data.tar.gz: 5f411af0470bd6c2320a669e479173693025f60463568fa1d21d085773bd4c0b19f87837d5200184279df71ed8fcef9caeddb1210e13d1caa22fb5cde6adf2c6
6
+ metadata.gz: 9b4846e10c4ca8541a121e8a552fd6dc6265e2de4716e50e9af4c97ed5a2b4a4e781b4b69f7e3edb2330374eaf90b80bb20f516fcefdd8474576ae08f9942a40
7
+ data.tar.gz: 5592767aca7bd94f9748dea4e0770daaa110f5dd04ca61b21ec2472c7e838fbba74b77f0685e7f613a30c564116eb8aea2795db8922f45bb5337daf2a6635cff
data/VERSION CHANGED
@@ -1 +1 @@
1
- 7.3.0-beta.7
1
+ 7.3.0-beta.9
@@ -5,7 +5,6 @@ module Habarico
5
5
  BASE_PREFERENCES = {
6
6
  "theme" => Schemas::THEME,
7
7
  "locale" => Schemas::LOCALE,
8
- "preferences.dashboard" => Schemas::DASHBOARD,
9
8
  "preferences.layout.app.pageWidth" => Schemas::APP_PAGE_WIDTH,
10
9
  "preferences.layout.app.contrastMode" => Schemas::CONTRAST_MODE,
11
10
  "preferences.layout.app.stripedRows" => Schemas::STRIPED_ROWS,
@@ -13,11 +13,10 @@ module Habarico
13
13
  # no limit. Mirrors the Python `HABARICO_PREFERENCES["MAX_VALUE_BYTES"]` setting.
14
14
  #
15
15
  # The values below sit an order of magnitude above what the frontend actually writes:
16
- # 100 datagrid columns are ~2 KB of `columns`/`columnWidths`, a 30-widget dashboard
17
- # ~19 KB and 20 saved queries ~11 KB at their fullest.
16
+ # 100 datagrid columns are ~2 KB of `columns`/`columnWidths` and 20 saved queries ~11 KB at
17
+ # their fullest. A package registering its own key brings its own limit with it.
18
18
  mattr_accessor :max_value_bytes, default: {
19
19
  "default" => 16 * 1024,
20
- "preferences.dashboard" => 32 * 1024,
21
20
  %r{[a-zA-Z][a-zA-Z0-9_./-]*\.savedQueries} => 32 * 1024,
22
21
  }
23
22
 
@@ -23,7 +23,7 @@ module Habarico
23
23
  # `max_bytes` needs no sentinel, unlike `default`: nil is not a usable size, so it simply
24
24
  # means "not given at registration" and defers to `max_value_bytes`. It is resolved once, at
25
25
  # registration, exactly like `defaults` - nothing reads the configuration per write.
26
- Entry = Struct.new(:schema, :default, :validator, :max_bytes, keyword_init: true)
26
+ Entry = Struct.new(:schema, :default, :validator, :max_bytes, :migrate_value, keyword_init: true)
27
27
 
28
28
  class PreferenceRegistry
29
29
  def initialize
@@ -37,11 +37,14 @@ module Habarico
37
37
  @pattern_entries.clear
38
38
  end
39
39
 
40
- def register(key, schema:, default: EMPTY, max_bytes: nil)
40
+ # `migrate_value` is called with a stored value that no longer validates, before it would be
41
+ # deleted; whatever it returns is validated and saved. It is how the package that owns a
42
+ # shape changes it without a data migration.
43
+ def register(key, schema:, default: EMPTY, max_bytes: nil, migrate_value: nil)
41
44
  raise InvalidPreferenceKey, key unless key.is_a?(String) && key.match?(KEY_PATTERN)
42
45
  raise PreferenceAlreadyRegistered, key if @entries.key?(key)
43
46
 
44
- @entries[key] = build_entry(key, schema, default, max_bytes)
47
+ @entries[key] = build_entry(key, schema, default, max_bytes, migrate_value)
45
48
  nil
46
49
  end
47
50
 
@@ -147,20 +150,20 @@ module Habarico
147
150
  raise PreferenceValueTooLarge.new(key, limit, size) if size > limit
148
151
  end
149
152
 
150
- # Configured limit for the key - or pattern - a preference is being registered under. An
151
- # explicit `max_bytes:` wins; otherwise `max_value_bytes` decides, by the very string the
152
- # preference is registered under, falling back to its "default" entry; a nil there means
153
- # unlimited. A pattern may be configured as a Regexp or as its source string. Read once, at
154
- # registration, exactly like `defaults`: changing the config afterwards does not move an
155
- # already-registered limit.
153
+ # Configured limit for the key - or pattern - a preference is being registered under.
154
+ # `max_value_bytes` decides first, by the very string the preference is registered under: a
155
+ # deployment's word outranks a limit a gem registered, which is itself a default above the
156
+ # hash's "default" entry. A nil anywhere there means unlimited. A pattern may be configured as
157
+ # a Regexp or as its source string. Read once, at registration, exactly like `defaults`:
158
+ # changing the config afterwards does not move an already-registered limit.
156
159
  def resolve_max_bytes(registered_as, max_bytes)
157
- return max_bytes unless max_bytes.nil?
158
-
159
160
  limits = Preferences.max_value_bytes || {}
160
161
  limits.each do |configured, bytes|
161
162
  name = configured.is_a?(Regexp) ? configured.source : configured.to_s
162
163
  return bytes if name == registered_as
163
164
  end
165
+ return max_bytes unless max_bytes.nil?
166
+
164
167
  limits["default"]
165
168
  end
166
169
 
@@ -191,10 +194,10 @@ module Habarico
191
194
  raise PreferenceNotRegistered, key
192
195
  end
193
196
 
194
- def build_entry(key, schema, default, max_bytes = nil)
197
+ def build_entry(key, schema, default, max_bytes = nil, migrate_value = nil)
195
198
  validator = JSONSchemer.schema(schema)
196
199
  entry = Entry.new(schema: schema, default: default, validator: validator,
197
- max_bytes: resolve_max_bytes(key, max_bytes))
200
+ max_bytes: resolve_max_bytes(key, max_bytes), migrate_value: migrate_value)
198
201
  return entry if default.equal?(EMPTY) || default.respond_to?(:call)
199
202
 
200
203
  errors = validator.validate(default).to_a
@@ -203,6 +206,22 @@ module Habarico
203
206
  entry
204
207
  end
205
208
 
209
+ # Gives the owner's schema a chance to bring a stored value up to date, in place.
210
+ #
211
+ # A migration that does not validate, or one that raises, leaves the record to be deleted:
212
+ # a schema's own bug must not make reading preferences fail.
213
+ def migrate_stored_value(entry, record)
214
+ return false if entry.migrate_value.nil?
215
+
216
+ migrated = entry.migrate_value.call(record.value)
217
+ return false unless entry.validator.valid?(migrated)
218
+
219
+ record.update!(value: migrated)
220
+ true
221
+ rescue StandardError
222
+ false
223
+ end
224
+
206
225
  # Resolves a (possibly callable) default. A callable that returns EMPTY or an
207
226
  # invalid value degrades to EMPTY, mirroring Python `_resolve_default`.
208
227
  def resolve_default(entry)
@@ -221,7 +240,7 @@ module Habarico
221
240
  # returns nil (single-GET => 404, list => omit).
222
241
  def resolve(key, entry, record)
223
242
  if record
224
- if entry.validator.valid?(record.value)
243
+ if entry.validator.valid?(record.value) || migrate_stored_value(entry, record)
225
244
  return ResolvedPreference.new(key: key, value: record.value, updated_at: record.updated_at)
226
245
  end
227
246
 
@@ -241,8 +260,8 @@ module Habarico
241
260
  @registry ||= PreferenceRegistry.new
242
261
  end
243
262
 
244
- def self.register_preference(key, schema:, default: EMPTY, max_bytes: nil)
245
- registry.register(key, schema: schema, default: default, max_bytes: max_bytes)
263
+ def self.register_preference(key, schema:, default: EMPTY, max_bytes: nil, migrate_value: nil)
264
+ registry.register(key, schema: schema, default: default, max_bytes: max_bytes, migrate_value: migrate_value)
246
265
  end
247
266
 
248
267
  def self.register_preference_pattern(pattern, schema:, max_bytes: nil)
@@ -46,86 +46,6 @@ module Habarico
46
46
  },
47
47
  },
48
48
  }.freeze
49
-
50
- WIDGET_LAYOUT = {
51
- "type" => "object",
52
- "required" => %w[x y w h],
53
- "properties" => {
54
- "x" => { "type" => "integer", "minimum" => 0 },
55
- "y" => { "type" => "integer", "minimum" => 0 },
56
- "w" => { "type" => "integer", "minimum" => 1 },
57
- "h" => { "type" => "integer", "minimum" => 1 },
58
- "stackedH" => { "type" => "integer", "minimum" => 1 },
59
- "minW" => { "type" => "integer", "minimum" => 1 },
60
- "minH" => { "type" => "integer", "minimum" => 1 },
61
- },
62
- }.freeze
63
-
64
- GROUP_BY_BUCKET = {
65
- "oneOf" => [
66
- {
67
- "type" => "object",
68
- "required" => %w[type granularity],
69
- "properties" => {
70
- "type" => { "const" => "trunc" },
71
- "granularity" => { "enum" => %w[second minute hour day week month quarter year] },
72
- },
73
- },
74
- {
75
- "type" => "object",
76
- "required" => %w[type size],
77
- "properties" => {
78
- "type" => { "const" => "width" },
79
- "size" => { "type" => "number" },
80
- },
81
- },
82
- ],
83
- }.freeze
84
-
85
- GROUP_BY_ITEM = {
86
- "type" => "object",
87
- "required" => %w[field],
88
- "properties" => {
89
- "field" => { "type" => "string" },
90
- "bucket" => { "anyOf" => [GROUP_BY_BUCKET, { "type" => "null" }] },
91
- },
92
- }.freeze
93
-
94
- CHART_OPTIONS = {
95
- "type" => "object",
96
- "properties" => {
97
- "orientation" => { "enum" => %w[vertical horizontal] },
98
- "variant" => { "enum" => %w[pie doughnut] },
99
- },
100
- }.freeze
101
-
102
- WIDGET_CONFIG = {
103
- "type" => "object",
104
- "required" => %w[id type layout],
105
- "properties" => {
106
- "id" => { "type" => "string" },
107
- "type" => { "type" => "string" },
108
- "resource" => { "type" => %w[string null] },
109
- "label" => { "type" => %w[string null] },
110
- "layout" => WIDGET_LAYOUT,
111
- "filters" => { "type" => %w[object null], "additionalProperties" => { "type" => "string" } },
112
- "sort" => { "anyOf" => [SORT, { "type" => "null" }] },
113
- "perPage" => { "type" => %w[integer null], "minimum" => 1 },
114
- "columns" => { "type" => %w[array null], "items" => { "type" => "string" } },
115
- "field" => { "type" => %w[string null] },
116
- "aggregateFunction" => { "type" => %w[string null], "enum" => [nil, "count", "sum", "avg", "min", "max"] },
117
- "color" => { "type" => %w[string null], "enum" => [nil, "primary", "secondary", "success", "error", "warning", "info", "inherit"] },
118
- "groupBy" => { "type" => %w[array null], "items" => GROUP_BY_ITEM },
119
- "chartOptions" => { "anyOf" => [CHART_OPTIONS, { "type" => "null" }] },
120
- },
121
- }.freeze
122
-
123
- DASHBOARD = {
124
- "type" => "object",
125
- "properties" => {
126
- "widgets" => { "type" => "array", "items" => WIDGET_CONFIG },
127
- },
128
- }.freeze
129
49
  end
130
50
  end
131
51
  end
@@ -2,7 +2,8 @@ module Habarico
2
2
  class ActionReference
3
3
  include DefinitionModel
4
4
 
5
- attr_accessor :name, :label, :icon, :color, :action, :if, :permission, :menu_items, :view_mode, :dialog_width, :view_choices, :view_parameters, :view_keep_linked_parent
5
+ attr_accessor :name, :label, :icon, :color, :action, :if, :permission, :menu_items, :view_mode, :dialog_width, :view_choices, :view_parameters, :view_keep_linked_parent,
6
+ :reference, :confirm_dialog_title, :confirm_dialog_message
6
7
 
7
8
  def initialize(params = {})
8
9
  @permission = case params[:name].to_s
@@ -15,7 +15,7 @@ module Habarico
15
15
  attr_accessor :grid_item
16
16
 
17
17
  # List
18
- attr_accessor :reference, :target, :extra_filters, :row_actions, :list_toolbar_actions, :list_actions, :filters, :default_sorting, # rubocop:disable Lint/DuplicateMethods -- intentional for readability
18
+ attr_accessor :reference, :target, :extra_filters, :row_actions, :list_actions, :filters, :default_sorting, # rubocop:disable Lint/DuplicateMethods -- intentional for readability
19
19
  :sortable_field_name, :sortable_sort_for_filters, :single_click_action, :double_click_action
20
20
 
21
21
  # Object
@@ -3,11 +3,15 @@ module Habarico
3
3
  extend ActiveSupport::Concern
4
4
 
5
5
  def initialize(params = {})
6
- self.class.attrs.each do |attr|
7
- if params.key?(attr)
8
- self.send("#{attr}=", params[attr])
9
- end
10
- end
6
+ assign(params)
7
+ end
8
+
9
+ # Whether the author said anything about this attribute, as opposed to leaving it alone.
10
+ #
11
+ # The distinction an action slot needs: a slot nobody mentioned publishes the resource's default
12
+ # actions, while `nil` and `[]` are both decisions to render none of them.
13
+ def assigned?(attr)
14
+ @assigned_attrs.present? && @assigned_attrs.include?(attr)
11
15
  end
12
16
 
13
17
  def assign(params = {})
@@ -28,9 +32,17 @@ module Habarico
28
32
  @attrs
29
33
  end
30
34
 
35
+ # Records every assignment, so `assigned?` can tell a slot left alone from one emptied on
36
+ # purpose - whichever way the host app wrote it, a constructor hash or a plain setter.
31
37
  def attr_accessor(*attrs)
32
38
  self.attrs.concat(attrs)
33
39
  super(*attrs)
40
+ attrs.each do |attr|
41
+ define_method("#{attr}=") do |value|
42
+ (@assigned_attrs ||= []) << attr
43
+ instance_variable_set("@#{attr}", value)
44
+ end
45
+ end
34
46
  end
35
47
  end
36
48
 
@@ -0,0 +1,18 @@
1
+ module Habarico
2
+ # What an author writes in an action slot to mean "this resource's default actions".
3
+ #
4
+ # The client expands it into one ordinary action descriptor per supported action, so it names no
5
+ # action and carries none of a button's appearance - a label, an icon and a colour belong to each
6
+ # action it expands into.
7
+ class DefaultActionsReference
8
+ include DefinitionModel
9
+
10
+ # Resource whose defaults expand here: a name, `ResourceReference::CURRENT`,
11
+ # `ResourceReference::LIST`, or nothing for the resource the slot is read in
12
+ attr_accessor :reference
13
+
14
+ # Which of them: `:list`, `:detail` or `:all`
15
+ attr_accessor :scope
16
+
17
+ end
18
+ end
@@ -38,7 +38,7 @@ module Habarico
38
38
  # Generator classes - action
39
39
  :link_action_parameters_generator, :resource_action_endpoint_parameters_generator,
40
40
  :resource_action_parameters_generator, :save_action_parameters_generator, :action_schema_generator,
41
- :view_action_endpoint_parameters_generator, :view_action_parameters_generator,
41
+ :view_action_parameters_generator,
42
42
  :confirm_dialog_properties_generator, :global_resource_action_schema_generator,
43
43
  :resource_action_schema_generator, :upload_files_action_parameters_generator,
44
44
  :trigger_action_schema_generator,
@@ -117,7 +117,6 @@ module Habarico
117
117
  @resource_action_parameters_generator = Habarico::ResourceActionParametersGenerator
118
118
  @save_action_parameters_generator = Habarico::SaveActionParametersGenerator
119
119
  @action_schema_generator = Habarico::ActionSchemaGenerator
120
- @view_action_endpoint_parameters_generator = Habarico::ViewActionEndpointParametersGenerator
121
120
  @view_action_parameters_generator = Habarico::ViewActionParametersGenerator
122
121
  @confirm_dialog_properties_generator = Habarico::ConfirmDialogPropertiesGenerator
123
122
  @global_resource_action_schema_generator = Habarico::GlobalResourceActionSchemaGenerator
@@ -9,9 +9,7 @@ module Habarico
9
9
  Habarico::ResourceActionEndpointParameters.new(
10
10
  path: generate_path(action, context),
11
11
  method: generate_method(action, context),
12
- reference: generate_reference(action, context),
13
- action_name: generate_action_name(action, context),
14
- record_pk: generate_record_pk(action, context)
12
+ reference: generate_endpoint_reference(action, context),
15
13
  )
16
14
  end
17
15
 
@@ -59,6 +57,18 @@ module Habarico
59
57
  action.endpoint_reference
60
58
  end
61
59
 
60
+ # What the endpoint addresses, as one address. `action_name` and `record_pk` live inside it, so
61
+ # they need a resource to belong to: an action without a declared reference is this admin's own.
62
+ def generate_endpoint_reference(action, context)
63
+ action_name = generate_action_name(action, context)
64
+ record_pk = generate_record_pk(action, context)
65
+ reference = generate_reference(action, context)
66
+ reference = resource_name if reference.blank? && (action_name.present? || record_pk.present?)
67
+ return nil if reference.blank?
68
+
69
+ Habarico::ResourceReference.build(reference, action_name: action_name, record_pk: record_pk)
70
+ end
71
+
62
72
  def generate_action_name(action, _context)
63
73
  return action.endpoint_action_name if action.endpoint_action_name.present?
64
74
  return nil if Habarico::ActionHelper.is_standard_action?(action)
@@ -17,7 +17,7 @@ module Habarico
17
17
  )
18
18
  end
19
19
 
20
- action_schema_generator.generate(retarget(action, record_pk, reference), context)
20
+ action_schema_generator.generate(retarget(action, record_pk || detail_record_pk(action, context), reference), context)
21
21
  end
22
22
 
23
23
  protected
@@ -26,6 +26,12 @@ module Habarico
26
26
  @action_schema_generator ||= definition.action_schema_generator.new(definition, resource_path)
27
27
  end
28
28
 
29
+ # A trigger runs outside the page of the record it was raised for, so a detail action names
30
+ # that record explicitly - a schema endpoint leaves it to the page.
31
+ def detail_record_pk(action, context)
32
+ context.record_pk if action.is_detail && action.endpoint_record_pk.blank?
33
+ end
34
+
29
35
  # Copies the action before setting the target, so a declared action is never mutated.
30
36
  def retarget(action, record_pk, reference)
31
37
  return action if record_pk.blank? && reference.blank?
@@ -8,7 +8,7 @@ module Habarico
8
8
 
9
9
  # Create model
10
10
  Habarico::UploadFilesActionParameters.new(
11
- reference: generate_reference(action, context),
11
+ reference: Habarico::ResourceReference.build(generate_reference(action, context)),
12
12
  target: generate_target(action, context),
13
13
  accept: generate_accept(action, context),
14
14
  file_field: generate_file_field(action, context),
@@ -20,8 +20,28 @@ module Habarico
20
20
 
21
21
  protected
22
22
 
23
- def view_action_endpoint_parameters_generator
24
- @view_action_endpoint_parameters_generator ||= definition.view_action_endpoint_parameters_generator.new(definition, resource_path)
23
+ # What the view's endpoint addresses, left unaddressed when the action points at nothing of its
24
+ # own. `action_name` and `record_pk` live inside the address, so an action that has either names
25
+ # the resource it belongs to - the same rule the resource action endpoint follows. Only an
26
+ # explicit `endpoint_record_pk` is carried: the record of the request is the one the frontend
27
+ # already renders.
28
+ def view_action_endpoint_reference(action, _context)
29
+ action_name = target_view_action_name(action)
30
+ record_pk = action.endpoint_record_pk.presence
31
+ reference = action.endpoint_reference
32
+ reference = resource_name if reference.blank? && (action_name.present? || record_pk.present?)
33
+ return nil if reference.blank?
34
+
35
+ Habarico::ResourceReference.build(reference, action_name: action_name, record_pk: record_pk)
36
+ end
37
+
38
+ # The action segment is the *target view's*, not the action's own name: a reserved view
39
+ # (`list` / `create` / `edit` / `show`) is addressed by its type and has none, so an action
40
+ # opening one carries no address at all.
41
+ def target_view_action_name(action)
42
+ return action.endpoint_action_name if action.endpoint_action_name.present?
43
+
44
+ definition.normalized_views.find { |view| view.name == action.view_name }&.endpoint_action_name
25
45
  end
26
46
 
27
47
  # ***********************************************************************
@@ -53,7 +73,7 @@ module Habarico
53
73
  # over the record it already has. One that names its own target does: the endpoint is where
54
74
  # the target resource and record are written down.
55
75
  return nil if Habarico::ActionHelper.is_standard_action?(action) && !Habarico::ActionHelper.has_explicit_target?(action)
56
- view_action_endpoint_parameters_generator.generate(action, context)
76
+ view_action_endpoint_reference(action, context)
57
77
  end
58
78
 
59
79
  def generate_keep_linked_parent(action, _context)
@@ -44,7 +44,7 @@ module Habarico
44
44
  required: generate_required(model, field_name, attr_name, variant),
45
45
  sortable: generate_sortable(model, field_name, attr_name, variant),
46
46
  sort_by: generate_sort_by(model, field_name, attr_name, variant),
47
- reference: generate_reference(model, field_name, attr_name, variant),
47
+ reference: Habarico::ResourceReference.build(generate_reference(model, field_name, attr_name, variant)),
48
48
  enum: generate_enum(model, field_name, attr_name, variant),
49
49
  items: generate_items(model, field_name, attr_name, variant, subfields),
50
50
  properties: generate_properties(model, field_name, attr_name, variant, subfields),
@@ -315,7 +315,7 @@ module Habarico
315
315
  label: label,
316
316
  type: 'string',
317
317
  format: 'relation',
318
- reference: reference_class.const_get(:REFERENCE),
318
+ reference: Habarico::ResourceReference.build(reference_class.const_get(:REFERENCE)),
319
319
  )
320
320
  end
321
321
  end
@@ -14,7 +14,7 @@ module Habarico
14
14
  format: generate_format(model, filter_name, filter_param),
15
15
  label: generate_label(model, filter_name, filter_param),
16
16
  help_text: generate_help_text(model, filter_name, filter_param),
17
- reference: generate_reference(model, filter_name, filter_param),
17
+ reference: Habarico::ResourceReference.build(generate_reference(model, filter_name, filter_param)),
18
18
  enum: generate_enum(model, filter_name, filter_param),
19
19
  items: generate_items(model, filter_name, filter_param),
20
20
  properties: generate_properties(model, filter_name, filter_param),
@@ -7,10 +7,10 @@ module Habarico
7
7
  raise Eluvia::Errors::StandardError.new('Argument `menu_item` must be of :view type') unless menu_item.type.to_sym == :view
8
8
 
9
9
  # Create model
10
- Habarico::ViewActionEndpointParameters.new(
11
- reference: generate_reference(menu_item),
10
+ Habarico::ResourceReference.build(
11
+ generate_reference(menu_item) || resource_name,
12
12
  action_name: generate_action_name(menu_item),
13
- record_pk: generate_record_pk(menu_item)
13
+ record_pk: generate_record_pk(menu_item),
14
14
  )
15
15
  end
16
16
 
@@ -1,10 +1,15 @@
1
1
  module Habarico
2
2
  class ActionDescriptorGenerator < AbstractGenerator
3
3
 
4
- def generate(action_reference, context)
4
+ # `list_source` is the address of the resource a surrounding box lists, which only that box
5
+ # knows - it is what `ResourceReference::LIST` resolves to.
6
+ def generate(action_reference, context, list_source: nil)
5
7
  # Fallback to a simple ActionReference object if a string or symbol is provided
6
8
  action_reference = Habarico::ActionReference.new(name: action_reference.to_s) if action_reference.is_a?(String) || action_reference.is_a?(Symbol)
7
9
 
10
+ # A group of defaults names no action, so none of the checks below apply to it
11
+ return generate_default_actions(action_reference, list_source) if action_reference.is_a?(Habarico::DefaultActionsReference)
12
+
8
13
  # Validate arguments
9
14
  raise Eluvia::Errors::StandardError.new('Argument `action_reference` must be a Habarico::ActionReference') unless action_reference.is_a?(Habarico::ActionReference)
10
15
 
@@ -25,6 +30,8 @@ module Habarico
25
30
  icon: generate_icon(action_reference, context),
26
31
  color: generate_color(action_reference, context),
27
32
  action: generate_action(action_reference, context),
33
+ reference: resolve_reference(action_reference.reference, context, list_source),
34
+ confirm_dialog: generate_confirm_dialog(action_reference, context),
28
35
  view: generate_view(action_reference, context),
29
36
  menu_items: generate_menu_items(action_reference, context),
30
37
  )
@@ -32,6 +39,34 @@ module Habarico
32
39
 
33
40
  protected
34
41
 
42
+ def confirm_dialog_properties_generator
43
+ @confirm_dialog_properties_generator ||= definition.confirm_dialog_properties_generator.new(definition, resource_path)
44
+ end
45
+
46
+ def generate_default_actions(action_reference, list_source)
47
+ Habarico::DefaultActionsDescriptor.new(
48
+ reference: resolve_reference(action_reference.reference, nil, list_source),
49
+ scope: action_reference.scope,
50
+ )
51
+ end
52
+
53
+ # The address a descriptor binds to: a name as written, or what a sentinel stands for.
54
+ #
55
+ # `CURRENT` is the resource the schema is being generated for, narrowed to the record when the
56
+ # request has one. `LIST` is the collection the surrounding box lists, which the box passes in.
57
+ def resolve_reference(reference, context, list_source)
58
+ case reference
59
+ when nil then nil
60
+ when Habarico::ResourceReference then reference
61
+ when Habarico::ResourceReference::CURRENT
62
+ Habarico::ResourceReference.build(resource_name, record_pk: context&.record_pk)
63
+ when Habarico::ResourceReference::LIST
64
+ raise Eluvia::Errors::StandardError.new('`ResourceReference::LIST` is stamped by the box that lists a resource, so this reference is not inside a list or gallery box') if list_source.nil?
65
+ list_source
66
+ else Habarico::ResourceReference.build(reference)
67
+ end
68
+ end
69
+
35
70
  def action_schema_generator
36
71
  @action_schema_generator ||= definition.action_schema_generator.new(definition, resource_path)
37
72
  end
@@ -56,6 +91,12 @@ module Habarico
56
91
  action_reference.color
57
92
  end
58
93
 
94
+ def generate_confirm_dialog(action_reference, context)
95
+ return nil if action_reference.confirm_dialog_title.blank? && action_reference.confirm_dialog_message.blank?
96
+ raise Eluvia::Errors::StandardError.new('A confirm dialog needs both a title and a message, or neither') if action_reference.confirm_dialog_title.blank? || action_reference.confirm_dialog_message.blank?
97
+ confirm_dialog_properties_generator.generate(action_reference, context)
98
+ end
99
+
59
100
  def generate_action(action_reference, context)
60
101
  if action_reference.action.present?
61
102
  return action_schema_generator.generate(action_reference.action, context)
@@ -124,18 +124,36 @@ module Habarico
124
124
 
125
125
  # Box-level toolbar (box header / card header area), relative to the box root. Emitted for all box types
126
126
  # including 'list' - for a list box the table's own toolbar is carried separately by 'list.toolbar_actions'.
127
+ # A descriptor here is rendered in the parent's context, so one meant for the resource a list or
128
+ # gallery box points at has to name it - `ResourceReference::LIST` is how an author says that
129
+ # without repeating the name the box already carries.
127
130
  def generate_toolbar_actions(_view, box, _index, context)
128
131
  return nil if box.toolbar_actions.nil?
132
+ list_source = listed_resource_address(box, context)
129
133
  box.toolbar_actions.map do |action|
130
- action_descriptor_generator.generate(action, context)
134
+ action_descriptor_generator.generate(action, context, list_source: list_source)
131
135
  end.compact
132
136
  end
133
137
 
134
- # Box-level bottom actions (rendered below the box fields), relative to the box root.
138
+ # What `ResourceReference::LIST` stands for inside this box: the collection it lists, with the
139
+ # query its own schema request carries. Only a list or gallery box has one.
140
+ LISTING_BOX_TYPES = %i[list gallery].freeze
141
+
142
+ def listed_resource_address(box, context)
143
+ return nil unless LISTING_BOX_TYPES.include?(box.type&.to_sym)
144
+ return nil if box.reference.blank?
145
+
146
+ link = Habarico::BoxHelper.parent_link_params(box, context, resource_name)
147
+ Habarico::ResourceReference.build(box.reference, params: (box.schema_params || {}).merge(link || {}).presence)
148
+ end
149
+
150
+ # Box-level bottom actions (rendered below the box fields), relative to the box root - the same
151
+ # parent context as the header, so a descriptor here resolves `ResourceReference::LIST` too.
135
152
  def generate_bottom_actions(_view, box, _index, context)
136
153
  return nil if box.bottom_actions.nil?
154
+ list_source = listed_resource_address(box, context)
137
155
  box.bottom_actions.map do |action|
138
- action_descriptor_generator.generate(action, context)
156
+ action_descriptor_generator.generate(action, context, list_source: list_source)
139
157
  end.compact
140
158
  end
141
159
 
@@ -33,13 +33,17 @@ module Habarico
33
33
  box_schema_generator.generate(view, view.box, 0, context)
34
34
  end
35
35
 
36
+ # Everything the resource supports, until the view says otherwise. Only a slot the author never
37
+ # mentioned carries the default: `nil` and `[]` are both decisions to render nothing.
36
38
  def generate_toolbar_actions(view, context)
37
- unless view.toolbar_actions.nil?
38
- return view.toolbar_actions.map do |action|
39
- action_descriptor_generator.generate(action, context)
40
- end.compact
39
+ unless view.assigned?(:toolbar_actions)
40
+ return [action_descriptor_generator.generate(Habarico::DefaultActionsReference.new, context)].compact
41
41
  end
42
- nil
42
+ return nil if view.toolbar_actions.nil?
43
+
44
+ view.toolbar_actions.map do |action|
45
+ action_descriptor_generator.generate(action, context)
46
+ end.compact
43
47
  end
44
48
 
45
49
  def generate_bottom_actions(view, context)