plutonium 0.23.4 → 0.23.5

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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/plutonium.css +2 -2
  3. data/config/initializers/sqlite_json_alias.rb +1 -1
  4. data/docs/.vitepress/config.ts +60 -19
  5. data/docs/guide/cursor-rules.md +75 -0
  6. data/docs/guide/deep-dive/authorization.md +189 -0
  7. data/docs/guide/{getting-started → deep-dive}/resources.md +137 -0
  8. data/docs/guide/getting-started/{installation.md → 01-installation.md} +0 -105
  9. data/docs/guide/index.md +28 -0
  10. data/docs/guide/introduction/02-core-concepts.md +440 -0
  11. data/docs/guide/tutorial/01-project-setup.md +75 -0
  12. data/docs/guide/tutorial/02-creating-a-feature-package.md +45 -0
  13. data/docs/guide/tutorial/03-defining-resources.md +90 -0
  14. data/docs/guide/tutorial/04-creating-a-portal.md +101 -0
  15. data/docs/guide/tutorial/05-customizing-the-ui.md +128 -0
  16. data/docs/guide/tutorial/06-adding-custom-actions.md +101 -0
  17. data/docs/guide/tutorial/07-implementing-authorization.md +90 -0
  18. data/docs/index.md +24 -31
  19. data/docs/modules/action.md +190 -0
  20. data/docs/modules/authentication.md +236 -0
  21. data/docs/modules/configuration.md +599 -0
  22. data/docs/modules/controller.md +398 -0
  23. data/docs/modules/core.md +316 -0
  24. data/docs/modules/definition.md +876 -0
  25. data/docs/modules/display.md +759 -0
  26. data/docs/modules/form.md +605 -0
  27. data/docs/modules/generator.md +288 -0
  28. data/docs/modules/index.md +167 -0
  29. data/docs/modules/interaction.md +470 -0
  30. data/docs/modules/package.md +151 -0
  31. data/docs/modules/policy.md +176 -0
  32. data/docs/modules/portal.md +710 -0
  33. data/docs/modules/query.md +287 -0
  34. data/docs/modules/resource_record.md +618 -0
  35. data/docs/modules/routing.md +641 -0
  36. data/docs/modules/table.md +293 -0
  37. data/docs/modules/ui.md +631 -0
  38. data/docs/public/plutonium.mdc +667 -0
  39. data/lib/generators/pu/core/assets/assets_generator.rb +0 -5
  40. data/lib/plutonium/ui/display/resource.rb +7 -2
  41. data/lib/plutonium/ui/table/resource.rb +8 -3
  42. data/lib/plutonium/version.rb +1 -1
  43. metadata +36 -9
  44. data/docs/guide/getting-started/authorization.md +0 -296
  45. data/docs/guide/getting-started/core-concepts.md +0 -432
  46. data/docs/guide/getting-started/index.md +0 -21
  47. data/docs/guide/tutorial.md +0 -401
  48. /data/docs/guide/{what-is-plutonium.md → introduction/01-what-is-plutonium.md} +0 -0
@@ -0,0 +1,605 @@
1
+ ---
2
+ title: Form Module
3
+ ---
4
+
5
+ # Form Module
6
+
7
+ The Form module provides a comprehensive form building system for Plutonium applications. Built on top of `Phlexi::Form`, it offers enhanced input components, automatic field inference, secure associations, and modern UI interactions for creating rich, accessible forms.
8
+
9
+ ::: tip
10
+ The Form module is located in `lib/plutonium/ui/form/`.
11
+ :::
12
+
13
+ ## Overview
14
+
15
+ - **Enhanced Input Components**: Rich input types with JavaScript integration.
16
+ - **Secure Associations**: SGID-based association handling with authorization.
17
+ - **Type Inference**: Automatic component selection based on field types.
18
+ - **Resource Integration**: Seamless integration with resource definitions.
19
+ - **Modern UI**: File uploads, date pickers, rich text editors, and more.
20
+ - **Accessibility**: ARIA-compliant forms with keyboard navigation.
21
+
22
+ ## Core Components
23
+
24
+ ### Base Form (`lib/plutonium/ui/form/base.rb`)
25
+
26
+ This is the foundation that all Plutonium form components inherit from. It extends `Phlexi::Form::Base` with Plutonium's specific behaviors and custom input components.
27
+
28
+ ::: details Base Form Implementation
29
+ ```ruby
30
+ class Plutonium::UI::Form::Base < Phlexi::Form::Base
31
+ include Plutonium::UI::Component::Behaviour
32
+
33
+ # Enhanced builder with Plutonium-specific components
34
+ class Builder < Builder
35
+ include Plutonium::UI::Form::Options::InferredTypes
36
+
37
+ def easymde_tag(**options, &block)
38
+ create_component(Plutonium::UI::Form::Components::Easymde, :easymde, **options, &block)
39
+ end
40
+ alias_method :markdown_tag, :easymde_tag
41
+
42
+ def flatpickr_tag(**options, &block)
43
+ create_component(Components::Flatpickr, :flatpickr, **options, &block)
44
+ end
45
+
46
+ def uppy_tag(**options, &block)
47
+ create_component(Components::Uppy, :uppy, **options, &block)
48
+ end
49
+ alias_method :file_tag, :uppy_tag
50
+ alias_method :attachment_tag, :uppy_tag
51
+
52
+ def secure_association_tag(**attributes, &block)
53
+ create_component(Components::SecureAssociation, :association, **attributes, &block)
54
+ end
55
+
56
+ # Override default association methods to use secure versions
57
+ alias_method :belongs_to_tag, :secure_association_tag
58
+ alias_method :has_many_tag, :secure_association_tag
59
+ alias_method :has_one_tag, :secure_association_tag
60
+ end
61
+ end
62
+ ```
63
+ :::
64
+
65
+ ### Resource Form (`lib/plutonium/ui/form/resource.rb`)
66
+
67
+ This is a specialized form for resource objects that automatically renders fields based on the resource's definition, handling nested resources and actions gracefully.
68
+
69
+ ```ruby
70
+ class PostForm < Plutonium::UI::Form::Resource
71
+ def initialize(post, resource_definition:)
72
+ super(post, resource_definition: resource_definition)
73
+ end
74
+
75
+ def form_template
76
+ render_resource_fields # Render configured input fields
77
+ render_nested_resources # Render nested associations
78
+ render_actions # Render submit/cancel buttons
79
+ end
80
+ end
81
+ ```
82
+
83
+ ## Enhanced Input Components
84
+
85
+ ### Rich Text Editor (Easymde)
86
+
87
+ A client-side markdown editor with live preview, based on [EasyMDE](https://github.com/Ionaru/easy-markdown-editor).
88
+
89
+ ::: code-group
90
+ ```ruby [Basic Usage]
91
+ # Automatically used for :markdown fields
92
+ render field(:content).easymde_tag
93
+ ```
94
+
95
+ ```ruby [With Options]
96
+ render field(:description).easymde_tag(
97
+ toolbar: ["bold", "italic", "heading", "|", "quote"],
98
+ spellChecker: false,
99
+ autosave: { enabled: true, uniqueId: "post_content" }
100
+ )
101
+ ```
102
+ :::
103
+
104
+ ### Date/Time Picker (Flatpickr)
105
+
106
+ A powerful and lightweight date and time picker from [Flatpickr](https://flatpickr.js.org/).
107
+
108
+ ::: code-group
109
+ ```ruby [Date Picker]
110
+ # Automatically used for :date fields
111
+ render field(:published_at).flatpickr_tag
112
+ ```
113
+ ```ruby [Time Picker]
114
+ # Automatically used for :time fields
115
+ render field(:meeting_time).flatpickr_tag
116
+ ```
117
+ ```ruby [Datetime Picker]
118
+ # Automatically used for :datetime fields
119
+ render field(:deadline).flatpickr_tag
120
+ ```
121
+ ```ruby [With HTML Attributes]
122
+ render field(:event_date).flatpickr_tag(
123
+ class: "custom-date-picker",
124
+ placeholder: "Select date..."
125
+ )
126
+ ```
127
+ :::
128
+
129
+ ::: warning Flatpickr Configuration
130
+ The current implementation uses automatic configuration based on field type:
131
+ - **Date fields**: Basic date picker with `altInput: true`
132
+ - **Time fields**: Time picker with `enableTime: true, noCalendar: true`
133
+ - **Datetime fields**: Date and time picker with `enableTime: true`
134
+
135
+ Custom Flatpickr options (like `dateFormat`, `mode: "range"`) are not currently supported through tag attributes.
136
+ :::
137
+
138
+ ### File Upload (Uppy)
139
+
140
+ A sleek, modern file uploader powered by [Uppy](https://uppy.io/).
141
+
142
+ ::: code-group
143
+ ```ruby [Single File]
144
+ # Automatically used for :file or :attachment fields
145
+ render field(:avatar).uppy_tag
146
+ ```
147
+ ```ruby [Multiple Files]
148
+ render field(:documents).uppy_tag(multiple: true)
149
+ ```
150
+ ```ruby [With Restrictions]
151
+ render field(:gallery).uppy_tag(
152
+ multiple: true,
153
+ allowed_file_types: ['.jpg', '.jpeg', '.png'],
154
+ max_file_size: 5.megabytes
155
+ )
156
+ ```
157
+ ```ruby [Direct to Cloud]
158
+ render field(:videos).uppy_tag(
159
+ direct_upload: true, # For S3, etc.
160
+ max_total_size: 100.megabytes
161
+ )
162
+ ```
163
+ :::
164
+
165
+ ::: details Uppy Component Implementation
166
+ The Uppy component automatically handles rendering existing attachments and providing an interface to upload new ones.
167
+ ```ruby
168
+ class Plutonium::UI::Form::Components::Uppy
169
+ # Automatic features:
170
+ # - Drag and drop upload
171
+ # - Progress indicators
172
+ # - Image previews and thumbnails
173
+ # - File type and size validation
174
+ # - Direct-to-cloud upload support
175
+ # - Interactive preview and deletion of existing attachments
176
+
177
+ def view_template
178
+ div(class: "flex flex-col-reverse gap-2") do
179
+ render_existing_attachments
180
+ render_upload_interface
181
+ end
182
+ end
183
+
184
+ private
185
+
186
+ def render_existing_attachments
187
+ Array(field.value).each do |attachment|
188
+ render_attachment_preview(attachment)
189
+ end
190
+ end
191
+
192
+ def render_attachment_preview(attachment)
193
+ # Interactive preview with delete option
194
+ div(class: "attachment-preview", data: { controller: "attachment-preview" }) do
195
+ render_thumbnail(attachment)
196
+ render_filename(attachment)
197
+ render_delete_button
198
+ end
199
+ end
200
+ end
201
+ ```
202
+ :::
203
+
204
+ ### International Phone Input
205
+
206
+ A user-friendly phone number input with country code selection, using [intl-tel-input](https://github.com/jackocnr/intl-tel-input).
207
+
208
+ ::: code-group
209
+ ```ruby [Basic Usage]
210
+ # Automatically used for :tel fields
211
+ render field(:phone).int_tel_input_tag
212
+ ```
213
+ ```ruby [Phone Tag Alias]
214
+ # Alias for int_tel_input_tag
215
+ render field(:mobile).phone_tag
216
+ ```
217
+ ```ruby [With HTML Attributes]
218
+ render field(:contact_phone).int_tel_input_tag(
219
+ class: "custom-phone-input",
220
+ placeholder: "Enter phone number"
221
+ )
222
+ ```
223
+ :::
224
+
225
+ ::: warning Int Tel Input Configuration
226
+ The current implementation uses a fixed configuration:
227
+ - **Strict Mode**: Enabled for validation
228
+ - **Utils Loading**: Automatically loads validation utilities
229
+ - **Hidden Input**: Creates hidden field for form submission
230
+
231
+ Custom intl-tel-input options (like `onlyCountries`, `preferredCountries`) are not currently supported through tag attributes.
232
+ :::
233
+
234
+ ### Secure Association Inputs
235
+
236
+ Plutonium's association inputs are secure by default, using SGIDs to prevent parameter tampering and scoping options based on user authorization.
237
+
238
+ ::: code-group
239
+ ```ruby [Belongs To]
240
+ # Automatically used for belongs_to associations
241
+ render field(:author).belongs_to_tag
242
+ ```
243
+ ```ruby [Has Many]
244
+ # Automatically used for has_many associations
245
+ render field(:tags).has_many_tag
246
+ ```
247
+ ```ruby [With Custom Choices]
248
+ render field(:category).belongs_to_tag(
249
+ choices: Category.published.pluck(:name, :id)
250
+ )
251
+ ```
252
+ ```ruby [With Add Action]
253
+ render field(:publisher).belongs_to_tag(
254
+ add_action: new_publisher_path
255
+ )
256
+ ```
257
+ :::
258
+
259
+ ::: details Secure Association Implementation
260
+ ```ruby
261
+ class Plutonium::UI::Form::Components::SecureAssociation
262
+ # Automatic features:
263
+ # - SGID-based value encoding for security.
264
+ # - Authorization checks before showing options.
265
+ # - "Add new record" button with `return_to` handling.
266
+ # - Polymorphic association support.
267
+ # - Search and filtering (via SlimSelect).
268
+
269
+ def choices
270
+ collection = if @skip_authorization
271
+ choices_from_association(association_reflection.klass)
272
+ else
273
+ # Only show records user is authorized to see
274
+ authorized_resource_scope(
275
+ association_reflection.klass,
276
+ with: @scope_with,
277
+ context: @scope_context
278
+ )
279
+ end
280
+ # ...
281
+ end
282
+ end
283
+ ```
284
+ :::
285
+
286
+ ## Type Inference
287
+
288
+ ### Automatic Component Selection (`lib/plutonium/ui/form/options/inferred_types.rb`)
289
+
290
+ The system automatically selects appropriate input components:
291
+
292
+ ```ruby
293
+ # Automatic inference based on Active Record column types
294
+ render field(:title).input_tag # → input_tag (string)
295
+ render field(:content).easymde_tag # → easymde_tag (text/rich_text)
296
+ render field(:published_at).flatpickr_tag # → flatpickr_tag (datetime)
297
+ render field(:author).secure_association_tag # → secure_association_tag (belongs_to)
298
+ render field(:featured_image).uppy_tag # → uppy_tag (Active Storage)
299
+ render field(:category).slim_select_tag # → slim_select_tag (select)
300
+
301
+ # Manual override
302
+ render field(:title).input_tag(as: :string)
303
+ render field(:content).easymde_tag
304
+ render field(:published_at).flatpickr_tag
305
+ render field(:documents).uppy_tag(multiple: true)
306
+ ```
307
+
308
+ ### Type Mapping
309
+
310
+ ```ruby
311
+ module Plutonium::UI::Form::Options::InferredTypes
312
+ private
313
+
314
+ def infer_field_component
315
+ case inferred_field_type
316
+ when :rich_text
317
+ :markdown # Use EasyMDE for rich text
318
+ end
319
+
320
+ inferred_component = super
321
+ case inferred_component
322
+ when :select
323
+ :slim_select # Enhance selects with SlimSelect
324
+ when :date, :time, :datetime
325
+ :flatpickr # Use Flatpickr for date/time
326
+ else
327
+ inferred_component
328
+ end
329
+ end
330
+ end
331
+ ```
332
+
333
+ ## Theme System
334
+
335
+ ### Form Theme (`lib/plutonium/ui/form/theme.rb`)
336
+
337
+ Comprehensive theming for consistent form appearance:
338
+
339
+ ```ruby
340
+ class Plutonium::UI::Form::Theme < Phlexi::Form::Theme
341
+ def self.theme
342
+ super.merge({
343
+ # Layout
344
+ fields_wrapper: "space-y-6",
345
+ actions_wrapper: "flex justify-end space-x-3 pt-6 border-t",
346
+
347
+ # Input styles
348
+ input: "w-full border rounded-md shadow-sm px-3 py-2 border-gray-300 dark:border-gray-600 focus:ring-primary-500 focus:border-primary-500",
349
+ textarea: "w-full border rounded-md shadow-sm px-3 py-2 border-gray-300 dark:border-gray-600 focus:ring-primary-500 focus:border-primary-500",
350
+ select: "w-full border rounded-md shadow-sm px-3 py-2 border-gray-300 dark:border-gray-600 focus:ring-primary-500 focus:border-primary-500",
351
+
352
+ # Enhanced components
353
+ flatpickr: :input,
354
+ int_tel_input: :input,
355
+ easymde: "w-full border rounded-md border-gray-300 dark:border-gray-600",
356
+ uppy: "w-full border rounded-md border-gray-300 dark:border-gray-600",
357
+
358
+ # Association components
359
+ association: :select,
360
+
361
+ # File input
362
+ file: "w-full border rounded-md shadow-sm font-medium text-sm border-gray-300 dark:border-gray-600",
363
+
364
+ # States
365
+ valid_input: "border-green-500 focus:ring-green-500 focus:border-green-500",
366
+ invalid_input: "border-red-500 focus:ring-red-500 focus:border-red-500",
367
+
368
+ # Labels and hints
369
+ label: "block text-sm font-medium text-gray-700 dark:text-gray-200 mb-1",
370
+ hint: "mt-2 text-sm text-gray-500 dark:text-gray-200",
371
+ error: "mt-2 text-sm text-red-600 dark:text-red-500"
372
+ })
373
+ end
374
+ end
375
+ ```
376
+
377
+ ## Usage Patterns
378
+
379
+ ### Basic Form
380
+
381
+ ```ruby
382
+ # Simple form
383
+ class ContactForm < Plutonium::UI::Form::Base
384
+ def form_template
385
+ render field(:name).input_tag(as: :string)
386
+ render field(:email).input_tag(as: :email)
387
+ render field(:message).textarea_tag
388
+ render field(:phone).int_tel_input_tag
389
+ end
390
+ end
391
+ ```
392
+
393
+ ### Field Rendering and Wrappers
394
+
395
+ All fields must be explicitly rendered using the `render` method. Use wrappers to control layout and styling:
396
+
397
+ ```ruby
398
+ class PostForm < Plutonium::UI::Form::Resource
399
+ def form_template
400
+ # Basic field rendering
401
+ render field(:title).input_tag
402
+
403
+ # Field with wrapper styling
404
+ render field(:content).wrapped(class: "col-span-full") do |f|
405
+ render f.easymde_tag
406
+ end
407
+
408
+ # Custom wrapper with data attributes
409
+ render field(:author).wrapped(
410
+ class: "border rounded-lg p-4",
411
+ data: { controller: "tooltip" }
412
+ ) do |f|
413
+ render f.belongs_to_tag
414
+ end
415
+ end
416
+ end
417
+ ```
418
+
419
+ ### Resource Form
420
+
421
+ ```ruby
422
+ # Automatic resource form based on definition
423
+ class PostsController < ApplicationController
424
+ def new
425
+ @post = Post.new
426
+ @form = Plutonium::UI::Form::Resource.new(
427
+ @post,
428
+ resource_definition: current_definition
429
+ )
430
+ end
431
+
432
+ def edit
433
+ @post = Post.find(params[:id])
434
+ @form = Plutonium::UI::Form::Resource.new(
435
+ @post,
436
+ resource_definition: current_definition
437
+ )
438
+ end
439
+ end
440
+
441
+ # In view
442
+ <%= render @form %>
443
+ ```
444
+
445
+ ### Custom Form Components
446
+
447
+ ```ruby
448
+ # Create custom input component
449
+ class ColorPickerComponent < Plutonium::UI::Form::Components::Input
450
+ def view_template
451
+ div(data: { controller: "color-picker" }) do
452
+ input(**attributes, type: :color)
453
+ input(**color_text_attributes, type: :text, placeholder: "#000000")
454
+ end
455
+ end
456
+
457
+ private
458
+
459
+ def color_text_attributes
460
+ attributes.merge(
461
+ name: "#{attributes[:name]}_text",
462
+ data: { color_picker_target: "text" }
463
+ )
464
+ end
465
+ end
466
+
467
+ # Register in form builder
468
+ class CustomFormBuilder < Plutonium::UI::Form::Base::Builder
469
+ def color_picker_tag(**options, &block)
470
+ create_component(ColorPickerComponent, :color_picker, **options, &block)
471
+ end
472
+ end
473
+
474
+ # Use in form
475
+ render field(:brand_color).color_picker_tag
476
+ ```
477
+
478
+ ### Nested Resources
479
+
480
+ ```ruby
481
+ class PostForm < Plutonium::UI::Form::Resource
482
+ def form_template
483
+ field(:title).input_tag
484
+ field(:content).easymde_tag
485
+
486
+ # Nested comments
487
+ nested(:comments, allow_destroy: true) do |comment_form|
488
+ comment_form.field(:content).textarea_tag
489
+ comment_form.field(:author_name).input_tag(as: :string)
490
+ end
491
+ end
492
+ end
493
+ ```
494
+
495
+ ## JavaScript Integration
496
+
497
+ ### Automatic Dependencies
498
+
499
+ Plutonium automatically includes JavaScript libraries for enhanced form components:
500
+ - **EasyMDE** for markdown editing
501
+ - **Flatpickr** for date/time picking
502
+ - **Intl-Tel-Input** for phone inputs
503
+ - **Uppy** for file uploads
504
+
505
+ ### Stimulus Controllers
506
+
507
+ Each enhanced component uses a Stimulus controller for initialization and cleanup:
508
+
509
+ ```javascript
510
+ // easymde_controller.js
511
+ export default class extends Controller {
512
+ connect() {
513
+ this.editor = new EasyMDE({
514
+ element: this.element,
515
+ spellChecker: false,
516
+ toolbar: ["bold", "italic", "heading", "|", "quote"]
517
+ });
518
+ }
519
+
520
+ disconnect() {
521
+ if (this.editor) {
522
+ this.editor.toTextArea();
523
+ this.editor = null;
524
+ }
525
+ }
526
+ }
527
+
528
+ // flatpickr_controller.js
529
+ export default class extends Controller {
530
+ connect() {
531
+ this.picker = new flatpickr(this.element, this.#buildOptions());
532
+ }
533
+
534
+ disconnect() {
535
+ if (this.picker) {
536
+ this.picker.destroy();
537
+ this.picker = null;
538
+ }
539
+ }
540
+
541
+ #buildOptions() {
542
+ let options = { altInput: true };
543
+ if (this.element.attributes.type.value == "datetime-local") {
544
+ options.enableTime = true;
545
+ } else if (this.element.attributes.type.value == "time") {
546
+ options.enableTime = true;
547
+ options.noCalendar = true;
548
+ }
549
+ return options;
550
+ }
551
+ }
552
+ ```
553
+
554
+ ## Advanced Features
555
+
556
+ ### Form Validation
557
+
558
+ ```ruby
559
+ class PostForm < Plutonium::UI::Form::Resource
560
+ def form_template
561
+ # Client-side validation attributes
562
+ render field(:title).input_tag(
563
+ required: true,
564
+ minlength: 3,
565
+ maxlength: 100
566
+ )
567
+
568
+ render field(:email).input_tag(
569
+ type: :email,
570
+ pattern: "[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$"
571
+ )
572
+
573
+ # Custom validation with JavaScript
574
+ render field(:password).input_tag(
575
+ type: :password,
576
+ data: {
577
+ controller: "password-validator",
578
+ action: "input->password-validator#validate"
579
+ }
580
+ )
581
+ end
582
+ end
583
+ ```
584
+
585
+ ### Dynamic Forms
586
+
587
+ The recommended way to create dynamic forms is by using the `condition` and `pre_submit` options in your resource definition file. This keeps the logic declarative and out of custom form classes.
588
+
589
+ ```ruby
590
+ # app/definitions/post_definition.rb
591
+ class PostDefinition < Plutonium::Resource::Definition
592
+ # This input will trigger a form refresh whenever its value changes.
593
+ input :send_notifications, as: :boolean, pre_submit: true
594
+
595
+ # This input will only be shown if the `condition` evaluates to true.
596
+ # The condition is re-evaluated after a pre-submit refresh.
597
+ input :notification_channel,
598
+ as: :select,
599
+ collection: %w[Email SMS Push],
600
+ condition: -> { object.send_notifications? }
601
+ end
602
+ ```
603
+ ::: tip
604
+ For more details on how to configure conditional inputs, see the [Definition Module documentation](./definition.md#4-add-conditional-logic).
605
+ :::