playbook_ui 18.1.0.pre.rc.0 → 18.1.0.pre.rc.1

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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/app/pb_kits/playbook/pb_form/docs/_form_model_values.html.erb +43 -0
  3. data/app/pb_kits/playbook/pb_form/docs/_form_model_values.md +22 -0
  4. data/app/pb_kits/playbook/pb_form/docs/example.yml +1 -0
  5. data/app/pb_kits/playbook/pb_multi_level_select/_helper_functions.tsx +12 -1
  6. data/app/pb_kits/playbook/pb_multi_level_select/_multi_level_select.tsx +2 -3
  7. data/app/pb_kits/playbook/pb_multi_level_select/multi_level_select.test.jsx +26 -0
  8. data/app/pb_kits/playbook/pb_multi_level_select/tree_helpers.js +10 -4
  9. data/dist/ai/all-schemas.json +166 -153
  10. data/dist/ai/forms.json +101 -99
  11. data/dist/ai/index.json +2 -2
  12. data/dist/ai/kits/button.schema.json +4 -3
  13. data/dist/ai/kits/checkbox.schema.json +7 -6
  14. data/dist/ai/kits/date_picker.schema.json +12 -10
  15. data/dist/ai/kits/dropdown.schema.json +14 -13
  16. data/dist/ai/kits/multi_level_select.schema.json +13 -12
  17. data/dist/ai/kits/phone_number_input.schema.json +18 -17
  18. data/dist/ai/kits/select.schema.json +18 -17
  19. data/dist/ai/kits/star_rating.schema.json +10 -9
  20. data/dist/ai/kits/text_input.schema.json +35 -34
  21. data/dist/ai/kits/textarea.schema.json +10 -9
  22. data/dist/ai/kits/time_picker.schema.json +12 -11
  23. data/dist/ai/kits/typeahead.schema.json +13 -12
  24. data/dist/chunks/vendor.js +1 -1
  25. data/dist/playbook-rails.js +1 -1
  26. data/lib/playbook/changelog_generator.rb +22 -1
  27. data/lib/playbook/forms/builder/attribute_defaults.rb +118 -0
  28. data/lib/playbook/forms/builder/collection_select_field.rb +3 -0
  29. data/lib/playbook/forms/builder/date_picker_field.rb +22 -7
  30. data/lib/playbook/forms/builder/dropdown_field.rb +12 -0
  31. data/lib/playbook/forms/builder/form_field_builder.rb +2 -0
  32. data/lib/playbook/forms/builder/intl_telephone_field.rb +8 -0
  33. data/lib/playbook/forms/builder/multi_level_select_field.rb +10 -0
  34. data/lib/playbook/forms/builder/phone_number_field.rb +8 -0
  35. data/lib/playbook/forms/builder/select_field.rb +3 -0
  36. data/lib/playbook/forms/builder/star_rating_field.rb +7 -0
  37. data/lib/playbook/forms/builder/time_picker_field.rb +8 -0
  38. data/lib/playbook/forms/builder/typeahead_field.rb +15 -0
  39. data/lib/playbook/forms/builder.rb +3 -0
  40. data/lib/playbook/version.rb +1 -1
  41. metadata +5 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 73aa2379cffe56b84ee997cf41ef0b64612bfe0d9759f98dc44b2ced32b2ec10
4
- data.tar.gz: 3b9a750583ef9058572ea2490712624b6cd1a69ece3435388fd55bcd88259820
3
+ metadata.gz: 8885b14507b1ffa09091188f028bc63097b1d187eae55a40673ac13a0a8d67c1
4
+ data.tar.gz: b5266d626ee2c9c069c5e18f7bf9b55c7e62d04f394645c1babca4bf7f1f0bf0
5
5
  SHA512:
6
- metadata.gz: 76deee8035672747d6d959b378ab0f04f6c10bbf4eb7029ad346977b1d1b687b3560ebd6707e4daf978a2c4b8069834f9017842da346afb92ca258232d68230b
7
- data.tar.gz: 8f1d3ea325d14bd6fdb3f179d818c44fe016535608370bb838620ed958ad8a0d7b26b5319e9021416b2e6eb4b08414bf933dd634a957543556b1254467a75c85
6
+ metadata.gz: 641e3a2789ab3416e5f5f594c0c33fe31510e55750c4d1432b9acfa35a9f5f50529e1eb1663244c4faefbd353ebf15731f470c457fe365954125bd18c7e55183
7
+ data.tar.gz: 90781fad9169b4c1a23c078ec59a38448562d6cee63811282cbddb011a9269ac177bf5435472d2164aeebe2c36139e0d3763cf52539a03aee3865c54c409fa46
@@ -0,0 +1,43 @@
1
+ <%
2
+ load Playbook.kit_path("form", "docs", "_sample_data.ruby").to_s
3
+
4
+ dropdown_options = [
5
+ { label: "Open", value: "open", id: "status-open" },
6
+ { label: "Closed", value: "closed", id: "status-closed" },
7
+ ]
8
+
9
+ example_model = Class.new do
10
+ include ActiveModel::Model
11
+ attr_accessor :name, :spots, :starts_at, :ends_at, :status, :phone
12
+
13
+ def self.name
14
+ "Example"
15
+ end
16
+ end.new(
17
+ name: "Gary 🎉",
18
+ spots: 42,
19
+ starts_at: Time.utc(2026, 8, 22, 19, 37, 0),
20
+ ends_at: Time.utc(2026, 8, 20, 23, 59, 0),
21
+ status: "open",
22
+ phone: "+15551234567"
23
+ )
24
+ example_model.errors.add(:name, "cannot contain emoji")
25
+ example_model.errors.add(:spots, "must be 20 or fewer")
26
+ example_model.errors.add(:starts_at, "must be before ends at")
27
+ %>
28
+
29
+ <%= pb_rails("card", props: PlaybookFormDocsSampleData.demo_form_card_props) do %>
30
+ <%= pb_form_with(model: example_model, method: :get, url: "", html: { onsubmit: "return false;" }) do |form| %>
31
+ <%= form.text_field :name, props: { label: true } %>
32
+ <%= form.number_field :spots, props: { label: true } %>
33
+ <%= form.date_picker :starts_at, props: { label: true } %>
34
+ <%= form.date_picker :ends_at, props: { label: true } %>
35
+ <%= form.dropdown_field :status, props: { label: true, options: dropdown_options } %>
36
+ <%= form.phone_number_field :phone, props: { label: true } %>
37
+
38
+ <%= form.actions do |action| %>
39
+ <%= action.submit %>
40
+ <%= action.button props: { type: "reset", text: "Cancel", variant: "secondary" } %>
41
+ <% end %>
42
+ <% end %>
43
+ <% end %>
@@ -0,0 +1,22 @@
1
+ When the form is bound to a model (`model:` / `options: { model: ... }`), builder fields read attribute values and validation errors automatically — similar to Rails `text_field`. You usually do not need `default_date`, `value`, `default_value`, `selected_ids`, or `error`:
2
+
3
+ ```erb
4
+ <%= form.date_picker :starts_at, props: { label: true } %>
5
+ ```
6
+
7
+ This example is an edit form after a failed validation: values come from the model, and errors appear under invalid attributes. Explicit props still win when you pass them.
8
+
9
+ | Field | Auto value | Auto error |
10
+ | --- | --- | --- |
11
+ | `date_picker` | `default_date` (wall-clock `YYYY-MM-DDT00:00:00`) | yes |
12
+ | `time_picker` | `default_time` (HH:MM) | yes |
13
+ | `dropdown_field` | `default_value` (matched via `options`) | yes |
14
+ | `star_rating_field` | `default_value` | n/a |
15
+ | `multi_level_select` | `selected_ids` | yes |
16
+ | `phone_number_field` / `intl_telephone` | `value` | yes |
17
+ | `typeahead` | `default_options` only if already option-shaped | yes |
18
+ | text / textarea / select / `collection_select` | via Rails | yes |
19
+
20
+ **Notes**
21
+ - Dropdown needs `options` so a stored id/value can resolve to an option hash; unmatched ids are left unset.
22
+ - Typeahead only auto-binds when the attribute is `{ label, value }` (or an array of those). A bare database id still needs explicit `default_options`.
@@ -4,5 +4,6 @@ examples:
4
4
  - form_defaults: Defaults
5
5
  - form_validation: Validation and Error Messages
6
6
  - form_default_values: Default Values
7
+ - form_model_values: Model Values + Errors
7
8
  - form_loading: Loading
8
9
  - form_multi_column: Multi-Column Layout
@@ -1,3 +1,14 @@
1
+ // selected_ids / tree_data ids may be strings or numbers; includes() is strict.
2
+ export const selectedIdsInclude = (
3
+ selectedIds: Array<string | number> | null | undefined,
4
+ id: string | number
5
+ ): boolean => {
6
+ if (!selectedIds || !selectedIds.length) return false
7
+
8
+ const target = String(id)
9
+ return selectedIds.some((selectedId) => String(selectedId) === target)
10
+ };
11
+
1
12
  // Function is going over formattedData and returning all objects that match the
2
13
  // ID of the clicked item from the dropdown
3
14
  export const filterFormattedDataById = (
@@ -160,7 +171,7 @@ export const getExpandedItems = (
160
171
 
161
172
  // Only expand based on selected items if showCheckedChildren is true
162
173
  if (showCheckedChildren) {
163
- if (selectedIds && selectedIds.length && selectedIds.includes(item.id)) {
174
+ if (selectedIdsInclude(selectedIds, item.id)) {
164
175
  expandedItems.push(...itemAncestors.map((ancestor: any) => ancestor.id));
165
176
  }
166
177
  if (Array.isArray(item.children)) {
@@ -32,6 +32,7 @@ import {
32
32
  getDefaultCheckedItems,
33
33
  recursiveCheckParent,
34
34
  getExpandedItems,
35
+ selectedIdsInclude,
35
36
  } from "./_helper_functions";
36
37
 
37
38
  import { DialogContext } from "../pb_dialog/_dialog_context";
@@ -248,9 +249,7 @@ const MultiLevelSelect = forwardRef<HTMLInputElement, MultiLevelSelectProps>(
248
249
  const isDisabled =
249
250
  item.disabled || (parentDisabled && !returnAllSelected);
250
251
 
251
- const explicitlySelected = Boolean(
252
- selectedIds && selectedIds?.length && selectedIds.includes(item.id)
253
- );
252
+ const explicitlySelected = selectedIdsInclude(selectedIds, item.id);
254
253
 
255
254
  const checked =
256
255
  !isDisabled &&
@@ -546,6 +546,32 @@ describe('MultiLevelSelect tree_helpers', () => {
546
546
  expect(formatted[0].children[1].checked).toBe(false)
547
547
  })
548
548
 
549
+ test('matches selected ids when selected_ids are numbers and tree ids are strings', () => {
550
+ const numericTree = [
551
+ {
552
+ id: '101',
553
+ label: 'Parent',
554
+ children: [{ id: '102', label: 'Child' }],
555
+ },
556
+ ]
557
+ const formatted = addCheckedAndParentProperty(cloneTree(numericTree), [102])
558
+ expect(formatted[0].children[0].checked).toBe(true)
559
+ expect(formatted[0].checked).toBe(false)
560
+ })
561
+
562
+ test('matches selected ids when selected_ids are strings and tree ids are numbers', () => {
563
+ const numericTree = [
564
+ {
565
+ id: 101,
566
+ label: 'Parent',
567
+ children: [{ id: 102, label: 'Child' }],
568
+ },
569
+ ]
570
+ const formatted = addCheckedAndParentProperty(cloneTree(numericTree), ['102'])
571
+ expect(formatted[0].children[0].checked).toBe(true)
572
+ expect(formatted[0].checked).toBe(false)
573
+ })
574
+
549
575
  test('cascades checked state to children in default multi mode', () => {
550
576
  const formatted = addCheckedAndParentProperty(cloneTree(helperTree), ['parent'])
551
577
  expect(formatted[0].checked).toBe(true)
@@ -1,5 +1,13 @@
1
1
  export const cloneTree = (value) => JSON.parse(JSON.stringify(value))
2
2
 
3
+ // selected_ids / tree_data ids may be strings or numbers; includes() is strict.
4
+ export const selectedIdsInclude = (selectedIds, id) => {
5
+ if (!selectedIds || !selectedIds.length) return false
6
+
7
+ const target = String(id)
8
+ return selectedIds.some((selectedId) => String(selectedId) === target)
9
+ }
10
+
3
11
  export const filterFormattedDataById = (formattedData, id) => {
4
12
  const matched = []
5
13
  const recursiveSearch = (data, term) => {
@@ -138,7 +146,7 @@ export const getExpandedItems = (
138
146
  }
139
147
 
140
148
  if (showCheckedChildren) {
141
- if (selectedIds && selectedIds.length && selectedIds.includes(item.id)) {
149
+ if (selectedIdsInclude(selectedIds, item.id)) {
142
150
  expandedItems.push(...itemAncestors.map((ancestor) => ancestor.id))
143
151
  }
144
152
  if (Array.isArray(item.children)) {
@@ -193,9 +201,7 @@ export const addCheckedAndParentProperty = (
193
201
  const isDisabled =
194
202
  item.disabled || (parentDisabled && !returnAllSelected)
195
203
 
196
- const explicitlySelected = Boolean(
197
- selectedIds && selectedIds.length && selectedIds.includes(item.id)
198
- )
204
+ const explicitlySelected = selectedIdsInclude(selectedIds, item.id)
199
205
 
200
206
  const checked =
201
207
  !isDisabled &&