avo 4.2.9 → 4.2.10

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.
@@ -118,8 +118,7 @@
118
118
 
119
119
  /* Background of the main content panel. Dedicated knob so installs can recolor
120
120
  the content surface independently of the primary surface token. Defaults to
121
- --color-primary; resolves per-scheme since --color-primary is redefined in
122
- .dark. */
121
+ --color-primary. */
123
122
  --color-main-content-background: var(--color-primary);
124
123
 
125
124
  /* Border between the sidebar and the main content panel. Dedicated knob so
@@ -185,7 +184,11 @@
185
184
  /* foundations - Dark Mode */
186
185
  --color-background: var(--color-avo-neutral-900);
187
186
  --color-foreground: var(--color-avo-neutral-50);
188
- --color-primary: var(--color-avo-neutral-950);
187
+ --color-primary: color-mix(
188
+ in oklab,
189
+ var(--color-avo-neutral-950),
190
+ var(--color-background) 20%
191
+ );
189
192
  --color-secondary: var(--color-avo-neutral-800);
190
193
  --color-tertiary: var(--color-avo-neutral-700);
191
194
  --color-quaternary: var(--color-avo-neutral-600);
@@ -219,130 +222,157 @@
219
222
  `.accent-theme-orange` still wins over a configured brand color. */
220
223
  @layer components {
221
224
  .neutral-theme-slate {
222
- --color-avo-neutral-25: oklch(98.51% 0 89.88);
223
- --color-avo-neutral-50: var(--color-slate-50);
224
- --color-avo-neutral-100: var(--color-slate-100);
225
- --color-avo-neutral-200: var(--color-slate-200);
226
- --color-avo-neutral-300: var(--color-slate-300);
227
- --color-avo-neutral-400: var(--color-slate-400);
228
- --color-avo-neutral-500: var(--color-slate-500);
229
- --color-avo-neutral-600: var(--color-slate-600);
230
- --color-avo-neutral-700: var(--color-slate-700);
231
- --color-avo-neutral-800: var(--color-slate-800);
232
- --color-avo-neutral-900: var(--color-slate-900);
233
- --color-avo-neutral-950: var(--color-slate-950);
225
+ --avo-neutral-source-25: var(--color-slate-50);
226
+ --avo-neutral-source-50: var(--color-slate-50);
227
+ --avo-neutral-source-100: var(--color-slate-100);
228
+ --avo-neutral-source-200: var(--color-slate-200);
229
+ --avo-neutral-source-300: var(--color-slate-300);
230
+ --avo-neutral-source-400: var(--color-slate-400);
231
+ --avo-neutral-source-500: var(--color-slate-500);
232
+ --avo-neutral-source-600: var(--color-slate-600);
233
+ --avo-neutral-source-700: var(--color-slate-700);
234
+ --avo-neutral-source-800: var(--color-slate-800);
235
+ --avo-neutral-source-900: var(--color-slate-900);
236
+ --avo-neutral-source-950: var(--color-slate-950);
234
237
  }
235
238
  .neutral-theme-stone {
236
- --color-avo-neutral-25: oklch(98.51% 0 89.88);
237
- --color-avo-neutral-50: var(--color-stone-50);
238
- --color-avo-neutral-100: var(--color-stone-100);
239
- --color-avo-neutral-200: var(--color-stone-200);
240
- --color-avo-neutral-300: var(--color-stone-300);
241
- --color-avo-neutral-400: var(--color-stone-400);
242
- --color-avo-neutral-500: var(--color-stone-500);
243
- --color-avo-neutral-600: var(--color-stone-600);
244
- --color-avo-neutral-700: var(--color-stone-700);
245
- --color-avo-neutral-800: var(--color-stone-800);
246
- --color-avo-neutral-900: var(--color-stone-900);
247
- --color-avo-neutral-950: var(--color-stone-950);
239
+ --avo-neutral-source-25: var(--color-stone-50);
240
+ --avo-neutral-source-50: var(--color-stone-50);
241
+ --avo-neutral-source-100: var(--color-stone-100);
242
+ --avo-neutral-source-200: var(--color-stone-200);
243
+ --avo-neutral-source-300: var(--color-stone-300);
244
+ --avo-neutral-source-400: var(--color-stone-400);
245
+ --avo-neutral-source-500: var(--color-stone-500);
246
+ --avo-neutral-source-600: var(--color-stone-600);
247
+ --avo-neutral-source-700: var(--color-stone-700);
248
+ --avo-neutral-source-800: var(--color-stone-800);
249
+ --avo-neutral-source-900: var(--color-stone-900);
250
+ --avo-neutral-source-950: var(--color-stone-950);
248
251
  }
249
252
  .neutral-theme-taupe {
250
- --color-avo-neutral-25: oklch(98.5% 0.004 70);
251
- --color-avo-neutral-50: var(--color-taupe-50);
252
- --color-avo-neutral-100: var(--color-taupe-100);
253
- --color-avo-neutral-200: var(--color-taupe-200);
254
- --color-avo-neutral-300: var(--color-taupe-300);
255
- --color-avo-neutral-400: var(--color-taupe-400);
256
- --color-avo-neutral-500: var(--color-taupe-500);
257
- --color-avo-neutral-600: var(--color-taupe-600);
258
- --color-avo-neutral-700: var(--color-taupe-700);
259
- --color-avo-neutral-800: var(--color-taupe-800);
260
- --color-avo-neutral-900: var(--color-taupe-900);
261
- --color-avo-neutral-950: var(--color-taupe-950);
253
+ --avo-neutral-source-25: var(--color-taupe-50);
254
+ --avo-neutral-source-50: var(--color-taupe-50);
255
+ --avo-neutral-source-100: var(--color-taupe-100);
256
+ --avo-neutral-source-200: var(--color-taupe-200);
257
+ --avo-neutral-source-300: var(--color-taupe-300);
258
+ --avo-neutral-source-400: var(--color-taupe-400);
259
+ --avo-neutral-source-500: var(--color-taupe-500);
260
+ --avo-neutral-source-600: var(--color-taupe-600);
261
+ --avo-neutral-source-700: var(--color-taupe-700);
262
+ --avo-neutral-source-800: var(--color-taupe-800);
263
+ --avo-neutral-source-900: var(--color-taupe-900);
264
+ --avo-neutral-source-950: var(--color-taupe-950);
262
265
  }
263
266
  .neutral-theme-mauve {
264
- --color-avo-neutral-25: oklch(98.5% 0.005 310);
265
- --color-avo-neutral-50: var(--color-mauve-50);
266
- --color-avo-neutral-100: var(--color-mauve-100);
267
- --color-avo-neutral-200: var(--color-mauve-200);
268
- --color-avo-neutral-300: var(--color-mauve-300);
269
- --color-avo-neutral-400: var(--color-mauve-400);
270
- --color-avo-neutral-500: var(--color-mauve-500);
271
- --color-avo-neutral-600: var(--color-mauve-600);
272
- --color-avo-neutral-700: var(--color-mauve-700);
273
- --color-avo-neutral-800: var(--color-mauve-800);
274
- --color-avo-neutral-900: var(--color-mauve-900);
275
- --color-avo-neutral-950: var(--color-mauve-950);
267
+ --avo-neutral-source-25: var(--color-mauve-50);
268
+ --avo-neutral-source-50: var(--color-mauve-50);
269
+ --avo-neutral-source-100: var(--color-mauve-100);
270
+ --avo-neutral-source-200: var(--color-mauve-200);
271
+ --avo-neutral-source-300: var(--color-mauve-300);
272
+ --avo-neutral-source-400: var(--color-mauve-400);
273
+ --avo-neutral-source-500: var(--color-mauve-500);
274
+ --avo-neutral-source-600: var(--color-mauve-600);
275
+ --avo-neutral-source-700: var(--color-mauve-700);
276
+ --avo-neutral-source-800: var(--color-mauve-800);
277
+ --avo-neutral-source-900: var(--color-mauve-900);
278
+ --avo-neutral-source-950: var(--color-mauve-950);
276
279
  }
277
280
  .neutral-theme-mist {
278
- --color-avo-neutral-25: oklch(98.5% 0.004 240);
279
- --color-avo-neutral-50: var(--color-mist-50);
280
- --color-avo-neutral-100: var(--color-mist-100);
281
- --color-avo-neutral-200: var(--color-mist-200);
282
- --color-avo-neutral-300: var(--color-mist-300);
283
- --color-avo-neutral-400: var(--color-mist-400);
284
- --color-avo-neutral-500: var(--color-mist-500);
285
- --color-avo-neutral-600: var(--color-mist-600);
286
- --color-avo-neutral-700: var(--color-mist-700);
287
- --color-avo-neutral-800: var(--color-mist-800);
288
- --color-avo-neutral-900: var(--color-mist-900);
289
- --color-avo-neutral-950: var(--color-mist-950);
281
+ --avo-neutral-source-25: var(--color-mist-50);
282
+ --avo-neutral-source-50: var(--color-mist-50);
283
+ --avo-neutral-source-100: var(--color-mist-100);
284
+ --avo-neutral-source-200: var(--color-mist-200);
285
+ --avo-neutral-source-300: var(--color-mist-300);
286
+ --avo-neutral-source-400: var(--color-mist-400);
287
+ --avo-neutral-source-500: var(--color-mist-500);
288
+ --avo-neutral-source-600: var(--color-mist-600);
289
+ --avo-neutral-source-700: var(--color-mist-700);
290
+ --avo-neutral-source-800: var(--color-mist-800);
291
+ --avo-neutral-source-900: var(--color-mist-900);
292
+ --avo-neutral-source-950: var(--color-mist-950);
290
293
  }
291
294
  .neutral-theme-gray {
292
- --color-avo-neutral-25: oklch(98.51% 0 89.88);
293
- --color-avo-neutral-50: var(--color-gray-50);
294
- --color-avo-neutral-100: var(--color-gray-100);
295
- --color-avo-neutral-200: var(--color-gray-200);
296
- --color-avo-neutral-300: var(--color-gray-300);
297
- --color-avo-neutral-400: var(--color-gray-400);
298
- --color-avo-neutral-500: var(--color-gray-500);
299
- --color-avo-neutral-600: var(--color-gray-600);
300
- --color-avo-neutral-700: var(--color-gray-700);
301
- --color-avo-neutral-800: var(--color-gray-800);
302
- --color-avo-neutral-900: var(--color-gray-900);
303
- --color-avo-neutral-950: var(--color-gray-950);
295
+ --avo-neutral-source-25: var(--color-gray-50);
296
+ --avo-neutral-source-50: var(--color-gray-50);
297
+ --avo-neutral-source-100: var(--color-gray-100);
298
+ --avo-neutral-source-200: var(--color-gray-200);
299
+ --avo-neutral-source-300: var(--color-gray-300);
300
+ --avo-neutral-source-400: var(--color-gray-400);
301
+ --avo-neutral-source-500: var(--color-gray-500);
302
+ --avo-neutral-source-600: var(--color-gray-600);
303
+ --avo-neutral-source-700: var(--color-gray-700);
304
+ --avo-neutral-source-800: var(--color-gray-800);
305
+ --avo-neutral-source-900: var(--color-gray-900);
306
+ --avo-neutral-source-950: var(--color-gray-950);
304
307
  }
305
308
  .neutral-theme-zinc {
306
- --color-avo-neutral-25: oklch(98.51% 0 89.88);
307
- --color-avo-neutral-50: var(--color-zinc-50);
308
- --color-avo-neutral-100: var(--color-zinc-100);
309
- --color-avo-neutral-200: var(--color-zinc-200);
310
- --color-avo-neutral-300: var(--color-zinc-300);
311
- --color-avo-neutral-400: var(--color-zinc-400);
312
- --color-avo-neutral-500: var(--color-zinc-500);
313
- --color-avo-neutral-600: var(--color-zinc-600);
314
- --color-avo-neutral-700: var(--color-zinc-700);
315
- --color-avo-neutral-800: var(--color-zinc-800);
316
- --color-avo-neutral-900: var(--color-zinc-900);
317
- --color-avo-neutral-950: var(--color-zinc-950);
309
+ --avo-neutral-source-25: var(--color-zinc-50);
310
+ --avo-neutral-source-50: var(--color-zinc-50);
311
+ --avo-neutral-source-100: var(--color-zinc-100);
312
+ --avo-neutral-source-200: var(--color-zinc-200);
313
+ --avo-neutral-source-300: var(--color-zinc-300);
314
+ --avo-neutral-source-400: var(--color-zinc-400);
315
+ --avo-neutral-source-500: var(--color-zinc-500);
316
+ --avo-neutral-source-600: var(--color-zinc-600);
317
+ --avo-neutral-source-700: var(--color-zinc-700);
318
+ --avo-neutral-source-800: var(--color-zinc-800);
319
+ --avo-neutral-source-900: var(--color-zinc-900);
320
+ --avo-neutral-source-950: var(--color-zinc-950);
318
321
  }
319
322
  .neutral-theme-neutral {
320
- --color-avo-neutral-25: oklch(98.51% 0 89.88);
321
- --color-avo-neutral-50: var(--color-neutral-50);
322
- --color-avo-neutral-100: var(--color-neutral-100);
323
- --color-avo-neutral-200: var(--color-neutral-200);
324
- --color-avo-neutral-300: var(--color-neutral-300);
325
- --color-avo-neutral-400: var(--color-neutral-400);
326
- --color-avo-neutral-500: var(--color-neutral-500);
327
- --color-avo-neutral-600: var(--color-neutral-600);
328
- --color-avo-neutral-700: var(--color-neutral-700);
329
- --color-avo-neutral-800: var(--color-neutral-800);
330
- --color-avo-neutral-900: var(--color-neutral-900);
331
- --color-avo-neutral-950: var(--color-neutral-950);
323
+ --avo-neutral-source-25: var(--color-neutral-50);
324
+ --avo-neutral-source-50: var(--color-neutral-50);
325
+ --avo-neutral-source-100: var(--color-neutral-100);
326
+ --avo-neutral-source-200: var(--color-neutral-200);
327
+ --avo-neutral-source-300: var(--color-neutral-300);
328
+ --avo-neutral-source-400: var(--color-neutral-400);
329
+ --avo-neutral-source-500: var(--color-neutral-500);
330
+ --avo-neutral-source-600: var(--color-neutral-600);
331
+ --avo-neutral-source-700: var(--color-neutral-700);
332
+ --avo-neutral-source-800: var(--color-neutral-800);
333
+ --avo-neutral-source-900: var(--color-neutral-900);
334
+ --avo-neutral-source-950: var(--color-neutral-950);
332
335
  }
333
336
  .neutral-theme-olive {
334
- --color-avo-neutral-25: oklch(98.5% 0.005 140);
335
- --color-avo-neutral-50: var(--color-olive-50);
336
- --color-avo-neutral-100: var(--color-olive-100);
337
- --color-avo-neutral-200: var(--color-olive-200);
338
- --color-avo-neutral-300: var(--color-olive-300);
339
- --color-avo-neutral-400: var(--color-olive-400);
340
- --color-avo-neutral-500: var(--color-olive-500);
341
- --color-avo-neutral-600: var(--color-olive-600);
342
- --color-avo-neutral-700: var(--color-olive-700);
343
- --color-avo-neutral-800: var(--color-olive-800);
344
- --color-avo-neutral-900: var(--color-olive-900);
345
- --color-avo-neutral-950: var(--color-olive-950);
337
+ --avo-neutral-source-25: var(--color-olive-50);
338
+ --avo-neutral-source-50: var(--color-olive-50);
339
+ --avo-neutral-source-100: var(--color-olive-100);
340
+ --avo-neutral-source-200: var(--color-olive-200);
341
+ --avo-neutral-source-300: var(--color-olive-300);
342
+ --avo-neutral-source-400: var(--color-olive-400);
343
+ --avo-neutral-source-500: var(--color-olive-500);
344
+ --avo-neutral-source-600: var(--color-olive-600);
345
+ --avo-neutral-source-700: var(--color-olive-700);
346
+ --avo-neutral-source-800: var(--color-olive-800);
347
+ --avo-neutral-source-900: var(--color-olive-900);
348
+ --avo-neutral-source-950: var(--color-olive-950);
349
+ }
350
+
351
+ /* Keep every selectable neutral theme on Brand's lightness curve while
352
+ preserving the source palette's chroma and hue at each shade. */
353
+ :is(
354
+ .neutral-theme-slate,
355
+ .neutral-theme-stone,
356
+ .neutral-theme-gray,
357
+ .neutral-theme-zinc,
358
+ .neutral-theme-neutral,
359
+ .neutral-theme-taupe,
360
+ .neutral-theme-mauve,
361
+ .neutral-theme-mist,
362
+ .neutral-theme-olive
363
+ ) {
364
+ --color-avo-neutral-25: oklch(from var(--avo-neutral-source-25) 99.71% c h);
365
+ --color-avo-neutral-50: oklch(from var(--avo-neutral-source-50) 98.51% c h);
366
+ --color-avo-neutral-100: oklch(from var(--avo-neutral-source-100) 97.31% c h);
367
+ --color-avo-neutral-200: oklch(from var(--avo-neutral-source-200) 92.8% c h);
368
+ --color-avo-neutral-300: oklch(from var(--avo-neutral-source-300) 75.72% c h);
369
+ --color-avo-neutral-400: oklch(from var(--avo-neutral-source-400) 62.68% c h);
370
+ --color-avo-neutral-500: oklch(from var(--avo-neutral-source-500) 53.48% c h);
371
+ --color-avo-neutral-600: oklch(from var(--avo-neutral-source-600) 47.84% c h);
372
+ --color-avo-neutral-700: oklch(from var(--avo-neutral-source-700) 42.76% c h);
373
+ --color-avo-neutral-800: oklch(from var(--avo-neutral-source-800) 39.04% c h);
374
+ --color-avo-neutral-900: oklch(from var(--avo-neutral-source-900) 27.68% c h);
375
+ --color-avo-neutral-950: oklch(from var(--avo-neutral-source-950) 20.46% c h);
346
376
  }
347
377
 
348
378
  .accent-theme-red {
@@ -10,8 +10,10 @@ class Avo::Fields::TrixField::EditComponent < Avo::Fields::EditComponent
10
10
 
11
11
  super
12
12
 
13
+ # A nested form (avo-nested) renders one editor per record of the same resource, so the form index
14
+ # keeps their ids apart. The new-record template's index is NEW_RECORD, replaced on each clone.
13
15
  @input_id = if @resource_name.present?
14
- "#{@field.type}_#{@resource_name}_#{@field.id}"
16
+ [@field.type, @resource_name, form&.index, @field.id].compact.join("_")
15
17
  elsif form.present?
16
18
  "#{@field.type}_#{form.index}_#{@field.id}"
17
19
  end
@@ -16,8 +16,7 @@ class Avo::Index::ResourceControlsComponent < Avo::ResourceComponent
16
16
  end
17
17
 
18
18
  def can_edit?
19
- # Disable edit for ArrayResources
20
- return false if @resource.resource_type_array?
19
+ return false if @resource.read_only_array?
21
20
 
22
21
  return authorize_association_for(:edit) if @reflection.present?
23
22
 
@@ -165,6 +164,9 @@ class Avo::Index::ResourceControlsComponent < Avo::ResourceComponent
165
164
  if params[:turbo_frame]
166
165
  hidden[:turbo_frame] = params[:turbo_frame]
167
166
  hidden[:referrer] = referrer_path
167
+ else
168
+ # Keep the filters, sorting and page after a destroy redirects back here.
169
+ hidden[:referrer] = request.fullpath
168
170
  end
169
171
 
170
172
  hidden.compact
@@ -41,8 +41,7 @@ class Avo::ResourceComponent < Avo::BaseComponent
41
41
  end
42
42
 
43
43
  def can_see_the_edit_button?
44
- # Disable edit for ArrayResources
45
- return false if @resource.resource_type_array?
44
+ return false if @resource.read_only_array?
46
45
 
47
46
  return authorize_association_for(:edit) if @reflection.present?
48
47
 
@@ -50,8 +49,7 @@ class Avo::ResourceComponent < Avo::BaseComponent
50
49
  end
51
50
 
52
51
  def can_see_the_destroy_button?
53
- # Disable destroy for ArrayResources
54
- return false if @resource.resource_type_array?
52
+ return false if @resource.read_only_array?
55
53
 
56
54
  @resource.authorization.authorize_action(:destroy, raise_exception: false)
57
55
  end
@@ -1,4 +1,4 @@
1
- <div class="resource-sidebar-component space-y-4"
1
+ <div class="resource-sidebar-component space-y-4 wrap-anywhere"
2
2
  data-component-name="<%= self.class.to_s.underscore %>"
3
3
  data-component-index="<%= @index %>"
4
4
  data-resource-name="<%= @resource.class.to_s %>"
@@ -32,8 +32,7 @@ class Avo::Views::ResourceIndexComponent < Avo::ResourceComponent
32
32
  # The Create button is dependent on the new? policy method.
33
33
  # The create? should be called only when the user clicks the Save button so the developers gets access to the params from the form.
34
34
  def can_see_the_create_button?
35
- # Disable creation for ArrayResources
36
- return false if @resource.resource_type_array?
35
+ return false if @resource.read_only_array?
37
36
 
38
37
  return authorize_association_for(:create) if @reflection.present?
39
38
 
@@ -3,5 +3,13 @@ module Avo
3
3
  def set_query
4
4
  @query ||= @resource.fetch_records
5
5
  end
6
+
7
+ private
8
+
9
+ # Fetching builds the model class with an accessor per attribute, so a new record can be filled.
10
+ def set_record_to_fill
11
+ @resource.fetch_records
12
+ super
13
+ end
6
14
  end
7
15
  end
@@ -355,8 +355,13 @@ module Avo
355
355
  def reload_frame_turbo_streams
356
356
  turbo_streams = super
357
357
 
358
- # We want to close the modal if the user wants to add just one record
359
- turbo_streams << turbo_stream.avo_close_modal if params[:button] != "attach_another"
358
+ # Close the modal if the user wants to add just one record, otherwise
359
+ # reload it so the options reflect the record that was just attached
360
+ turbo_streams << if params[:button] == "attach_another"
361
+ turbo_stream.turbo_frame_reload(Avo::MODAL_FRAME_ID)
362
+ else
363
+ turbo_stream.avo_close_modal
364
+ end
360
365
 
361
366
  turbo_streams
362
367
  end
@@ -26,6 +26,8 @@ export default class extends Controller {
26
26
 
27
27
  searchDebounce = 500
28
28
 
29
+ searchCount = 0
30
+
29
31
  debouncedFetch = debouncePromise(fetch, this.searchDebounce)
30
32
 
31
33
  get suggestionsAreObjects() {
@@ -107,6 +109,8 @@ export default class extends Controller {
107
109
  q: query,
108
110
  })
109
111
 
112
+ const search = ++that.searchCount
113
+
110
114
  // reset current whitelist
111
115
  that.tagify.whitelist = null
112
116
  // show the loader animation
@@ -115,6 +119,9 @@ export default class extends Controller {
115
119
  // get new whitelist from a request
116
120
  that.fetchResults(uri.toString())
117
121
  .then((result) => {
122
+ // The user kept typing while this search was in flight; the newer search fills the dropdown
123
+ if (search !== that.searchCount) return
124
+
118
125
  that.tagify.settings.whitelist = result // add already-existing tags to the new whitelist array
119
126
 
120
127
  that.tagify
@@ -11,7 +11,7 @@
11
11
  <meta name="avo:resources-index-path" content="<%= resources_index_path %>">
12
12
  <% end %>
13
13
  <%# Same for "c" → new, on show only, and only when the create button would render on the index. %>
14
- <% if action_name == "show" && !@resource.resource_type_array? && @resource.authorization.authorize_action(:new, raise_exception: false) %>
14
+ <% if action_name == "show" && !@resource.read_only_array? && @resource.authorization.authorize_action(:new, raise_exception: false) %>
15
15
  <% if (resources_new_path = (new_resource_path(resource: @resource) rescue nil)) %>
16
16
  <meta name="avo:resources-new-path" content="<%= resources_new_path %>">
17
17
  <% end %>
@@ -93,7 +93,8 @@ module Avo
93
93
  :string
94
94
  end
95
95
 
96
- merge_values_as(as: values_type, values: values)
96
+ # A blank data hash still needs to be a Hash so the stimulus targets get merged in.
97
+ merge_values_as(as: values_type, values: values) || default_attribute_value(name)
97
98
  end
98
99
 
99
100
  def get_html_from_hash(name = nil, element:, hash:, view:)
@@ -79,7 +79,8 @@ module Avo
79
79
  def whitelist_items
80
80
  return suggestions.to_json if enforce_suggestions
81
81
 
82
- (suggestions + field_value).uniq.to_json
82
+ # Tags and suggestions can be strings or {value:} objects; Tagify compares them by their stringified value
83
+ (suggestions + field_value).as_json.uniq { |item| (item.is_a?(Hash) ? item["value"] : item).to_s }.to_json
83
84
  end
84
85
 
85
86
  def suggestions
@@ -13,6 +13,10 @@ module Avo
13
13
  type: :array
14
14
  }
15
15
 
16
+ # Shows the create, edit and delete controls. The records have no save! or destroy!,
17
+ # so the resource's controller overrides save_record_action and destroy_record_action.
18
+ class_attribute :writable, default: false
19
+
16
20
  class << self
17
21
  # Each array resource keeps its own model class. It starts as a placeholder class and
18
22
  # fetch_records replaces it with the class of the records it builds or finds.
@@ -131,6 +135,11 @@ module Avo
131
135
 
132
136
  def resource_type_array? = true
133
137
 
138
+ def read_only_array? = !writable
139
+
140
+ # Base derives it from ActiveRecord's base_class.
141
+ def form_scope = class_name.underscore
142
+
134
143
  def sort_by_param = nil
135
144
 
136
145
  def sorting_supported? = false
@@ -845,6 +845,8 @@ module Avo
845
845
 
846
846
  def resource_type_array? = false
847
847
 
848
+ def read_only_array? = false
849
+
848
850
  def sort_by_param
849
851
  available_columns = model_class.column_names
850
852
 
@@ -206,6 +206,31 @@ class Avo::Resources::Movie < Avo::Resources::ArrayResource
206
206
  end
207
207
  ```
208
208
 
209
+ Array resources are read-only by default. To add create, edit and delete, set `self.writable = true` and persist the changes in the resource's controller. Avo fills the record from the form, but array records have no `save!` or `destroy!`:
210
+
211
+ ```ruby
212
+ class Avo::Resources::Bookmark < Avo::Resources::ArrayResource
213
+ self.writable = true
214
+
215
+ def records = BookmarkStore.all # array of hashes with an :id
216
+ end
217
+
218
+ # app/controllers/avo/bookmarks_controller.rb
219
+ class Avo::BookmarksController < Avo::ArrayController
220
+ private
221
+
222
+ def save_record_action
223
+ @record.id = BookmarkStore.save(id: @record.id, title: @record.title)
224
+ end
225
+
226
+ def destroy_record_action
227
+ BookmarkStore.destroy(@record.id)
228
+ end
229
+ end
230
+ ```
231
+
232
+ The form's params are scoped under the resource name (`bookmark[title]`). The policy still decides who can create, edit and delete.
233
+
209
234
  Array resources are **Beta**: sorting is not supported, and the array is rebuilt on every request (cache inside `records` if it gets heavy). For external-API-backed data, prefer an [HTTP Resource](https://docs.avohq.io/4.0/http-resource.md).
210
235
 
211
236
  ### 7. Sidebar, shortcuts, external links, and multiple resources per model
data/lib/avo/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Avo
2
- VERSION = "4.2.9" unless const_defined?(:VERSION)
2
+ VERSION = "4.2.10" unless const_defined?(:VERSION)
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: avo
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.2.9
4
+ version: 4.2.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adrian Marin