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,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "decidim/translatable_attributes"
4
+
5
+ module Decidim
6
+ module Toggle
7
+ class UpdateNameForm < Decidim::Form
8
+ include Decidim::TranslatableAttributes
9
+
10
+ mimic :organization
11
+
12
+ translatable_attribute :name, String
13
+ attribute :host, String
14
+ attribute :secondary_hosts, String
15
+
16
+ validates :host, presence: true
17
+ validate :validate_name_presence
18
+
19
+ def self.from_model(organization)
20
+ secondary_hosts = case organization.secondary_hosts
21
+ when Array
22
+ (organization.secondary_hosts || []).join("\n")
23
+ when String
24
+ organization.secondary_hosts
25
+ else
26
+ ""
27
+ end
28
+
29
+ from_params(
30
+ name: organization.name,
31
+ host: organization.host,
32
+ secondary_hosts:
33
+ )
34
+ end
35
+
36
+ def self.from_params(params, additional_params = {})
37
+ params = params.to_h.with_indifferent_access if params.respond_to?(:to_h)
38
+ attrs = params[:organization] || params
39
+ name_hash = I18n.available_locales.to_h { |l| [l.to_s, attrs["name_#{l}"]] }.compact_blank
40
+ if name_hash.present?
41
+ params = params.dup
42
+ params[:organization] = (params[:organization] || {}).merge(name: name_hash)
43
+ end
44
+ super(params, additional_params)
45
+ end
46
+
47
+ def clean_secondary_hosts
48
+ return [] if secondary_hosts.blank?
49
+
50
+ secondary_hosts.split("\n").map(&:chomp).select(&:present?)
51
+ end
52
+
53
+ private
54
+
55
+ def validate_name_presence
56
+ return if name.is_a?(Hash) && name.values.any?(&:present?)
57
+
58
+ locale = current_organization&.default_locale || Decidim.default_locale.to_s
59
+ errors.add(:"name_#{locale}", :blank) if name.blank? || name[locale].blank?
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Toggle
5
+ class UpdateOmniauthForm < Decidim::System::BaseOrganizationForm
6
+ include InformativeCallouts
7
+
8
+ mimic :organization
9
+
10
+ info :space_page_defaults_callout,
11
+ if_predicate: ->(*) { Decidim::Toggle.gem_present?("decidim-space_page") }
12
+
13
+ def space_page_defaults_callout
14
+ "For generated organization defaults, check the decidim space page"
15
+ end
16
+
17
+ # This tab only updates a small JSONB slice (omniauth settings).
18
+ # The generic system form validates presence of `host` and `users_registration_mode`,
19
+ # so fall back to the current organization when those attributes aren't part of
20
+ # the tab payload.
21
+ def host
22
+ super.presence || current_organization&.host
23
+ end
24
+
25
+ def users_registration_mode
26
+ super.presence || current_organization&.users_registration_mode
27
+ end
28
+
29
+ private
30
+
31
+ # No-op: this tab doesn't touch organization identity/uniqueness.
32
+ def validate_organization_uniqueness
33
+ # intentionally blank
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Toggle
5
+ class UpdateSecurityForm < Decidim::Form
6
+ mimic :organization
7
+
8
+ attribute :force_users_to_authenticate_before_access_organization, Boolean
9
+ attribute :users_registration_mode, String
10
+ attribute :"default-src", String
11
+ attribute :"img-src", String
12
+ attribute :"media-src", String
13
+ attribute :"script-src", String
14
+ attribute :"style-src", String
15
+ attribute :"frame-src", String
16
+ attribute :"font-src", String
17
+ attribute :"connect-src", String
18
+
19
+ validates :users_registration_mode, presence: true,
20
+ inclusion: { in: Decidim::Organization.users_registration_modes }
21
+
22
+ def self.from_model(organization)
23
+ csp = organization.content_security_policy || {}
24
+ attrs = {
25
+ force_users_to_authenticate_before_access_organization: organization.force_users_to_authenticate_before_access_organization,
26
+ users_registration_mode: organization.users_registration_mode,
27
+ "default-src": csp["default-src"],
28
+ "img-src": csp["img-src"],
29
+ "media-src": csp["media-src"],
30
+ "script-src": csp["script-src"],
31
+ "style-src": csp["style-src"],
32
+ "frame-src": csp["frame-src"],
33
+ "font-src": csp["font-src"],
34
+ "connect-src": csp["connect-src"]
35
+ }
36
+ from_params({ organization: attrs })
37
+ end
38
+
39
+ def self.from_params(params, additional_params = {})
40
+ params = params.to_h.with_indifferent_access if params.respond_to?(:to_h)
41
+ params[:organization] || params
42
+ super(params, additional_params)
43
+ end
44
+
45
+ def self.collection_for_users_registration_mode
46
+ Decidim::Organization.users_registration_modes.map do |mode|
47
+ [mode.first, I18n.t("decidim.system.organizations.users_registration_mode.#{mode.first}", default: mode.first)]
48
+ end
49
+ end
50
+
51
+ def content_security_policy
52
+ {
53
+ "default-src" => send(:"default-src"),
54
+ "img-src" => send(:"img-src"),
55
+ "media-src" => send(:"media-src"),
56
+ "script-src" => send(:"script-src"),
57
+ "style-src" => send(:"style-src"),
58
+ "frame-src" => send(:"frame-src"),
59
+ "font-src" => send(:"font-src"),
60
+ "connect-src" => send(:"connect-src")
61
+ }.compact_blank
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Toggle
5
+ module JavascriptConfigHelper
6
+ def decidim_toggle_javascript_config
7
+ Decidim::Toggle.javascript_config_for(current_organization)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Toggle
5
+ module SystemSettingsTabHelper
6
+ ##
7
+ # Check if Decidim can use encryption
8
+ def encryption_configured?
9
+ return false if Rails.application.secret_key_base.blank?
10
+
11
+ probe = "decidim-toggle-secret-probe"
12
+ encrypted = Decidim::AttributeEncryptor.encrypt(probe)
13
+ return false if encrypted.blank?
14
+
15
+ Decidim::AttributeEncryptor.decrypt(encrypted) == probe
16
+ rescue ArgumentError, OpenSSL::Cipher::CipherError,
17
+ ActiveSupport::MessageEncryptor::InvalidMessage,
18
+ ActiveSupport::MessageVerifier::InvalidSignature
19
+ false
20
+ end
21
+
22
+ def decidim_toggle_update_settings_tab_organization_path(organization, tab_id:)
23
+ path = Decidim::Toggle::Engine.routes.url_helpers.update_settings_tab_organization_path(organization, tab_id:)
24
+ prefixed = path.start_with?("/decidim_toggle") ? path : "/decidim_toggle#{path}"
25
+ prefixed.sub(%r{\A/decidim_toggle/decidim_toggle}, "/decidim_toggle")
26
+ end
27
+
28
+ def decidim_toggle_settings_tab_form(organization, tab, &block)
29
+ tab_form = tab.form_class.from_model(organization)
30
+ if (stored = flash[:decidim_toggle_invalid_settings_tab]) &&
31
+ stored[:organization_id].to_i == organization.id &&
32
+ stored[:tab_id].to_s == tab.identifier.to_s
33
+ flash.delete(:decidim_toggle_invalid_settings_tab)
34
+ tab_form = tab.form_class.from_params(organization: stored[:params])
35
+ tab_form = tab_form.with_context(current_organization: organization) if tab_form.respond_to?(:with_context)
36
+ stored[:errors].each do |attribute, messages|
37
+ messages.each { |message| tab_form.errors.add(attribute, message) }
38
+ end
39
+ end
40
+ form_with(
41
+ url: decidim_toggle_update_settings_tab_organization_path(organization, tab_id: tab.identifier),
42
+ model: tab_form,
43
+ scope: :organization,
44
+ builder: Decidim::Toggle::SettingsFormBuilder,
45
+ method: :patch,
46
+ html: { id: "settings_tab_form_#{tab.identifier}" },
47
+ local: true
48
+ ) do |tf|
49
+ safe_join([
50
+ render("decidim_toggle/system/organizations/settings_tab_active_tab_field", tab:),
51
+ tf.informative_callouts,
52
+ content_tag(:div, class: "form__wrapper") { capture(tf, &block) },
53
+ render("decidim_toggle/system/organizations/settings_tab_submit", form: tf)
54
+ ])
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module Toggle
5
+ # Per-organization JSON configuration for a module (see {Decidim::Toggle.config_for}).
6
+ class OrganizationModuleConfig < Decidim::ApplicationRecord
7
+ self.table_name = "decidim_toggle_organization_module_configs"
8
+
9
+ belongs_to :organization, class_name: "Decidim::Organization", foreign_key: :decidim_organization_id
10
+
11
+ validates :module_name, presence: true
12
+ validates :module_name, uniqueness: { scope: :decidim_organization_id }
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ Deface::Override.new(
4
+ virtual_path: "layouts/decidim/admin/_header",
5
+ name: "toggle_add_javascript_config_admin",
6
+ insert_after: "erb[loud]:contains('append_stylesheet_pack_tag')",
7
+ original: '<%= append_stylesheet_pack_tag "decidim_admin_overrides", media: "all" %>',
8
+ text: <<~ERB
9
+ <%= render partial: "layouts/decidim/toggle/javascript_config" %>
10
+ ERB
11
+ )
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ Deface::Override.new(
4
+ virtual_path: "layouts/decidim/_decidim_javascript",
5
+ name: "toggle_add_javascript_config_public",
6
+ insert_after: "erb[loud]:contains('layouts/decidim/js_configuration')",
7
+ original: '<%= render partial: "layouts/decidim/js_configuration" %>',
8
+ text: <<~ERB
9
+ <%= render partial: "layouts/decidim/toggle/javascript_config" %>
10
+ ERB
11
+ )
@@ -0,0 +1,3 @@
1
+ // CSS for Decidim::System organization settings (tabbed UI)
2
+ import "stylesheets/decidim/toggle/organization_settings.scss";
3
+ import "src/decidim/toggle/organization_settings_tabs";
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Self-contained settings tabs (no Decidim accordion).
3
+ * Keeps tab selection in the URL hash (#panel-toggle-<id>) so it survives save redirects.
4
+ */
5
+ const CONTAINER_SELECTOR = ".js-decidim-toggle-settings-tabs";
6
+ const PANEL_PREFIX = "panel-toggle-";
7
+
8
+ function getContainers() {
9
+ return document.querySelectorAll(CONTAINER_SELECTOR);
10
+ }
11
+
12
+ function getTriggerForPanel(container, panelId) {
13
+ return container.querySelector(`[data-controls="${panelId}"]`);
14
+ }
15
+
16
+ function getDefaultTrigger(container) {
17
+ return (
18
+ container.querySelector('.tab-x[data-controls][aria-expanded="true"]') ||
19
+ container.querySelector(".tab-x[data-controls]")
20
+ );
21
+ }
22
+
23
+ function scrollActiveTabIntoView(
24
+ container,
25
+ trigger,
26
+ { behavior = "smooth" } = {}
27
+ ) {
28
+ const scroller = container.querySelector(".tab-x-container");
29
+ const tabItem = trigger?.closest("li");
30
+ if (!scroller || !tabItem) return;
31
+
32
+ scroller.scrollTo({
33
+ left: Math.max(tabItem.offsetLeft - 64, 0),
34
+ behavior
35
+ });
36
+ }
37
+
38
+ function activateTab(container, trigger, { scrollBehavior = "smooth" } = {}) {
39
+ const panelId = trigger.dataset.controls;
40
+ if (!panelId) return;
41
+
42
+ container.querySelectorAll(".tab-x[data-controls]").forEach((button) => {
43
+ const isActive = button === trigger;
44
+ button.setAttribute("aria-expanded", isActive ? "true" : "false");
45
+ });
46
+
47
+ container.querySelectorAll(`[id^="${PANEL_PREFIX}"]`).forEach((panel) => {
48
+ panel.setAttribute("aria-hidden", panel.id === panelId ? "false" : "true");
49
+ });
50
+
51
+ scrollActiveTabIntoView(container, trigger, { behavior: scrollBehavior });
52
+ }
53
+
54
+ function activateTabFromHash(container) {
55
+ const raw = window.location.hash.replace(/^#/, "");
56
+ if (!raw || !raw.startsWith(PANEL_PREFIX)) return;
57
+
58
+ const panel = document.getElementById(raw);
59
+ if (!panel || !container.contains(panel)) return;
60
+
61
+ const trigger = getTriggerForPanel(container, raw);
62
+ if (!trigger || trigger.getAttribute("aria-expanded") === "true") return;
63
+
64
+ activateTab(container, trigger);
65
+ }
66
+
67
+ function initialTrigger(container) {
68
+ const raw = window.location.hash.replace(/^#/, "");
69
+ if (raw && raw.startsWith(PANEL_PREFIX)) {
70
+ const panel = document.getElementById(raw);
71
+ const trigger = getTriggerForPanel(container, raw);
72
+ if (panel && container.contains(panel) && trigger) return trigger;
73
+ }
74
+
75
+ return getDefaultTrigger(container);
76
+ }
77
+
78
+ function syncHash(panelId) {
79
+ const newHash = `#${panelId}`;
80
+ if (window.location.hash === newHash) return;
81
+
82
+ const url = `${window.location.pathname}${window.location.search}${newHash}`;
83
+ window.history.replaceState(null, "", url);
84
+ }
85
+
86
+ function initContainer(container) {
87
+ const trigger = initialTrigger(container);
88
+ if (trigger) activateTab(container, trigger, { scrollBehavior: "auto" });
89
+
90
+ container.addEventListener("click", (event) => {
91
+ const button = event.target.closest(".tab-x[data-controls]");
92
+ if (!button || !container.contains(button)) return;
93
+
94
+ activateTab(container, button);
95
+
96
+ const panelId = button.dataset.controls;
97
+ if (panelId && panelId.startsWith(PANEL_PREFIX)) {
98
+ syncHash(panelId);
99
+ }
100
+ });
101
+ }
102
+
103
+ function init() {
104
+ getContainers().forEach(initContainer);
105
+ window.addEventListener("hashchange", () => {
106
+ getContainers().forEach(activateTabFromHash);
107
+ });
108
+ }
109
+
110
+ if (document.readyState === "loading") {
111
+ document.addEventListener("DOMContentLoaded", init);
112
+ } else {
113
+ init();
114
+ }
@@ -0,0 +1,160 @@
1
+ // Organization settings form + tabs (decidim-toggle).
2
+ // Loaded only when the tabbed settings partial is rendered (decidim system).
3
+ #aside-system ~ main {
4
+ max-width: calc(100% - 16.6666%);
5
+ }
6
+
7
+ #decidim-toggle-callout {
8
+ @apply border-b-2 border-r-2;
9
+
10
+ .invitation_pending_title {
11
+ @apply text-xl font-semibold mb-2;
12
+ }
13
+
14
+ .invitation_pending_body {
15
+ @apply text-md font-normal mb-0;
16
+ }
17
+
18
+ .callout-actions {
19
+ @apply mt-4;
20
+ }
21
+ }
22
+
23
+ #decidim-toggle-settings-tabs {
24
+ @apply mt-4;
25
+
26
+ .button {
27
+ @apply px-4 py-1.5 ml-0 text-sm leading-[18px] !important;
28
+ }
29
+
30
+ .field-helptext {
31
+ @apply text-xs font-normal text-gray m-0 -translate-y-[7px];
32
+ }
33
+
34
+ .form__wrapper-block {
35
+ @apply border-0 border-transparent;
36
+ }
37
+
38
+ .tab-x-container {
39
+ @apply flex flex-nowrap gap-x-2 gap-y-1 list-none m-0 p-0 border-b border-gray/35;
40
+ max-width: 100%;
41
+ overflow-x: auto;
42
+ scroll-behavior: smooth;
43
+ }
44
+
45
+ .tab-x-container > li {
46
+ @apply m-0 p-0;
47
+ text-wrap: nowrap;
48
+ }
49
+
50
+ .tab-x {
51
+ @apply appearance-none bg-[rgba(243,244,247,1)] border border-transparent border-b-0 rounded-t
52
+ text-secondary cursor-pointer text-[0.8125rem] font-semibold leading-tight m-0 px-3 py-2
53
+ transition-[background-color,border-color] duration-150 ease-in-out;
54
+ }
55
+
56
+ .tab-x:hover,
57
+ .tab-x:focus-visible {
58
+ @apply bg-[rgba(243,244,247,1)] border-gray/35 outline-none;
59
+ }
60
+
61
+ .tab-x[aria-expanded="true"] {
62
+ @apply bg-white border-gray/35 border-b-white -mb-px relative z-[1];
63
+ }
64
+
65
+ > [id^="panel-toggle-"] {
66
+ @apply clear-both border border-[#eee] py-4 px-2;
67
+ }
68
+
69
+ > [id^="panel-toggle-"][aria-hidden="true"] {
70
+ @apply hidden;
71
+ }
72
+
73
+ .form__wrapper {
74
+ @apply mt-0 gap-0 bg-[#f6f6f6] pt-2 pr-6 pl-4 pb-6;
75
+ }
76
+
77
+ .form__wrapper .field {
78
+ @apply mb-3 !important;
79
+ }
80
+
81
+ .form__wrapper .field:last-child {
82
+ @apply mb-0;
83
+ }
84
+
85
+ .form__wrapper .field.is-disabled {
86
+ @apply cursor-not-allowed;
87
+ label,
88
+ input {
89
+ opacity: 0.33;
90
+ @apply cursor-not-allowed;
91
+ }
92
+ }
93
+
94
+ fieldset {
95
+ @apply border-0 rounded-none m-0 mb-2 px-2 pt-2 pb-1;
96
+ }
97
+
98
+ fieldset:last-of-type {
99
+ @apply mb-2;
100
+ }
101
+
102
+ .form-legend,
103
+ legend,
104
+ legend > label {
105
+ @apply text-md font-semibold p-0 uppercase mt-4;
106
+ }
107
+
108
+ label {
109
+ @apply text-sm font-normal p-0 normal-case;
110
+ }
111
+
112
+ input {
113
+ @apply my-1.5 py-0.5 px-2;
114
+ }
115
+
116
+ input[type="checkbox"],
117
+ input[type="radio"] {
118
+ @apply my-1.5 mr-2 py-0.5 px-2;
119
+ }
120
+
121
+ .table-fields {
122
+ @apply mb-4 w-full;
123
+ }
124
+
125
+ .button.button__sm {
126
+ @apply mt-4;
127
+ }
128
+
129
+ .decidim_toggle_informative_callout {
130
+ .flash__message {
131
+ h1 {
132
+ @apply font-sans font-bold text-4xl;
133
+ }
134
+
135
+ h2 {
136
+ @apply font-sans font-bold text-3xl;
137
+ }
138
+
139
+ h3 {
140
+ @apply font-sans font-semibold text-2xl;
141
+ }
142
+
143
+ h4 {
144
+ @apply font-sans font-semibold text-xl;
145
+ }
146
+
147
+ h5 {
148
+ @apply font-sans font-semibold text-lg;
149
+ }
150
+
151
+ h6 {
152
+ @apply font-sans font-semibold text-md;
153
+ }
154
+
155
+ p {
156
+ @apply prose max-w-full;
157
+ }
158
+ }
159
+ }
160
+ }
@@ -0,0 +1,20 @@
1
+ <% add_decidim_page_title(t(".title")) %>
2
+
3
+ <% provide :title do %>
4
+ <h1 class="h1"><%= t ".title" %></h1>
5
+ <% end %>
6
+ <% if @organization&.users&.first&.invitation_pending? %>
7
+ <div id="decidim-toggle-callout" class="callout warning invitation_pending form__wrapper-block flex-col-reverse md:flex-row justify-between mt-8">
8
+ <div class="callout-content">
9
+ <h4 class="invitation_pending_title h4 font-bold pb-2"><%= t("decidim_toggle.system.organizations.invitation_pending_title") %></h4>
10
+ <p class="invitation_pending_body"><%= t("decidim_toggle.system.organizations.invitation_pending_body") %></p>
11
+ </div>
12
+ <div class="callout-actions">
13
+ <%= link_to t(".resend_invitation"),
14
+ resend_invitation_organization_path(@organization),
15
+ method: :post, class: "button button__sm button__transparent-secondary", data: { confirm: t(".confirm_resend_invitation") } %>
16
+ </div>
17
+ </div>
18
+ <% end %>
19
+
20
+ <%= render "decidim_toggle/system/organizations/settings_tabs", organization: @organization %>
@@ -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
+ <%= tf.all_fields %>
5
+ <% end %>
@@ -0,0 +1,6 @@
1
+ <div id="decidim-toggle-callout" class="callout alert mt-8">
2
+ <div class="callout-content">
3
+ <h4 class="h4 font-bold pb-2 encryption_not_configured_title"><%= t("decidim_toggle.system.organizations.encryption_not_configured_title") %></h4>
4
+ <p class="encryption_not_configured_body"><%= t("decidim_toggle.system.organizations.encryption_not_configured") %></p>
5
+ </div>
6
+ </div>
@@ -0,0 +1 @@
1
+ <%= hidden_field_tag :decidim_toggle_active_tab, tab.identifier %>
@@ -0,0 +1,4 @@
1
+ <div class="flex justify-end items-center gap-6 mr-6">
2
+ <%= link_to t("cancel", scope: "decidim_toggle.system.organizations.form_tab"), decidim_system.organizations_path, class: "button button__sm" %>
3
+ <%= form.submit t("save", scope: "decidim_toggle.system.organizations.form_tab"), class: "button button__sm button__primary" %>
4
+ </div>
@@ -0,0 +1,31 @@
1
+ <% if encryption_configured? %>
2
+ <%= append_stylesheet_pack_tag "decidim_toggle", media: "all" %>
3
+ <%= append_javascript_pack_tag "decidim_toggle" %>
4
+ <% organization = local_assigns[:organization] %>
5
+ <% tabs = Decidim::Toggle::SettingsTabs.new(:organization_settings) %>
6
+ <% tabs.build_for(self) %>
7
+ <div class="js-decidim-toggle-settings-tabs" id="decidim-toggle-settings-tabs">
8
+ <ul class="tab-x-container tabs-<%= tabs.items.size %>">
9
+ <% tabs.items.each do |tab| %>
10
+ <li>
11
+ <button type="button" id="trigger-toggle-<%= tab.identifier %>" class="tab-x" data-controls="panel-toggle-<%= tab.identifier %>" aria-expanded="<%= tab.open? ? "true" : "false" %>">
12
+ <%= tab.label %>
13
+ </button>
14
+ </li>
15
+ <% end %>
16
+ </ul>
17
+
18
+ <% tabs.items.each do |tab| %>
19
+ <div id="panel-toggle-<%= tab.identifier %>" class="border-2 rounded border-background p-4 form__wrapper mt-8" aria-hidden="<%= tab.open? ? "false" : "true" %>">
20
+ <% if tab.form_layout_partial.present? %>
21
+ <%= render tab.form_layout_partial, tab:, organization: %>
22
+ <% else %>
23
+ <%= render "decidim_toggle/system/organizations/default_form_tab", tab:, organization: %>
24
+ <% end %>
25
+ </div>
26
+ <% end %>
27
+ </div>
28
+
29
+ <% else %>
30
+ <%= render "decidim_toggle/system/organizations/encryption_not_configured_callout" %>
31
+ <% end %>
@@ -0,0 +1,9 @@
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.authorizations_tab.legend") %></legend>
6
+ <p class="text-sm text-gray-2 mb-4"><%= t("decidim_toggle.system.organizations.authorizations_tab.hint") %></p>
7
+ <%= tf.all_fields %>
8
+ </fieldset>
9
+ <% 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/smtp_settings", f: tf %>
5
+ <% 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/file_upload_settings", f: tf %>
5
+ <% end %>