Skip to content

Commit e263516

Browse files
authored
Merge pull request #643 from codex-team/feat/events-multiselect-bulk-actions
Feat/events multiselect bulk actions
2 parents 5b20ecd + ccd4a7e commit e263516

18 files changed

Lines changed: 844 additions & 163 deletions

jest.config.js

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,6 @@ module.exports = {
3737
moduleNameMapper: {
3838
'^node:crypto$': '<rootDir>/test/__mocks__/node_crypto.js',
3939
'^node:util$': '<rootDir>/test/__mocks__/node_util.js',
40-
/**
41-
* demoWorkspace is TypeScript; CommonJS resolvers use require() without extension
42-
*/
43-
'^.+/constants/demoWorkspace$': '<rootDir>/src/constants/demoWorkspace.ts',
4440
},
4541

4642
/**

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.1",
3+
"version": "1.5.2",
44
"main": "index.ts",
55
"license": "BUSL-1.1",
66
"scripts": {

src/constants/demoWorkspace.ts

Lines changed: 0 additions & 5 deletions
This file was deleted.

src/integrations/github/routes.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import ProjectModel from '../../models/project';
1111
import WorkspaceModel from '../../models/workspace';
1212
import { sgr, Effect } from '../../utils/ansi';
1313
import { databases } from '../../mongo';
14-
import { DEMO_WORKSPACE_ID } from '../../constants/demoWorkspace';
1514

1615
/**
1716
* Default task threshold for automatic task creation
@@ -109,7 +108,7 @@ export function createGitHubRouter(factories: ContextFactories): express.Router
109108
/**
110109
* Check if project is demo project (cannot be modified)
111110
*/
112-
if (project.workspaceId.toString() === DEMO_WORKSPACE_ID) {
111+
if (project.workspaceId.toString() === '6213b6a01e6281087467cc7a') {
113112
res.status(400).json({ error: 'Unable to update demo project' });
114113

115114
return null;

src/models/eventsFactory.js

Lines changed: 109 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -882,6 +882,48 @@ class EventsFactory extends Factory {
882882
return result;
883883
}
884884

885+
/**
886+
* Mark many original events as visited for passed user
887+
*
888+
* @param {string[]} eventIds - original event ids
889+
* @param {string|ObjectId} userId - id of the user who is visiting events
890+
* @returns {Promise<{ updatedCount: number, updatedEventIds: string[], failedEventIds: string[] }>}
891+
*/
892+
async bulkVisitEvent(eventIds, userId) {
893+
const {
894+
collection,
895+
found,
896+
failedEventIds,
897+
} = await this._resolveBulkEventsByIds(eventIds);
898+
const userIdStr = String(userId);
899+
900+
const docsToUpdate = found.filter((doc) => {
901+
const visitedBy = Array.isArray(doc.visitedBy) ? doc.visitedBy : [];
902+
903+
return !visitedBy.some((visitedUserId) => String(visitedUserId) === userIdStr);
904+
});
905+
const updatedEventIds = docsToUpdate.map(doc => doc._id.toString());
906+
907+
if (docsToUpdate.length === 0) {
908+
return {
909+
updatedCount: 0,
910+
updatedEventIds: [],
911+
failedEventIds,
912+
};
913+
}
914+
915+
const updateManyResult = await collection.updateMany(
916+
{ _id: { $in: docsToUpdate.map(doc => doc._id) } },
917+
{ $addToSet: { visitedBy: new ObjectId(userId) } }
918+
);
919+
920+
return {
921+
updatedCount: updateManyResult.modifiedCount,
922+
updatedEventIds,
923+
failedEventIds,
924+
};
925+
}
926+
885927
/**
886928
* Mark or unmark event as Resolved, Ignored or Starred
887929
*
@@ -919,65 +961,18 @@ class EventsFactory extends Factory {
919961
}
920962

921963
/**
922-
* Max original event ids per bulkToggleEventMark request
923-
*/
924-
static get BULK_TOGGLE_EVENT_MARK_MAX() {
925-
return 100;
926-
}
927-
928-
/**
929-
* Bulk mark for resolved / ignored / starred (not the same as per-event toggleEventMark).
930-
* - If every found event already has the mark: remove it from all (bulk "undo").
931-
* - Otherwise: set the mark on every found event that does not have it yet (never remove
932-
* from a subset when the selection is mixed).
933-
* Only 'resolved', 'ignored' and 'starred' are allowed for bulk.
964+
* Bulk toggle mark for original events.
934965
*
935966
* @param {string[]} eventIds - original event ids
936967
* @param {string} mark - 'resolved' | 'ignored' | 'starred'
937968
* @returns {Promise<{ updatedCount: number, updatedEventIds: string[], failedEventIds: string[] }>}
938969
*/
939970
async bulkToggleEventMark(eventIds, mark) {
940-
if (mark !== 'resolved' && mark !== 'ignored' && mark !== 'starred') {
941-
throw new Error(`bulkToggleEventMark: mark must be resolved, ignored or starred, got ${mark}`);
942-
}
943-
944-
const max = EventsFactory.BULK_TOGGLE_EVENT_MARK_MAX;
945-
const unique = [ ...new Set((eventIds || []).map(id => String(id))) ];
946-
947-
if (unique.length > max) {
948-
throw new Error(`bulkToggleEventMark: at most ${max} event ids allowed`);
949-
}
950-
951-
const failedEventIds = [];
952-
const validObjectIds = [];
953-
954-
for (const id of unique) {
955-
if (!ObjectId.isValid(id)) {
956-
failedEventIds.push(id);
957-
} else {
958-
validObjectIds.push(new ObjectId(id));
959-
}
960-
}
961-
962-
if (validObjectIds.length === 0) {
963-
return {
964-
updatedCount: 0,
965-
updatedEventIds: [],
966-
failedEventIds,
967-
};
968-
}
969-
970-
const collection = this.getCollection(this.TYPES.EVENTS);
971-
const found = await collection.find({ _id: { $in: validObjectIds } }).toArray();
972-
const foundByIdStr = new Map(found.map(doc => [doc._id.toString(), doc]));
973-
974-
for (const oid of validObjectIds) {
975-
const idStr = oid.toString();
976-
977-
if (!foundByIdStr.has(idStr)) {
978-
failedEventIds.push(idStr);
979-
}
980-
}
971+
const {
972+
collection,
973+
found,
974+
failedEventIds,
975+
} = await this._resolveBulkEventsByIds(eventIds);
981976

982977
const nowSec = Math.floor(Date.now() / 1000);
983978
const markKey = `marks.${mark}`;
@@ -1023,6 +1018,44 @@ class EventsFactory extends Factory {
10231018
};
10241019
}
10251020

1021+
/**
1022+
* Bulk set/clear assignee for many original events.
1023+
*
1024+
* @param {string[]} eventIds - original event ids
1025+
* @param {string|null|undefined} assignee - target assignee id, null/undefined to clear
1026+
* @returns {Promise<{ updatedCount: number, updatedEventIds: string[], failedEventIds: string[] }>}
1027+
*/
1028+
async bulkUpdateAssignee(eventIds, assignee) {
1029+
const {
1030+
collection,
1031+
found,
1032+
failedEventIds,
1033+
} = await this._resolveBulkEventsByIds(eventIds);
1034+
1035+
const normalizedAssignee = assignee ? String(assignee) : '';
1036+
const docsToUpdate = found.filter(doc => String(doc.assignee || '') !== normalizedAssignee);
1037+
const updatedEventIds = docsToUpdate.map(doc => doc._id.toString());
1038+
1039+
if (docsToUpdate.length === 0) {
1040+
return {
1041+
updatedCount: 0,
1042+
updatedEventIds: [],
1043+
failedEventIds,
1044+
};
1045+
}
1046+
1047+
const updateManyResult = await collection.updateMany(
1048+
{ _id: { $in: docsToUpdate.map(doc => doc._id) } },
1049+
{ $set: { assignee: normalizedAssignee } }
1050+
);
1051+
1052+
return {
1053+
updatedCount: updateManyResult.modifiedCount,
1054+
updatedEventIds,
1055+
failedEventIds,
1056+
};
1057+
}
1058+
10261059
/**
10271060
* Remove all project events
10281061
*
@@ -1071,6 +1104,29 @@ class EventsFactory extends Factory {
10711104
return result;
10721105
}
10731106

1107+
/**
1108+
* Resolve original events for bulk operations and collect not found ids.
1109+
*
1110+
* @param {string[]} eventIds - original event ids
1111+
* @returns {Promise<{ collection: any, found: any[], failedEventIds: string[] }>}
1112+
*/
1113+
async _resolveBulkEventsByIds(eventIds) {
1114+
const unique = [ ...new Set((eventIds || []).map(id => String(id))) ];
1115+
const objectIds = unique.map(id => new ObjectId(id));
1116+
const collection = this.getCollection(this.TYPES.EVENTS);
1117+
const found = await collection.find({ _id: { $in: objectIds } }).toArray();
1118+
const foundByIdStr = new Set(found.map(doc => doc._id.toString()));
1119+
const failedEventIds = objectIds
1120+
.map(id => id.toString())
1121+
.filter(id => !foundByIdStr.has(id));
1122+
1123+
return {
1124+
collection,
1125+
found,
1126+
failedEventIds,
1127+
};
1128+
}
1129+
10741130
/**
10751131
* Compose event with repetition
10761132
*

0 commit comments

Comments
 (0)