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,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "decidim/toggle/module_configuration_presenter"
|
|
4
|
+
|
|
5
|
+
module Decidim
|
|
6
|
+
module Toggle
|
|
7
|
+
class << self
|
|
8
|
+
def normalize_module_name(name)
|
|
9
|
+
name.to_s
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def config_for(organization, module_name, registry_name: :organization_settings)
|
|
13
|
+
key = normalize_module_name(module_name)
|
|
14
|
+
raw = raw_config_hash(organization, key)
|
|
15
|
+
entry = SettingsTabRegistry.find(registry_name)&.form_tab_for_module(key)
|
|
16
|
+
|
|
17
|
+
if entry && entry[:form]
|
|
18
|
+
form = build_form_from_hash(entry[:form], organization, raw)
|
|
19
|
+
ModuleConfigurationPresenter.new(form).to_config_hash.with_indifferent_access
|
|
20
|
+
else
|
|
21
|
+
raw.with_indifferent_access
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def save_config!(organization, module_name, attributes, merge: true)
|
|
26
|
+
key = normalize_module_name(module_name)
|
|
27
|
+
record = OrganizationModuleConfig.find_or_initialize_by(
|
|
28
|
+
decidim_organization_id: organization.id,
|
|
29
|
+
module_name: key
|
|
30
|
+
)
|
|
31
|
+
incoming = stringify_keys(attributes)
|
|
32
|
+
record.config = merge ? (record.config || {}).stringify_keys.merge(incoming) : incoming
|
|
33
|
+
record.save!
|
|
34
|
+
record
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def build_form_from_hash(form_class, organization, raw_hash)
|
|
40
|
+
form_class.from_params(organization: raw_hash).with_context(current_organization: organization)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def raw_config_hash(organization, module_name)
|
|
44
|
+
OrganizationModuleConfig.find_by(
|
|
45
|
+
decidim_organization_id: organization.id,
|
|
46
|
+
module_name:
|
|
47
|
+
)&.config || {}
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def stringify_keys(hash)
|
|
51
|
+
hash.to_h.stringify_keys.transform_values do |v|
|
|
52
|
+
case v
|
|
53
|
+
when Hash
|
|
54
|
+
stringify_keys(v)
|
|
55
|
+
when Array
|
|
56
|
+
v.map { |e| e.is_a?(Hash) ? stringify_keys(e) : e }
|
|
57
|
+
else
|
|
58
|
+
v
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Decidim
|
|
4
|
+
module Toggle
|
|
5
|
+
# Include in a {Decidim::Form} used with +add_tab ..., module_name:+ so +from_model(organization)+
|
|
6
|
+
# loads JSON from {OrganizationModuleConfig} and +UpdateModuleConfigCommand+ can persist it.
|
|
7
|
+
# Field labels resolve from +decidim_toggle.system.<module_config_name>+ (see integrate/labels.md).
|
|
8
|
+
#
|
|
9
|
+
# class MyModule::AdminConfigForm < Decidim::Form
|
|
10
|
+
# include Decidim::Toggle::ModuleConfigForm
|
|
11
|
+
#
|
|
12
|
+
# self.module_config_name = "decidim_geo"
|
|
13
|
+
# mimic :organization
|
|
14
|
+
# attribute :enabled, :boolean
|
|
15
|
+
# end
|
|
16
|
+
module ModuleConfigForm
|
|
17
|
+
extend ActiveSupport::Concern
|
|
18
|
+
|
|
19
|
+
class_methods do
|
|
20
|
+
def human_attribute_name(attr, options = {})
|
|
21
|
+
ModuleConfigI18n.translate_label(module_config_name, attr) || super
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def from_model(organization)
|
|
25
|
+
raise NotImplementedError, "#{name} must set self.module_config_name = \"...\"" if module_config_name.blank?
|
|
26
|
+
|
|
27
|
+
raw = OrganizationModuleConfig.find_by(
|
|
28
|
+
decidim_organization_id: organization.id,
|
|
29
|
+
module_name: module_config_name
|
|
30
|
+
)&.config || {}
|
|
31
|
+
|
|
32
|
+
from_params(organization: raw).with_context(current_organization: organization)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
included do
|
|
37
|
+
class_attribute :module_config_name, instance_accessor: false
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Decidim
|
|
4
|
+
module Toggle
|
|
5
|
+
# I18n scope helpers for {ModuleConfigForm} tab fields.
|
|
6
|
+
module ModuleConfigI18n
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def scope_for(module_name)
|
|
10
|
+
return if module_name.blank?
|
|
11
|
+
|
|
12
|
+
"decidim_toggle.system.#{module_name}"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def label_key(module_name, attribute)
|
|
16
|
+
base = scope_for(module_name)
|
|
17
|
+
return unless base
|
|
18
|
+
|
|
19
|
+
"#{base}.#{attribute}"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def helptext_key(module_name, attribute)
|
|
23
|
+
base = scope_for(module_name)
|
|
24
|
+
return unless base
|
|
25
|
+
|
|
26
|
+
"#{base}.helptext.#{attribute}"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def translate_label(module_name, attribute)
|
|
30
|
+
key = label_key(module_name, attribute)
|
|
31
|
+
return unless key && I18n.exists?(key)
|
|
32
|
+
|
|
33
|
+
I18n.t(key)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def translate_helptext(module_name, attribute)
|
|
37
|
+
key = helptext_key(module_name, attribute)
|
|
38
|
+
return unless key && I18n.exists?(key)
|
|
39
|
+
|
|
40
|
+
I18n.t(key)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Decidim
|
|
4
|
+
module Toggle
|
|
5
|
+
# Read model around a {Decidim::Form} built from stored JSON: normalizes +nil+ for
|
|
6
|
+
# array/hash/boolean attributes so runtime code can use +enabled?+, empty collections, etc.
|
|
7
|
+
class ModuleConfigurationPresenter
|
|
8
|
+
def initialize(form)
|
|
9
|
+
@form = form
|
|
10
|
+
define_accessors!
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
attr_reader :form
|
|
14
|
+
|
|
15
|
+
def to_config_hash
|
|
16
|
+
@form.class.attribute_names.each_with_object({}) do |attr, hash|
|
|
17
|
+
next if attr == "id"
|
|
18
|
+
|
|
19
|
+
type = @form.class.attribute_types[attr]
|
|
20
|
+
hash[attr] = normalize(@form.public_send(attr), attr, type)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def define_accessors!
|
|
27
|
+
@form.class.attribute_names.each do |attr|
|
|
28
|
+
next if attr == "id"
|
|
29
|
+
|
|
30
|
+
type = @form.class.attribute_types[attr]
|
|
31
|
+
|
|
32
|
+
define_singleton_method(attr) do
|
|
33
|
+
normalize(@form.public_send(attr), attr, type)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
next unless type.is_a?(ActiveModel::Type::Boolean)
|
|
37
|
+
|
|
38
|
+
define_singleton_method("#{attr}?") do
|
|
39
|
+
!!normalize(@form.public_send(attr), attr, type)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def normalize(value, _attr_name, type)
|
|
45
|
+
return value unless value.nil?
|
|
46
|
+
|
|
47
|
+
return [] if type.try(:type) == :array
|
|
48
|
+
return {} if type.try(:type) == :hash
|
|
49
|
+
return false if type.is_a?(ActiveModel::Type::Boolean)
|
|
50
|
+
|
|
51
|
+
value
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Decidim
|
|
4
|
+
module Toggle
|
|
5
|
+
# Registers default system organization settings tabs.
|
|
6
|
+
#
|
|
7
|
+
# Extension contract: the authorizations tab uses the stable identifier
|
|
8
|
+
# +:authorizations+ (vanilla Decidim: string array of verification workflow names).
|
|
9
|
+
# Another engine may register an additional +Decidim::Toggle.settings_tabs+
|
|
10
|
+
# block **after** +decidim_toggle.organization_settings_tabs+ and call
|
|
11
|
+
# +remove_tab(:authorizations)+ then +add_tab(:authorizations, ...)+ with the
|
|
12
|
+
# same identifier to replace the tab. The last +register_form_tab+ for that id
|
|
13
|
+
# wins; see {Decidim::Toggle::SettingsTabRegistry#register_form_tab}.
|
|
14
|
+
class OrganizationSettingsTabs
|
|
15
|
+
def self.register!
|
|
16
|
+
scope = "decidim_toggle.system.organizations.settings_tabs"
|
|
17
|
+
Decidim::Toggle.settings_tabs :organization_settings do |tabs|
|
|
18
|
+
tabs.add_tab :name,
|
|
19
|
+
t("name", scope:),
|
|
20
|
+
form: Decidim::Toggle::UpdateNameForm,
|
|
21
|
+
command: Decidim::Toggle::UpdateNameCommand,
|
|
22
|
+
position: 1, open: true
|
|
23
|
+
|
|
24
|
+
tabs.add_tab :omniauth,
|
|
25
|
+
t("omniauth", scope:),
|
|
26
|
+
form: Decidim::Toggle::UpdateOmniauthForm,
|
|
27
|
+
command: Decidim::Toggle::UpdateOmniauthCommand,
|
|
28
|
+
position: 2,
|
|
29
|
+
form_layout_partial: "decidim_toggle/system/organizations/tabs/omniauth_tab"
|
|
30
|
+
|
|
31
|
+
tabs.add_tab :emails,
|
|
32
|
+
t("emails", scope:),
|
|
33
|
+
form: Decidim::Toggle::UpdateEmailsForm,
|
|
34
|
+
command: Decidim::Toggle::UpdateEmailsCommand,
|
|
35
|
+
position: 3,
|
|
36
|
+
form_layout_partial: "decidim_toggle/system/organizations/tabs/emails_tab"
|
|
37
|
+
|
|
38
|
+
tabs.add_tab :language,
|
|
39
|
+
t("language", scope:),
|
|
40
|
+
form: Decidim::Toggle::UpdateLocaleForm,
|
|
41
|
+
command: Decidim::Toggle::UpdateLocaleCommand,
|
|
42
|
+
position: 4,
|
|
43
|
+
form_layout_partial: "decidim_toggle/system/organizations/tabs/language_tab"
|
|
44
|
+
|
|
45
|
+
tabs.add_tab :authorizations,
|
|
46
|
+
t("authorizations", scope:),
|
|
47
|
+
form: Decidim::Toggle::UpdateAuthorizationsForm,
|
|
48
|
+
command: Decidim::Toggle::UpdateAuthorizationsCommand,
|
|
49
|
+
position: 5,
|
|
50
|
+
form_layout_partial: "decidim_toggle/system/organizations/tabs/authorizations_tab"
|
|
51
|
+
|
|
52
|
+
tabs.add_tab :security,
|
|
53
|
+
t("security", scope:),
|
|
54
|
+
form: Decidim::Toggle::UpdateSecurityForm,
|
|
55
|
+
command: Decidim::Toggle::UpdateSecurityCommand,
|
|
56
|
+
form_layout_partial: "decidim_toggle/system/organizations/tabs/security_tab",
|
|
57
|
+
position: 6
|
|
58
|
+
|
|
59
|
+
tabs.add_tab :other,
|
|
60
|
+
t("file_upload", scope:),
|
|
61
|
+
form: Decidim::Toggle::UpdateFileUploadSettingsForm,
|
|
62
|
+
command: Decidim::Toggle::UpdateFileUploadSettingsCommand,
|
|
63
|
+
position: 7,
|
|
64
|
+
form_layout_partial: "decidim_toggle/system/organizations/tabs/file_upload_tab"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Decidim
|
|
4
|
+
module Toggle
|
|
5
|
+
# Form builder for add_tab form tabs. Renders all form attributes via #all_fields.
|
|
6
|
+
# Form classes can define class method collection_for_<attribute> to return [[value, label], ...] for collection inputs.
|
|
7
|
+
# Loaded from the engine after +require "decidim/core"+ so +Decidim::FormBuilder+ autoload works.
|
|
8
|
+
class SettingsFormBuilder < Decidim::FormBuilder
|
|
9
|
+
CALLOUT_CLASS_BY_TYPE = {
|
|
10
|
+
info: "info",
|
|
11
|
+
warning: "warning",
|
|
12
|
+
danger: "alert"
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
def informative_callouts
|
|
16
|
+
return "".html_safe unless object.class.include?(InformativeCallouts)
|
|
17
|
+
|
|
18
|
+
entries = object.visible_informative_callouts
|
|
19
|
+
return "".html_safe if entries.blank?
|
|
20
|
+
|
|
21
|
+
callouts = entries.map do |entry|
|
|
22
|
+
callout = @template.cell(
|
|
23
|
+
"decidim/announcement",
|
|
24
|
+
entry.message_for(object),
|
|
25
|
+
callout_class: CALLOUT_CLASS_BY_TYPE.fetch(entry.type)
|
|
26
|
+
)
|
|
27
|
+
@template.content_tag(:div, callout, class: InformativeCallouts::WRAPPER_CLASS)
|
|
28
|
+
end
|
|
29
|
+
safe_join(callouts)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def all_fields
|
|
33
|
+
fields = attribute_names.map do |name|
|
|
34
|
+
@template.content_tag(:div, input_field(name), class: field_wrapper_classes(name))
|
|
35
|
+
end
|
|
36
|
+
safe_join(fields)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Renders a subset of attributes (e.g. machine translation fields after a custom locale table).
|
|
40
|
+
def fields_for_names(*names)
|
|
41
|
+
names = names.flatten.map(&:to_s)
|
|
42
|
+
fields = names.filter_map do |name|
|
|
43
|
+
next unless object.class.respond_to?(:attribute_types) && object.class.attribute_types.has_key?(name)
|
|
44
|
+
|
|
45
|
+
@template.content_tag(:div, input_field(name.to_sym), class: field_wrapper_classes(name))
|
|
46
|
+
end
|
|
47
|
+
safe_join(fields)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def input_field(name)
|
|
53
|
+
name = name.to_sym
|
|
54
|
+
input_html =
|
|
55
|
+
if translatable_hash_attribute?(name)
|
|
56
|
+
translated_input_field(name)
|
|
57
|
+
else
|
|
58
|
+
build_input_field(name)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
helptext = helptext_for_attribute(name)
|
|
62
|
+
return input_html if helptext.blank?
|
|
63
|
+
|
|
64
|
+
# Render the help text under the field label (the upstream `Decidim::FormBuilder`
|
|
65
|
+
# typically renders the label inside the field HTML returned above).
|
|
66
|
+
@template.safe_join(
|
|
67
|
+
[
|
|
68
|
+
input_html,
|
|
69
|
+
@template.content_tag(:p, helptext, class: "field-helptext")
|
|
70
|
+
]
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def build_input_field(name)
|
|
75
|
+
type = attribute_type(name)
|
|
76
|
+
|
|
77
|
+
if (collection = collection_for(name))
|
|
78
|
+
if type == :array
|
|
79
|
+
collection_check_boxes(name, collection, :first, :last) do |b|
|
|
80
|
+
@template.content_tag(:div, b.check_box(checked: Array(object.public_send(name)).include?(b.value)) + b.label { b.text })
|
|
81
|
+
end
|
|
82
|
+
else
|
|
83
|
+
collection_radio_buttons(name, collection, :first, :last) do |b|
|
|
84
|
+
@template.content_tag(:div, b.radio_button + b.label { b.text })
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
else
|
|
88
|
+
options = field_html_options_for(name)
|
|
89
|
+
case type
|
|
90
|
+
when :string
|
|
91
|
+
name.to_s == "secondary_hosts" ? text_area(name, options) : text_field(name, options)
|
|
92
|
+
when :integer then number_field(name, options)
|
|
93
|
+
when :boolean then check_box(name, options)
|
|
94
|
+
else text_field(name, options)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def field_html_options_for(name)
|
|
100
|
+
attribute_disabled?(name) ? { disabled: true } : {}
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def field_wrapper_classes(name)
|
|
104
|
+
attribute_disabled?(name) ? "field is-disabled" : "field"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def attribute_disabled?(name)
|
|
108
|
+
name = name.to_sym
|
|
109
|
+
method = :"disabled_for_#{name}?"
|
|
110
|
+
return object.public_send(method) if object.respond_to?(method)
|
|
111
|
+
|
|
112
|
+
return object.attribute_disabled?(name) if object.respond_to?(:attribute_disabled?)
|
|
113
|
+
|
|
114
|
+
false
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def collection_for(attribute)
|
|
118
|
+
method = :"collection_for_#{attribute}"
|
|
119
|
+
object.class.respond_to?(method) ? object.class.public_send(method) : nil
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def attribute_names
|
|
123
|
+
return [] unless object.class.respond_to?(:attribute_types)
|
|
124
|
+
|
|
125
|
+
object.class.attribute_types.keys.reject do |k|
|
|
126
|
+
k.to_s == "id" || translatable_locale_field?(k.to_s)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def attribute_type(name)
|
|
131
|
+
return :string unless object.class.respond_to?(:attribute_types)
|
|
132
|
+
|
|
133
|
+
type_obj = object.class.attribute_types[name.to_s]
|
|
134
|
+
type_obj.respond_to?(:type) ? type_obj.type : :string
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# translatable_attribute adds a hash (:name) plus one String per locale (:name_en, …).
|
|
138
|
+
# Decidim forms use #translated (tabs or locale select); listing locale keys duplicates the UI.
|
|
139
|
+
def translatable_hash_attribute?(base)
|
|
140
|
+
return false unless object.class.include?(Decidim::TranslatableAttributes)
|
|
141
|
+
|
|
142
|
+
type = object.class.attribute_types[base.to_s]
|
|
143
|
+
type.respond_to?(:type) && type.type == :hash
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def translatable_locale_field?(key)
|
|
147
|
+
return false unless object.class.include?(Decidim::TranslatableAttributes)
|
|
148
|
+
|
|
149
|
+
Decidim.available_locales.any? do |locale|
|
|
150
|
+
suffix = locale.to_s.tr("-", "__")
|
|
151
|
+
next unless key.end_with?("_#{suffix}")
|
|
152
|
+
|
|
153
|
+
base = key.delete_suffix("_#{suffix}")
|
|
154
|
+
next if base.blank?
|
|
155
|
+
|
|
156
|
+
translatable_hash_attribute?(base)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def translated_input_field(name)
|
|
161
|
+
locale = Decidim.available_locales.first
|
|
162
|
+
locale_key = "#{name}_#{locale.to_s.tr("-", "__")}"
|
|
163
|
+
t = attribute_type(locale_key)
|
|
164
|
+
case t
|
|
165
|
+
when :"decidim/attributes/rich_text"
|
|
166
|
+
translated(:editor, name)
|
|
167
|
+
else
|
|
168
|
+
translated(:text_field, name)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def helptext_for_attribute(name)
|
|
173
|
+
attribute_name = name.to_s
|
|
174
|
+
|
|
175
|
+
if object.class.respond_to?(:module_config_name)
|
|
176
|
+
helptext = ModuleConfigI18n.translate_helptext(object.class.module_config_name, attribute_name)
|
|
177
|
+
return helptext if helptext.present?
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# When a form `mimic`s another ActiveModel (e.g. organization), `model_name`
|
|
181
|
+
# is expected to match i18n keys used across Decidim.
|
|
182
|
+
candidate_model_keys = []
|
|
183
|
+
candidate_model_keys << object.class.model_name.i18n_key if object.class.respond_to?(:model_name) && object.class.model_name.respond_to?(:i18n_key)
|
|
184
|
+
candidate_model_keys << object_name if respond_to?(:object_name)
|
|
185
|
+
|
|
186
|
+
candidate_model_keys.compact!
|
|
187
|
+
candidate_model_keys.uniq!
|
|
188
|
+
|
|
189
|
+
candidate_model_keys.each do |model_key|
|
|
190
|
+
# Preferred location avoids making `activemodel.attributes.<model>.<attr>` a Hash (which can break
|
|
191
|
+
# attribute name i18n lookups for the same key).
|
|
192
|
+
helptext = I18n.t("activemodel.attributes.#{model_key}.helptext.#{attribute_name}", default: nil)
|
|
193
|
+
return helptext if helptext.present?
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
nil
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Decidim
|
|
4
|
+
module Toggle
|
|
5
|
+
class SettingsTabItem
|
|
6
|
+
def initialize(identifier, label, options = {})
|
|
7
|
+
@identifier = identifier
|
|
8
|
+
@label = label
|
|
9
|
+
@form_layout_partial = options[:form_layout_partial]
|
|
10
|
+
@form_class = options[:form_class]
|
|
11
|
+
@command_class = options[:command_class]
|
|
12
|
+
@position = options[:position] || Float::INFINITY
|
|
13
|
+
@if = options[:if]
|
|
14
|
+
@open = options.fetch(:open, false)
|
|
15
|
+
@module_name = options[:module_name]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
attr_reader :identifier, :label, :form_layout_partial, :form_class, :command_class, :module_name
|
|
19
|
+
|
|
20
|
+
attr_accessor :position
|
|
21
|
+
|
|
22
|
+
def form_tab?
|
|
23
|
+
form_class.present? && command_class.present?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def open?
|
|
27
|
+
@open
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def visible?
|
|
31
|
+
return true if @if.nil? || @if
|
|
32
|
+
|
|
33
|
+
false
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Decidim
|
|
4
|
+
module Toggle
|
|
5
|
+
class DuplicateTabRegistrationError < StandardError; end
|
|
6
|
+
|
|
7
|
+
class SettingsTabRegistry
|
|
8
|
+
class << self
|
|
9
|
+
def register(name, &block)
|
|
10
|
+
registry = find(name) || create(name)
|
|
11
|
+
registry.configurations << block
|
|
12
|
+
registry
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def find(name)
|
|
16
|
+
all[name.to_sym]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def create(name)
|
|
20
|
+
all[name.to_sym] = new(name)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def destroy(name)
|
|
24
|
+
all[name.to_sym] = nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def all
|
|
30
|
+
@all ||= {}
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
attr_reader :configurations, :form_tabs, :module_configs, :registry_name
|
|
35
|
+
|
|
36
|
+
def initialize(name)
|
|
37
|
+
@registry_name = name.to_sym
|
|
38
|
+
@configurations = []
|
|
39
|
+
@form_tabs = {}
|
|
40
|
+
@module_configs = {}
|
|
41
|
+
@tab_to_module_name = {}
|
|
42
|
+
@configurations_applied = false
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def mark_configurations_applied!
|
|
46
|
+
@configurations_applied = true
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Runs registered +settings_tabs+ blocks so +register_form_tab+ / +module_configs+ are populated
|
|
50
|
+
# (e.g. for {Decidim::Toggle.config_for} before any HTTP request builds tabs).
|
|
51
|
+
def ensure_configurations_applied!
|
|
52
|
+
return if @configurations_applied
|
|
53
|
+
|
|
54
|
+
ctx = Object.new
|
|
55
|
+
ctx.define_singleton_method(:t) { |key, **kw| I18n.t(key, **kw) }
|
|
56
|
+
SettingsTabs.new(@registry_name).build_for(ctx)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Last registration for a given +identifier+ wins (used when an extension replaces a tab).
|
|
60
|
+
# When +module_name+ is set, last registration for that module name wins (same as tab id).
|
|
61
|
+
def register_form_tab(identifier, form_class, command_class, module_name: nil)
|
|
62
|
+
tid = identifier.to_sym
|
|
63
|
+
if (previous = @tab_to_module_name[tid])
|
|
64
|
+
@module_configs.delete(previous)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
if duplicate_tab_registration?(tid, form_class, command_class)
|
|
68
|
+
raise DuplicateTabRegistrationError,
|
|
69
|
+
"Tab :#{tid} is already registered with form #{@form_tabs[tid][:form]} " \
|
|
70
|
+
"and command #{@form_tabs[tid][:command]}; attempted #{form_class} / #{command_class}"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
@form_tabs[tid] = { form: form_class, command: command_class }
|
|
74
|
+
if module_name.blank?
|
|
75
|
+
@tab_to_module_name.delete(tid)
|
|
76
|
+
return
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
key = module_name.to_s
|
|
80
|
+
@module_configs[key] = {
|
|
81
|
+
form: form_class,
|
|
82
|
+
command: command_class,
|
|
83
|
+
tab_identifier: tid
|
|
84
|
+
}
|
|
85
|
+
@tab_to_module_name[tid] = key
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def form_tab(identifier)
|
|
89
|
+
form_tabs[identifier.to_sym]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# @return [Hash, nil] +:form+, +:command+, +:tab_identifier+ when an +add_tab+ registered this +module_name+
|
|
93
|
+
def form_tab_for_module(module_name)
|
|
94
|
+
ensure_configurations_applied!
|
|
95
|
+
module_configs[module_name.to_s]
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
def duplicate_tab_registration?(tid, form_class, command_class)
|
|
101
|
+
return false unless Rails.env.development? || Rails.env.test?
|
|
102
|
+
return false unless @form_tabs[tid]
|
|
103
|
+
|
|
104
|
+
existing = @form_tabs[tid]
|
|
105
|
+
existing[:form] != form_class || existing[:command] != command_class
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Decidim
|
|
4
|
+
module Toggle
|
|
5
|
+
#
|
|
6
|
+
# Builds a list of settings tabs from registered configurations (same pattern as Decidim::Menu).
|
|
7
|
+
#
|
|
8
|
+
class SettingsTabs
|
|
9
|
+
def initialize(name)
|
|
10
|
+
@name = name
|
|
11
|
+
@items = []
|
|
12
|
+
@removed_items = []
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def build_for(context, **options)
|
|
16
|
+
raise "Settings tabs #{@name} is not registered" if registry.blank?
|
|
17
|
+
|
|
18
|
+
registry.configurations.each do |configuration|
|
|
19
|
+
context.instance_exec(self, **options, &configuration)
|
|
20
|
+
end
|
|
21
|
+
registry.mark_configurations_applied!
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Tab with form + command: common layout renders form.all_fields and submits to generic controller.
|
|
25
|
+
# @param identifier [Symbol] Tab id
|
|
26
|
+
# @param label [String] Tab button label
|
|
27
|
+
# @param form [Class] Decidim::Form subclass (must respond to .from_model(organization))
|
|
28
|
+
# @param command [Class] Decidim::Command that receives (organization, form)
|
|
29
|
+
# @param options [Hash] :position, :if, :open,
|
|
30
|
+
# :form_layout_partial (optional full tab layout partial)
|
|
31
|
+
def add_tab(identifier, label, form:, **options)
|
|
32
|
+
command = options.fetch(:command)
|
|
33
|
+
options = { position: (1 + @items.length) }.merge(options)
|
|
34
|
+
|
|
35
|
+
module_name = options[:module_name]
|
|
36
|
+
registry.register_form_tab(identifier, form, command, module_name:)
|
|
37
|
+
@items << SettingsTabItem.new(identifier, label, options.merge(form_class: form, command_class: command))
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def remove_tab(identifier)
|
|
41
|
+
@removed_items << identifier
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def items
|
|
45
|
+
@items.reject! { |item| @removed_items.include?(item.identifier) }
|
|
46
|
+
@items.select(&:visible?).sort_by(&:position)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def registry
|
|
52
|
+
@registry ||= SettingsTabRegistry.find(@name)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|