decidim-core 0.32.1 → 0.32.2

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 (149) hide show
  1. checksums.yaml +4 -4
  2. data/app/cells/decidim/content_blocks/participatory_space_main_data/title.erb +1 -14
  3. data/app/cells/decidim/following_cell.rb +1 -1
  4. data/app/cells/decidim/member/show.erb +1 -1
  5. data/app/cells/decidim/nav_links/show.erb +3 -1
  6. data/app/cells/decidim/share_text_widget/modal.erb +1 -0
  7. data/app/cells/decidim/share_widget/modal.erb +1 -0
  8. data/app/cells/decidim/share_widget/qr_code_modal.erb +2 -4
  9. data/app/commands/decidim/create_omniauth_registration.rb +17 -2
  10. data/app/controllers/concerns/decidim/devise_authentication_methods.rb +4 -0
  11. data/app/controllers/concerns/decidim/devise_controllers.rb +2 -0
  12. data/app/controllers/concerns/decidim/disable_redirection_to_external_host.rb +2 -0
  13. data/app/controllers/concerns/decidim/filter_resource.rb +2 -0
  14. data/app/controllers/concerns/decidim/impersonate_users.rb +2 -2
  15. data/app/controllers/concerns/decidim/locale_switcher.rb +12 -10
  16. data/app/controllers/concerns/decidim/needs_permission.rb +22 -10
  17. data/app/controllers/concerns/decidim/onboarding_action_methods.rb +2 -0
  18. data/app/controllers/concerns/decidim/paginable.rb +7 -3
  19. data/app/controllers/concerns/decidim/payload_info.rb +2 -0
  20. data/app/controllers/concerns/decidim/resource_versions_concern.rb +14 -0
  21. data/app/controllers/decidim/application_controller.rb +0 -6
  22. data/app/controllers/decidim/authorization_modals_controller.rb +2 -0
  23. data/app/controllers/decidim/components/base_controller.rb +2 -0
  24. data/app/controllers/decidim/devise/passwords_controller.rb +8 -0
  25. data/app/controllers/decidim/favicon_controller.rb +9 -0
  26. data/app/controllers/decidim/free_resource_authorization_modals_controller.rb +2 -0
  27. data/app/controllers/decidim/gamification/badges_controller.rb +2 -0
  28. data/app/controllers/decidim/homepage_controller.rb +4 -1
  29. data/app/controllers/decidim/likes_controller.rb +1 -1
  30. data/app/controllers/decidim/links_controller.rb +2 -0
  31. data/app/controllers/decidim/manifests_controller.rb +2 -0
  32. data/app/controllers/decidim/messaging/conversations_controller.rb +10 -4
  33. data/app/controllers/decidim/newsletters_controller.rb +4 -0
  34. data/app/controllers/decidim/notifications_subscriptions_controller.rb +4 -0
  35. data/app/controllers/decidim/offline_controller.rb +5 -1
  36. data/app/controllers/decidim/open_data_controller.rb +5 -1
  37. data/app/controllers/decidim/pages_controller.rb +31 -3
  38. data/app/controllers/decidim/private_downloads_controller.rb +2 -0
  39. data/app/controllers/decidim/profiles_controller.rb +19 -1
  40. data/app/controllers/decidim/qr_controller.rb +3 -1
  41. data/app/controllers/decidim/resource_autocomplete_controller.rb +2 -0
  42. data/app/controllers/decidim/searches_controller.rb +2 -0
  43. data/app/controllers/decidim/short_links_controller.rb +4 -0
  44. data/app/controllers/decidim/static_map_controller.rb +2 -0
  45. data/app/controllers/decidim/timeouts_controller.rb +4 -0
  46. data/app/controllers/decidim/tos_controller.rb +2 -0
  47. data/app/controllers/decidim/upload_validations_controller.rb +4 -2
  48. data/app/controllers/decidim/user_activities_controller.rb +3 -0
  49. data/app/forms/decidim/upload_validation_form.rb +16 -10
  50. data/app/helpers/concerns/decidim/user_role_checker.rb +35 -10
  51. data/app/helpers/decidim/decidim_form_helper.rb +1 -1
  52. data/app/helpers/decidim/sanitize_helper.rb +11 -1
  53. data/app/jobs/decidim/application_job.rb +4 -0
  54. data/app/models/decidim/authorization.rb +24 -0
  55. data/app/models/decidim/oauth_application.rb +1 -0
  56. data/app/models/decidim/user.rb +1 -0
  57. data/app/models/decidim/user_base_entity.rb +1 -1
  58. data/app/packs/src/decidim/callout.js +10 -0
  59. data/app/packs/src/decidim/callout.test.js +32 -0
  60. data/app/packs/src/decidim/controllers/accordion/accordion.test.js +64 -0
  61. data/app/packs/src/decidim/controllers/accordion/controller.js +43 -0
  62. data/app/packs/src/decidim/controllers/autofocus/controller.js +30 -0
  63. data/app/packs/src/decidim/controllers/language_change/controller.js +21 -8
  64. data/app/packs/src/decidim/controllers/language_change/language_change.test.js +0 -1
  65. data/app/packs/src/decidim/controllers/mention/controller.js +32 -3
  66. data/app/packs/src/decidim/controllers/mention/input_mentions.test.js +8 -5
  67. data/app/packs/src/decidim/controllers/multiple_mentions/input_multiple_mentions.test.js +1 -1
  68. data/app/packs/src/decidim/datepicker/datepicker_functions.js +41 -25
  69. data/app/packs/src/decidim/datepicker/generate_datepicker.js +4 -4
  70. data/app/packs/src/decidim/datepicker/test/datepicker_functions_adjust_picker_position.test.js +64 -0
  71. data/app/packs/src/decidim/datepicker/test/time.test.js +39 -24
  72. data/app/packs/src/decidim/editor/common/suggestion.js +32 -22
  73. data/app/packs/src/decidim/editor/extensions/mention/index.js +14 -3
  74. data/app/packs/src/decidim/editor/index.js +4 -0
  75. data/app/packs/src/decidim/editor/test/extensions/mention.test.js +32 -9
  76. data/app/packs/src/decidim/refactor/implementation/autofocus.js +22 -0
  77. data/app/packs/src/decidim/refactor/implementation/autofocus.test.js +171 -0
  78. data/app/packs/src/decidim/utilities/dom.js +2 -2
  79. data/app/packs/stylesheets/decidim/_content_blocks.scss +4 -10
  80. data/app/packs/stylesheets/decidim/_conversations.scss +0 -8
  81. data/app/packs/stylesheets/decidim/_modal.scss +16 -8
  82. data/app/packs/stylesheets/decidim/_modal_share.scss +0 -22
  83. data/app/packs/stylesheets/decidim/_participatory_spaces.scss +11 -1
  84. data/app/packs/stylesheets/decidim/_rich_text.scss +12 -9
  85. data/app/packs/stylesheets/decidim/_typography.scss +5 -2
  86. data/app/packs/stylesheets/decidim/application.scss +1 -0
  87. data/app/packs/stylesheets/decidim/legacy/email.scss +0 -5
  88. data/app/permissions/decidim/permissions.rb +20 -0
  89. data/app/presenters/decidim/admin_log/component_presenter.rb +1 -1
  90. data/app/presenters/decidim/log/base_presenter.rb +10 -1
  91. data/app/presenters/decidim/log/user_presenter.rb +2 -2
  92. data/app/presenters/decidim/menu_item_presenter.rb +1 -5
  93. data/app/presenters/decidim/resource_presenter.rb +11 -0
  94. data/app/resolvers/decidim/meta_image_url_resolver.rb +7 -1
  95. data/app/serializers/decidim/exporters/open_data_blocked_user_serializer.rb +11 -2
  96. data/app/serializers/decidim/exporters/open_data_moderation_serializer.rb +25 -1
  97. data/app/services/decidim/svg_content_validator.rb +554 -0
  98. data/app/uploaders/decidim/application_uploader.rb +2 -2
  99. data/app/validators/uploader_image_content_validator.rb +276 -0
  100. data/app/views/decidim/application/_radio_accordion.html.erb +1 -5
  101. data/app/views/decidim/messaging/conversations/_add_conversation_users.html.erb +1 -1
  102. data/app/views/decidim/messaging/conversations/create.js.erb +1 -3
  103. data/app/views/decidim/shared/_login_modal.html.erb +12 -14
  104. data/app/views/layouts/decidim/_mailer_logo.html.erb +1 -1
  105. data/app/views/layouts/decidim/header/_menu_breadcrumb_desktop.html.erb +3 -2
  106. data/config/initializers/rack_attack.rb +10 -0
  107. data/config/locales/bg.yml +0 -1
  108. data/config/locales/ca-IT.yml +3 -1
  109. data/config/locales/ca.yml +10 -6
  110. data/config/locales/cs.yml +15 -1
  111. data/config/locales/de.yml +99 -3
  112. data/config/locales/en.yml +5 -1
  113. data/config/locales/es-MX.yml +5 -1
  114. data/config/locales/es-PY.yml +5 -1
  115. data/config/locales/es.yml +9 -5
  116. data/config/locales/eu.yml +161 -160
  117. data/config/locales/fi-plain.yml +3 -1
  118. data/config/locales/fi.yml +3 -1
  119. data/config/locales/fr-CA.yml +5 -1
  120. data/config/locales/fr.yml +5 -1
  121. data/config/locales/gl.yml +1485 -33
  122. data/config/locales/hu.yml +0 -1
  123. data/config/locales/ja.yml +3 -1
  124. data/config/locales/pl.yml +0 -1
  125. data/config/locales/pt-BR.yml +0 -1
  126. data/config/locales/ro-RO.yml +0 -1
  127. data/config/locales/sk.yml +2 -1
  128. data/config/locales/sv.yml +68 -12
  129. data/config/locales/tr-TR.yml +0 -2
  130. data/config/routes.rb +11 -2
  131. data/db/migrate/20260909000000_add_failed_attempts_and_locked_at_to_decidim_authorizations.rb +8 -0
  132. data/decidim-core.gemspec +1 -1
  133. data/lib/decidim/api/functions/user_entity_list.rb +1 -0
  134. data/lib/decidim/api/types/user_moderation_type.rb +2 -2
  135. data/lib/decidim/content_parsers/user_parser.rb +1 -1
  136. data/lib/decidim/content_parsers.rb +0 -1
  137. data/lib/decidim/content_renderers/mention_resource_renderer.rb +18 -4
  138. data/lib/decidim/content_renderers/user_renderer.rb +15 -4
  139. data/lib/decidim/core/test/shared_examples/participatory_space_members_page_examples.rb +77 -0
  140. data/lib/decidim/core/test/shared_examples/participatory_space_members_shared_examples.rb +58 -0
  141. data/lib/decidim/core/test/shared_examples/versions_controller_examples.rb +23 -1
  142. data/lib/decidim/core/version.rb +1 -1
  143. data/lib/decidim/core.rb +9 -0
  144. data/lib/decidim/form_builder.rb +9 -1
  145. data/lib/decidim/has_upload_validations.rb +1 -0
  146. data/lib/decidim/legacy_form_builder.rb +4 -0
  147. metadata +12 -7
  148. data/lib/decidim/content_parsers/inline_images_parser.rb +0 -68
  149. data/lib/tasks/upgrade/decidim_active_storage_migration_tasks.rake +0 -79
@@ -1,6 +1,6 @@
1
1
  /* global jest, global */
2
2
 
3
- import { createBasicEditor, updateContent } from "src/decidim/editor/test/helpers";
3
+ import { createBasicEditor, updateContent, sleep } from "src/decidim/editor/test/helpers";
4
4
 
5
5
  import Mention from "src/decidim/editor/extensions/mention";
6
6
 
@@ -87,8 +87,11 @@ describe("Mention", () => {
87
87
  const items = suggestions.querySelectorAll(".editor-suggestions-item");
88
88
  expect(items.length).toEqual(expectedTags.length);
89
89
  for (const item of items) {
90
- expect(expectedTags.includes(item.textContent)).toBe(true);
90
+ expect(expectedTags.includes(item.querySelector(".editor-suggestions-item-label").textContent)).toBe(true);
91
91
  }
92
+
93
+ expect(items[0].querySelector(".editor-suggestions-item-avatar")).toBeInstanceOf(HTMLImageElement);
94
+ expect(items[0].querySelector(".editor-suggestions-item-avatar").getAttribute("src")).toBe("/avatars/joannadoe.jpg");
92
95
  });
93
96
 
94
97
  it("does not display the suggestions when less than two characters are entered", async () => {
@@ -97,9 +100,12 @@ describe("Mention", () => {
97
100
 
98
101
  const suggestions = document.querySelector(".editor-suggestions");
99
102
  expect(suggestions).toBeInstanceOf(HTMLDivElement);
100
- expect(suggestions.childNodes.length).toBe(0);
101
- expect(suggestions.classList.contains("hidden")).toBe(true);
102
- expect(suggestions.classList.contains("hide")).toBe(true);
103
+ const items = suggestions.querySelectorAll(".editor-suggestions-item");
104
+ expect(items.length).toBe(1);
105
+ expect(items[0].textContent).toBe("Type to search participants");
106
+ expect(items[0].disabled).toBe(true);
107
+ expect(suggestions.classList.contains("hidden")).toBe(false);
108
+ expect(suggestions.classList.contains("hide")).toBe(false);
103
109
  });
104
110
 
105
111
  it("allows selecting a mention from the list by clicking it", async () => {
@@ -110,10 +116,10 @@ describe("Mention", () => {
110
116
  suggestions.querySelector(".editor-suggestions-item").click();
111
117
 
112
118
  expect(editorElement.innerHTML).toEqual(
113
- '<p><span data-suggestion="mention" data-id="@johndoe" data-label="@johndoe (John Doe)">@johndoe (John Doe)</span> </p>'
119
+ '<p><span data-suggestion="mention" data-id="@johndoe" data-label="@johndoe">@johndoe</span> </p>'
114
120
  );
115
121
  expect(normalizeHTML(editor.getHTML())).toEqual(
116
- '<p><span data-type="mention" data-id="@johndoe" data-label="@johndoe (John Doe)">@johndoe (John Doe)</span> </p>'
122
+ '<p><span data-type="mention" data-id="@johndoe" data-label="@johndoe">@johndoe</span> </p>'
117
123
  );
118
124
  });
119
125
 
@@ -124,10 +130,27 @@ describe("Mention", () => {
124
130
  editorElement.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter" }));
125
131
 
126
132
  expect(editorElement.innerHTML).toEqual(
127
- '<p><span data-suggestion="mention" data-id="@johndoe" data-label="@johndoe (John Doe)">@johndoe (John Doe)</span> </p>'
133
+ '<p><span data-suggestion="mention" data-id="@johndoe" data-label="@johndoe">@johndoe</span> </p>'
128
134
  );
129
135
  expect(normalizeHTML(editor.getHTML())).toEqual(
130
- '<p><span data-type="mention" data-id="@johndoe" data-label="@johndoe (John Doe)">@johndoe (John Doe)</span> </p>'
136
+ '<p><span data-type="mention" data-id="@johndoe" data-label="@johndoe">@johndoe</span> </p>'
131
137
  );
132
138
  });
139
+
140
+ it("allows selecting a mention when content changes after triggering", async () => {
141
+ editorElement.focus();
142
+ await updateContent(editorElement, "@joh", editor);
143
+
144
+ const suggestions = document.querySelector(".editor-suggestions");
145
+ expect(suggestions).toBeInstanceOf(HTMLDivElement);
146
+
147
+ editor.commands.setContent("<p>Some new content @joh</p>");
148
+ await sleep(50);
149
+
150
+ expect(() => {
151
+ suggestions.querySelector(".editor-suggestions-item").click();
152
+ }).not.toThrow();
153
+
154
+ expect(normalizeHTML(editor.getHTML())).toContain('data-type="mention"');
155
+ });
133
156
  });
@@ -0,0 +1,22 @@
1
+ /**
2
+ * This focuses an element
3
+ * @param {HTMLElement} element The container element to search for a focusable input or editor.
4
+ * @returns {void} Nothing
5
+ */
6
+ export default function autofocus(element) {
7
+ let node = element.querySelector(".editor .ProseMirror, input[type='text'], textarea")
8
+
9
+ if (!node) {
10
+ return;
11
+ }
12
+
13
+ node.dispatchEvent(new CustomEvent("move-cursor-to-end", { bubbles: true }));
14
+
15
+ if (typeof node.focus === "function") {
16
+ node.focus();
17
+ }
18
+
19
+ let value = node.value;
20
+ node.value = "";
21
+ node.value = value;
22
+ }
@@ -0,0 +1,171 @@
1
+ /* global jest */
2
+
3
+ import autofocus from "src/decidim/refactor/implementation/autofocus"
4
+
5
+ describe("autofocus", () => {
6
+ describe("when ProseMirror editor is present", () => {
7
+ it("dispatches move-cursor-to-end event on ProseMirror", () => {
8
+ const container = document.createElement("div");
9
+ const editor = document.createElement("div");
10
+ editor.className = "editor";
11
+ const prosemirror = document.createElement("div");
12
+ prosemirror.className = "ProseMirror";
13
+ editor.appendChild(prosemirror);
14
+ container.appendChild(editor);
15
+ document.body.appendChild(container);
16
+
17
+ const dispatchSpy = jest.spyOn(prosemirror, "dispatchEvent");
18
+
19
+ autofocus(container);
20
+
21
+ expect(dispatchSpy).toHaveBeenCalledWith(expect.objectContaining({
22
+ type: "move-cursor-to-end",
23
+ bubbles: true
24
+ }));
25
+ dispatchSpy.mockRestore();
26
+ container.remove();
27
+ });
28
+
29
+ it("calls focus on ProseMirror element", () => {
30
+ const container = document.createElement("div");
31
+ const editor = document.createElement("div");
32
+ editor.className = "editor";
33
+ const prosemirror = document.createElement("div");
34
+ prosemirror.className = "ProseMirror";
35
+ prosemirror.contentEditable = true;
36
+ const focusSpy = jest.spyOn(prosemirror, "focus");
37
+ editor.appendChild(prosemirror);
38
+ container.appendChild(editor);
39
+ document.body.appendChild(container);
40
+
41
+ autofocus(container);
42
+
43
+ expect(focusSpy).toHaveBeenCalled();
44
+ focusSpy.mockRestore();
45
+ container.remove();
46
+ });
47
+ });
48
+
49
+ describe("when input[type='text'] is present", () => {
50
+ it("focuses on the input element", () => {
51
+ const container = document.createElement("div");
52
+ const input = document.createElement("input");
53
+ input.type = "text";
54
+ input.value = "test value";
55
+ const focusSpy = jest.spyOn(input, "focus");
56
+ container.appendChild(input);
57
+ document.body.appendChild(container);
58
+
59
+ autofocus(container);
60
+
61
+ expect(focusSpy).toHaveBeenCalled();
62
+ expect(input.value).toBe("test value");
63
+ focusSpy.mockRestore();
64
+ container.remove();
65
+ });
66
+
67
+ it("clears and restores the input value", () => {
68
+ const container = document.createElement("div");
69
+ const input = document.createElement("input");
70
+ input.type = "text";
71
+ input.value = "original value";
72
+ container.appendChild(input);
73
+ document.body.appendChild(container);
74
+
75
+ autofocus(container);
76
+
77
+ expect(input.value).toBe("original value");
78
+ container.remove();
79
+ });
80
+ });
81
+
82
+ describe("when textarea is present", () => {
83
+ it("focuses on the textarea element", () => {
84
+ const container = document.createElement("div");
85
+ const textarea = document.createElement("textarea");
86
+ textarea.value = "textarea content";
87
+ const focusSpy = jest.spyOn(textarea, "focus");
88
+ container.appendChild(textarea);
89
+ document.body.appendChild(container);
90
+
91
+ autofocus(container);
92
+
93
+ expect(focusSpy).toHaveBeenCalled();
94
+ expect(textarea.value).toBe("textarea content");
95
+ focusSpy.mockRestore();
96
+ container.remove();
97
+ });
98
+
99
+ it("clears and restores the textarea value", () => {
100
+ const container = document.createElement("div");
101
+ const textarea = document.createElement("textarea");
102
+ textarea.value = "original content";
103
+ container.appendChild(textarea);
104
+ document.body.appendChild(container);
105
+
106
+ autofocus(container);
107
+
108
+ expect(textarea.value).toBe("original content");
109
+ container.remove();
110
+ });
111
+ });
112
+
113
+ describe("when no focusable element is present", () => {
114
+ it("does nothing and does not throw", () => {
115
+ const container = document.createElement("div");
116
+ container.innerHTML = "<p>No focusable element here</p>";
117
+ document.body.appendChild(container);
118
+
119
+ expect(() => autofocus(container)).not.toThrow();
120
+ container.remove();
121
+ });
122
+ });
123
+
124
+ describe("priority of elements", () => {
125
+ it("prefers ProseMirror over input", () => {
126
+ const container = document.createElement("div");
127
+ const editor = document.createElement("div");
128
+ editor.className = "editor";
129
+ const prosemirror = document.createElement("div");
130
+ prosemirror.className = "ProseMirror";
131
+ editor.appendChild(prosemirror);
132
+ const input = document.createElement("input");
133
+ input.type = "text";
134
+ editor.appendChild(input);
135
+ container.appendChild(editor);
136
+ document.body.appendChild(container);
137
+
138
+ const prosemirrorDispatchSpy = jest.spyOn(prosemirror, "dispatchEvent");
139
+ const inputFocusSpy = jest.spyOn(input, "focus");
140
+
141
+ autofocus(container);
142
+
143
+ expect(prosemirrorDispatchSpy).toHaveBeenCalled();
144
+ expect(inputFocusSpy).not.toHaveBeenCalled();
145
+ prosemirrorDispatchSpy.mockRestore();
146
+ inputFocusSpy.mockRestore();
147
+ container.remove();
148
+ });
149
+
150
+ it("prefers input over textarea", () => {
151
+ const container = document.createElement("div");
152
+ const input = document.createElement("input");
153
+ input.type = "text";
154
+ const textarea = document.createElement("textarea");
155
+ container.appendChild(input);
156
+ container.appendChild(textarea);
157
+ document.body.appendChild(container);
158
+
159
+ const inputFocusSpy = jest.spyOn(input, "focus");
160
+ const textareaFocusSpy = jest.spyOn(textarea, "focus");
161
+
162
+ autofocus(container);
163
+
164
+ expect(inputFocusSpy).toHaveBeenCalled();
165
+ expect(textareaFocusSpy).not.toHaveBeenCalled();
166
+ inputFocusSpy.mockRestore();
167
+ textareaFocusSpy.mockRestore();
168
+ container.remove();
169
+ });
170
+ });
171
+ });
@@ -77,7 +77,7 @@ const createUnloadPreventer = () => {
77
77
  ev.preventDefault();
78
78
  ev.stopPropagation();
79
79
 
80
- confirmAction(confirmMessage, link).then((answer) => {
80
+ confirmAction(confirmMessage, link, { iconName: "alert-line" }).then((answer) => {
81
81
  if (!answer) {
82
82
  return;
83
83
  }
@@ -109,7 +109,7 @@ const createUnloadPreventer = () => {
109
109
  ev.stopImmediatePropagation();
110
110
  ev.stopPropagation();
111
111
 
112
- confirmAction(confirmMessage, button).then((answer) => {
112
+ confirmAction(confirmMessage, button, { iconName: "alert-line" }).then((answer) => {
113
113
  if (!answer) {
114
114
  return;
115
115
  }
@@ -13,18 +13,12 @@
13
13
  @apply block max-h-40 overflow-hidden relative first:[&_*]:mt-0 before:content-[''] before:absolute before:inset-0 before:h-full before:w-full before:bg-gradient-to-b before:from-transparent before:to-white after:content-[''] after:absolute after:inset-0 after:h-full after:w-full after:bg-gradient-to-b after:from-transparent after:to-white;
14
14
  }
15
15
 
16
- [aria-expanded="false"] > svg:last-of-type,
17
- [aria-expanded="false"] > span:last-of-type,
18
- [aria-expanded="true"] > span:first-of-type,
19
- [aria-expanded="true"] > svg:first-of-type {
20
- @apply hidden;
16
+ [aria-expanded] > svg {
17
+ @apply transition-transform;
21
18
  }
22
19
 
23
- [aria-expanded="true"] > svg:last-of-type,
24
- [aria-expanded="true"] > span:last-of-type,
25
- [aria-expanded="false"] > span:first-of-type,
26
- [aria-expanded="false"] > svg:first-of-type {
27
- @apply block;
20
+ [aria-expanded="true"] > svg {
21
+ @apply rotate-180;
28
22
  }
29
23
  }
30
24
 
@@ -92,14 +92,6 @@
92
92
  [data-dialog-closable] {
93
93
  @apply top-6 right-6;
94
94
  }
95
-
96
- [data-dialog-container] {
97
- @apply inline grid-cols-none;
98
- }
99
-
100
- [data-dialog-title] {
101
- @apply col-span-2 md:col-span-1 md:col-start-2 grid grid-cols-[auto_1fr] items-start md:items-center gap-2 pb-4 text-left text-xl text-gray-2 md:border-b md:border-gray-3;
102
- }
103
95
  }
104
96
 
105
97
  &-results {
@@ -22,19 +22,27 @@
22
22
  }
23
23
 
24
24
  [data-dialog-container] {
25
- @apply grid grid-cols-[auto_1fr] items-start md:items-center gap-2 text-left;
25
+ @apply block text-left;
26
26
 
27
- > svg {
28
- @apply w-6 h-6 text-gray fill-current flex-none;
29
- }
27
+ &:has(> svg) {
28
+ @apply grid grid-cols-[auto_1fr] items-start md:items-center gap-2;
29
+
30
+ > svg {
31
+ @apply w-6 h-6 text-gray fill-current flex-none mb-6;
32
+ }
30
33
 
31
- > :last-child {
32
- @apply col-span-2 md:col-span-1 md:col-start-2;
34
+ > :last-child {
35
+ @apply col-span-2 md:col-span-1 md:col-start-2;
36
+ }
33
37
  }
34
38
  }
35
39
 
36
40
  [data-dialog-title] {
37
- @apply text-2xl text-black font-semibold focus:outline-none;
41
+ @apply flex items-center gap-2 text-xl text-gray-2 font-semibold focus:outline-none pb-2 mb-4 md:border-b md:border-gray-3;
42
+
43
+ > svg {
44
+ @apply w-6 h-6 text-gray fill-current flex-none;
45
+ }
38
46
  }
39
47
 
40
48
  [data-dialog-actions] {
@@ -59,7 +67,7 @@
59
67
  }
60
68
 
61
69
  [data-dialog-container] {
62
- @apply grid-cols-none;
70
+ @apply grid grid-cols-none;
63
71
 
64
72
  &.layout-content {
65
73
  @apply bg-white;
@@ -1,21 +1,7 @@
1
1
  .qr-modal {
2
2
  @apply md:p-6 lg:p-8;
3
3
 
4
- [data-dialog-title] {
5
- @apply flex justify-start items-center text-2xl text-black font-semibold focus:outline-none;
6
-
7
- > svg {
8
- @apply w-10 h-10 text-gray fill-current flex-none;
9
- }
10
-
11
- > :last-child {
12
- @apply ml-1;
13
- }
14
- }
15
-
16
4
  [data-dialog-container] {
17
- @apply inline grid-cols-none;
18
-
19
5
  .content {
20
6
  @apply md:grid md:grid-cols-2 gap-5 md:py-4;
21
7
 
@@ -59,14 +45,6 @@
59
45
  @apply top-6 right-6;
60
46
  }
61
47
 
62
- [data-dialog-container] {
63
- @apply inline grid-cols-none;
64
- }
65
-
66
- [data-dialog-title] {
67
- @apply col-span-2 md:col-span-1 md:col-start-2 grid grid-cols-[auto_1fr] items-start md:items-center gap-2 pb-4 text-left text-xl text-gray-2 md:border-b md:border-gray-3;
68
- }
69
-
70
48
  &__list {
71
49
  @apply my-6 flex flex-wrap justify-center items-center md:grid grid-cols-4 md:grid-cols-5 gap-3 md:gap-6 max-w-96 mx-auto text-secondary;
72
50
 
@@ -111,7 +111,17 @@
111
111
  }
112
112
 
113
113
  &-item {
114
- @apply flex items-center gap-1 text-white font-semibold hover:underline [&>svg]:fill-current;
114
+ @apply flex items-center gap-1 text-white font-semibold hover:underline;
115
+ }
116
+
117
+ &-label {
118
+ @apply flex-1 min-w-0 overflow-hidden text-ellipsis;
119
+
120
+ overflow-wrap: anywhere;
121
+ }
122
+
123
+ &-item > svg {
124
+ @apply shrink-0 fill-current;
115
125
  }
116
126
  }
117
127
 
@@ -7,18 +7,21 @@
7
7
  .editor-content-videoEmbed {
8
8
  @apply my-10 mx-auto;
9
9
  }
10
+ }
10
11
 
11
- .editor-mention {
12
- @apply inline-flex items-center align-bottom font-semibold underline text-secondary;
12
+ // Resource mentions are rendered both inside rich text (`.rich-text-display`)
13
+ // and inside comments (`.editor-content`), so they cannot be scoped to one of
14
+ // those wrappers.
15
+ .editor-mention {
16
+ @apply inline-flex items-center align-bottom font-semibold underline text-secondary;
13
17
 
14
- img,
15
- svg {
16
- @apply w-5 h-5 rounded-full;
17
- }
18
+ img,
19
+ svg {
20
+ @apply w-5 h-5 m-0 rounded-full;
21
+ }
18
22
 
19
- span {
20
- @apply ml-1;
21
- }
23
+ span {
24
+ @apply ml-1;
22
25
  }
23
26
  }
24
27
 
@@ -27,7 +27,10 @@
27
27
  }
28
28
 
29
29
  .title-decorator {
30
- @apply h2 md:h1 decorator;
30
+ @apply h2 md:h1 decorator break-normal;
31
+
32
+ hyphens: auto;
33
+ overflow-wrap: anywhere;
31
34
  }
32
35
 
33
36
  .text-with-links {
@@ -38,7 +41,7 @@
38
41
 
39
42
  /* default styles for those dynamic text blocks */
40
43
  .editor-content {
41
- @apply prose max-w-full text-gray-2 text-lg prose-a:text-secondary break-words prose-ul:pl-4;
44
+ @apply prose max-w-full text-gray-2 text-lg prose-a:text-secondary break-words prose-ul:pl-8 prose-ol:pl-8;
42
45
 
43
46
  iframe {
44
47
  @apply w-full block max-w-[600px] max-h-[70vh] h-[450px] my-10 mx-auto;
@@ -83,6 +83,7 @@
83
83
  @include meta.load-css("stylesheets/decidim/actions");
84
84
  @include meta.load-css("stylesheets/decidim/emoji");
85
85
  @include meta.load-css("stylesheets/decidim/success_image");
86
+ @include meta.load-css("stylesheets/decidim/editor_suggestions");
86
87
  }
87
88
 
88
89
  // This applies the custom style imports registered at the application.
@@ -1725,7 +1725,6 @@ body.outlook p {
1725
1725
  table.body th.decidim-bar,
1726
1726
  table.body td.decidim-bar {
1727
1727
  padding: 10px 0;
1728
- background-color: #1a181d;
1729
1728
  }
1730
1729
 
1731
1730
  table.body {
@@ -1742,10 +1741,6 @@ table.body {
1742
1741
  height: 30px;
1743
1742
  }
1744
1743
 
1745
- .cityhall-bar {
1746
- background-color: #2c2930;
1747
- }
1748
-
1749
1744
  .cityhall-logo {
1750
1745
  width: 150px;
1751
1746
  margin-top: 16px;
@@ -9,6 +9,7 @@ module Decidim
9
9
  locales_action?
10
10
  component_public_action?
11
11
  search_scope_action?
12
+ public_content_action?
12
13
 
13
14
  public_report_content_action?
14
15
 
@@ -66,6 +67,23 @@ module Decidim
66
67
  toggle_allow([:search, :pick].include?(permission_action.action))
67
68
  end
68
69
 
70
+ # Public, non-resource content that any visitor is allowed to reach. The
71
+ # controllers using these subjects enforce the actual data visibility
72
+ # themselves (profile publication, private download tokens, newsletter
73
+ # tokens, etc.).
74
+ def public_content_action?
75
+ case permission_action.subject
76
+ when :open_data, :search, :profile, :private_download, :short_link,
77
+ :calendar, :badges, :static_map, :qr_code, :homepage, :manifest,
78
+ :offline, :link, :authorization, :error, :timeout, :page
79
+ allow! if permission_action.action == :read
80
+ when :newsletter
81
+ allow! if [:read, :unsubscribe].include?(permission_action.action)
82
+ when :upload_validation
83
+ allow! if permission_action.action == :create
84
+ end
85
+ end
86
+
69
87
  def manage_self_user_action?
70
88
  return unless permission_action.subject == :user
71
89
 
@@ -78,6 +96,8 @@ module Decidim
78
96
  authorization = context.fetch(:authorization, nil)
79
97
 
80
98
  case permission_action.action
99
+ when :read
100
+ allow!
81
101
  when :create
82
102
  toggle_allow(authorization.user == user && not_already_active?(authorization))
83
103
  when :update, :destroy
@@ -29,7 +29,7 @@ module Decidim
29
29
 
30
30
  def action_string
31
31
  case action
32
- when "export_component", "soft_delete", "restore"
32
+ when "export_component", "soft_delete", "restore", "menu_hidden"
33
33
  "decidim.admin_log.component.#{action}"
34
34
  when "create", "delete", "publish", "unpublish", "update_permissions", "update_filters"
35
35
  generate_action_string(action)
@@ -111,9 +111,18 @@ module Decidim
111
111
  # Returns an HTML-safe String.
112
112
  def present_explanation
113
113
  h.content_tag(:div, class: "logs__log__explanation") do
114
+ escaped_params = i18n_params.transform_values do |value|
115
+ if value.is_a?(String) && !value.html_safe?
116
+ ERB::Util.html_escape(value)
117
+ else
118
+ value
119
+ end
120
+ end
121
+
114
122
  I18n.t(
115
123
  action_string,
116
- **i18n_params
124
+ escape: false,
125
+ **escaped_params
117
126
  ).html_safe
118
127
  end
119
128
  end
@@ -67,9 +67,9 @@ module Decidim
67
67
 
68
68
  # Private: Presents the nickname of the user performing the action.
69
69
  #
70
- # Returns an HTML-safe String.
70
+ # Returns a String.
71
71
  def present_user_nickname
72
- extra["nickname"].html_safe
72
+ extra["nickname"]
73
73
  end
74
74
 
75
75
  # Private: Calculates the path for the user. Returns the path of the
@@ -48,11 +48,7 @@ module Decidim
48
48
  private
49
49
 
50
50
  def link_options
51
- if active?
52
- { aria: { current: "page" } }
53
- else
54
- {}
55
- end.merge({ class: link_classes, role: menuitem_role })
51
+ { class: link_classes, role: menuitem_role }
56
52
  end
57
53
 
58
54
  def composed_label
@@ -45,5 +45,16 @@ module Decidim
45
45
  content
46
46
  end
47
47
  end
48
+
49
+ # Prepares content for plain text areas (non-editor). Renders mentions
50
+ # to plain @nickname text instead of editor-specific HTML tags.
51
+ def plain_locales(data, all_locales)
52
+ handle_locales(data, all_locales) do |content|
53
+ content = Decidim::ContentRenderers::BlobRenderer.new(content).render
54
+ content = Decidim::ContentRenderers::UserRenderer.new(content).render(plain: true)
55
+ content = Decidim::ContentRenderers::MentionResourceRenderer.new(content).render(plain: true)
56
+ content.html_safe
57
+ end
58
+ end
48
59
  end
49
60
  end
@@ -82,7 +82,7 @@ module Decidim
82
82
  blob = find_blob_by_id(image_url.split("/").last)
83
83
  return blob if blob.present?
84
84
 
85
- blob = find_blob_by_key(image_url.split("/").second_to_last)
85
+ blob = find_blob_from_url(image_url)
86
86
  return blob if blob.present?
87
87
  end
88
88
 
@@ -119,6 +119,12 @@ module Decidim
119
119
  nil
120
120
  end
121
121
 
122
+ def find_blob_from_url(url)
123
+ return GlobalID::Locator.locate(url, only: ActiveStorage::Blob) if url.start_with?("gid://")
124
+
125
+ find_blob_by_key(url.split("/").second_to_last)
126
+ end
127
+
122
128
  def find_blob_by_key(blob_key)
123
129
  ActiveStorage::Blob.find_signed(blob_key) if blob_key.present?
124
130
  end