diff --git a/app/controllers/documents_controller.rb b/app/controllers/documents_controller.rb index 2e81dd2b86..2480e8407c 100644 --- a/app/controllers/documents_controller.rb +++ b/app/controllers/documents_controller.rb @@ -43,6 +43,25 @@ def destroy end end + def upload + @document = Document.new(creator_id: current_user.id, document: params[:documents]) + + if @document.save_and_verify + render json: { file: { originalname: @document.document.filename, filename: @document.id }, success: { messageHtml: "#{@document.document.filename} uploaded"} }, status: :created + else + render json: { error: { message: "Upload of #{@document.document.filename} failed" } }, status: :unprocessable_entity + end + end + + def delete + # TODO: This does not do any checking to see if the document is owned by the current user + @document = Document.find(params[:delete]) + + @document.destroy + + render json: { file: { filename: params[:delete] } } + end + private def document diff --git a/app/models/ability.rb b/app/models/ability.rb index 970d661b36..d81cb16a1e 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -86,7 +86,7 @@ def can_administer_any_claim_in_provider(persona) end def can_administer_documents_in_provider(persona) - can %i[index create], Document + can %i[index create upload delete], Document # NOTE: for destroy action, at least, the document may not be persisted/saved can %i[show download destroy], Document do |document| @@ -127,7 +127,7 @@ def can_manage_own_claims_of_class(persona, claim_klass) end def can_manage_own_documents(persona) - can %i[index create], Document + can %i[index create upload delete], Document can %i[show download destroy], Document do |document| if document.external_user_id.nil? diff --git a/app/views/shared/_message_controls.html.haml b/app/views/shared/_message_controls.html.haml index 41d94dcb06..2f4371a711 100644 --- a/app/views/shared/_message_controls.html.haml +++ b/app/views/shared/_message_controls.html.haml @@ -28,12 +28,20 @@ link_errors: true, label: { text: t('.written_reasons') } - = f.govuk_file_field :attachment, - label: { text: t('.attachment_label') }, - hint: { text: t('.accepted_files_help_text') } + .moj-multi-file-upload + .moj-multi-file__uploaded-files + %h2.govuk-headings-m Files added + .govuk-summary-list.moj-multi-file-upload__list + .moj-multi-file__uploaded-fields + .moj-multi-file-upload__upload + .govuk-form-group + %label.govuk-label.govuk-label--m{for: "attachments"} + Upload a file + %input#attachments.govuk-file-upload.moj-multi-file-upload__input{multiple: "multiple", name: "attachments", type: "file"}/ - .file-to-be-uploaded.govuk-form-group - %span.filename - = govuk_link_to t('.remove_file_html'), '#' + %button.govuk-button.govuk-button--secondary.moj-multi-file-upload__button{"data-module" => "govuk-button", type: "submit"} + Upload file + %button.govuk-button{"data-module" => "govuk-button", type: "submit"} + Continue = f.govuk_submit t('.send'), class: 'govuk-button--secondary' diff --git a/app/webpack/javascripts/modules/Modules.MultiFileUpload.js b/app/webpack/javascripts/modules/Modules.MultiFileUpload.js new file mode 100644 index 0000000000..09eb367592 --- /dev/null +++ b/app/webpack/javascripts/modules/Modules.MultiFileUpload.js @@ -0,0 +1,28 @@ +moj.Modules.MultiFileUpload = { + init: function () { + let container = document.querySelector('.moj-multi-file-upload'); + + new MOJFrontend.MultiFileUpload({ + container: container, + uploadUrl: '/documents/upload', + deleteUrl: '/documents/delete', + uploadFileExitHook: function (uploader, file, response) { + let fields = container.querySelector('.moj-multi-file__uploaded-fields'); + console.log('fields'); + console.log(fields); + let input = document.createElement('input'); + input.type = 'hidden'; + input.name = 'message[document_ids][]'; + input.value = response.file.filename; + fields.appendChild(input); + }, + fileDeleteHook: function (uploader, response) { + let fields = container.querySelector('.moj-multi-file__uploaded-fields'); + let input = fields.querySelector('input[value="' + response.file.filename + '"]'); + console.log('remove input'); + console.log(input); + input.parentNode.removeChild(input); + } + }); + } +} diff --git a/app/webpack/packs/application.js b/app/webpack/packs/application.js index e7affb9383..6a958698f8 100644 --- a/app/webpack/packs/application.js +++ b/app/webpack/packs/application.js @@ -72,6 +72,7 @@ import '../javascripts/modules/Helpers.Autocomplete.js' import '../javascripts/modules/Modules.TableRowClick.js' import '../javascripts/modules/Modules.AllocationScheme.js' import '../javascripts/modules/Modules.AddEditAdvocate.js' +import '../javascripts/modules/Modules.MultiFileUpload.js' import '../javascripts/plugins/jquery.numbered.elements.js' diff --git a/app/webpack/stylesheets/application.scss b/app/webpack/stylesheets/application.scss index 079fa3486a..ed9cf4b0f4 100644 --- a/app/webpack/stylesheets/application.scss +++ b/app/webpack/stylesheets/application.scss @@ -8,6 +8,8 @@ $govuk-fonts-path: "~govuk-frontend/dist/govuk/assets/fonts/"; @import "~govuk-frontend/dist/govuk/base"; @import "~govuk-frontend/dist/govuk/core/typography"; +@import "@ministryofjustice/frontend/moj/components/multi-file-upload/_multi-file-upload.scss"; + // Project specific // Everything below this comment should be project specific. // If you absolutely need to override gov.uk styles, don't, diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb index 0325c6c7cf..4a57cddc70 100644 --- a/config/initializers/assets.rb +++ b/config/initializers/assets.rb @@ -14,3 +14,4 @@ Rails.application.config.assets.paths << Rails.root.join('node_modules', 'govuk-frontend', 'dist', 'govuk', 'assets') Rails.application.config.assets.paths << Rails.root.join('node_modules', 'govuk-frontend', 'dist', 'govuk', 'assets', 'images') Rails.application.config.assets.paths << Rails.root.join('app', 'webpack', 'packs') +Rails.application.config.assets.paths << Rails.root.join('node_modules', '@ministryofjustice', 'frontend', 'moj', 'assets') diff --git a/config/routes.rb b/config/routes.rb index 55b72967be..f2a060b8b6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -60,6 +60,8 @@ resources :documents do get 'download', on: :member + post 'upload', on: :collection + post 'delete', on: :collection end resources :messages, only: [:create] do diff --git a/config/webpack/webpack.config.js b/config/webpack/webpack.config.js index 906966ed92..4b4ddcd1f4 100644 --- a/config/webpack/webpack.config.js +++ b/config/webpack/webpack.config.js @@ -95,7 +95,8 @@ module.exports = { jQuery: require.resolve('jquery'), jquery: require.resolve('jquery'), Dropzone: require.resolve('dropzone/dist/dropzone.js'), - Stickyfill: require.resolve('stickyfilljs') + Stickyfill: require.resolve('stickyfilljs'), + MOJFrontend: require.resolve('@ministryofjustice/frontend/moj/all.js') }) ] } diff --git a/package.json b/package.json index 369763581a..9d59e35bb2 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@babel/plugin-proposal-private-methods": "^7.18.6", "@babel/plugin-transform-runtime": "^7.25.9", "@babel/preset-env": "^7.26.0", + "@ministryofjustice/frontend": "^2.2.4", "accessible-autocomplete": "^3.0.1", "babel-loader": "^9.2.1", "babel-plugin-macros": "^3.1.0", diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb index 11fe4965ec..83aeaf43f4 100644 --- a/spec/models/ability_spec.rb +++ b/spec/models/ability_spec.rb @@ -87,8 +87,8 @@ end end - context 'can index and create documents' do - %i[index create].each do |action| + context 'can index, create and upload documents' do + %i[index create upload].each do |action| it { should be_able_to(action, Document.new(external_user:)) } end end @@ -199,8 +199,8 @@ end end - context 'can index and create documents' do - %i[index create].each do |action| + context 'can index, create and upload documents' do + %i[index create upload].each do |action| it { should be_able_to(action, Document.new(external_user:)) } end end diff --git a/spec/requests/documents_spec.rb b/spec/requests/documents_spec.rb index 8bd530f425..6d25f8a346 100644 --- a/spec/requests/documents_spec.rb +++ b/spec/requests/documents_spec.rb @@ -178,4 +178,77 @@ it { expect { delete_document }.not_to change(ActiveStorage::Blob, :count) } end end + + describe 'POST /documents/upload' do + subject(:create_document) { post upload_documents_path, params: } + + let(:params) do + { + documents: Rack::Test::UploadedFile.new(Rails.root + 'features/examples/longer_lorem.pdf', 'application/pdf') + } + end + + context 'when the document is valid' do + it 'creates a document' do + expect { create_document }.to change(Document, :count).by(1) + end + + it 'returns status created' do + create_document + expect(response).to have_http_status(:created) + end + + it 'returns the id of the created document' do + create_document + # The MultiFileUpload uses the filename as the only paramater to identify the document for the delte button + # For this reason the 'filename' in the response is the document id + expect(response.parsed_body['file']['filename']).to eq Document.last.id + end + + it 'returns a success message' do + create_document + expect(response.parsed_body['success']).to have_key('messageHtml') + end + end + + context 'when the document is invalid' do + let(:params) do + { + documents: Rack::Test::UploadedFile.new(Rails.root + 'features/examples/longer_lorem.html', 'text/html') + } + end + + it 'does not create a document' do + expect { create_document }.not_to change(Document, :count) + end + + it 'returns status unprocessable entity' do + create_document + expect(response).to have_http_status(:unprocessable_entity) + end + + it 'returns errors in response' do + create_document + expect(response.parsed_body['error']).to have_key('message') + end + end + + context 'when the document is missing' do + let(:params) { { documents: nil } } + + it 'does not create a document' do + expect { create_document }.not_to change(Document, :count) + end + + it 'returns status unprocessable entity' do + create_document + expect(response).to have_http_status(:unprocessable_entity) + end + + it 'returns errors in response' do + create_document + expect(response.parsed_body['error']).to have_key('message') + end + end + end end diff --git a/yarn.lock b/yarn.lock index e96f18364a..8728a0fe6f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -984,6 +984,14 @@ resolved "https://registry.yarnpkg.com/@kurkle/color/-/color-0.3.4.tgz#4d4ff677e1609214fc71c580125ddddd86abcabf" integrity sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w== +"@ministryofjustice/frontend@^2.2.4": + version "2.2.5" + resolved "https://registry.yarnpkg.com/@ministryofjustice/frontend/-/frontend-2.2.5.tgz#e4371759f915db8d0677c03daee72c5cb6ddbd8e" + integrity sha512-UR8WTME0vrYhtCxBESd0XaAvfjdi9UujieeooQj1ddQKdcfI/wkXUMJO8hWWvXjeqRxfKB2v62rw1mSRc+Yqrw== + dependencies: + govuk-frontend "^5.0.0" + moment "^2.27.0" + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -2887,6 +2895,11 @@ gopd@^1.0.1, gopd@^1.1.0: resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== +govuk-frontend@^5.0.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/govuk-frontend/-/govuk-frontend-5.7.1.tgz#d4c561ebf8c0b76130f31df8c2e4d70d340cd63f" + integrity sha512-jF1cq5rn57kxZmJRprUZhTQ31zaBBK4b5AyeJaPX3Yhg22lk90Mx/dQLvOk/ycV3wM7e0y+s4IPvb2fFaPlCGg== + govuk-frontend@^5.6.0: version "5.6.0" resolved "https://registry.yarnpkg.com/govuk-frontend/-/govuk-frontend-5.6.0.tgz#8c0975f0d825ec7192bcfe64e3e97ef3dfa7dea1" @@ -3753,6 +3766,11 @@ minimist@^1.2.0, minimist@^1.2.6: resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== +moment@^2.27.0: + version "2.30.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae" + integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how== + ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"