From 8bcd508b4c7f559f27db3c05b6ae4a3a33dfff95 Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Fri, 21 Dec 2018 01:19:17 +0100 Subject: [PATCH 01/42] Add lock version to issuable helpers --- app/controllers/concerns/issuable_actions.rb | 3 ++- app/helpers/issuables_helper.rb | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb index 3d64ae8b775..8ef3b6502df 100644 --- a/app/controllers/concerns/issuable_actions.rb +++ b/app/controllers/concerns/issuable_actions.rb @@ -58,7 +58,8 @@ module IssuableActions title_text: issuable.title, description: view_context.markdown_field(issuable, :description), description_text: issuable.description, - task_status: issuable.task_status + task_status: issuable.task_status, + lock_version: issuable.lock_version } if issuable.edited? diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index f8176facce9..0fee29bf7c7 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -269,6 +269,7 @@ module IssuablesHelper markdownPreviewPath: preview_markdown_path(parent), markdownDocsPath: help_page_path('user/markdown'), markdownVersion: issuable.cached_markdown_version, + lockVersion: issuable.lock_version, issuableTemplates: issuable_templates(issuable), initialTitleHtml: markdown_field(issuable, :title), initialTitleText: issuable.title, From 45eabf921a9bb90677d1e4f59544f0a7abcbc879 Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Fri, 21 Dec 2018 01:19:41 +0100 Subject: [PATCH 02/42] Accept lockVersion as a prop and add to store --- .../javascripts/issue_show/components/app.vue | 6 +++++ .../javascripts/issue_show/stores/index.js | 2 ++ spec/helpers/issuables_helper_spec.rb | 1 + .../issue_show/components/app_spec.js | 26 ++++++++++++++----- spec/javascripts/issue_show/mock_data.js | 2 ++ 5 files changed, 31 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue index cd569eb3045..b6eacf839b9 100644 --- a/app/assets/javascripts/issue_show/components/app.vue +++ b/app/assets/javascripts/issue_show/components/app.vue @@ -130,6 +130,10 @@ export default { required: false, default: true, }, + lockVersion: { + type: Number, + required: true, + }, }, data() { const store = new Store({ @@ -141,6 +145,7 @@ export default { updatedByName: this.updatedByName, updatedByPath: this.updatedByPath, taskStatus: this.initialTaskStatus, + lock_version: this.lockVersion, }); return { @@ -214,6 +219,7 @@ export default { this.store.setFormState({ title: this.state.titleText, description: this.state.descriptionText, + lock_version: this.state.lock_version, lockedWarningVisible: false, updateLoading: false, }); diff --git a/app/assets/javascripts/issue_show/stores/index.js b/app/assets/javascripts/issue_show/stores/index.js index 32044d6da25..2b3903def6b 100644 --- a/app/assets/javascripts/issue_show/stores/index.js +++ b/app/assets/javascripts/issue_show/stores/index.js @@ -6,6 +6,7 @@ export default class Store { description: '', lockedWarningVisible: false, updateLoading: false, + lock_version: 0, }; } @@ -22,6 +23,7 @@ export default class Store { this.state.updatedAt = data.updated_at; this.state.updatedByName = data.updated_by_name; this.state.updatedByPath = data.updated_by_path; + this.state.lock_version = data.lock_version; } stateShouldUpdate(data) { diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb index 03e3a72a82f..af319e5ebfe 100644 --- a/spec/helpers/issuables_helper_spec.rb +++ b/spec/helpers/issuables_helper_spec.rb @@ -190,6 +190,7 @@ describe IssuablesHelper do markdownDocsPath: '/help/user/markdown', markdownVersion: CacheMarkdownField::CACHE_COMMONMARK_VERSION, issuableTemplates: [], + lockVersion: issue.lock_version, projectPath: @project.path, projectNamespace: @project.namespace.path, initialTitleHtml: issue.title, diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js index 2bd1b3996dc..9b2b6b670c3 100644 --- a/spec/javascripts/issue_show/components/app_spec.js +++ b/spec/javascripts/issue_show/components/app_spec.js @@ -43,6 +43,7 @@ describe('Issuable output', () => { initialTitleText: '', initialDescriptionHtml: 'test', initialDescriptionText: 'test', + lockVersion: 1, markdownPreviewPath: '/', markdownDocsPath: '/', projectNamespace: '/', @@ -78,6 +79,7 @@ describe('Issuable output', () => { expect(formatText(editedText.innerText)).toMatch(/Edited[\s\S]+?by Some User/); expect(editedText.querySelector('.author-link').href).toMatch(/\/some_user$/); expect(editedText.querySelector('time')).toBeTruthy(); + expect(vm.state.lock_version).toEqual(1); }) .then(() => { vm.poll.makeRequest(); @@ -95,6 +97,7 @@ describe('Issuable output', () => { expect(editedText.querySelector('.author-link').href).toMatch(/\/other_user$/); expect(editedText.querySelector('time')).toBeTruthy(); + expect(vm.state.lock_version).toEqual(2); }) .then(done) .catch(done.fail); @@ -255,15 +258,10 @@ describe('Issuable output', () => { describe('error when updating', () => { beforeEach(() => { spyOn(window, 'Flash').and.callThrough(); - spyOn(vm.service, 'updateIssuable').and.callFake( - () => - new Promise((resolve, reject) => { - reject(); - }), - ); }); it('closes form on error', done => { + spyOn(vm.service, 'updateIssuable').and.callFake(() => Promise.resolve()); vm.updateIssuable(); setTimeout(() => { @@ -276,6 +274,7 @@ describe('Issuable output', () => { }); it('returns the correct error message for issuableType', done => { + spyOn(vm.service, 'updateIssuable').and.callFake(() => Promise.reject()); vm.issuableType = 'merge request'; Vue.nextTick(() => { @@ -290,6 +289,20 @@ describe('Issuable output', () => { }); }); }); + + it('shows error mesage from backend if exists', done => { + const msg = 'Custom error message from backend'; + spyOn(vm.service, 'updateIssuable').and.callFake(() => + Promise.reject({ response: { data: { errors: msg } } }), // eslint-disable-line prefer-promise-reject-errors + ); + + vm.updateIssuable(); + setTimeout(() => { + expect(window.Flash).toHaveBeenCalledWith(msg); + + done(); + }); + }); }); }); @@ -420,6 +433,7 @@ describe('Issuable output', () => { .then(vm.$nextTick) .then(() => { expect(vm.formState.lockedWarningVisible).toEqual(true); + expect(vm.formState.lock_version).toEqual(1); expect(vm.$el.querySelector('.alert')).not.toBeNull(); }) .then(done) diff --git a/spec/javascripts/issue_show/mock_data.js b/spec/javascripts/issue_show/mock_data.js index 74b3efb014b..f4475aadb8b 100644 --- a/spec/javascripts/issue_show/mock_data.js +++ b/spec/javascripts/issue_show/mock_data.js @@ -8,6 +8,7 @@ export default { updated_at: '2015-05-15T12:31:04.428Z', updated_by_name: 'Some User', updated_by_path: '/some_user', + lock_version: 1, }, secondRequest: { title: '

2

', @@ -18,5 +19,6 @@ export default { updated_at: '2016-05-15T12:31:04.428Z', updated_by_name: 'Other User', updated_by_path: '/other_user', + lock_version: 2, }, }; From f1acd5051513ee815578f7311ca88ee54e79e323 Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Mon, 7 Jan 2019 16:23:53 +0100 Subject: [PATCH 03/42] Show error message from backend --- app/assets/javascripts/issue_show/components/app.vue | 8 +++++++- spec/javascripts/issue_show/components/app_spec.js | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue index b6eacf839b9..d65b44ca6f3 100644 --- a/app/assets/javascripts/issue_show/components/app.vue +++ b/app/assets/javascripts/issue_show/components/app.vue @@ -250,8 +250,14 @@ export default { if (error && error.name === 'SpamError') { this.openRecaptcha(); } else { + let errMsg = `Error updating ${this.issuableType}`; + + if (error && error.response && error.response.data && error.response.data.errors) { + errMsg = error.response.data.errors; + } + eventHub.$emit('close.form'); - window.Flash(`Error updating ${this.issuableType}`); + window.Flash(errMsg); } }); }, diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js index 9b2b6b670c3..863d15570a0 100644 --- a/spec/javascripts/issue_show/components/app_spec.js +++ b/spec/javascripts/issue_show/components/app_spec.js @@ -292,8 +292,8 @@ describe('Issuable output', () => { it('shows error mesage from backend if exists', done => { const msg = 'Custom error message from backend'; - spyOn(vm.service, 'updateIssuable').and.callFake(() => - Promise.reject({ response: { data: { errors: msg } } }), // eslint-disable-line prefer-promise-reject-errors + spyOn(vm.service, 'updateIssuable').and.callFake( + () => Promise.reject({ response: { data: { errors: msg } } }), // eslint-disable-line prefer-promise-reject-errors ); vm.updateIssuable(); From 83306d249e0762d21b9ca128b9ebb57a0bef6f8b Mon Sep 17 00:00:00 2001 From: Brett Walker Date: Thu, 10 Jan 2019 15:22:28 -0600 Subject: [PATCH 04/42] Pass tasklist lock version receive data on when there is a conflict --- app/assets/javascripts/issue_show/components/app.vue | 1 + .../javascripts/issue_show/components/description.vue | 10 ++++++++++ app/assets/javascripts/task_list.js | 4 +++- app/controllers/concerns/issuable_actions.rb | 5 ++++- 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue index d65b44ca6f3..3e71c40e896 100644 --- a/app/assets/javascripts/issue_show/components/app.vue +++ b/app/assets/javascripts/issue_show/components/app.vue @@ -326,6 +326,7 @@ export default { :task-status="state.taskStatus" :issuable-type="issuableType" :update-url="updateEndpoint" + :lock-version="state.lock_version" /> {}); - this.onError = function showFlash(e) { + this.onError = options.onError || function showFlash(e) { let errorMessages = ''; if (e.response.data && typeof e.response.data === 'object') { @@ -43,6 +44,7 @@ export default class TaskList { const patchData = {}; patchData[this.dataType] = { [this.fieldName]: $target.val(), + ['lock_version']: this.lockVersion, }; return axios diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb index 8ef3b6502df..f0b4198b2d9 100644 --- a/app/controllers/concerns/issuable_actions.rb +++ b/app/controllers/concerns/issuable_actions.rb @@ -162,10 +162,13 @@ module IssuableActions end format.json do + # We want to pass back the latest valid data, so reload the model + @issuable.reload render json: { errors: [ "Someone edited this #{issuable.human_class_name} at the same time you did. Please refresh your browser and make sure your changes will not unintentionally remove theirs." - ] + ], + data: serializer.represent(@issuable) }, status: :conflict end end From 1362267d45dee0a6a4311c59e72b90e3ea032da6 Mon Sep 17 00:00:00 2001 From: Fatih Acet Date: Fri, 11 Jan 2019 01:07:09 +0100 Subject: [PATCH 05/42] Fix data coming down to error handler of tasklist --- app/assets/javascripts/issue_show/components/description.vue | 3 ++- app/assets/javascripts/task_list.js | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/issue_show/components/description.vue b/app/assets/javascripts/issue_show/components/description.vue index 4443d6c6760..d5b355a39a2 100644 --- a/app/assets/javascripts/issue_show/components/description.vue +++ b/app/assets/javascripts/issue_show/components/description.vue @@ -1,5 +1,6 @@