Skip to content

Commit 5b20ecd

Browse files
authored
Merge pull request #642 from codex-team/feat/events-multiselect-bulk-actions
Feat/events multiselect bulk actions
2 parents 4eb4c0f + 9a0fc08 commit 5b20ecd

6 files changed

Lines changed: 89 additions & 20 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "hawk.api",
3-
"version": "1.5.0",
3+
"version": "1.5.1",
44
"main": "index.ts",
55
"license": "BUSL-1.1",
66
"scripts": {

src/models/eventsFactory.js

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -926,19 +926,19 @@ class EventsFactory extends Factory {
926926
}
927927

928928
/**
929-
* Bulk mark for resolved / ignored (not the same as per-event toggleEventMark).
929+
* Bulk mark for resolved / ignored / starred (not the same as per-event toggleEventMark).
930930
* - If every found event already has the mark: remove it from all (bulk "undo").
931931
* - Otherwise: set the mark on every found event that does not have it yet (never remove
932932
* from a subset when the selection is mixed).
933-
* Only 'resolved' and 'ignored' are allowed for bulk.
933+
* Only 'resolved', 'ignored' and 'starred' are allowed for bulk.
934934
*
935935
* @param {string[]} eventIds - original event ids
936-
* @param {string} mark - 'resolved' | 'ignored'
937-
* @returns {Promise<{ updatedCount: number, failedEventIds: string[] }>}
936+
* @param {string} mark - 'resolved' | 'ignored' | 'starred'
937+
* @returns {Promise<{ updatedCount: number, updatedEventIds: string[], failedEventIds: string[] }>}
938938
*/
939939
async bulkToggleEventMark(eventIds, mark) {
940-
if (mark !== 'resolved' && mark !== 'ignored') {
941-
throw new Error(`bulkToggleEventMark: mark must be resolved or ignored, got ${mark}`);
940+
if (mark !== 'resolved' && mark !== 'ignored' && mark !== 'starred') {
941+
throw new Error(`bulkToggleEventMark: mark must be resolved, ignored or starred, got ${mark}`);
942942
}
943943

944944
const max = EventsFactory.BULK_TOGGLE_EVENT_MARK_MAX;
@@ -962,6 +962,7 @@ class EventsFactory extends Factory {
962962
if (validObjectIds.length === 0) {
963963
return {
964964
updatedCount: 0,
965+
updatedEventIds: [],
965966
failedEventIds,
966967
};
967968
}
@@ -982,6 +983,7 @@ class EventsFactory extends Factory {
982983
const markKey = `marks.${mark}`;
983984
const allHaveMark = found.length > 0 && found.every(doc => doc.marks && doc.marks[mark]);
984985
const ops = [];
986+
const updatedEventIds = [];
985987

986988
for (const doc of found) {
987989
const hasMark = doc.marks && doc.marks[mark];
@@ -1001,11 +1003,13 @@ class EventsFactory extends Factory {
10011003
update,
10021004
},
10031005
});
1006+
updatedEventIds.push(doc._id.toString());
10041007
}
10051008

10061009
if (ops.length === 0) {
10071010
return {
10081011
updatedCount: 0,
1012+
updatedEventIds: [],
10091013
failedEventIds,
10101014
};
10111015
}
@@ -1014,6 +1018,7 @@ class EventsFactory extends Factory {
10141018

10151019
return {
10161020
updatedCount: bulkResult.modifiedCount + bulkResult.upsertedCount,
1021+
updatedEventIds,
10171022
failedEventIds,
10181023
};
10191024
}

src/resolvers/event.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,8 +167,8 @@ module.exports = {
167167
* @return {Promise<{ updatedCount: number, failedEventIds: string[] }>}
168168
*/
169169
async bulkToggleEventMarks(_obj, { projectId, eventIds, mark }, context) {
170-
if (mark !== 'resolved' && mark !== 'ignored') {
171-
throw new UserInputError('bulkToggleEventMarks supports only resolved and ignored marks');
170+
if (mark !== 'resolved' && mark !== 'ignored' && mark !== 'starred') {
171+
throw new UserInputError('bulkToggleEventMarks supports only resolved, ignored and starred marks');
172172
}
173173

174174
if (!eventIds || !eventIds.length) {

src/typeDefs/event.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,11 @@ type BulkToggleEventMarksResult {
461461
"""
462462
updatedCount: Int!
463463
464+
"""
465+
Original event ids actually toggled in this operation
466+
"""
467+
updatedEventIds: [ID!]!
468+
464469
"""
465470
Event ids that were not updated (invalid id or not found)
466471
"""
@@ -520,7 +525,7 @@ extend type Mutation {
520525
): Boolean!
521526
522527
"""
523-
Toggle the same mark on many original events at once (only resolved or ignored).
528+
Toggle the same mark on many original events at once (resolved, ignored or starred).
524529
Same toggle semantics as toggleEventMark per event.
525530
"""
526531
bulkToggleEventMarks(
@@ -535,7 +540,7 @@ extend type Mutation {
535540
eventIds: [ID!]!
536541
537542
"""
538-
Mark (resolved or ignored only): if every selected event already has it, clear it for all;
543+
Mark (resolved, ignored or starred): if every selected event already has it, clear it for all;
539544
otherwise set it on every selected event that does not have it yet.
540545
"""
541546
mark: EventMark!

test/models/eventsFactory-bulk-toggle.test.ts

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,43 @@ describe('EventsFactory.bulkToggleEventMark', () => {
4949
});
5050
});
5151

52-
it('should throw when mark is not resolved or ignored', async () => {
52+
it('should throw when mark is unsupported', async () => {
5353
const factory = new EventsFactory(projectId);
5454

55-
await expect(factory.bulkToggleEventMark([], 'starred' as any)).rejects.toThrow(
56-
'bulkToggleEventMark: mark must be resolved or ignored'
55+
await expect(factory.bulkToggleEventMark([], 'some-unknown-mark' as any)).rejects.toThrow(
56+
'bulkToggleEventMark: mark must be resolved, ignored or starred'
57+
);
58+
});
59+
60+
it('should support starred mark', async () => {
61+
const factory = new EventsFactory(projectId);
62+
const id = new ObjectId();
63+
64+
collectionMock.find.mockReturnValue({
65+
toArray: () =>
66+
Promise.resolve([
67+
{
68+
_id: id,
69+
marks: {},
70+
},
71+
]),
72+
});
73+
collectionMock.bulkWrite.mockResolvedValue({
74+
modifiedCount: 1,
75+
upsertedCount: 0,
76+
});
77+
78+
const result = await factory.bulkToggleEventMark([ id.toString() ], 'starred');
79+
80+
expect(result.updatedCount).toBe(1);
81+
expect(result.updatedEventIds).toEqual([ id.toString() ]);
82+
const ops = collectionMock.bulkWrite.mock.calls[0][0];
83+
84+
expect(ops).toHaveLength(1);
85+
expect(ops[0].updateOne.update).toEqual(
86+
expect.objectContaining({
87+
$set: { 'marks.starred': expect.any(Number) },
88+
})
5789
);
5890
});
5991

@@ -99,6 +131,7 @@ describe('EventsFactory.bulkToggleEventMark', () => {
99131
const result = await factory.bulkToggleEventMark([ 'not-a-valid-id' ], 'resolved');
100132

101133
expect(result.updatedCount).toBe(0);
134+
expect(result.updatedEventIds).toEqual([]);
102135
expect(result.failedEventIds).toContain('not-a-valid-id');
103136
expect(collectionMock.bulkWrite).not.toHaveBeenCalled();
104137
});
@@ -114,6 +147,7 @@ describe('EventsFactory.bulkToggleEventMark', () => {
114147
const result = await factory.bulkToggleEventMark([ missing.toString() ], 'ignored');
115148

116149
expect(result.updatedCount).toBe(0);
150+
expect(result.updatedEventIds).toEqual([]);
117151
expect(result.failedEventIds).toContain(missing.toString());
118152
expect(collectionMock.bulkWrite).not.toHaveBeenCalled();
119153
});
@@ -138,6 +172,7 @@ describe('EventsFactory.bulkToggleEventMark', () => {
138172
const result = await factory.bulkToggleEventMark([ a.toString(), b.toString() ], 'ignored');
139173

140174
expect(result.updatedCount).toBe(1);
175+
expect(result.updatedEventIds).toEqual([ b.toString() ]);
141176
const ops = collectionMock.bulkWrite.mock.calls[0][0];
142177

143178
expect(ops).toHaveLength(1);
@@ -169,6 +204,7 @@ describe('EventsFactory.bulkToggleEventMark', () => {
169204
const result = await factory.bulkToggleEventMark([ a.toString(), b.toString() ], 'resolved');
170205

171206
expect(result.updatedCount).toBe(2);
207+
expect(result.updatedEventIds).toEqual([ a.toString(), b.toString() ]);
172208
const ops = collectionMock.bulkWrite.mock.calls[0][0];
173209

174210
expect(ops).toHaveLength(2);
@@ -196,6 +232,7 @@ describe('EventsFactory.bulkToggleEventMark', () => {
196232
const result = await factory.bulkToggleEventMark([ a.toString(), b.toString() ], 'ignored');
197233

198234
expect(result.updatedCount).toBe(1);
235+
expect(result.updatedEventIds).toEqual([ b.toString() ]);
199236
const ops = collectionMock.bulkWrite.mock.calls[0][0];
200237

201238
expect(ops).toHaveLength(1);

test/resolvers/event-bulk-toggle-marks.test.ts

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const eventResolvers = require('../../src/resolvers/event') as {
1515
o: unknown,
1616
args: { projectId: string; eventIds: string[]; mark: string },
1717
ctx: unknown
18-
) => Promise<{ updatedCount: number; failedEventIds: string[] }>;
18+
) => Promise<{ updatedCount: number; updatedEventIds: string[]; failedEventIds: string[] }>;
1919
};
2020
};
2121

@@ -29,22 +29,22 @@ describe('Mutation.bulkToggleEventMarks', () => {
2929
(getEventsFactory as unknown as jest.Mock).mockReturnValue({ bulkToggleEventMark });
3030
});
3131

32-
it('should throw when mark is not resolved or ignored', async () => {
32+
it('should throw when mark is not supported', async () => {
3333
await expect(
3434
eventResolvers.Mutation.bulkToggleEventMarks(
3535
{},
36-
{ projectId: 'p1', eventIds: [ '507f1f77bcf86cd799439012' ], mark: 'starred' },
36+
{ projectId: 'p1', eventIds: [ '507f1f77bcf86cd799439012' ], mark: 'some-unknown-mark' },
3737
ctx
3838
)
3939
).rejects.toThrow(UserInputError);
4040

4141
await expect(
4242
eventResolvers.Mutation.bulkToggleEventMarks(
4343
{},
44-
{ projectId: 'p1', eventIds: [ '507f1f77bcf86cd799439012' ], mark: 'starred' },
44+
{ projectId: 'p1', eventIds: [ '507f1f77bcf86cd799439012' ], mark: 'some-unknown-mark' },
4545
ctx
4646
)
47-
).rejects.toThrow('bulkToggleEventMarks supports only resolved and ignored marks');
47+
).rejects.toThrow('bulkToggleEventMarks supports only resolved, ignored and starred marks');
4848

4949
expect(bulkToggleEventMark).not.toHaveBeenCalled();
5050
});
@@ -62,7 +62,7 @@ describe('Mutation.bulkToggleEventMarks', () => {
6262
});
6363

6464
it('should call factory with original event ids and return its result', async () => {
65-
const payload = { updatedCount: 2, failedEventIds: [ 'x' ] };
65+
const payload = { updatedCount: 2, updatedEventIds: [ 'a', 'b' ], failedEventIds: [ 'x' ] };
6666

6767
bulkToggleEventMark.mockResolvedValue(payload);
6868

@@ -84,6 +84,28 @@ describe('Mutation.bulkToggleEventMarks', () => {
8484
expect(result).toEqual(payload);
8585
});
8686

87+
it('should allow starred mark for bulk toggle', async () => {
88+
const payload = { updatedCount: 1, updatedEventIds: [ '507f1f77bcf86cd799439011' ], failedEventIds: [] };
89+
90+
bulkToggleEventMark.mockResolvedValue(payload);
91+
92+
const result = await eventResolvers.Mutation.bulkToggleEventMarks(
93+
{},
94+
{
95+
projectId: 'p1',
96+
eventIds: [ '507f1f77bcf86cd799439011' ],
97+
mark: 'starred',
98+
},
99+
ctx
100+
);
101+
102+
expect(bulkToggleEventMark).toHaveBeenCalledWith(
103+
[ '507f1f77bcf86cd799439011' ],
104+
'starred'
105+
);
106+
expect(result).toEqual(payload);
107+
});
108+
87109
it('should map factory max-length error to UserInputError', async () => {
88110
bulkToggleEventMark.mockRejectedValue(
89111
new Error('bulkToggleEventMark: at most 100 event ids allowed')

0 commit comments

Comments
 (0)