decidim-core 0.30.5 → 0.30.7
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/announcement_cell.rb +10 -2
- data/app/cells/decidim/attachments_file_tab/show.erb +1 -1
- data/app/cells/decidim/content_blocks/highlighted_elements_with_cell_for_list_cell.rb +5 -1
- data/app/cells/decidim/content_blocks/participatory_space_metadata/content.erb +2 -2
- data/app/cells/decidim/nav_links/show.erb +2 -2
- data/app/cells/decidim/report_button/already_reported_modal.erb +1 -1
- data/app/cells/decidim/report_button/flag_modal.erb +1 -1
- data/app/cells/decidim/report_user_button/already_reported_modal.erb +1 -1
- data/app/cells/decidim/report_user_button/flag_modal.erb +1 -1
- data/app/cells/decidim/statistic/show.erb +4 -4
- data/app/cells/decidim/upload_modal/files.erb +16 -8
- data/app/cells/decidim/upload_modal_cell.rb +21 -5
- data/app/commands/decidim/multiple_attachments_methods.rb +20 -3
- data/app/controllers/concerns/decidim/direct_upload.rb +2 -12
- data/app/controllers/decidim/devise/sessions_controller.rb +7 -0
- data/app/helpers/concerns/decidim/flash_helper_extensions.rb +2 -2
- data/app/jobs/decidim/export_participatory_space_job.rb +1 -1
- data/app/jobs/decidim/find_and_update_descendants_job.rb +8 -2
- data/app/jobs/decidim/update_search_indexes_job.rb +2 -2
- data/app/models/decidim/attachment.rb +22 -1
- data/app/packs/src/decidim/a11y.js +29 -0
- data/app/packs/src/decidim/a11y.test.js +81 -0
- data/app/packs/src/decidim/confirm.js +8 -1
- data/app/packs/src/decidim/confirm.test.js +225 -0
- data/app/packs/src/decidim/direct_uploads/upload_field.js +1 -1
- data/app/packs/src/decidim/focus_guard.js +4 -4
- data/app/packs/stylesheets/decidim/_cards.scss +12 -4
- data/app/packs/stylesheets/decidim/_modal_update.scss +1 -1
- data/app/packs/stylesheets/decidim/_participatory_spaces.scss +1 -1
- data/app/services/decidim/open_data_exporter.rb +1 -1
- data/app/views/decidim/devise/invitations/edit.html.erb +3 -3
- data/app/views/layouts/decidim/header/_main_links_mobile_account.html.erb +1 -1
- data/config/initializers/devise.rb +6 -0
- data/config/locales/ar.yml +3 -3
- data/config/locales/bg.yml +0 -4
- data/config/locales/ca-IT.yml +12 -6
- data/config/locales/ca.yml +12 -6
- data/config/locales/cs.yml +23 -8
- data/config/locales/de.yml +4 -8
- data/config/locales/el.yml +1 -2
- data/config/locales/en.yml +10 -4
- data/config/locales/es-MX.yml +13 -7
- data/config/locales/es-PY.yml +13 -7
- data/config/locales/es.yml +15 -9
- data/config/locales/eu.yml +13 -10
- data/config/locales/fi-plain.yml +5 -4
- data/config/locales/fi.yml +6 -5
- data/config/locales/fr-CA.yml +11 -5
- data/config/locales/fr.yml +13 -7
- data/config/locales/gl.yml +0 -2
- data/config/locales/hu.yml +5 -9
- data/config/locales/id-ID.yml +0 -2
- data/config/locales/it.yml +1 -3
- data/config/locales/ja.yml +11 -8
- data/config/locales/lb.yml +0 -2
- data/config/locales/lt.yml +1 -3
- data/config/locales/lv.yml +0 -2
- data/config/locales/nl.yml +0 -2
- data/config/locales/no.yml +0 -2
- data/config/locales/pl.yml +0 -4
- data/config/locales/pt-BR.yml +29 -5
- data/config/locales/pt.yml +0 -2
- data/config/locales/ro-RO.yml +5 -6
- data/config/locales/ru.yml +0 -2
- data/config/locales/sk.yml +0 -4
- data/config/locales/sv.yml +254 -31
- data/config/locales/tr-TR.yml +17 -5
- data/config/locales/zh-CN.yml +0 -2
- data/config/locales/zh-TW.yml +1 -3
- data/lib/decidim/assets/tailwind/tailwind.config.js.erb +1 -1
- data/lib/decidim/core/version.rb +1 -1
- data/lib/decidim/form_builder.rb +40 -6
- data/lib/decidim/maintenance/taxonomy_importer.rb +1 -1
- data/lib/decidim/searchable.rb +4 -4
- metadata +8 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5110ff22ed67aef95bf387023ec7d18d9602a92944c82c2ab9373e786c04393c
|
|
4
|
+
data.tar.gz: b754a2b2ddb4a72ae07dcd384203d61454ced7f6a3a41a4f80a5bb501b3dc310
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3659a2a34d2531735e1a03710329261952c724361d0661bb9cf02c7a2acc76c2516266e8030a82e4dcc40789361d67845f1421addfcbf0f1fe4e0743a1288572
|
|
7
|
+
data.tar.gz: 9ec948751168cd3f4a8cc2b768be6a118cd22b47aecb0712fa64a1c01d58a5cdf2b7f2f20963fdd0eec2384a41753bbaec558016f533bfab7176227df6f3cadd
|
|
@@ -68,15 +68,23 @@ module Decidim
|
|
|
68
68
|
def clean_body
|
|
69
69
|
return unless body
|
|
70
70
|
|
|
71
|
-
Array(body).map { |paragraph|
|
|
71
|
+
Array(body).map { |paragraph| clean(paragraph) }.join
|
|
72
72
|
end
|
|
73
73
|
|
|
74
74
|
def clean_announcement
|
|
75
|
+
return if announcement.is_a?(Hash) && announcement.values.all?(&:blank?)
|
|
76
|
+
|
|
75
77
|
clean(announcement)
|
|
76
78
|
end
|
|
77
79
|
|
|
78
80
|
def clean(value)
|
|
79
|
-
|
|
81
|
+
return if value.blank? || value.nil?
|
|
82
|
+
|
|
83
|
+
if value.include?("rich-text-display")
|
|
84
|
+
decidim_sanitize_admin(translated_attribute(value))
|
|
85
|
+
else
|
|
86
|
+
tag.p(decidim_sanitize_admin(translated_attribute(value)))
|
|
87
|
+
end
|
|
80
88
|
end
|
|
81
89
|
end
|
|
82
90
|
end
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
<div class="row column">
|
|
2
|
-
<%= form.upload :file, button_class: "button button__sm button__transparent-secondary" %>
|
|
2
|
+
<%= form.upload :file, attachments: form.object.file.present? ? [form.object.file] : [], button_class: "button button__sm button__transparent-secondary" %>
|
|
3
3
|
</div>
|
|
@@ -19,10 +19,14 @@ module Decidim
|
|
|
19
19
|
@list_cell ||= cell(
|
|
20
20
|
list_cell_path,
|
|
21
21
|
published_components.one? ? published_components.first : published_components,
|
|
22
|
-
**model.settings.to_h, see_all_path:
|
|
22
|
+
**model.settings.to_h, **extra_list_cell_options, see_all_path:
|
|
23
23
|
)
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
+
def extra_list_cell_options
|
|
27
|
+
{}
|
|
28
|
+
end
|
|
29
|
+
|
|
26
30
|
def see_all_path; end
|
|
27
31
|
end
|
|
28
32
|
end
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
<% metadata_valued_items.each do |item| %>
|
|
3
3
|
<div class="participatory-space__metadata-item">
|
|
4
4
|
<div class="participatory-space__metadata-item-title">
|
|
5
|
-
<
|
|
5
|
+
<p><%= item[:title] %></p>
|
|
6
6
|
</div>
|
|
7
|
-
<
|
|
7
|
+
<p><%= item[:value] %></p>
|
|
8
8
|
</div>
|
|
9
9
|
<% end %>
|
|
10
10
|
</div>
|
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
<%= icon "arrow-down-s-line" %>
|
|
5
5
|
<%= icon "arrow-up-s-line" %>
|
|
6
6
|
</button>
|
|
7
|
-
<ul id="dropdown-menu-participatory-space" class="participatory-space__nav">
|
|
7
|
+
<ul id="dropdown-menu-participatory-space" class="participatory-space__nav" aria-hidden="true">
|
|
8
8
|
<% model.each do |item| %>
|
|
9
9
|
<li role="menuitem">
|
|
10
10
|
<%= link_to item[:url], class: "participatory-space__nav-item" do %>
|
|
11
|
-
<%= item[:name] %>
|
|
11
|
+
<%= decidim_escape_translated(item[:name]) %>
|
|
12
12
|
<%= icon "arrow-right-line" %>
|
|
13
13
|
<% end %>
|
|
14
14
|
</li>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<%= decidim_modal id: modal_id, class: "flag-modal" do %>
|
|
2
2
|
<div data-dialog-container>
|
|
3
3
|
<%= icon "flag-line" %>
|
|
4
|
-
<h2 tabindex="-1" data-dialog-title><%= t("decidim.shared.flag_modal.title") %></h2>
|
|
4
|
+
<h2 id="dialog-title-<%= modal_id %>" tabindex="-1" data-dialog-title><%= t("decidim.shared.flag_modal.title") %></h2>
|
|
5
5
|
<div>
|
|
6
6
|
<div class="form__wrapper flag-modal__form">
|
|
7
7
|
<p class="flag-modal__form-description"><%= t("decidim.shared.flag_modal.already_reported") %></p>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<%= form_for report_form, builder:, url: report_path, method: :post, html: { id: nil } do |f| %>
|
|
3
3
|
<div data-dialog-container>
|
|
4
4
|
<%= icon "flag-line" %>
|
|
5
|
-
<h2 tabindex="-1" data-dialog-title><%= t("decidim.shared.flag_modal.title") %></h2>
|
|
5
|
+
<h2 id="dialog-title-<%= modal_id %>" tabindex="-1" data-dialog-title><%= t("decidim.shared.flag_modal.title") %></h2>
|
|
6
6
|
<div>
|
|
7
7
|
<div class="form__wrapper flag-modal__form">
|
|
8
8
|
<p id="dialog-desc-<%= modal_id %>" class="flag-modal__form-description"><%= t("decidim.shared.flag_modal.description") %></p>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<%= decidim_modal id: modal_id, class: "flag-user-modal" do %>
|
|
2
2
|
<div data-dialog-container>
|
|
3
3
|
<%= icon "flag-line" %>
|
|
4
|
-
<h2 tabindex="-1" data-dialog-title><%= t("decidim.shared.flag_user_modal.title") %></h2>
|
|
4
|
+
<h2 id="dialog-title-<%= modal_id %>" tabindex="-1" data-dialog-title><%= t("decidim.shared.flag_user_modal.title") %></h2>
|
|
5
5
|
<div>
|
|
6
6
|
<div class="form__wrapper flag-modal__form">
|
|
7
7
|
<p class="flag-modal__form-description"><%= t("decidim.shared.flag_user_modal.already_reported") %></p>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<%= form_for report_form, builder:, url: report_path, method: :post, html: { id: nil } do |f| %>
|
|
3
3
|
<div data-dialog-container>
|
|
4
4
|
<%= icon "flag-line" %>
|
|
5
|
-
<h2 tabindex="-1" data-dialog-title><%= t("decidim.shared.flag_user_modal.title") %></h2>
|
|
5
|
+
<h2 id="dialog-title-<%= modal_id %>" tabindex="-1" data-dialog-title><%= t("decidim.shared.flag_user_modal.title") %></h2>
|
|
6
6
|
<div>
|
|
7
7
|
<div class="form__wrapper flag-modal__form">
|
|
8
8
|
<p class="flag-modal__form-description"><%= t("decidim.shared.flag_user_modal.description") %></p>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<div class="statistic <%= stat_dom_class %>" data-statistic>
|
|
2
|
-
<
|
|
2
|
+
<p class="statistic__title" title="<%= stat_title %>">
|
|
3
3
|
<%= stat_title %>
|
|
4
|
-
</
|
|
5
|
-
<
|
|
4
|
+
</p>
|
|
5
|
+
<p class="statistic__number">
|
|
6
6
|
<%= stat_number %>
|
|
7
|
-
</
|
|
7
|
+
</p>
|
|
8
8
|
</div>
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<div class="upload-modal__files-container upload-container-for-<%= attribute %> <%= with_title %>">
|
|
2
2
|
<div>
|
|
3
|
-
<%= label %>
|
|
3
|
+
<%= options[:paragraph] == true ? paragraph : label %>
|
|
4
4
|
|
|
5
5
|
<% if options[:help_text].present? %>
|
|
6
|
-
<
|
|
6
|
+
<p class="help-text"><%= options[:help_text] %></p>
|
|
7
7
|
<% end %>
|
|
8
8
|
|
|
9
9
|
<%# NOTE: this block is about wrapping a default image for the avatar with the new styles,
|
|
@@ -22,24 +22,32 @@
|
|
|
22
22
|
<div class="upload-modal__files" data-active-uploads="<%= modal_id %>">
|
|
23
23
|
<% attachments.each do |attachment| %>
|
|
24
24
|
<% next if [Array, Hash].any? { |klass| attachment.is_a? klass } %>
|
|
25
|
+
<% is_persisted_attachment = attachment.is_a?(Decidim::Attachment) && attachment.persisted? %>
|
|
26
|
+
<% attachment_blob = blob(attachment) %>
|
|
25
27
|
|
|
26
|
-
<div class="attachment-details" data-attachment-id="<%= attachment.id %>" data-title="<%= title_for(attachment) %>" data-filename="<%= file_name_for(attachment) %>" data-state="uploaded">
|
|
27
|
-
<% if file_attachment_path(attachment) &&
|
|
28
|
+
<div class="attachment-details"<% if is_persisted_attachment %> data-attachment-id="<%= attachment.id %>"<% end %> data-title="<%= title_for(attachment) %>" data-filename="<%= file_name_for(attachment) %>" data-state="uploaded" data-hidden-field="<%= attachment_blob&.signed_id %>">
|
|
29
|
+
<% if file_attachment_path(attachment) && attachment_blob&.image? %>
|
|
28
30
|
<div><%= image_tag(file_attachment_path(attachment), alt: "") %></div>
|
|
29
31
|
<% elsif uploader_default_image_path(attribute).present? %>
|
|
30
32
|
<div><%= image_tag uploader_default_image_path(attribute) %></div>
|
|
31
33
|
<% end %>
|
|
32
34
|
|
|
33
35
|
<% if has_title? %>
|
|
34
|
-
<
|
|
35
|
-
<%= form.hidden_field attribute, multiple: true, value: attachment.id, id: attachment.id %>
|
|
36
|
+
<p><%= title_for(attachment) %></p>
|
|
36
37
|
<% else %>
|
|
37
|
-
<% if
|
|
38
|
-
<
|
|
38
|
+
<% if attachment_blob&.image? %>
|
|
39
|
+
<p><%= title_for(attachment) %></p>
|
|
39
40
|
<% else %>
|
|
40
41
|
<%= link_to title_for(attachment), file_attachment_path(attachment), class: "w-full break-all mb-2" %>
|
|
41
42
|
<% end %>
|
|
42
43
|
<% end %>
|
|
44
|
+
<% if attachment_blob.present? %>
|
|
45
|
+
<% if is_persisted_attachment %>
|
|
46
|
+
<%= form.hidden_field attribute, value: attachment.id, id: "hidden_#{attribute}_#{attachment.id}" %>
|
|
47
|
+
<% else %>
|
|
48
|
+
<%= form.hidden_field attribute, value: attachment_blob.signed_id, id: "hidden_#{attribute}_#{attachment_blob.id}" %>
|
|
49
|
+
<% end %>
|
|
50
|
+
<% end %>
|
|
43
51
|
</div>
|
|
44
52
|
<% end %>
|
|
45
53
|
</div>
|
|
@@ -30,6 +30,10 @@ module Decidim
|
|
|
30
30
|
form.send(:custom_label, attribute, options[:label], { required: required?, for: nil })
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
+
def paragraph
|
|
34
|
+
form.send(:custom_paragraph, attribute, options[:label], { required: required? })
|
|
35
|
+
end
|
|
36
|
+
|
|
33
37
|
def button_label
|
|
34
38
|
return button_edit_label if attachments.count.positive?
|
|
35
39
|
|
|
@@ -71,13 +75,16 @@ module Decidim
|
|
|
71
75
|
end
|
|
72
76
|
|
|
73
77
|
# By default FoundationRailsHelper adds form errors next to input, but since input is in the modal
|
|
74
|
-
# and modal is hidden by default, we
|
|
78
|
+
# and modal is hidden by default, we add a hidden checkbox field to handle HTML5 validation.
|
|
75
79
|
# This should only be necessary when file is required by the form.
|
|
80
|
+
# Note: Validation errors are now displayed in the main form area, not inside the modal.
|
|
76
81
|
def input_validation_field
|
|
77
82
|
object_name = form.object.present? ? "#{form.object.model_name.param_key}[#{add_attribute}_validation]" : "#{add_attribute}_validation"
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
83
|
+
check_box_tag object_name, 1, attachments.present?, class: "reset-defaults", hidden: true, label: false, required: required?, id: validation_field_id
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def validation_field_id
|
|
87
|
+
"#{attribute}_validation"
|
|
81
88
|
end
|
|
82
89
|
|
|
83
90
|
def explanation
|
|
@@ -121,7 +128,16 @@ module Decidim
|
|
|
121
128
|
@attachments = begin
|
|
122
129
|
attachments = options[:attachments] || form.object.send(attribute)
|
|
123
130
|
attachments = Array(attachments).compact_blank
|
|
124
|
-
attachments.map
|
|
131
|
+
attachments.map do |attachment|
|
|
132
|
+
case attachment
|
|
133
|
+
when String
|
|
134
|
+
ActiveStorage::Blob.find_signed(attachment)
|
|
135
|
+
when Integer
|
|
136
|
+
Decidim::Attachment.find_by(id: attachment)
|
|
137
|
+
else
|
|
138
|
+
attachment
|
|
139
|
+
end
|
|
140
|
+
end.compact
|
|
125
141
|
end
|
|
126
142
|
end
|
|
127
143
|
|
|
@@ -41,8 +41,9 @@ module Decidim
|
|
|
41
41
|
|
|
42
42
|
def create_attachments(first_weight: 0)
|
|
43
43
|
weight = first_weight
|
|
44
|
-
# Add the weights first to the old
|
|
45
|
-
|
|
44
|
+
# Add the weights first to the old documents
|
|
45
|
+
document_ids = keep_ids
|
|
46
|
+
Decidim::Attachment.where(id: document_ids).each do |document|
|
|
46
47
|
document.update!(weight:)
|
|
47
48
|
weight += 1
|
|
48
49
|
end
|
|
@@ -59,7 +60,7 @@ module Decidim
|
|
|
59
60
|
documents = include_all_attachments ? documents_attached_to.attachments.with_attached_file : documents_attached_to.documents
|
|
60
61
|
|
|
61
62
|
documents.each do |document|
|
|
62
|
-
document.destroy!
|
|
63
|
+
document.destroy! unless keep_ids.include?(document.id)
|
|
63
64
|
end
|
|
64
65
|
|
|
65
66
|
documents_attached_to.reload
|
|
@@ -98,5 +99,21 @@ module Decidim
|
|
|
98
99
|
def blob(signed_id)
|
|
99
100
|
ActiveStorage::Blob.find_signed(signed_id)
|
|
100
101
|
end
|
|
102
|
+
|
|
103
|
+
def keep_ids
|
|
104
|
+
documents_array = Array(@form.documents)
|
|
105
|
+
documents_array.map do |doc|
|
|
106
|
+
case doc
|
|
107
|
+
when Decidim::Attachment
|
|
108
|
+
doc.id
|
|
109
|
+
when Integer
|
|
110
|
+
doc
|
|
111
|
+
when String
|
|
112
|
+
doc.match?(/\A\d+\z/) ? doc.to_i : nil
|
|
113
|
+
when Hash
|
|
114
|
+
(doc[:id] || doc["id"]).to_i
|
|
115
|
+
end
|
|
116
|
+
end.compact
|
|
117
|
+
end
|
|
101
118
|
end
|
|
102
119
|
end
|
|
@@ -9,8 +9,6 @@ module Decidim
|
|
|
9
9
|
skip_before_action :verify_organization
|
|
10
10
|
|
|
11
11
|
before_action :check_organization!,
|
|
12
|
-
:check_authenticated!,
|
|
13
|
-
:check_user_belongs_to_organization,
|
|
14
12
|
:validate_direct_upload
|
|
15
13
|
end
|
|
16
14
|
|
|
@@ -42,16 +40,6 @@ module Decidim
|
|
|
42
40
|
head :unauthorized if current_organization.blank? && current_admin.blank?
|
|
43
41
|
end
|
|
44
42
|
|
|
45
|
-
def check_authenticated!
|
|
46
|
-
head :unauthorized if current_user.blank? && current_admin.blank?
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def check_user_belongs_to_organization
|
|
50
|
-
return if current_admin.present?
|
|
51
|
-
|
|
52
|
-
head :unauthorized unless current_organization == current_user.organization
|
|
53
|
-
end
|
|
54
|
-
|
|
55
43
|
def allowed_extensions
|
|
56
44
|
if user_has_elevated_role?
|
|
57
45
|
current_organization.settings.upload_allowed_file_extensions_admin
|
|
@@ -71,6 +59,8 @@ module Decidim
|
|
|
71
59
|
private
|
|
72
60
|
|
|
73
61
|
def user_has_elevated_role?
|
|
62
|
+
return false if current_user.blank? || current_organization.blank? || current_user.organization != current_organization
|
|
63
|
+
|
|
74
64
|
[
|
|
75
65
|
current_user&.admin?,
|
|
76
66
|
defined?(Decidim::Assemblies::AssembliesWithUserRole) && Decidim::Assemblies::AssembliesWithUserRole.for(current_user).any?,
|
|
@@ -9,6 +9,8 @@ module Decidim
|
|
|
9
9
|
|
|
10
10
|
before_action :check_sign_in_enabled, only: :create
|
|
11
11
|
|
|
12
|
+
rescue_from ActionController::InvalidAuthenticityToken, with: :redirect_to_referer_or_path
|
|
13
|
+
|
|
12
14
|
def create
|
|
13
15
|
super do |user|
|
|
14
16
|
if user.admin?
|
|
@@ -44,6 +46,11 @@ module Decidim
|
|
|
44
46
|
|
|
45
47
|
private
|
|
46
48
|
|
|
49
|
+
def redirect_to_referer_or_path
|
|
50
|
+
set_flash_message(:alert, "csrf_token", scope: "devise.failure")
|
|
51
|
+
redirect_back(fallback_location: root_path) && return
|
|
52
|
+
end
|
|
53
|
+
|
|
47
54
|
def check_sign_in_enabled
|
|
48
55
|
redirect_to new_user_session_path unless current_organization.sign_in_enabled?
|
|
49
56
|
end
|
|
@@ -117,9 +117,9 @@ module Decidim
|
|
|
117
117
|
end
|
|
118
118
|
|
|
119
119
|
def message(value)
|
|
120
|
-
return content_tag(:
|
|
120
|
+
return content_tag(:p, value, class: "flash__message") unless value.is_a?(Hash)
|
|
121
121
|
|
|
122
|
-
content_tag(:
|
|
122
|
+
content_tag(:p, class: "flash__message") do
|
|
123
123
|
concat value[:title]
|
|
124
124
|
concat content_tag(:span, value[:body], class: "flash__message-body")
|
|
125
125
|
end
|
|
@@ -11,7 +11,7 @@ module Decidim
|
|
|
11
11
|
manifest.name == name.to_sym
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
collection = export_manifest.collection.call(participatory_space)
|
|
14
|
+
collection = export_manifest.collection.call(participatory_space, user)
|
|
15
15
|
serializer = export_manifest.serializer
|
|
16
16
|
|
|
17
17
|
export_data = Decidim::Exporters.find_exporter(format).new(collection, serializer).export
|
|
@@ -4,8 +4,14 @@ module Decidim
|
|
|
4
4
|
# Update search indexes for each descendants of a given element
|
|
5
5
|
class FindAndUpdateDescendantsJob < ApplicationJob
|
|
6
6
|
queue_as :default
|
|
7
|
+
MAX_DEPTH = 5
|
|
8
|
+
|
|
9
|
+
def perform(element, current_depth = 0)
|
|
10
|
+
if current_depth >= MAX_DEPTH
|
|
11
|
+
Rails.logger.warn "Max depth of #{MAX_DEPTH} reached for element #{element.class.name} with id #{element.id}. Stopping recursion."
|
|
12
|
+
return
|
|
13
|
+
end
|
|
7
14
|
|
|
8
|
-
def perform(element)
|
|
9
15
|
descendants_collector = components_for(element)
|
|
10
16
|
descendants_collector << element.comments.to_a if element.respond_to?(:comments)
|
|
11
17
|
|
|
@@ -14,7 +20,7 @@ module Decidim
|
|
|
14
20
|
descendants_collector.each do |descendants|
|
|
15
21
|
next if descendants.blank?
|
|
16
22
|
|
|
17
|
-
Decidim::UpdateSearchIndexesJob.perform_later(descendants)
|
|
23
|
+
Decidim::UpdateSearchIndexesJob.perform_later(descendants, current_depth + 1)
|
|
18
24
|
end
|
|
19
25
|
end
|
|
20
26
|
|
|
@@ -4,8 +4,8 @@ module Decidim
|
|
|
4
4
|
class UpdateSearchIndexesJob < ApplicationJob
|
|
5
5
|
queue_as :default
|
|
6
6
|
|
|
7
|
-
def perform(elements)
|
|
8
|
-
elements.each { |element| element.try(:try_update_index_for_search_resource) }
|
|
7
|
+
def perform(elements, current_depth = 0)
|
|
8
|
+
elements.each { |element| element.try(:try_update_index_for_search_resource, current_depth) }
|
|
9
9
|
end
|
|
10
10
|
end
|
|
11
11
|
end
|
|
@@ -9,7 +9,7 @@ module Decidim
|
|
|
9
9
|
include Traceable
|
|
10
10
|
|
|
11
11
|
before_save :set_content_type_and_size, if: :attached?
|
|
12
|
-
before_validation :set_link_content_type_and_size, if: :
|
|
12
|
+
before_validation :set_link_content_type_and_size, if: :editable_link?
|
|
13
13
|
|
|
14
14
|
translatable_fields :title, :description
|
|
15
15
|
belongs_to :attachment_collection, class_name: "Decidim::AttachmentCollection", optional: true
|
|
@@ -69,6 +69,20 @@ module Decidim
|
|
|
69
69
|
link.present?
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
+
# Whether this attachment is a link that can be edited or not.
|
|
73
|
+
#
|
|
74
|
+
# Returns Boolean.
|
|
75
|
+
def editable_link?
|
|
76
|
+
!destroyed? && !frozen? && link?
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Whether this attachment has a file or not.
|
|
80
|
+
#
|
|
81
|
+
# Returns Boolean.
|
|
82
|
+
def file?
|
|
83
|
+
file.attached?
|
|
84
|
+
end
|
|
85
|
+
|
|
72
86
|
# Which kind of file this is.
|
|
73
87
|
#
|
|
74
88
|
# Returns String.
|
|
@@ -123,5 +137,12 @@ module Decidim
|
|
|
123
137
|
def self.log_presenter_class_for(_log)
|
|
124
138
|
Decidim::AdminLog::AttachmentPresenter
|
|
125
139
|
end
|
|
140
|
+
|
|
141
|
+
def can_participate?(user)
|
|
142
|
+
return true unless attached_to
|
|
143
|
+
return true unless attached_to.respond_to?(:can_participate?)
|
|
144
|
+
|
|
145
|
+
attached_to.can_participate?(user)
|
|
146
|
+
end
|
|
126
147
|
end
|
|
127
148
|
end
|
|
@@ -124,6 +124,13 @@ const createDropdown = (component) => {
|
|
|
124
124
|
* @return {void}
|
|
125
125
|
*/
|
|
126
126
|
const createDialog = (component) => {
|
|
127
|
+
const getFocusableElements = (container) => {
|
|
128
|
+
const selectors = "a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex='-1'])";
|
|
129
|
+
return Array.from(container.querySelectorAll(selectors)).filter(
|
|
130
|
+
(el) => el.offsetParent !== null
|
|
131
|
+
);
|
|
132
|
+
};
|
|
133
|
+
|
|
127
134
|
const {
|
|
128
135
|
dataset: { dialog, ...attrs }
|
|
129
136
|
} = component;
|
|
@@ -145,11 +152,33 @@ const createDialog = (component) => {
|
|
|
145
152
|
backdropSelector: `[data-dialog="${dialog}"]`,
|
|
146
153
|
enableAutoFocus: false,
|
|
147
154
|
onOpen: (params, trigger) => {
|
|
155
|
+
const keyHandler = (event) => {
|
|
156
|
+
if (event.key !== "Tab") {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const focusable = getFocusableElements(params);
|
|
160
|
+
if (focusable.length === 0) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (event.shiftKey && document.activeElement === focusable[0]) {
|
|
164
|
+
event.preventDefault();
|
|
165
|
+
focusable[focusable.length - 1].focus({ preventScroll: true });
|
|
166
|
+
} else if (!event.shiftKey && document.activeElement === focusable[focusable.length - 1]) {
|
|
167
|
+
event.preventDefault();
|
|
168
|
+
focusable[0].focus({ preventScroll: true });
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
params._focusTrapHandler = keyHandler;
|
|
172
|
+
params.addEventListener("keydown", keyHandler);
|
|
148
173
|
setFocusOnTitle(params);
|
|
149
174
|
window.focusGuard.trap(params, trigger);
|
|
150
175
|
params.dispatchEvent(new CustomEvent("open.dialog"));
|
|
151
176
|
},
|
|
152
177
|
onClose: (params) => {
|
|
178
|
+
if (params._focusTrapHandler) {
|
|
179
|
+
params.removeEventListener("keydown", params._focusTrapHandler);
|
|
180
|
+
Reflect.deleteProperty(params, "_focusTrapHandler");
|
|
181
|
+
}
|
|
153
182
|
window.focusGuard.disable();
|
|
154
183
|
params.dispatchEvent(new CustomEvent("close.dialog"));
|
|
155
184
|
},
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/* global jest */
|
|
2
|
+
|
|
3
|
+
import { createDialog } from "src/decidim/a11y"
|
|
4
|
+
|
|
5
|
+
describe("a11y dialog focus trap", () => {
|
|
6
|
+
const dialogHtml = `
|
|
7
|
+
<button id="open-btn" data-dialog-open="testDialog">Open</button>
|
|
8
|
+
<div id="test-dialog" data-dialog="testDialog">
|
|
9
|
+
<div data-dialog-container>
|
|
10
|
+
<h2 id="dialog-title-testDialog" tabindex="-1" data-dialog-title>Test Dialog</h2>
|
|
11
|
+
<a href="#">Link 1</a>
|
|
12
|
+
<button>Button 1</button>
|
|
13
|
+
<input type="text" />
|
|
14
|
+
<button>Button 2</button>
|
|
15
|
+
<a href="#">Link 2</a>
|
|
16
|
+
</div>
|
|
17
|
+
<div data-dialog-actions>
|
|
18
|
+
<button data-dialog-close="testDialog">Close</button>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
`;
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
document.body.innerHTML = dialogHtml;
|
|
25
|
+
window.Decidim = {
|
|
26
|
+
currentDialogs: {}
|
|
27
|
+
};
|
|
28
|
+
window.focusGuard = {
|
|
29
|
+
trap: jest.fn(),
|
|
30
|
+
disable: jest.fn()
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("keydown handler", () => {
|
|
35
|
+
let dialogEl = null;
|
|
36
|
+
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
const component = document.querySelector("[data-dialog]");
|
|
39
|
+
createDialog(component);
|
|
40
|
+
dialogEl = document.querySelector("[data-dialog='testDialog']");
|
|
41
|
+
// Get the dialog from Decidim.currentDialogs and open it
|
|
42
|
+
const dialog = window.Decidim.currentDialogs.testDialog;
|
|
43
|
+
dialog.open();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("adds keydown handler on open", () => {
|
|
47
|
+
expect(dialogEl._focusTrapHandler).toBeDefined();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("handles Tab key", () => {
|
|
51
|
+
const selectors = "a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex='-1'])";
|
|
52
|
+
const tabbableElements = Array.from(dialogEl.querySelectorAll(selectors)).filter(
|
|
53
|
+
(el) => el.offsetParent || el.offsetParent === null
|
|
54
|
+
);
|
|
55
|
+
tabbableElements[tabbableElements.length - 1].focus();
|
|
56
|
+
|
|
57
|
+
const event = new KeyboardEvent("keydown", { key: "Tab", bubbles: true });
|
|
58
|
+
const preventDefault = jest.fn();
|
|
59
|
+
event.preventDefault = preventDefault;
|
|
60
|
+
|
|
61
|
+
dialogEl.dispatchEvent(event);
|
|
62
|
+
|
|
63
|
+
expect(preventDefault).toHaveBeenCalled();
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("onClose cleanup", () => {
|
|
68
|
+
it("removes the keydown handler on close", () => {
|
|
69
|
+
const component = document.querySelector("[data-dialog]");
|
|
70
|
+
createDialog(component);
|
|
71
|
+
const dialogEl = document.querySelector("[data-dialog='testDialog']");
|
|
72
|
+
|
|
73
|
+
const dialog = window.Decidim.currentDialogs.testDialog;
|
|
74
|
+
dialog.open();
|
|
75
|
+
expect(dialogEl._focusTrapHandler).toBeDefined();
|
|
76
|
+
|
|
77
|
+
dialog.close();
|
|
78
|
+
expect(dialogEl._focusTrapHandler).toBeUndefined();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -162,7 +162,9 @@ export const initializeConfirm = () => {
|
|
|
162
162
|
return handleDocumentEvent(ev, [
|
|
163
163
|
Rails.linkClickSelector,
|
|
164
164
|
Rails.buttonClickSelector,
|
|
165
|
-
Rails.formInputClickSelector
|
|
165
|
+
Rails.formInputClickSelector,
|
|
166
|
+
'button[data-confirm][type="button"]',
|
|
167
|
+
"form button[data-confirm]"
|
|
166
168
|
]);
|
|
167
169
|
});
|
|
168
170
|
document.addEventListener("change", (ev) => {
|
|
@@ -180,6 +182,11 @@ export const initializeConfirm = () => {
|
|
|
180
182
|
$(Rails.formInputClickSelector).on("click.confirm", (ev) => {
|
|
181
183
|
handleConfirm(ev, getMatchingEventTarget(ev, Rails.formInputClickSelector));
|
|
182
184
|
});
|
|
185
|
+
|
|
186
|
+
// Handle button[type="button"] with data-confirm inside forms
|
|
187
|
+
$('button[data-confirm][type="button"]').on("click.confirm", (ev) => {
|
|
188
|
+
handleConfirm(ev, ev.currentTarget);
|
|
189
|
+
});
|
|
183
190
|
});
|
|
184
191
|
};
|
|
185
192
|
|