diff --git a/app/change_sets/change_set_workflow.rb b/app/change_sets/change_set_workflow.rb index 54a09ade94..861272d2f2 100644 --- a/app/change_sets/change_set_workflow.rb +++ b/app/change_sets/change_set_workflow.rb @@ -32,11 +32,11 @@ def state_changed? end def new_state - Array.wrap(state).first + @new_state ||= Array.wrap(state).first end def old_state - Array.wrap(model.state).first + @old_state ||= Array.wrap(model.state).first end end end diff --git a/app/controllers/resources_controller.rb b/app/controllers/resources_controller.rb index 365010db82..25d8d39e1e 100644 --- a/app/controllers/resources_controller.rb +++ b/app/controllers/resources_controller.rb @@ -108,6 +108,7 @@ def update obj = nil change_set_persister.buffer_into_index do |persist| obj = persist.save(change_set: @change_set) + flash[:confetti] = true if @change_set.try(:state_changed?) && @change_set.new_state == "complete" end after_update_success(obj, @change_set) else diff --git a/app/javascript/figgy/figgy_boot.js b/app/javascript/figgy/figgy_boot.js index 804df06014..340d645f0e 100644 --- a/app/javascript/figgy/figgy_boot.js +++ b/app/javascript/figgy/figgy_boot.js @@ -13,6 +13,8 @@ import ParentResourcesTables from '@figgy/relationships/parent_resources_table' import BulkLabeler from '@figgy/bulk_labeler/bulk_label' import BoundingBoxSelector from '@figgy/bounding_box_selector' import FieldManager from '@figgy/field_manager' +import Confetti from 'canvas-confetti' + export default class Initializer { constructor() { this.initialize_form() @@ -37,6 +39,7 @@ export default class Initializer { $("select:not(.select2)").selectpicker({'liveSearch': true}) this.initialize_datatables() + this.do_confetti() } initialize_timepicker() { @@ -206,4 +209,29 @@ export default class Initializer { } }); } + + do_confetti() { + if ($('*[data-confetti-trigger]').length > 0) { + var duration = 5 * 1000; // 5 seconds + var animationEnd = Date.now() + duration; + var defaults = { startVelocity: 30, spread: 360, ticks: 60, zIndex: 0, disableForReducedMotion: true }; + + function randomInRange(min, max) { + return Math.random() * (max - min) + min; + } + + var interval = setInterval(function() { + var timeLeft = animationEnd - Date.now(); + + if (timeLeft <= 0) { + return clearInterval(interval); + } + + var particleCount = 50 * (timeLeft / duration); + // since particles fall down, start a bit higher than random + Confetti(Object.assign({}, defaults, { particleCount, origin: { x: randomInRange(0.1, 0.3), y: Math.random() - 0.2 } })); + Confetti(Object.assign({}, defaults, { particleCount, origin: { x: randomInRange(0.7, 0.9), y: Math.random() - 0.2 } })); + }, 250); + } + } } diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 2c88bc8d0b..2d045c2ba6 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -3,6 +3,7 @@ <%= render "shared/header" %> <%= render "shared/flash_message" %> + <%= render "shared/confetti" %> <% end %> <% content_for(:content) do %> <% if content_for? :sidebar %> diff --git a/app/views/shared/_confetti.html.erb b/app/views/shared/_confetti.html.erb new file mode 100644 index 0000000000..d7a0ce71da --- /dev/null +++ b/app/views/shared/_confetti.html.erb @@ -0,0 +1,3 @@ +<% if flash[:confetti].present? %> + +<% end %> diff --git a/package.json b/package.json index a61707291e..abc4075ddc 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "apollo-boost": "^0.1.10", "axios": "^0.21.2", "babel-plugin-macros": "^3.1.0", + "canvas-confetti": "^1.6.0", "core-js": "^3.16.2", "flush-promises": "^1.0.2", "graphql": "^16.5.0", diff --git a/spec/features/scanned_resource_spec.rb b/spec/features/scanned_resource_spec.rb index be80924dbe..9beec2daa6 100644 --- a/spec/features/scanned_resource_spec.rb +++ b/spec/features/scanned_resource_spec.rb @@ -45,4 +45,17 @@ visit solr_document_path(id: resource.id) expect(page).to have_selector("#health-status") end + + scenario "show page can display confetti" do + stub_ezid + file = fixture_file_upload("files/example.tif", "image/tiff") + resource = FactoryBot.create_for_repository(:pending_scanned_resource, files: [file]) + + visit solr_document_path(id: resource.id) + + choose("Complete") + click_button("Submit") + + expect(page).to have_selector("*[data-confetti-trigger]") + end end diff --git a/spec/support/shared/workflow_specs.rb b/spec/support/shared/workflow_specs.rb index d8d24081c4..974f3b388a 100644 --- a/spec/support/shared/workflow_specs.rb +++ b/spec/support/shared/workflow_specs.rb @@ -8,6 +8,16 @@ expect(reloaded.workflow_note.first.author).to eq ["Shakespeare"] expect(reloaded.workflow_note.first.note).to eq ["Test"] end + + it "adds a confetti-triggering flash when resource is completed" do + stub_ezid + resource = FactoryBot.create_for_repository(factory_resource) + skip("No Complete state to confetti for #{resource.class}") unless ChangeSet.for(resource).workflow.valid_states.include?("complete") + + patch :update, params: { id: resource.id.to_s, resource.model_name.singular.to_sym => { state: "complete" } } + + expect(flash[:confetti]).to eq true + end it "doesn't create a workflow note with an empty note" do resource = FactoryBot.create_for_repository(factory_resource) diff --git a/yarn.lock b/yarn.lock index deb250e6c6..9f829cea7f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2778,6 +2778,11 @@ caniuse-lite@^1.0.30001370, caniuse-lite@^1.0.30001449: resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001488.tgz" integrity sha512-NORIQuuL4xGpIy6iCCQGN4iFjlBXtfKWIenlUuyZJumLRIindLb7wXM+GO8erEhb7vXfcnf4BAg2PrSDN5TNLQ== +canvas-confetti@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/canvas-confetti/-/canvas-confetti-1.6.0.tgz#193f71aa8f38fc850a5ba94f59091a7afdb43ead" + integrity sha512-ej+w/m8Jzpv9Z7W7uJZer14Ke8P2ogsjg4ZMGIuq4iqUOqY2Jq8BNW42iGmNfRwREaaEfFIczLuZZiEVSYNHAA== + chai@^4.3.7: version "4.3.7" resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.7.tgz#ec63f6df01829088e8bf55fca839bcd464a8ec51"