diff --git a/decidim-core/app/cells/decidim/diff/show.erb b/decidim-core/app/cells/decidim/diff/show.erb
index ef3d22f96d9a7..d38d236d11fd3 100644
--- a/decidim-core/app/cells/decidim/diff/show.erb
+++ b/decidim-core/app/cells/decidim/diff/show.erb
@@ -1,8 +1,50 @@
-<%= render :diff_mode_dropdown %>
-<%= render :diff_mode_html %>
+
+
+
+ -
+
+
+ <%= link_to "#diff-text", class: "button button--nomargin button--sc hollow" do %>
+ <%= t("versions.tabs.text") %>
+ <% end %>
+
+
+ -
+
+
+ <%= link_to "#diff-source", class: "button button--nomargin button--sc hollow" do %>
+ <%= t("versions.tabs.source") %>
+ <% end %>
+
+
+ -
+
+
+ <%= link_to "#diff-preview", class: "button button--nomargin button--sc hollow" do %>
+ <%= t("versions.tabs.preview") %>
+ <% end %>
+
+
+
+
+
+
+
+
+ <%= render :diff_mode_dropdown %>
+ <% diff_data.each do |data| %>
+ <%= attribute(data, :text) %>
+ <% end %>
+
+
+ <%= render :diff_mode_dropdown %>
+ <% diff_data.each do |data| %>
+ <%= attribute(data, :html) %>
+ <% end %>
+
+
+ <%= preview %>
+
+
-
- <% diff_data.each do |data| %>
- <%= attribute(data) %>
- <% end %>
diff --git a/decidim-core/app/cells/decidim/diff_cell.rb b/decidim-core/app/cells/decidim/diff_cell.rb
index 63cc301707da0..b2d75b12aead2 100644
--- a/decidim-core/app/cells/decidim/diff_cell.rb
+++ b/decidim-core/app/cells/decidim/diff_cell.rb
@@ -1,15 +1,15 @@
# frozen_string_literal: true
-require "decidim/diffy_extension"
-
module Decidim
# This cell renders the diff between `:old_data` and `:new_data`.
class DiffCell < Decidim::ViewModel
include Cell::ViewModel::Partial
+ include ::HtmlToPlainText # from the premailer gem
+ include LanguageChooserHelper
include LayoutHelper
- def attribute(data)
- render locals: { data: data }
+ def attribute(data, format)
+ render locals: { data: data, format: format }
end
def diff_unified(data, format)
@@ -22,12 +22,6 @@ def diff_split(data, direction, format)
private
- # Adds a unique ID prefix for the attribute div IDs to avoid duplicate IDs
- # in the DOM.
- def attribute_diff_id(id)
- "#{SecureRandom.uuid}_#{id}"
- end
-
# A PaperTrail::Version.
def current_version
model
@@ -38,6 +32,11 @@ def item
current_version.item
end
+ # preview (if associated item allows it)
+ def preview
+ diff_renderer.preview
+ end
+
# DiffRenderer class for the current_version's item; falls back to `BaseDiffRenderer`.
def diff_renderer_class
if current_version.item_type.deconstantize == "Decidim"
@@ -63,17 +62,32 @@ def diff_data
diff_renderer.diff.values
end
+ def available_locales_for(data)
+ locales = { I18n.locale.to_s => true }
+
+ locales.merge! valid_locale_keys(data[:old_value]) if data[:old_value].is_a?(Hash)
+ locales.merge! valid_locale_keys(data[:new_value]) if data[:new_value].is_a?(Hash)
+
+ locales.filter { |k| I18n.locale_available?(k) }
+ end
+
+ def valid_locale_keys(input)
+ locales = input.transform_values(&:present?)
+ locales.merge!(input["machine_translations"].transform_values(&:present?)) if input["machine_translations"].is_a?(Hash)
+ locales
+ end
+
# Outputs the diff as HTML with inline highlighting of the character
# changes between lines.
#
# Returns an HTML-safe string.
- def output_unified_diff(data, format)
+ def output_unified_diff(data, format, locale)
Diffy::Diff.new(
- data[:old_value].to_s,
- data[:new_value].to_s,
+ old_new_values(data, format, locale)[0],
+ old_new_values(data, format, locale)[1],
allow_empty_diff: false,
include_plus_and_minus_in_html: true
- ).to_s(format)
+ ).to_s(:html)
end
# Outputs the diff as HTML with side-by-side changes between lines.
@@ -81,16 +95,37 @@ def output_unified_diff(data, format)
# The left side represents deletions while the right side represents insertions.
#
# Returns an HTML-safe string.
- def output_split_diff(data, direction, format)
+ def output_split_diff(data, direction, format, locale)
Diffy::SplitDiff.new(
- data[:old_value].to_s,
- data[:new_value].to_s,
+ old_new_values(data, format, locale)[0],
+ old_new_values(data, format, locale)[1],
allow_empty_diff: false,
- format: format,
+ format: :html,
include_plus_and_minus_in_html: true
).send(direction)
end
+ def old_new_values(data, format, locale)
+ original_translations = data[:old_value].try(:filter) { |key, value| value.present? && key != "machine_translations" }
+ [
+ value_from_locale(data[:old_value], format, locale),
+ value_from_locale(data[:new_value], format, locale, original_translations)
+ ]
+ end
+
+ def value_from_locale(value, format, locale, skip_machine_keys = {})
+ text = value.is_a?(Hash) ? find_locale_value(value, locale, skip_machine_keys) : value
+
+ text = text.first if text.is_a?(Array)
+ # return text.to_s if format == :html || text.blank?
+
+ convert_to_text(text.to_s.dup, 100)
+ end
+
+ def find_locale_value(input, locale, skip_machine_keys = {})
+ input.dig(locale).presence || skip_machine_keys[locale].presence || input.dig("machine_translations", locale)
+ end
+
# Gives the option to view HTML unescaped for better user experience.
# Official means created from admin (where rich text editor is enabled).
def show_html_view_dropdown?
diff --git a/decidim-core/app/packs/src/decidim/diff_mode_dropdown.js b/decidim-core/app/packs/src/decidim/diff_mode_dropdown.js
index 04ac309f50165..8fd7e5f87d213 100644
--- a/decidim-core/app/packs/src/decidim/diff_mode_dropdown.js
+++ b/decidim-core/app/packs/src/decidim/diff_mode_dropdown.js
@@ -3,8 +3,10 @@ $(() => {
$(document).on("click", ".diff-view-by a.diff-view-mode", (event) => {
event.preventDefault();
- const $target = $(event.target)
- let type = "escaped";
+ const $target = $(event.target);
+ const $container = $target.closest(".tabs-panel");
+ const $unified = $container.find(".diff_view_unified")
+ const $split = $container.find(".diff_view_split")
const $selected = $target.parents(".is-dropdown-submenu-parent").find("#diff-view-selected");
if ($selected.text().trim() === $target.text().trim()) {
return;
@@ -12,38 +14,13 @@ $(() => {
$selected.text($target.text());
- if ($target.attr("id") === "diff-view-unified") {
- if ($(".row.diff_view_split_escaped").hasClass("hide")) {
- type = "unescaped";
- }
-
- $allDiffViews.addClass("hide");
- $(`.row.diff_view_unified_${type}`).removeClass("hide");
- }
- if ($target.attr("id") === "diff-view-split") {
- if ($(".row.diff_view_unified_escaped").hasClass("hide")) {
- type = "unescaped";
- }
-
- $allDiffViews.addClass("hide");
- $(`.row.diff_view_split_${type}`).removeClass("hide");
- }
- })
-
- $(document).on("click", ".diff-view-by a.diff-view-html", (event) => {
- event.preventDefault();
- const $target = $(event.target);
- $target.parents(".is-dropdown-submenu-parent").find("#diff-view-html-selected").text($target.text());
- const $visibleDiffViewsId = $allDiffViews.not(".hide").first().attr("id").split("_").slice(1, -1).join("_");
- const $visibleDiffViews = $allDiffViews.filter(`[id*=${$visibleDiffViewsId}]`)
-
- if ($target.attr("id") === "diff-view-escaped-html") {
- $visibleDiffViews.filter("[id$=_unescaped]").addClass("hide");
- $visibleDiffViews.filter("[id$=_escaped]").removeClass("hide");
+ if ($target.hasClass("diff-view-unified")) {
+ $split.addClass("hide");
+ $unified.removeClass("hide");
}
- if ($target.attr("id") === "diff-view-unescaped-html") {
- $visibleDiffViews.filter("[id$=_escaped]").addClass("hide");
- $visibleDiffViews.filter("[id$=_unescaped]").removeClass("hide");
+ if ($target.hasClass("diff-view-split")) {
+ $unified.addClass("hide");
+ $split.removeClass("hide");
}
})
-});
+});
\ No newline at end of file
diff --git a/decidim-core/app/packs/stylesheets/decidim/modules/_versions.scss b/decidim-core/app/packs/stylesheets/decidim/modules/_versions.scss
index 1219712295e9a..308feba12fc21 100644
--- a/decidim-core/app/packs/stylesheets/decidim/modules/_versions.scss
+++ b/decidim-core/app/packs/stylesheets/decidim/modules/_versions.scss
@@ -1,3 +1,55 @@
+.versions-diff{
+ .versions-selector{
+ border-bottom: $border;
+ padding-bottom: $global-margin * .5;
+ color: $muted;
+ font-size: rem-calc(19);
+ display: flex;
+ align-items: center;
+
+ //Override foundation
+ .tabs{
+ width: 100%;
+
+ @include flexgap(.5rem);
+ }
+ }
+
+ //Override foundation
+ .tabs,
+ .tabs-content{
+ background: transparent;
+ }
+
+ //Override foundation
+ .tabs-title{
+ > a{
+ padding: 0;
+
+ &:hover{
+ background: transparent;
+ }
+ }
+
+ > a[aria-selected='true']{
+ background: transparent;
+ }
+
+ &:not(.is-active) .button{
+ opacity: .4;
+ }
+ }
+
+ .diff-preview{
+ margin: 1em 0;
+
+ .heading3{
+ font-size: 1.5em;
+ margin-bottom: 1em;
+ }
+ }
+}
+
.diff-direction-label{
display: block;
font-size: 85%;
@@ -13,7 +65,6 @@
padding: 0;
width: 100%;
background: transparent;
- min-height: 2.7rem;
}
del,
@@ -28,19 +79,23 @@
text-decoration: none;
}
- del strong{
+ .del strong,
+ .del strong *{
font-weight: normal;
- background: scale-color($color-removal, $lightness: -8%);
+ background: scale-color($color-removal, $lightness: -10%);
+ display: inline;
}
- ins strong{
+ .ins strong,
+ .ins strong *{
font-weight: normal;
- background: scale-color($color-addition, $lightness: -8%);
+ background: scale-color($color-addition, $lightness: -10%);
+ display: inline;
}
li{
position: relative;
- padding: .5rem 1rem .5rem 1.5rem;
+ padding: .1rem 1rem .1rem 1.5rem;
margin: 0;
&.ins,
@@ -48,18 +103,18 @@
.symbol{
position: absolute;
left: .5rem;
- top: .5rem;
+ top: .1rem;
width: 1rem;
}
}
&.ins{
- background: $color-addition;
+ background: scale-color($color-addition, $lightness: 30%, $saturation: 30%);
color: scale-color($color-addition, $lightness: -75%, $saturation: -75%);
}
&.del{
- background: $color-removal;
+ background: scale-color($color-removal, $lightness: 30%, $saturation: 30%);
color: scale-color($color-removal, $lightness: -75%, $saturation: -75%);
}
@@ -71,4 +126,4 @@
background: none repeat scroll 0 0 gray;
}
}
-}
+}
\ No newline at end of file
diff --git a/decidim-core/app/services/decidim/base_diff_renderer.rb b/decidim-core/app/services/decidim/base_diff_renderer.rb
index cc3997410e53b..049fc068e43d8 100644
--- a/decidim-core/app/services/decidim/base_diff_renderer.rb
+++ b/decidim-core/app/services/decidim/base_diff_renderer.rb
@@ -28,6 +28,11 @@ def diff
end
end
+ # implement if item can be previewed
+ def preview
+ nil
+ end
+
private
attr_reader :version
diff --git a/decidim-core/config/locales/en.yml b/decidim-core/config/locales/en.yml
index 705d88a8abafb..082b6233e8410 100644
--- a/decidim-core/config/locales/en.yml
+++ b/decidim-core/config/locales/en.yml
@@ -1746,6 +1746,10 @@ en:
short: "%d/%m/%Y %H:%M"
time_of_day: "%H:%M"
versions:
+ tabs:
+ text: Text diff
+ source: Source diff
+ preview: Preview
directions:
left: Deletions
right: Additions
diff --git a/decidim-core/lib/decidim/diffy_extension.rb b/decidim-core/lib/decidim/diffy_extension.rb
deleted file mode 100644
index 9f29facda60a0..0000000000000
--- a/decidim-core/lib/decidim/diffy_extension.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-# frozen_string_literal: true
-
-module Decidim
- # Extending Diffy gem to accomodate the needs of app/cells/decidim/diff_cell.rb
- module DiffyExtension
- # HtmlFormatter that returns basic html output (no inline highlighting)
- # and does not escape HTML tags.
- class UnescapedHtmlFormatter < Diffy::HtmlFormatter
- # We exclude the tags `del` and `ins` so the diffy styling does not apply.
- TAGS = (UserInputScrubber.new.tags.to_a - %w(del ins)).freeze
-
- def to_s
- str = wrap_lines(@diff.map { |line| wrap_line(line) })
- ActionView::Base.new(ActionView::LookupContext.new(nil), {}, nil).sanitize(str, tags: TAGS)
- end
- end
-
- # Adding a new method to Diffy::Format so we can pass the
- # `:unescaped_html` option when calling Diffy::Diff#to_s.
- Diffy::Format.module_eval do
- def unescaped_html
- UnescapedHtmlFormatter.new(self, options).to_s
- end
- end
-
- # The private "split" method SplitDiff needs to be overriden to take into
- # account the new :unescaped_html format, and the fact that the tags
- #
are not there anymore
- Diffy::SplitDiff.module_eval do
- private
-
- def split
- return [split_left, split_right] unless @format == :unescaped_html
-
- [unescaped_split_left, unescaped_split_right]
- end
-
- def unescaped_split_left
- @diff.gsub(%r{([\s\S]*?)}, "")
- end
-
- def unescaped_split_right
- @diff.gsub(%r{([\s\S]*?)}, "")
- end
- end
- end
-end
diff --git a/decidim-proposals/app/helpers/decidim/proposals/application_helper.rb b/decidim-proposals/app/helpers/decidim/proposals/application_helper.rb
index 2278f0b08f3d3..6f7031bcbb173 100644
--- a/decidim-proposals/app/helpers/decidim/proposals/application_helper.rb
+++ b/decidim-proposals/app/helpers/decidim/proposals/application_helper.rb
@@ -8,6 +8,7 @@ module ApplicationHelper
include Decidim::Comments::CommentsHelper
include PaginateHelper
include ProposalVotesHelper
+ include ProposalPresenterHelper
include ::Decidim::EndorsableHelper
include ::Decidim::FollowableHelper
include Decidim::MapHelper
@@ -84,27 +85,6 @@ def minimum_votes_per_user_enabled?
minimum_votes_per_user.positive?
end
- def not_from_collaborative_draft(proposal)
- proposal.linked_resources(:proposals, "created_from_collaborative_draft").empty?
- end
-
- def not_from_participatory_text(proposal)
- proposal.participatory_text_level.nil?
- end
-
- # If the proposal is official or the rich text editor is enabled on the
- # frontend, the proposal body is considered as safe content; that's unless
- # the proposal comes from a collaborative_draft or a participatory_text.
- def safe_content?
- rich_text_editor_in_public_views? && not_from_collaborative_draft(@proposal) ||
- (@proposal.official? || @proposal.official_meeting?) && not_from_participatory_text(@proposal)
- end
-
- # If the content is safe, HTML tags are sanitized, otherwise, they are stripped.
- def render_proposal_body(proposal)
- Decidim::ContentProcessor.render(render_sanitized_content(proposal, :body), "div")
- end
-
# Returns :text_area or :editor based on the organization' settings.
def text_editor_for_proposal_body(form)
options = {
diff --git a/decidim-proposals/app/helpers/decidim/proposals/proposal_presenter_helper.rb b/decidim-proposals/app/helpers/decidim/proposals/proposal_presenter_helper.rb
new file mode 100644
index 0000000000000..a5f34f319fbd0
--- /dev/null
+++ b/decidim-proposals/app/helpers/decidim/proposals/proposal_presenter_helper.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module Decidim
+ module Proposals
+ module ProposalPresenterHelper
+ include Decidim::ApplicationHelper
+
+ def not_from_collaborative_draft(proposal)
+ proposal.linked_resources(:proposals, "created_from_collaborative_draft").empty?
+ end
+
+ def not_from_participatory_text(proposal)
+ proposal.participatory_text_level.nil?
+ end
+
+ # If the proposal is official or the rich text editor is enabled on the
+ # frontend, the proposal body is considered as safe content; that's unless
+ # the proposal comes from a collaborative_draft or a participatory_text.
+ def safe_content?
+ rich_text_editor_in_public_views? && not_from_collaborative_draft(@proposal) ||
+ (@proposal.official? || @proposal.official_meeting?) && not_from_participatory_text(@proposal)
+ end
+
+ def render_proposal_title(proposal)
+ Decidim::Proposals::ProposalPresenter.new(proposal).title(links: true, html_escape: true)
+ end
+
+ # If the content is safe, HTML tags are sanitized, otherwise, they are stripped.
+ def render_proposal_body(proposal)
+ Decidim::ContentProcessor.render(render_sanitized_content(proposal, :body), "div")
+ end
+ end
+ end
+end
diff --git a/decidim-proposals/app/services/decidim/proposals/diff_renderer.rb b/decidim-proposals/app/services/decidim/proposals/diff_renderer.rb
index aa607cf606bd1..9ca60f57dddfc 100644
--- a/decidim-proposals/app/services/decidim/proposals/diff_renderer.rb
+++ b/decidim-proposals/app/services/decidim/proposals/diff_renderer.rb
@@ -3,6 +3,19 @@
module Decidim
module Proposals
class DiffRenderer < BaseDiffRenderer
+ include ActionView::Helpers::TextHelper
+ include ActionView::Helpers::TagHelper
+ include ProposalPresenterHelper
+ include SanitizeHelper
+
+ delegate :organization, to: :proposal, prefix: :current
+
+ def preview
+ title = content_tag(:h3, render_proposal_title(proposal), class: "heading3")
+ body = content_tag(:div, render_proposal_body(proposal), class: "body")
+ content_tag(:div, "#{title}#{body}".html_safe, class: "diff-preview diff-proposal")
+ end
+
private
# Lists which attributes will be diffable and how they should be rendered.