decidim-admin 0.30.9 → 0.31.0.rc1

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 (269) hide show
  1. checksums.yaml +4 -4
  2. data/app/cells/decidim/admin/content_block/show.erb +1 -1
  3. data/app/commands/decidim/admin/content_blocks/update_content_block.rb +3 -12
  4. data/app/commands/decidim/admin/create_attachment.rb +0 -1
  5. data/app/commands/decidim/admin/destroy_participatory_space_private_user.rb +0 -10
  6. data/app/commands/decidim/admin/hide_menu_component.rb +1 -1
  7. data/app/commands/decidim/admin/publish_component.rb +0 -1
  8. data/app/commands/decidim/admin/unpublish_component.rb +0 -1
  9. data/app/commands/decidim/admin/unreport_user.rb +1 -1
  10. data/app/commands/decidim/admin/update_organization.rb +18 -3
  11. data/app/controllers/concerns/decidim/admin/content_blocks/landing_page_content_blocks.rb +1 -1
  12. data/app/controllers/concerns/decidim/admin/filterable.rb +1 -1
  13. data/app/controllers/decidim/admin/area_types_controller.rb +2 -2
  14. data/app/controllers/decidim/admin/areas_controller.rb +2 -2
  15. data/app/controllers/decidim/admin/block_user_controller.rb +2 -2
  16. data/app/controllers/decidim/admin/component_permissions_controller.rb +1 -1
  17. data/app/controllers/decidim/admin/components_controller.rb +2 -2
  18. data/app/controllers/decidim/admin/concerns/has_attachment_collections.rb +2 -2
  19. data/app/controllers/decidim/admin/concerns/has_attachments.rb +2 -2
  20. data/app/controllers/decidim/admin/concerns/has_private_users.rb +3 -3
  21. data/app/controllers/decidim/admin/concerns/has_private_users_csv_import.rb +1 -1
  22. data/app/controllers/decidim/admin/conflicts_controller.rb +1 -1
  23. data/app/controllers/decidim/admin/dashboard_controller.rb +0 -9
  24. data/app/controllers/decidim/admin/impersonations_controller.rb +1 -1
  25. data/app/controllers/decidim/admin/imports_controller.rb +1 -1
  26. data/app/controllers/decidim/admin/managed_users/promotions_controller.rb +1 -1
  27. data/app/controllers/decidim/admin/newsletters_controller.rb +4 -4
  28. data/app/controllers/decidim/admin/organization_appearance_controller.rb +1 -3
  29. data/app/controllers/decidim/admin/organization_controller.rb +32 -1
  30. data/app/controllers/decidim/admin/organization_external_domain_allowlist_controller.rb +1 -1
  31. data/app/controllers/decidim/admin/organization_homepage_controller.rb +4 -0
  32. data/app/controllers/decidim/admin/participatory_space/user_role_controller.rb +2 -2
  33. data/app/controllers/decidim/admin/reminders_controller.rb +1 -1
  34. data/app/controllers/decidim/admin/resource_permissions_controller.rb +1 -1
  35. data/app/controllers/decidim/admin/scope_types_controller.rb +2 -2
  36. data/app/controllers/decidim/admin/scopes_controller.rb +2 -2
  37. data/app/controllers/decidim/admin/share_tokens_controller.rb +2 -2
  38. data/app/controllers/decidim/admin/static_page_topics_controller.rb +2 -2
  39. data/app/controllers/decidim/admin/static_pages_controller.rb +2 -2
  40. data/app/controllers/decidim/admin/statistics_controller.rb +34 -0
  41. data/app/controllers/decidim/admin/taxonomies_controller.rb +2 -2
  42. data/app/controllers/decidim/admin/taxonomy_filters_controller.rb +2 -2
  43. data/app/controllers/decidim/admin/taxonomy_items_controller.rb +2 -2
  44. data/app/controllers/decidim/admin/users_controller.rb +1 -1
  45. data/app/forms/decidim/admin/block_user_form.rb +1 -1
  46. data/app/forms/decidim/admin/block_users_form.rb +1 -1
  47. data/app/forms/decidim/admin/organization_form.rb +35 -6
  48. data/app/helpers/decidim/admin/filterable_helper.rb +27 -8
  49. data/app/helpers/decidim/admin/icon_link_helper.rb +2 -3
  50. data/app/helpers/decidim/admin/icon_with_tooltip_helper.rb +1 -2
  51. data/app/helpers/decidim/admin/imports_helper.rb +0 -5
  52. data/app/helpers/decidim/admin/menu_helper.rb +3 -2
  53. data/app/helpers/decidim/admin/moderations/reports_helper.rb +2 -2
  54. data/app/helpers/decidim/admin/resource_permissions_helper.rb +13 -13
  55. data/app/helpers/decidim/admin/settings_helper.rb +12 -16
  56. data/app/jobs/decidim/admin/newsletter_job.rb +1 -0
  57. data/app/packs/entrypoints/decidim_admin.js +2 -3
  58. data/app/packs/entrypoints/decidim_admin.scss +1 -1
  59. data/app/packs/entrypoints/decidim_admin_overrides.scss +1 -1
  60. data/app/packs/src/decidim/admin/admin_autocomplete.js +12 -18
  61. data/app/packs/src/decidim/admin/application.js +11 -10
  62. data/app/packs/src/decidim/admin/budget_rule_toggler.component.js +84 -34
  63. data/app/packs/src/decidim/admin/controllers/slug/controller.js +25 -0
  64. data/app/packs/src/decidim/admin/controllers/slug/slug.test.js +239 -0
  65. data/app/packs/src/decidim/admin/css_preview.js +1 -1
  66. data/app/packs/src/decidim/admin/draggable-list.js +1 -1
  67. data/app/packs/src/decidim/admin/draggable-table.js +18 -36
  68. data/app/packs/src/decidim/admin/dynamic_fields.component.js +13 -12
  69. data/app/packs/src/decidim/admin/external_domain_allowlist.js +1 -1
  70. data/app/packs/src/decidim/admin/form.js +21 -4
  71. data/app/packs/src/decidim/admin/global_moderations.js +2 -2
  72. data/app/packs/src/decidim/admin/managed_moderated_users.js +2 -2
  73. data/app/packs/src/decidim/admin/moderations.js +1 -1
  74. data/app/packs/src/decidim/admin/newsletters.js +1 -1
  75. data/app/packs/src/decidim/admin/officializations.js +1 -1
  76. data/app/packs/src/decidim/admin/participatory_space_search.js +2 -2
  77. data/app/packs/src/decidim/admin/proposal_infinite_edit.js +1 -1
  78. data/app/packs/src/decidim/admin/resources_permissions.js +1 -1
  79. data/app/packs/src/decidim/admin/sort_list.component.js +2 -3
  80. data/app/packs/src/decidim/admin/sortable.js +1 -1
  81. data/app/packs/src/decidim/admin/sync_radio_buttons.js +1 -1
  82. data/app/packs/src/decidim/admin/tab_focus.js +1 -1
  83. data/app/packs/src/decidim/admin/text_copy.js +1 -1
  84. data/app/packs/src/decidim/admin/triadic_color_picker.js +1 -1
  85. data/app/packs/stylesheets/decidim/admin/_datepicker.scss +2 -2
  86. data/app/packs/stylesheets/decidim/admin/_dropdown.scss +12 -2
  87. data/app/packs/stylesheets/decidim/admin/_filters.scss +1 -57
  88. data/app/packs/stylesheets/decidim/admin/_forms.scss +14 -31
  89. data/app/packs/stylesheets/decidim/admin/_item_edit.scss +2 -10
  90. data/app/packs/stylesheets/decidim/admin/_item_show.scss +1 -5
  91. data/app/packs/stylesheets/decidim/admin/_legacy_foundation.scss +0 -412
  92. data/app/packs/stylesheets/decidim/admin/_main-nav.scss +75 -3
  93. data/app/packs/stylesheets/decidim/admin/_secondary-nav.scss +6 -10
  94. data/app/packs/stylesheets/decidim/admin/_select_picker.scss +1 -1
  95. data/app/packs/stylesheets/decidim/admin/_sidebar-menu.scss +4 -8
  96. data/app/packs/stylesheets/decidim/admin/_table-list.scss +26 -8
  97. data/app/packs/stylesheets/decidim/admin/_tabs.scss +4 -0
  98. data/app/packs/stylesheets/decidim/admin/_taxonomies.scss +7 -3
  99. data/app/packs/stylesheets/decidim/admin/application.scss +28 -29
  100. data/app/permissions/decidim/admin/permissions.rb +17 -5
  101. data/app/presenters/decidim/admin/dashboard_statistic_charts_presenter.rb +18 -0
  102. data/app/queries/decidim/admin/user_filter.rb +3 -3
  103. data/app/views/decidim/admin/area_types/index.html.erb +34 -11
  104. data/app/views/decidim/admin/areas/index.html.erb +32 -12
  105. data/app/views/decidim/admin/attachment_collections/index.html.erb +42 -14
  106. data/app/views/decidim/admin/attachments/index.html.erb +32 -13
  107. data/app/views/decidim/admin/block_user/bulk_new.html.erb +1 -1
  108. data/app/views/decidim/admin/block_user/new.html.erb +1 -1
  109. data/app/views/decidim/admin/components/_actions.html.erb +114 -54
  110. data/app/views/decidim/admin/components/_component_row.html.erb +5 -5
  111. data/app/views/decidim/admin/components/_components_table.html.erb +1 -1
  112. data/app/views/decidim/admin/components/_form.html.erb +4 -4
  113. data/app/views/decidim/admin/components/index.html.erb +12 -13
  114. data/app/views/decidim/admin/dashboard/show.html.erb +9 -21
  115. data/app/views/decidim/admin/exports/_dropdown.html.erb +16 -14
  116. data/app/views/decidim/admin/help_sections/_form.html.erb +1 -1
  117. data/app/views/decidim/admin/impersonatable_users/index.html.erb +68 -33
  118. data/app/views/decidim/admin/imports/_dropdown.html.erb +13 -11
  119. data/app/views/decidim/admin/imports/new.html.erb +13 -12
  120. data/app/views/decidim/admin/moderated_users/bulk_actions/_dropdown.html.erb +29 -32
  121. data/app/views/decidim/admin/moderated_users/index.html.erb +61 -21
  122. data/app/views/decidim/admin/moderations/_moderation-tr.html.erb +87 -32
  123. data/app/views/decidim/admin/moderations/_report.html.erb +2 -4
  124. data/app/views/decidim/admin/moderations/bulk_actions/_dropdown.html.erb +30 -34
  125. data/app/views/decidim/admin/moderations/index.html.erb +1 -1
  126. data/app/views/decidim/admin/moderations/reports/index.html.erb +1 -2
  127. data/app/views/decidim/admin/newsletters/index.html.erb +64 -21
  128. data/app/views/decidim/admin/officializations/index.html.erb +91 -37
  129. data/app/views/decidim/admin/organization/_form.html.erb +6 -92
  130. data/app/views/decidim/admin/organization/edit.html.erb +1 -5
  131. data/app/views/decidim/admin/organization/form/_admin_terms_of_service.html.erb +15 -0
  132. data/app/views/decidim/admin/organization/form/_basic_configuration.html.erb +31 -0
  133. data/app/views/decidim/admin/{organization_appearance → organization}/form/_colors.html.erb +9 -9
  134. data/app/views/decidim/admin/organization/form/_extra_features.html.erb +84 -0
  135. data/app/views/decidim/admin/{organization_appearance/form/_images.html.erb → organization/form/_logos.html.erb} +4 -4
  136. data/app/views/decidim/admin/organization/form/_welcome_notification.html.erb +25 -0
  137. data/app/views/decidim/admin/organization_external_domain_allowlist/_external_domain.html.erb +8 -6
  138. data/app/views/decidim/admin/participatory_space_private_users/index.html.erb +49 -22
  139. data/app/views/decidim/admin/resource_permissions/edit.html.erb +1 -1
  140. data/app/views/decidim/admin/scope_types/index.html.erb +31 -12
  141. data/app/views/decidim/admin/scopes/index.html.erb +40 -13
  142. data/app/views/decidim/admin/share_tokens/_form.html.erb +1 -1
  143. data/app/views/decidim/admin/share_tokens/edit.html.erb +1 -1
  144. data/app/views/decidim/admin/share_tokens/index.html.erb +69 -12
  145. data/app/views/decidim/admin/share_tokens/new.html.erb +1 -1
  146. data/app/views/decidim/admin/shared/_filters.html.erb +8 -10
  147. data/app/views/decidim/admin/shared/landing_page/_content_blocks.html.erb +14 -11
  148. data/app/views/decidim/admin/shared/landing_page_content_blocks/edit.html.erb +2 -4
  149. data/app/views/decidim/admin/static_page_topics/index.html.erb +31 -10
  150. data/app/views/decidim/admin/static_pages/_form.html.erb +1 -1
  151. data/app/views/decidim/admin/static_pages/_topic.html.erb +46 -16
  152. data/app/views/decidim/admin/statistics/_statistics.html.erb +13 -0
  153. data/app/views/decidim/admin/statistics/index.html.erb +6 -0
  154. data/app/views/decidim/admin/taxonomies/_form.html.erb +1 -1
  155. data/app/views/decidim/admin/taxonomies/_row.html.erb +24 -6
  156. data/app/views/decidim/admin/taxonomies/_table.html.erb +2 -2
  157. data/app/views/decidim/admin/taxonomies/_taxonomy_actions.html.erb +48 -15
  158. data/app/views/decidim/admin/taxonomies/edit.html.erb +10 -0
  159. data/app/views/decidim/admin/taxonomy_filters/_form.html.erb +1 -1
  160. data/app/views/decidim/admin/taxonomy_filters/_table.html.erb +52 -15
  161. data/app/views/decidim/admin/taxonomy_filters/new.html.erb +1 -1
  162. data/app/views/decidim/admin/taxonomy_filters_selector/_component_table.html.erb +38 -23
  163. data/app/views/decidim/admin/taxonomy_items/_form.html.erb +1 -1
  164. data/app/views/decidim/admin/users/index.html.erb +48 -24
  165. data/app/views/layouts/decidim/admin/_application.html.erb +3 -6
  166. data/app/views/layouts/decidim/admin/_header.html.erb +3 -1
  167. data/app/views/layouts/decidim/admin/_js_configuration.html.erb +11 -12
  168. data/app/views/layouts/decidim/admin/_sidebar_menu.html.erb +1 -1
  169. data/app/views/layouts/decidim/admin/_title_bar.html.erb +4 -4
  170. data/app/views/layouts/decidim/admin/_title_bar_responsive.html.erb +4 -4
  171. data/app/views/layouts/decidim/admin/insights.html.erb +6 -0
  172. data/config/assets.rb +2 -2
  173. data/config/locales/ar.yml +26 -96
  174. data/config/locales/bg.yml +37 -114
  175. data/config/locales/bs-BA.yml +7 -62
  176. data/config/locales/ca-IT.yml +79 -145
  177. data/config/locales/ca.yml +79 -145
  178. data/config/locales/cs.yml +61 -134
  179. data/config/locales/de.yml +82 -137
  180. data/config/locales/el.yml +28 -132
  181. data/config/locales/en.yml +66 -132
  182. data/config/locales/eo.yml +1 -8
  183. data/config/locales/es-MX.yml +78 -144
  184. data/config/locales/es-PY.yml +78 -144
  185. data/config/locales/es.yml +87 -153
  186. data/config/locales/eu.yml +98 -164
  187. data/config/locales/fi-plain.yml +71 -137
  188. data/config/locales/fi.yml +69 -135
  189. data/config/locales/fr-CA.yml +83 -185
  190. data/config/locales/fr.yml +83 -185
  191. data/config/locales/ga-IE.yml +14 -39
  192. data/config/locales/gl.yml +22 -103
  193. data/config/locales/he-IL.yml +1 -15
  194. data/config/locales/hu.yml +40 -108
  195. data/config/locales/id-ID.yml +10 -70
  196. data/config/locales/is-IS.yml +6 -43
  197. data/config/locales/it.yml +74 -122
  198. data/config/locales/ja.yml +80 -146
  199. data/config/locales/kaa.yml +3 -22
  200. data/config/locales/ko.yml +15 -77
  201. data/config/locales/lb.yml +25 -104
  202. data/config/locales/lt.yml +35 -108
  203. data/config/locales/lv.yml +13 -84
  204. data/config/locales/nl.yml +25 -104
  205. data/config/locales/no.yml +24 -103
  206. data/config/locales/pl.yml +38 -116
  207. data/config/locales/pt-BR.yml +26 -295
  208. data/config/locales/pt.yml +25 -104
  209. data/config/locales/ro-RO.yml +50 -112
  210. data/config/locales/ru.yml +11 -67
  211. data/config/locales/sk.yml +9 -842
  212. data/config/locales/sl.yml +1 -24
  213. data/config/locales/sq-AL.yml +14 -55
  214. data/config/locales/sr-CS.yml +14 -69
  215. data/config/locales/sv.yml +89 -144
  216. data/config/locales/th-TH.yml +1 -15
  217. data/config/locales/tr-TR.yml +37 -277
  218. data/config/locales/uk.yml +9 -52
  219. data/config/locales/val-ES.yml +0 -1
  220. data/config/locales/zh-CN.yml +11 -82
  221. data/config/locales/zh-TW.yml +33 -106
  222. data/config/routes.rb +5 -11
  223. data/lib/decidim/admin/engine.rb +5 -1
  224. data/lib/decidim/admin/form_builder.rb +4 -2
  225. data/lib/decidim/admin/menu.rb +32 -16
  226. data/lib/decidim/admin/test/admin_participatory_space_access_examples.rb +0 -20
  227. data/lib/decidim/admin/test/filterable_examples.rb +8 -18
  228. data/lib/decidim/admin/test/filters_participatory_space_users_examples.rb +1 -1
  229. data/lib/decidim/admin/test/invite_participatory_space_admins_shared_examples.rb +4 -4
  230. data/lib/decidim/admin/test/invite_participatory_space_collaborators_shared_examples.rb +2 -2
  231. data/lib/decidim/admin/test/invite_participatory_space_moderators_shared_examples.rb +9 -6
  232. data/lib/decidim/admin/test/invite_participatory_space_users_shared_context.rb +1 -0
  233. data/lib/decidim/admin/test/manage_attachment_collections_examples.rb +5 -1
  234. data/lib/decidim/admin/test/manage_attachments_examples.rb +4 -11
  235. data/lib/decidim/admin/test/manage_component_permissions_examples.rb +31 -4
  236. data/lib/decidim/admin/test/manage_hide_content_examples.rb +0 -1
  237. data/lib/decidim/admin/test/manage_moderations_examples.rb +12 -5
  238. data/lib/decidim/admin/test/manage_participatory_space_publications_examples.rb +12 -6
  239. data/lib/decidim/admin/test/manage_resource_soft_deletion_examples.rb +19 -4
  240. data/lib/decidim/admin/test/manage_taxonomy_filters_examples.rb +28 -8
  241. data/lib/decidim/admin/version.rb +1 -1
  242. metadata +22 -37
  243. data/app/commands/decidim/admin/process_user_group_verification_csv.rb +0 -44
  244. data/app/commands/decidim/admin/reject_user_group.rb +0 -42
  245. data/app/commands/decidim/admin/update_organization_appearance.rb +0 -39
  246. data/app/commands/decidim/admin/update_user_groups.rb +0 -44
  247. data/app/commands/decidim/admin/verify_user_group.rb +0 -43
  248. data/app/controllers/concerns/decidim/admin/user_groups/filterable.rb +0 -45
  249. data/app/controllers/concerns/decidim/admin/user_groups.rb +0 -24
  250. data/app/controllers/decidim/admin/metrics_controller.rb +0 -23
  251. data/app/controllers/decidim/admin/user_groups_controller.rb +0 -76
  252. data/app/controllers/decidim/admin/user_groups_csv_verifications_controller.rb +0 -36
  253. data/app/forms/decidim/admin/organization_appearance_form.rb +0 -88
  254. data/app/forms/decidim/admin/user_group_csv_verification_form.rb +0 -25
  255. data/app/jobs/decidim/admin/destroy_private_users_follows_job.rb +0 -37
  256. data/app/jobs/decidim/admin/verify_user_group_from_csv_job.rb +0 -30
  257. data/app/packs/src/decidim/admin/welcome_notification.js +0 -32
  258. data/app/packs/stylesheets/decidim/admin/_bulk_actions.scss +0 -16
  259. data/app/presenters/decidim/admin/dashboard_metric_charts_presenter.rb +0 -33
  260. data/app/queries/decidim/admin/user_groups_evaluation.rb +0 -56
  261. data/app/views/decidim/admin/metrics/_metrics.html.erb +0 -16
  262. data/app/views/decidim/admin/metrics/index.html.erb +0 -7
  263. data/app/views/decidim/admin/organization_appearance/_form.html.erb +0 -100
  264. data/app/views/decidim/admin/organization_appearance/edit.html.erb +0 -18
  265. data/app/views/decidim/admin/organization_appearance/form/_header_snippets.html.erb +0 -17
  266. data/app/views/decidim/admin/user_groups/index.html.erb +0 -68
  267. data/app/views/decidim/admin/user_groups_csv_verifications/new.html.erb +0 -31
  268. data/lib/decidim/admin/test/admin_participatory_space_component_access_examples.rb +0 -36
  269. /data/app/views/decidim/admin/{organization_appearance → organization}/form/_minimap.html.erb +0 -0
@@ -1,50 +1,100 @@
1
+ /**
2
+ * BudgetRuleTogglerComponent
3
+ *
4
+ * Handles showing and hiding rule-specific input containers
5
+ * based on the selected radio option.
6
+ */
1
7
  export default class BudgetRuleTogglerComponent {
8
+
9
+ /**
10
+ * @param {Object} options - Configuration options
11
+ * @param {HTMLInputElement[]} options.ruleRadios - Array of radio inputs controlling the rules
12
+ * @param {Record<string, string[]>} options.mapping - Mapping from radio values to selectors of containers to show
13
+ */
2
14
  constructor(options = {}) {
3
- this.ruleCheckboxes = options.ruleCheckboxes;
4
- this._runAll();
15
+ this.ruleRadios = options.ruleRadios;
16
+ this.mapping = options.mapping || {};
5
17
  }
6
18
 
7
- _runAll() {
8
- this.ruleCheckboxes.each((_i, checkbox) => {
9
- this._bindEvent(checkbox);
10
- this.run(checkbox);
11
- });
19
+ /**
20
+ * Initialize the component (bind events + run initial state).
21
+ * @returns {void}
22
+ */
23
+ init() {
24
+ this._bindEvents();
25
+ this._runInitial();
12
26
  }
13
27
 
14
- _bindEvent(target) {
15
- $(target).on("change", (event) => {
16
- this.run(event.target);
28
+ /**
29
+ * Bind change events on all radios
30
+ * @private
31
+ * @returns {void}
32
+ */
33
+ _bindEvents() {
34
+ this.ruleRadios.forEach((radio) => {
35
+ radio.addEventListener("change", (event) => {
36
+ this._run(event.target);
37
+ });
17
38
  });
18
39
  }
19
40
 
20
- run(target) {
21
- this.toggleTextInput(target);
22
-
23
- if ($(target).prop("checked")) {
24
- this.ruleCheckboxes.filter(
25
- (_i, checkbox) => {
26
- return checkbox !== target;
27
- }).prop("checked", false).each(
28
- (_i, checkbox) => {
29
- this.toggleTextInput(checkbox);
30
- });
41
+ /**
42
+ * Run toggler logic on page load
43
+ * @private
44
+ * @returns {void}
45
+ */
46
+ _runInitial() {
47
+ const checked = this.ruleRadios.find((radio) => radio.checked);
48
+ if (checked) {
49
+ this._run(checked);
50
+ } else {
51
+ this._hideAll();
31
52
  }
32
53
  }
33
54
 
34
- toggleTextInput(target) {
35
- const container = $(target).closest("div");
36
- if (container.length < 1) {
37
- return;
38
- }
39
- const containerClassPrefix = container.attr("class").
40
- replace(/^vote_rule_/, "vote_").
41
- replace(/_enabled_container$/, "");
42
- const input = $(`[class^="${containerClassPrefix}"][class$="_container"]`);
55
+ /**
56
+ * Show the containers associated with the selected radio
57
+ * @param {HTMLInputElement} target - The radio input that triggered the change
58
+ * @private
59
+ * @returns {void}
60
+ */
61
+ _run(target) {
62
+ this._hideAll();
43
63
 
44
- if ($(target).prop("checked")) {
45
- input.slideDown();
46
- } else {
47
- input.slideUp();
64
+ // Normalize radio value (snake_case → camelCase)
65
+ const rawValue = target.value;
66
+ const camelValue = rawValue.replace(/_([a-z])/g, (_match, letter) => letter.toUpperCase());
67
+
68
+ const selectors = this.mapping[camelValue] || [];
69
+
70
+ selectors.forEach((selector) => this._show(selector));
71
+ }
72
+
73
+ /**
74
+ * Hide all containers referenced in the mapping
75
+ * @private
76
+ * @returns {void}
77
+ */
78
+ _hideAll() {
79
+ const allSelectors = Object.values(this.mapping).flat();
80
+ allSelectors.forEach((selector) => {
81
+ const el = document.querySelector(selector);
82
+ if (el) {
83
+ el.style.display = "none";
84
+ }
85
+ });
86
+ }
87
+
88
+ /**
89
+ * Show a container by selector
90
+ * @param {string} selector - CSS selector of the container to show
91
+ * @private
92
+ * @returns {void}
93
+ */
94
+ _show(selector) {
95
+ const el = document.querySelector(selector);
96
+ if (el) {
97
+ el.style.display = "";
48
98
  }
49
99
  }
50
100
  }
@@ -0,0 +1,25 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ connect() {
5
+ this.input = this.element.querySelector("input");
6
+ this.target = this.element.querySelector("span.slug-url-value");
7
+ this.boundUpdate = null;
8
+
9
+ if (this.input) {
10
+ this.boundUpdate = this.slugUpdater.bind(this);
11
+ this.input.addEventListener("keyup", this.boundUpdate)
12
+ }
13
+ }
14
+
15
+ disconnect() {
16
+ if (this.boundUpdate !== null) {
17
+ this.input.removeEventListener("keyup", this.boundUpdate)
18
+ this.boundUpdate = null;
19
+ }
20
+ }
21
+
22
+ slugUpdater(event) {
23
+ this.target.innerHTML = event.target.value;
24
+ }
25
+ }
@@ -0,0 +1,239 @@
1
+ /* global jest */
2
+
3
+ import { Application } from "@hotwired/stimulus"
4
+ import SlugController from "./controller"
5
+
6
+ describe("SlugController", () => {
7
+ let application = null;
8
+ let controller = null;
9
+ let element = null;
10
+ let input = null;
11
+ let target = null;
12
+
13
+ beforeEach(() => {
14
+ // Set up the DOM structure
15
+ document.body.innerHTML = `
16
+ <div data-controller="slug" class="slug-wrapper">
17
+ <input type="text" placeholder="Enter slug" />
18
+ <span class="slug-url-value"></span>
19
+ </div>
20
+ `
21
+
22
+ element = document.querySelector('[data-controller="slug"]')
23
+ input = element.querySelector("input")
24
+ target = element.querySelector("span.slug-url-value")
25
+
26
+ // Set up Stimulus application
27
+ application = Application.start()
28
+ application.register("slug", SlugController)
29
+
30
+ return new Promise((resolve) => {
31
+ setTimeout(() => {
32
+ controller = application.getControllerForElementAndIdentifier(element, "slug")
33
+ resolve();
34
+ }, 0);
35
+ });
36
+ })
37
+
38
+ afterEach(() => {
39
+ controller.disconnect()
40
+ application.stop()
41
+ document.body.innerHTML = ""
42
+ })
43
+
44
+ describe("connect", () => {
45
+ it("finds and stores the input element", () => {
46
+ expect(controller.input).toBe(input)
47
+ })
48
+
49
+ it("finds and stores the target element", () => {
50
+ expect(controller.target).toBe(target)
51
+ })
52
+
53
+ it("adds keyup event listener to input when input exists", () => {
54
+ const addEventListenerSpy = jest.spyOn(input, "addEventListener")
55
+
56
+ controller.connect()
57
+
58
+ expect(addEventListenerSpy).toHaveBeenCalledWith("keyup", controller.boundUpdate)
59
+ })
60
+
61
+ it("binds the slugUpdater method correctly", () => {
62
+ controller.connect()
63
+
64
+ expect(controller.boundUpdate).toBeDefined()
65
+ expect(typeof controller.boundUpdate).toBe("function")
66
+ })
67
+
68
+ it("handles missing input gracefully", () => {
69
+ // Remove input from DOM
70
+ input.remove()
71
+
72
+ controller.disconnect()
73
+ controller.connect()
74
+
75
+ expect(() => controller.connect()).not.toThrow()
76
+ expect(controller.boundUpdate).toBeNull()
77
+ })
78
+ })
79
+
80
+ describe("disconnect", () => {
81
+ beforeEach(() => {
82
+ controller.connect()
83
+ })
84
+
85
+ it("removes keyup event listener when boundUpdate exists", () => {
86
+ const removeEventListenerSpy = jest.spyOn(controller.input, "removeEventListener")
87
+
88
+ let bound = controller.boundUpdate;
89
+
90
+ controller.disconnect()
91
+
92
+ expect(removeEventListenerSpy).toHaveBeenCalledWith("keyup", bound)
93
+ })
94
+
95
+ it("handles disconnect when boundUpdate is undefined", () => {
96
+ // eslint-disable-next-line no-undefined
97
+ controller.boundUpdate = undefined
98
+
99
+ expect(() => controller.disconnect()).not.toThrow()
100
+ })
101
+ })
102
+
103
+ describe("slugUpdater", () => {
104
+ beforeEach(() => {
105
+ controller.connect()
106
+ })
107
+
108
+ it("updates target innerHTML with input value", () => {
109
+ const mockEvent = {
110
+ target: {
111
+ value: "test-slug"
112
+ }
113
+ }
114
+
115
+ controller.slugUpdater(mockEvent)
116
+
117
+ expect(target.innerHTML).toBe("test-slug")
118
+ })
119
+
120
+ it("handles empty input value", () => {
121
+ const mockEvent = {
122
+ target: {
123
+ value: ""
124
+ }
125
+ }
126
+
127
+ controller.slugUpdater(mockEvent)
128
+
129
+ expect(target.innerHTML).toBe("")
130
+ })
131
+
132
+ it("handles special characters in input value", () => {
133
+ const mockEvent = {
134
+ target: {
135
+ value: "test-slug-with-special-chars-123"
136
+ }
137
+ }
138
+
139
+ controller.slugUpdater(mockEvent)
140
+
141
+ expect(target.innerHTML).toBe("test-slug-with-special-chars-123")
142
+ })
143
+ })
144
+
145
+ describe("integration tests", () => {
146
+ beforeEach(() => {
147
+ controller.connect()
148
+ })
149
+
150
+ it("updates target when typing in input", () => {
151
+ input.value = "my-new-slug"
152
+
153
+ // Simulate keyup event
154
+ const keyupEvent = new KeyboardEvent("keyup", { bubbles: true })
155
+ Reflect.defineProperty(keyupEvent, "target", {
156
+ value: input,
157
+ enumerable: true
158
+ })
159
+
160
+ input.dispatchEvent(keyupEvent)
161
+
162
+ expect(target.innerHTML).toBe("my-new-slug")
163
+ })
164
+
165
+ it("updates target multiple times as user types", () => {
166
+ const testValues = ["m", "my", "my-", "my-s", "my-sl", "my-slug"]
167
+
168
+ testValues.forEach((value) => {
169
+ input.value = value
170
+ const keyupEvent = new KeyboardEvent("keyup", { bubbles: true })
171
+ Reflect.defineProperty(keyupEvent, "target", {
172
+ value: input,
173
+ enumerable: true
174
+ })
175
+ input.dispatchEvent(keyupEvent)
176
+
177
+ expect(target.innerHTML).toBe(value)
178
+ })
179
+ })
180
+ })
181
+
182
+ describe("edge cases", () => {
183
+ it("handles DOM structure without input", async () => {
184
+ // Stop the current application to avoid conflicts
185
+ application.stop()
186
+
187
+ // Set up new DOM without input
188
+ document.body.innerHTML = `
189
+ <div data-controller="slug" class="slug-wrapper">
190
+ <span class="slug-url-value"></span>
191
+ </div>
192
+ `
193
+
194
+ // Start a fresh application
195
+ application = Application.start()
196
+ application.register("slug", SlugController)
197
+
198
+ const elementWithoutInput = document.querySelector('[data-controller="slug"]')
199
+ let controllerWithoutInput = null
200
+
201
+ await new Promise((resolve) => {
202
+ setTimeout(() => {
203
+ controllerWithoutInput = application.getControllerForElementAndIdentifier(elementWithoutInput, "slug")
204
+ resolve();
205
+ }, 0);
206
+ });
207
+
208
+ expect(() => controllerWithoutInput.connect()).not.toThrow()
209
+ expect(controllerWithoutInput.input).toBeNull()
210
+ })
211
+
212
+ it("handles DOM structure without target", async () => {
213
+ document.body.innerHTML = `
214
+ <div data-controller="slug" class="slug-wrapper">
215
+ <input type="text" placeholder="Enter slug" />
216
+ </div>
217
+ `
218
+
219
+ const elementWithoutTarget = document.querySelector('[data-controller="slug"]')
220
+ let controllerWithoutInput = null
221
+
222
+ await new Promise((resolve) => {
223
+ setTimeout(() => {
224
+ controllerWithoutInput = application.getControllerForElementAndIdentifier(elementWithoutTarget, "slug")
225
+ resolve();
226
+ }, 0);
227
+ });
228
+
229
+
230
+ controllerWithoutInput.connect()
231
+
232
+ expect(controllerWithoutInput.target).toBeNull()
233
+
234
+ // Should not throw when trying to update nonexistent target
235
+ const mockEvent = { target: { value: "test" } }
236
+ expect(() => controllerWithoutInput.slugUpdater(mockEvent)).toThrow()
237
+ })
238
+ })
239
+ })
@@ -23,7 +23,7 @@
23
23
  // for example:
24
24
  // strong[data-css-example]:color:#0000ff; strong[data-css-example]:backgroundColor:#eeeeee;
25
25
  //
26
- window.addEventListener("DOMContentLoaded", () => {
26
+ document.addEventListener("turbo:load", () => {
27
27
  document.querySelectorAll("input[data-css-preview=true]").forEach((element) => {
28
28
  element.addEventListener("change", (event) => {
29
29
  const updateRules = event.target.dataset.cssPreviewUpdates.split(";");
@@ -10,7 +10,7 @@ export default function createSortableList(lists) {
10
10
  }
11
11
 
12
12
  // Once in DOM
13
- $(() => {
13
+ document.addEventListener("turbo:load", () => {
14
14
  const $draggables = $(".draggable-list")
15
15
  let draggablesClassNames = []
16
16
 
@@ -4,48 +4,30 @@ import createSortList from "src/decidim/admin/sort_list.component"
4
4
  * Draggable table
5
5
  *
6
6
  * This script is used to make a table draggable.
7
- * It works with the following data attributes:
7
+ * It works with two data attributes:
8
8
  *
9
9
  * - data-draggable-table: The table that will be draggable.
10
10
  * - data-sort-url: The URL where the order will be sent.
11
- * - data-draggable-handle: (optional) CSS selector for the drag handle element.
12
- * When specified, dragging can only be initiated from this element.
13
- * - data-draggable-placeholder: (optional) HTML for the placeholder element.
14
11
  */
15
- document.addEventListener("DOMContentLoaded", () => {
16
- document.querySelectorAll("[data-draggable-table]").forEach((container) => {
17
- const options = {
18
- forcePlaceholderSize: true,
19
- onSortUpdate: ($children) => {
20
- const children = $children.toArray();
12
+ document.addEventListener("turbo:load", () => {
13
+ createSortList("[data-draggable-table]", {
14
+ onSortUpdate: ($children) => {
15
+ const children = $children.toArray();
21
16
 
22
- if (children.length === 0) {
23
- return;
24
- }
25
-
26
- const parent = children[0].parentNode;
27
- const sortUrl = parent.dataset.sortUrl;
28
- const order = children.map((child) => child.dataset.recordId);
29
-
30
- if (sortUrl && sortUrl !== "#") {
31
- $.ajax({
32
- method: "PUT",
33
- url: sortUrl,
34
- contentType: "application/json",
35
- data: JSON.stringify({ order_ids: order }) // eslint-disable-line camelcase
36
- });
37
- }
17
+ if (children.length === 0) {
18
+ return;
38
19
  }
39
- };
40
20
 
41
- // Read optional configuration from data attributes
42
- if (container.dataset.draggableHandle) {
43
- options.handle = container.dataset.draggableHandle;
44
- }
45
- if (container.dataset.draggablePlaceholder) {
46
- options.placeholder = container.dataset.draggablePlaceholder;
47
- }
21
+ const parent = children[0].parentNode;
22
+ const sortUrl = parent.dataset.sortUrl;
23
+ const order = children.map((child) => child.dataset.recordId);
48
24
 
49
- createSortList(container, options);
25
+ $.ajax({
26
+ method: "PUT",
27
+ url: sortUrl,
28
+ contentType: "application/json",
29
+ data: JSON.stringify({ order_ids: order }) // eslint-disable-line camelcase
30
+ });
31
+ }
50
32
  });
51
- });
33
+ })
@@ -34,9 +34,12 @@ class DynamicFieldsComponent {
34
34
  }
35
35
 
36
36
  $.fn.template = function(placeholder, value) {
37
+ // Clean up the template HTML to ensure it only contains elements.
38
+ const $template = $(this).filter((_idx, el) => el.nodeType === Node.ELEMENT_NODE);
39
+
37
40
  // See the comment below in the `_addField()` method regarding the
38
41
  // `<template>` tag support in IE11.
39
- const $subtemplate = $(this).find("template, .decidim-template");
42
+ const $subtemplate = $template.find("template, .decidim-template");
40
43
 
41
44
  if ($subtemplate.length > 0) {
42
45
  $subtemplate.html((index, oldHtml) => $(oldHtml).template(placeholder, value)[0].outerHTML);
@@ -44,7 +47,7 @@ class DynamicFieldsComponent {
44
47
 
45
48
  // Handle those subtemplates that are mapped with the `data-template`
46
49
  // attribute. This is also because of the IE11 support.
47
- const $subtemplateParents = $(this).find("[data-template]");
50
+ const $subtemplateParents = $template.find("[data-template]");
48
51
 
49
52
  if ($subtemplateParents.length > 0) {
50
53
  $subtemplateParents.each((_i, elem) => {
@@ -64,15 +67,15 @@ class DynamicFieldsComponent {
64
67
  });
65
68
  }
66
69
 
67
- $(this).replaceAttribute("id", placeholder, value);
68
- $(this).replaceAttribute("name", placeholder, value);
69
- $(this).replaceAttribute("data-tabs-content", placeholder, value);
70
- $(this).replaceAttribute("for", placeholder, value);
71
- $(this).replaceAttribute("tabs_id", placeholder, value);
72
- $(this).replaceAttribute("href", placeholder, value);
73
- $(this).replaceAttribute("value", placeholder, value);
70
+ $template.replaceAttribute("id", placeholder, value);
71
+ $template.replaceAttribute("name", placeholder, value);
72
+ $template.replaceAttribute("data-tabs-content", placeholder, value);
73
+ $template.replaceAttribute("for", placeholder, value);
74
+ $template.replaceAttribute("tabs_id", placeholder, value);
75
+ $template.replaceAttribute("href", placeholder, value);
76
+ $template.replaceAttribute("value", placeholder, value);
74
77
 
75
- return this;
78
+ return $template;
76
79
  }
77
80
  }
78
81
 
@@ -147,9 +150,7 @@ class DynamicFieldsComponent {
147
150
  // as they are submitted with them.
148
151
  $template = $wrapper.children(`template, ${templateClass}`);
149
152
  }
150
-
151
153
  const $newField = $($template.html()).template(this.placeholderId, this._getUID());
152
-
153
154
  $newField.find("ul.tabs").attr("data-tabs", true);
154
155
 
155
156
  const $lastQuestion = $container.find(this.fieldSelector).last()
@@ -3,7 +3,7 @@ import AutoLabelByPositionComponent from "src/decidim/admin/auto_label_by_positi
3
3
  import createDynamicFields from "src/decidim/admin/dynamic_fields.component"
4
4
  import createSortList from "src/decidim/admin/sort_list.component"
5
5
 
6
- $(() => {
6
+ document.addEventListener("turbo:load", () => {
7
7
  const dynamicFieldDefinitions = [
8
8
  {
9
9
  placeHolderId: "external-domain-id",
@@ -4,12 +4,29 @@ import BudgetRuleTogglerComponent from "src/decidim/admin/budget_rule_toggler.co
4
4
 
5
5
  // Checks if the form contains fields with special CSS classes added in
6
6
  // Decidim::Admin::SettingsHelper and acts accordingly.
7
- $(() => {
8
- const budgetRuleToggler = new BudgetRuleTogglerComponent({
9
- ruleCheckboxes: $("input[id^='component_settings_vote_rule_']")
7
+ document.addEventListener("turbo:load", () => {
8
+
9
+ const budgetTogglerRadios = Array.from(
10
+ document.querySelectorAll(
11
+ "input[type='radio'][name='component[settings][voting_rule]']"
12
+ )
13
+ );
14
+
15
+ const budgetTogglerMapping = {
16
+ thresholdPercent: [".vote_threshold_percent_container"],
17
+ minimumProjects: [".vote_minimum_budget_projects_number_container"],
18
+ selectedProjects: [
19
+ ".vote_selected_projects_minimum_container",
20
+ ".vote_selected_projects_maximum_container"
21
+ ]
22
+ };
23
+
24
+ const budgetToggler = new BudgetRuleTogglerComponent({
25
+ ruleRadios: budgetTogglerRadios,
26
+ mapping: budgetTogglerMapping
10
27
  });
11
28
 
12
- budgetRuleToggler.run();
29
+ budgetToggler.init();
13
30
 
14
31
  // Prevents readonly containers from being modified.
15
32
  const $readonlyContainer = $(".readonly_container input");
@@ -2,7 +2,7 @@
2
2
  /* eslint no-unused-vars: 0 */
3
3
  /* eslint id-length: ["error", { "exceptions": ["e"] }] */
4
4
 
5
- document.addEventListener("DOMContentLoaded", () => {
5
+ document.addEventListener("turbo:load", () => {
6
6
  const selectedModerationsCount = () => {
7
7
  return document.querySelectorAll(".table-list .js-check-all-moderations:checked").length;
8
8
  };
@@ -69,7 +69,7 @@ document.addEventListener("DOMContentLoaded", () => {
69
69
  hideBulkActionForms();
70
70
  bulkActionsButton.classList.add("hide");
71
71
 
72
- document.querySelectorAll("#js-bulk-actions-dropdown ul li button").forEach((button) => {
72
+ document.querySelectorAll("#js-bulk-actions-dropdown li button").forEach((button) => {
73
73
  button.addEventListener("click", (event) => {
74
74
  const bulkActionsDropdown = document.getElementById("js-bulk-actions-dropdown");
75
75
  bulkActionsDropdown.classList.remove("is-open");
@@ -2,7 +2,7 @@
2
2
  /* eslint no-unused-vars: 0 */
3
3
  /* eslint id-length: ["error", { "exceptions": ["e"] }] */
4
4
 
5
- document.addEventListener("DOMContentLoaded", () => {
5
+ document.addEventListener("turbo:load", () => {
6
6
  const selectedModeratedUsersCount = () => {
7
7
  return document.querySelectorAll(".table-list .js-check-all-moderated_users:checked").length;
8
8
  };
@@ -69,7 +69,7 @@ document.addEventListener("DOMContentLoaded", () => {
69
69
  hideBulkActionForms();
70
70
  bulkActionsButton.classList.add("hide");
71
71
 
72
- document.querySelectorAll("#js-bulk-actions-dropdown ul li button").forEach((button) => {
72
+ document.querySelectorAll("#js-bulk-actions-dropdown li button").forEach((button) => {
73
73
  button.addEventListener("click", (event) => {
74
74
  const bulkActionsDropdown = document.getElementById("js-bulk-actions-dropdown");
75
75
  bulkActionsDropdown.classList.remove("is-open");
@@ -1,4 +1,4 @@
1
- $(() => {
1
+ document.addEventListener("turbo:load", () => {
2
2
  const $moderationDetails = $(".moderation-details");
3
3
  const $toggleContentButton = $moderationDetails.find(".toggle-content");
4
4
  const $reportedContent = $moderationDetails.find(".reported-content");
@@ -1,6 +1,6 @@
1
1
  import TomSelect from "tom-select/dist/cjs/tom-select.popular";
2
2
 
3
- document.addEventListener("DOMContentLoaded", () => {
3
+ document.addEventListener("turbo:load", () => {
4
4
  const isOnSelectRecipientsPage = window.location.pathname.includes("/select_recipients_to_deliver");
5
5
 
6
6
  const selectors = {
@@ -1,4 +1,4 @@
1
- $(() => {
1
+ document.addEventListener("turbo:load", () => {
2
2
  const $modal = $("#show-email-modal");
3
3
 
4
4
  if ($modal.length === 0) {
@@ -1,4 +1,4 @@
1
- import AutoComplete from "src/decidim/autocomplete";
1
+ import AutoComplete from "src/decidim/refactor/moved/autocomplete";
2
2
 
3
3
  /**
4
4
  * Sends a query to the API and resolves the resulting data in the returned
@@ -252,7 +252,7 @@ const createAutocomplete = (searchInput, spaces, inputIndex) => {
252
252
  return ac;
253
253
  }
254
254
 
255
- document.addEventListener("DOMContentLoaded", () => {
255
+ document.addEventListener("turbo:load", () => {
256
256
  const searchElements = document.querySelectorAll("input.participatory-space-search")
257
257
  if (searchElements.length < 1) {
258
258
  return;
@@ -1,4 +1,4 @@
1
- $(() => {
1
+ document.addEventListener("turbo:load", () => {
2
2
  const $limitedTimeRadioButton = $("#component_settings_proposal_edit_time_limited");
3
3
  const $infiniteTimeRadioButton = $("#component_settings_proposal_edit_time_infinite");
4
4
  const $editTimeContainer = $(".edit_time_container");