Skip to content

Commit 1b1e655

Browse files
committed
Merge branch 'staging' of https://github.com/3drepo/3drepo.io into ISSUE_5188
2 parents ee21bf3 + 216aa77 commit 1b1e655

File tree

148 files changed

+13079
-9347
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

148 files changed

+13079
-9347
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ matrix:
88
submodules: false
99
depth: 1
1010
node_js:
11-
- "18.12.1"
11+
- "18.20.4"
1212
sudo: true
1313
addons:
1414
apt:

backend/VERSION.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
{ "VERSION" : "5.12.1",
1+
{ "VERSION" : "5.13.1",
22
"unity" : {
33
"current" : "2.20.0",
44
"supported": []

backend/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "3drepo.io",
3-
"version": "5.12.1",
3+
"version": "5.13.1",
44
"engines": {
55
"node": "18.x.x"
66
},
@@ -43,7 +43,7 @@
4343
"countries-and-timezones": "3.6.0",
4444
"crypto-js": "4.2.0",
4545
"cryptolens": "1.0.1-4.2",
46-
"csv-parse": "4.8.5",
46+
"csv-parse": "4.16.3",
4747
"dayjs": "1.11.11",
4848
"device": "0.3.12",
4949
"ejs": "3.1.10",
@@ -67,7 +67,7 @@
6767
"pug": "3.0.3",
6868
"serialize-javascript": "4.0.0",
6969
"serve-favicon": "2.5.0",
70-
"sharp": "0.32.6",
70+
"sharp": "0.33.5",
7171
"slash": "3.0.0",
7272
"socket.io": "4.8.0",
7373
"string-to-stream": "3.0.1",

backend/src/scripts/utility/scheduler.config.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
{
22
"daily": [
33
{ "name": "cleanUpSharedDir" },
4-
{ "name": "removeUnverifiedUsers" }
4+
{ "name": "removeUnverifiedUsers" },
5+
{ "name": "sendDailyDigest" }
56
],
67
"weekly": [
78
{"name": "removeIncompleteRevisions"}
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
/**
2+
* Copyright (C) 2024 3D Repo Ltd
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU Affero General Public License as
6+
* published by the Free Software Foundation, either version 3 of the
7+
* License, or (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU Affero General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU Affero General Public License
15+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
18+
const Path = require('path');
19+
const { getTeamspaceList } = require('../../utils');
20+
const { v5Path } = require('../../../interop');
21+
22+
const { composeDailyDigests } = require(`${v5Path}/models/notifications`);
23+
const { notificationTypes } = require(`${v5Path}/models/notifications.constants`);
24+
const { getAddOns } = require(`${v5Path}/models/teamspaceSettings`);
25+
const { ADD_ONS } = require(`${v5Path}/models/teamspaces.constants`);
26+
const { getTicketsByQuery } = require(`${v5Path}/models/tickets`);
27+
const { getProjectList } = require(`${v5Path}/models/projectSettings`);
28+
const { getAllTemplates } = require(`${v5Path}/models/tickets.templates`);
29+
const { findModels } = require(`${v5Path}/models/modelSettings`);
30+
const { getUsersByQuery } = require(`${v5Path}/models/users`);
31+
32+
const { logger } = require(`${v5Path}/utils/logger`);
33+
const { UUIDToString } = require(`${v5Path}/utils/helper/uuids`);
34+
const { uniqueElements } = require(`${v5Path}/utils/helper/arrays`);
35+
36+
const { sendEmail } = require(`${v5Path}/services/mailer`);
37+
const { templates } = require(`${v5Path}/services/mailer/mailer.constants`);
38+
39+
// this processes the list of project/model/ticket ids into their names
40+
const getContextDataLookUp = async (contextData) => {
41+
const dataLookUp = {};
42+
43+
await Promise.all(contextData.map(async ({ _id: teamspace, data }) => {
44+
dataLookUp[teamspace] = { projects: {}, models: {}, tickets: {} };
45+
46+
const [ticketTemplates, projectsData, modelsData] = await Promise.all([
47+
getAllTemplates(teamspace, true, { code: 1, _id: 1 }),
48+
getProjectList(teamspace, { name: 1 }),
49+
findModels(teamspace, {}, { name: 1 }),
50+
]);
51+
52+
const templateIdToCode = {};
53+
54+
ticketTemplates.forEach(({ _id, code }) => {
55+
const idStr = UUIDToString(_id);
56+
templateIdToCode[idStr] = code;
57+
});
58+
59+
projectsData.forEach(({ _id, name }) => {
60+
const idStr = UUIDToString(_id);
61+
dataLookUp[teamspace].projects[idStr] = name;
62+
});
63+
64+
modelsData.forEach(({ _id, name }) => {
65+
const idStr = UUIDToString(_id);
66+
dataLookUp[teamspace].models[idStr] = name;
67+
});
68+
69+
const ticketProcessingProm = data.map(async ({ project, model, tickets }) => {
70+
const ticketsData = await getTicketsByQuery(
71+
teamspace, project, model, { _id: { $in: tickets } }, { type: 1, number: 1 });
72+
73+
ticketsData.forEach(({ _id, number, type }) => {
74+
const code = templateIdToCode[UUIDToString(type)];
75+
if (code) dataLookUp[teamspace].tickets[UUIDToString(_id)] = `${code}:${number}`;
76+
});
77+
});
78+
await Promise.all(ticketProcessingProm);
79+
}));
80+
81+
return dataLookUp;
82+
};
83+
84+
const getUserDetails = async (users) => {
85+
const usersData = await getUsersByQuery({ user: { $in: users } }, { 'customData.email': 1, 'customData.firstName': 1, user: 1 });
86+
87+
const userLUT = {};
88+
89+
usersData.forEach(({ user, customData: { email, firstName } }) => {
90+
userLUT[user] = { email, firstName };
91+
});
92+
93+
return userLUT;
94+
};
95+
96+
const generateEmails = (data, dataRef, usersToUserInfo) => Promise.all(
97+
data.map(async ({ _id: { teamspace, user }, data: modelList }) => {
98+
const userInfo = usersToUserInfo[user];
99+
const tsData = dataRef[teamspace];
100+
101+
if (!userInfo || !tsData) return;
102+
const notifications = modelList.flatMap(({ model: modelID, project: projectID, data: notifData }) => {
103+
const modelIDStr = UUIDToString(modelID);
104+
const projectIDStr = UUIDToString(projectID);
105+
const model = tsData.models[modelIDStr];
106+
const project = tsData.projects[projectIDStr];
107+
108+
if (!model || !project) return [];
109+
110+
const tickets = {};
111+
const uri = `/v5/viewer/${teamspace}/${projectIDStr}/${modelIDStr}`;
112+
113+
notifData.forEach(({ type, tickets: ticketsArr, count }) => {
114+
const ticketCodes = uniqueElements(ticketsArr.flatMap(
115+
(ticketId) => tsData.tickets[(UUIDToString(ticketId))] ?? []));
116+
if (!ticketCodes.length) return;
117+
const ticketData = { count, link: `${uri}?ticketSearch=${ticketCodes.join(',')}` };
118+
switch (type) {
119+
case notificationTypes.TICKET_UPDATED:
120+
tickets.updated = ticketData;
121+
break;
122+
case notificationTypes.TICKET_CLOSED:
123+
tickets.closed = ticketData;
124+
tickets.closed.link = `${tickets.closed.link}&ticketCompleted=true`;
125+
break;
126+
case notificationTypes.TICKET_ASSIGNED:
127+
tickets.assigned = ticketData;
128+
break;
129+
default:
130+
logger.logInfo(`Unrecognised notification type ${type}, ignoring...`);
131+
}
132+
});
133+
134+
return Object.keys(tickets).length ? { project, model, tickets } : [];
135+
});
136+
137+
if (notifications.length) {
138+
const emailData = {
139+
username: user,
140+
firstName: userInfo.firstName,
141+
teamspace,
142+
notifications,
143+
};
144+
145+
logger.logInfo(`Sending email to ${user} for ${teamspace}`);
146+
await sendEmail(templates.DAILY_DIGEST.name, userInfo.email, emailData);
147+
}
148+
}));
149+
150+
const run = async (teamspace) => {
151+
const teamspaces = teamspace ? [teamspace] : await getTeamspaceList();
152+
const teamspacesWithDDEnabled = await Promise.all(teamspaces.map(async (ts) => {
153+
const addOns = await getAddOns(ts);
154+
return addOns[ADD_ONS.DAILY_DIGEST] ? ts : undefined;
155+
}));
156+
157+
const teamspacesToProcess = teamspacesWithDDEnabled.filter((ts) => !!ts);
158+
159+
if (teamspacesToProcess?.length) {
160+
const { contextData, recipients, digestData } = await composeDailyDigests(teamspacesToProcess);
161+
const [
162+
dataLookUp, usersToUserInfo,
163+
] = await Promise.all([
164+
getContextDataLookUp(contextData),
165+
getUserDetails(recipients),
166+
]);
167+
168+
await generateEmails(digestData, dataLookUp, usersToUserInfo);
169+
}
170+
};
171+
172+
const genYargs = /* istanbul ignore next */(yargs) => {
173+
const commandName = Path.basename(__filename, Path.extname(__filename));
174+
const argsSpec = (subYargs) => subYargs.option('teamspace',
175+
{
176+
describe: 'teamspace to send notifications for',
177+
type: 'string',
178+
});
179+
return yargs.command(commandName,
180+
'Send daily digests to any users subscribed',
181+
argsSpec,
182+
({ teamspace }) => run(teamspace));
183+
};
184+
185+
module.exports = {
186+
run,
187+
genYargs,
188+
};

backend/src/scripts/utility/teamspaces/updateAddOns.js

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ const { logger } = require(`${v5Path}/utils/logger`);
2222

2323
const { getAddOns, removeAddOns, updateAddOns } = require(`${v5Path}/models/teamspaceSettings`);
2424
const { deleteIfUndefined } = require(`${v5Path}/utils/helper/objects`);
25-
const { ADD_ONS_MODULES } = require(`${v5Path}/models/teamspaces.constants`);
25+
const { ADD_ONS, ADD_ONS_MODULES } = require(`${v5Path}/models/teamspaces.constants`);
2626

27-
const run = async (teamspace, vrEnabled, srcEnabled, hereEnabled, powerBIEnabled, modulesString, removeAll) => {
27+
const run = async (teamspace, removeAll, addOnsConfigured) => {
2828
const addOns = await getAddOns(teamspace);
2929
logger.logInfo(`${teamspace} currently has the following addOns(s): ${JSON.stringify(addOns)}`);
3030

@@ -33,21 +33,18 @@ const run = async (teamspace, vrEnabled, srcEnabled, hereEnabled, powerBIEnabled
3333
} else {
3434
let modules;
3535

36-
if (modulesString === 'null') {
36+
if (addOnsConfigured[ADD_ONS.MODULES] === 'null') {
3737
modules = null;
38-
} else if (modulesString) {
39-
modules = modulesString?.split(',');
38+
} else if (addOnsConfigured[ADD_ONS.MODULES]) {
39+
modules = addOnsConfigured[ADD_ONS.MODULES].split(',');
4040

4141
if (!modules.every((m) => Object.values(ADD_ONS_MODULES).includes(m))) {
4242
throw new Error(`Modules must be one of the following: ${Object.values(ADD_ONS_MODULES)}`);
4343
}
4444
}
4545

4646
const toUpdate = deleteIfUndefined({
47-
vrEnabled,
48-
hereEnabled,
49-
srcEnabled,
50-
powerBIEnabled,
47+
...addOnsConfigured,
5148
modules,
5249
});
5350

@@ -64,26 +61,31 @@ const run = async (teamspace, vrEnabled, srcEnabled, hereEnabled, powerBIEnabled
6461

6562
const genYargs = /* istanbul ignore next */(yargs) => {
6663
const commandName = Path.basename(__filename, Path.extname(__filename));
67-
const argsSpec = (subYargs) => subYargs.option('vrEnabled',
64+
const argsSpec = (subYargs) => subYargs.option(ADD_ONS.VR,
6865
{
6966
describe: 'Enable VR support',
7067
type: 'boolean',
71-
}).option('srcEnabled',
68+
}).option(ADD_ONS.SRC,
7269
{
7370
describe: 'Enable SRC (unreal) support',
7471
type: 'boolean',
75-
}).option('hereEnabled',
72+
}).option(ADD_ONS.HERE,
7673
{
7774
describe: 'Enable HERE maps support',
7875
type: 'boolean',
79-
}).option('powerBIEnabled',
76+
}).option(ADD_ONS.POWERBI,
8077
{
8178
describe: 'Enable PowerBI support',
8279
type: 'boolean',
8380
})
84-
.option('modules',
81+
.option(ADD_ONS.DAILY_DIGEST,
8582
{
86-
describe: 'Comma seperated string of enabled modules',
83+
describe: 'Enable daily email digest',
84+
type: 'boolean',
85+
})
86+
.option(ADD_ONS.MODULES,
87+
{
88+
describe: 'Comma seperated string of enabled modules (null to remove all):',
8789
type: 'string',
8890
})
8991
.option('teamspace',
@@ -101,8 +103,7 @@ const genYargs = /* istanbul ignore next */(yargs) => {
101103
return yargs.command(commandName,
102104
'Update addOns configurations on a teamspace',
103105
argsSpec,
104-
(argv) => run(argv.teamspace,
105-
argv.vrEnabled, argv.srcEnabled, argv.hereEnabled, argv.powerBIEnabled, argv.modules, argv.removeAll));
106+
({ teamspace, removeAll, ...addOns }) => run(teamspace, removeAll, addOns));
106107
};
107108

108109
module.exports = {

backend/src/v4/models/notification.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,6 @@ module.exports = {
443443
criteria._id = utils.stringToUUID(criteria._id);
444444
}
445445

446-
return db.find(INTERNAL_DB, NOTIFICATIONS_COLL, { user, ...criteria }, undefined, {timestamp: -1}).then(fillModelData);
446+
return db.find(INTERNAL_DB, NOTIFICATIONS_COLL, { user, type: {$in: Object.values(types)}, ...criteria }, undefined, {timestamp: -1}).then(fillModelData);
447447
}
448448
};

backend/src/v4/models/shapes.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ const shapeSchema = yup.object().shape({
3131
"normals": yup.array().of(coordinatesSchema),
3232
"value": yup.number().min(0),
3333
"color": colorSchema.required(),
34-
"type": yup.mixed().oneOf([0, 1, 2]).required(),
34+
"type": yup.mixed().oneOf([0, 1, 2, 3]).required(),
3535
"name": yup.string()
3636
}).noUnknown();
3737

0 commit comments

Comments
 (0)