diff --git a/decidim-core/app/cells/decidim/diff/attribute.erb b/decidim-core/app/cells/decidim/diff/attribute.erb index cb2f4040a9fc2..3d4649f0bd5d4 100644 --- a/decidim-core/app/cells/decidim/diff/attribute.erb +++ b/decidim-core/app/cells/decidim/diff/attribute.erb @@ -5,23 +5,13 @@ -
" id="<%= attribute_diff_id("diff_view_unified_unescaped") %>"> - <%= diff_unified(data, :unescaped_html) %> +
+ <%= diff_unified(data, format) %>
-
" id="<%= attribute_diff_id("diff_view_unified_escaped") %>"> - <%= diff_unified(data, :html) %> +
+ <%= diff_split(data, "left", format) %> + <%= diff_split(data, "right", format) %>
-
"> - <%= diff_split(data, "left", :unescaped_html) %> - - <%= diff_split(data, "right", :unescaped_html) %> -
- -
"> - <%= diff_split(data, "left", :html) %> - - <%= diff_split(data, "right", :html) %> -
diff --git a/decidim-core/app/cells/decidim/diff/diff_mode_dropdown.erb b/decidim-core/app/cells/decidim/diff/diff_mode_dropdown.erb index 5166529a0bf11..e31dc38517107 100644 --- a/decidim-core/app/cells/decidim/diff/diff_mode_dropdown.erb +++ b/decidim-core/app/cells/decidim/diff/diff_mode_dropdown.erb @@ -17,13 +17,13 @@
-
-
- <%= output_split_diff(data, direction, format) %> + <% available_locales_for(data).each do |locale, filled| %> + <% next unless filled %> + <% unless I18n.locale.to_s == locale %> +

<%= locale_name(locale) %>

+ <% end %> +
+
+ <%= output_split_diff(data, direction, format, locale) %> +
-
+ <% end %>
diff --git a/decidim-core/app/cells/decidim/diff/diff_unified.erb b/decidim-core/app/cells/decidim/diff/diff_unified.erb index 6b95eabe5b9ee..afbd8e3b8c1cb 100644 --- a/decidim-core/app/cells/decidim/diff/diff_unified.erb +++ b/decidim-core/app/cells/decidim/diff/diff_unified.erb @@ -1,7 +1,13 @@
-
-
- <%= output_unified_diff(data, format) %> + <% available_locales_for(data).each do |locale, filled| %> + <% next unless filled %> + <% unless I18n.locale.to_s == locale %> +

<%= locale_name(locale) %>

+ <% end %> +
+
+ <%= output_unified_diff(data, format, locale) %> +
-
+ <% end %>
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.