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,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,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,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 %>
|
data/app/views/decidim_toggle/system/organizations/_encryption_not_configured_callout.html.erb
ADDED
|
@@ -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 %>
|