decidim-toggle 0.1.3

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 (151) hide show
  1. checksums.yaml +7 -0
  2. data/.erb-lint.yml +2134 -0
  3. data/.github/workflows/website.yml +57 -0
  4. data/.gitignore +13 -0
  5. data/.gitlab-ci.yml +165 -0
  6. data/.node-version +1 -0
  7. data/.rspec +1 -0
  8. data/.rubocop.yml +2 -0
  9. data/.ruby-version +1 -0
  10. data/.simplecov +18 -0
  11. data/CONTRIBUTING.md +17 -0
  12. data/Gemfile +33 -0
  13. data/Gemfile.lock +843 -0
  14. data/LICENSE.md +661 -0
  15. data/README.md +90 -0
  16. data/Rakefile +38 -0
  17. data/app/commands/decidim/toggle/update_authorizations_command.rb +31 -0
  18. data/app/commands/decidim/toggle/update_emails_command.rb +30 -0
  19. data/app/commands/decidim/toggle/update_file_upload_settings_command.rb +31 -0
  20. data/app/commands/decidim/toggle/update_locale_command.rb +47 -0
  21. data/app/commands/decidim/toggle/update_module_config_command.rb +31 -0
  22. data/app/commands/decidim/toggle/update_name_command.rb +31 -0
  23. data/app/commands/decidim/toggle/update_omniauth_command.rb +30 -0
  24. data/app/commands/decidim/toggle/update_security_command.rb +31 -0
  25. data/app/controllers/decidim_toggle/system/settings_tab_controller.rb +64 -0
  26. data/app/forms/decidim/toggle/update_authorizations_form.rb +54 -0
  27. data/app/forms/decidim/toggle/update_emails_form.rb +28 -0
  28. data/app/forms/decidim/toggle/update_file_upload_settings_form.rb +26 -0
  29. data/app/forms/decidim/toggle/update_locale_form.rb +116 -0
  30. data/app/forms/decidim/toggle/update_name_form.rb +63 -0
  31. data/app/forms/decidim/toggle/update_omniauth_form.rb +37 -0
  32. data/app/forms/decidim/toggle/update_security_form.rb +65 -0
  33. data/app/helpers/decidim/toggle/javascript_config_helper.rb +11 -0
  34. data/app/helpers/decidim/toggle/system_settings_tab_helper.rb +59 -0
  35. data/app/models/decidim/toggle/organization_module_config.rb +15 -0
  36. data/app/overrides/add_toggle_javascript_admin.rb +11 -0
  37. data/app/overrides/add_toggle_javascript_public.rb +11 -0
  38. data/app/packs/entrypoints/decidim_toggle.js +3 -0
  39. data/app/packs/src/decidim/toggle/organization_settings_tabs.js +114 -0
  40. data/app/packs/stylesheets/decidim/toggle/organization_settings.scss +160 -0
  41. data/app/views/decidim/system/organizations/edit.html.erb +20 -0
  42. data/app/views/decidim_toggle/system/organizations/_default_form_tab.html.erb +5 -0
  43. data/app/views/decidim_toggle/system/organizations/_encryption_not_configured_callout.html.erb +6 -0
  44. data/app/views/decidim_toggle/system/organizations/_settings_tab_active_tab_field.html.erb +1 -0
  45. data/app/views/decidim_toggle/system/organizations/_settings_tab_submit.html.erb +4 -0
  46. data/app/views/decidim_toggle/system/organizations/_settings_tabs.html.erb +31 -0
  47. data/app/views/decidim_toggle/system/organizations/tabs/_authorizations_tab.html.erb +9 -0
  48. data/app/views/decidim_toggle/system/organizations/tabs/_emails_tab.html.erb +5 -0
  49. data/app/views/decidim_toggle/system/organizations/tabs/_file_upload_tab.html.erb +5 -0
  50. data/app/views/decidim_toggle/system/organizations/tabs/_language_tab.html.erb +35 -0
  51. data/app/views/decidim_toggle/system/organizations/tabs/_omniauth_tab.html.erb +5 -0
  52. data/app/views/decidim_toggle/system/organizations/tabs/_security_tab.html.erb +20 -0
  53. data/app/views/layouts/decidim/toggle/_javascript_config.html.erb +3 -0
  54. data/bin/check +10 -0
  55. data/bin/i18n-tasks +27 -0
  56. data/bin/postversion +14 -0
  57. data/config/assets.rb +8 -0
  58. data/config/locales/decidim_toggle_en.yml +58 -0
  59. data/crowdin.yml +15 -0
  60. data/db/migrate/20260321120000_create_decidim_toggle_organization_module_configs.rb +20 -0
  61. data/decidim-toggle.gemspec +35 -0
  62. data/docker-compose.yml +41 -0
  63. data/lib/decidim/toggle/engine.rb +62 -0
  64. data/lib/decidim/toggle/expose_attributes_to_js.rb +26 -0
  65. data/lib/decidim/toggle/expose_attributes_to_js_validator.rb +32 -0
  66. data/lib/decidim/toggle/gem_registry.rb +15 -0
  67. data/lib/decidim/toggle/informative_callouts.rb +76 -0
  68. data/lib/decidim/toggle/javascript_config.rb +87 -0
  69. data/lib/decidim/toggle/module_config.rb +64 -0
  70. data/lib/decidim/toggle/module_config_form.rb +41 -0
  71. data/lib/decidim/toggle/module_config_i18n.rb +44 -0
  72. data/lib/decidim/toggle/module_configuration_presenter.rb +55 -0
  73. data/lib/decidim/toggle/organization_settings_tabs.rb +69 -0
  74. data/lib/decidim/toggle/settings_form_builder.rb +200 -0
  75. data/lib/decidim/toggle/settings_tab_item.rb +37 -0
  76. data/lib/decidim/toggle/settings_tab_registry.rb +109 -0
  77. data/lib/decidim/toggle/settings_tabs.rb +56 -0
  78. data/lib/decidim/toggle/tab_form.rb +20 -0
  79. data/lib/decidim/toggle/version.rb +14 -0
  80. data/lib/decidim/toggle.rb +36 -0
  81. data/lib/tasks/decidim/toggle/toggle_upgrade.rake +13 -0
  82. data/lib/tasks/decidim/toggle/toggle_webpacker.rake +60 -0
  83. data/log/.gitignore +2 -0
  84. data/package.json +18 -0
  85. data/prettier.config.js +15 -0
  86. data/spec/commands/decidim/toggle/update_authorizations_command_spec.rb +41 -0
  87. data/spec/commands/decidim/toggle/update_emails_command_spec.rb +84 -0
  88. data/spec/commands/decidim/toggle/update_file_upload_settings_command_spec.rb +28 -0
  89. data/spec/commands/decidim/toggle/update_locale_command_spec.rb +53 -0
  90. data/spec/commands/decidim/toggle/update_module_config_command_spec.rb +38 -0
  91. data/spec/commands/decidim/toggle/update_name_command_spec.rb +49 -0
  92. data/spec/commands/decidim/toggle/update_omniauth_command_spec.rb +80 -0
  93. data/spec/commands/decidim/toggle/update_security_command_spec.rb +25 -0
  94. data/spec/decidim/toggle/settings_tab_item_spec.rb +34 -0
  95. data/spec/decidim/toggle/settings_tab_registry_spec.rb +66 -0
  96. data/spec/decidim/toggle/settings_tabs_spec.rb +60 -0
  97. data/spec/forms/concerns/decidim/toggle/informative_callouts_spec.rb +48 -0
  98. data/spec/forms/decidim/toggle/update_authorizations_form_spec.rb +40 -0
  99. data/spec/forms/decidim/toggle/update_emails_form_spec.rb +35 -0
  100. data/spec/forms/decidim/toggle/update_file_upload_settings_form_spec.rb +20 -0
  101. data/spec/forms/decidim/toggle/update_locale_form_spec.rb +64 -0
  102. data/spec/forms/decidim/toggle/update_name_form_spec.rb +57 -0
  103. data/spec/forms/decidim/toggle/update_omniauth_form_spec.rb +56 -0
  104. data/spec/forms/decidim/toggle/update_security_form_spec.rb +32 -0
  105. data/spec/helpers/decidim/toggle/system_settings_tab_helper_spec.rb +99 -0
  106. data/spec/lib/decidim/toggle/expose_attributes_to_js_spec.rb +31 -0
  107. data/spec/lib/decidim/toggle/expose_attributes_to_js_validator_spec.rb +30 -0
  108. data/spec/lib/decidim/toggle/gem_registry_spec.rb +30 -0
  109. data/spec/lib/decidim/toggle/javascript_config_spec.rb +169 -0
  110. data/spec/lib/decidim/toggle/module_config_form_spec.rb +45 -0
  111. data/spec/lib/decidim/toggle/module_config_spec.rb +74 -0
  112. data/spec/lib/decidim/toggle/module_configuration_presenter_spec.rb +53 -0
  113. data/spec/lib/decidim/toggle/settings_form_builder_spec.rb +115 -0
  114. data/spec/requests/decidim_toggle/system/settings_tab_spec.rb +144 -0
  115. data/spec/spec_helper.rb +12 -0
  116. data/spec/support/decidim_factories.rb +3 -0
  117. data/spec/support/devise.rb +5 -0
  118. data/spec/system/decidim_toggle/javascript_config_spec.rb +56 -0
  119. data/website/.gitignore +4 -0
  120. data/website/docs/developer/_category_.json +8 -0
  121. data/website/docs/developer/code-of-conduct.md +99 -0
  122. data/website/docs/developer/contribute.md +50 -0
  123. data/website/docs/developer/deface-usage.md +37 -0
  124. data/website/docs/developer/documentation.md +58 -0
  125. data/website/docs/developer/error-handling.md +51 -0
  126. data/website/docs/developer/tab-registry.md +51 -0
  127. data/website/docs/developer/view-customization.md +49 -0
  128. data/website/docs/index.md +80 -0
  129. data/website/docs/integrate/_category_.json +8 -0
  130. data/website/docs/integrate/attributes.md +121 -0
  131. data/website/docs/integrate/customize-views.md +62 -0
  132. data/website/docs/integrate/index.md +38 -0
  133. data/website/docs/integrate/informative_callout.md +80 -0
  134. data/website/docs/integrate/javascript.md +84 -0
  135. data/website/docs/integrate/labels.md +91 -0
  136. data/website/docs/integrate/quickstart.md +77 -0
  137. data/website/docusaurus.config.ts +100 -0
  138. data/website/package.json +31 -0
  139. data/website/sidebars.ts +7 -0
  140. data/website/src/css/custom.css +37 -0
  141. data/website/static/img/logo.svg +1 -0
  142. data/website/static/img/schema_overview.png +0 -0
  143. data/website/static/img/screenshot_informative_callouts.png +0 -0
  144. data/website/static/img/screenshot_saved_flash.png +0 -0
  145. data/website/static/img/screenshots_locale_tab.png +0 -0
  146. data/website/static/img/screenshots_name_tab.png +0 -0
  147. data/website/static/img/screenshots_security_tab.png +0 -0
  148. data/website/tsconfig.json +6 -0
  149. data/website/yarn.lock +8336 -0
  150. data/yarn.lock +13 -0
  151. metadata +249 -0
@@ -0,0 +1,169 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ module Decidim
6
+ module Toggle
7
+ describe JavascriptConfig do
8
+ let(:organization) { create(:organization) }
9
+ let(:registry_name) { :javascript_config_spec }
10
+ let(:registry) { SettingsTabRegistry.find(registry_name) }
11
+
12
+ def self.ensure_registry!
13
+ @registry_ready ||= begin
14
+ geo_form_class = Class.new(Decidim::Form) do
15
+ include ExposeAttributesToJs
16
+ include ModuleConfigForm
17
+
18
+ self.module_config_name = "decidim_geo"
19
+
20
+ mimic :organization
21
+ attribute :enabled, :boolean
22
+ attribute :search_bar, :boolean
23
+ attribute :tags, [String]
24
+ attribute :secret, :string
25
+
26
+ expose_to_javascript :enabled, :search_bar, :tags
27
+ end
28
+
29
+ other_form_class = Class.new(Decidim::Form) do
30
+ include ExposeAttributesToJs
31
+ include ModuleConfigForm
32
+
33
+ self.module_config_name = "decidim_other"
34
+
35
+ mimic :organization
36
+ attribute :mode, :string
37
+ expose_to_javascript :mode
38
+ end
39
+
40
+ test_registry = SettingsTabRegistry.create(:javascript_config_spec)
41
+ test_registry.register_form_tab(:geo, geo_form_class, Integer, module_name: :decidim_geo)
42
+ test_registry.register_form_tab(:other, other_form_class, Integer, module_name: :decidim_other)
43
+ test_registry.mark_configurations_applied!
44
+ true
45
+ end
46
+ end
47
+
48
+ def self.reset_registry!
49
+ SettingsTabRegistry.destroy(:javascript_config_spec) if SettingsTabRegistry.find(:javascript_config_spec)
50
+ remove_instance_variable(:@registry_ready) if instance_variable_defined?(:@registry_ready)
51
+ ensure_registry!
52
+ end
53
+
54
+ before do
55
+ self.class.reset_registry!
56
+ end
57
+
58
+ # rubocop:disable RSpec/BeforeAfterAll -- isolated registry for this file
59
+ after(:context) do
60
+ SettingsTabRegistry.destroy(:javascript_config_spec)
61
+ end
62
+ # rubocop:enable RSpec/BeforeAfterAll
63
+
64
+ it "returns an empty hash without organization" do
65
+ expect(described_class.for(nil, registry_name:)).to eq({})
66
+ end
67
+
68
+ it "builds flat dot-notation keys from exposed attributes" do
69
+ Decidim::Toggle.save_config!(
70
+ organization,
71
+ :decidim_geo,
72
+ { "enabled" => true, "search_bar" => false, "tags" => %w(a b), "secret" => "hidden" }
73
+ )
74
+ Decidim::Toggle.save_config!(organization, :decidim_other, { "mode" => "live" })
75
+
76
+ expect(described_class.for(organization, registry_name:)).to eq(
77
+ "decidim_geo.enabled" => true,
78
+ "decidim_geo.search_bar" => false,
79
+ "decidim_geo.tags" => %w(a b),
80
+ "decidim_other.mode" => "live"
81
+ )
82
+ end
83
+
84
+ it "skips forms without ExposeAttributesToJs" do
85
+ plain_form = Class.new(Decidim::Form) do
86
+ include ModuleConfigForm
87
+
88
+ self.module_config_name = "decidim_plain"
89
+ mimic :organization
90
+ attribute :enabled, :boolean
91
+ end
92
+
93
+ registry.register_form_tab(:plain, plain_form, Integer, module_name: :decidim_plain)
94
+ Decidim::Toggle.save_config!(organization, :decidim_plain, { "enabled" => true })
95
+
96
+ expect(described_class.for(organization, registry_name:)).not_to have_key("decidim_plain.enabled")
97
+ end
98
+
99
+ it "skips unsupported scalar types with a warning in test" do
100
+ expect(Rails.logger).to receive(:warn).with(/Skipping unsupported JavaScript config value/)
101
+
102
+ expect(described_class.send(:serialize_value, Object.new, nil)).to be_nil
103
+ end
104
+
105
+ it "stringifies nested hashes and arrays inside hash values" do
106
+ nested_form_class = Class.new(Decidim::Form) do
107
+ include ExposeAttributesToJs
108
+ include ModuleConfigForm
109
+
110
+ self.module_config_name = "decidim_nested"
111
+
112
+ mimic :organization
113
+ attribute :settings, Object
114
+ expose_to_javascript :settings
115
+ end
116
+
117
+ registry.register_form_tab(:nested, nested_form_class, Integer, module_name: :decidim_nested)
118
+ Decidim::Toggle.save_config!(
119
+ organization,
120
+ :decidim_nested,
121
+ { "settings" => { labels: %w(a b), meta: { "count" => 2 } } }
122
+ )
123
+
124
+ expect(described_class.for(organization, registry_name:)).to include(
125
+ "decidim_nested.settings" => { "labels" => %w(a b), "meta" => { "count" => 2 } }
126
+ )
127
+ end
128
+
129
+ context "with translatable attributes" do
130
+ def self.ensure_translatable_form!
131
+ @translatable_ready ||= begin
132
+ translatable_form_class = Class.new(Decidim::Form) do
133
+ include Decidim::TranslatableAttributes
134
+ include ExposeAttributesToJs
135
+ include ModuleConfigForm
136
+
137
+ self.module_config_name = "decidim_i18n"
138
+
139
+ mimic :organization
140
+ translatable_attribute :title, String
141
+ expose_to_javascript :title
142
+ end
143
+
144
+ SettingsTabRegistry.find(:javascript_config_spec).register_form_tab(
145
+ :i18n, translatable_form_class, Integer, module_name: :decidim_i18n
146
+ )
147
+ true
148
+ end
149
+ end
150
+
151
+ before do
152
+ self.class.ensure_translatable_form!
153
+ Decidim::Toggle.save_config!(
154
+ organization,
155
+ :decidim_i18n,
156
+ { "title" => { "en" => "Hello", "ca" => "Hola" } }
157
+ )
158
+ end
159
+
160
+ it "exposes the raw locale hash" do
161
+ expect(described_class.for(organization, registry_name:)["decidim_i18n.title"]).to eq(
162
+ "en" => "Hello",
163
+ "ca" => "Hola"
164
+ )
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ module Decidim
6
+ module Toggle
7
+ describe ModuleConfigForm do
8
+ let(:form_class) do
9
+ Class.new(Decidim::Form) do
10
+ include ModuleConfigForm
11
+
12
+ self.module_config_name = "decidim_demo"
13
+
14
+ mimic :organization
15
+
16
+ attribute :enabled, :boolean
17
+ end
18
+ end
19
+
20
+ describe ".human_attribute_name" do
21
+ it "uses decidim_toggle.system.<module_config_name> when present" do
22
+ I18n.with_locale(:en) do
23
+ I18n.backend.store_translations(:en, {
24
+ decidim_toggle: {
25
+ system: {
26
+ decidim_demo: {
27
+ enabled: "Demo enabled label"
28
+ }
29
+ }
30
+ }
31
+ })
32
+
33
+ expect(form_class.human_attribute_name(:enabled)).to eq("Demo enabled label")
34
+ end
35
+ end
36
+
37
+ it "falls back when the module key is missing" do
38
+ I18n.with_locale(:en) do
39
+ expect(form_class.human_attribute_name(:missing_attr)).to eq("Missing attr")
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ module Decidim
6
+ module Toggle
7
+ describe Decidim::Toggle do
8
+ let(:organization) { create(:organization) }
9
+
10
+ after do
11
+ SettingsTabRegistry.destroy(:module_config_spec)
12
+ end
13
+
14
+ describe ".save_config!" do
15
+ it "creates a row and shallow-merges by default" do
16
+ described_class.save_config!(organization, :decidim_geo, { "enabled" => true })
17
+ described_class.save_config!(organization, :decidim_geo, { "foo" => "bar" })
18
+
19
+ row = OrganizationModuleConfig.find_by!(decidim_organization_id: organization.id, module_name: "decidim_geo")
20
+ expect(row.config).to eq("enabled" => true, "foo" => "bar")
21
+ end
22
+
23
+ it "replaces config when merge: false" do
24
+ described_class.save_config!(organization, :decidim_geo, { "a" => 1 })
25
+ described_class.save_config!(organization, :decidim_geo, { "b" => 2 }, merge: false)
26
+
27
+ row = OrganizationModuleConfig.find_by!(decidim_organization_id: organization.id, module_name: "decidim_geo")
28
+ expect(row.config).to eq("b" => 2)
29
+ end
30
+ end
31
+
32
+ describe ".config_for" do
33
+ let(:demo_form_class) do
34
+ Class.new(Decidim::Form) do
35
+ mimic :organization
36
+ attribute :enabled, :boolean
37
+ end
38
+ end
39
+
40
+ it "returns indifferent hash when no form is registered for the module" do
41
+ described_class.save_config!(organization, :unknown_mod, { "x" => 1 })
42
+ result = described_class.config_for(organization, :unknown_mod, registry_name: :module_config_spec)
43
+ expect(result[:x]).to eq(1)
44
+ expect(result["x"]).to eq(1)
45
+ end
46
+
47
+ it "returns a normalized hash when a tab registered module_name with a form" do
48
+ form_class = demo_form_class
49
+ SettingsTabRegistry.register(:module_config_spec) do |tabs|
50
+ tabs.add_tab :geo, "Geo", form: form_class, command: Integer, module_name: :decidim_geo
51
+ end
52
+
53
+ described_class.save_config!(organization, :decidim_geo, { "enabled" => true })
54
+
55
+ result = described_class.config_for(organization, :decidim_geo, registry_name: :module_config_spec)
56
+ expect(result).to be_a(ActiveSupport::HashWithIndifferentAccess)
57
+ expect(result[:enabled]).to be(true)
58
+ end
59
+
60
+ it "normalizes nil boolean to false when a form is registered" do
61
+ form_class = demo_form_class
62
+ SettingsTabRegistry.register(:module_config_spec) do |tabs|
63
+ tabs.add_tab :geo, "Geo", form: form_class, command: Integer, module_name: :decidim_geo
64
+ end
65
+
66
+ described_class.save_config!(organization, :decidim_geo, {})
67
+
68
+ result = described_class.config_for(organization, :decidim_geo, registry_name: :module_config_spec)
69
+ expect(result[:enabled]).to be(false)
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ module Decidim
6
+ module Toggle
7
+ describe ModuleConfigurationPresenter do
8
+ let(:organization) { create(:organization) }
9
+
10
+ let(:demo_form_class) do
11
+ Class.new(Decidim::Form) do
12
+ mimic :organization
13
+
14
+ attribute :enabled, :boolean
15
+ attribute :tags, :array
16
+ attribute :meta, :hash
17
+ end
18
+ end
19
+
20
+ def build_presenter(attrs)
21
+ form = demo_form_class.from_params(organization: attrs).with_context(current_organization: organization)
22
+ described_class.new(form)
23
+ end
24
+
25
+ it "normalizes nil array to empty array" do
26
+ p = build_presenter(tags: nil)
27
+ expect(p.tags).to eq([])
28
+ end
29
+
30
+ it "normalizes nil hash to empty hash" do
31
+ p = build_presenter(meta: nil)
32
+ expect(p.meta).to eq({})
33
+ end
34
+
35
+ it "normalizes nil boolean to false and exposes predicate" do
36
+ p = build_presenter(enabled: nil)
37
+ expect(p.enabled).to be(false)
38
+ expect(p.enabled?).to be(false)
39
+ end
40
+
41
+ it "preserves true boolean" do
42
+ p = build_presenter(enabled: true)
43
+ expect(p.enabled).to be(true)
44
+ expect(p.enabled?).to be(true)
45
+ end
46
+
47
+ it "exports normalized attributes as a hash" do
48
+ p = build_presenter(enabled: nil, tags: nil, meta: nil)
49
+ expect(p.to_config_hash).to eq("enabled" => false, "tags" => [], "meta" => {})
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ module Decidim
6
+ module Toggle
7
+ describe SettingsFormBuilder do
8
+ include ActionView::TestCase::Behavior
9
+
10
+ it "renders fields_for_names subset" do
11
+ organization = create(:organization)
12
+ form = UpdateLocaleForm.from_model(organization)
13
+ template = ActionView::Base.with_empty_template_cache.new(ActionView::LookupContext.new([]), {}, nil)
14
+ builder = described_class.new(:organization, form, template, {})
15
+
16
+ html = builder.fields_for_names(:enable_machine_translations)
17
+ expect(html).to be_present
18
+ end
19
+
20
+ it "renders informative callouts with decidim announcement cell markup" do
21
+ form_class = Class.new(UpdateSecurityForm) do
22
+ include InformativeCallouts
23
+
24
+ info :spec_info_callout
25
+
26
+ def spec_info_callout
27
+ "Spec info callout"
28
+ end
29
+ end
30
+ form = form_class.from_params(organization: {})
31
+ template = ActionView::Base.with_empty_template_cache.new(ActionView::LookupContext.new([]), {}, nil)
32
+ allow(template).to receive(:cell) do |_name, message, options|
33
+ %(<div class="flash flex-col #{options[:callout_class]}">#{message}</div>).html_safe
34
+ end
35
+ builder = described_class.new(:organization, form, template, {})
36
+
37
+ html = builder.informative_callouts
38
+ expect(html).to include("Spec info callout")
39
+ expect(html).to include('class="flash flex-col info"')
40
+ expect(html).to include('class="decidim_toggle_informative_callout"')
41
+ end
42
+
43
+ it "renders i18n helptext under the field when present" do
44
+ organization = create(:organization)
45
+ form = UpdateSecurityForm.from_model(organization)
46
+ template = ActionView::Base.with_empty_template_cache.new(ActionView::LookupContext.new([]), {}, nil)
47
+ builder = described_class.new(:organization, form, template, {})
48
+
49
+ helptext = "Spec helptext for users_registration_mode"
50
+ model_key = form.class.model_name.i18n_key.to_s
51
+
52
+ I18n.with_locale(:en) do
53
+ I18n.backend.store_translations(:en, {
54
+ activemodel: {
55
+ attributes: {
56
+ "organization" => {
57
+ helptext: {
58
+ "users_registration_mode" => helptext
59
+ }
60
+ },
61
+ model_key => {
62
+ helptext: {
63
+ "users_registration_mode" => helptext
64
+ }
65
+ }
66
+ }
67
+ }
68
+ })
69
+
70
+ html = builder.fields_for_names(:users_registration_mode)
71
+ expect(html).to include(helptext)
72
+ end
73
+ end
74
+
75
+ it "disables fields when the form implements attribute_disabled?" do
76
+ form_class = Class.new(Decidim::Form) do
77
+ attribute :enabled, :boolean
78
+ attribute :locked, :boolean
79
+
80
+ def attribute_disabled?(attribute)
81
+ attribute == :locked
82
+ end
83
+ end
84
+ form = form_class.from_params(organization: { enabled: true, locked: false })
85
+ template = ActionView::Base.with_empty_template_cache.new(ActionView::LookupContext.new([]), {}, nil)
86
+ builder = described_class.new(:organization, form, template, {})
87
+
88
+ html = builder.all_fields
89
+ expect(html).to include('name="organization[enabled]"')
90
+ expect(html).not_to include('organization[enabled]" disabled')
91
+ expect(html).to include('name="organization[locked]" disabled="disabled"')
92
+ expect(html).to include('class="field is-disabled"')
93
+ end
94
+
95
+ it "renders boolean fields, text areas, and collection inputs" do
96
+ organization = create(:organization, secondary_hosts: %w(extra.example.org))
97
+ template = ActionView::Base.with_empty_template_cache.new(ActionView::LookupContext.new([]), {}, nil)
98
+
99
+ security_form = UpdateSecurityForm.from_model(organization)
100
+ security_builder = described_class.new(:organization, security_form, template, {})
101
+ security_html = security_builder.all_fields
102
+ expect(security_html).to include('type="checkbox"')
103
+ expect(security_builder.fields_for_names(:users_registration_mode)).to include('type="radio"')
104
+
105
+ name_form = UpdateNameForm.from_model(organization)
106
+ name_builder = described_class.new(:organization, name_form, template, {})
107
+ expect(name_builder.all_fields).to include("extra.example.org")
108
+
109
+ authorizations_form = UpdateAuthorizationsForm.from_model(organization)
110
+ authorizations_builder = described_class.new(:organization, authorizations_form, template, {})
111
+ expect(authorizations_builder.all_fields).to include('type="checkbox"')
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ describe "DecidimToggle::System::SettingsTabController" do
6
+ let(:organization) { create(:organization) }
7
+ let(:admin) { create(:admin) }
8
+
9
+ before { login_as admin, scope: :admin }
10
+
11
+ describe "PATCH /decidim_toggle/system/organizations/:organization_id/settings_tab/:tab_id" do
12
+ let(:path) { "/decidim_toggle/system/organizations/#{organization.id}/settings_tab/security" }
13
+ let(:valid_params) do
14
+ {
15
+ organization: {
16
+ force_users_to_authenticate_before_access_organization: "1",
17
+ users_registration_mode: "enabled"
18
+ }
19
+ }
20
+ end
21
+
22
+ it "updates the tab and redirects with notice" do
23
+ patch path, params: valid_params
24
+
25
+ expect(response).to redirect_to(decidim_system.edit_organization_path(organization))
26
+ expect(flash[:notice]).to eq(I18n.t("decidim_toggle.system.organizations.form_tab.success"))
27
+ expect(organization.reload.force_users_to_authenticate_before_access_organization).to be(true)
28
+ end
29
+
30
+ it "redirects with anchor when active tab param matches a registered tab" do
31
+ patch path, params: valid_params.merge(decidim_toggle_active_tab: "security")
32
+
33
+ expect(response).to redirect_to(
34
+ decidim_system.edit_organization_path(organization, anchor: "panel-toggle-security")
35
+ )
36
+ end
37
+
38
+ it "ignores unknown active tab ids when building the redirect anchor" do
39
+ patch path, params: valid_params.merge(decidim_toggle_active_tab: "not-a-tab")
40
+
41
+ expect(response).to redirect_to(decidim_system.edit_organization_path(organization))
42
+ end
43
+
44
+ it "returns not found for unknown tab ids" do
45
+ patch "/decidim_toggle/system/organizations/#{organization.id}/settings_tab/unknown"
46
+
47
+ expect(response).to have_http_status(:not_found)
48
+ end
49
+
50
+ it "stores invalid tab data in flash when the form is invalid" do
51
+ patch path, params: { organization: { users_registration_mode: "not_a_mode" } }
52
+
53
+ expect(response).to redirect_to(decidim_system.edit_organization_path(organization))
54
+ expect(flash[:decidim_toggle_invalid_settings_tab]).to include(
55
+ organization_id: organization.id,
56
+ tab_id: "security"
57
+ )
58
+ end
59
+
60
+ it "re-renders field errors after redirect" do
61
+ patch path, params: { organization: { users_registration_mode: "not_a_mode" } }
62
+ follow_redirect!
63
+
64
+ expect(response.body).to include("form-error")
65
+ end
66
+ end
67
+
68
+ describe "PATCH name tab" do
69
+ let(:path) { "/decidim_toggle/system/organizations/#{organization.id}/settings_tab/name" }
70
+ let(:valid_params) do
71
+ {
72
+ organization: {
73
+ name_en: "Renamed organization",
74
+ host: "renamed.example.org",
75
+ secondary_hosts: ""
76
+ }
77
+ }
78
+ end
79
+
80
+ it "updates host and redirects with notice" do
81
+ patch path, params: valid_params
82
+
83
+ expect(response).to redirect_to(decidim_system.edit_organization_path(organization))
84
+ expect(flash[:notice]).to eq(I18n.t("decidim_toggle.system.organizations.form_tab.success"))
85
+ organization.reload
86
+ expect(organization.host).to eq("renamed.example.org")
87
+ expect(organization.name["en"]).to eq("Renamed organization")
88
+ end
89
+
90
+ it "stores invalid tab data in flash when host is blank" do
91
+ patch path, params: { organization: { name_en: "", host: "", secondary_hosts: "" } }
92
+
93
+ expect(response).to redirect_to(decidim_system.edit_organization_path(organization))
94
+ expect(flash[:decidim_toggle_invalid_settings_tab]).to include(
95
+ organization_id: organization.id,
96
+ tab_id: "name"
97
+ )
98
+ end
99
+ end
100
+
101
+ describe "PATCH language tab" do
102
+ let(:path) { "/decidim_toggle/system/organizations/#{organization.id}/settings_tab/language" }
103
+ let(:organization) { create(:organization, available_locales: %w(en ca), default_locale: "ca") }
104
+
105
+ before do
106
+ allow(Rails.application).to receive(:load_tasks)
107
+ allow(Rake::Task).to receive(:[]).and_call_original
108
+ allow(Rake::Task).to receive(:[]).with("decidim:locales:rebuild_search").and_return(
109
+ instance_double(Rake::Task, invoke: nil, reenable: nil)
110
+ )
111
+ end
112
+
113
+ it "updates available locales and default locale" do
114
+ patch path, params: {
115
+ organization: {
116
+ available_locales: { "en" => "1" },
117
+ default_locale: "en",
118
+ enable_machine_translations: false,
119
+ machine_translation_display_priority: "original"
120
+ }
121
+ }
122
+
123
+ expect(response).to redirect_to(decidim_system.edit_organization_path(organization))
124
+ organization.reload
125
+ expect(organization.available_locales).to eq(%w(en))
126
+ expect(organization.default_locale).to eq("en")
127
+ end
128
+
129
+ it "stores invalid tab data when default locale is not enabled" do
130
+ patch path, params: {
131
+ organization: {
132
+ available_locales: { "en" => "1" },
133
+ default_locale: "ca"
134
+ }
135
+ }
136
+
137
+ expect(response).to redirect_to(decidim_system.edit_organization_path(organization))
138
+ expect(flash[:decidim_toggle_invalid_settings_tab]).to include(
139
+ organization_id: organization.id,
140
+ tab_id: "language"
141
+ )
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "decidim/dev"
4
+
5
+ ENV["ENGINE_ROOT"] = File.dirname(__dir__)
6
+
7
+ Decidim::Dev.dummy_app_path = File.expand_path(File.join(__dir__, "decidim_dummy_app"))
8
+
9
+ require "decidim/dev/test/map_server"
10
+ require "decidim/dev/test/base_spec_helper"
11
+
12
+ Dir[File.expand_path("support/**/*.rb", __dir__)].each { |f| require f }
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "decidim/system/test/factories"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.configure do |config|
4
+ config.include Devise::Test::IntegrationHelpers, type: :request
5
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ module Decidim
6
+ module Toggle
7
+ describe "JavaScript config injection" do
8
+ let(:organization) { create(:organization) }
9
+ let(:admin) { create(:user, :confirmed, :admin, organization:) }
10
+
11
+ def self.ensure_demo_tab!
12
+ @demo_tab_ready ||= begin
13
+ demo_form_class = Class.new(Decidim::Form) do
14
+ include ExposeAttributesToJs
15
+ include ModuleConfigForm
16
+
17
+ self.module_config_name = "decidim_toggle_demo"
18
+
19
+ mimic :organization
20
+ attribute :enabled, :boolean
21
+ expose_to_javascript :enabled
22
+ end
23
+
24
+ SettingsTabRegistry.find(:organization_settings).register_form_tab(
25
+ :toggle_demo_js,
26
+ demo_form_class,
27
+ UpdateModuleConfigCommand,
28
+ module_name: :decidim_toggle_demo
29
+ )
30
+ true
31
+ end
32
+ end
33
+
34
+ before do
35
+ self.class.ensure_demo_tab!
36
+ Decidim::Toggle.save_config!(organization, :decidim_toggle_demo, { "enabled" => true })
37
+ switch_to_host(organization.host)
38
+ end
39
+
40
+ it "exposes window.DecidimToggle on the public site" do
41
+ visit decidim.root_path
42
+
43
+ expect(page.body).to include("window.DecidimToggle")
44
+ expect(page.body).to include('"decidim_toggle_demo.enabled":true')
45
+ end
46
+
47
+ it "exposes window.DecidimToggle in the admin layout" do
48
+ login_as admin, scope: :user
49
+ visit decidim_admin.root_path
50
+
51
+ expect(page.body).to include("window.DecidimToggle")
52
+ expect(page.body).to include('"decidim_toggle_demo.enabled":true')
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,4 @@
1
+ node_modules/
2
+ build/
3
+ .docusaurus/
4
+ *.log