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.
- checksums.yaml +7 -0
- data/.erb-lint.yml +2134 -0
- data/.github/workflows/website.yml +57 -0
- data/.gitignore +13 -0
- data/.gitlab-ci.yml +165 -0
- data/.node-version +1 -0
- data/.rspec +1 -0
- data/.rubocop.yml +2 -0
- data/.ruby-version +1 -0
- data/.simplecov +18 -0
- data/CONTRIBUTING.md +17 -0
- data/Gemfile +33 -0
- data/Gemfile.lock +843 -0
- data/LICENSE.md +661 -0
- data/README.md +90 -0
- data/Rakefile +38 -0
- data/app/commands/decidim/toggle/update_authorizations_command.rb +31 -0
- data/app/commands/decidim/toggle/update_emails_command.rb +30 -0
- data/app/commands/decidim/toggle/update_file_upload_settings_command.rb +31 -0
- data/app/commands/decidim/toggle/update_locale_command.rb +47 -0
- data/app/commands/decidim/toggle/update_module_config_command.rb +31 -0
- data/app/commands/decidim/toggle/update_name_command.rb +31 -0
- data/app/commands/decidim/toggle/update_omniauth_command.rb +30 -0
- data/app/commands/decidim/toggle/update_security_command.rb +31 -0
- data/app/controllers/decidim_toggle/system/settings_tab_controller.rb +64 -0
- data/app/forms/decidim/toggle/update_authorizations_form.rb +54 -0
- data/app/forms/decidim/toggle/update_emails_form.rb +28 -0
- data/app/forms/decidim/toggle/update_file_upload_settings_form.rb +26 -0
- data/app/forms/decidim/toggle/update_locale_form.rb +116 -0
- data/app/forms/decidim/toggle/update_name_form.rb +63 -0
- data/app/forms/decidim/toggle/update_omniauth_form.rb +37 -0
- data/app/forms/decidim/toggle/update_security_form.rb +65 -0
- data/app/helpers/decidim/toggle/javascript_config_helper.rb +11 -0
- data/app/helpers/decidim/toggle/system_settings_tab_helper.rb +59 -0
- data/app/models/decidim/toggle/organization_module_config.rb +15 -0
- data/app/overrides/add_toggle_javascript_admin.rb +11 -0
- data/app/overrides/add_toggle_javascript_public.rb +11 -0
- data/app/packs/entrypoints/decidim_toggle.js +3 -0
- data/app/packs/src/decidim/toggle/organization_settings_tabs.js +114 -0
- data/app/packs/stylesheets/decidim/toggle/organization_settings.scss +160 -0
- data/app/views/decidim/system/organizations/edit.html.erb +20 -0
- data/app/views/decidim_toggle/system/organizations/_default_form_tab.html.erb +5 -0
- data/app/views/decidim_toggle/system/organizations/_encryption_not_configured_callout.html.erb +6 -0
- data/app/views/decidim_toggle/system/organizations/_settings_tab_active_tab_field.html.erb +1 -0
- data/app/views/decidim_toggle/system/organizations/_settings_tab_submit.html.erb +4 -0
- data/app/views/decidim_toggle/system/organizations/_settings_tabs.html.erb +31 -0
- data/app/views/decidim_toggle/system/organizations/tabs/_authorizations_tab.html.erb +9 -0
- data/app/views/decidim_toggle/system/organizations/tabs/_emails_tab.html.erb +5 -0
- data/app/views/decidim_toggle/system/organizations/tabs/_file_upload_tab.html.erb +5 -0
- data/app/views/decidim_toggle/system/organizations/tabs/_language_tab.html.erb +35 -0
- data/app/views/decidim_toggle/system/organizations/tabs/_omniauth_tab.html.erb +5 -0
- data/app/views/decidim_toggle/system/organizations/tabs/_security_tab.html.erb +20 -0
- data/app/views/layouts/decidim/toggle/_javascript_config.html.erb +3 -0
- data/bin/check +10 -0
- data/bin/i18n-tasks +27 -0
- data/bin/postversion +14 -0
- data/config/assets.rb +8 -0
- data/config/locales/decidim_toggle_en.yml +58 -0
- data/crowdin.yml +15 -0
- data/db/migrate/20260321120000_create_decidim_toggle_organization_module_configs.rb +20 -0
- data/decidim-toggle.gemspec +35 -0
- data/docker-compose.yml +41 -0
- data/lib/decidim/toggle/engine.rb +62 -0
- data/lib/decidim/toggle/expose_attributes_to_js.rb +26 -0
- data/lib/decidim/toggle/expose_attributes_to_js_validator.rb +32 -0
- data/lib/decidim/toggle/gem_registry.rb +15 -0
- data/lib/decidim/toggle/informative_callouts.rb +76 -0
- data/lib/decidim/toggle/javascript_config.rb +87 -0
- data/lib/decidim/toggle/module_config.rb +64 -0
- data/lib/decidim/toggle/module_config_form.rb +41 -0
- data/lib/decidim/toggle/module_config_i18n.rb +44 -0
- data/lib/decidim/toggle/module_configuration_presenter.rb +55 -0
- data/lib/decidim/toggle/organization_settings_tabs.rb +69 -0
- data/lib/decidim/toggle/settings_form_builder.rb +200 -0
- data/lib/decidim/toggle/settings_tab_item.rb +37 -0
- data/lib/decidim/toggle/settings_tab_registry.rb +109 -0
- data/lib/decidim/toggle/settings_tabs.rb +56 -0
- data/lib/decidim/toggle/tab_form.rb +20 -0
- data/lib/decidim/toggle/version.rb +14 -0
- data/lib/decidim/toggle.rb +36 -0
- data/lib/tasks/decidim/toggle/toggle_upgrade.rake +13 -0
- data/lib/tasks/decidim/toggle/toggle_webpacker.rake +60 -0
- data/log/.gitignore +2 -0
- data/package.json +18 -0
- data/prettier.config.js +15 -0
- data/spec/commands/decidim/toggle/update_authorizations_command_spec.rb +41 -0
- data/spec/commands/decidim/toggle/update_emails_command_spec.rb +84 -0
- data/spec/commands/decidim/toggle/update_file_upload_settings_command_spec.rb +28 -0
- data/spec/commands/decidim/toggle/update_locale_command_spec.rb +53 -0
- data/spec/commands/decidim/toggle/update_module_config_command_spec.rb +38 -0
- data/spec/commands/decidim/toggle/update_name_command_spec.rb +49 -0
- data/spec/commands/decidim/toggle/update_omniauth_command_spec.rb +80 -0
- data/spec/commands/decidim/toggle/update_security_command_spec.rb +25 -0
- data/spec/decidim/toggle/settings_tab_item_spec.rb +34 -0
- data/spec/decidim/toggle/settings_tab_registry_spec.rb +66 -0
- data/spec/decidim/toggle/settings_tabs_spec.rb +60 -0
- data/spec/forms/concerns/decidim/toggle/informative_callouts_spec.rb +48 -0
- data/spec/forms/decidim/toggle/update_authorizations_form_spec.rb +40 -0
- data/spec/forms/decidim/toggle/update_emails_form_spec.rb +35 -0
- data/spec/forms/decidim/toggle/update_file_upload_settings_form_spec.rb +20 -0
- data/spec/forms/decidim/toggle/update_locale_form_spec.rb +64 -0
- data/spec/forms/decidim/toggle/update_name_form_spec.rb +57 -0
- data/spec/forms/decidim/toggle/update_omniauth_form_spec.rb +56 -0
- data/spec/forms/decidim/toggle/update_security_form_spec.rb +32 -0
- data/spec/helpers/decidim/toggle/system_settings_tab_helper_spec.rb +99 -0
- data/spec/lib/decidim/toggle/expose_attributes_to_js_spec.rb +31 -0
- data/spec/lib/decidim/toggle/expose_attributes_to_js_validator_spec.rb +30 -0
- data/spec/lib/decidim/toggle/gem_registry_spec.rb +30 -0
- data/spec/lib/decidim/toggle/javascript_config_spec.rb +169 -0
- data/spec/lib/decidim/toggle/module_config_form_spec.rb +45 -0
- data/spec/lib/decidim/toggle/module_config_spec.rb +74 -0
- data/spec/lib/decidim/toggle/module_configuration_presenter_spec.rb +53 -0
- data/spec/lib/decidim/toggle/settings_form_builder_spec.rb +115 -0
- data/spec/requests/decidim_toggle/system/settings_tab_spec.rb +144 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/support/decidim_factories.rb +3 -0
- data/spec/support/devise.rb +5 -0
- data/spec/system/decidim_toggle/javascript_config_spec.rb +56 -0
- data/website/.gitignore +4 -0
- data/website/docs/developer/_category_.json +8 -0
- data/website/docs/developer/code-of-conduct.md +99 -0
- data/website/docs/developer/contribute.md +50 -0
- data/website/docs/developer/deface-usage.md +37 -0
- data/website/docs/developer/documentation.md +58 -0
- data/website/docs/developer/error-handling.md +51 -0
- data/website/docs/developer/tab-registry.md +51 -0
- data/website/docs/developer/view-customization.md +49 -0
- data/website/docs/index.md +80 -0
- data/website/docs/integrate/_category_.json +8 -0
- data/website/docs/integrate/attributes.md +121 -0
- data/website/docs/integrate/customize-views.md +62 -0
- data/website/docs/integrate/index.md +38 -0
- data/website/docs/integrate/informative_callout.md +80 -0
- data/website/docs/integrate/javascript.md +84 -0
- data/website/docs/integrate/labels.md +91 -0
- data/website/docs/integrate/quickstart.md +77 -0
- data/website/docusaurus.config.ts +100 -0
- data/website/package.json +31 -0
- data/website/sidebars.ts +7 -0
- data/website/src/css/custom.css +37 -0
- data/website/static/img/logo.svg +1 -0
- data/website/static/img/schema_overview.png +0 -0
- data/website/static/img/screenshot_informative_callouts.png +0 -0
- data/website/static/img/screenshot_saved_flash.png +0 -0
- data/website/static/img/screenshots_locale_tab.png +0 -0
- data/website/static/img/screenshots_name_tab.png +0 -0
- data/website/static/img/screenshots_security_tab.png +0 -0
- data/website/tsconfig.json +6 -0
- data/website/yarn.lock +8336 -0
- data/yarn.lock +13 -0
- 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,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 %>
|
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,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
|
data/docker-compose.yml
ADDED
|
@@ -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
|