@crowdin/app-project-module 0.91.1 → 0.93.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,7 +16,7 @@ const types_1 = require("../types");
16
16
  const storage_1 = require("../../../storage");
17
17
  function handle(integration) {
18
18
  return (0, util_1.runAsyncWrapper)((req, res) => __awaiter(this, void 0, void 0, function* () {
19
- var _a, _b, _c, _d, _e, _f, _g, _h;
19
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
20
20
  const { parent_id: parentId, search, page } = req.query;
21
21
  req.logInfo('Received request to get integration data');
22
22
  let message;
@@ -34,15 +34,18 @@ function handle(integration) {
34
34
  else {
35
35
  files = result || [];
36
36
  }
37
- files = (0, files_1.skipFilesByRegex)(files, integration.skipIntegrationNodes);
37
+ if ((appSettings === null || appSettings === void 0 ? void 0 : appSettings.skipIntegrationNodesToggle) === true ||
38
+ ((appSettings === null || appSettings === void 0 ? void 0 : appSettings.skipIntegrationNodesToggle) === undefined && ((_a = integration.skipIntegrationNodesToggle) === null || _a === void 0 ? void 0 : _a.value))) {
39
+ files = (0, files_1.skipFilesByRegex)(files, integration.skipIntegrationNodes);
40
+ }
38
41
  if (integration.filterByPathIntegrationFiles) {
39
- const includePatterns = (_a = appSettings === null || appSettings === void 0 ? void 0 : appSettings.includeByFilePath) === null || _a === void 0 ? void 0 : _a.split('\n').filter(Boolean);
40
- const excludePatterns = (_b = appSettings === null || appSettings === void 0 ? void 0 : appSettings.excludeByFilePath) === null || _b === void 0 ? void 0 : _b.split('\n').filter(Boolean);
42
+ const includePatterns = (_b = appSettings === null || appSettings === void 0 ? void 0 : appSettings.includeByFilePath) === null || _b === void 0 ? void 0 : _b.split('\n').filter(Boolean);
43
+ const excludePatterns = (_c = appSettings === null || appSettings === void 0 ? void 0 : appSettings.excludeByFilePath) === null || _c === void 0 ? void 0 : _c.split('\n').filter(Boolean);
41
44
  files = (0, files_1.filterFilesByPath)(files, includePatterns, excludePatterns);
42
45
  }
43
- if (((_d = (_c = integration.filtering) === null || _c === void 0 ? void 0 : _c.integrationFileStatus) === null || _d === void 0 ? void 0 : _d.isNew) ||
44
- ((_f = (_e = integration.filtering) === null || _e === void 0 ? void 0 : _e.integrationFileStatus) === null || _f === void 0 ? void 0 : _f.isUpdated) ||
45
- ((_h = (_g = integration.filtering) === null || _g === void 0 ? void 0 : _g.integrationFileStatus) === null || _h === void 0 ? void 0 : _h.notSynced)) {
46
+ if (((_e = (_d = integration.filtering) === null || _d === void 0 ? void 0 : _d.integrationFileStatus) === null || _e === void 0 ? void 0 : _e.isNew) ||
47
+ ((_g = (_f = integration.filtering) === null || _f === void 0 ? void 0 : _f.integrationFileStatus) === null || _g === void 0 ? void 0 : _g.isUpdated) ||
48
+ ((_j = (_h = integration.filtering) === null || _h === void 0 ? void 0 : _h.integrationFileStatus) === null || _j === void 0 ? void 0 : _j.notSynced)) {
46
49
  const syncedData = yield (0, storage_1.getStorage)().getSyncedData(clientId, crowdinId, types_1.Provider.INTEGRATION);
47
50
  const syncedFiles = (syncedData === null || syncedData === void 0 ? void 0 : syncedData.files) ? JSON.parse(syncedData.files) : [];
48
51
  const lastSyncTimestamp = (syncedData === null || syncedData === void 0 ? void 0 : syncedData.updatedAt) ? Number(syncedData.updatedAt) : null;
@@ -10,270 +10,21 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  const util_1 = require("../../../util");
13
- const types_1 = require("../../../types");
14
13
  const storage_1 = require("../../../storage");
15
- const users_1 = require("./users");
16
- const CONSTANTS = {
17
- MAX_EMAIL_LENGTH: 76,
18
- MANAGER_ROLES: ['owner', 'manager'],
19
- PROJECT_INTEGRATIONS_MODULE_TYPE: 'project-integrations',
20
- };
21
14
  function handle() {
22
15
  return (0, util_1.runAsyncWrapper)((req, res) => __awaiter(this, void 0, void 0, function* () {
23
- const onlyCheck = req.body.onlyCheck;
24
- const usersToInvite = req.body.users;
25
- if (!Array.isArray(usersToInvite)) {
26
- return res.status(400).send('Invalid request');
27
- }
28
- const client = req.crowdinApiClient;
29
- const projectId = req.crowdinContext.jwtPayload.context.project_id;
30
- const userId = req.crowdinContext.jwtPayload.context.user_id;
31
- const { projectMembers, organizationMembers } = yield (0, users_1.getUsers)({ client, projectId });
32
- const integrationCredentials = yield (0, storage_1.getStorage)().getIntegrationCredentials(req.crowdinContext.clientId);
33
- let applicationInstallation = null;
34
- try {
35
- applicationInstallation = yield client.applicationsApi.getApplicationInstallation(req.crowdinContext.appIdentifier);
36
- }
37
- catch (error) {
38
- if (error.code !== 403) {
39
- console.error('Failed to get application installation', error);
40
- }
41
- }
42
- const isAdmin = organizationMembers.some((member) => member.id === +userId && member.isAdmin) ||
43
- projectMembers.some((member) => member.id === +userId && 'role' in member && member.role === 'owner');
44
- const hasManagerAccess = isAdmin ||
45
- projectMembers.some((member) => member.id === +userId &&
46
- ('role' in member ? CONSTANTS.MANAGER_ROLES.includes(member.role) : member.isManager));
47
- if (!hasManagerAccess) {
48
- return res.status(403).send({ error: 'Access denied' });
49
- }
50
- const inviteRestricted = !isAdmin &&
51
- 'invite_restrict_enabled' in req.crowdinContext.jwtPayload.context &&
52
- !!req.crowdinContext.jwtPayload.context.invite_restrict_enabled;
53
- const usersWhoWillBeInvitedToOrganization = filterOrganizationUsers(usersToInvite);
54
- const usersWhoWillBeInvitedToProject = filterProjectUsers({
55
- inviteRestricted,
56
- usersToInvite,
57
- projectMembers,
58
- organizationMembers,
59
- });
60
- const usersWhoNotIssetInApplicationInstallation = filterNotIssetApplicationUsers({
61
- inviteRestricted,
62
- usersToInvite,
63
- applicationInstallation,
64
- projectMembers,
65
- organizationMembers,
66
- });
67
- const usersWhoNotIssetInIntegration = filterNotIssetIntegrationUsers({
68
- inviteRestricted,
69
- usersToInvite,
70
- integrationCredentials,
71
- projectMembers,
72
- organizationMembers,
73
- });
74
- if (onlyCheck) {
16
+ const clientId = req.crowdinContext.clientId;
17
+ const users = req.body.users;
18
+ if (!users) {
75
19
  return res.send({
76
- data: {
77
- isAdmin,
78
- inviteRestricted,
79
- editApplicationAvailable: applicationInstallation !== null,
80
- usersWhoWillBeInvitedToOrganization,
81
- usersWhoWillBeInvitedToProject,
82
- usersWhoNotIssetInApplicationInstallation,
83
- usersWhoNotIssetInIntegration,
84
- },
20
+ success: false,
21
+ message: 'No users provided',
85
22
  });
86
23
  }
87
- const response = yield inviteUsers({
88
- req,
89
- projectId,
90
- isAdmin,
91
- inviteRestricted,
92
- usersToInvite,
93
- applicationInstallation,
94
- usersWhoWillBeInvitedToOrganization,
95
- usersWhoWillBeInvitedToProject,
96
- usersWhoNotIssetInApplicationInstallation,
24
+ yield (0, storage_1.getStorage)().updateIntegrationManagers(clientId, JSON.stringify(users.map((user) => `${user.id}`)));
25
+ return res.send({
26
+ success: true,
97
27
  });
98
- return res.send(response);
99
28
  }));
100
29
  }
101
30
  exports.default = handle;
102
- function filterOrganizationUsers(usersToInvite) {
103
- return usersToInvite.filter((identifier) => (0, util_1.validateEmail)(identifier)).map((name) => ({ name: `${name}` }));
104
- }
105
- function filterProjectUsers({ inviteRestricted, usersToInvite, projectMembers, organizationMembers, }) {
106
- return usersToInvite
107
- .map((identifier) => {
108
- if ((0, util_1.validateEmail)(identifier) && !inviteRestricted) {
109
- return { name: `${identifier}` };
110
- }
111
- const user = projectMembers.find((member) => member.id === +identifier);
112
- if (!user) {
113
- const organizationUser = organizationMembers.find((member) => member.id === +identifier);
114
- return organizationUser && !organizationUser.isAdmin
115
- ? { id: organizationUser.id, name: (0, users_1.getUserFullName)(organizationUser) }
116
- : null;
117
- }
118
- return ('role' in user ? !CONSTANTS.MANAGER_ROLES.includes(user.role) : !user.isManager)
119
- ? { id: user.id, name: (0, users_1.getUserFullName)(user) }
120
- : null;
121
- })
122
- .filter(Boolean);
123
- }
124
- function filterNotIssetApplicationUsers({ inviteRestricted, usersToInvite, applicationInstallation, projectMembers, organizationMembers, }) {
125
- let userIdentifiers = [];
126
- if (!applicationInstallation) {
127
- return [];
128
- }
129
- const projectIntegrationModules = applicationInstallation.data.modules.filter((module) => module.type === CONSTANTS.PROJECT_INTEGRATIONS_MODULE_TYPE);
130
- if (!projectIntegrationModules.length) {
131
- return [];
132
- }
133
- if (projectIntegrationModules.some((module) => module.permissions.user.value === types_1.UserPermissions.ALL_MEMBERS)) {
134
- return [];
135
- }
136
- else {
137
- userIdentifiers = usersToInvite.filter((userId) => projectIntegrationModules.every((module) => !module.permissions.user.ids.includes(+userId)));
138
- }
139
- return userIdentifiers
140
- .map((identifier) => {
141
- if ((0, util_1.validateEmail)(identifier) && !inviteRestricted) {
142
- return { name: `${identifier}` };
143
- }
144
- let user;
145
- user = projectMembers.find((member) => member.id === +identifier);
146
- if (!user) {
147
- user = organizationMembers.find((member) => member.id === +identifier);
148
- }
149
- return user ? { id: user.id, name: (0, users_1.getUserFullName)(user) } : null;
150
- })
151
- .filter(Boolean);
152
- }
153
- function filterNotIssetIntegrationUsers({ inviteRestricted, usersToInvite, integrationCredentials, projectMembers, organizationMembers, }) {
154
- let integrationManagers = [];
155
- if (integrationCredentials === null || integrationCredentials === void 0 ? void 0 : integrationCredentials.managers) {
156
- integrationManagers = JSON.parse(integrationCredentials.managers);
157
- }
158
- return usersToInvite
159
- .map((identifier) => {
160
- if (integrationManagers.includes(`${identifier}`)) {
161
- return null;
162
- }
163
- if ((0, util_1.validateEmail)(identifier) && !inviteRestricted) {
164
- return { name: `${identifier}` };
165
- }
166
- let user;
167
- user = projectMembers.find((member) => member.id === +identifier);
168
- if (!user) {
169
- user = organizationMembers.find((member) => member.id === +identifier);
170
- }
171
- return user ? { id: user.id, name: (0, users_1.getUserFullName)(user) } : null;
172
- })
173
- .filter(Boolean);
174
- }
175
- function inviteUsers({ req, projectId, isAdmin, inviteRestricted, usersToInvite, applicationInstallation, usersWhoWillBeInvitedToOrganization, usersWhoWillBeInvitedToProject, usersWhoNotIssetInApplicationInstallation, }) {
176
- return __awaiter(this, void 0, void 0, function* () {
177
- const client = req.crowdinApiClient;
178
- if (inviteRestricted) {
179
- usersWhoWillBeInvitedToOrganization = [];
180
- }
181
- const alreadyAddedUserIds = yield inviteUsersToProject({
182
- client,
183
- projectId,
184
- usersWhoWillBeInvitedToOrganization,
185
- usersWhoWillBeInvitedToProject,
186
- });
187
- if (isAdmin) {
188
- yield addUsersToApplicationInstallation({
189
- client,
190
- ownerId: req.integrationCredentials.ownerId,
191
- applicationInstallation,
192
- alreadyAddedUserIds,
193
- usersWhoNotIssetInApplicationInstallation,
194
- });
195
- }
196
- yield addUsersToIntegration({
197
- clientId: req.crowdinContext.clientId,
198
- alreadyAddedUserIds,
199
- usersToInvite,
200
- });
201
- return { success: true };
202
- });
203
- }
204
- function inviteUsersToProject({ client, projectId, usersWhoWillBeInvitedToOrganization, usersWhoWillBeInvitedToProject, }) {
205
- return __awaiter(this, void 0, void 0, function* () {
206
- let addedIds = [];
207
- const emailInvites = usersWhoWillBeInvitedToOrganization.map((user) => user.name);
208
- const userIdInvites = usersWhoWillBeInvitedToProject.map((user) => user.id).filter(Boolean);
209
- try {
210
- if (emailInvites.length) {
211
- const inviteResponse = (yield client.usersApi.addProjectMember(projectId, {
212
- managerAccess: true,
213
- emails: emailInvites,
214
- })); // TODO: fix typings in the @crowdin/crowdin-api-client
215
- addedIds = inviteResponse.data.added.map((member) => member.id);
216
- }
217
- }
218
- catch (error) {
219
- console.error('Failed to invite users', error);
220
- }
221
- try {
222
- if (userIdInvites.length) {
223
- yield client.usersApi.addProjectMember(projectId, {
224
- managerAccess: true,
225
- userIds: userIdInvites,
226
- }); // TODO: fix typings in the @crowdin/crowdin-api-client
227
- }
228
- }
229
- catch (error) {
230
- console.error('Failed to grant project manager access', error);
231
- }
232
- return addedIds;
233
- });
234
- }
235
- function addUsersToApplicationInstallation({ client, ownerId, applicationInstallation, alreadyAddedUserIds, usersWhoNotIssetInApplicationInstallation, }) {
236
- return __awaiter(this, void 0, void 0, function* () {
237
- if (!applicationInstallation || !usersWhoNotIssetInApplicationInstallation.length) {
238
- return;
239
- }
240
- const applicationInvites = [
241
- ownerId,
242
- ...alreadyAddedUserIds,
243
- ...usersWhoNotIssetInApplicationInstallation.map((user) => user.id).filter(Boolean),
244
- ];
245
- const permissions = applicationInstallation.data.modules.filter((module) => module.type === CONSTANTS.PROJECT_INTEGRATIONS_MODULE_TYPE);
246
- if (!permissions.length) {
247
- return;
248
- }
249
- try {
250
- for (const module of permissions) {
251
- if (module.permissions.user.value === types_1.UserPermissions.ALL_MEMBERS) {
252
- continue;
253
- }
254
- const userIds = [...new Set([...module.permissions.user.ids, ...applicationInvites])];
255
- client.applicationsApi.editApplicationInstallation(applicationInstallation.data.identifier, [
256
- {
257
- op: 'replace',
258
- path: `/modules/${module.key}/permissions`,
259
- value: {
260
- user: {
261
- value: types_1.UserPermissions.RESTRICTED,
262
- ids: userIds,
263
- },
264
- },
265
- },
266
- ]);
267
- }
268
- }
269
- catch (error) {
270
- console.error('Failed to update application permissions', error);
271
- }
272
- });
273
- }
274
- function addUsersToIntegration({ clientId, alreadyAddedUserIds, usersToInvite, }) {
275
- return __awaiter(this, void 0, void 0, function* () {
276
- const integrationInvites = [...alreadyAddedUserIds, ...usersToInvite.filter((identifier) => +identifier)];
277
- yield (0, storage_1.getStorage)().updateIntegrationManagers(clientId, JSON.stringify(integrationInvites.map((id) => `${id}`)));
278
- });
279
- }
@@ -1,27 +1,4 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || function (mod) {
19
- if (mod && mod.__esModule) return mod;
20
- var result = {};
21
- if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
- __setModuleDefault(result, mod);
23
- return result;
24
- };
25
2
  var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
26
3
  function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
27
4
  return new (P || (P = Promise))(function (resolve, reject) {
@@ -36,7 +13,7 @@ const util_1 = require("../../../util");
36
13
  const logger_1 = require("../../../util/logger");
37
14
  const subscription_1 = require("../../../util/subscription");
38
15
  const defaults_1 = require("../util/defaults");
39
- const crowdinAppFunctions = __importStar(require("@crowdin/crowdin-apps-functions"));
16
+ const users_1 = require("./users");
40
17
  function handle(config, integration) {
41
18
  return (0, util_1.runAsyncWrapper)((req, res) => __awaiter(this, void 0, void 0, function* () {
42
19
  var _a, _b, _c, _d;
@@ -64,21 +41,29 @@ function handle(config, integration) {
64
41
  options.oauthMode = integration.oauthLogin.mode;
65
42
  }
66
43
  }
67
- else if (integration.getConfiguration) {
68
- const { userId } = crowdinAppFunctions.parseCrowdinId(req.crowdinContext.clientId);
44
+ else {
45
+ if (integration.getConfiguration) {
46
+ options.config = JSON.stringify(req.integrationSettings || {});
47
+ options.reloadOnConfigSave = !!integration.reloadOnConfigSave;
48
+ options.integrationPagination = integration.integrationPagination;
49
+ if ((_b = req.query) === null || _b === void 0 ? void 0 : _b.parentUrl) {
50
+ const parentUrl = new URL(req.query.parentUrl);
51
+ parentUrl.searchParams.set('zen-mode', 'true');
52
+ options.zenModeUrl = parentUrl.toString();
53
+ }
54
+ const configurationFields = yield integration.getConfiguration(req.crowdinContext.jwtPayload.context.project_id, req.crowdinApiClient, req.integrationCredentials);
55
+ options.configurationFields = (0, defaults_1.groupFieldsByCategory)(configurationFields);
56
+ logger(`Adding configuration fields ${JSON.stringify(options.configurationFields, null, 2)}`);
57
+ }
69
58
  options.hasOrganization = !!req.crowdinContext.jwtPayload.domain;
59
+ const userId = req.crowdinContext.jwtPayload.context.user_id;
70
60
  options.isOwner = req.integrationCredentials.ownerId === userId;
71
- options.config = JSON.stringify(req.integrationSettings || {});
72
- options.reloadOnConfigSave = !!integration.reloadOnConfigSave;
73
- options.integrationPagination = integration.integrationPagination;
74
- if ((_b = req.query) === null || _b === void 0 ? void 0 : _b.parentUrl) {
75
- const parentUrl = new URL(req.query.parentUrl);
76
- parentUrl.searchParams.set('zen-mode', 'true');
77
- options.zenModeUrl = parentUrl.toString();
78
- }
79
- const configurationFields = yield integration.getConfiguration(req.crowdinContext.jwtPayload.context.project_id, req.crowdinApiClient, req.integrationCredentials);
80
- options.configurationFields = (0, defaults_1.groupFieldsByCategory)(configurationFields);
81
- logger(`Adding configuration fields ${JSON.stringify(options.configurationFields, null, 2)}`);
61
+ options.userId = userId;
62
+ options.isManager = yield (0, users_1.isManager)({
63
+ client: req.crowdinApiClient,
64
+ projectId: req.crowdinContext.jwtPayload.context.project_id,
65
+ memberId: userId,
66
+ });
82
67
  }
83
68
  options.infoModal = integration.infoModal;
84
69
  options.syncNewElements = integration.syncNewElements;
@@ -25,6 +25,21 @@ function handle(config, integration) {
25
25
  },
26
26
  });
27
27
  }
28
+ if (integration.validateSettings) {
29
+ const validationResult = yield integration.validateSettings({
30
+ client: req.crowdinApiClient,
31
+ credentials: req.integrationCredentials,
32
+ appSettings,
33
+ });
34
+ if (validationResult && Object.keys(validationResult).length > 0) {
35
+ return res.status(400).json({
36
+ error: {
37
+ type: 'validation_error',
38
+ details: validationResult,
39
+ },
40
+ });
41
+ }
42
+ }
28
43
  const clientId = req.crowdinContext.clientId;
29
44
  req.logInfo(`Saving settings ${JSON.stringify(appSettings, null, 2)}`);
30
45
  const integrationConfig = yield (0, storage_1.getStorage)().getIntegrationConfig(clientId);
@@ -3,11 +3,8 @@ import Crowdin, { UsersModel } from '@crowdin/crowdin-api-client';
3
3
  import { Response } from 'express';
4
4
  export type ProjectMember = UsersModel.ProjectMember | UsersModel.EnterpriseProjectMember;
5
5
  export default function handle(): (req: import("../../../types").CrowdinClientRequest | import("express").Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>, res: Response<any, Record<string, any>>, next: Function) => void;
6
- export declare function getUsers({ client, projectId }: {
6
+ export declare function isManager({ client, projectId, memberId, }: {
7
7
  client: Crowdin;
8
8
  projectId: number;
9
- }): Promise<{
10
- projectMembers: ProjectMember[];
11
- organizationMembers: UsersModel.User[];
12
- }>;
13
- export declare function getUserFullName(user: UsersModel.User | ProjectMember): string;
9
+ memberId: number;
10
+ }): Promise<{}>;
@@ -1,27 +1,4 @@
1
1
  "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || function (mod) {
19
- if (mod && mod.__esModule) return mod;
20
- var result = {};
21
- if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
- __setModuleDefault(result, mod);
23
- return result;
24
- };
25
2
  var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
26
3
  function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
27
4
  return new (P || (P = Promise))(function (resolve, reject) {
@@ -32,32 +9,12 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
32
9
  });
33
10
  };
34
11
  Object.defineProperty(exports, "__esModule", { value: true });
35
- exports.getUserFullName = exports.getUsers = void 0;
12
+ exports.isManager = void 0;
36
13
  const util_1 = require("../../../util");
37
14
  const storage_1 = require("../../../storage");
38
- const crowdinAppFunctions = __importStar(require("@crowdin/crowdin-apps-functions"));
15
+ const MANAGER_ROLES = ['owner', 'manager'];
39
16
  function handle() {
40
17
  return (0, util_1.runAsyncWrapper)((req, res) => __awaiter(this, void 0, void 0, function* () {
41
- const client = req.crowdinApiClient;
42
- const ownerId = req.integrationCredentials.ownerId;
43
- const { projectId } = crowdinAppFunctions.parseCrowdinId(req.crowdinContext.clientId);
44
- const { projectMembers, organizationMembers } = yield getUsers({ client, projectId });
45
- const uniqueUsers = new Map();
46
- [...projectMembers, ...organizationMembers].forEach((manager) => {
47
- if (manager.id !== ownerId && !uniqueUsers.has(manager.id)) {
48
- uniqueUsers.set(manager.id, manager);
49
- }
50
- });
51
- const sortedUsers = Array.from(uniqueUsers.values())
52
- .sort((a, b) => {
53
- const aValue = a.firstName || a.username || '';
54
- const bValue = b.firstName || b.username || '';
55
- return aValue.localeCompare(bValue);
56
- })
57
- .map((user) => ({
58
- id: user.id.toString(),
59
- name: getUserFullName(user),
60
- }));
61
18
  let integrationManagers = [];
62
19
  const integrationCredentials = yield (0, storage_1.getStorage)().getIntegrationCredentials(req.crowdinContext.clientId);
63
20
  if (integrationCredentials === null || integrationCredentials === void 0 ? void 0 : integrationCredentials.managers) {
@@ -65,38 +22,18 @@ function handle() {
65
22
  }
66
23
  res.json({
67
24
  data: {
68
- users: sortedUsers,
69
25
  managers: integrationManagers,
70
26
  },
71
27
  });
72
28
  }));
73
29
  }
74
30
  exports.default = handle;
75
- function getUsers({ client, projectId }) {
31
+ function isManager({ client, projectId, memberId, }) {
76
32
  return __awaiter(this, void 0, void 0, function* () {
77
- const users = {
78
- projectMembers: [],
79
- organizationMembers: [],
80
- };
81
- const projectMembers = (yield client.usersApi.withFetchAll().listProjectMembers(projectId)).data;
82
- users.projectMembers = projectMembers.map((members) => members.data);
83
- if (client.organization) {
84
- try {
85
- const admins = (yield client.usersApi.withFetchAll().listUsers()).data;
86
- users.organizationMembers = admins.map((admin) => admin.data);
87
- }
88
- catch (error) {
89
- console.error('Failed to get organization users', error);
90
- }
91
- }
92
- return users;
33
+ const member = (yield client.usersApi.getProjectMemberPermissions(projectId, memberId)).data;
34
+ const isAdmin = 'isAdmin' in member ? member.isAdmin : 'role' in member && member.role === 'owner';
35
+ const isManager = ('role' in member && MANAGER_ROLES.includes(member.role)) || ('isManager' in member && member.isManager);
36
+ return isAdmin || isManager;
93
37
  });
94
38
  }
95
- exports.getUsers = getUsers;
96
- function getUserFullName(user) {
97
- const ownerFullName = 'fullName' in user ? user.fullName : ((user.firstName || '') + ' ' + (user.lastName || '')).trim();
98
- return !!ownerFullName && (user === null || user === void 0 ? void 0 : user.username) !== ownerFullName
99
- ? `${ownerFullName} (${user === null || user === void 0 ? void 0 : user.username})`
100
- : user === null || user === void 0 ? void 0 : user.username;
101
- }
102
- exports.getUserFullName = getUserFullName;
39
+ exports.isManager = isManager;
@@ -19,6 +19,16 @@ export interface IntegrationLogic extends ModuleKey {
19
19
  * flag that defines if the app should have a dedicated root folder in Crowdin files, default 'false'
20
20
  */
21
21
  withRootFolder?: boolean;
22
+ /**
23
+ * Validate integration settings before saving
24
+ */
25
+ validateSettings?: ({ client, credentials, appSettings, }: {
26
+ client: Crowdin;
27
+ credentials: any;
28
+ appSettings: AppSettings;
29
+ }) => Promise<{
30
+ [key: string]: string;
31
+ } | null>;
22
32
  /**
23
33
  * function which will be used to check connection with integration service
24
34
  */
@@ -186,7 +196,18 @@ export interface IntegrationLogic extends ModuleKey {
186
196
  icon: boolean;
187
197
  close: boolean;
188
198
  };
199
+ /**
200
+ * Skip integration nodes
201
+ */
189
202
  skipIntegrationNodes?: SkipIntegrationNodes;
203
+ /**
204
+ * Configuration for toggling skipIntegrationNodes functionality with custom title and description
205
+ */
206
+ skipIntegrationNodesToggle?: {
207
+ title: string;
208
+ description: string;
209
+ value: boolean;
210
+ };
190
211
  /**
191
212
  * Async progress checking time interval to update job progress, im ms.
192
213
  *
@@ -287,6 +287,17 @@ function applyIntegrationModuleDefaults(config, integration) {
287
287
  helpText: 'Enter the path patterns for files or folders to exclude. Use wildcard selectors like `*` and `**` to match multiple files. Example: `/drafts/**`',
288
288
  });
289
289
  }
290
+ if (integration.skipIntegrationNodes && integration.skipIntegrationNodesToggle) {
291
+ defaultSettings.push({
292
+ key: 'skipIntegrationNodesToggle',
293
+ label: integration.skipIntegrationNodesToggle.title,
294
+ type: 'checkbox',
295
+ helpText: integration.skipIntegrationNodesToggle.description,
296
+ defaultValue: integration.skipIntegrationNodesToggle.value,
297
+ category: types_1.DefaultCategory.GENERAL,
298
+ position: 7,
299
+ });
300
+ }
290
301
  return [...defaultSettings, ...fields];
291
302
  });
292
303
  if (!integration.checkConnection) {
@@ -51,6 +51,7 @@ function getOneLevelFetchingFiles(integration, integrationCredentials, integrati
51
51
  });
52
52
  }
53
53
  function getIntegrationSnapshot(integration, integrationCredentials, integrationSettings) {
54
+ var _a;
54
55
  return __awaiter(this, void 0, void 0, function* () {
55
56
  let files = [];
56
57
  let integrationData = [];
@@ -59,7 +60,10 @@ function getIntegrationSnapshot(integration, integrationCredentials, integration
59
60
  if (integration.integrationOneLevelFetching) {
60
61
  files = yield getOneLevelFetchingFiles(integration, integrationCredentials, integrationSettings, files);
61
62
  }
62
- files = (0, files_1.skipFilesByRegex)(files, integration.skipIntegrationNodes);
63
+ if ((integrationSettings === null || integrationSettings === void 0 ? void 0 : integrationSettings.skipIntegrationNodesToggle) === true ||
64
+ ((integrationSettings === null || integrationSettings === void 0 ? void 0 : integrationSettings.skipIntegrationNodesToggle) === null && ((_a = integration.skipIntegrationNodesToggle) === null || _a === void 0 ? void 0 : _a.value))) {
65
+ files = (0, files_1.skipFilesByRegex)(files, integration.skipIntegrationNodes);
66
+ }
63
67
  // trick for compatibility in requests and set files
64
68
  files = files.map((file) => (Object.assign(Object.assign({}, file), { parentId: file.parent_id || file.parentId,
65
69
  // eslint-disable-next-line @typescript-eslint/camelcase
@@ -362,9 +362,7 @@ function handle(config) {
362
362
  if (!(0, subscription_1.isAppFree)(config)) {
363
363
  events['subscription_paid'] = '/subscription-paid';
364
364
  }
365
- const defaultScopes = config.projectIntegration
366
- ? [types_1.Scope.USERS, types_1.Scope.PROJECTS, types_1.Scope.APPLICATIONS]
367
- : [types_1.Scope.PROJECTS];
365
+ const defaultScopes = config.projectIntegration ? [types_1.Scope.USERS, types_1.Scope.PROJECTS] : [types_1.Scope.PROJECTS];
368
366
  return (_req, res) => {
369
367
  const manifest = Object.assign(Object.assign(Object.assign(Object.assign({ identifier: config.identifier, name: config.name, logo: (0, util_1.getLogoUrl)(), baseUrl: config.baseUrl, authentication: {
370
368
  type: config.authenticationType || types_1.AuthenticationType.APP,
package/out/types.d.ts CHANGED
@@ -261,7 +261,7 @@ export interface ClientConfig extends ImagePath {
261
261
  /**
262
262
  * property that tells backend that AiProvider and AiPromptProvider modules can cooperate only with each one
263
263
  */
264
- restrictAiToSameApp: boolean;
264
+ restrictAiToSameApp?: boolean;
265
265
  }
266
266
  export interface Environments {
267
267
  environments?: Environment | Environment[];
@@ -36,7 +36,9 @@
36
36
  <div id="buttons">
37
37
  <crowdin-button id="show-integration-btn" class="hidden" icon-before="arrow_back" onclick="showIntegration();">Integration</crowdin-button>
38
38
  <crowdin-button id="show-error-logs-btn" icon-before="list" onclick="showErrorLogs();">Error logs</crowdin-button>
39
- <crowdin-button icon-before="link" onclick="showPermissionsDialog()">Share</crowdin-button>
39
+ {{#if isManager}}
40
+ <crowdin-button icon-before="link" onclick="showPermissionsDialog()">Share</crowdin-button>
41
+ {{/if}}
40
42
  {{#if infoModal}}
41
43
  <crowdin-button icon-before="info" onclick="openModal(infoModal);">{{infoModal.title}}</crowdin-button>
42
44
  {{/if}}
@@ -154,7 +156,7 @@
154
156
  label="Users"
155
157
  help-text="Search for members by name, username, email, or invite new ones using their email."
156
158
  is-position-fixed
157
- onchange="inviteUsers()"
159
+ onchange="checkUsers()"
158
160
  >
159
161
  </crowdin-select>
160
162
  </div>
@@ -221,7 +223,7 @@
221
223
  </div>
222
224
 
223
225
  <div slot="footer">
224
- <crowdin-button id="confirm-users-btn" outlined onclick="inviteUsers(false)">Save</crowdin-button>
226
+ <crowdin-button id="confirm-users-btn" outlined onclick="inviteUsers()">Save</crowdin-button>
225
227
  </div>
226
228
  </crowdin-modal>
227
229
 
@@ -285,6 +287,7 @@
285
287
  {{#if dependencySettings}}
286
288
  data-dependency="{{dependencySettings}}"
287
289
  {{/if}}
290
+ onchange="settingsFieldChange(this)"
288
291
  >
289
292
  </crowdin-checkbox>
290
293
  {{/ifeq}}
@@ -310,6 +313,7 @@
310
313
  data-dependency="{{dependencySettings}}"
311
314
  {{/if}}
312
315
  is-position-fixed
316
+ onchange="settingsFieldChange(this)"
313
317
  >
314
318
  {{#each options}}
315
319
  <option
@@ -338,6 +342,7 @@
338
342
  data-dependency="{{dependencySettings}}"
339
343
  {{/if}}
340
344
  value="{{#if defaultValue}}{{defaultValue}}{{/if}}"
345
+ onchange="settingsFieldChange(this)"
341
346
  >
342
347
  </crowdin-input>
343
348
  {{/ifeq}}
@@ -355,7 +360,9 @@
355
360
  {{#if dependencySettings}}
356
361
  data-dependency="{{dependencySettings}}"
357
362
  {{/if}}
358
- value="{{#if defaultValue}}{{defaultValue}}{{/if}}">
363
+ value="{{#if defaultValue}}{{defaultValue}}{{/if}}"
364
+ onchange="settingsFieldChange(this)"
365
+ >
359
366
  </crowdin-textarea>
360
367
  {{/ifeq}}
361
368
  {{#ifeq type "notice"}}
@@ -959,6 +966,18 @@
959
966
  permissionsModal.setAttribute('body-overflow-unset', true)
960
967
  }
961
968
 
969
+ function getUserFullName(user) {
970
+ if (user.full_name) {
971
+ return user.full_name;
972
+ }
973
+
974
+ const ownerFullName = user.name || ((user.first_name || '') + ' ' + (user.last_name || '')).trim();
975
+
976
+ return !!ownerFullName && user?.login !== ownerFullName
977
+ ? `${ownerFullName} (${user?.login})`
978
+ : user?.login;
979
+ }
980
+
962
981
  function showPermissionsDialog() {
963
982
  hideConfirmUsersBlock();
964
983
  openModal(permissions)
@@ -967,13 +986,26 @@
967
986
 
968
987
  select.value = '[]';
969
988
 
970
- checkOrigin()
971
- .then(restParams => fetch('api/users' + restParams))
972
- .then(checkResponse)
973
- .then((res) => {
974
- let userOptions = res.data.users.map(user => `<option value="${user.id}">${sanitizeHTML(user.name)}</option>`).join('');
975
- select.innerHTML = userOptions;
976
- select.value = JSON.stringify(res.data.managers);
989
+ Promise.all([
990
+ new Promise((resolve) => {
991
+ checkOrigin()
992
+ .then(restParams => fetch('api/users' + restParams))
993
+ .then(checkResponse)
994
+ .then((res) => {
995
+ resolve(JSON.stringify(res.data.managers));
996
+ })
997
+ }),
998
+ new Promise((resolve) => {
999
+ AP.getUsers(res => {
1000
+ const users = Object.values(res).filter(user => user.id !== Number('{{userId}}'));
1001
+
1002
+ resolve(users.map(user => `<option value="${user.id}">${sanitizeHTML(getUserFullName(user))}</option>`).join(''));
1003
+ })
1004
+ })
1005
+ ])
1006
+ .then(([managers, users]) => {
1007
+ select.innerHTML = users;
1008
+ select.value = managers;
977
1009
  })
978
1010
  .catch(e => catchRejection(e, 'Can\'t fetch users'))
979
1011
  .finally(() => unsetLoader('#permissions-modal'));
@@ -987,43 +1019,116 @@
987
1019
  permissionsModal.removeAttribute('body-overflow-unset')
988
1020
  }
989
1021
 
990
- async function inviteUsers(onlyCheck = true) {
1022
+ function checkUsers() {
991
1023
  setLoader('#permissions-modal');
992
1024
 
993
1025
  const select = document.getElementById('users');
994
1026
 
995
- if (onlyCheck && select.value === '[]') {
1027
+ if (select.value === '[]') {
996
1028
  hideConfirmUsersBlock();
997
1029
  unsetLoader('#permissions-modal');
998
1030
  return;
999
1031
  }
1000
1032
 
1001
- const params = {
1002
- users: JSON.parse(select.value),
1003
- onlyCheck,
1004
- };
1033
+ const users = JSON.parse(select.value);
1034
+ const user_ids = users.filter(id => parseInt(id));
1035
+ const emails = users.filter(id => !parseInt(id));
1036
+
1037
+ const params = { user_ids, emails };
1038
+
1039
+ Promise.all([
1040
+ new Promise((resolve) => {
1041
+ checkOrigin()
1042
+ .then(restParams => fetch('api/users' + restParams))
1043
+ .then(checkResponse)
1044
+ .then((usersResponse) => {
1045
+ resolve(usersResponse.data.managers);
1046
+ });
1047
+ }),
1048
+ new Promise((resolve) => {
1049
+ AP.validateUsers(params, (res) => {
1050
+ resolve(res.data);
1051
+ });
1052
+ }),
1053
+ new Promise((resolve) => {
1054
+ AP.getUsers(res => {
1055
+ resolve(res);
1056
+ });
1057
+ })
1058
+ ])
1059
+ .then(([appManagers, checkUsersResponse, allUsers]) => {
1060
+ const newUserEmails = checkUsersResponse.newUserEmails.map(email => ({name: email}));
1061
+
1062
+ const newProjectManagers = [
1063
+ ...newUserEmails,
1064
+ ...checkUsersResponse.newProjectManagers.map(user => ({
1065
+ name: getUserFullName(user),
1066
+ })),
1067
+ ];
1068
+
1069
+ const newApplicationAccessUsers = [
1070
+ ...newUserEmails,
1071
+ ...checkUsersResponse.newApplicationAccessUsers.map(user => ({
1072
+ name: getUserFullName(user),
1073
+ })),
1074
+ ];
1075
+
1076
+ const newApplicationManagers = [
1077
+ ...newUserEmails,
1078
+ ...Object.values(allUsers)
1079
+ .filter(user => users.some(id => +id === user.id) && !appManagers.some(id => +id === user.id))
1080
+ .map(user => ({name: getUserFullName(user)})),
1081
+ ];
1082
+
1083
+ prepareUsersConfirmBlock({
1084
+ ...checkUsersResponse,
1085
+ newUserEmails,
1086
+ newProjectManagers,
1087
+ newApplicationAccessUsers,
1088
+ newApplicationManagers,
1089
+ });
1090
+ })
1091
+ .catch(e => catchRejection(e, e?.error || 'Can\'t invite users'))
1092
+ .finally(() => unsetLoader('#permissions-modal'));
1093
+ }
1094
+
1095
+ function inviteUsers() {
1096
+ setLoader('#permissions-modal');
1097
+
1098
+ const select = document.getElementById('users');
1099
+ const users = JSON.parse(select.value);
1100
+ const user_ids = users.filter(id => parseInt(id));
1101
+ const emails = users.filter(id => !parseInt(id));
1102
+
1103
+ const params = { user_ids, emails };
1104
+
1105
+ AP.inviteUsers(params, (res) => {
1106
+ if (!res.success) {
1107
+ catchRejection(res, res?.error?.message || 'Can\'t invite users')
1108
+ unsetLoader('#permissions-modal');
1109
+ return;
1110
+ }
1111
+
1112
+ checkOrigin()
1113
+ .then(restParams => fetch('api/invite-users' + restParams, {
1114
+ method: 'POST',
1115
+ headers: { 'Content-Type': 'application/json' },
1116
+ body: JSON.stringify(res.data)
1117
+ }))
1118
+ .then(checkResponse)
1119
+ .then((res) => {
1120
+ if (!res.success) {
1121
+ catchRejection(res, res?.message || 'Can\'t invite users');
1122
+ return;
1123
+ }
1005
1124
 
1006
- checkOrigin()
1007
- .then(restParams => fetch('api/invite-users' + restParams, {
1008
- method: 'POST',
1009
- headers: { 'Content-Type': 'application/json' },
1010
- body: JSON.stringify(params)
1011
- }))
1012
- .then(checkResponse)
1013
- .then((response) => {
1014
- if (!onlyCheck) {
1015
1125
  showToast('Users successfully updated');
1016
1126
  hideConfirmUsersBlock();
1017
1127
  closeModal(permissions);
1018
- return;
1019
- }
1020
-
1021
- prepareUsersConfirmBlock(response.data);
1022
- })
1023
- .catch(e => {
1024
- catchRejection(e, e?.error || 'Can\'t invite users')
1025
- })
1026
- .finally(() => unsetLoader('#permissions-modal'));
1128
+ })
1129
+ .catch((e) => catchRejection(e, e?.error || 'Can\'t invite users'))
1130
+ .finally(() => unsetLoader('#permissions-modal'));
1131
+ });
1027
1132
  }
1028
1133
 
1029
1134
  function prepareUsersConfirmBlock(usersData) {
@@ -1044,8 +1149,8 @@
1044
1149
  processUsersWhoWillBeInvitedToOrganization(organizationInvite, usersData, organizationWillGrantElement, grantedElement, notAvailableElement);
1045
1150
  }
1046
1151
 
1047
- processUsersWhoWillBeInvited(projectInvite, usersData.usersWhoWillBeInvitedToProject, willGrantedElement, grantedElement);
1048
- processUsersWhoWillBeInvited(applicationCredentialsInvite, usersData.usersWhoNotIssetInIntegration, willGrantedElement, grantedElement);
1152
+ processUsersWhoWillBeInvited(projectInvite, usersData.newProjectManagers, willGrantedElement, grantedElement);
1153
+ processUsersWhoWillBeInvited(applicationCredentialsInvite, usersData.newApplicationManagers, willGrantedElement, grantedElement);
1049
1154
 
1050
1155
  processUsersWhoWillBeInvitedApplicationSettings(usersData, willGrantedElement, grantedElement, notAvailableElement);
1051
1156
  }
@@ -1058,7 +1163,7 @@
1058
1163
  description.classList.remove('text-warning');
1059
1164
  description.innerText = '';
1060
1165
 
1061
- const users = usersData.usersWhoWillBeInvitedToOrganization;
1166
+ const users = usersData.newUserEmails;
1062
1167
 
1063
1168
  if (users.length) {
1064
1169
  if (usersData.inviteRestricted) {
@@ -1114,25 +1219,13 @@
1114
1219
 
1115
1220
  let affectedUsers = '<ul>';
1116
1221
 
1117
- if (!usersData.editApplicationAvailable) {
1118
- descriptionMessage += ' The application doesn\'t have permission to update this setting.';
1119
- descriptionMessage += usersData.isAdmin ? ' Please reinstall the app.' : ' Please ask the organization admin to reinstall the app.';
1120
-
1121
- description.classList.add('text-warning');
1122
- description.innerText = descriptionMessage;
1123
-
1124
- tooltip.innerHTML = notAvailableElement;
1125
- for (const user of usersData.usersWhoNotIssetInApplicationInstallation) {
1126
- affectedUsers += `<li><crowdin-p>${sanitizeHTML(user.name)}</crowdin-p></li>`;
1127
- }
1128
- userList.innerHTML = affectedUsers + '</ul>';
1129
- } else if (!usersData.usersWhoNotIssetInApplicationInstallation.length) {
1222
+ if (!usersData.newApplicationAccessUsers.length) {
1130
1223
  tooltip.innerHTML = grantedElement;
1131
1224
  userList.innerHTML = grantedElement;
1132
1225
  } else {
1133
1226
  if (usersData.isAdmin) {
1134
1227
  tooltip.innerHTML = willGrantedElement;
1135
- for (const user of usersData.usersWhoNotIssetInApplicationInstallation) {
1228
+ for (const user of usersData.newApplicationAccessUsers) {
1136
1229
  affectedUsers += `<li><crowdin-p>${sanitizeHTML(user.name)}</crowdin-p></li>`;
1137
1230
  }
1138
1231
  userList.innerHTML = affectedUsers + '</ul>';
@@ -1143,7 +1236,7 @@
1143
1236
  description.innerText = descriptionMessage;
1144
1237
 
1145
1238
  tooltip.innerHTML = notAvailableElement;
1146
- for (const user of usersData.usersWhoNotIssetInApplicationInstallation) {
1239
+ for (const user of usersData.newApplicationAccessUsers) {
1147
1240
  affectedUsers += `<li><crowdin-p>${sanitizeHTML(user.name)}</crowdin-p></li>`;
1148
1241
  }
1149
1242
  userList.innerHTML = affectedUsers + '</ul>';
@@ -1227,6 +1320,7 @@
1227
1320
  });
1228
1321
  });
1229
1322
 
1323
+ let isValidationError = false;
1230
1324
  settingsSaveBtn.setAttribute('disabled', true);
1231
1325
  checkOrigin()
1232
1326
  .then(restParams => fetch('api/settings' + restParams, {
@@ -1239,15 +1333,33 @@
1239
1333
  showToast('Settings successfully saved');
1240
1334
  config = configReq;
1241
1335
  })
1242
- .catch(e => catchRejection(e, 'Can\'t save settings'))
1336
+ .catch(e => {
1337
+ if (e?.error && e?.error?.type === 'validation_error') {
1338
+ Object.keys(e?.error?.details).forEach(key => {
1339
+ const el = document.getElementById(`${key}-settings`);
1340
+ if (el) {
1341
+ el.setAttribute('error', 'true');
1342
+ el.setAttribute('error-text', e?.error?.details[key]);
1343
+ }
1344
+ });
1345
+
1346
+ isValidationError = true;
1347
+ showToast(e?.error?.details?.message || 'Can\'t save settings');
1348
+ settingsSaveBtn.removeAttribute('disabled');
1349
+ return;
1350
+ }
1351
+ catchRejection(e, 'Can\'t save settings')
1352
+ })
1243
1353
  .finally(() => {
1244
1354
  unsetLoader('#settings-modal');
1245
1355
  settingsSaveBtn.removeAttribute('disabled');
1246
- closeModal(settingsModal);
1247
- {{#if reloadOnConfigSave}}
1248
- getIntegrationData(true);
1249
- getCrowdinData();
1250
- {{/if}}
1356
+ if (!isValidationError) {
1357
+ closeModal(settingsModal);
1358
+ {{#if reloadOnConfigSave}}
1359
+ getIntegrationData(true);
1360
+ getCrowdinData();
1361
+ {{/if}}
1362
+ }
1251
1363
  });
1252
1364
  }
1253
1365
  {{else}}
@@ -1682,6 +1794,11 @@
1682
1794
  modal.style.display = 'none';
1683
1795
  modal.close()
1684
1796
  }
1797
+
1798
+ function settingsFieldChange(el) {
1799
+ el.removeAttribute('error');
1800
+ el.removeAttribute('error-text');
1801
+ }
1685
1802
  </script>
1686
1803
 
1687
1804
  </html>
@@ -26,24 +26,28 @@
26
26
  crossorigin="anonymous"
27
27
  ></script>
28
28
  <script>
29
- Sentry.init({
30
- dsn: "{{sentryData.dsn}}",
31
- environment: "frontend",
32
- replaysSessionSampleRate: 0,
33
- replaysOnErrorSampleRate: 1.0,
34
- integrations: [new Sentry.Replay()],
35
- });
29
+ if (typeof Sentry !== 'undefined') {
30
+ Sentry.init({
31
+ dsn: "{{sentryData.dsn}}",
32
+ environment: "frontend",
33
+ replaysSessionSampleRate: 0,
34
+ replaysOnErrorSampleRate: 1.0,
35
+ integrations: [new Sentry.Replay()],
36
+ });
36
37
 
37
- Sentry.configureScope(function(scope) {
38
- scope.setTag("identifier", "{{sentryData.appIdentifier}}");
38
+ Sentry.configureScope(function(scope) {
39
+ scope.setTag("identifier", "{{sentryData.appIdentifier}}");
39
40
 
40
- AP.getContext(contextData => {
41
- const { user_id, ...rest } = contextData;
41
+ AP.getContext(contextData => {
42
+ const { user_id, ...rest } = contextData;
42
43
 
43
- user_id && scope.setUser({ id: user_id });
44
- scope.setTags(rest);
44
+ user_id && scope.setUser({ id: user_id });
45
+ scope.setTags(rest);
46
+ });
45
47
  });
46
- });
48
+ } else {
49
+ console.warn('Sentry is not available. This might be due to ad/tracking blockers or network issues.');
50
+ }
47
51
  </script>
48
52
  {{/if}}
49
53
  </head>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crowdin/app-project-module",
3
- "version": "0.91.1",
3
+ "version": "0.93.0",
4
4
  "description": "Module that generates for you all common endpoints for serving standalone Crowdin App",
5
5
  "main": "out/index.js",
6
6
  "types": "out/index.d.ts",