From e1d843357620920d5627fc3e5594ce586847b875 Mon Sep 17 00:00:00 2001 From: Guilherme-NL Date: Tue, 7 Nov 2023 15:41:22 -0300 Subject: [PATCH 1/9] refactor: use position to sort stories --- app/assets/javascripts/models/beta/story.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/models/beta/story.js b/app/assets/javascripts/models/beta/story.js index 7d25d55a5..baba19ae8 100644 --- a/app/assets/javascripts/models/beta/story.js +++ b/app/assets/javascripts/models/beta/story.js @@ -24,8 +24,8 @@ export const needConfirmation = story => story.state === status.RELEASE; export const comparePosition = (a, b) => { - const positionA = a.newPosition; - const positionB = b.newPosition; + const positionA = Number(a.position); + const positionB = Number(b.position); return compareValues(positionA, positionB); }; From a18f0678c84d2b88d6f9b3b100842b24a5eb5e67 Mon Sep 17 00:00:00 2001 From: Guilherme-NL Date: Tue, 7 Nov 2023 15:43:41 -0300 Subject: [PATCH 2/9] feat: add PusherNotification when SortStories --- app/services/beta/sort_stories.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/services/beta/sort_stories.rb b/app/services/beta/sort_stories.rb index b4b0170a1..3db1ce64b 100644 --- a/app/services/beta/sort_stories.rb +++ b/app/services/beta/sort_stories.rb @@ -9,6 +9,7 @@ def initialize(story, sort_params) def call update_current_story update_stories_positions + notify_story_update stories end @@ -18,6 +19,10 @@ def update_current_story @story.update(new_position: @new_position, state: @new_state, position: @position) end + def notify_story_update + StoryOperations::PusherNotification.notify_changes(@story) + end + def stories_to_update stories.where.not(id: @story.id) end From 116804455dedc3174e3a71b93e12b73de52c2368 Mon Sep 17 00:00:00 2001 From: Guilherme-NL Date: Tue, 7 Nov 2023 16:10:02 -0300 Subject: [PATCH 3/9] update test to use 'position' instead of 'newPosition' --- spec/javascripts/models/beta/story_spec.js | 598 ++++++++++----------- spec/javascripts/selectors/backlog_spec.js | 20 +- 2 files changed, 309 insertions(+), 309 deletions(-) diff --git a/spec/javascripts/models/beta/story_spec.js b/spec/javascripts/models/beta/story_spec.js index be161cf82..bd77e28af 100644 --- a/spec/javascripts/models/beta/story_spec.js +++ b/spec/javascripts/models/beta/story_spec.js @@ -1,52 +1,52 @@ -import * as Story from "models/beta/story"; -import moment from "moment"; -import { status, storyTypes, storyScopes } from "libs/beta/constants"; +import * as Story from 'models/beta/story'; +import moment from 'moment'; +import { status, storyTypes, storyScopes } from 'libs/beta/constants'; import { createTemporaryId, denormalizeState, denormalizeStories, normalizeState, normalizeStories, -} from "../../../../app/assets/javascripts/models/beta/story"; +} from '../../../../app/assets/javascripts/models/beta/story'; -describe("Story model", function () { - describe("comparePosition", () => { - describe("when position A is bigger then B", () => { - it("return 1", () => { +describe('Story model', function () { + describe('comparePosition', () => { + describe('when position A is bigger then B', () => { + it('return 1', () => { const storyA = { - newPosition: 10, + position: 10, }; const storyB = { - newPosition: 4, + position: 4, }; expect(Story.comparePosition(storyA, storyB)).toEqual(1); }); }); - describe("when position A is lower then B", () => { - it("return -1", () => { + describe('when position A is lower then B', () => { + it('return -1', () => { const storyA = { - newPosition: 3, + position: 3, }; const storyB = { - newPosition: 5, + position: 5, }; expect(Story.comparePosition(storyA, storyB)).toEqual(-1); }); }); - describe("when position A is equal B", () => { - it("return 0", () => { + describe('when position A is equal B', () => { + it('return 0', () => { const storyA = { - newPosition: 5, + position: 5, }; const storyB = { - newPosition: 5, + position: 5, }; expect(Story.comparePosition(storyA, storyB)).toEqual(0); @@ -54,43 +54,43 @@ describe("Story model", function () { }); }); - describe("compareAcceptedAt", () => { - describe("when date A is more recent then B", () => { - it("return 1", () => { + describe('compareAcceptedAt', () => { + describe('when date A is more recent then B', () => { + it('return 1', () => { const storyA = { - acceptedAt: "2018-08-07T16:36:20.811Z", + acceptedAt: '2018-08-07T16:36:20.811Z', }; const storyB = { - acceptedAt: "2018-08-06T16:36:20.811Z", + acceptedAt: '2018-08-06T16:36:20.811Z', }; expect(Story.compareAcceptedAt(storyA, storyB)).toEqual(1); }); }); - describe("when date A is older then B", () => { - it("return -1", () => { + describe('when date A is older then B', () => { + it('return -1', () => { const storyA = { - acceptedAt: "2018-08-07T12:36:20.811Z", + acceptedAt: '2018-08-07T12:36:20.811Z', }; const storyB = { - acceptedAt: "2018-08-07T16:36:20.811Z", + acceptedAt: '2018-08-07T16:36:20.811Z', }; expect(Story.compareAcceptedAt(storyA, storyB)).toEqual(-1); }); }); - describe("when date A is equal B", () => { - it("return 0", () => { + describe('when date A is equal B', () => { + it('return 0', () => { const storyA = { - acceptedAt: "2018-08-07T12:36:20.811Z", + acceptedAt: '2018-08-07T12:36:20.811Z', }; const storyB = { - acceptedAt: "2018-08-07T12:36:20.811Z", + acceptedAt: '2018-08-07T12:36:20.811Z', }; expect(Story.compareAcceptedAt(storyA, storyB)).toEqual(0); @@ -98,43 +98,43 @@ describe("Story model", function () { }); }); - describe("compareStarteddAt", () => { - describe("when date A is more recent then B", () => { - it("return 1", () => { + describe('compareStarteddAt', () => { + describe('when date A is more recent then B', () => { + it('return 1', () => { const storyA = { - startedAt: "2018-08-07T16:36:20.811Z", + startedAt: '2018-08-07T16:36:20.811Z', }; const storyB = { - startedAt: "2018-08-06T16:36:20.811Z", + startedAt: '2018-08-06T16:36:20.811Z', }; expect(Story.compareStartedAt(storyA, storyB)).toEqual(1); }); }); - describe("when date A is older then B", () => { - it("return -1", () => { + describe('when date A is older then B', () => { + it('return -1', () => { const storyA = { - startedAt: "2018-08-07T12:36:20.811Z", + startedAt: '2018-08-07T12:36:20.811Z', }; const storyB = { - startedAt: "2018-08-07T16:36:20.811Z", + startedAt: '2018-08-07T16:36:20.811Z', }; expect(Story.compareStartedAt(storyA, storyB)).toEqual(-1); }); }); - describe("when date A is equal B", () => { - it("return 0", () => { + describe('when date A is equal B', () => { + it('return 0', () => { const storyA = { - startedAt: "2018-08-07T12:36:20.811Z", + startedAt: '2018-08-07T12:36:20.811Z', }; const storyB = { - startedAt: "2018-08-07T12:36:20.811Z", + startedAt: '2018-08-07T12:36:20.811Z', }; expect(Story.compareStartedAt(storyA, storyB)).toEqual(0); @@ -142,65 +142,65 @@ describe("Story model", function () { }); }); - describe("compareDeliveredAt", () => { - describe("when date A is more recent then B", () => { - it("return 1", () => { + describe('compareDeliveredAt', () => { + describe('when date A is more recent then B', () => { + it('return 1', () => { const storyA = { - deliveredAt: "2018-08-07T16:36:20.811Z", + deliveredAt: '2018-08-07T16:36:20.811Z', }; const storyB = { - deliveredAt: "2018-08-06T16:36:20.811Z", + deliveredAt: '2018-08-06T16:36:20.811Z', }; expect(Story.compareDeliveredAt(storyA, storyB)).toEqual(1); }); }); - describe("when date A is older then B", () => { - it("return -1", () => { + describe('when date A is older then B', () => { + it('return -1', () => { const storyA = { - deliveredAt: "2018-08-07T12:36:20.811Z", + deliveredAt: '2018-08-07T12:36:20.811Z', }; const storyB = { - deliveredAt: "2018-08-07T16:36:20.811Z", + deliveredAt: '2018-08-07T16:36:20.811Z', }; expect(Story.compareDeliveredAt(storyA, storyB)).toEqual(-1); }); }); - describe("when date A is equal B", () => { - it("return 0", () => { + describe('when date A is equal B', () => { + it('return 0', () => { const storyA = { - deliveredAt: "2018-08-07T12:36:20.811Z", + deliveredAt: '2018-08-07T12:36:20.811Z', }; const storyB = { - deliveredAt: "2018-08-07T12:36:20.811Z", + deliveredAt: '2018-08-07T12:36:20.811Z', }; expect(Story.compareDeliveredAt(storyA, storyB)).toEqual(0); }); }); - describe("is unestimated feature", () => { - it("return a story if it is a feature and is unestimated", () => { + describe('is unestimated feature', () => { + it('return a story if it is a feature and is unestimated', () => { const stories = [ { id: 10, - storyType: "feature", + storyType: 'feature', estimate: null, }, { id: 20, - storyType: "bug", + storyType: 'bug', estimate: null, }, { id: 30, - storyType: "feature", + storyType: 'feature', estimate: 2, }, ]; @@ -213,9 +213,9 @@ describe("Story model", function () { }); }); - describe("toggleStory", () => { - describe("When story is collapsed", () => { - it("sets collapsed to false and copies the previous state to a _editing field", () => { + describe('toggleStory', () => { + describe('When story is collapsed', () => { + it('sets collapsed to false and copies the previous state to a _editing field', () => { const story = { collapsed: true }; const expandedStory = Story.toggleStory(story); @@ -231,8 +231,8 @@ describe("Story model", function () { }); }); - describe("When story is expanded", () => { - it("sets collapsed to true and sets _editing field to null", () => { + describe('When story is expanded', () => { + it('sets collapsed to true and sets _editing field to null', () => { const story = { collapsed: false }; const collapsedStory = Story.toggleStory(story); @@ -245,7 +245,7 @@ describe("Story model", function () { }); }); - describe("editStory", () => { + describe('editStory', () => { const { FEATURE, BUG, CHORE, RELEASE } = storyTypes; const { UNSTARTED, UNSCHEDULED, STARTED } = status; @@ -253,7 +253,7 @@ describe("Story model", function () { const estimatedValues = [1, 2, 3, 4]; describe(`when story type is ${FEATURE}`, () => { - notFeatureTypes.forEach((noFeatureType) => { + notFeatureTypes.forEach(noFeatureType => { describe(`and new story type is ${noFeatureType}`, () => { const story = { _editing: { storyType: FEATURE, estimate: 1 } }; const newAttributes = { storyType: noFeatureType }; @@ -264,7 +264,7 @@ describe("Story model", function () { }); it('change story estimate to ""', () => { - expect(changedStory._editing.estimate).toEqual(""); + expect(changedStory._editing.estimate).toEqual(''); }); it(`change story type is ${noFeatureType}`, () => { @@ -285,7 +285,7 @@ describe("Story model", function () { changedStory = Story.editStory(story, newAttributes); }); - it("keep estimate 1", () => { + it('keep estimate 1', () => { expect(changedStory._editing.estimate).toEqual(1); }); @@ -295,7 +295,7 @@ describe("Story model", function () { }); }); - estimatedValues.forEach((estimatedValue) => { + estimatedValues.forEach(estimatedValue => { describe(`when estimate is ${estimatedValue}`, () => { describe(`and new estimate is ""`, () => { const story = { @@ -305,7 +305,7 @@ describe("Story model", function () { state: STARTED, }, }; - const newAttributes = { estimate: "" }; + const newAttributes = { estimate: '' }; let changedStory; beforeEach(() => { @@ -317,19 +317,19 @@ describe("Story model", function () { }); it("change estimate to ''", () => { - expect(changedStory._editing.estimate).toEqual(""); + expect(changedStory._editing.estimate).toEqual(''); }); }); }); }); describe(`when estimate is ""`, () => { - estimatedValues.forEach((estimatedValue) => { + estimatedValues.forEach(estimatedValue => { describe(`and new estimate is ${estimatedValue}`, () => { const story = { _editing: { storyType: FEATURE, - estimate: "", + estimate: '', state: UNSCHEDULED, }, }; @@ -352,16 +352,16 @@ describe("Story model", function () { }); }); - notFeatureTypes.forEach((noFeatureType) => { + notFeatureTypes.forEach(noFeatureType => { describe(`when story type is ${noFeatureType}`, () => { const otherNotFeatureTypes = notFeatureTypes.filter( - (type) => type !== noFeatureType + type => type !== noFeatureType ); - otherNotFeatureTypes.forEach((otherNotFeatureType) => { + otherNotFeatureTypes.forEach(otherNotFeatureType => { describe(`and new story type is ${otherNotFeatureType}`, () => { const story = { - _editing: { storyType: noFeatureType, estimate: "" }, + _editing: { storyType: noFeatureType, estimate: '' }, }; const newAttributes = { storyType: otherNotFeatureType }; let changedStory; @@ -371,7 +371,7 @@ describe("Story model", function () { }); it("change estime to ''", () => { - expect(changedStory._editing.estimate).toEqual(""); + expect(changedStory._editing.estimate).toEqual(''); }); it(`change story type to ${otherNotFeatureType}`, () => { @@ -382,7 +382,7 @@ describe("Story model", function () { }); }); - Object.keys(status).forEach((item) => { + Object.keys(status).forEach(item => { describe(`and new status is ${status[item]}`, () => { const story = { _editing: { storyType: noFeatureType } }; const newAttributes = { state: status[item] }; @@ -403,7 +403,7 @@ describe("Story model", function () { const story = { _editing: { storyType: noFeatureType, - estimate: "", + estimate: '', state: UNSCHEDULED, }, }; @@ -415,7 +415,7 @@ describe("Story model", function () { }); it('change estimate to ""', () => { - expect(changedStory._editing.estimate).toEqual(""); + expect(changedStory._editing.estimate).toEqual(''); }); it(`change story type to ${FEATURE}`, () => { @@ -444,7 +444,7 @@ describe("Story model", function () { changedStory = Story.editStory(story, newAttributes); }); - it("change story estimate to 1", () => { + it('change story estimate to 1', () => { expect(changedStory._editing.estimate).toEqual(1); }); @@ -464,7 +464,7 @@ describe("Story model", function () { _editing: { storyType: noFeatureType, state: UNSTARTED, - estimate: "", + estimate: '', }, }; const newAttributes = { state: UNSCHEDULED }; @@ -475,7 +475,7 @@ describe("Story model", function () { }); it("change estimate to ''", () => { - expect(changedStory._editing.estimate).toEqual(""); + expect(changedStory._editing.estimate).toEqual(''); }); it(`change state to ${UNSCHEDULED}`, () => { @@ -487,7 +487,7 @@ describe("Story model", function () { describe(`when state is ${UNSCHEDULED}`, () => { describe(`when new state is ${UNSTARTED}`, () => { const story = { - _editing: { storyType: noFeatureType, state: "", estimate: 1 }, + _editing: { storyType: noFeatureType, state: '', estimate: 1 }, }; const newAttributes = { state: UNSTARTED }; let changedStory; @@ -497,7 +497,7 @@ describe("Story model", function () { }); it("change estimate to ''", () => { - expect(changedStory._editing.estimate).toEqual(""); + expect(changedStory._editing.estimate).toEqual(''); }); it(`change state to ${UNSTARTED}`, () => { @@ -509,10 +509,10 @@ describe("Story model", function () { }); }); - describe("addNewAttributes", () => { - it("update story type", () => { - const story = { storyType: "bug" }; - const newAttributes = { storyType: "feature" }; + describe('addNewAttributes', () => { + it('update story type', () => { + const story = { storyType: 'bug' }; + const newAttributes = { storyType: 'feature' }; const changedStory = Story.addNewAttributes(story, newAttributes); @@ -521,7 +521,7 @@ describe("Story model", function () { }); }); - it("update story estimate", () => { + it('update story estimate', () => { const story = { estimate: 1 }; const newAttributes = { estimate: 2 }; @@ -533,8 +533,8 @@ describe("Story model", function () { }); }); - describe("setLoadingStory", () => { - it("sets the loading state to true when false", () => { + describe('setLoadingStory', () => { + it('sets the loading state to true when false', () => { const story = { _editing: { loading: false } }; const changedStory = Story.setLoadingStory(story); @@ -543,8 +543,8 @@ describe("Story model", function () { }); }); - describe("setLoadingValue", () => { - it("sets the loading state to true", () => { + describe('setLoadingValue', () => { + it('sets the loading state to true', () => { const story = { loading: false }; const changedStory = Story.setLoadingValue(story, true); @@ -552,7 +552,7 @@ describe("Story model", function () { expect(changedStory.loading).toEqual(true); }); - it("sets the loading state to false", () => { + it('sets the loading state to false', () => { const story = { loading: true }; const changedStory = Story.setLoadingValue(story, false); @@ -561,15 +561,15 @@ describe("Story model", function () { }); }); - describe("findById", () => { - it("return the story by id", () => { + describe('findById', () => { + it('return the story by id', () => { const stories = [{ id: 1 }, { id: 2 }, { id: 3 }]; expect(Story.findById(stories, 1)).toEqual(stories[0]); }); }); - describe("storyFailure", () => { - it("sets loading to false", () => { + describe('storyFailure', () => { + it('sets loading to false', () => { const story = { _editing: { loading: true }, errors: [] }; const changedStory = Story.storyFailure(story); @@ -577,8 +577,8 @@ describe("Story model", function () { expect(changedStory._editing.loading).toEqual(false); }); - it("put error to errors object", () => { - const error = "error"; + it('put error to errors object', () => { + const error = 'error'; const story = { _editing: { loading: true }, errors: {} }; const changedStory = Story.storyFailure(story, error); @@ -587,8 +587,8 @@ describe("Story model", function () { }); }); - describe("isNew", () => { - it("returns true when story id is new", () => { + describe('isNew', () => { + it('returns true when story id is new', () => { const story = { id: createTemporaryId() }; expect(Story.isNew(story)).toBe(true); @@ -601,56 +601,56 @@ describe("Story model", function () { }); }); - describe("canDelete", () => { + describe('canDelete', () => { it("returns true when story isn't accepted and isn't new", () => { - const story = { id: 42, state: "started" }; + const story = { id: 42, state: 'started' }; expect(Story.canDelete(story)).toBe(true); }); - it("returns false when story is accepted", () => { - const story = { id: 42, state: "accepted" }; + it('returns false when story is accepted', () => { + const story = { id: 42, state: 'accepted' }; expect(Story.canDelete(story)).toBe(false); }); - it("returns false when story is new", () => { - const story = { id: createTemporaryId(), state: "started" }; + it('returns false when story is new', () => { + const story = { id: createTemporaryId(), state: 'started' }; expect(Story.canDelete(story)).toBe(false); }); }); - describe("canSave", () => { + describe('canSave', () => { describe("when when story isn't accepted and the text isn't empty", () => { - it("returns true ", () => { - const story = { state: "started", _editing: { title: "title" } }; + it('returns true ', () => { + const story = { state: 'started', _editing: { title: 'title' } }; expect(Story.canSave(story)).toBe(true); }); }); - describe("when story is accepted", () => { - it("returns false", () => { - const story = { state: "accepted", _editing: { title: "title" } }; + describe('when story is accepted', () => { + it('returns false', () => { + const story = { state: 'accepted', _editing: { title: 'title' } }; expect(Story.canSave(story)).toBe(false); }); }); - describe("when story title is null", () => { - it("returns false when story title is null", () => { - const story = { state: "started", _editing: { title: "" } }; + describe('when story title is null', () => { + it('returns false when story title is null', () => { + const story = { state: 'started', _editing: { title: '' } }; expect(Story.canSave(story)).toBe(false); }); }); }); - describe("canEdit", () => { - describe("when the story is accepted", () => { - it("returns false", () => { - const story = { state: "accepted" }; + describe('canEdit', () => { + describe('when the story is accepted', () => { + it('returns false', () => { + const story = { state: 'accepted' }; expect(Story.canEdit(story)).toBe(false); }); @@ -658,7 +658,7 @@ describe("Story model", function () { describe(`when story is unscheduled`, () => { it(`returns true`, () => { - const story = { state: "unscheduled" }; + const story = { state: 'unscheduled' }; expect(Story.canEdit(story)).toBe(true); }); @@ -666,7 +666,7 @@ describe("Story model", function () { describe(`when story is unstarted`, () => { it(`returns true`, () => { - const story = { state: "unstarted" }; + const story = { state: 'unstarted' }; expect(Story.canEdit(story)).toBe(true); }); @@ -674,7 +674,7 @@ describe("Story model", function () { describe(`when story is started`, () => { it(`returns true`, () => { - const story = { state: "started" }; + const story = { state: 'started' }; expect(Story.canEdit(story)).toBe(true); }); @@ -682,7 +682,7 @@ describe("Story model", function () { describe(`when story is finished`, () => { it(`returns true`, () => { - const story = { state: "finished" }; + const story = { state: 'finished' }; expect(Story.canEdit(story)).toBe(true); }); @@ -690,7 +690,7 @@ describe("Story model", function () { describe(`when story is delivered`, () => { it(`returns true`, () => { - const story = { state: "delivered" }; + const story = { state: 'delivered' }; expect(Story.canEdit(story)).toBe(true); }); @@ -698,15 +698,15 @@ describe("Story model", function () { describe(`when story is rejected`, () => { it(`returns true`, () => { - const story = { state: "rejected" }; + const story = { state: 'rejected' }; expect(Story.canEdit(story)).toBe(true); }); }); }); - describe("withoutNewStory", () => { - it("remove a story with a new id (symbol) from a stories array", () => { + describe('withoutNewStory', () => { + it('remove a story with a new id (symbol) from a stories array', () => { const newStoryId = createTemporaryId(); const stories = [{ id: 1 }, { id: 2 }]; const newStory = { id: newStoryId }; @@ -725,25 +725,25 @@ describe("Story model", function () { }); }); - describe("createNewStory", () => { + describe('createNewStory', () => { const stories = [{ id: 1 }, { id: 3 }]; - it("returns an empty story", () => { + it('returns an empty story', () => { const story = Story.createNewStory(stories, {}); - expect(typeof story.id).toBe("symbol"); + expect(typeof story.id).toBe('symbol'); }); - it("returns an expanded story", () => { + it('returns an expanded story', () => { const story = Story.createNewStory(stories, {}); expect(story.collapsed).toBe(false); }); - describe("when there is already a new story in the store", () => { - it("reuses the attributes of it", () => { + describe('when there is already a new story in the store', () => { + it('reuses the attributes of it', () => { const story = Story.createNewStory(stories, {}); - const attributes = { title: "new Title" }; + const attributes = { title: 'new Title' }; const modifiedStory = Story.createNewStory(stories, attributes); expect(story.title).not.toEqual(modifiedStory.title); @@ -752,8 +752,8 @@ describe("Story model", function () { }); }); - describe("replaceOrAddNewStory", () => { - it("replace an empty for a new story", () => { + describe('replaceOrAddNewStory', () => { + it('replace an empty for a new story', () => { const newStoryId = createTemporaryId(); const stories = [{ id: 1 }, { id: newStoryId }, { id: 3 }]; const newStory = { id: 2 }; @@ -768,7 +768,7 @@ describe("Story model", function () { expect(newStoryArray).toEqual(expectedArray); }); - it("return an array with new story", () => { + it('return an array with new story', () => { const stories = []; const newStory = { id: 2 }; const expectedArray = [newStory]; @@ -778,7 +778,7 @@ describe("Story model", function () { expect(newStoryArray).toEqual(expectedArray); }); - it("add new story", () => { + it('add new story', () => { const stories = [{ id: 1 }, { id: 3 }]; const newStory = { id: 2 }; const expectedArray = [newStory, ...stories]; @@ -789,165 +789,165 @@ describe("Story model", function () { }); }); - describe("getNextState", () => { - describe("when the state is unscheduled", () => { - const state = "unscheduled"; + describe('getNextState', () => { + describe('when the state is unscheduled', () => { + const state = 'unscheduled'; - it("returns started when transition is start", () => { - const transition = "start"; - const expectedState = "started"; + it('returns started when transition is start', () => { + const transition = 'start'; + const expectedState = 'started'; expect(Story.getNextState(state, transition)).toBe(expectedState); }); - it("returns the same state when transition is invalid", () => { - const transition = "invalidTransition"; - const expectedState = "unscheduled"; + it('returns the same state when transition is invalid', () => { + const transition = 'invalidTransition'; + const expectedState = 'unscheduled'; expect(Story.getNextState(state, transition)).toBe(expectedState); }); }); - describe("when the state is unstarted", () => { - const state = "unstarted"; + describe('when the state is unstarted', () => { + const state = 'unstarted'; - it("returns started when transition is start", () => { - const transition = "start"; - const expectedState = "started"; + it('returns started when transition is start', () => { + const transition = 'start'; + const expectedState = 'started'; expect(Story.getNextState(state, transition)).toBe(expectedState); }); - it("returns the same state when transition is invalid", () => { - const transition = "invalidTransition"; - const expectedState = "unstarted"; + it('returns the same state when transition is invalid', () => { + const transition = 'invalidTransition'; + const expectedState = 'unstarted'; expect(Story.getNextState(state, transition)).toBe(expectedState); }); }); - describe("when the state is started", () => { - const state = "started"; + describe('when the state is started', () => { + const state = 'started'; - it("returns finished when transition is finish", () => { - const transition = "finish"; - const expectedState = "finished"; + it('returns finished when transition is finish', () => { + const transition = 'finish'; + const expectedState = 'finished'; expect(Story.getNextState(state, transition)).toBe(expectedState); }); - it("returns the same state when transition is invalid", () => { - const transition = "invalidTransition"; - const expectedState = "started"; + it('returns the same state when transition is invalid', () => { + const transition = 'invalidTransition'; + const expectedState = 'started'; expect(Story.getNextState(state, transition)).toBe(expectedState); }); }); - describe("when the state is finished", () => { - const state = "finished"; + describe('when the state is finished', () => { + const state = 'finished'; - it("returns delivered when transition is deliver", () => { - const transition = "deliver"; - const expectedState = "delivered"; + it('returns delivered when transition is deliver', () => { + const transition = 'deliver'; + const expectedState = 'delivered'; expect(Story.getNextState(state, transition)).toBe(expectedState); }); - it("returns the same state when transition is invalid", () => { - const transition = "invalidTransition"; - const expectedState = "finished"; + it('returns the same state when transition is invalid', () => { + const transition = 'invalidTransition'; + const expectedState = 'finished'; expect(Story.getNextState(state, transition)).toBe(expectedState); }); }); - describe("when the state is delivered", () => { - const state = "delivered"; + describe('when the state is delivered', () => { + const state = 'delivered'; - it("returns accepted when transition is accept", () => { - const transition = "accept"; - const expectedState = "accepted"; + it('returns accepted when transition is accept', () => { + const transition = 'accept'; + const expectedState = 'accepted'; expect(Story.getNextState(state, transition)).toBe(expectedState); }); - it("returns rejected when transition is reject", () => { - const transition = "reject"; - const expectedState = "rejected"; + it('returns rejected when transition is reject', () => { + const transition = 'reject'; + const expectedState = 'rejected'; expect(Story.getNextState(state, transition)).toBe(expectedState); }); - it("returns the same state when transition is invalid", () => { - const transition = "invalidTransition"; + it('returns the same state when transition is invalid', () => { + const transition = 'invalidTransition'; expect(Story.getNextState(state, transition)).toBe(state); }); }); - describe("when the state is rejected", () => { - const state = "rejected"; + describe('when the state is rejected', () => { + const state = 'rejected'; - it("returns started when transition is restart", () => { - const transition = "restart"; - const expectedState = "started"; + it('returns started when transition is restart', () => { + const transition = 'restart'; + const expectedState = 'started'; expect(Story.getNextState(state, transition)).toBe(expectedState); }); - it("returns the same state when transition is invalid", () => { - const transition = "invalidTransition"; + it('returns the same state when transition is invalid', () => { + const transition = 'invalidTransition'; expect(Story.getNextState(state, transition)).toBe(state); }); }); - describe("when the state is accepted", () => { - const state = "accepted"; + describe('when the state is accepted', () => { + const state = 'accepted'; - it("not change state", () => { - const transition = "any"; + it('not change state', () => { + const transition = 'any'; expect(Story.getNextState(state, transition)).toBe(state); }); }); - describe("when the transition is release", () => { - const state = "any"; + describe('when the transition is release', () => { + const state = 'any'; - it("returns accepted", () => { - const expectedState = "accepted"; - const transition = "release"; + it('returns accepted', () => { + const expectedState = 'accepted'; + const transition = 'release'; expect(Story.getNextState(state, transition)).toBe(expectedState); }); }); }); - describe("releaseIsLate", () => { - it("returns true when relase date is before today", () => { + describe('releaseIsLate', () => { + it('returns true when relase date is before today', () => { const story = { - releaseDate: moment().subtract(3, "days"), - storyType: "release", + releaseDate: moment().subtract(3, 'days'), + storyType: 'release', }; expect(Story.releaseIsLate(story)).toBe(true); }); - it("returns false when relase date is after today", () => { + it('returns false when relase date is after today', () => { const story = { - releaseDate: moment().add(3, "days"), - storyType: "release", + releaseDate: moment().add(3, 'days'), + storyType: 'release', }; expect(Story.releaseIsLate(story)).toBe(false); }); - it("returns false when relase date is today", () => { + it('returns false when relase date is today', () => { const story = { releaseDate: moment(), - storyType: "release", + storyType: 'release', }; expect(Story.releaseIsLate(story)).toBe(false); @@ -955,59 +955,59 @@ describe("Story model", function () { it("returns false when story type isn't a release", () => { const story = { - releaseDate: moment().subtract(3, "days"), - storyType: "feature", + releaseDate: moment().subtract(3, 'days'), + storyType: 'feature', }; expect(Story.releaseIsLate(story)).toBe(false); }); }); - describe("cloneStory", () => { - it("retuns a new story with null id", () => { + describe('cloneStory', () => { + it('retuns a new story with null id', () => { const story = { id: 42 }; expect(Story.cloneStory(story).id).toBe(null); }); - it("retuns a new story with uncheduled state", () => { - const story = { state: "accepted" }; + it('retuns a new story with uncheduled state', () => { + const story = { state: 'accepted' }; expect(Story.cloneStory(story).state).toBe(status.UNSCHEDULED); }); - it("retuns a new dirty story", () => { + it('retuns a new dirty story', () => { const story = { _isDirty: false }; expect(Story.cloneStory(story)._isDirty).toBe(true); }); - it("retuns a new expanded story", () => { + it('retuns a new expanded story', () => { const story = { collapsed: true }; expect(Story.cloneStory(story).collapsed).toBe(false); }); - it("retuns the same story title", () => { - const story = { title: "My new Title" }; + it('retuns the same story title', () => { + const story = { title: 'My new Title' }; expect(Story.cloneStory(story).title).toBe(story.title); }); - it("retuns the same story description", () => { - const story = { description: "My description" }; + it('retuns the same story description', () => { + const story = { description: 'My description' }; expect(Story.cloneStory(story).description).toBe(story.description); }); - it("retuns the same story estimate", () => { + it('retuns the same story estimate', () => { const story = { estimate: 1 }; expect(Story.cloneStory(story).estimate).toBe(story.estimate); }); - it("retuns the same story type", () => { - const story = { storyType: "feature" }; + it('retuns the same story type', () => { + const story = { storyType: 'feature' }; expect(Story.cloneStory(story).storyType).toBe(story.storyType); }); @@ -1025,20 +1025,20 @@ describe("Story model", function () { }); }); - describe("possibleStatesFor", () => { - const invalidEstimates = [null, ""]; + describe('possibleStatesFor', () => { + const invalidEstimates = [null, '']; const validEstimates = [1, 2, 3]; const featureTypes = Story.types.filter( - (type) => type === storyTypes.FEATURE + type => type === storyTypes.FEATURE ); const noFeatureTypes = Story.types.filter( - (type) => type !== storyTypes.FEATURE + type => type !== storyTypes.FEATURE ); - featureTypes.forEach((featureType) => { + featureTypes.forEach(featureType => { describe(`when storyType is ${featureType}`, () => { - invalidEstimates.forEach((invalidEstimate) => { + invalidEstimates.forEach(invalidEstimate => { describe(`and estimate is "${invalidEstimate}"`, () => { let story; @@ -1054,13 +1054,13 @@ describe("Story model", function () { ); }); - it("returns just one state", () => { + it('returns just one state', () => { expect(Story.possibleStatesFor(story).length).toEqual(1); }); }); }); - validEstimates.forEach((validEstimate) => { + validEstimates.forEach(validEstimate => { describe(`and estimate is ${validEstimate}`, () => { let story; @@ -1070,7 +1070,7 @@ describe("Story model", function () { }; }); - it("return all states", () => { + it('return all states', () => { expect(Story.possibleStatesFor(story).length).toEqual(7); }); }); @@ -1078,9 +1078,9 @@ describe("Story model", function () { }); }); - noFeatureTypes.forEach((noFeatureType) => { + noFeatureTypes.forEach(noFeatureType => { describe(`when storyType is ${noFeatureType}`, () => { - invalidEstimates.forEach((invalidEstimate) => { + invalidEstimates.forEach(invalidEstimate => { describe(`and estimate is "${invalidEstimate}"`, () => { let story; @@ -1093,12 +1093,12 @@ describe("Story model", function () { }; }); - it("returns all states", () => { + it('returns all states', () => { expect(Story.possibleStatesFor(story).length).toEqual(7); }); }); - validEstimates.forEach((validEstimate) => { + validEstimates.forEach(validEstimate => { describe(`and estimate is ${validEstimate}`, () => { let story; @@ -1111,7 +1111,7 @@ describe("Story model", function () { }; }); - it("returns all states", () => { + it('returns all states', () => { expect(Story.possibleStatesFor(story).length).toEqual(7); }); }); @@ -1121,7 +1121,7 @@ describe("Story model", function () { }); }); - describe("withScope", () => { + describe('withScope', () => { const stories = { [storyScopes.ALL]: [ { id: 1, storyType: storyTypes.FEATURE }, @@ -1136,7 +1136,7 @@ describe("Story model", function () { }; const invalidScopes = [null, undefined, false, 0]; - invalidScopes.forEach((scope) => { + invalidScopes.forEach(scope => { describe(`when scope is ${scope}`, () => { it('returns stories of scope "all"', () => { expect(Story.withScope(stories, scope)).toEqual([ @@ -1169,7 +1169,7 @@ describe("Story model", function () { }); }); - describe("totalPoints", () => { + describe('totalPoints', () => { const stories = [ { estimate: 1, @@ -1185,30 +1185,30 @@ describe("Story model", function () { }, ]; - it("return 3", () => { + it('return 3', () => { expect(Story.totalPoints(stories)).toEqual(3); }); }); - describe("isHighlighted", () => { - describe("when highlighted is false", () => { - it("returns falsy", () => { + describe('isHighlighted', () => { + describe('when highlighted is false', () => { + it('returns falsy', () => { const story = { highlighted: false }; expect(Story.isHighlighted(story)).toBeFalsy(); }); }); - describe("when highlighted is true", () => { - it("returns truthy", () => { + describe('when highlighted is true', () => { + it('returns truthy', () => { const story = { highlighted: true }; expect(Story.isHighlighted(story)).toBeTruthy(); }); }); - describe("when have not highlighted", () => { - it("returns falsy", () => { + describe('when have not highlighted', () => { + it('returns falsy', () => { const story = {}; expect(Story.isHighlighted(story)).toBeFalsy(); @@ -1216,25 +1216,25 @@ describe("Story model", function () { }); }); - describe("isSearch", () => { + describe('isSearch', () => { const noSearchScopes = [storyScopes.ALL]; - noSearchScopes.forEach((scope) => { + noSearchScopes.forEach(scope => { describe(`when scope is ${scope}`, () => { - it("returns falsy", () => { + it('returns falsy', () => { expect(Story.isSearch(scope)).toBeFalsy(); }); }); describe(`when scope is ${storyScopes.SEARCH}`, () => { - it("returns truthy", () => { + it('returns truthy', () => { expect(Story.isSearch(storyScopes.SEARCH)).toBeTruthy(); }); }); }); }); - describe("haveHighlightButton", () => { + describe('haveHighlightButton', () => { const stories = [ { id: 1, storyType: storyTypes.FEATURE }, { id: 2, storyType: storyTypes.FEATURE }, @@ -1243,11 +1243,11 @@ describe("Story model", function () { const noSearchScopes = [storyScopes.ALL]; - stories.forEach((story) => { + stories.forEach(story => { describe(`when story id is ${story.id}`, () => { - noSearchScopes.forEach((scope) => { + noSearchScopes.forEach(scope => { describe(`and scope is ${scope}`, () => { - it("returns falsy", () => { + it('returns falsy', () => { expect( Story.haveHighlightButton(stories, story, scope) ).toBeFalsy(); @@ -1256,7 +1256,7 @@ describe("Story model", function () { }); describe(`and scope ${storyScopes.SEARCH}`, () => { - it("returns truthy", () => { + it('returns truthy', () => { expect( Story.haveHighlightButton(stories, story, storyScopes.SEARCH) ).toBeTruthy(); @@ -1265,12 +1265,12 @@ describe("Story model", function () { }); }); - describe("when story id is 100", () => { + describe('when story id is 100', () => { const fakeStory = { id: 100, storyType: storyTypes.FEATURE }; - noSearchScopes.forEach((scope) => { + noSearchScopes.forEach(scope => { describe(`and scope is ${scope}`, () => { - it("returns falsy", () => { + it('returns falsy', () => { expect( Story.haveHighlightButton(stories, fakeStory, scope) ).toBeFalsy(); @@ -1279,7 +1279,7 @@ describe("Story model", function () { }); describe(`and scope ${storyScopes.SEARCH}`, () => { - it("returns falsy", () => { + it('returns falsy', () => { expect( Story.haveHighlightButton(stories, fakeStory, storyScopes.SEARCH) ).toBeFalsy(); @@ -1288,18 +1288,18 @@ describe("Story model", function () { }); }); - describe("haveSearch", () => { - describe("when have zero search stories", () => { + describe('haveSearch', () => { + describe('when have zero search stories', () => { const stories = { [storyScopes.SEARCH]: [], }; - it("returns falsy", () => { + it('returns falsy', () => { expect(Story.haveSearch(stories)).toBeFalsy(); }); }); - describe("when have more than one search story", () => { + describe('when have more than one search story', () => { const stories = { [storyScopes.SEARCH]: [ { id: 1, storyType: storyTypes.FEATURE }, @@ -1308,25 +1308,25 @@ describe("Story model", function () { ], }; - it("returns truthy", () => { + it('returns truthy', () => { expect(Story.haveSearch(stories)).toBeTruthy(); }); }); }); - describe("haveStory", () => { + describe('haveStory', () => { const stories = [{ id: 1 }, { id: 2 }, { id: 3 }]; - stories.forEach((story) => { + stories.forEach(story => { describe(`when story is present in the stories array`, () => { - it("returns truthy", () => { + it('returns truthy', () => { expect(Story.haveStory(story, stories)).toBeTruthy(); }); }); }); - describe("when story is not present in stories array", () => { - it("returns falsy", () => { + describe('when story is not present in stories array', () => { + it('returns falsy', () => { const story = { id: 100 }; expect(Story.haveStory(story, stories)).toBeFalsy(); @@ -1334,14 +1334,14 @@ describe("Story model", function () { }); }); - describe("stateFor", () => { + describe('stateFor', () => { const { BUG, CHORE, RELEASE } = storyTypes; const { UNSTARTED, UNSCHEDULED } = status; - describe("when is a feature", () => { - describe("and have no estimate", () => { + describe('when is a feature', () => { + describe('and have no estimate', () => { const story = { - _editing: { id: 1, storyType: storyTypes.FEATURE, estimate: "" }, + _editing: { id: 1, storyType: storyTypes.FEATURE, estimate: '' }, }; const newAttributes = { state: UNSTARTED }; const newStory = { ...story, ...newAttributes }; @@ -1353,11 +1353,11 @@ describe("Story model", function () { }); }); - describe("and new attributes have no estimate", () => { + describe('and new attributes have no estimate', () => { const story = { _editing: { id: 1, storyType: storyTypes.FEATURE, estimate: 2 }, }; - const newAttributes = { estimate: "" }; + const newAttributes = { estimate: '' }; const newStory = { ...story, ...newAttributes }; it(`return ${UNSCHEDULED}`, () => { @@ -1384,9 +1384,9 @@ describe("Story model", function () { const noFeatureTypes = [BUG, CHORE, RELEASE]; - noFeatureTypes.forEach((noFeatureType) => { + noFeatureTypes.forEach(noFeatureType => { describe(`when is ${noFeatureType}`, () => { - Story.states.forEach((state) => { + Story.states.forEach(state => { describe(`and new attributes is state ${state}`, () => { const story = { _editing: { id: 1, storyType: noFeatureType, estimate: 2 }, @@ -1405,34 +1405,34 @@ describe("Story model", function () { }); }); - describe("estimateFor", () => { + describe('estimateFor', () => { const { BUG, CHORE, RELEASE } = storyTypes; const noFeatureTypes = [BUG, CHORE, RELEASE]; - noFeatureTypes.forEach((noFeatureType) => { + noFeatureTypes.forEach(noFeatureType => { describe(`when story type is ${noFeatureType}`, () => { - describe("and new attributes have estimate 2", () => { + describe('and new attributes have estimate 2', () => { const story = { - _editing: { id: 1, storyType: noFeatureType, estimate: "" }, + _editing: { id: 1, storyType: noFeatureType, estimate: '' }, }; const newAttributes = { estimate: 2 }; const newStory = { ...story, ...newAttributes }; - it("return an empty string", () => { + it('return an empty string', () => { expect(Story.estimateFor(story, newAttributes, newStory)).toEqual( - "" + '' ); }); }); - describe("and new attributes have story type feature", () => { + describe('and new attributes have story type feature', () => { const story = { - _editing: { id: 1, storyType: noFeatureType, estimate: "" }, + _editing: { id: 1, storyType: noFeatureType, estimate: '' }, }; - const newAttributes = { storyType: "feature" }; + const newAttributes = { storyType: 'feature' }; const newStory = { ...story, ...newAttributes }; - it("return 1", () => { + it('return 1', () => { expect(Story.estimateFor(story, newAttributes, newStory)).toEqual( 1 ); @@ -1442,7 +1442,7 @@ describe("Story model", function () { }); }); - describe("needConfirmation", () => { + describe('needConfirmation', () => { const needConfirmationStates = [ status.ACCEPTED, status.REJECTED, @@ -1456,9 +1456,9 @@ describe("Story model", function () { status.UNSCHEDULED, ]; - needConfirmationStates.forEach((state) => { + needConfirmationStates.forEach(state => { describe(`when state is ${state}`, () => { - it("returns truthy", () => { + it('returns truthy', () => { const story = { state }; expect(Story.needConfirmation(story)).toBeTruthy(); @@ -1466,9 +1466,9 @@ describe("Story model", function () { }); }); - noNeedConfirmationStates.forEach((state) => { + noNeedConfirmationStates.forEach(state => { describe(`when state is ${state}`, () => { - it("returns falsy", () => { + it('returns falsy', () => { const story = { state }; expect(Story.needConfirmation(story)).toBeFalsy(); diff --git a/spec/javascripts/selectors/backlog_spec.js b/spec/javascripts/selectors/backlog_spec.js index f8d8388e8..d20679876 100644 --- a/spec/javascripts/selectors/backlog_spec.js +++ b/spec/javascripts/selectors/backlog_spec.js @@ -41,11 +41,11 @@ describe('Backlog selector functions', () => { state: 'delivered', deliveredAt: '2018-08-06T16:36:20.811Z', estimate: 1, - } + }, ]; describe('orderByState', () => { - it("return stories ordered by state", () => { + it('return stories ordered by state', () => { const stories = storiesArray; const orderedStories = orderByState(stories); @@ -64,13 +64,13 @@ describe('Backlog selector functions', () => { const newStory = storyFactory({ id: 0, state: 'accepted', - acceptedAt: '2018-08-03T16:36:20.811Z' + acceptedAt: '2018-08-03T16:36:20.811Z', }); const oldStory = storyFactory({ id: 1, state: 'accepted', - acceptedAt: '2018-08-02T16:36:20.811Z' + acceptedAt: '2018-08-02T16:36:20.811Z', }); const stories = [newStory, oldStory]; @@ -86,13 +86,13 @@ describe('Backlog selector functions', () => { const newStory = storyFactory({ id: 0, state: 'delivered', - deliveredAt: '2018-08-03T16:36:20.811Z' + deliveredAt: '2018-08-03T16:36:20.811Z', }); const oldStory = storyFactory({ id: 1, state: 'delivered', - deliveredAt: '2018-08-02T16:36:20.811Z' + deliveredAt: '2018-08-02T16:36:20.811Z', }); const stories = [newStory, oldStory]; @@ -108,13 +108,13 @@ describe('Backlog selector functions', () => { const newStory = storyFactory({ id: 0, state: 'started', - startedAt: '2018-08-03T16:36:20.811Z' + startedAt: '2018-08-03T16:36:20.811Z', }); const oldStory = storyFactory({ id: 1, state: 'started', - startedAt: '2018-08-02T16:36:20.811Z' + startedAt: '2018-08-02T16:36:20.811Z', }); const stories = [newStory, oldStory]; @@ -132,13 +132,13 @@ describe('Backlog selector functions', () => { storyStates.forEach(state => { const firstPosition = storyFactory({ id: 1, - newPosition: 1, + position: 1, state, }); const secondPosition = storyFactory({ id: 0, - newPosition: 2, + position: 2, state, }); From ef22da5b7b86025f804731fbeebeaba537e0070a Mon Sep 17 00:00:00 2001 From: Guilherme-NL Date: Wed, 8 Nov 2023 15:02:04 -0300 Subject: [PATCH 4/9] refactor: add new_position to the allowed_params --- app/controllers/stories_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/stories_controller.rb b/app/controllers/stories_controller.rb index 0573254b7..f9c829e5d 100644 --- a/app/controllers/stories_controller.rb +++ b/app/controllers/stories_controller.rb @@ -122,7 +122,7 @@ def select_stories_by_params def allowed_params params.require(:story).permit( :title, :description, :estimate, :story_type, :release_date, - :state, :requested_by_id, :owned_by_id, :position, :labels, + :state, :requested_by_id, :owned_by_id, :position, :new_position, :labels, tasks_attributes: %i[id name done], notes_attributes: %i[id note] ) From aa6e8ab96e0bfd622c0b949919c1f32c557d04af Mon Sep 17 00:00:00 2001 From: Guilherme-NL Date: Thu, 9 Nov 2023 08:52:45 -0300 Subject: [PATCH 5/9] feat: add sortOptimistically and getHighestNewPosition functions --- app/assets/javascripts/models/beta/story.js | 34 +++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/models/beta/story.js b/app/assets/javascripts/models/beta/story.js index baba19ae8..f4fe6c65d 100644 --- a/app/assets/javascripts/models/beta/story.js +++ b/app/assets/javascripts/models/beta/story.js @@ -9,6 +9,7 @@ import StoryPropTypesShape, { import moment from 'moment'; import { has } from 'underscore'; import * as History from './history'; +import { storiesWithScope } from '../../reducers/stories'; const compareValues = (a, b) => { if (a > b) return 1; @@ -24,8 +25,8 @@ export const needConfirmation = story => story.state === status.RELEASE; export const comparePosition = (a, b) => { - const positionA = Number(a.position); - const positionB = Number(b.position); + const positionA = Number(a.newPosition); + const positionB = Number(b.newPosition); return compareValues(positionA, positionB); }; @@ -138,6 +139,16 @@ export const update = async (story, projectId, options) => { return deserialize(data.story, options); }; +export const getHighestNewPosition = stories => { + const highestPositionValue = stories + .filter(story => story.newPosition !== null) + .reduce((acc, story) => { + return story.newPosition > acc ? story.newPosition : acc; + }, stories[0].newPosition); + + return highestPositionValue + 1; +}; + export const post = async (story, projectId) => { const serializedStory = serialize(story); @@ -469,3 +480,22 @@ export const donePoints = stories => export const remainingPoints = stories => totalPoints(stories) - donePoints(stories); + +export const sortOptimistically = (stories, newStory, from) => { + const scopeStories = storiesWithScope(stories, from); + const isChillyBinStory = isUnscheduled(newStory); + const targetStories = scopeStories.filter( + story => isUnscheduled(story) === isChillyBinStory + ); + + const storiesToUpdate = targetStories.filter( + story => + story.newPosition >= newStory.newPosition && story.id !== newStory.id + ); + const updatedPositions = storiesToUpdate.map(story => ({ + ...story, + newPosition: story.newPosition + 1, + })); + + return [...updatedPositions, newStory]; +}; From f7f01532df70f7748e18e639b64d4209df53ba67 Mon Sep 17 00:00:00 2001 From: Guilherme-NL Date: Thu, 9 Nov 2023 10:53:10 -0300 Subject: [PATCH 6/9] refactor: remove optimistically_update reducer and change sort_stories reducer name --- app/assets/javascripts/actions/actionTypes.js | 3 +-- app/assets/javascripts/actions/story.js | 27 +++++++++++-------- app/assets/javascripts/reducers/stories.js | 16 +---------- 3 files changed, 18 insertions(+), 28 deletions(-) diff --git a/app/assets/javascripts/actions/actionTypes.js b/app/assets/javascripts/actions/actionTypes.js index 1061f2a26..df547f88c 100644 --- a/app/assets/javascripts/actions/actionTypes.js +++ b/app/assets/javascripts/actions/actionTypes.js @@ -22,7 +22,7 @@ export default keyMirror({ RECEIVE_HISTORY_ERROR: null, CLOSE_HISTORY: null, UPDATE_STORY_SUCCESS: null, - SORT_STORIES_SUCCESS: null, + SORT_STORIES: null, STORY_FAILURE: null, SET_LOADING_STORY: null, ADD_TASK: null, @@ -45,5 +45,4 @@ export default keyMirror({ TOGGLE_COLUMN_VISIBILITY: null, REVERT_TO_CALCULATED_VELOCITY: null, SIMULATE_SPRINT_VELOCITY: null, - OPTIMISTICALLY_UPDATE: null, }); diff --git a/app/assets/javascripts/actions/story.js b/app/assets/javascripts/actions/story.js index 0cdc7080b..4ca9b8ea2 100644 --- a/app/assets/javascripts/actions/story.js +++ b/app/assets/javascripts/actions/story.js @@ -63,18 +63,12 @@ export const updateStorySuccess = (story, from) => ({ from, }); -export const sortStoriesSuccess = (stories, from) => ({ - type: actionTypes.SORT_STORIES_SUCCESS, +export const sortStories = (stories, from) => ({ + type: actionTypes.SORT_STORIES, stories, from, }); -export const optimisticallyUpdate = (story, from) => ({ - type: actionTypes.OPTIMISTICALLY_UPDATE, - story, - from, -}); - export const storyFailure = (id, error, from) => ({ type: actionTypes.STORY_FAILURE, id, @@ -227,13 +221,20 @@ export const dragDropStory = const newStory = Story.addNewAttributes(story, newAttributes); + const storiesWithUpdatedPositions = Story.sortOptimistically( + stories, + newStory, + from + ); + try { - dispatch(optimisticallyUpdate(newStory, from)); + // Optimistic Update: + dispatch(sortStories(storiesWithUpdatedPositions, from)); const updatedStories = await Story.updatePosition(newStory); await wait(300); - return dispatch(sortStoriesSuccess(updatedStories, from)); + return dispatch(sortStories(updatedStories, from)); } catch (error) { dispatch(sendErrorNotification(error)); return dispatch(storyFailure(story.id, error, from)); @@ -256,7 +257,11 @@ export const saveStory = Story.needConfirmation, { onConfirmed: async () => { - const newStory = await Story.post(story._editing, projectId); + const newPosition = Story.getHighestNewPosition( + storiesWithScope(stories, from) + ); + const storyWithNewPosition = { ...story._editing, newPosition }; + const newStory = await Story.post(storyWithNewPosition, projectId); dispatch( addStory({ story: newStory, id: story._editing.id }, from) ); diff --git a/app/assets/javascripts/reducers/stories.js b/app/assets/javascripts/reducers/stories.js index 1bc69ccf4..066fe879a 100644 --- a/app/assets/javascripts/reducers/stories.js +++ b/app/assets/javascripts/reducers/stories.js @@ -134,7 +134,7 @@ const storiesReducer = (state = initialState, action) => { ); }) ); - case actionTypes.SORT_STORIES_SUCCESS: + case actionTypes.SORT_STORIES: return normalizeAllScopes( allScopes(state, null, stories => { return stories.map(story => { @@ -152,20 +152,6 @@ const storiesReducer = (state = initialState, action) => { }); }) ); - case actionTypes.OPTIMISTICALLY_UPDATE: - return normalizeAllScopes( - allScopes(state, action.story.id, stories => { - return stories.map( - updateIfSameId(action.story.id, story => { - const newStory = setLoadingValue(action.story, true); - return addNewAttributes(story, { - ...newStory, - needsToSave: false, - }); - }) - ); - }) - ); case actionTypes.STORY_FAILURE: return { ...state, From 9ca5cfebbb932dff2a15fa1a7494e8788b594f28 Mon Sep 17 00:00:00 2001 From: Guilherme-NL Date: Thu, 9 Nov 2023 13:33:56 -0300 Subject: [PATCH 7/9] test: update tests --- app/assets/javascripts/actions/story.js | 5 +- app/assets/javascripts/models/beta/story.js | 9 +- spec/javascripts/actions/story_spec.js | 128 +++++++++++--------- spec/javascripts/models/beta/story_spec.js | 72 +++++++++-- spec/javascripts/selectors/backlog_spec.js | 4 +- 5 files changed, 139 insertions(+), 79 deletions(-) diff --git a/app/assets/javascripts/actions/story.js b/app/assets/javascripts/actions/story.js index 4ca9b8ea2..8ef359192 100644 --- a/app/assets/javascripts/actions/story.js +++ b/app/assets/javascripts/actions/story.js @@ -222,9 +222,8 @@ export const dragDropStory = const newStory = Story.addNewAttributes(story, newAttributes); const storiesWithUpdatedPositions = Story.sortOptimistically( - stories, - newStory, - from + storiesWithScope(stories, from), + newStory ); try { diff --git a/app/assets/javascripts/models/beta/story.js b/app/assets/javascripts/models/beta/story.js index f4fe6c65d..bf6add078 100644 --- a/app/assets/javascripts/models/beta/story.js +++ b/app/assets/javascripts/models/beta/story.js @@ -9,7 +9,6 @@ import StoryPropTypesShape, { import moment from 'moment'; import { has } from 'underscore'; import * as History from './history'; -import { storiesWithScope } from '../../reducers/stories'; const compareValues = (a, b) => { if (a > b) return 1; @@ -140,6 +139,9 @@ export const update = async (story, projectId, options) => { }; export const getHighestNewPosition = stories => { + if (stories.length === 1) { + return 1; + } const highestPositionValue = stories .filter(story => story.newPosition !== null) .reduce((acc, story) => { @@ -481,10 +483,9 @@ export const donePoints = stories => export const remainingPoints = stories => totalPoints(stories) - donePoints(stories); -export const sortOptimistically = (stories, newStory, from) => { - const scopeStories = storiesWithScope(stories, from); +export const sortOptimistically = (stories, newStory) => { const isChillyBinStory = isUnscheduled(newStory); - const targetStories = scopeStories.filter( + const targetStories = stories.filter( story => isUnscheduled(story) === isChillyBinStory ); diff --git a/spec/javascripts/actions/story_spec.js b/spec/javascripts/actions/story_spec.js index f8999c854..3978496ac 100644 --- a/spec/javascripts/actions/story_spec.js +++ b/spec/javascripts/actions/story_spec.js @@ -1,17 +1,17 @@ -import { sendDefaultErrorNotification } from "actions/notifications"; -import * as Story from "actions/story"; -import storyFactory from "../support/factories/storyFactory"; +import { sendDefaultErrorNotification } from 'actions/notifications'; +import * as Story from 'actions/story'; +import storyFactory from '../support/factories/storyFactory'; -jest.mock("../../../app/assets/javascripts/reducers/stories", () => ({ +jest.mock('../../../app/assets/javascripts/reducers/stories', () => ({ storiesWithScope: jest.fn(), })); -describe("Story Actions", () => { - describe("saveStory", () => { +describe('Story Actions', () => { + describe('saveStory', () => { const story = storyFactory(); const projectId = 42; - it("calls Story.update with story._editing and projectId", async () => { + it('calls Story.update with story._editing and projectId', async () => { const editedStory = { ...story, _editing: { @@ -44,7 +44,7 @@ describe("Story Actions", () => { ); }); - it("dispatch only toggleStory when _isDirty is false", async () => { + it('dispatch only toggleStory when _isDirty is false', async () => { const editedStory = { ...story, _editing: { @@ -78,7 +78,7 @@ describe("Story Actions", () => { ); }); - it("dispatch updateStorySuccess when _isDirty", async () => { + it('dispatch updateStorySuccess when _isDirty', async () => { const editedStory = { ...story, _editing: { @@ -110,7 +110,7 @@ describe("Story Actions", () => { ); }); - it("dispatch only addStory when isNew", async () => { + it('dispatch only addStory when isNew', async () => { const editedStory = { ...story, _editing: { @@ -123,6 +123,7 @@ describe("Story Actions", () => { post: sinon.stub().resolves(story), isNew: sinon.stub().returns(true), needConfirmation: sinon.stub().returns(false), + getHighestNewPosition: sinon.stub().returns(1), }; const fakeDispatch = sinon.stub().resolves({}); @@ -147,8 +148,8 @@ describe("Story Actions", () => { ); }); - it("dispatches storyFailure when promise fails", async () => { - const error = { error: "boom" }; + it('dispatches storyFailure when promise fails', async () => { + const error = { error: 'boom' }; const editedStory = { ...story, @@ -182,12 +183,12 @@ describe("Story Actions", () => { }); }); - describe("deleteStory", () => { + describe('deleteStory', () => { const storyId = 420; const projectId = 42; - const story = { id: storyId, title: "foo" }; + const story = { id: storyId, title: 'foo' }; - it("calls Story.deleteStory with projectId and storyId", async () => { + it('calls Story.deleteStory with projectId and storyId', async () => { const FakeStory = { findById: sinon.stub().returns(story), deleteStory: sinon.stub().resolves({}), @@ -206,7 +207,7 @@ describe("Story Actions", () => { expect(FakeStory.deleteStory).toHaveBeenCalledWith(storyId, projectId); }); - it("dispatch deleteStorySuccess", async () => { + it('dispatch deleteStorySuccess', async () => { const FakeStory = { findById: sinon.stub().returns(story), deleteStory: sinon.stub().resolves({}), @@ -225,8 +226,8 @@ describe("Story Actions", () => { ); }); - it("dispatches storyFailure when promise fails", async () => { - const error = { error: "boom" }; + it('dispatches storyFailure when promise fails', async () => { + const error = { error: 'boom' }; const FakeStory = { findById: sinon.stub().returns(story), @@ -247,8 +248,8 @@ describe("Story Actions", () => { }); }); - describe("highlight", () => { - it("always dispatch updateHighlight", () => { + describe('highlight', () => { + it('always dispatch updateHighlight', () => { const storyId = 1; const fakeDispatch = sinon.stub().resolves({}); const fakeGetState = sinon.stub().returns({}); @@ -261,7 +262,7 @@ describe("Story Actions", () => { }); }); - describe("dragDropStory", () => { + describe('dragDropStory', () => { const story = storyFactory(); const updatedStories = [ { ...story, newPosition: 4, id: 42 }, @@ -270,12 +271,13 @@ describe("Story Actions", () => { const updatedStory = { ...story, newPosition: 4 }; const from = 1; - it("calls Story.sortStoriesSuccess with new position", async () => { + it('calls Story.sortStories with new position', async () => { const FakeStory = { findById: sinon.stub().returns(updatedStory), updatePosition: sinon.stub().resolves(updatedStories), isNew: sinon.stub().returns(false), addNewAttributes: sinon.stub().returns(story), + sortOptimistically: sinon.stub().resolves(updatedStories), }; const fakeGetState = sinon.stub().returns({ stories: { all: updatedStories }, @@ -294,18 +296,19 @@ describe("Story Actions", () => { )(fakeDispatch, fakeGetState, { Story: FakeStory }); expect(fakeDispatch).toHaveBeenCalledWith( - Story.sortStoriesSuccess(updatedStories, from) + Story.sortStories(updatedStories, from) ); }); - describe("when promise fails", () => { - const error = { error: "boom" }; + describe('when promise fails', () => { + const error = { error: 'boom' }; const FakeStory = { findById: sinon.stub().returns(updatedStory), updatePosition: sinon.stub().rejects(error), isNew: sinon.stub().returns(false), addNewAttributes: sinon.stub().returns(story), + sortOptimistically: sinon.stub().returns(updatedStory), }; const fakeGetState = sinon.stub().returns({ @@ -320,28 +323,35 @@ describe("Story Actions", () => { })(fakeDispatch, fakeGetState, { Story: FakeStory }); }); - it("dispatches storyFailure", () => { + it('dispatches storyFailure', () => { expect(fakeDispatch).toHaveBeenCalledWith( Story.storyFailure(updatedStory.id, error) ); }); - it("do not dispatch sortStoriesSuccess", () => { - expect(fakeDispatch).not.toHaveBeenCalledWith( - Story.sortStoriesSuccess(updatedStory) - ); - }); + // it('do not dispatch sortStories', () => { + // // expect(fakeDispatch).not.toHaveBeenCalledWith( + // // Story.sortStories(updatedStory) + // // ); + // expect(fakeDispatch).toHaveBeenCalledWith( + // Story.sortStories(storiesWithUpdatedPositions, from) + // ); + + // expect(fakeDispatch).not.toHaveBeenCalledWith( + // Story.sortStories(updatedStory, from) + // ); + // }); }); }); - describe("confirmBeforeSaveIfNeeded", () => { + describe('confirmBeforeSaveIfNeeded', () => { let story; beforeEach(() => { story = storyFactory(); }); - describe("when do not need of confirmation", () => { + describe('when do not need of confirmation', () => { let needConfirmation; let confirm; @@ -350,8 +360,8 @@ describe("Story Actions", () => { confirm = sinon.stub(); }); - describe("and callback.onConfirmed do not throws an error", () => { - it("call callback.onConfirmed", async () => { + describe('and callback.onConfirmed do not throws an error', () => { + it('call callback.onConfirmed', async () => { const callback = { onConfirmed: sinon.stub() }; await Story.confirmBeforeSaveIfNeeded( @@ -364,12 +374,12 @@ describe("Story Actions", () => { }); }); - describe("and callback.onConfirmed throws an error", () => { + describe('and callback.onConfirmed throws an error', () => { let error; let callback; beforeEach(() => { - error = { error: "boom" }; + error = { error: 'boom' }; callback = { onConfirmed: sinon.stub().rejects(error), onError: sinon.stub(), @@ -377,7 +387,7 @@ describe("Story Actions", () => { }; }); - it("call callback.onConfirmed", async () => { + it('call callback.onConfirmed', async () => { await Story.confirmBeforeSaveIfNeeded( story, confirm, @@ -388,7 +398,7 @@ describe("Story Actions", () => { expect(callback.onConfirmed).toHaveBeenCalled(); }); - it("call callback.onError", async () => { + it('call callback.onError', async () => { await Story.confirmBeforeSaveIfNeeded( story, confirm, @@ -399,7 +409,7 @@ describe("Story Actions", () => { expect(callback.onError).toHaveBeenCalled(); }); - it("do not call callback.onCanceled", async () => { + it('do not call callback.onCanceled', async () => { await Story.confirmBeforeSaveIfNeeded( story, confirm, @@ -412,8 +422,8 @@ describe("Story Actions", () => { }); }); - describe("when need of confirmation", () => { - describe("and is not confirmed", () => { + describe('when need of confirmation', () => { + describe('and is not confirmed', () => { let callback; let needConfirmation; let confirm; @@ -424,7 +434,7 @@ describe("Story Actions", () => { confirm = sinon.stub().returns(false); }); - it("do not call callback.onConfirmed", async () => { + it('do not call callback.onConfirmed', async () => { await Story.confirmBeforeSaveIfNeeded( story, confirm, @@ -435,7 +445,7 @@ describe("Story Actions", () => { expect(callback.onConfirmed).not.toHaveBeenCalled(); }); - it("call callback.onCanceled", async () => { + it('call callback.onCanceled', async () => { await Story.confirmBeforeSaveIfNeeded( story, confirm, @@ -447,7 +457,7 @@ describe("Story Actions", () => { }); }); - describe("and is confirmed", () => { + describe('and is confirmed', () => { let needConfirmation; let confirm; @@ -456,8 +466,8 @@ describe("Story Actions", () => { confirm = sinon.stub().returns(true); }); - describe("and callback.onConfirmed do not throws an error", () => { - it("call callback.onConfirmed", async () => { + describe('and callback.onConfirmed do not throws an error', () => { + it('call callback.onConfirmed', async () => { const callback = { onConfirmed: sinon.stub() }; await Story.confirmBeforeSaveIfNeeded( @@ -470,12 +480,12 @@ describe("Story Actions", () => { }); }); - describe("and callback.onConfirmed throws an error", () => { + describe('and callback.onConfirmed throws an error', () => { let error; let callback; beforeEach(() => { - error = { error: "boom" }; + error = { error: 'boom' }; callback = { onConfirmed: sinon.stub().rejects(error), onError: sinon.stub(), @@ -483,7 +493,7 @@ describe("Story Actions", () => { }; }); - it("call callback.onConfirmed", async () => { + it('call callback.onConfirmed', async () => { await Story.confirmBeforeSaveIfNeeded( story, confirm, @@ -494,7 +504,7 @@ describe("Story Actions", () => { expect(callback.onConfirmed).toHaveBeenCalled(); }); - it("call callback.onError", async () => { + it('call callback.onError', async () => { await Story.confirmBeforeSaveIfNeeded( story, confirm, @@ -505,7 +515,7 @@ describe("Story Actions", () => { expect(callback.onError).toHaveBeenCalled(); }); - it("do not call callback.onCanceled", async () => { + it('do not call callback.onCanceled', async () => { await Story.confirmBeforeSaveIfNeeded( story, confirm, @@ -520,21 +530,21 @@ describe("Story Actions", () => { }); }); - describe("fetchEpic", () => { + describe('fetchEpic', () => { let stories; let fakeGetState; let fakeDispatch; - const label = "label"; + const label = 'label'; beforeEach(() => { fakeGetState = jest.fn(() => ({ - projectBoard: { projectId: "test-project" }, + projectBoard: { projectId: 'test-project' }, })); stories = Array(3).fill(storyFactory()); fakeDispatch = jest.fn(); }); - describe("when does not throws an error", () => { + describe('when does not throws an error', () => { let fakeStory; beforeEach(() => { @@ -543,17 +553,17 @@ describe("Story Actions", () => { }; }); - it("dispatch receiveStories with stories in epic scope", async () => { + it('dispatch receiveStories with stories in epic scope', async () => { await Story.fetchEpic(label)(fakeDispatch, fakeGetState, { Story: fakeStory, }); expect(fakeDispatch).toHaveBeenCalledWith( - Story.receiveStories(stories, "epic") + Story.receiveStories(stories, 'epic') ); }); - it("does not dispatch sendDefaultErrorNotification", async () => { + it('does not dispatch sendDefaultErrorNotification', async () => { await Story.fetchEpic(label)(fakeDispatch, fakeGetState, { Story: fakeStory, }); diff --git a/spec/javascripts/models/beta/story_spec.js b/spec/javascripts/models/beta/story_spec.js index bd77e28af..4dfa8e353 100644 --- a/spec/javascripts/models/beta/story_spec.js +++ b/spec/javascripts/models/beta/story_spec.js @@ -1,24 +1,18 @@ import * as Story from 'models/beta/story'; import moment from 'moment'; import { status, storyTypes, storyScopes } from 'libs/beta/constants'; -import { - createTemporaryId, - denormalizeState, - denormalizeStories, - normalizeState, - normalizeStories, -} from '../../../../app/assets/javascripts/models/beta/story'; +import { createTemporaryId } from '../../../../app/assets/javascripts/models/beta/story'; describe('Story model', function () { describe('comparePosition', () => { describe('when position A is bigger then B', () => { it('return 1', () => { const storyA = { - position: 10, + newPosition: 10, }; const storyB = { - position: 4, + newPosition: 4, }; expect(Story.comparePosition(storyA, storyB)).toEqual(1); @@ -28,11 +22,11 @@ describe('Story model', function () { describe('when position A is lower then B', () => { it('return -1', () => { const storyA = { - position: 3, + newPosition: 3, }; const storyB = { - position: 5, + newPosition: 5, }; expect(Story.comparePosition(storyA, storyB)).toEqual(-1); @@ -1476,4 +1470,60 @@ describe('Story model', function () { }); }); }); + describe('sortOptimistically', () => { + const stories = [ + { id: 1, newPosition: 3, state: 'unscheduled' }, + { id: 2, newPosition: 2, state: 'unscheduled' }, + { id: 3, newPosition: 1, state: 'unscheduled' }, + { id: 4, newPosition: 1, state: 'started' }, + { id: 5, newPosition: 2, state: 'started' }, + ]; + + it('sorts stories optimistically when status is unscheduled', () => { + const newStory = { id: 100, newPosition: 1, state: 'unscheduled' }; + + const expectedResponse = [ + { id: 1, newPosition: 4, state: 'unscheduled' }, + { id: 2, newPosition: 3, state: 'unscheduled' }, + { id: 3, newPosition: 2, state: 'unscheduled' }, + { id: 100, newPosition: 1, state: 'unscheduled' }, + ]; + + expect(Story.sortOptimistically(stories, newStory)).toEqual( + expectedResponse + ); + }); + it('sorts stories optimistically when status is not unscheduled', () => { + const newStory = { id: 100, newPosition: 1, state: 'unstarted' }; + + const expectedResponse = [ + { id: 4, newPosition: 2, state: 'started' }, + { id: 5, newPosition: 3, state: 'started' }, + { id: 100, newPosition: 1, state: 'unstarted' }, + ]; + + expect(Story.sortOptimistically(stories, newStory)).toEqual( + expectedResponse + ); + }); + }); + describe('getHighestNewPosition', () => { + it('returns 1 when there is only the new story', () => { + const stories = [{ id: 1, newPosition: null }]; + + expect(Story.getHighestNewPosition(stories)).toBe(1); + }); + + it('returns the highest new position value + 1', () => { + const stories = [ + { id: 1, newPosition: 2 }, + { id: 2, newPosition: 5 }, + { id: 3, newPosition: 3 }, + { id: 4, newPosition: null }, + { id: 5, newPosition: 1 }, + ]; + + expect(Story.getHighestNewPosition(stories)).toBe(6); + }); + }); }); diff --git a/spec/javascripts/selectors/backlog_spec.js b/spec/javascripts/selectors/backlog_spec.js index d20679876..c2b231237 100644 --- a/spec/javascripts/selectors/backlog_spec.js +++ b/spec/javascripts/selectors/backlog_spec.js @@ -132,13 +132,13 @@ describe('Backlog selector functions', () => { storyStates.forEach(state => { const firstPosition = storyFactory({ id: 1, - position: 1, + newPosition: 1, state, }); const secondPosition = storyFactory({ id: 0, - position: 2, + newPosition: 2, state, }); From 857203c8f2ba11e16c790072c842def1a6e0347b Mon Sep 17 00:00:00 2001 From: Guilherme-NL Date: Fri, 10 Nov 2023 08:46:21 -0300 Subject: [PATCH 8/9] test: remove commented test --- spec/javascripts/actions/story_spec.js | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/spec/javascripts/actions/story_spec.js b/spec/javascripts/actions/story_spec.js index 3978496ac..d6ec16f4d 100644 --- a/spec/javascripts/actions/story_spec.js +++ b/spec/javascripts/actions/story_spec.js @@ -328,19 +328,6 @@ describe('Story Actions', () => { Story.storyFailure(updatedStory.id, error) ); }); - - // it('do not dispatch sortStories', () => { - // // expect(fakeDispatch).not.toHaveBeenCalledWith( - // // Story.sortStories(updatedStory) - // // ); - // expect(fakeDispatch).toHaveBeenCalledWith( - // Story.sortStories(storiesWithUpdatedPositions, from) - // ); - - // expect(fakeDispatch).not.toHaveBeenCalledWith( - // Story.sortStories(updatedStory, from) - // ); - // }); }); }); From c66259504e5e7ac240d4948135e74243695de75e Mon Sep 17 00:00:00 2001 From: Guilherme-NL Date: Fri, 17 Nov 2023 09:13:19 -0300 Subject: [PATCH 9/9] refactor: remove reduce and add Math.max to find the highest position --- app/assets/javascripts/models/beta/story.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/models/beta/story.js b/app/assets/javascripts/models/beta/story.js index bf6add078..d171169f0 100644 --- a/app/assets/javascripts/models/beta/story.js +++ b/app/assets/javascripts/models/beta/story.js @@ -142,11 +142,8 @@ export const getHighestNewPosition = stories => { if (stories.length === 1) { return 1; } - const highestPositionValue = stories - .filter(story => story.newPosition !== null) - .reduce((acc, story) => { - return story.newPosition > acc ? story.newPosition : acc; - }, stories[0].newPosition); + const storiesNewPosition = stories.map(story => story.newPosition); + const highestPositionValue = Math.max(...storiesNewPosition); return highestPositionValue + 1; };