decidim-core 0.31.5 → 0.31.6
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 +4 -4
- data/app/cells/decidim/amendable/amend_button_card/show.erb +2 -2
- data/app/cells/decidim/author_cell.rb +0 -4
- data/app/cells/decidim/content_blocks/cta_settings_form/show.erb +1 -1
- data/app/cells/decidim/content_blocks/hero_settings_form/show.erb +2 -2
- data/app/cells/decidim/content_blocks/highlighted_content_banner_cell.rb +1 -1
- data/app/cells/decidim/content_blocks/highlighted_content_banner_settings_form/show.erb +1 -1
- data/app/cells/decidim/content_blocks/highlighted_participatory_spaces_cell.rb +1 -1
- data/app/cells/decidim/content_blocks/participatory_space_hero_settings_form/show.erb +1 -1
- data/app/cells/decidim/content_blocks/static_page/section_settings_form/show.erb +3 -1
- data/app/cells/decidim/content_blocks/static_page/summary_settings_form/show.erb +3 -1
- data/app/cells/decidim/content_blocks/static_page/two_pane_section_settings_form/show.erb +4 -2
- data/app/cells/decidim/resource_types_filter/show.erb +2 -2
- data/app/commands/decidim/destroy_account.rb +12 -1
- data/app/commands/decidim/multiple_attachments_methods.rb +28 -27
- data/app/controllers/concerns/decidim/devise_controllers.rb +10 -0
- data/app/jobs/decidim/process_inactive_participant_job.rb +0 -7
- data/app/mailers/decidim/delete_user_mailer.rb +14 -0
- data/app/mailers/decidim/participants_account_mailer.rb +0 -16
- data/app/models/decidim/moderation.rb +1 -1
- data/app/models/decidim/participatory_space_private_user.rb +1 -1
- data/app/models/decidim/user_base_entity.rb +17 -2
- data/app/models/decidim/user_moderation.rb +1 -1
- data/app/packs/src/decidim/controllers/form_validator/form_validator.js +6 -6
- data/app/packs/src/decidim/controllers/form_validator/form_validator.test.js +23 -1
- data/app/packs/src/decidim/controllers/mention/controller.js +296 -140
- data/app/packs/src/decidim/controllers/mention/input_mentions.test.js +120 -457
- data/app/packs/src/decidim/controllers/multiple_mentions/controller.js +68 -32
- data/app/packs/src/decidim/controllers/multiple_mentions/input_multiple_mentions.test.js +30 -23
- data/app/packs/src/decidim/direct_uploads/upload_field.js +4 -4
- data/app/packs/src/decidim/direct_uploads/upload_modal.js +11 -7
- data/app/packs/src/decidim/geocoding/reverse_geocoding.js +15 -5
- data/app/packs/src/decidim/geocoding/reverse_geocoding.test.js +197 -0
- data/app/packs/src/decidim/index.js +4 -3
- data/app/packs/src/decidim/sw/sw.js +1 -1
- data/app/packs/src/decidim/utilities/text.js +6 -6
- data/app/packs/stylesheets/decidim/_conversations.scss +14 -0
- data/app/packs/stylesheets/decidim/_editor_suggestions.scss +49 -0
- data/app/packs/stylesheets/decidim/_floating_help.scss +1 -1
- data/app/packs/stylesheets/decidim/_tom_select.scss +23 -0
- data/app/packs/stylesheets/decidim/application.scss +2 -0
- data/app/packs/stylesheets/decidim/editor.scss +2 -33
- data/app/packs/stylesheets/decidim/geocoding_addons.scss +10 -2
- data/app/presenters/decidim/menu_item_presenter.rb +9 -3
- data/app/presenters/decidim/stats_presenter.rb +1 -1
- data/app/uploaders/decidim/image_uploader.rb +1 -1
- data/app/views/decidim/delete_user_mailer/delete.html.erb +6 -0
- data/app/views/decidim/gamification/badges/index.html.erb +1 -1
- data/app/views/decidim/homepage/show.html.erb +1 -1
- data/app/views/decidim/last_activities/index.html.erb +1 -1
- data/app/views/decidim/messaging/conversations/_error_modal.html.erb +11 -19
- data/app/views/decidim/messaging/conversations/error.js.erb +12 -7
- data/app/views/decidim/newsletters/unsubscribe.html.erb +1 -1
- data/app/views/decidim/offline/show.html.erb +1 -1
- data/app/views/decidim/pages/index.html.erb +1 -1
- data/app/views/decidim/profiles/show.html.erb +1 -1
- data/app/views/decidim/shared/_resource_actions.html.erb +4 -4
- data/app/views/decidim/user_activities/index.html.erb +1 -1
- data/app/views/layouts/decidim/_wrapper.html.erb +2 -2
- data/app/views/layouts/decidim/header/_menu_breadcrumb_mobile_tablet.html.erb +1 -1
- data/app/views/layouts/decidim/shared/_layout_center.html.erb +1 -1
- data/app/views/layouts/decidim/shared/_layout_item.html.erb +1 -1
- data/app/views/layouts/decidim/shared/_layout_two_col.html.erb +1 -1
- data/config/locales/ar.yml +0 -5
- data/config/locales/bg.yml +0 -6
- data/config/locales/ca-IT.yml +9 -10
- data/config/locales/ca.yml +9 -10
- data/config/locales/cs.yml +2 -13
- data/config/locales/de.yml +5 -17
- data/config/locales/el.yml +0 -4
- data/config/locales/en.yml +8 -9
- data/config/locales/es-MX.yml +8 -9
- data/config/locales/es-PY.yml +8 -9
- data/config/locales/es.yml +8 -9
- data/config/locales/eu.yml +48 -49
- data/config/locales/fi-plain.yml +9 -9
- data/config/locales/fi.yml +9 -9
- data/config/locales/fr-CA.yml +9 -9
- data/config/locales/fr.yml +9 -9
- data/config/locales/gl.yml +0 -3
- data/config/locales/hu.yml +0 -5
- data/config/locales/id-ID.yml +0 -3
- data/config/locales/it.yml +1 -3
- data/config/locales/ja.yml +24 -21
- data/config/locales/lb.yml +0 -5
- data/config/locales/lt.yml +0 -5
- data/config/locales/lv.yml +0 -3
- data/config/locales/nl.yml +0 -5
- data/config/locales/no.yml +0 -5
- data/config/locales/pl.yml +2 -8
- data/config/locales/pt-BR.yml +2 -15
- data/config/locales/pt.yml +0 -5
- data/config/locales/ro-RO.yml +1 -14
- data/config/locales/sk.yml +1 -14
- data/config/locales/sv.yml +2 -11
- data/config/locales/tr-TR.yml +0 -6
- data/config/locales/zh-CN.yml +0 -5
- data/config/locales/zh-TW.yml +0 -5
- data/lib/decidim/api/functions/user_entity_list.rb +2 -0
- data/lib/decidim/attachment_attributes.rb +58 -9
- data/lib/decidim/command.rb +1 -1
- data/lib/decidim/content_renderers/base_renderer.rb +112 -0
- data/lib/decidim/content_renderers/blob_renderer.rb +4 -7
- data/lib/decidim/content_renderers/mention_resource_renderer.rb +10 -6
- data/lib/decidim/content_renderers/resource_renderer.rb +16 -7
- data/lib/decidim/content_renderers/user_renderer.rb +11 -9
- data/lib/decidim/core/content_blocks/registry_manager.rb +4 -4
- data/lib/decidim/core/engine.rb +8 -0
- data/lib/decidim/core/test/factories.rb +3 -0
- data/lib/decidim/core/test/shared_examples/admin_resource_gallery_examples.rb +10 -10
- data/lib/decidim/core/test/shared_examples/comments_examples.rb +6 -6
- data/lib/decidim/core/version.rb +1 -1
- data/lib/decidim/map/autocomplete.rb +4 -3
- data/lib/decidim/searchable.rb +5 -0
- data/lib/decidim/view_model.rb +1 -1
- data/lib/tasks/decidim_mailers_tasks.rake +31 -9
- metadata +11 -10
- data/app/commands/decidim/gallery_methods.rb +0 -107
- data/app/packs/src/decidim/vendor/tribute.js +0 -1890
- data/app/packs/stylesheets/decidim/_tribute.scss +0 -36
- data/app/views/decidim/participants_account_mailer/removal_notification.html.erb +0 -11
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
|
-
import
|
|
2
|
+
import TomSelect from "tom-select/dist/cjs/tom-select.popular";
|
|
3
3
|
import icon from "src/decidim/refactor/moved/icon";
|
|
4
4
|
|
|
5
5
|
export default class extends Controller {
|
|
@@ -16,20 +16,6 @@ export default class extends Controller {
|
|
|
16
16
|
|
|
17
17
|
this.initializeEmptyFocusElement();
|
|
18
18
|
this.initializeAutoComplete();
|
|
19
|
-
this.setupSelectionListener();
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Setup the selection event listener
|
|
24
|
-
* @returns {void}
|
|
25
|
-
*/
|
|
26
|
-
setupSelectionListener() {
|
|
27
|
-
this.selectionHandler = (event) => {
|
|
28
|
-
const feedback = event.detail;
|
|
29
|
-
const selection = feedback.selection;
|
|
30
|
-
this.handleSelection(selection);
|
|
31
|
-
};
|
|
32
|
-
this.searchInput.addEventListener("selection", this.selectionHandler);
|
|
33
19
|
}
|
|
34
20
|
|
|
35
21
|
/*
|
|
@@ -37,8 +23,8 @@ export default class extends Controller {
|
|
|
37
23
|
* @returns {void}
|
|
38
24
|
*/
|
|
39
25
|
disconnect() {
|
|
40
|
-
if (this.
|
|
41
|
-
this.
|
|
26
|
+
if (this.tomSelect) {
|
|
27
|
+
this.tomSelect.destroy();
|
|
42
28
|
}
|
|
43
29
|
}
|
|
44
30
|
|
|
@@ -80,11 +66,56 @@ export default class extends Controller {
|
|
|
80
66
|
* @returns {void}
|
|
81
67
|
*/
|
|
82
68
|
initializeAutoComplete() {
|
|
83
|
-
this.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
69
|
+
this.tomSelect = new TomSelect(this.searchInput, {
|
|
70
|
+
maxItems: 1,
|
|
71
|
+
valueField: "id",
|
|
72
|
+
labelField: "name",
|
|
73
|
+
searchField: ["name", "nickname"],
|
|
74
|
+
loadThrottle: 200,
|
|
75
|
+
loadingClass: "loading",
|
|
76
|
+
preload: false,
|
|
77
|
+
highlight: true,
|
|
78
|
+
load: (query, callback) => {
|
|
79
|
+
if (!query || query.length < 2) {
|
|
80
|
+
callback();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
this.getDataSource(query, (results) => {
|
|
84
|
+
const filtered = this.filterResults(results);
|
|
85
|
+
filtered.forEach((item) => {
|
|
86
|
+
if (item.directMessagesEnabled === "false") {
|
|
87
|
+
item.disabled = true;
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
callback(filtered);
|
|
91
|
+
});
|
|
92
|
+
},
|
|
93
|
+
render: {
|
|
94
|
+
option: (data, escape) => {
|
|
95
|
+
const isDisabled = data.directMessagesEnabled === "false";
|
|
96
|
+
const className = isDisabled
|
|
97
|
+
? "disabled"
|
|
98
|
+
: "";
|
|
99
|
+
const disabledMsg = isDisabled
|
|
100
|
+
? `<small>${escape(this.searchInput.dataset.directMessagesDisabled)}</small>`
|
|
101
|
+
: "";
|
|
102
|
+
return `<div class="${className}">
|
|
103
|
+
<img src="${escape(data.avatarUrl)}" alt="${escape(data.name)}">
|
|
104
|
+
<span>${escape(data.nickname)}</span>
|
|
105
|
+
<small>${escape(data.name)}</small>
|
|
106
|
+
${disabledMsg}
|
|
107
|
+
</div>`;
|
|
108
|
+
},
|
|
109
|
+
"no_results": () => `<div class="no-results">${this.searchInput.dataset.noresults || ""}</div>`
|
|
110
|
+
},
|
|
111
|
+
onChange: (value) => {
|
|
112
|
+
if (value) {
|
|
113
|
+
const option = this.tomSelect.options[value];
|
|
114
|
+
this.handleSelection({ value: option });
|
|
115
|
+
this.tomSelect.clear();
|
|
116
|
+
this.tomSelect.clearOptions();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
88
119
|
});
|
|
89
120
|
}
|
|
90
121
|
|
|
@@ -129,7 +160,7 @@ export default class extends Controller {
|
|
|
129
160
|
*/
|
|
130
161
|
filterResults(list) {
|
|
131
162
|
return list.filter(
|
|
132
|
-
(item) => !this.selected.includes(item.
|
|
163
|
+
(item) => !this.selected.includes(item.id)
|
|
133
164
|
);
|
|
134
165
|
}
|
|
135
166
|
|
|
@@ -161,18 +192,23 @@ export default class extends Controller {
|
|
|
161
192
|
*/
|
|
162
193
|
handleSelection(selection) {
|
|
163
194
|
const id = selection.value.id;
|
|
164
|
-
// Check if we have reached the maximum limit or if direct messages are disabled
|
|
165
195
|
if (this.isMaxLimitReached() || selection.value.directMessagesEnabled === "false") {
|
|
166
196
|
return;
|
|
167
197
|
}
|
|
168
198
|
|
|
169
199
|
this.addSelectedUser(selection, id);
|
|
170
|
-
this.autoComplete.setInput("");
|
|
171
200
|
this.selected.push(id);
|
|
201
|
+
}
|
|
172
202
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
203
|
+
/**
|
|
204
|
+
* Escape HTML characters in a string
|
|
205
|
+
* @param {string} str - The string to escape
|
|
206
|
+
* @returns {string} The escaped HTML string
|
|
207
|
+
*/
|
|
208
|
+
htmlEscape(str) {
|
|
209
|
+
const div = document.createElement("div");
|
|
210
|
+
div.appendChild(document.createTextNode(str));
|
|
211
|
+
return div.innerHTML;
|
|
176
212
|
}
|
|
177
213
|
|
|
178
214
|
/**
|
|
@@ -187,10 +223,10 @@ export default class extends Controller {
|
|
|
187
223
|
const listItem = document.createElement("li");
|
|
188
224
|
listItem.tabIndex = "-1";
|
|
189
225
|
listItem.innerHTML = `
|
|
190
|
-
<input type="hidden" name="${this.options.name}" value="${id}">
|
|
191
|
-
<img src="${selection.value.avatarUrl}" alt="${selection.value.name}">
|
|
192
|
-
<span>${selection.value.name}</span>
|
|
193
|
-
<button type="button" data-remove="${id}" tabindex="0" aria-controls="0" aria-label="${label}">${icon("delete-bin-line")}</button>
|
|
226
|
+
<input type="hidden" name="${this.htmlEscape(this.options.name)}" value="${this.htmlEscape(id)}">
|
|
227
|
+
<img src="${this.htmlEscape(selection.value.avatarUrl)}" alt="${this.htmlEscape(selection.value.name)}">
|
|
228
|
+
<span>${this.htmlEscape(selection.value.name)}</span>
|
|
229
|
+
<button type="button" data-remove="${this.htmlEscape(id)}" tabindex="0" aria-controls="0" aria-label="${this.htmlEscape(label)}">${icon("delete-bin-line")}</button>
|
|
194
230
|
`;
|
|
195
231
|
|
|
196
232
|
this.selectedItems.appendChild(listItem);
|
|
@@ -7,13 +7,14 @@
|
|
|
7
7
|
import { Application } from "@hotwired/stimulus"
|
|
8
8
|
import MultipleMentionsController from "src/decidim/controllers/multiple_mentions/controller"
|
|
9
9
|
|
|
10
|
-
const
|
|
10
|
+
const TomSelect = require("tom-select/dist/cjs/tom-select.popular");
|
|
11
11
|
const iconMock = require("src/decidim/refactor/moved/icon");
|
|
12
12
|
|
|
13
13
|
// Mock the dependencies
|
|
14
|
-
jest.mock("
|
|
14
|
+
jest.mock("tom-select/dist/cjs/tom-select.popular", () => {
|
|
15
15
|
return jest.fn().mockImplementation(() => ({
|
|
16
|
-
|
|
16
|
+
destroy: jest.fn(),
|
|
17
|
+
clearOptions: jest.fn()
|
|
17
18
|
}));
|
|
18
19
|
});
|
|
19
20
|
|
|
@@ -114,12 +115,22 @@ describe("MultipleMentionsManager", () => {
|
|
|
114
115
|
expect(controller.emptyFocusElement).toBe(existingElement);
|
|
115
116
|
});
|
|
116
117
|
|
|
117
|
-
it("should initialize
|
|
118
|
-
expect(
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
118
|
+
it("should initialize TomSelect with correct configuration", () => {
|
|
119
|
+
expect(TomSelect).toHaveBeenCalledWith(searchInput, {
|
|
120
|
+
maxItems: 1,
|
|
121
|
+
valueField: "id",
|
|
122
|
+
labelField: "name",
|
|
123
|
+
searchField: ["name", "nickname"],
|
|
124
|
+
loadThrottle: 200,
|
|
125
|
+
loadingClass: "loading",
|
|
126
|
+
preload: false,
|
|
127
|
+
highlight: true,
|
|
128
|
+
load: expect.any(Function),
|
|
129
|
+
render: {
|
|
130
|
+
option: expect.any(Function),
|
|
131
|
+
"no_results": expect.any(Function)
|
|
132
|
+
},
|
|
133
|
+
onChange: expect.any(Function)
|
|
123
134
|
});
|
|
124
135
|
});
|
|
125
136
|
});
|
|
@@ -226,24 +237,24 @@ describe("MultipleMentionsManager", () => {
|
|
|
226
237
|
controller.selected = ["1", "3"];
|
|
227
238
|
|
|
228
239
|
const list = [
|
|
229
|
-
{
|
|
230
|
-
{
|
|
231
|
-
{
|
|
232
|
-
{
|
|
240
|
+
{ id: "1" },
|
|
241
|
+
{ id: "2" },
|
|
242
|
+
{ id: "3" },
|
|
243
|
+
{ id: "4" }
|
|
233
244
|
];
|
|
234
245
|
|
|
235
246
|
const filtered = controller.filterResults(list);
|
|
236
247
|
|
|
237
248
|
expect(filtered).toEqual([
|
|
238
|
-
{
|
|
239
|
-
{
|
|
249
|
+
{ id: "2" },
|
|
250
|
+
{ id: "4" }
|
|
240
251
|
]);
|
|
241
252
|
});
|
|
242
253
|
|
|
243
254
|
it("should return all users when none are selected", () => {
|
|
244
255
|
const list = [
|
|
245
|
-
{
|
|
246
|
-
{
|
|
256
|
+
{ id: "1" },
|
|
257
|
+
{ id: "2" }
|
|
247
258
|
];
|
|
248
259
|
|
|
249
260
|
const filtered = controller.filterResults(list);
|
|
@@ -255,8 +266,8 @@ describe("MultipleMentionsManager", () => {
|
|
|
255
266
|
controller.selected = ["1", "2"];
|
|
256
267
|
|
|
257
268
|
const list = [
|
|
258
|
-
{
|
|
259
|
-
{
|
|
269
|
+
{ id: "1" },
|
|
270
|
+
{ id: "2" }
|
|
260
271
|
];
|
|
261
272
|
|
|
262
273
|
const filtered = controller.filterResults(list);
|
|
@@ -330,7 +341,6 @@ describe("MultipleMentionsManager", () => {
|
|
|
330
341
|
|
|
331
342
|
expect(controller.selected).toContain("1");
|
|
332
343
|
expect(selectedItems.children.length).toBe(1);
|
|
333
|
-
expect(controller.autoComplete.setInput).toHaveBeenCalledWith("");
|
|
334
344
|
});
|
|
335
345
|
|
|
336
346
|
it("should not add user when max limit is reached", () => {
|
|
@@ -350,7 +360,6 @@ describe("MultipleMentionsManager", () => {
|
|
|
350
360
|
|
|
351
361
|
expect(controller.selected).not.toContain("10");
|
|
352
362
|
expect(selectedItems.children.length).toBe(0);
|
|
353
|
-
expect(controller.autoComplete.setInput).not.toHaveBeenCalled();
|
|
354
363
|
});
|
|
355
364
|
|
|
356
365
|
it("should not add user when direct messages are disabled", () => {
|
|
@@ -368,7 +377,6 @@ describe("MultipleMentionsManager", () => {
|
|
|
368
377
|
|
|
369
378
|
expect(controller.selected).not.toContain("1");
|
|
370
379
|
expect(selectedItems.children.length).toBe(0);
|
|
371
|
-
expect(controller.autoComplete.setInput).not.toHaveBeenCalled();
|
|
372
380
|
});
|
|
373
381
|
|
|
374
382
|
it("should handle multiple valid selections", () => {
|
|
@@ -383,7 +391,6 @@ describe("MultipleMentionsManager", () => {
|
|
|
383
391
|
|
|
384
392
|
expect(controller.selected).toEqual(["1", "2"]);
|
|
385
393
|
expect(selectedItems.children.length).toBe(2);
|
|
386
|
-
expect(controller.autoComplete.setInput).toHaveBeenCalledTimes(2);
|
|
387
394
|
});
|
|
388
395
|
});
|
|
389
396
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import UploadModal from "src/decidim/direct_uploads/upload_modal";
|
|
2
|
-
import { escapeHtml
|
|
2
|
+
import { escapeHtml } from "src/decidim/utilities/text";
|
|
3
3
|
|
|
4
4
|
const updateModalTitle = (modal) => {
|
|
5
5
|
if (modal.uploadItems.children.length === 0) {
|
|
@@ -67,7 +67,7 @@ const updateActiveUploads = (modal) => {
|
|
|
67
67
|
const titleField = isMultiple
|
|
68
68
|
? `${modal.options.resourceName}[${modal.options.addAttribute}][${ix}][title]`
|
|
69
69
|
: `${modal.options.resourceName}[${modal.options.addAttribute}][title]`
|
|
70
|
-
hidden += `<input type="hidden" name="${titleField}" value="${
|
|
70
|
+
hidden += `<input type="hidden" name="${titleField}" value="${escapeHtml(titleValue)}" />`
|
|
71
71
|
|
|
72
72
|
title = titleValue
|
|
73
73
|
}
|
|
@@ -77,8 +77,8 @@ const updateActiveUploads = (modal) => {
|
|
|
77
77
|
: `data-hidden-field="${file.hiddenField}"`
|
|
78
78
|
|
|
79
79
|
const template = `
|
|
80
|
-
<div ${attachmentIdOrHiddenField} data-filename="${
|
|
81
|
-
${(/image/).test(file.type) && "<div><img src=\"data:,\"
|
|
80
|
+
<div ${attachmentIdOrHiddenField} data-filename="${escapeHtml(file.name)}" data-title="${escapeHtml(title)}">
|
|
81
|
+
${(/image/).test(file.type) && "<div><img src=\"data:,\" alt=\"\" /></div>" || ""}
|
|
82
82
|
<p>${escapeHtml(title)}</p>
|
|
83
83
|
${hidden}
|
|
84
84
|
</div>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Uploader } from "src/decidim/direct_uploads/uploader";
|
|
2
2
|
import icon from "src/decidim/refactor/moved/icon";
|
|
3
|
-
import { escapeHtml
|
|
3
|
+
import { escapeHtml } from "src/decidim/utilities/text";
|
|
4
4
|
|
|
5
5
|
const STATUS = {
|
|
6
6
|
VALIDATED: "validated",
|
|
@@ -170,7 +170,7 @@ export default class UploadModal {
|
|
|
170
170
|
|
|
171
171
|
createUploadItem(file, errors, opts = {}) {
|
|
172
172
|
const okTemplate = `
|
|
173
|
-
<img src="data:,"
|
|
173
|
+
<img src="data:," alt="" />
|
|
174
174
|
<span class="upload-modal__span">${escapeHtml(file.name)}</span>
|
|
175
175
|
`
|
|
176
176
|
|
|
@@ -183,16 +183,17 @@ export default class UploadModal {
|
|
|
183
183
|
</div>
|
|
184
184
|
`
|
|
185
185
|
|
|
186
|
+
const titleInputId = `upload-title-${this.getOrdinalNumber()}`
|
|
186
187
|
const titleTemplate = `
|
|
187
|
-
<img src="data:,"
|
|
188
|
+
<img src="data:," alt="" />
|
|
188
189
|
<div>
|
|
189
190
|
<div>
|
|
190
191
|
<label>${this.locales.filename}</label>
|
|
191
192
|
<span class="upload-modal__span">${escapeHtml(file.name)}</span>
|
|
192
193
|
</div>
|
|
193
194
|
<div>
|
|
194
|
-
<label for="${
|
|
195
|
-
<input class="sm" type="text" value="${
|
|
195
|
+
<label for="${titleInputId}">${this.locales.title}</label>
|
|
196
|
+
<input class="sm" type="text" value="${escapeHtml(opts.title || file.name)}" id="${titleInputId}" />
|
|
196
197
|
</div>
|
|
197
198
|
</div>
|
|
198
199
|
`
|
|
@@ -216,7 +217,7 @@ export default class UploadModal {
|
|
|
216
217
|
? `data-attachment-id="${opts.attachmentId}"`
|
|
217
218
|
: ""
|
|
218
219
|
const fullTemplate = `
|
|
219
|
-
<li ${attachmentId} data-filename="${
|
|
220
|
+
<li ${attachmentId} data-filename="${escapeHtml(file.name)}" data-state="${state}">
|
|
220
221
|
<div data-template="${template}">
|
|
221
222
|
${content.trim()}
|
|
222
223
|
<button>${this.locales.remove}</button>
|
|
@@ -252,7 +253,10 @@ export default class UploadModal {
|
|
|
252
253
|
}
|
|
253
254
|
|
|
254
255
|
setProgressBar(name, value) {
|
|
255
|
-
this.uploadItems.
|
|
256
|
+
const item = Array.from(this.uploadItems.querySelectorAll("[data-filename]")).find((el) => el.dataset.filename === name);
|
|
257
|
+
if (item) {
|
|
258
|
+
item.querySelector("progress").value = value;
|
|
259
|
+
}
|
|
256
260
|
}
|
|
257
261
|
|
|
258
262
|
updateAddAttachmentsButton() {
|
|
@@ -13,13 +13,24 @@ export const initializeReverseGeocoding = function() {
|
|
|
13
13
|
|
|
14
14
|
const setLocating = (button, enable) => {
|
|
15
15
|
if (enable) {
|
|
16
|
+
button.dataset.originalContent = button.innerHTML;
|
|
17
|
+
button.textContent = "";
|
|
18
|
+
const spinner = document.createElement("span");
|
|
19
|
+
spinner.className = "geocoding__spinner";
|
|
20
|
+
button.appendChild(spinner);
|
|
21
|
+
button.append(` ${button.dataset.locatingText || "Locating..."}`);
|
|
16
22
|
button.setAttribute("disabled", true);
|
|
23
|
+
button.classList.add("geocoding__button--locating");
|
|
17
24
|
} else {
|
|
25
|
+
if (button.dataset.originalContent) {
|
|
26
|
+
button.innerHTML = button.dataset.originalContent;
|
|
27
|
+
}
|
|
18
28
|
button.removeAttribute("disabled");
|
|
29
|
+
button.classList.remove("geocoding__button--locating");
|
|
19
30
|
}
|
|
20
31
|
};
|
|
21
32
|
|
|
22
|
-
document.querySelectorAll(".
|
|
33
|
+
document.querySelectorAll(".geocoding__button").forEach((button) => {
|
|
23
34
|
button.addEventListener("click", (event) => {
|
|
24
35
|
const target = event.target;
|
|
25
36
|
if (target.disabled) {
|
|
@@ -36,19 +47,18 @@ export const initializeReverseGeocoding = function() {
|
|
|
36
47
|
navigator.geolocation.getCurrentPosition((position) => {
|
|
37
48
|
const coordinates = [position.coords.latitude, position.coords.longitude];
|
|
38
49
|
|
|
39
|
-
// reverse geolocation
|
|
40
50
|
$.post(url, { latitude: coordinates[0], longitude: coordinates[1] }, (data) => {
|
|
41
51
|
input.value = data.address;
|
|
42
52
|
$(input).trigger("geocoder-suggest-coordinates.decidim", [coordinates]);
|
|
53
|
+
setLocating(target, false);
|
|
43
54
|
}).fail((xhr, status, error) => {
|
|
44
55
|
info(input, `${errorNoLocation} ${error}`);
|
|
56
|
+
setLocating(target, false);
|
|
45
57
|
});
|
|
46
58
|
|
|
47
|
-
setLocating(target, false);
|
|
48
|
-
|
|
49
59
|
}, (evt) => {
|
|
50
60
|
info(input, `${errorNoLocation} ${evt.message}`);
|
|
51
|
-
target
|
|
61
|
+
setLocating(target, false);
|
|
52
62
|
}, {
|
|
53
63
|
enableHighAccuracy: true
|
|
54
64
|
});
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/* global global, jest */
|
|
2
|
+
|
|
3
|
+
import { initializeReverseGeocoding } from "src/decidim/geocoding/reverse_geocoding";
|
|
4
|
+
|
|
5
|
+
describe("reverseGeocoding", () => {
|
|
6
|
+
let container = null;
|
|
7
|
+
let button = null;
|
|
8
|
+
let input = null;
|
|
9
|
+
let label = null;
|
|
10
|
+
let mockGeolocation = null;
|
|
11
|
+
let mockPost = null;
|
|
12
|
+
let originalGeolocation = null;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
document.body.innerHTML = "";
|
|
16
|
+
|
|
17
|
+
container = document.createElement("div");
|
|
18
|
+
container.innerHTML = `
|
|
19
|
+
<label>
|
|
20
|
+
<input id="test_address" type="text" />
|
|
21
|
+
<div class="geocoding__locate">
|
|
22
|
+
<button
|
|
23
|
+
class="geocoding__button"
|
|
24
|
+
type="button"
|
|
25
|
+
data-input="test_address"
|
|
26
|
+
data-error-no-location="Could not detect location."
|
|
27
|
+
data-error-unsupported="Device not supported."
|
|
28
|
+
data-locating-text="Locating..."
|
|
29
|
+
data-url="/locate">
|
|
30
|
+
Use my location
|
|
31
|
+
</button>
|
|
32
|
+
</div>
|
|
33
|
+
</label>
|
|
34
|
+
`;
|
|
35
|
+
document.body.appendChild(container);
|
|
36
|
+
|
|
37
|
+
button = container.querySelector(".geocoding__button");
|
|
38
|
+
input = container.querySelector("#test_address");
|
|
39
|
+
label = container.querySelector("label");
|
|
40
|
+
|
|
41
|
+
originalGeolocation = navigator.geolocation;
|
|
42
|
+
mockGeolocation = {
|
|
43
|
+
getCurrentPosition: jest.fn()
|
|
44
|
+
};
|
|
45
|
+
Reflect.defineProperty(global.navigator, "geolocation", {
|
|
46
|
+
value: mockGeolocation,
|
|
47
|
+
configurable: true
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
mockPost = jest.fn();
|
|
51
|
+
global.jQuery = jest.fn(() => ({ trigger: jest.fn() }));
|
|
52
|
+
global.jQuery.post = mockPost;
|
|
53
|
+
// eslint-disable-next-line id-length
|
|
54
|
+
global.$ = global.jQuery;
|
|
55
|
+
|
|
56
|
+
initializeReverseGeocoding();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
afterEach(() => {
|
|
60
|
+
Reflect.defineProperty(global.navigator, "geolocation", {
|
|
61
|
+
value: originalGeolocation,
|
|
62
|
+
configurable: true
|
|
63
|
+
});
|
|
64
|
+
jest.restoreAllMocks();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("when clicking the button", () => {
|
|
68
|
+
it("shows the spinner and disables the button", () => {
|
|
69
|
+
mockGeolocation.getCurrentPosition.mockImplementation(() => {});
|
|
70
|
+
|
|
71
|
+
button.click();
|
|
72
|
+
|
|
73
|
+
expect(button.disabled).toBe(true);
|
|
74
|
+
expect(button.classList.contains("geocoding__button--locating")).toBe(true);
|
|
75
|
+
expect(button.querySelector(".geocoding__spinner")).not.toBeNull();
|
|
76
|
+
expect(button.textContent).toContain("Locating...");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("uses custom locating text from data attribute", () => {
|
|
80
|
+
button.dataset.locatingText = "Finding you...";
|
|
81
|
+
mockGeolocation.getCurrentPosition.mockImplementation(() => {});
|
|
82
|
+
|
|
83
|
+
button.click();
|
|
84
|
+
|
|
85
|
+
expect(button.textContent).toContain("Finding you...");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("stores original content for restoration", () => {
|
|
89
|
+
mockGeolocation.getCurrentPosition.mockImplementation(() => {});
|
|
90
|
+
|
|
91
|
+
button.click();
|
|
92
|
+
|
|
93
|
+
expect(button.dataset.originalContent).toContain("Use my location");
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("when geolocation succeeds", () => {
|
|
98
|
+
it("restores the button after successful reverse geocoding", () => {
|
|
99
|
+
const mockPosition = {
|
|
100
|
+
coords: { latitude: 40.7128, longitude: -74.006 }
|
|
101
|
+
};
|
|
102
|
+
mockGeolocation.getCurrentPosition.mockImplementation((success) => {
|
|
103
|
+
success(mockPosition);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const mockDeferred = { fail: jest.fn() };
|
|
107
|
+
mockPost.mockImplementation((url, data, callback) => {
|
|
108
|
+
callback({ address: "New York, NY, USA" });
|
|
109
|
+
return mockDeferred;
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
button.click();
|
|
113
|
+
|
|
114
|
+
expect(button.disabled).toBe(false);
|
|
115
|
+
expect(button.classList.contains("geocoding__button--locating")).toBe(false);
|
|
116
|
+
expect(button.querySelector(".geocoding__spinner")).toBeNull();
|
|
117
|
+
expect(button.textContent).toContain("Use my location");
|
|
118
|
+
expect(input.value).toBe("New York, NY, USA");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("restores the button when reverse geocoding fails", () => {
|
|
122
|
+
const mockPosition = {
|
|
123
|
+
coords: { latitude: 40.7128, longitude: -74.006 }
|
|
124
|
+
};
|
|
125
|
+
mockGeolocation.getCurrentPosition.mockImplementation((success) => {
|
|
126
|
+
success(mockPosition);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const mockDeferred = { fail: jest.fn((callback) => {
|
|
130
|
+
callback({}, "error", "Not Found");
|
|
131
|
+
}) };
|
|
132
|
+
mockPost.mockReturnValue(mockDeferred);
|
|
133
|
+
|
|
134
|
+
button.click();
|
|
135
|
+
|
|
136
|
+
expect(button.disabled).toBe(false);
|
|
137
|
+
expect(button.classList.contains("geocoding__button--locating")).toBe(false);
|
|
138
|
+
expect(button.textContent).toContain("Use my location");
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe("when geolocation fails", () => {
|
|
143
|
+
it("restores the button when user denies permission", () => {
|
|
144
|
+
const mockError = { message: "User denied geolocation" };
|
|
145
|
+
mockGeolocation.getCurrentPosition.mockImplementation((success, error) => {
|
|
146
|
+
error(mockError);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
button.click();
|
|
150
|
+
|
|
151
|
+
expect(button.disabled).toBe(false);
|
|
152
|
+
expect(button.classList.contains("geocoding__button--locating")).toBe(false);
|
|
153
|
+
expect(button.textContent).toContain("Use my location");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("shows an error message", () => {
|
|
157
|
+
const mockError = { message: "User denied geolocation" };
|
|
158
|
+
mockGeolocation.getCurrentPosition.mockImplementation((success, error) => {
|
|
159
|
+
error(mockError);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
button.click();
|
|
163
|
+
|
|
164
|
+
const errorElement = label.querySelector(".form-error");
|
|
165
|
+
expect(errorElement).not.toBeNull();
|
|
166
|
+
expect(errorElement.textContent).toContain("Could not detect location.");
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe("when geolocation is not supported", () => {
|
|
171
|
+
it("shows an unsupported error and does not show spinner", () => {
|
|
172
|
+
Reflect.defineProperty(global.navigator, "geolocation", {
|
|
173
|
+
value: null,
|
|
174
|
+
configurable: true
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
button.click();
|
|
178
|
+
|
|
179
|
+
expect(button.disabled).toBe(false);
|
|
180
|
+
expect(button.classList.contains("geocoding__button--locating")).toBe(false);
|
|
181
|
+
const errorElement = label.querySelector(".form-error");
|
|
182
|
+
expect(errorElement).not.toBeNull();
|
|
183
|
+
expect(errorElement.textContent).toContain("Device not supported.");
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe("when button is already disabled", () => {
|
|
188
|
+
it("does not trigger geolocation again", () => {
|
|
189
|
+
button.disabled = true;
|
|
190
|
+
mockGeolocation.getCurrentPosition.mockImplementation(() => {});
|
|
191
|
+
|
|
192
|
+
button.click();
|
|
193
|
+
|
|
194
|
+
expect(mockGeolocation.getCurrentPosition).not.toHaveBeenCalled();
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
});
|
|
@@ -34,7 +34,6 @@ import "src/decidim/security/selfxss_warning"
|
|
|
34
34
|
import "src/decidim/session_timeouter"
|
|
35
35
|
import "src/decidim/results_listing"
|
|
36
36
|
import "src/decidim/data_consent"
|
|
37
|
-
import "src/decidim/sw"
|
|
38
37
|
import "src/decidim/attachments"
|
|
39
38
|
import "src/decidim/dropdown_menu"
|
|
40
39
|
import "src/decidim/callout"
|
|
@@ -62,6 +61,7 @@ window.Decidim = window.Decidim || {
|
|
|
62
61
|
announceForScreenReader
|
|
63
62
|
};
|
|
64
63
|
|
|
64
|
+
window.createDialog = createDialog;
|
|
65
65
|
window.morphdom = morphdom
|
|
66
66
|
|
|
67
67
|
// eslint-disable-next-line max-params
|
|
@@ -156,6 +156,8 @@ document.addEventListener("turbo:load", () => {
|
|
|
156
156
|
deprecationMessage(container, '.callout[role="alert"]', '.flash[role="alert"]'));
|
|
157
157
|
document.querySelectorAll(".js-back-to-list").forEach((container) =>
|
|
158
158
|
deprecationMessage(container, ".js-back-to-list", "NEEDS TO BE REMOVED"));
|
|
159
|
+
document.querySelectorAll("[data-toggler]").forEach((container) =>
|
|
160
|
+
deprecationMessage(container, "[data-toggler]", "Use the Stimulus toggle controller with hidden targets"));
|
|
159
161
|
})
|
|
160
162
|
|
|
161
163
|
// REDESIGN_PENDING: deprecated
|
|
@@ -236,8 +238,7 @@ const initializer = (element = document) => {
|
|
|
236
238
|
document.dispatchEvent(new CustomEvent("decidim:loaded", { detail: { element } }));
|
|
237
239
|
}
|
|
238
240
|
|
|
239
|
-
//
|
|
240
|
-
// mentions stops working
|
|
241
|
+
// Keep this under jQuery ready to support components initialized on legacy templates
|
|
241
242
|
$(() => initializer());
|
|
242
243
|
|
|
243
244
|
// Run initializer action over the new DOM elements
|
|
@@ -53,7 +53,7 @@ self.addEventListener("notificationclick", (event) => {
|
|
|
53
53
|
// avoid caching admin or users paths
|
|
54
54
|
registerRoute(
|
|
55
55
|
({ url }) =>
|
|
56
|
-
["
|
|
56
|
+
["admin", "users"].some((pathSegment) => url.pathname.split("/").includes(pathSegment)),
|
|
57
57
|
new NetworkOnly()
|
|
58
58
|
);
|
|
59
59
|
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
const escapeQuotes = (text) => {
|
|
2
2
|
if (!text) {
|
|
3
3
|
return "";
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
el.appendChild(document.createTextNode(text));
|
|
8
|
-
return el.innerHTML;
|
|
6
|
+
return text.replace(/"/g, """);
|
|
9
7
|
}
|
|
10
8
|
|
|
11
|
-
export const
|
|
9
|
+
export const escapeHtml = (text) => {
|
|
12
10
|
if (!text) {
|
|
13
11
|
return "";
|
|
14
12
|
}
|
|
15
13
|
|
|
16
|
-
|
|
14
|
+
const el = document.createElement("div");
|
|
15
|
+
el.appendChild(document.createTextNode(text));
|
|
16
|
+
return escapeQuotes(el.innerHTML);
|
|
17
17
|
}
|