glib-web 3.9.0 → 3.11.0

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: d1d627b99feb7c052188e21ff35b225428e2c5affb1ea7f8111582681ba92a88
4
- data.tar.gz: 216d55f78c01ef928f54d1c4971b9c0fe4e632a4add7da00f9dc218c658cb201
3
+ metadata.gz: 29bf404f48d26b824a2e10162a8ec01be7a13754027b4bd3e6b625c1255dbb80
4
+ data.tar.gz: 5ed5e65ac4e3cbd23180de8fd03f2a98746b4a5df787d8d69f5f25c9d0f66bbb
5
5
  SHA512:
6
- metadata.gz: 62d0d1bfd6619de0f66d3332abad1eb7971952c98000982d4ef1fa7dfa5c2258978b1ee3f78907fd0e0e8a8b1a9a1117f843410bac26a045aee29f2a9513f6fe
7
- data.tar.gz: 88f948b5cb7c56269d68b8afbf56ffee729813c60061eb7fc10369e945d813d93d037efd2e5ad1ac482f446b5c28bab981c60dee59832c0ddff3f63d7ea12396
6
+ metadata.gz: 3c42ecfe127600686502e3721e37a05bf62ce0b194801f42c0c1d49c5df09d12f86cdd8435e682686de4d2c2cf7d288ca6c205cf3da48a452d1e2aadb1f685e1
7
+ data.tar.gz: '082866223cfe93db662d532a2030bc60428d82ae1d62c2b98c9910d2e37c3054ca5120d851cb021210eaca9cf60828483a3f91acc70061110d3581dcf17d015a'
@@ -1,10 +1,17 @@
1
1
  module Glib
2
2
  module EnumHelper
3
- def glib_enum_options(clazz, enum_field, keys = nil)
4
- # keys ||= clazz.send("#{enum_field}s").keys
5
-
6
- keys ||= clazz.send(enum_field.to_s.pluralize).keys
7
- keys.map { |i| { text: clazz.glib_enum_humanize(enum_field, i), value: i } }
3
+ def glib_enum_options(clazz, enum_field, keys: nil, include_hints: false)
4
+ enum_name = enum_field.to_s
5
+ keys ||= clazz.defined_enums[enum_name].keys
6
+ keys.map do |i|
7
+ text = clazz.glib_enum_humanize(enum_field, i)
8
+ if include_hints
9
+ i18n_key = clazz.model_name.i18n_key
10
+ hint = I18n.t("dt_models.#{i18n_key}.#{enum_name.pluralize}.#{i}.hint")
11
+ text += " #{hint}"
12
+ end
13
+ { text: text, value: i }
14
+ end
8
15
  end
9
16
 
10
17
  # See https://en.wikipedia.org/wiki/List_of_tz_database_time_zones and description
@@ -1,15 +1,16 @@
1
1
  module Glib
2
2
  module FormsHelper
3
3
  def glib_form_field_label(model_name, prop, args = {})
4
- I18n.t("dt_models.#{model_name}.#{prop}.label", args.merge(default: nil)) || I18n.t("activerecord.attributes.#{model_name}.#{prop}", args)
4
+ I18n.t("dt_models.#{model_name}.#{prop}.label", **args.merge(default: nil)) ||
5
+ I18n.t("activerecord.attributes.#{model_name}.#{prop}", **args)
5
6
  end
6
7
 
7
8
  def glib_form_hint_label(model_name, prop, args = {})
8
- I18n.t("dt_models.#{model_name}.#{prop}.hint", args.merge(default: nil))
9
+ I18n.t("dt_models.#{model_name}.#{prop}.hint", **args.merge(default: nil))
9
10
  end
10
11
 
11
12
  def glib_form_placeholder_label(model_name, prop, args = {})
12
- I18n.t("dt_models.#{model_name}.#{prop}.placeholder", args.merge(default: nil))
13
+ I18n.t("dt_models.#{model_name}.#{prop}.placeholder", **args.merge(default: nil))
13
14
  end
14
15
  end
15
16
  end
@@ -5,7 +5,7 @@ module Glib
5
5
  ### Padding
6
6
 
7
7
  def glib_json_padding_body
8
- { top: 12, left: 20, right: 20, bottom: 12 }
8
+ { x: 20, y: 12, top: 22 }
9
9
  end
10
10
 
11
11
  def glib_json_padding_list
@@ -48,15 +48,21 @@ class Glib::JsonUi::ViewBuilder
48
48
  # Override
49
49
  def created
50
50
  if @prop && (form = page.current_form)
51
- form.field_assert_respond_to(@prop)
51
+ association = form.nested_associations.last
52
+ context = association || form
52
53
 
53
- @name ||= form.field_name(@prop, @multiple || false)
54
- @value ||= form.field_value(@prop)
55
- @label ||= form.field_label(@prop, @label_args || {})
56
- @hint ||= form.hint_label(@prop, @hint_args || {})
57
- @placeholder ||= form.placeholder_label(@prop, @placeholder_args || {})
54
+ context.field_assert_respond_to(@prop)
58
55
 
59
- @validation ||= form.field_validation(@prop)
56
+ @name ||= context.field_name(@prop, @multiple || false)
57
+ @label ||= context.field_label(@prop, @label_args || {})
58
+ @hint ||= context.hint_label(@prop, @hint_args || {})
59
+ @placeholder ||= context.placeholder_label(@prop, @placeholder_args || {})
60
+ @validation ||= context.field_validation(@prop)
61
+
62
+ if form.current_dynamic_group.nil?
63
+ # This is not relevant inside a dynamic group
64
+ @value ||= context.field_value(@prop)
65
+ end
60
66
  end
61
67
  json.name @name
62
68
  json.value @value if @value
@@ -196,17 +202,28 @@ class Glib::JsonUi::ViewBuilder
196
202
  end
197
203
 
198
204
  class DynamicGroup < AbstractField
205
+ include Panels::ModelPanel
206
+
199
207
  string :titlePrefix
200
208
  panels_builder :content, :template
201
209
  hash :groupFieldProperties
202
210
 
203
211
  # NOTE: Consider using sub-panel instead (e.g. groupTemplate)
204
212
  # views :groupTemplateViews
213
+
214
+ def content(value)
215
+ @delegate_class = Glib::JsonUi::ViewBuilder::Panels::Form
216
+ form = page.current_form
217
+
218
+ form.current_dynamic_group = self
219
+ form.nested_associations << self
220
+ value.call(page.content_builder([:template]))
221
+ form.nested_associations.pop
222
+ form.current_dynamic_group = nil
223
+ end
205
224
  end
206
225
 
207
226
  class RadioGroup < AbstractField
208
- # string :name
209
- # string :value
210
227
  views :childViews
211
228
  bool :row
212
229
 
@@ -1,15 +1,22 @@
1
1
  class Glib::JsonUi::ViewBuilder
2
2
  module Panels
3
3
  class Form < View
4
+ attr_reader :model_name # See Panels::Form.field_name
5
+ attr_accessor :current_dynamic_group
6
+
4
7
  action :onSubmit
5
8
  string :paramNameForFormData
6
9
  bool :local
7
10
 
11
+ def nested_associations
12
+ @nested_associations ||= []
13
+ end
14
+
8
15
  # TODO: Enable this when we know it won't break existing apps.
9
16
  # Even for pure client-side apps, this is required because form.validate() requires a URL to construct form data.
10
17
  # required :url
11
18
 
12
- def is_array_association?(prop)
19
+ def self.is_array_association?(model, prop)
13
20
  # # Not all model is ActiveRecord
14
21
  # if @model.class.respond_to?(:reflect_on_association)
15
22
  # return @model.class.reflect_on_association(prop).macro
@@ -17,42 +24,103 @@ class Glib::JsonUi::ViewBuilder
17
24
  # false
18
25
 
19
26
  # Not all model is ActiveRecord
20
- @model.class.try(:reflect_on_association, prop)&.macro == :has_many
27
+ model.class.try(:reflect_on_association, prop)&.macro == :has_many
28
+ end
29
+
30
+ def self.is_single_association?(model, prop)
31
+ # Not all model is ActiveRecord
32
+ model.class.try(:reflect_on_association, prop)&.macro == :belongs_to
21
33
  end
22
34
 
23
35
  def field_assert_respond_to(prop)
36
+ self.class.field_assert_respond_to(@model, prop)
37
+ # # Identify a prop being used on a nil model or incorrect model.
38
+ # raise "Invalid property `#{prop}` on '#{@model.class}'" unless @model.respond_to?(prop)
39
+ end
40
+
41
+ def self.field_assert_respond_to(model, prop)
24
42
  # Identify a prop being used on a nil model or incorrect model.
25
- raise "Invalid property `#{prop}` on '#{@model.class}'" unless @model.respond_to?(prop)
43
+ raise "Invalid property `#{prop}` on '#{model.class}'" unless model.respond_to?(prop)
26
44
  end
27
45
 
28
46
  def field_name(prop, multiple)
29
- suffix = is_array_association?(prop) || multiple ? '[]' : ''
30
- "#{@model_name}[#{prop}]#{suffix}"
47
+ self.class.field_name(@model, prop, multiple, page)
48
+ end
49
+
50
+ def self.field_name(model, prop, multiple, page)
51
+ form = page.current_form
52
+ include_form_model = true
53
+ reversed_model_panels = []
54
+ form.nested_associations.reverse.each do |panel|
55
+ if panel.is_a?(Fields::DynamicGroup)
56
+ include_form_model = false
57
+ break # Remove anything before Fields::DynamicGroup
58
+ end
59
+ reversed_model_panels << panel
60
+ end
61
+
62
+ name = include_form_model ? form.model_name : ''
63
+ reversed_model_panels.reverse.each do |panel|
64
+ index_exists = !panel.assoc_order_index.nil?
65
+ field_name = index_exists ? panel.model_name.pluralize : panel.model_name
66
+ name += "[#{field_name}_attributes]"
67
+ if index_exists
68
+ index = panel.assoc_order_index == :auto ? '' : panel.assoc_order_index
69
+ name += "[#{index}]"
70
+ end
71
+ end
72
+
73
+ suffix = is_array_association?(model, prop) || multiple ? '[]' : ''
74
+ "#{name}[#{prop}]#{suffix}"
31
75
  end
32
76
 
33
77
  def field_value(prop)
34
- if is_array_association?(prop)
35
- @model.send(prop)&.map { |record| record.id }
78
+ self.class.field_value(@model, prop)
79
+ end
80
+
81
+ def self.field_value(model, prop)
82
+ if is_array_association?(model, prop)
83
+ model.send(prop)&.map { |record| record.id }
36
84
  else
37
- @model.send(prop)
85
+ model.send(prop)
38
86
  end
39
87
  end
40
88
 
41
89
  def field_label(prop, args)
42
- I18n.t("dt_models.#{@model_name}.#{prop}.label", **args.merge(default: nil)) || I18n.t("activerecord.attributes.#{@model_name}.#{prop}", **args)
90
+ self.class.field_label(@model, @model_name, prop, args)
91
+ end
92
+
93
+ def self.field_label(model, model_name, prop, args)
94
+ name = prop.to_s
95
+ if name.ends_with?('_id') && is_single_association?(model, name.chomp('_id'))
96
+ name = name.chomp('_id') # Always uses non-ID property name in i18n files
97
+ end
98
+ I18n.t("dt_models.#{model_name}.#{name}.label", **args.merge(default: nil)) || I18n.t("activerecord.attributes.#{model_name}.#{name}", **args)
43
99
  end
44
100
 
45
101
  def hint_label(prop, args)
46
- I18n.t("dt_models.#{@model_name}.#{prop}.hint", **args.merge(default: nil))
102
+ self.class.hint_label(@model_name, prop, args)
103
+ end
104
+
105
+ def self.hint_label(model_name, prop, args)
106
+ I18n.t("dt_models.#{model_name}.#{prop}.hint", **args.merge(default: nil))
47
107
  end
48
108
 
49
109
  def placeholder_label(prop, args)
50
- I18n.t("dt_models.#{@model_name}.#{prop}.placeholder", **args.merge(default: nil))
110
+ self.class.placeholder_label(@model_name, prop, args)
111
+ end
112
+
113
+ def self.placeholder_label(model_name, prop, args)
114
+ I18n.t("dt_models.#{model_name}.#{prop}.placeholder", **args.merge(default: nil))
51
115
  end
52
116
 
53
117
  def field_validation(prop)
118
+ self.class.field_validation(@model, prop)
119
+ end
120
+
121
+ def self.field_validation(model, prop)
54
122
  validations = {}
55
- @model.class.validators_on(prop).each do |validator|
123
+ model.class.validators_on(prop).each do |validator|
56
124
  if validator.kind == :presence
57
125
  validations[:required] = { message: 'Required' }
58
126
  end
@@ -299,5 +367,61 @@ class Glib::JsonUi::ViewBuilder
299
367
  # end
300
368
  end
301
369
 
370
+ module ModelPanel
371
+ attr_reader :model_name # See Panels::Form.field_name
372
+ attr_reader :assoc_order_index
373
+
374
+ def model(model)
375
+ @model = model
376
+ @model_name = @model.class.model_name.singular
377
+ end
378
+
379
+ def order_index(index)
380
+ @assoc_order_index = index
381
+ end
382
+
383
+ def field_name(prop, multiple)
384
+ @delegate_class.field_name(@model, prop, multiple, page)
385
+ end
386
+
387
+ def field_value(prop)
388
+ @delegate_class.field_value(@model, prop)
389
+ end
390
+
391
+ def field_label(prop, args)
392
+ @delegate_class.field_label(@model, @model_name, prop, args)
393
+ end
394
+
395
+ def hint_label(prop, args)
396
+ @delegate_class.hint_label(@model_name, prop, args)
397
+ end
398
+
399
+ def placeholder_label(prop, args)
400
+ @delegate_class.placeholder_label(@model_name, prop, args)
401
+ end
402
+
403
+ def field_validation(prop)
404
+ @delegate_class.field_validation(@model, prop)
405
+ end
406
+
407
+ def field_assert_respond_to(prop)
408
+ raise "Please specify a model for #{self.class.component_name} before using its property" unless @model
409
+ @delegate_class.field_assert_respond_to(@model, prop)
410
+ end
411
+ end
412
+
413
+ class Association < View
414
+ include ModelPanel
415
+
416
+ def childViews(value)
417
+ @delegate_class = Glib::JsonUi::ViewBuilder::Panels::Form
418
+ form = page.current_form
419
+
420
+ form.nested_associations << self
421
+ json.set!(:childViews) { value.call(page.view_builder) }
422
+ form.nested_associations.pop
423
+ end
424
+ end
425
+
302
426
  end
303
427
  end
@@ -47,6 +47,9 @@ module Glib
47
47
  # hash :tooltip
48
48
  array :extensions
49
49
 
50
+ def self.component_name
51
+ @component_name ||= self.name.sub('Glib::JsonUi::ViewBuilder::', '')
52
+ end
50
53
 
51
54
  # def initialize(json, page)
52
55
  # super(json, page)
@@ -11,15 +11,20 @@ module Glib
11
11
  extend ClassMethods
12
12
  end
13
13
 
14
- def glib_enum_humanize(enum_name, default_value = nil)
15
- self.class.glib_enum_humanize(enum_name, send(enum_name), default_value)
14
+ def glib_enum_humanize(enum_name, default_enum_value = nil)
15
+ self.class.glib_enum_humanize(enum_name, send(enum_name) || default_enum_value)
16
16
  end
17
17
  end
18
18
 
19
19
  module ClassMethods
20
- def glib_enum_humanize(enum_name, enum_value, default_value = nil)
20
+ def glib_enum_humanize(enum_name, enum_value, default_translation = nil)
21
21
  if enum_value
22
- I18n.t("activerecord.attributes.#{model_name.i18n_key}.#{enum_name.to_s.pluralize}.#{enum_value}", default: default_value)
22
+ if default_translation.nil? && Rails.env.development?
23
+ i18n_key = "activerecord.attributes.#{model_name.i18n_key}.#{enum_name.to_s.pluralize}.#{enum_value}"
24
+ I18n.t(i18n_key, raise: I18n::MissingTranslationData)
25
+ else
26
+ I18n.t(i18n_key, default: default_translation)
27
+ end
23
28
  end
24
29
  end
25
30
  end
@@ -1,40 +1,72 @@
1
1
  # To use this, simply:
2
2
  # - Include the module
3
3
  # - Add this migration: `add_column :model, :deleted_at, :datetime, null: true`
4
- #
5
- # After that, any call to `destroy` will automatically be a soft-deletion.
6
4
  module Glib
7
5
  module SoftDeletable
8
6
  extend ActiveSupport::Concern
9
7
 
8
+ module ClassMethods
9
+ def auto_hide_soft_deleted_records
10
+ column_name = :deleted_at
11
+ default_scope { where(column_name => nil) }
12
+
13
+ if Rails.env.development? || Rails.env.test?
14
+ raise ActiveRecord::ConfigurationError, "#{table_name}.#{column_name} need to be indexed" unless connection.index_exists?(table_name, column_name)
15
+ end
16
+ end
17
+
18
+ # `behaviour` can be either `:soft_destroy` or `:hard_destroy``
19
+ def on_mark_for_destruction(behaviour)
20
+ @__glib_mark_for_destruction_behaviour = behaviour
21
+ end
22
+
23
+ def __mark_for_destruction_behaviour
24
+ @__glib_mark_for_destruction_behaviour ||= :disallowed
25
+ end
26
+ end
27
+
10
28
  included do
11
- default_scope { where(deleted_at: nil) }
29
+ extend ClassMethods
30
+
12
31
  scope :with_deleted, -> { unscope(where: :deleted_at) }
13
32
 
14
- # "Soft delete" - set deleted_at if it is nil, actually destroy the record
15
- # if forced.
16
- #
17
- # @param force [Boolean]
18
- def destroy(force = nil)
19
- return force_destroy_record if force == :force
20
- return self if deleted?
33
+ # Override
34
+ def destroy
35
+ if marked_for_destruction?
36
+ case (behaviour = self.class.__mark_for_destruction_behaviour)
37
+ when :soft_destroy
38
+ soft_destroy
39
+ when :hard_destroy
40
+ # Make sure that the associated records are handled the same way as this record (i.e. the parent record).
41
+ mark_applicable_associations_for_destruction
42
+
43
+ hard_destroy
44
+ when :disallowed
45
+ raise_hard_delete_disallowed
46
+ else
47
+ raise "Unsupported on_mark_for_destruction behaviour: #{behaviour}"
48
+ end
49
+
50
+ return true # Tell Rails that destroy has succeeded
51
+ end
21
52
 
22
- soft_destroy_record
53
+ raise_hard_delete_disallowed
23
54
  end
24
55
 
25
- # More explicit naming which is sometimes useful for readability.
26
- def soft_destroy
27
- soft_destroy_record
56
+
57
+ def hard_destroy
58
+ method(:destroy).super_method.call
28
59
  end
29
60
 
30
- # Revive a soft-deleted record and associated records if soft-deleted,
31
- # otherwise return self
32
- def revive
33
- return self unless deleted?
61
+ def soft_destroy
62
+ ActiveRecord::Base.transaction do
63
+ _soft_destroy
64
+ end
65
+ end
34
66
 
67
+ def undo_soft_destroy
35
68
  ActiveRecord::Base.transaction do
36
- update(deleted_at: nil)
37
- revive_associated_records
69
+ _undo_soft_destroy
38
70
  end
39
71
  end
40
72
 
@@ -43,34 +75,69 @@ module Glib
43
75
  deleted_at.present?
44
76
  end
45
77
 
78
+ # Bypass validation because it is impossible to ensure records that have circular dependencies are valid at all time because
79
+ # records are updated/validated one at a time.
80
+ # Besides, it's probably not a good idea to prevent soft-deletion of objects that are already not valid.
81
+ def _soft_destroy
82
+ update_columns(deleted_at: DateTime.current)
83
+ soft_destroy_associated_records
84
+ end
85
+
86
+ def _undo_soft_destroy
87
+ undo_soft_destroy_associated_records
88
+ update_columns(deleted_at: nil)
89
+ end
90
+
46
91
  private
47
- def force_destroy_record
48
- ActiveRecord::Base.transaction do
49
- destroy_associated_records(:force)
50
- method(:destroy).super_method.call
92
+ def mark_applicable_associations_for_destruction
93
+ self.class.reflect_on_all_associations.each do |assoc|
94
+ next unless assoc.options[:dependent] == :destroy
95
+
96
+ process_soft_deletable_relationship(assoc.name) do |record|
97
+ record.mark_for_destruction
98
+ end
51
99
  end
52
100
  end
53
101
 
54
- def soft_destroy_record
55
- ActiveRecord::Base.transaction do
56
- destroy_associated_records
57
- update(deleted_at: Time.zone.now)
102
+ def raise_hard_delete_disallowed
103
+ raise ActiveRecord::ConfigurationError, "Hard deletion is not allowed for #{self.class.name}"
104
+ end
105
+
106
+ def process_soft_deletable_relationship(relationship, &block)
107
+ association(relationship) # Raises an exception if relationship doesn't exist. This is a security safe guard.
108
+
109
+ # Use `send()` instead of `association(relationship).scope` because the latter returns new record objects as opposed
110
+ # to existing objects which could cause a side effect.
111
+ relation = send(relationship)
112
+
113
+ if relation.respond_to?(:find_each)
114
+ relation.find_each do |record|
115
+ block.call(record)
116
+ end
117
+ else
118
+ block.call(relation) unless relation.nil? # Process a single record
58
119
  end
59
120
  end
60
121
 
61
- def destroy_associated_records(force = nil)
62
- associated_records.each { |r| r.destroy(force) }
122
+ def soft_destroy_associated_records
123
+ soft_deletable_associations.each do |relationship|
124
+ process_soft_deletable_relationship(relationship) { |record| record._soft_destroy }
125
+ end
63
126
  end
64
127
 
65
- def revive_associated_records
66
- associated_records.each(&:revive)
128
+ def undo_soft_destroy_associated_records
129
+ soft_deletable_associations.each do |relationship|
130
+ process_soft_deletable_relationship(relationship) { |record| record._undo_soft_destroy }
131
+ end
67
132
  end
68
133
 
69
134
  # This should return an array of all associated records of an object that
70
- # should be soft deleted with it (i.e. those that have dependent: :destroy
71
- # set). In principle we could figure this out automatically but in the
72
- # interest of simplicity we'll just define it manually for each class.
73
- def associated_records
135
+ # should be soft deleted with it. This provides a non-intrusive way that can
136
+ # co-exist with hard deletion (e.g. using `dependent: :destroy`), which means
137
+ # that the model can use both for different purposes.
138
+ # For example, a model may use soft-deletion for archiving and hard-deletion
139
+ # for editing (e.g. using `accepts_nested_attributes_for``)
140
+ def soft_deletable_associations
74
141
  []
75
142
  end
76
143
  end
@@ -7,14 +7,16 @@ module Glib
7
7
  scope :created_asc, -> { order(created_at: :asc) }
8
8
  scope :created_desc, -> { order(created_at: :desc) }
9
9
 
10
- # def glib_enum_humanize(enum_name, default_value = nil)
11
- # self.class.glib_enum_humanize(enum_name, send(enum_name), default_value)
12
- # end
10
+ def self.glib_has_one_attached(field_name)
11
+ has_one_attached field_name
13
12
 
14
- # def self.glib_enum_humanize(enum_name, enum_value, default_value = nil)
15
- # if enum_value
16
- # I18n.t("activerecord.attributes.#{model_name.i18n_key}.#{enum_name.to_s.pluralize}.#{enum_value}", default: default_value)
17
- # end
18
- # end
13
+ define_method("#{field_name}=") do |value|
14
+ if value == ''
15
+ send(field_name).detach
16
+ else
17
+ super(value)
18
+ end
19
+ end
20
+ end
19
21
  end
20
22
  end
@@ -15,7 +15,7 @@ page.form url: json_ui_garage_url(path: 'forms/generic_post'), method: 'post', p
15
15
  [
16
16
  { name: 'question', value: 'Quality of work' },
17
17
  { name: 'type', value: 'rating' },
18
- { name: 'enabled', value: '1', styleClasses: ['success'] },
18
+ { name: 'enabled', value: '1' },
19
19
  ],
20
20
  [
21
21
  { name: 'question', value: 'Satisfied?' },
@@ -26,9 +26,21 @@ page.form url: json_ui_garage_url(path: 'forms/generic_post'), method: 'post', p
26
26
  group.template padding: { left: 32 }, childViews: ->(template) do
27
27
  template.spacer height: 10
28
28
  template.fields_text width: 'matchParent', name: 'question', label: 'Question', placeholder: 'Question'
29
+
29
30
  options = [ :rating, :yes_no ]
30
- template.fields_select width: 'matchParent', name: 'type', label: 'Answer Type', placeholder: 'Answer Type', options: options.map { |o| { text: o.to_s.humanize, value: o } }
31
- template.fields_check width: 'matchParent', name: 'enabled', label: 'Enable', checkValue: '1'
31
+ template.fields_select \
32
+ width: 'matchParent',
33
+ name: 'type',
34
+ label: 'Answer Type',
35
+ placeholder: 'Answer Type',
36
+ options: options.map { |o| { text: o.to_s.humanize, value: o } }
37
+
38
+ template.fields_check \
39
+ width: 'matchParent',
40
+ name: 'enabled',
41
+ label: 'Enable',
42
+ checkValue: '1',
43
+ showIf: { "==": [{ "var": 'user[evaluation][{{index}}][type]' }, 'rating'] }
32
44
 
33
45
  template.spacer height: 14
34
46
  end
@@ -25,8 +25,8 @@ page.form options.merge(childViews: ->(form) do
25
25
  form.fields_file name: 'user[pdf1][]', width: 'matchParent', label: 'PDF Document', accepts: rules, directUploadUrl: rails_direct_uploads_url,
26
26
  placeholderView: { type: 'image', width: 100, height: 100, url: 'data:image/webp;base64,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' }
27
27
 
28
- rules = { fileType: 'pdf', maxFileSize: 5000 }
29
- form.fields_file name: 'user[pdf2][]', width: 'matchParent', label: 'PDF Document', accepts: rules, directUploadUrl: rails_direct_uploads_url
28
+ rules = { fileType: ['pdf', 'image'], maxFileSize: 5000 }
29
+ form.fields_file name: 'user[pdf2][]', width: 'matchParent', label: 'PDF/Image File', accepts: rules, directUploadUrl: rails_direct_uploads_url
30
30
 
31
31
  rules = { fileType: 'zip', maxFileSize: 5000 }
32
32
  form.fields_file name: 'user[zip][]', width: 'matchParent', label: 'ZIP Document', accepts: rules, directUploadUrl: rails_direct_uploads_url
@@ -50,8 +50,9 @@ page.list sections: [
50
50
  template.thumbnail title: 'Submit on Change', onClick: ->(action) do
51
51
  action.windows_open url: json_ui_garage_url(path: 'forms/submit_on_change')
52
52
  end
53
-
54
-
53
+ template.thumbnail title: 'Dynamic Group', onClick: ->(action) do
54
+ action.windows_open url: json_ui_garage_url(path: 'forms/dynamic_group')
55
+ end
55
56
  end
56
57
  end, ->(section) do
57
58
  section.header padding: glib_json_padding_list, childViews: ->(header) do
@@ -105,7 +106,6 @@ page.list sections: [
105
106
  section.header padding: glib_json_padding_list, childViews: ->(header) do
106
107
  header.h2 text: 'Web Only'
107
108
  end
108
-
109
109
  section.rows builder: ->(template) do
110
110
  template.thumbnail title: 'Basic Rich Text Editor', onClick: ->(action) do
111
111
  action.windows_open url: json_ui_garage_url(path: 'forms/rich_text')
@@ -124,9 +124,6 @@ page.list sections: [
124
124
  end
125
125
 
126
126
  section.rows builder: ->(template) do
127
- template.thumbnail title: 'Dynamic Group', onClick: ->(action) do
128
- action.windows_open url: json_ui_garage_url(path: 'forms/dynamic_group')
129
- end
130
127
  template.thumbnail title: 'Dynamic Select', onClick: ->(action) do
131
128
  action.windows_open url: json_ui_garage_url(path: 'forms/dynamic_select')
132
129
  end
@@ -56,7 +56,7 @@ page.form url: json_ui_garage_url(path: 'forms/generic_post'), method: 'post', p
56
56
  options = []
57
57
  languages.each do |group, sub|
58
58
  options << { type: 'label', text: group }
59
- options.concat(sub.map { |k, v| { value: k, text: v } })
59
+ options.concat(sub.map { |k, v| { value: k, text: v, subtitle: 'Country' } })
60
60
  options << { type: 'divider' }
61
61
  end
62
62
 
@@ -99,7 +99,13 @@ page.form url: json_ui_garage_url(path: 'forms/generic_post'), method: 'post', p
99
99
 
100
100
  form.spacer height: 20
101
101
  form.h1 text: 'Select'
102
- options = ['', 'show', 'hide']
102
+ # options = ['', 'show', 'hide']
103
+ options = {
104
+ '' => '<EMPTY>',
105
+ 'show' => 'Show',
106
+ 'hide' => 'Hide',
107
+ nil => '<NULL>'
108
+ }.map { |k, v| { value: k, text: v } }
103
109
  form.fields_select name: 'user[select1]', width: 'matchParent', label: 'Select "show"', options: options, value: ''
104
110
  form.label text: 'Selected', showIf: {
105
111
  "==": [
@@ -117,6 +123,22 @@ page.form url: json_ui_garage_url(path: 'forms/generic_post'), method: 'post', p
117
123
  ''
118
124
  ]
119
125
  }
126
+ form.label text: 'Null', showIf: {
127
+ "==": [
128
+ {
129
+ "var": 'user[select1]'
130
+ },
131
+ nil
132
+ ]
133
+ }
134
+ form.label text: 'Any', showIf: {
135
+ "!=": [
136
+ {
137
+ "var": 'user[select1]'
138
+ },
139
+ nil
140
+ ]
141
+ }
120
142
 
121
143
  form.spacer height: 20
122
144
  form.h1 text: 'Combined conditions'
@@ -7,11 +7,6 @@ render "#{@path_prefix}/nav_menu", json: json, page: page
7
7
  small_image_url = 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSGQpSWjtELISLBlmugOZ6wzl1JamYXQvbFeYywpfg3E8b8DrO0Kg&s'
8
8
 
9
9
  page.scroll padding: glib_json_padding_body, childViews: ->(scroll) do
10
- scroll.panels_column lg: { cols: 3 }, md: { cols: 1 }, sm: { cols: 1 }, xs: { cols: 1 }
11
- scroll.panels_column backgroundColor: '#333333', height: 'matchParent', lg: { cols: 3 }, xs: { cols: 4 }, childViews: ->(column) do
12
- column.image width: 'matchParent', url: glib_json_image_standard_url
13
- end
14
-
15
10
  scroll.h2 text: 'Avatar'
16
11
  scroll.spacer height: 6
17
12
  scroll.avatar \
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: glib-web
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.9.0
4
+ version: 3.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ''