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,35 @@
1
+ <% organization = local_assigns[:organization] %>
2
+ <% tab = local_assigns[:tab] %>
3
+ <%= decidim_toggle_settings_tab_form(organization, tab) do |tf| %>
4
+ <% tab_form = tf.object %>
5
+ <fieldset>
6
+ <legend class="form-legend"><%= t("decidim_toggle.system.organizations.language_tab.legend") %></legend>
7
+ <table class="table-fields">
8
+ <thead>
9
+ <tr>
10
+ <th><%= t("decidim_toggle.system.organizations.language_tab.locale") %></th>
11
+ <th><%= t("decidim_toggle.system.organizations.language_tab.enabled") %></th>
12
+ <th><%= t("decidim_toggle.system.organizations.language_tab.default") %></th>
13
+ </tr>
14
+ </thead>
15
+ <tbody>
16
+ <% localized_locales(I18n.available_locales).each do |locale| %>
17
+ <tr>
18
+ <td><%= locale.name %></td>
19
+ <td><%= check_box_tag "organization[available_locales][#{locale.id}]", locale.id, Array(tab_form.available_locales).map(&:to_s).include?(locale.id.to_s), class: "!m-0" %></td>
20
+ <td><%= radio_button_tag "organization[default_locale]", locale.id, tab_form.default_locale.to_s == locale.id.to_s, class: "!m-0" %></td>
21
+ </tr>
22
+ <% end %>
23
+ </tbody>
24
+ </table>
25
+ </fieldset>
26
+ <fieldset>
27
+ <legend class="form-legend"><%= t("decidim_toggle.system.organizations.language_tab.machine_translations_legend") %></legend>
28
+ <%= tf.fields_for_names(:enable_machine_translations) %>
29
+
30
+ <p>
31
+ <label class="form-legend" for="organization[machine_translation_display_priority]"><%= t("decidim_toggle.system.organizations.language_tab.machine_translation_display_priority_hint") %></label>
32
+ <%= tf.fields_for_names(:machine_translation_display_priority) %>
33
+ </p>
34
+ </fieldset>
35
+ <% end %>
@@ -0,0 +1,5 @@
1
+ <% organization = local_assigns[:organization] %>
2
+ <% tab = local_assigns[:tab] %>
3
+ <%= decidim_toggle_settings_tab_form(organization, tab) do |tf| %>
4
+ <%= render "decidim/system/organizations/omniauth_settings", f: tf %>
5
+ <% end %>
@@ -0,0 +1,20 @@
1
+ <% organization = local_assigns[:organization] %>
2
+ <% tab = local_assigns[:tab] %>
3
+ <%= decidim_toggle_settings_tab_form(organization, tab) do |tf| %>
4
+ <fieldset>
5
+ <legend class="form-legend"><%= t("decidim_toggle.system.organizations.security_tab.access_legend") %></legend>
6
+
7
+ <p>
8
+ <%= tf.check_box :force_users_to_authenticate_before_access_organization %>
9
+ </p>
10
+ <p class="mt-6">
11
+ <label class="form-legend"><%= t("decidim_toggle.system.organizations.security_tab.sign_in_mode") %></label>
12
+ <%= tf.fields_for_names(:users_registration_mode) %>
13
+ </p>
14
+ </fieldset>
15
+
16
+ <fieldset>
17
+ <legend class="form-legend"><%= t("decidim_toggle.system.organizations.security_tab.csp_legend") %></legend>
18
+ <%= tf.fields_for_names("default-src", "img-src", "media-src", "script-src", "style-src", "frame-src", "font-src", "connect-src") %>
19
+ </fieldset>
20
+ <% end %>
@@ -0,0 +1,3 @@
1
+ <script nonce="<%= content_security_policy_nonce %>">
2
+ window.DecidimToggle = <%== decidim_toggle_javascript_config.to_json %>;
3
+ </script>
data/bin/check ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env bash
2
+ # RuboCop + erblint + RSpec (matches CI ruby::rubocop, ruby::erb, ruby::rspec).
3
+ # Does not run Prettier or Crowdin — see README / CONTRIBUTING "bin/check vs CI".
4
+ set -euo pipefail
5
+ cd "$(dirname "$0")/.."
6
+ unset DATABASE_URL
7
+ export RAILS_ENV=test
8
+ bundle exec rubocop .
9
+ bundle exec erblint --lint-all --enable-all-linters
10
+ bundle exec rspec "$@"
data/bin/i18n-tasks ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'i18n-tasks' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
12
+
13
+ bundle_binstub = File.expand_path("bundle", __dir__)
14
+
15
+ if File.file?(bundle_binstub)
16
+ if File.read(bundle_binstub, 300).include?("This file was generated by Bundler")
17
+ load(bundle_binstub)
18
+ else
19
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
20
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
21
+ end
22
+ end
23
+
24
+ require "rubygems"
25
+ require "bundler/setup"
26
+
27
+ load Gem.bin_path("i18n-tasks", "i18n-tasks")
data/bin/postversion ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Syncs lib/decidim/toggle/version.rb with package.json version (single source: npm version).
4
+ #
5
+ set -euo pipefail
6
+
7
+ PACKAGE_VERSION=$(jq -r '.version' package.json)
8
+
9
+ file="lib/decidim/toggle/version.rb"
10
+ if [[ -f "$file" ]]; then
11
+ sed -i "/# DO NOT UPDATE MANUALLY/s/\".*\"/\"${PACKAGE_VERSION}\"/" "$file"
12
+ fi
13
+
14
+ echo "$PACKAGE_VERSION"
data/config/assets.rb ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ base_path = File.expand_path("..", __dir__)
4
+
5
+ Decidim::Webpacker.register_path("#{base_path}/app/packs")
6
+ Decidim::Webpacker.register_entrypoints(
7
+ decidim_toggle: "#{base_path}/app/packs/entrypoints/decidim_toggle.js"
8
+ )
@@ -0,0 +1,58 @@
1
+ en:
2
+ # Field labels for ModuleConfigForm tabs live under decidim_toggle.system.<module_config_name>
3
+ # in each integrating gem (e.g. decidim_geo, spaces). This file holds built-in org settings UI only.
4
+ decidim_toggle:
5
+ system:
6
+ organizations:
7
+ invitation_pending_title: "Invitation pending"
8
+ invitation_pending_body: "The first administrator of the organization still did not accept the invitation"
9
+ encryption_not_configured: "Error: Please configure SECRET_KEY_BASE or master.key to edit this organization."
10
+ encryption_not_configured_title: "Encryption not configured"
11
+ settings_tabs:
12
+ name: "Name"
13
+ omniauth: "OmniAuth"
14
+ emails: "Emails"
15
+ language: "Language"
16
+ security: "Security"
17
+ authorizations: "Authorizations"
18
+ file_upload: "File upload"
19
+ language_placeholder: "Default and available locales (configured in the form above)."
20
+ form_tab:
21
+ save: "Save"
22
+ cancel: "Cancel"
23
+ success: "Settings updated successfully."
24
+ authorizations_tab:
25
+ legend: "Verification workflows"
26
+ hint: "Choose which verification methods participants can use in this organization."
27
+ language_tab:
28
+ legend: "Available and default locale"
29
+ locale: "Locale"
30
+ enabled: "Enabled"
31
+ default: "Default"
32
+ save: "Save locales"
33
+ success: "Locales updated successfully."
34
+ error: "Could not update locales."
35
+ machine_translations_legend: "Machine translations"
36
+ machine_translation_display_priority_hint: "Text version participant will see first"
37
+ tabs:
38
+ language_tab:
39
+ machine_translations_legend: "Machine translations"
40
+ machine_translation_display_priority_hint: "Text version participant will see first"
41
+ security_tab:
42
+ access_legend: "Access to the organization"
43
+ force_auth_label: "Force users to authenticate before accessing the organization"
44
+ sign_in_mode: "Sign in mode"
45
+ csp_legend: "Content security policy"
46
+
47
+ activemodel:
48
+ attributes:
49
+ organization:
50
+ helptext:
51
+ "default-src": "Fallback directive used when no other *-src directive matches. Mostly used to set a secure baseline for all resource types. Examples: 'self' https: ; 'none' ; 'self' https: data:"
52
+ "img-src": "Sources allowed to load images (e.g. <img>, favicons, CSS background images). Use for image CDNs or when you need data/blob URIs. Examples: 'self' https: data: blob: ; https://*.fbcdn.net https://*.instagram.com"
53
+ "media-src": "Sources allowed to load audio/video (e.g. <video>, <audio>). Use for media hosting/CDNs and blob URLs from uploads. Examples: 'self' https: blob: ; https://media.example.com"
54
+ "script-src": "Sources allowed to execute scripts (e.g. <script src>, imports, inline scripts when enabled). Use for third-party scripts such as Matomo. Examples: 'self' https://*.matomo.cloud ; 'self' 'nonce-<nonce>'"
55
+ "style-src": "Sources allowed to load CSS (e.g. <link rel='stylesheet'> and style attributes). Use for CSS from CDNs and, if needed, inline styles. Examples: 'self' https: 'unsafe-inline' ; https://fonts.googleapis.com https://fonts.gstatic.com"
56
+ "frame-src": "Sources allowed to embed iframes/frames (including embedded players). Use for embeds such as YouTube and Google Scholar. Examples: 'self' https://www.youtube.com https://scholar.google.com ; https://*.trusted-partner.com"
57
+ "font-src": "Sources allowed to load fonts (e.g. @font-face). Use for external font providers such as Google Fonts. Examples: 'self' https: data: ; https://fonts.gstatic.com"
58
+ "connect-src": "Sources allowed to make network connections from the page (fetch/XHR, EventSource, WebSocket). Use for endpoints such as Matomo. Examples: 'self' https: wss: ; https://*.matomo.cloud wss://realtime.example.com ; blob:"
data/crowdin.yml ADDED
@@ -0,0 +1,15 @@
1
+ # Crowdin — set api_token / project_id via CI or local env as needed.
2
+ #
3
+ base_path: .
4
+ base_url: https://api.crowdin.com
5
+ api_token: "insecure_api_token"
6
+ project_id: "project_id"
7
+
8
+ preserve_hierarchy: true
9
+
10
+ files: [
11
+ {
12
+ "source": "/config/locales/en.yml",
13
+ "translation": "/config/locales/decidim_toggle_%two_letters_code%.yml",
14
+ }
15
+ ]
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateDecidimToggleOrganizationModuleConfigs < ActiveRecord::Migration[7.0]
4
+ def change
5
+ create_table :decidim_toggle_organization_module_configs do |t|
6
+ t.belongs_to :decidim_organization,
7
+ null: false,
8
+ foreign_key: { to_table: :decidim_organizations, on_delete: :cascade },
9
+ index: { name: "idx_dtoggle_omc_on_org" }
10
+ t.string :module_name, null: false
11
+ t.jsonb :config, null: false, default: {}
12
+ t.timestamps
13
+ end
14
+
15
+ add_index :decidim_toggle_organization_module_configs,
16
+ [:decidim_organization_id, :module_name],
17
+ unique: true,
18
+ name: "idx_dtoggle_org_module_configs_unique"
19
+ end
20
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.push File.expand_path("lib", __dir__)
4
+
5
+ require "decidim/toggle/version"
6
+
7
+ Gem::Specification.new do |s|
8
+ s.version = Decidim::Toggle.version
9
+ s.authors = ["hadrien@octree.ch"]
10
+ s.email = ["hadrien@octree.ch"]
11
+ s.license = "AGPL-3.0"
12
+ s.homepage = "https://octree-gva.github.io/decidim-toggle"
13
+ s.required_ruby_version = ">= 3.2"
14
+ s.name = "decidim-toggle"
15
+ s.summary = "Feature toggle for Decidim system administration"
16
+ s.description = "Tabbed System organization settings (OmniAuth, emails, language, security, other) " \
17
+ "with an API for other modules to add tabs. Overrides decidim-system organization views via prepended paths."
18
+
19
+ s.files = Dir.chdir(__dir__) do
20
+ tracked = `git ls-files -z 2>/dev/null`.split("\x0").reject(&:empty?)
21
+ if tracked.any?
22
+ tracked
23
+ else
24
+ Dir["{app,config,lib}/**/*", "Rakefile", "README.md", "CONTRIBUTING.md"].reject { |f| f.match(%r{^(spec|decidim_dummy_app)/}) }
25
+ end
26
+ end
27
+
28
+ s.require_paths = ["lib"]
29
+
30
+ s.add_dependency "decidim-core", Decidim::Toggle.decidim_version
31
+ s.add_dependency "decidim-system", Decidim::Toggle.decidim_version
32
+ s.add_dependency "deface", "~> 1.9.0"
33
+
34
+ s.metadata["rubygems_mfa_required"] = "true"
35
+ end
@@ -0,0 +1,41 @@
1
+ services:
2
+ toggle:
3
+ command: bash -c "sleep infinity"
4
+ image: octree/decidim-dev:0.29
5
+ entrypoint: ""
6
+ working_dir: /home/module
7
+ volumes:
8
+ - .:/home/module
9
+ - bundle:/home/decidim/vendor
10
+ - node_modules:/home/decidim/node_modules
11
+ environment:
12
+ - PORT=3000
13
+ - DATABASE_URL=postgresql://decidim:pleaseChangeMe@toggle_db:5432/decidim
14
+ - DATABASE_HOST=toggle_db
15
+ - DATABASE_USERNAME=decidim
16
+ - DATABASE_PASSWORD=pleaseChangeMe
17
+ - DEV_MODULE=decidim-toggle
18
+ - RAILS_ENV=development
19
+ - DISABLED_DOCKER_COMPOSE=true
20
+ - DECIDIM_FORCE_SSL=false
21
+ - RAILS_FORCE_SSL=false
22
+ depends_on:
23
+ - toggle_db
24
+ ports:
25
+ - 3000:3000
26
+ links:
27
+ - toggle_db
28
+
29
+ toggle_db:
30
+ image: postgres:17
31
+ volumes:
32
+ - toggle-pg-data:/var/lib/postgresql/data
33
+ environment:
34
+ - POSTGRES_DATABASE=decidim
35
+ - POSTGRES_USER=decidim
36
+ - POSTGRES_PASSWORD=pleaseChangeMe
37
+
38
+ volumes:
39
+ node_modules: {}
40
+ bundle: {}
41
+ toggle-pg-data: {}
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails"
4
+ require "deface"
5
+ require "decidim/core"
6
+ require "decidim/system"
7
+ # After core so Decidim::FormBuilder autoload pulls Map, TranslatableAttributes, etc.
8
+ require "decidim/toggle/settings_form_builder"
9
+ require "decidim/toggle/organization_settings_tabs"
10
+ require "decidim/toggle/expose_attributes_to_js_validator"
11
+
12
+ module Decidim
13
+ module Toggle
14
+ class Engine < ::Rails::Engine
15
+ isolate_namespace Decidim::Toggle
16
+
17
+ routes do
18
+ scope path: "system" do
19
+ patch "organizations/:organization_id/settings_tab/:tab_id",
20
+ controller: "/decidim_toggle/system/settings_tab",
21
+ action: :update,
22
+ as: :update_settings_tab_organization
23
+ end
24
+ end
25
+
26
+ initializer "decidim_toggle.ignore_deface_overrides_in_zeitwerk" do
27
+ overrides_path = root.join("app/overrides").to_s
28
+ Rails.autoloaders.main.ignore(overrides_path) if defined?(Rails.autoloaders) && Dir.exist?(overrides_path)
29
+ end
30
+
31
+ initializer "decidim_toggle.mount_routes" do
32
+ Rails.application.routes.append do
33
+ mount Decidim::Toggle::Engine, at: "/decidim_toggle", as: "decidim_toggle"
34
+ end
35
+ end
36
+
37
+ initializer "decidim_toggle.organization_settings_tabs" do
38
+ Decidim::Toggle::OrganizationSettingsTabs.register!
39
+ end
40
+
41
+ initializer "decidim_toggle.webpacker.assets_path" do
42
+ Decidim.register_assets_path File.expand_path("app/packs", root)
43
+ end
44
+
45
+ # So System::OrganizationsController resolves this before decidim-system's partial.
46
+ initializer "decidim_toggle.prepend_views" do
47
+ ActiveSupport.on_load(:action_controller) do
48
+ prepend_view_path Decidim::Toggle::Engine.root.join("app/views")
49
+ end
50
+ end
51
+
52
+ config.to_prepare do
53
+ Decidim::Toggle::ExposeAttributesToJsValidator.validate! if Rails.env.development? || Rails.env.test?
54
+
55
+ ActiveSupport.on_load(:action_view) do
56
+ include Decidim::Toggle::SystemSettingsTabHelper
57
+ include Decidim::Toggle::JavascriptConfigHelper
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Toggle
5
+ # Opt-in DSL to expose module config form attributes to +window.DecidimToggle+.
6
+ # To add this in your form, you need to add the following:
7
+ # ```ruby
8
+ # include Decidim::Toggle::ExposeAttributesToJs
9
+ # expose_to_javascript :enabled, :search_bar
10
+ # ```
11
+ module ExposeAttributesToJs
12
+ extend ActiveSupport::Concern
13
+
14
+ class_methods do
15
+ def expose_to_javascript(*names)
16
+ javascript_exposed_attribute_names.concat(names.map(&:to_s))
17
+ javascript_exposed_attribute_names.uniq!
18
+ end
19
+
20
+ def javascript_exposed_attribute_names
21
+ @javascript_exposed_attribute_names ||= []
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Toggle
5
+ module ExposeAttributesToJsValidator
6
+ module_function
7
+
8
+ def validate!
9
+ registry = SettingsTabRegistry.find(:organization_settings)
10
+ return unless registry
11
+
12
+ registry.ensure_configurations_applied!
13
+ registry.module_configs.each_value do |entry|
14
+ validate_form!(entry[:form])
15
+ end
16
+ end
17
+
18
+ def validate_form!(form_class)
19
+ return unless form_class
20
+ return unless form_class.included_modules.include?(ExposeAttributesToJs)
21
+
22
+ form_class.javascript_exposed_attribute_names.each do |attr|
23
+ next if form_class.attribute_names.include?(attr)
24
+
25
+ Rails.logger.warn(
26
+ "[decidim-toggle] #{form_class} exposes unknown attribute #{attr.inspect} to JS"
27
+ )
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Toggle
5
+ # Detects whether a Ruby gem is loaded in the current Bundler bundle.
6
+ class GemRegistry
7
+ def self.present?(gem_name)
8
+ name = gem_name.to_s
9
+ return false if name.blank?
10
+
11
+ Bundler.load.specs.any? { |spec| spec.name == name }
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Toggle
5
+ # Declare form-level informative callouts (info, warning, danger) on toggle settings forms.
6
+ #
7
+ # class MyForm < Decidim::Form
8
+ # include Decidim::Toggle::InformativeCallouts
9
+ #
10
+ # info :management_callout_html
11
+ # warning :conditional_callout, if_predicate: ->(form) { form.some_flag? }
12
+ #
13
+ # def management_callout_html
14
+ # I18n.t("...")
15
+ # end
16
+ # end
17
+ #
18
+ # +message+ is always a Symbol naming an instance method on the form.
19
+ # Messages are rendered as HTML (sanitized by the announcement cell), wrapped in
20
+ # +decidim_toggle_informative_callout+ for system typography styles.
21
+ module InformativeCallouts
22
+ extend ActiveSupport::Concern
23
+
24
+ WRAPPER_CLASS = "decidim_toggle_informative_callout"
25
+ CALLOUT_TYPES = [:info, :warning, :danger].freeze
26
+
27
+ class InformativeEntry
28
+ attr_reader :type, :message, :if_predicate
29
+
30
+ def initialize(type:, message:, if_predicate: nil)
31
+ @type = type
32
+ @message = message
33
+ @if_predicate = if_predicate
34
+ end
35
+
36
+ def visible?(form)
37
+ return true if if_predicate.nil?
38
+
39
+ if_predicate.call(form)
40
+ end
41
+
42
+ def message_for(form)
43
+ form.public_send(message)
44
+ end
45
+ end
46
+
47
+ class_methods do
48
+ def informative_callouts
49
+ @informative_callouts ||= []
50
+ end
51
+
52
+ def info(message, if_predicate: nil)
53
+ register_informative(:info, message, if_predicate:)
54
+ end
55
+
56
+ def warning(message, if_predicate: nil)
57
+ register_informative(:warning, message, if_predicate:)
58
+ end
59
+
60
+ def danger(message, if_predicate: nil)
61
+ register_informative(:danger, message, if_predicate:)
62
+ end
63
+
64
+ private
65
+
66
+ def register_informative(type, message, if_predicate: nil)
67
+ informative_callouts << InformativeEntry.new(type:, message:, if_predicate:)
68
+ end
69
+ end
70
+
71
+ def visible_informative_callouts
72
+ self.class.informative_callouts.select { |entry| entry.visible?(self) }
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Toggle
5
+ class JavascriptConfig
6
+ SUPPORTED_VALUE_CLASSES = [TrueClass, FalseClass, String, Integer, Float, NilClass].freeze
7
+
8
+ class << self
9
+ def for(organization, registry_name: :organization_settings)
10
+ return {} if organization.blank?
11
+
12
+ registry = SettingsTabRegistry.find(registry_name)
13
+ return {} unless registry
14
+
15
+ registry.ensure_configurations_applied!
16
+ build_from_registry(organization, registry)
17
+ end
18
+
19
+ private
20
+
21
+ def build_from_registry(organization, registry)
22
+ registry.module_configs.each_with_object({}) do |(module_name, entry), flat_config|
23
+ form_class = entry[:form]
24
+ next unless form_class&.included_modules&.include?(ExposeAttributesToJs)
25
+
26
+ exposed = form_class.javascript_exposed_attribute_names
27
+ next if exposed.empty?
28
+
29
+ module_config = Decidim::Toggle.config_for(organization, module_name, registry_name: registry.registry_name)
30
+ merge_exposed_attributes!(flat_config, module_name, module_config, form_class, exposed)
31
+ end
32
+ end
33
+
34
+ def merge_exposed_attributes!(flat_config, module_name, module_config, form_class, exposed)
35
+ exposed.each do |attr|
36
+ type = form_class.attribute_types[attr]
37
+ raw = module_config[attr]
38
+ value = serialize_value(raw, type)
39
+ next if value.nil? && !supported_scalar?(raw)
40
+
41
+ flat_config["#{module_name}.#{attr}"] = value
42
+ end
43
+ end
44
+
45
+ def serialize_value(value, _type)
46
+ return value if supported_scalar?(value)
47
+ return value.map { |element| serialize_element(element) } if value.is_a?(Array)
48
+ return stringify_hash(value) if value.is_a?(Hash)
49
+
50
+ warn_unsupported_type(value)
51
+ nil
52
+ end
53
+
54
+ def supported_scalar?(value)
55
+ SUPPORTED_VALUE_CLASSES.any? { |klass| value.is_a?(klass) }
56
+ end
57
+
58
+ def serialize_element(element)
59
+ return stringify_hash(element) if element.is_a?(Hash)
60
+
61
+ element
62
+ end
63
+
64
+ def stringify_hash(hash)
65
+ hash.to_h.transform_keys(&:to_s).transform_values do |value|
66
+ case value
67
+ when Hash
68
+ stringify_hash(value)
69
+ when Array
70
+ value.map { |element| serialize_element(element) }
71
+ else
72
+ value
73
+ end
74
+ end
75
+ end
76
+
77
+ def warn_unsupported_type(value)
78
+ return unless Rails.env.development? || Rails.env.test?
79
+
80
+ Rails.logger.warn(
81
+ "[decidim-toggle] Skipping unsupported JavaScript config value #{value.class}"
82
+ )
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end