diff --git a/Gemfile b/Gemfile index 947a7208f..78e64c0e2 100644 --- a/Gemfile +++ b/Gemfile @@ -6,6 +6,7 @@ gem "aws-sdk-s3" gem "bootsnap", require: false gem "bootstrap-kaminari-views" gem "dartsass-rails" +gem "diffy" gem "gds-api-adapters" gem "gds-sso" gem "govspeak" diff --git a/Gemfile.lock b/Gemfile.lock index ddcd363ee..304209cc1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -143,6 +143,7 @@ GEM date (3.3.4) debug_inspector (1.2.0) diff-lcs (1.5.1) + diffy (3.4.2) docile (1.4.0) domain_name (0.6.20240107) drb (2.2.1) @@ -799,6 +800,7 @@ DEPENDENCIES capybara-select-2 dartsass-rails database_cleaner-mongoid + diffy factory_bot gds-api-adapters gds-sso diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js index dbad5426c..31890d8fd 100644 --- a/app/assets/config/manifest.js +++ b/app/assets/config/manifest.js @@ -1,3 +1,4 @@ //= link_directory ../javascripts .js //= link_tree ../builds //= link application.css +//= link diff.css diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index d29032fe2..db48c3074 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -1,3 +1,5 @@ +@import "govuk_publishing_components/all_components"; + @import "select2"; @import "select2-bootstrap"; diff --git a/app/assets/stylesheets/diff.scss b/app/assets/stylesheets/diff.scss new file mode 100644 index 000000000..eab6079f8 --- /dev/null +++ b/app/assets/stylesheets/diff.scss @@ -0,0 +1,83 @@ +// stylelint-disable max-nesting-depth + +// Diff of two editions + +$added-color: #ddffdd; +$strong-added-color: #77f177; +$removed-color: #ffdddd; +$strong-removed-color: #ffaaaa; +$gray-lighter: govuk-colour("light-grey"); +$state-danger-text: govuk-colour("red"); +$state-success-text: govuk-colour("green"); + +.diff { + border: 1px solid $gray-lighter; + border-left: 40px solid $gray-lighter; + border-radius: 3px; + padding: 15px; + + ul { + padding-left: 0; + + li { + min-height: 24px; + margin: 0 -15px; + padding: 0 15px; + word-wrap: break-word; + list-style: none; + position: relative; + + del, + ins { + text-decoration: none; + } + } + + .del, + .ins { + padding-top: 2px; + } + + .del { + background-color: $removed-color; + + strong { + font-weight: normal; + background-color: $strong-removed-color; + } + } + + .ins { + background-color: $added-color; + + strong { + font-weight: normal; + background-color: $strong-added-color; + } + } + + .del::before, + .ins::before { + position: absolute; + font-weight: bold; + margin-left: -55px; + width: 40px; + text-align: center; + min-height: 24px; + top: 0; + bottom: 0; + } + + .del::before { + color: $state-danger-text; + background-color: $removed-color; + content: "-"; + } + + .ins::before { + color: $state-success-text; + background-color: $added-color; + content: "+"; + } + } +} diff --git a/app/controllers/admin_controller.rb b/app/controllers/admin_controller.rb new file mode 100644 index 000000000..e084d4835 --- /dev/null +++ b/app/controllers/admin_controller.rb @@ -0,0 +1,74 @@ +class AdminController < ApplicationController + layout "design_system" + + before_action :check_authorisation, if: :document_type_slug + + def index_of_admin_forms; end + + def edit_metadata; end + + def edit_facets; end + + def save_metadata + @submitted_params = params.permit( + :name, + :description, + :organisations, + :editing_organisations, + :related, + :base_path, + :content_id, + "filter.format".to_sym, + :format_name, + :document_title, + :document_noun, + ).to_unsafe_h + %i[organisations editing_organisations related].each do |str_that_should_be_arr| + @submitted_params[str_that_should_be_arr] = @submitted_params[str_that_should_be_arr].split("\r\n") + if @submitted_params[str_that_should_be_arr].empty? + @submitted_params.delete(str_that_should_be_arr) + end + end + @submitted_params["filter"] = { "format": @submitted_params["filter.format".to_sym] } + @submitted_params.delete("filter.format".to_sym) + + @original_schema = current_format.finder_schema.schema + @proposed_schema = @original_schema.merge(@submitted_params) + render :temporary_output + end + + def save_facets + @submitted_params = params.except(:authenticity_token, :action, :controller, :document_type_slug).to_unsafe_h + # hashes come through as e.g. `facets: { "0": { "name": "Category"... }}`, + # we need to convert to array e.g. `facets: [ { "name", "Category"... } ]` + @submitted_params["facets"] = @submitted_params["facets"].keys.map(&:to_i).sort.map do |i| + @submitted_params["facets"][i.to_s] + end + @submitted_params["facets"].each_with_index do |hash, i| + hash.each do |key, value| + # delete empty values + if value == "" + @submitted_params["facets"][i].delete(key) + # cast booleans + elsif %w[true false].include?(value) + @submitted_params["facets"][i][key] = value == "true" + end + end + end + + @original_schema = current_format.finder_schema.schema + @proposed_schema = @original_schema.merge(@submitted_params) + render :temporary_output + end + +private + + def check_authorisation + if current_format + authorize current_format + else + flash[:danger] = "That format doesn't exist. If you feel you've reached this in error, please contact your main GDS contact." + redirect_to root_path + end + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index a2424b2de..46da43d40 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,4 +1,8 @@ module ApplicationHelper + def render_back_link(options) + render("govuk_publishing_components/components/back_link", options) + end + def facet_options(form, facet) form.object.facet_options(facet) end diff --git a/app/policies/document_policy.rb b/app/policies/document_policy.rb index 2c3e15954..79e5d938b 100644 --- a/app/policies/document_policy.rb +++ b/app/policies/document_policy.rb @@ -11,6 +11,16 @@ def index? # FIXME: fix this, attachments are using the wrong policy alias_method :destroy?, :index? + def can_request_edits_to_finder? + # TODO: figure out who should be allowed to do what RE administrating finders + publish? + end + alias_method :index_of_admin_forms?, :can_request_edits_to_finder? + alias_method :edit_metadata?, :can_request_edits_to_finder? + alias_method :save_metadata?, :can_request_edits_to_finder? + alias_method :edit_facets?, :can_request_edits_to_finder? + alias_method :save_facets?, :can_request_edits_to_finder? + def publish? document_type_editor? || gds_editor? || departmental_editor? end diff --git a/app/views/admin/edit_facets.erb b/app/views/admin/edit_facets.erb new file mode 100644 index 000000000..19e12214b --- /dev/null +++ b/app/views/admin/edit_facets.erb @@ -0,0 +1,41 @@ +<%= content_for :page_title, "Edit #{current_format.title} finder" %> + +<% content_for :back_link, render_back_link(href: "/admin/#{current_format.admin_slug}") %> + +

Editing <%= current_format.title %> finder

+ +
+
+

The intention is for this to start off as "pseudo self serve". Departments would be able to make edits to this form, generate the JSON output, and then send the JSON to a developer to apply (the onus would be on the dev to 'git diff' the changes to make sure they make sense). In time, we may then swap out aspects of this for 'true' self-serve. It could also form the basis of a form be used for 'new' specialist finders, as opposed to just editing existing ones.

+ + <%= form_tag do %> +

Facets

+ <% current_format.finder_schema.schema["facets"].each_with_index do |facet, i| %> +

<%= facet["name"] %>

+ <% %w[key name short_name type preposition display_as_result_metadata filterable].each do |property| %> + <%= render "govuk_publishing_components/components/input", { + label: { + text: property, + }, + name: "facets[#{i}][#{property}]", + value: facet[property].to_s, + } # could add ` if facet[property]` to hide the empty inputs, but then there'd be no way of adding them + %> + <% end %> + <%= render "govuk_publishing_components/components/textarea", { + label: { + text: "'allowed_values' (JSON hashes, each on a separate line)", + }, + name: "facets[#{i}][allowed_values]", + rows: facet["allowed_values"] ? 10 : 3, # only show a large textarea if there is an existing value + value: facet["allowed_values"]&.join("\n"), + } %> +
+ <% end %> + + <%= render "govuk_publishing_components/components/button", { + text: "Generate schema" + } %> + <% end %> +
+
diff --git a/app/views/admin/edit_metadata.erb b/app/views/admin/edit_metadata.erb new file mode 100644 index 000000000..45392c53c --- /dev/null +++ b/app/views/admin/edit_metadata.erb @@ -0,0 +1,62 @@ +<%= content_for :page_title, "Edit #{current_format.title} finder" %> + +<% content_for :back_link, render_back_link(href: "/admin/#{current_format.admin_slug}") %> + +

Editing <%= current_format.title %> finder

+ +
+
+ <%= form_tag do %> +

Finder metadata

+

How the finder displays to users

+ <% %w[name description].each do |input| %> + <%= render "govuk_publishing_components/components/input", { + label: { + text: input, + }, + name: input, + value: current_format.finder_schema.schema[input], + } %> + <% end %> + + <% # special case for arrays %> + <% %w[organisations editing_organisations related].each do |input| %> + <%= render "govuk_publishing_components/components/textarea", { + label: { + text: "#{input} (content IDs, each on a separate line)", + }, + name: input, + rows: 3, + value: current_format.finder_schema.schema[input]&.join(","), + } %> + <% end %> + + <% # special case for 'nested' structure `filter.format` %> + <%= render "govuk_publishing_components/components/input", { + label: { + text: "filter.format", + }, + name: "filter.format", + value: current_format.finder_schema.schema["filter"]["format"], + readonly: true, + } %> + +

Publisher configuration

+

How the finder and its documents are referred to in Specialist Publisher.

+ + <% %w[format_name document_noun document_title].each do |input| %> + <%= render "govuk_publishing_components/components/input", { + label: { + text: input, + }, + name: input, + value: current_format.finder_schema.schema[input], + } %> + <% end %> + + <%= render "govuk_publishing_components/components/button", { + text: "Generate schema" + } %> + <% end %> +
+
diff --git a/app/views/admin/index_of_admin_forms.erb b/app/views/admin/index_of_admin_forms.erb new file mode 100644 index 000000000..94b192baf --- /dev/null +++ b/app/views/admin/index_of_admin_forms.erb @@ -0,0 +1,14 @@ +<%= content_for :page_title, "Edit #{current_format.title} finder" %> + +<% content_for :back_link, render_back_link(href: documents_path(current_format.admin_slug)) %> + +

Editing <%= current_format.title %> finder

+ +
+
+ +
+
diff --git a/app/views/admin/temporary_output.erb b/app/views/admin/temporary_output.erb new file mode 100644 index 000000000..86c9f91dd --- /dev/null +++ b/app/views/admin/temporary_output.erb @@ -0,0 +1,30 @@ +<%= content_for :page_title, "Edit #{current_format.title} finder" %> + +<% content_for :back_link, render_back_link(href: "/admin/#{current_format.admin_slug}") %> + +

Submit request for changes to <%= current_format.title %> finder

+ +
+
+

Please visit Changes to publishing applications or technical advice and copy and paste the "Proposed JSON" code for a developer to action. Ignore the other outputs below it, which are for debugging only.

+

Longer term, we will eliminate this step and automatically raise a Zendesk ticket on your behalf.

+ +

Proposed JSON

+
<%= JSON.pretty_generate(@proposed_schema) %>
+ +
+ +

Diff

+ <%= Diffy::Diff.new( + JSON.pretty_generate(@original_schema).to_s, + JSON.pretty_generate(@proposed_schema).to_s, + allow_empty_diff: false, + ).to_s(:html).html_safe %> + +

Submitted params

+
<%= JSON.pretty_generate(@submitted_params) %>
+ +

Original JSON

+
<%= JSON.pretty_generate(@original_schema) %>
+
+
diff --git a/app/views/layouts/design_system.html.erb b/app/views/layouts/design_system.html.erb new file mode 100644 index 000000000..63659caa8 --- /dev/null +++ b/app/views/layouts/design_system.html.erb @@ -0,0 +1,183 @@ +<% content_for :head do %> + <% if ENV["SENTRY_DSN"] && ENV["SENTRY_CURRENT_ENV"] %> + "> + "> + <% end %> + + <%= stylesheet_link_tag "diff", :media => "all" %> + + + + +<% end %> +<% render "layouts/google_tag_manager" %> + +<%= render 'govuk_publishing_components/components/layout_for_admin', + environment: GovukPublishingComponents::AppHelpers::Environment.current_acceptance_environment, + browser_title: yield(:browser_title).presence || yield(:title) do %> + + <%= render "govuk_publishing_components/components/skip_link" %> + + <% + navigation_items = [ + { text: "Switch app", href: Plek.external_url_for("signon") }, + { text: "Raise a support request", href: "https://support.publishing.service.gov.uk/technical_fault_report/new", show_only_in_collapsed_menu: true }, + # { text: "What’s new", href: publisher_updates_path, show_only_in_collapsed_menu: true }, + # { text: "Request training", href: request_training_path, show_only_in_collapsed_menu: true }, + ] + + if current_user + navigation_items += [ + { text: current_user.name, href: Plek.external_url_for("signon") }, + { text: "Log out", href: gds_sign_out_path }, + ] + end + %> + <%= render "govuk_publishing_components/components/layout_header", { + environment: GovukPublishingComponents::AppHelpers::Environment.current_acceptance_environment, + navigation_items: navigation_items, + } %> + +
+ <%# render "govuk_publishing_components/components/phase_banner", { + app_name: "Content Publisher", + phase: "beta", + message: capture do + render "layouts/phase_banner_elements" + end, + } %> + + <%= yield(:back_link) %> + +
" id="main-content" role="main"> + <% if flash["notice"] %> + <%= render "govuk_publishing_components/components/success_alert", { + message: flash["notice"] + } %> + <% end %> + + <% if flash["alert_with_description"] %> + <% alert = flash["alert_with_description"].stringify_keys %> + <%= render "govuk_publishing_components/components/error_alert", { + message: alert.fetch("title"), + data_attributes: { + gtm: "alert-with-description", + "gtm-value" => alert.fetch("title"), + "gtm-visibility-tracking" => true + }, + description: render_govspeak(alert.fetch("description_govspeak")) + } %> + <% end %> + + <% if flash["requirements"] %> + <% items = flash["requirements"]["message"] ? + [{ text: flash["requirements"]["message"] }] : + flash["requirements"]["items"].map(&:symbolize_keys) %> + + <%= render "govuk_publishing_components/components/error_summary", { + title: t("documents.flashes.requirements"), + items: track_requirements(items), + data_attributes: { + gtm: "alert-requirements", + "gtm-visibility-tracking" => true + }, + } %> + <% end %> + + <% if yield(:title).present? %> +
+
+ <%= yield(:context) %> +

<%= yield(:title) %>

+
+
+ <%= yield(:title_side) %> +
+
+ <% end %> + <%= yield %> +
+
+ + <%= render "govuk_publishing_components/components/layout_footer", { + navigation: [ + { + title: t("application.footer.support_and_feedback"), + items: [ + { + href: "https://support.publishing.service.gov.uk/technical_fault_report/new", + text: "Raise a support request", + attributes: { target: "_blank", "data-gtm": "footer-raise-support-request" } + }, + { + href: "https://status.publishing.service.gov.uk", + text: "GOV.UK status", + attributes: { "data-gtm": "footer-view-govuk-status" } + }, + { + href: "https://www.gov.uk/government/content-publishing", + text: "How to write, publish, and improve content", + attributes: { "data-gtm": "footer-content-publishing-guidance" } + }, + # { + # href: "#{Plek.external_url_for('content-publisher')}#{guidance_path}", + # text: "What to publish on GOV.UK", + # attributes: { "data-gtm": "footer-what-to-publish"} + # } + ] + }, + { + title: t("application.footer.documentation"), + items: [ + # { + # href: "#{Plek.external_url_for('content-publisher')}#{how_to_use_publisher_path}", + # text: "How to use Content Publisher", + # attributes: { "data-gtm": "footer-how-to-use-app" } + # }, + # { + # href: "#{Plek.external_url_for('content-publisher')}#{publisher_updates_path}", + # text: "What’s new in Content Publisher", + # attributes: { "data-gtm": "footer-view-whats-new" } + # }, + # { + # href: "#{Plek.external_url_for('content-publisher')}#{managing_editors_path}", + # text: "What Managing Editors can do", + # attributes: { "data-gtm": "footer-what-managing-editors-can-do"} + # }, + # { + # href: "#{Plek.external_url_for('content-publisher')}#{beta_capabilities_path}", + # text: "What the Beta can and cannot do", + # attributes: { "data-gtm": "footer-beta-capabilities" } + # }, + # { + # href: "#{Plek.external_url_for('content-publisher')}#{request_training_path}", + # text: "Request Content Publisher training", + # attributes: { "data-gtm": "footer-beta-capabilities" } + # }, + ] + }, + ] + } %> + + <%= render "govuk_publishing_components/components/modal_dialogue", { id: "modal", wide: true } do %> + <%# render "components/multi_section_viewer", { + sections: [ + { + id: "loading", + content: render("components/loading_spinner") + }, + { + id: "error", + content: render("layouts/modal_error") + } + ] + } %> + <% end %> +<% end %> diff --git a/config/routes.rb b/config/routes.rb index ebd100fa6..95089f58c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -13,6 +13,12 @@ resources :document_list_export_request, path: "/export/:document_type_slug", param: :export_id, only: [:show] + get "/admin/:document_type_slug", to: "admin#index_of_admin_forms" + get "/admin/metadata/:document_type_slug", to: "admin#edit_metadata" + post "/admin/metadata/:document_type_slug", to: "admin#save_metadata" + get "/admin/facets/:document_type_slug", to: "admin#edit_facets" + post "/admin/facets/:document_type_slug", to: "admin#save_facets" + resources :documents, path: "/:document_type_slug", param: :content_id_and_locale, except: :destroy do collection do resource :export, only: %i[show create], as: :export_documents