@crowdin/app-project-module 0.72.0 → 0.73.1

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.
@@ -53,7 +53,7 @@ function handle(config, integration, optional = false) {
53
53
  if (projectIntegrationCredentials.length) {
54
54
  for (const credentials of projectIntegrationCredentials) {
55
55
  ownerIds.push(crowdinAppFunctions.parseCrowdinId(credentials.id).userId);
56
- if (yield checkUserAccessToIntegration(credentials, `${userId}`)) {
56
+ if (checkUserAccessToIntegration(credentials, `${userId}`)) {
57
57
  integrationCredentials = credentials;
58
58
  clientId = credentials.id;
59
59
  req.crowdinContext.clientId = clientId;
@@ -99,14 +99,11 @@ function handle(config, integration, optional = false) {
99
99
  }
100
100
  exports.default = handle;
101
101
  function checkUserAccessToIntegration(integrationCredentials, userId) {
102
- return __awaiter(this, void 0, void 0, function* () {
103
- const projectIntegrationConfig = yield (0, storage_1.getStorage)().getIntegrationConfig(integrationCredentials.id);
104
- if (projectIntegrationConfig === null || projectIntegrationConfig === void 0 ? void 0 : projectIntegrationConfig.config) {
105
- const appSettings = JSON.parse(projectIntegrationConfig.config);
106
- return ((appSettings === null || appSettings === void 0 ? void 0 : appSettings.managers) || []).includes(userId);
107
- }
108
- return false;
109
- });
102
+ if (integrationCredentials === null || integrationCredentials === void 0 ? void 0 : integrationCredentials.managers) {
103
+ const managers = JSON.parse(integrationCredentials.managers);
104
+ return (managers || []).includes(userId);
105
+ }
106
+ return false;
110
107
  }
111
108
  function getIntegrationManagedBy(ownerIds, req) {
112
109
  return __awaiter(this, void 0, void 0, function* () {
@@ -0,0 +1,3 @@
1
+ /// <reference types="qs" />
2
+ import { Response } from 'express';
3
+ 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;
@@ -0,0 +1,268 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ const util_1 = require("../../../util");
13
+ const types_1 = require("../../../types");
14
+ 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
+ function handle() {
22
+ 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 usersWhoWillBeInvitedToOrganization = filterOrganizationUsers(usersToInvite);
51
+ const usersWhoWillBeInvitedToProject = filterProjectUsers({
52
+ usersToInvite,
53
+ projectMembers,
54
+ organizationMembers,
55
+ });
56
+ const usersWhoNotIssetInApplicationInstallation = filterNotIssetApplicationUsers({
57
+ usersToInvite,
58
+ applicationInstallation,
59
+ projectMembers,
60
+ organizationMembers,
61
+ });
62
+ const usersWhoNotIssetInIntegration = filterNotIssetIntegrationUsers({
63
+ usersToInvite,
64
+ integrationCredentials,
65
+ projectMembers,
66
+ organizationMembers,
67
+ });
68
+ if (onlyCheck) {
69
+ return res.send({
70
+ data: {
71
+ isAdmin,
72
+ editApplicationAvailable: applicationInstallation !== null,
73
+ usersWhoWillBeInvitedToOrganization,
74
+ usersWhoWillBeInvitedToProject,
75
+ usersWhoNotIssetInApplicationInstallation,
76
+ usersWhoNotIssetInIntegration,
77
+ },
78
+ });
79
+ }
80
+ const response = yield inviteUsers({
81
+ req,
82
+ projectId,
83
+ isAdmin,
84
+ usersToInvite,
85
+ applicationInstallation,
86
+ usersWhoWillBeInvitedToOrganization,
87
+ usersWhoWillBeInvitedToProject,
88
+ usersWhoNotIssetInApplicationInstallation,
89
+ });
90
+ return res.send(response);
91
+ }));
92
+ }
93
+ exports.default = handle;
94
+ function filterOrganizationUsers(usersToInvite) {
95
+ return usersToInvite.filter((identifier) => (0, util_1.validateEmail)(identifier)).map((name) => ({ name: `${name}` }));
96
+ }
97
+ function filterProjectUsers({ usersToInvite, projectMembers, organizationMembers, }) {
98
+ return usersToInvite
99
+ .map((identifier) => {
100
+ if ((0, util_1.validateEmail)(identifier)) {
101
+ return { name: `${identifier}` };
102
+ }
103
+ const user = projectMembers.find((member) => member.id === +identifier);
104
+ if (!user) {
105
+ const organizationUser = organizationMembers.find((member) => member.id === +identifier);
106
+ return organizationUser && !organizationUser.isAdmin
107
+ ? { id: organizationUser.id, name: (0, users_1.getUserFullName)(organizationUser) }
108
+ : null;
109
+ }
110
+ return ('role' in user ? !CONSTANTS.MANAGER_ROLES.includes(user.role) : !user.isManager)
111
+ ? { id: user.id, name: (0, users_1.getUserFullName)(user) }
112
+ : null;
113
+ })
114
+ .filter(Boolean);
115
+ }
116
+ function filterNotIssetApplicationUsers({ usersToInvite, applicationInstallation, projectMembers, organizationMembers, }) {
117
+ let userIdentifiers = [];
118
+ if (!applicationInstallation) {
119
+ return [];
120
+ }
121
+ const projectIntegrationModules = applicationInstallation.data.modules.filter((module) => module.type === CONSTANTS.PROJECT_INTEGRATIONS_MODULE_TYPE);
122
+ if (!projectIntegrationModules.length) {
123
+ return [];
124
+ }
125
+ if (projectIntegrationModules.some((module) => module.permissions.user.value === types_1.UserPermissions.ALL_MEMBERS)) {
126
+ return [];
127
+ }
128
+ else {
129
+ userIdentifiers = usersToInvite.filter((userId) => projectIntegrationModules.every((module) => !module.permissions.user.ids.includes(+userId)));
130
+ }
131
+ return userIdentifiers
132
+ .map((identifier) => {
133
+ if ((0, util_1.validateEmail)(identifier)) {
134
+ return { name: `${identifier}` };
135
+ }
136
+ let user;
137
+ user = projectMembers.find((member) => member.id === +identifier);
138
+ if (!user) {
139
+ user = organizationMembers.find((member) => member.id === +identifier);
140
+ }
141
+ return user ? { id: user.id, name: (0, users_1.getUserFullName)(user) } : null;
142
+ })
143
+ .filter(Boolean);
144
+ }
145
+ function filterNotIssetIntegrationUsers({ usersToInvite, integrationCredentials, projectMembers, organizationMembers, }) {
146
+ let integrationManagers = [];
147
+ if (integrationCredentials === null || integrationCredentials === void 0 ? void 0 : integrationCredentials.managers) {
148
+ integrationManagers = JSON.parse(integrationCredentials.managers);
149
+ }
150
+ return usersToInvite
151
+ .map((identifier) => {
152
+ if (integrationManagers.includes(`${identifier}`)) {
153
+ return null;
154
+ }
155
+ if ((0, util_1.validateEmail)(identifier)) {
156
+ return { name: `${identifier}` };
157
+ }
158
+ let user;
159
+ user = projectMembers.find((member) => member.id === +identifier);
160
+ if (!user) {
161
+ user = organizationMembers.find((member) => member.id === +identifier);
162
+ }
163
+ return user ? { id: user.id, name: (0, users_1.getUserFullName)(user) } : null;
164
+ })
165
+ .filter(Boolean);
166
+ }
167
+ function inviteUsers({ req, projectId, isAdmin, usersToInvite, applicationInstallation, usersWhoWillBeInvitedToOrganization, usersWhoWillBeInvitedToProject, usersWhoNotIssetInApplicationInstallation, }) {
168
+ return __awaiter(this, void 0, void 0, function* () {
169
+ const client = req.crowdinApiClient;
170
+ const alreadyAddedUserIds = yield inviteUsersToProject({
171
+ client,
172
+ projectId,
173
+ usersWhoWillBeInvitedToOrganization,
174
+ usersWhoWillBeInvitedToProject,
175
+ });
176
+ if (isAdmin) {
177
+ yield addUsersToApplicationInstallation({
178
+ client,
179
+ ownerId: req.integrationCredentials.ownerId,
180
+ applicationInstallation,
181
+ alreadyAddedUserIds,
182
+ usersWhoNotIssetInApplicationInstallation,
183
+ });
184
+ }
185
+ yield addUsersToIntegration({
186
+ clientId: req.crowdinContext.clientId,
187
+ alreadyAddedUserIds,
188
+ usersToInvite,
189
+ });
190
+ return { success: true };
191
+ });
192
+ }
193
+ function inviteUsersToProject({ client, projectId, usersWhoWillBeInvitedToOrganization, usersWhoWillBeInvitedToProject, }) {
194
+ return __awaiter(this, void 0, void 0, function* () {
195
+ let addedIds = [];
196
+ const emailInvites = usersWhoWillBeInvitedToOrganization.map((user) => user.name);
197
+ const userIdInvites = usersWhoWillBeInvitedToProject.map((user) => user.id).filter(Boolean);
198
+ try {
199
+ if (emailInvites.length) {
200
+ const inviteResponse = (yield client.usersApi.addProjectMember(projectId, {
201
+ managerAccess: true,
202
+ emails: emailInvites,
203
+ })); // TODO: fix typings in the @crowdin/crowdin-api-client
204
+ addedIds = inviteResponse.data.added.map((member) => member.id);
205
+ }
206
+ }
207
+ catch (error) {
208
+ console.error('Failed to invite users', error);
209
+ }
210
+ try {
211
+ if (userIdInvites.length) {
212
+ yield client.usersApi.addProjectMember(projectId, {
213
+ managerAccess: true,
214
+ userIds: userIdInvites,
215
+ }); // TODO: fix typings in the @crowdin/crowdin-api-client
216
+ }
217
+ }
218
+ catch (error) {
219
+ console.error('Failed to grant project manager access', error);
220
+ }
221
+ return addedIds;
222
+ });
223
+ }
224
+ function addUsersToApplicationInstallation({ client, ownerId, applicationInstallation, alreadyAddedUserIds, usersWhoNotIssetInApplicationInstallation, }) {
225
+ return __awaiter(this, void 0, void 0, function* () {
226
+ if (!applicationInstallation || !usersWhoNotIssetInApplicationInstallation.length) {
227
+ return;
228
+ }
229
+ const applicationInvites = [
230
+ ownerId,
231
+ ...alreadyAddedUserIds,
232
+ ...usersWhoNotIssetInApplicationInstallation.map((user) => user.id).filter(Boolean),
233
+ ];
234
+ const permissions = applicationInstallation.data.modules.filter((module) => module.type === CONSTANTS.PROJECT_INTEGRATIONS_MODULE_TYPE);
235
+ if (!permissions.length) {
236
+ return;
237
+ }
238
+ try {
239
+ for (const module of permissions) {
240
+ if (module.permissions.user.value === types_1.UserPermissions.ALL_MEMBERS) {
241
+ continue;
242
+ }
243
+ const userIds = [...new Set([...module.permissions.user.ids, ...applicationInvites])];
244
+ client.applicationsApi.editApplicationInstallation(applicationInstallation.data.identifier, [
245
+ {
246
+ op: 'replace',
247
+ path: `/modules/${module.key}/permissions`,
248
+ value: {
249
+ user: {
250
+ value: types_1.UserPermissions.RESTRICTED,
251
+ ids: userIds,
252
+ },
253
+ },
254
+ },
255
+ ]);
256
+ }
257
+ }
258
+ catch (error) {
259
+ console.error('Failed to update application permissions', error);
260
+ }
261
+ });
262
+ }
263
+ function addUsersToIntegration({ clientId, alreadyAddedUserIds, usersToInvite, }) {
264
+ return __awaiter(this, void 0, void 0, function* () {
265
+ const integrationInvites = [...alreadyAddedUserIds, ...usersToInvite.filter((identifier) => +identifier)];
266
+ yield (0, storage_1.getStorage)().updateIntegrationManagers(clientId, JSON.stringify(integrationInvites.map((id) => `${id}`)));
267
+ });
268
+ }
@@ -39,7 +39,7 @@ const defaults_1 = require("../util/defaults");
39
39
  const crowdinAppFunctions = __importStar(require("@crowdin/crowdin-apps-functions"));
40
40
  function handle(config, integration) {
41
41
  return (0, util_1.runAsyncWrapper)((req, res) => __awaiter(this, void 0, void 0, function* () {
42
- var _a, _b;
42
+ var _a, _b, _c;
43
43
  const logger = req.logInfo || logger_1.log;
44
44
  const installed = !!req.crowdinApiClient;
45
45
  const loggedIn = !!req.integrationCredentials;
@@ -66,12 +66,18 @@ function handle(config, integration) {
66
66
  }
67
67
  else if (integration.getConfiguration) {
68
68
  const { userId } = crowdinAppFunctions.parseCrowdinId(req.crowdinContext.clientId);
69
- options.isOwner = +req.crowdinContext.jwtPayload.sub === userId;
70
- const configurationFields = yield integration.getConfiguration(req.crowdinContext.jwtPayload.context.project_id, req.crowdinApiClient, req.integrationCredentials);
71
- options.configurationFields = configurationFields;
69
+ options.hasOrganization = !!req.crowdinContext.jwtPayload.domain;
70
+ options.isOwner = req.integrationCredentials.ownerId === userId;
72
71
  options.config = JSON.stringify(req.integrationSettings || {});
73
72
  options.reloadOnConfigSave = !!integration.reloadOnConfigSave;
74
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 = configurationFields;
75
81
  logger(`Adding configuration fields ${JSON.stringify(configurationFields, null, 2)}`);
76
82
  }
77
83
  options.infoModal = integration.infoModal;
@@ -96,7 +102,7 @@ function handle(config, integration) {
96
102
  : null;
97
103
  options.notice = integration.notice;
98
104
  options.asyncProgress = {
99
- checkInterval: ((_b = integration.asyncProgress) === null || _b === void 0 ? void 0 : _b.checkInterval) || 1000,
105
+ checkInterval: ((_c = integration.asyncProgress) === null || _c === void 0 ? void 0 : _c.checkInterval) || 1000,
100
106
  };
101
107
  logger(`Routing user to ${view} view`);
102
108
  return res.render(view, options);
@@ -0,0 +1,13 @@
1
+ /// <reference types="qs" />
2
+ import Crowdin, { UsersModel } from '@crowdin/crowdin-api-client';
3
+ import { Response } from 'express';
4
+ export type ProjectMember = UsersModel.ProjectMember | UsersModel.EnterpriseProjectMember;
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 }: {
7
+ client: Crowdin;
8
+ projectId: number;
9
+ }): Promise<{
10
+ projectMembers: ProjectMember[];
11
+ organizationMembers: UsersModel.User[];
12
+ }>;
13
+ export declare function getUserFullName(user: UsersModel.User | ProjectMember): string;
@@ -0,0 +1,102 @@
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
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
26
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
27
+ return new (P || (P = Promise))(function (resolve, reject) {
28
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
29
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
30
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
31
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
32
+ });
33
+ };
34
+ Object.defineProperty(exports, "__esModule", { value: true });
35
+ exports.getUserFullName = exports.getUsers = void 0;
36
+ const util_1 = require("../../../util");
37
+ const storage_1 = require("../../../storage");
38
+ const crowdinAppFunctions = __importStar(require("@crowdin/crowdin-apps-functions"));
39
+ function handle() {
40
+ 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
+ let integrationManagers = [];
62
+ const integrationCredentials = yield (0, storage_1.getStorage)().getIntegrationCredentials(req.crowdinContext.clientId);
63
+ if (integrationCredentials === null || integrationCredentials === void 0 ? void 0 : integrationCredentials.managers) {
64
+ integrationManagers = JSON.parse(integrationCredentials.managers);
65
+ }
66
+ res.json({
67
+ data: {
68
+ users: sortedUsers,
69
+ managers: integrationManagers,
70
+ },
71
+ });
72
+ }));
73
+ }
74
+ exports.default = handle;
75
+ function getUsers({ client, projectId }) {
76
+ 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;
93
+ });
94
+ }
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;
@@ -64,6 +64,8 @@ const subscription_info_1 = __importDefault(require("./handlers/subscription-inf
64
64
  const sync_settings_1 = __importDefault(require("./handlers/sync-settings"));
65
65
  const sync_settings_save_1 = __importDefault(require("./handlers/sync-settings-save"));
66
66
  const user_errors_1 = __importDefault(require("./handlers/user-errors"));
67
+ const users_1 = __importDefault(require("./handlers/users"));
68
+ const invite_users_1 = __importDefault(require("./handlers/invite-users"));
67
69
  const cron_1 = require("./util/cron");
68
70
  const storage_1 = require("../../storage");
69
71
  function register({ config, app }) {
@@ -222,6 +224,18 @@ function register({ config, app }) {
222
224
  checkSubscriptionExpiration: true,
223
225
  moduleKey: integrationLogic.key,
224
226
  }), (0, integration_credentials_1.default)(config, integrationLogic), (0, user_errors_1.default)());
227
+ app.get('/api/users', json_response_1.default, (0, crowdin_client_1.default)({
228
+ config,
229
+ optional: false,
230
+ checkSubscriptionExpiration: true,
231
+ moduleKey: integrationLogic.key,
232
+ }), (0, integration_credentials_1.default)(config, integrationLogic), (0, users_1.default)());
233
+ app.post('/api/invite-users', json_response_1.default, (0, crowdin_client_1.default)({
234
+ config,
235
+ optional: false,
236
+ checkSubscriptionExpiration: true,
237
+ moduleKey: integrationLogic.key,
238
+ }), (0, integration_credentials_1.default)(config, integrationLogic), (0, invite_users_1.default)());
225
239
  cron.schedule('0 0 1 * *', () => (0, cron_1.removeFinishedJobs)());
226
240
  }
227
241
  exports.register = register;
@@ -355,6 +355,7 @@ export interface IntegrationCredentials {
355
355
  id: string;
356
356
  credentials: any;
357
357
  crowdinId: string;
358
+ managers?: any;
358
359
  }
359
360
  export interface IntegrationConfig {
360
361
  id: number;
@@ -147,27 +147,6 @@ function applyIntegrationModuleDefaults(config, integration) {
147
147
  fields = yield getUserSettings(projectId, crowdinClient, integrationCredentials);
148
148
  }
149
149
  const defaultSettings = [];
150
- const mangers = yield getManagers(crowdinClient, projectId, integrationCredentials.ownerId);
151
- if (mangers.length) {
152
- defaultSettings.push({
153
- key: 'managers',
154
- label: 'Managers',
155
- type: 'select',
156
- isMulti: true,
157
- isSearchable: true,
158
- options: mangers.map((manager) => {
159
- const ownerFullName = 'fullName' in manager
160
- ? manager.fullName
161
- : ((manager.firstName || '') + ' ' + (manager.lastName || '')).trim();
162
- return {
163
- value: manager.id.toString(),
164
- label: !!ownerFullName && (manager === null || manager === void 0 ? void 0 : manager.username) !== ownerFullName
165
- ? `${ownerFullName} (${manager === null || manager === void 0 ? void 0 : manager.username})`
166
- : manager === null || manager === void 0 ? void 0 : manager.username,
167
- };
168
- }),
169
- });
170
- }
171
150
  if (project.data.inContext) {
172
151
  defaultSettings.push({
173
152
  key: 'inContext',
@@ -320,30 +299,3 @@ function getOAuthLoginFormId(clientId) {
320
299
  return `oauth_form_${clientId}`;
321
300
  }
322
301
  exports.getOAuthLoginFormId = getOAuthLoginFormId;
323
- function getManagers(client, projectId, ownerId) {
324
- return __awaiter(this, void 0, void 0, function* () {
325
- const managers = [];
326
- if (client.organization) {
327
- try {
328
- const admins = (yield client.usersApi.withFetchAll().listUsers()).data.filter((user) => user.data.isAdmin);
329
- managers.push(...admins.map((admin) => admin.data));
330
- }
331
- catch (e) {
332
- console.error('Failed to get organization users', e);
333
- }
334
- }
335
- const projectMembers = (yield client.usersApi.withFetchAll().listProjectMembers(projectId)).data.filter((user) => 'role' in user.data ? ['owner', 'manager'].includes(user.data.role) : user.data.isManager);
336
- managers.push(...projectMembers.map((members) => members.data));
337
- const uniqueManagers = new Map();
338
- managers.forEach((manager) => {
339
- if (manager.id !== ownerId && !uniqueManagers.has(manager.id)) {
340
- uniqueManagers.set(manager.id, manager);
341
- }
342
- });
343
- return Array.from(uniqueManagers.values()).sort((a, b) => {
344
- const aValue = a.firstName || a.username || '';
345
- const bValue = b.firstName || b.username || '';
346
- return aValue.localeCompare(bValue);
347
- });
348
- });
349
- }
@@ -285,11 +285,14 @@ function handle(config) {
285
285
  if (!(0, subscription_1.isAppFree)(config)) {
286
286
  events['subscription_paid'] = '/subscription-paid';
287
287
  }
288
+ const defaultScopes = config.projectIntegration
289
+ ? [types_1.Scope.USERS, types_1.Scope.PROJECTS, types_1.Scope.APPLICATIONS]
290
+ : [types_1.Scope.PROJECTS];
288
291
  return (_req, res) => {
289
292
  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: {
290
293
  type: config.authenticationType || types_1.AuthenticationType.APP,
291
294
  clientId: config.clientId,
292
- } }, (config.agent && { agent: config.agent })), { events, scopes: config.scopes ? config.scopes : [types_1.Scope.PROJECTS] }), (config.defaultPermissions && { default_permissions: config.defaultPermissions })), { modules });
295
+ } }, (config.agent && { agent: config.agent })), { events, scopes: config.scopes ? config.scopes : defaultScopes }), (config.defaultPermissions && { default_permissions: config.defaultPermissions })), { modules });
293
296
  res.send(manifest);
294
297
  };
295
298
  }
@@ -67,6 +67,10 @@
67
67
  margin: 0;
68
68
  }
69
69
 
70
+ .m-2 {
71
+ margin: 16px;
72
+ }
73
+
70
74
  .info-text {
71
75
  max-width: 800px;
72
76
  }
@@ -259,3 +263,80 @@
259
263
  display: none;
260
264
  }
261
265
  }
266
+
267
+ .confirm-users-block .flex > div {
268
+ width: 40%;
269
+ margin-bottom: 8px
270
+ }
271
+
272
+ .confirm-users-block crowdin-p {
273
+ line-height: 1;
274
+ }
275
+
276
+ table {
277
+ width: 100%;
278
+ border-collapse: separate;
279
+ border-spacing: 0;
280
+ }
281
+ th, td {
282
+ padding: 16px;
283
+ text-align: left;
284
+ border-bottom: 1px solid var(--crowdin-border-color);
285
+ }
286
+ th {
287
+ font-size: 14px;
288
+ }
289
+ .permission-description {
290
+ font-size: 12px;
291
+ color: var(--crowdin-text-muted);
292
+ margin-top: 4px;
293
+ }
294
+ .affected-users {
295
+ ul {
296
+ list-style: none;
297
+ padding: 0;
298
+ margin: 0;
299
+ }
300
+
301
+ li {
302
+ font-size: 14px;
303
+
304
+ &:not(:last-child) {
305
+ margin-bottom: 4px;
306
+ }
307
+ }
308
+ }
309
+
310
+ .badge {
311
+ display: inline-block;
312
+ padding: 4px 12px;
313
+ border-radius: 15px;
314
+ font-size: 12px;
315
+ font-weight: 500;
316
+ text-align: center;
317
+ position: relative;
318
+ cursor: default;
319
+ }
320
+ .badge-granted {
321
+ background-color: var(--crowdin-success);
322
+ color: var(--crowdin-white);
323
+ }
324
+
325
+ .badge-will-be-granted {
326
+ background-color: var(--crowdin-info);
327
+ color: var(--crowdin-white);
328
+ }
329
+
330
+ .badge-not-available {
331
+ background-color: var(--crowdin-warning-bg);
332
+ color: var(--crowdin-warning);
333
+ }
334
+
335
+ .status {
336
+ position: relative;
337
+ text-align: right;
338
+ }
339
+
340
+ .text-warning {
341
+ color: var(--crowdin-warning) !important;
342
+ }
@@ -11,6 +11,7 @@ export interface Storage {
11
11
  deleteCrowdinCredentials(id: string): Promise<void>;
12
12
  saveIntegrationCredentials(id: string, credentials: any, crowdinId: string): Promise<void>;
13
13
  updateIntegrationCredentials(id: string, credentials: any): Promise<void>;
14
+ updateIntegrationManagers(id: string, managers: any): Promise<void>;
14
15
  getIntegrationCredentials(id: string): Promise<IntegrationCredentials | undefined>;
15
16
  getAllIntegrationCredentials(crowdinId: string): Promise<IntegrationCredentials[]>;
16
17
  deleteIntegrationCredentials(id: string): Promise<void>;
@@ -28,6 +28,7 @@ export declare class MySQLStorage implements Storage {
28
28
  deleteCrowdinCredentials(id: string): Promise<void>;
29
29
  saveIntegrationCredentials(id: string, credentials: any, crowdinId: string): Promise<void>;
30
30
  updateIntegrationCredentials(id: string, credentials: any): Promise<void>;
31
+ updateIntegrationManagers(id: string, managers: any): Promise<void>;
31
32
  getIntegrationCredentials(id: string): Promise<IntegrationCredentials | undefined>;
32
33
  getAllIntegrationCredentials(crowdinId: string): Promise<IntegrationCredentials[]>;
33
34
  deleteIntegrationCredentials(id: string): Promise<void>;
@@ -84,7 +84,8 @@ class MySQLStorage {
84
84
  (
85
85
  id varchar(255) primary key,
86
86
  credentials text,
87
- crowdin_id varchar(255) not null
87
+ crowdin_id varchar(255) not null,
88
+ managers text
88
89
  )
89
90
  `);
90
91
  yield connection.execute(`
@@ -266,11 +267,17 @@ class MySQLStorage {
266
267
  yield this.executeQuery((connection) => connection.execute('UPDATE integration_credentials SET credentials = ? WHERE id = ?', [credentials, id]));
267
268
  });
268
269
  }
270
+ updateIntegrationManagers(id, managers) {
271
+ return __awaiter(this, void 0, void 0, function* () {
272
+ yield this.dbPromise;
273
+ yield this.executeQuery((connection) => connection.execute('UPDATE integration_credentials SET managers = ? WHERE id = ?', [managers, id]));
274
+ });
275
+ }
269
276
  getIntegrationCredentials(id) {
270
277
  return __awaiter(this, void 0, void 0, function* () {
271
278
  yield this.dbPromise;
272
279
  return this.executeQuery((connection) => __awaiter(this, void 0, void 0, function* () {
273
- const [rows] = yield connection.execute('SELECT id, credentials, crowdin_id as "crowdinId" FROM integration_credentials WHERE id = ?', [id]);
280
+ const [rows] = yield connection.execute('SELECT id, credentials, crowdin_id as "crowdinId", managers FROM integration_credentials WHERE id = ?', [id]);
274
281
  return (rows || [])[0];
275
282
  }));
276
283
  });
@@ -279,7 +286,7 @@ class MySQLStorage {
279
286
  return __awaiter(this, void 0, void 0, function* () {
280
287
  yield this.dbPromise;
281
288
  return this.executeQuery((connection) => __awaiter(this, void 0, void 0, function* () {
282
- const [rows] = yield connection.execute('SELECT id, credentials, crowdin_id as "crowdinId" FROM integration_credentials WHERE crowdin_id = ?', [crowdinId]);
289
+ const [rows] = yield connection.execute('SELECT id, credentials, crowdin_id as "crowdinId", managers FROM integration_credentials WHERE crowdin_id = ?', [crowdinId]);
283
290
  return rows || [];
284
291
  }));
285
292
  });
@@ -35,6 +35,7 @@ export declare class PostgreStorage implements Storage {
35
35
  deleteCrowdinCredentials(id: string): Promise<void>;
36
36
  saveIntegrationCredentials(id: string, credentials: any, crowdinId: string): Promise<void>;
37
37
  updateIntegrationCredentials(id: string, credentials: any): Promise<void>;
38
+ updateIntegrationManagers(id: string, managers: any): Promise<void>;
38
39
  getIntegrationCredentials(id: string): Promise<IntegrationCredentials | undefined>;
39
40
  getAllIntegrationCredentials(crowdinId: string): Promise<IntegrationCredentials[]>;
40
41
  deleteIntegrationCredentials(id: string): Promise<void>;
@@ -112,7 +112,8 @@ class PostgreStorage {
112
112
  (
113
113
  id varchar primary key,
114
114
  credentials varchar,
115
- crowdin_id varchar not null
115
+ crowdin_id varchar not null,
116
+ managers varchar
116
117
  )
117
118
  `);
118
119
  yield client.query(`
@@ -293,11 +294,17 @@ class PostgreStorage {
293
294
  yield this.executeQuery((client) => client.query('UPDATE integration_credentials SET credentials = $1 WHERE id = $2', [credentials, id]));
294
295
  });
295
296
  }
297
+ updateIntegrationManagers(id, managers) {
298
+ return __awaiter(this, void 0, void 0, function* () {
299
+ yield this.dbPromise;
300
+ yield this.executeQuery((client) => client.query('UPDATE integration_credentials SET managers = $1 WHERE id = $2', [managers, id]));
301
+ });
302
+ }
296
303
  getIntegrationCredentials(id) {
297
304
  return __awaiter(this, void 0, void 0, function* () {
298
305
  yield this.dbPromise;
299
306
  return this.executeQuery((client) => __awaiter(this, void 0, void 0, function* () {
300
- const res = yield client.query('SELECT id, credentials, crowdin_id as "crowdinId" FROM integration_credentials WHERE id = $1', [id]);
307
+ const res = yield client.query('SELECT id, credentials, crowdin_id as "crowdinId", managers FROM integration_credentials WHERE id = $1', [id]);
301
308
  return res === null || res === void 0 ? void 0 : res.rows[0];
302
309
  }));
303
310
  });
@@ -306,7 +313,7 @@ class PostgreStorage {
306
313
  return __awaiter(this, void 0, void 0, function* () {
307
314
  yield this.dbPromise;
308
315
  return this.executeQuery((client) => __awaiter(this, void 0, void 0, function* () {
309
- const res = yield client.query('SELECT id, credentials, crowdin_id as "crowdinId" FROM integration_credentials WHERE crowdin_id = $1', [crowdinId]);
316
+ const res = yield client.query('SELECT id, credentials, crowdin_id as "crowdinId", managers FROM integration_credentials WHERE crowdin_id = $1', [crowdinId]);
310
317
  return (res === null || res === void 0 ? void 0 : res.rows) || [];
311
318
  }));
312
319
  });
@@ -22,6 +22,7 @@ export declare class SQLiteStorage implements Storage {
22
22
  private addColumn;
23
23
  private updateTables;
24
24
  private moveIntegrationSettings;
25
+ private migrateManagers;
25
26
  migrate(): Promise<void>;
26
27
  saveCrowdinCredentials(credentials: CrowdinCredentials): Promise<void>;
27
28
  updateCrowdinCredentials(credentials: CrowdinCredentials): Promise<void>;
@@ -30,6 +31,7 @@ export declare class SQLiteStorage implements Storage {
30
31
  deleteCrowdinCredentials(id: string): Promise<void>;
31
32
  saveIntegrationCredentials(id: string, credentials: any, crowdinId: string): Promise<void>;
32
33
  updateIntegrationCredentials(id: string, credentials: any): Promise<void>;
34
+ updateIntegrationManagers(id: string, managers: any): Promise<void>;
33
35
  getIntegrationCredentials(id: string): Promise<IntegrationCredentials | undefined>;
34
36
  getAllIntegrationCredentials(crowdinId: string): Promise<IntegrationCredentials[]>;
35
37
  deleteIntegrationCredentials(id: string): Promise<void>;
@@ -145,6 +145,26 @@ class SQLiteStorage {
145
145
  yield this.removeColumns('config', 'integration_credentials');
146
146
  });
147
147
  }
148
+ migrateManagers() {
149
+ return __awaiter(this, void 0, void 0, function* () {
150
+ const tableInfo = yield this.each('PRAGMA table_info(integration_credentials);', []);
151
+ const exists = tableInfo.some((columnInfo) => columnInfo.name === 'managers');
152
+ if (exists) {
153
+ return;
154
+ }
155
+ yield this.addColumn('integration_credentials', 'managers', 'null');
156
+ const integrationSettings = yield this.each('SELECT integration_id, config FROM integration_settings', []);
157
+ for (const settings of integrationSettings) {
158
+ const config = JSON.parse(settings.config);
159
+ if (config.managers) {
160
+ yield this.run('UPDATE integration_credentials SET managers = ? WHERE id = ?', [
161
+ JSON.stringify(config.managers),
162
+ settings.integration_id,
163
+ ]);
164
+ }
165
+ }
166
+ });
167
+ }
148
168
  migrate() {
149
169
  return __awaiter(this, void 0, void 0, function* () {
150
170
  let _connection_res;
@@ -186,7 +206,8 @@ class SQLiteStorage {
186
206
  (
187
207
  id varchar not null primary key,
188
208
  credentials varchar not null,
189
- crowdin_id varchar not null
209
+ crowdin_id varchar not null,
210
+ managers varchar null
190
211
  );
191
212
  `, []);
192
213
  yield this._run(`
@@ -283,6 +304,7 @@ class SQLiteStorage {
283
304
  // TODO: temporary code
284
305
  yield this.updateTables();
285
306
  yield this.moveIntegrationSettings();
307
+ yield this.migrateManagers();
286
308
  }
287
309
  catch (e) {
288
310
  this._rej && this._rej(e);
@@ -353,16 +375,19 @@ class SQLiteStorage {
353
375
  updateIntegrationCredentials(id, credentials) {
354
376
  return this.run('UPDATE integration_credentials SET credentials = ? WHERE id = ?', [credentials, id]);
355
377
  }
378
+ updateIntegrationManagers(id, managers) {
379
+ return this.run('UPDATE integration_credentials SET managers = ? WHERE id = ?', [managers, id]);
380
+ }
356
381
  getIntegrationCredentials(id) {
357
382
  return __awaiter(this, void 0, void 0, function* () {
358
- const row = yield this.get('SELECT id, credentials, crowdin_id as crowdinId FROM integration_credentials WHERE id = ?', [id]);
383
+ const row = yield this.get('SELECT id, credentials, crowdin_id as crowdinId, managers FROM integration_credentials WHERE id = ?', [id]);
359
384
  if (row) {
360
385
  return row;
361
386
  }
362
387
  });
363
388
  }
364
389
  getAllIntegrationCredentials(crowdinId) {
365
- return this.each('SELECT id, credentials, crowdin_id as crowdinId FROM integration_credentials WHERE crowdin_id = ?', [crowdinId]);
390
+ return this.each('SELECT id, credentials, crowdin_id as crowdinId, managers FROM integration_credentials WHERE crowdin_id = ?', [crowdinId]);
366
391
  }
367
392
  deleteIntegrationCredentials(id) {
368
393
  return __awaiter(this, void 0, void 0, function* () {
package/out/types.d.ts CHANGED
@@ -281,7 +281,8 @@ export declare enum Scope {
281
281
  AI = "ai",
282
282
  AI_PROVIDERS = "ai.provider",
283
283
  AI_PROMPTS = "ai.prompt",
284
- AI_PROXIES = "ai.proxy"
284
+ AI_PROXIES = "ai.proxy",
285
+ APPLICATIONS = "application"
285
286
  }
286
287
  export interface CrowdinClientRequest extends Request {
287
288
  crowdinApiClient: Crowdin;
@@ -423,7 +424,8 @@ export declare enum UserPermissions {
423
424
  OWNER = "owner",
424
425
  MANAGERS = "managers",
425
426
  ALL_MEMBERS = "all",
426
- GUESTS = "guests"
427
+ GUESTS = "guests",
428
+ RESTRICTED = "restricted"
427
429
  }
428
430
  export declare enum ProjectPermissions {
429
431
  OWN = "own",
package/out/types.js CHANGED
@@ -34,6 +34,7 @@ var Scope;
34
34
  Scope["AI_PROVIDERS"] = "ai.provider";
35
35
  Scope["AI_PROMPTS"] = "ai.prompt";
36
36
  Scope["AI_PROXIES"] = "ai.proxy";
37
+ Scope["APPLICATIONS"] = "application";
37
38
  })(Scope = exports.Scope || (exports.Scope = {}));
38
39
  var AccountType;
39
40
  (function (AccountType) {
@@ -58,6 +59,7 @@ var UserPermissions;
58
59
  UserPermissions["MANAGERS"] = "managers";
59
60
  UserPermissions["ALL_MEMBERS"] = "all";
60
61
  UserPermissions["GUESTS"] = "guests";
62
+ UserPermissions["RESTRICTED"] = "restricted";
61
63
  })(UserPermissions = exports.UserPermissions || (exports.UserPermissions = {}));
62
64
  var ProjectPermissions;
63
65
  (function (ProjectPermissions) {
@@ -13,3 +13,4 @@ export declare function isAuthorizedConfig(config: Config | UnauthorizedConfig):
13
13
  export declare function isJson(string: string): boolean;
14
14
  export declare function getPreviousDate(days: number): Date;
15
15
  export declare function prepareFormDataMetadataId(req: CrowdinClientRequest, config: Config): Promise<string>;
16
+ export declare function validateEmail(email: string | number): boolean;
package/out/util/index.js CHANGED
@@ -32,7 +32,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
32
32
  });
33
33
  };
34
34
  Object.defineProperty(exports, "__esModule", { value: true });
35
- exports.prepareFormDataMetadataId = exports.getPreviousDate = exports.isJson = exports.isAuthorizedConfig = exports.getLogoUrl = exports.executeWithRetry = exports.decryptData = exports.encryptData = exports.runAsyncWrapper = exports.CodeError = void 0;
35
+ exports.validateEmail = exports.prepareFormDataMetadataId = exports.getPreviousDate = exports.isJson = exports.isAuthorizedConfig = exports.getLogoUrl = exports.executeWithRetry = exports.decryptData = exports.encryptData = exports.runAsyncWrapper = exports.CodeError = void 0;
36
36
  const crypto = __importStar(require("crypto-js"));
37
37
  const storage_1 = require("../storage");
38
38
  const types_1 = require("../types");
@@ -151,3 +151,14 @@ function prepareFormDataMetadataId(req, config) {
151
151
  });
152
152
  }
153
153
  exports.prepareFormDataMetadataId = prepareFormDataMetadataId;
154
+ function validateEmail(email) {
155
+ if (!isNaN(+email)) {
156
+ return false;
157
+ }
158
+ if (`${email}`.trim().length > 76) {
159
+ return false;
160
+ }
161
+ const emailRegExp = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
162
+ return emailRegExp.test(String(email).toLowerCase());
163
+ }
164
+ exports.validateEmail = validateEmail;
@@ -36,6 +36,7 @@
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
40
  {{#if infoModal}}
40
41
  <crowdin-button icon-before="info" onclick="openModal(infoModal);">{{infoModal.title}}</crowdin-button>
41
42
  {{/if}}
@@ -115,19 +116,113 @@
115
116
  <crowdin-button class="ml-10" secondary onclick="integrationLogout()">Log out</crowdin-button>
116
117
  </div>
117
118
  </crowdin-modal>
118
- {{#if infoModal}}
119
+
119
120
  <crowdin-modal
120
121
  style="display: none;"
121
- id="info-modal"
122
- modal-width="50"
123
- modal-title="{{infoModal.title}}"
122
+ id="permissions-modal"
123
+ modal-title="Share"
124
124
  close-button-title="Close"
125
+ close-button
126
+ body-overflow-unset
125
127
  >
126
- <div>
127
- {{{infoModal.content}}}
128
- </div>
128
+ <div class="loader hidden">
129
+ <crowdin-progress-indicator></crowdin-progress-indicator>
130
+ </div>
131
+ <div class="permissions-modal-content">
132
+ <div class="select-users-block">
133
+ <crowdin-users-select
134
+ allow-new-options
135
+ is-multi
136
+ is-searchable
137
+ id="users"
138
+ key="users"
139
+ label="Users"
140
+ help-text="Search for members by name, username, email, or invite new ones using their email."
141
+ is-position-fixed
142
+ onchange="inviteUsers()"
143
+ >
144
+ </crowdin-select>
145
+ </div>
146
+ <div class="select-users-info">
147
+ <div class="confirm-users-block mt-2 hidden">
148
+ <table>
149
+ <thead>
150
+ <tr>
151
+ <th style="width: 40%;">Access</th>
152
+ <th style="width: 35%;">Users</th>
153
+ <th class="status" style="width: 20%;">Upcoming changes</th>
154
+ </tr>
155
+ </thead>
156
+ <tbody>
157
+ {{#if hasOrganization}}
158
+ <tr class="organization-invite">
159
+ <td>
160
+ <crowdin-p>Registration in Organization</crowdin-p>
161
+ </td>
162
+ <td class="affected-users"></td>
163
+ <td class="status"></td>
164
+ </tr>
165
+ {{/if}}
166
+ <tr class="project-invite">
167
+ <td>
168
+ <crowdin-p>Project Manager Access</crowdin-p>
169
+ </td>
170
+ <td class="affected-users"></td>
171
+ <td class="status"></td>
172
+ </tr>
173
+ <tr class="application-settings-invite">
174
+ <td>
175
+ <crowdin-p>Application Visibility Access</crowdin-p>
176
+ <div class="permission-description"></div>
177
+ </td>
178
+ <td class="affected-users"></td>
179
+ <td class="status"></td>
180
+ </tr>
181
+ <tr class="application-credentials-invite">
182
+ <td>
183
+ <crowdin-p>Access to the Integration</crowdin-p>
184
+ <div class="permission-description">This provides access to sync files and modify the current integration settings.</div>
185
+ </td>
186
+ <td class="affected-users"></td>
187
+ <td class="status"></td>
188
+ </tr>
189
+ </tbody>
190
+ </table>
191
+ </div>
192
+ {{#if zenModeUrl}}
193
+ <div class="mt-2">
194
+ <crowdin-input
195
+ with-fixed-height
196
+ label="Zen Mode link"
197
+ help-text="This focused view allows you to concentrate solely on the Integrations section, eliminating other distractions."
198
+ value="{{zenModeUrl}}"
199
+ name="zenModeLink"
200
+ with-copy-button
201
+ ></crowdin-input>
202
+ </div>
203
+ {{/if}}
204
+ </div>
205
+ </div>
206
+
207
+ <div slot="footer">
208
+ <crowdin-button id="confirm-users-btn" outlined onclick="inviteUsers(false)">Save</crowdin-button>
209
+ </div>
129
210
  </crowdin-modal>
211
+
212
+ {{#if infoModal}}
213
+ <crowdin-modal
214
+ style="display: none;"
215
+ id="info-modal"
216
+ modal-width="50"
217
+ modal-title="{{infoModal.title}}"
218
+ close-button-title="Close"
219
+ >
220
+ <div>
221
+ {{{infoModal.content}}}
222
+ </div>
223
+ </crowdin-modal>
130
224
  {{/if}}
225
+
131
226
  {{#if configurationFields}}
132
227
  <crowdin-modal
133
228
  style="display: none;"
@@ -785,7 +880,7 @@
785
880
  }
786
881
 
787
882
  function saveSettings() {
788
- setLoader();
883
+ setLoader('#settings-modal');
789
884
  const settingsElements = Array.from(document.getElementById('modal-content').children);
790
885
  const tags = ['crowdin-checkbox', 'crowdin-select', 'crowdin-input'];
791
886
  const configReq = {};
@@ -820,7 +915,7 @@
820
915
  })
821
916
  .catch(e => catchRejection(e, 'Can\'t save settings'))
822
917
  .finally(() => {
823
- unsetLoader();
918
+ unsetLoader('#settings-modal');
824
919
  settingsSaveBtn.removeAttribute('disabled');
825
920
  closeModal(settingsModal);
826
921
  {{#if reloadOnConfigSave}}
@@ -830,13 +925,181 @@
830
925
  });
831
926
  }
832
927
 
833
- function setLoader() {
834
- const loader = document.querySelector('#settings-modal .loader');
928
+ function showConfirmUsersBlock() {
929
+ const confirmUsersBlock = document.querySelector('.confirm-users-block');
930
+ confirmUsersBlock.classList.remove('hidden');
931
+
932
+ const permissionsModal = document.getElementById('permissions-modal');
933
+ permissionsModal.removeAttribute('body-overflow-unset')
934
+ }
935
+
936
+ function hideConfirmUsersBlock() {
937
+ const confirmUsersBlock = document.querySelector('.confirm-users-block');
938
+ confirmUsersBlock.classList.add('hidden');
939
+
940
+ const permissionsModal = document.getElementById('permissions-modal');
941
+ permissionsModal.setAttribute('body-overflow-unset', true)
942
+ }
943
+
944
+ function showPermissionsDialog() {
945
+ hideConfirmUsersBlock();
946
+ openModal(permissions)
947
+ setLoader('#permissions-modal');
948
+ const select = document.getElementById('users');
949
+
950
+ select.value = '[]';
951
+
952
+ checkOrigin()
953
+ .then(restParams => fetch('api/users' + restParams))
954
+ .then(checkResponse)
955
+ .then((res) => {
956
+ let userOptions = res.data.users.map(user => `<option value="${user.id}">${user.name}</option>`).join('');
957
+ select.innerHTML = userOptions;
958
+ select.value = JSON.stringify(res.data.managers);
959
+ })
960
+ .catch(e => catchRejection(e, 'Can\'t fetch users'))
961
+ .finally(() => unsetLoader('#permissions-modal'));
962
+ }
963
+
964
+ async function inviteUsers(onlyCheck = true) {
965
+ setLoader('#permissions-modal');
966
+
967
+ const select = document.getElementById('users');
968
+
969
+ if (onlyCheck && select.value === '[]') {
970
+ hideConfirmUsersBlock();
971
+ unsetLoader('#permissions-modal');
972
+ return;
973
+ }
974
+
975
+ const params = {
976
+ users: JSON.parse(select.value),
977
+ onlyCheck,
978
+ };
979
+
980
+ checkOrigin()
981
+ .then(restParams => fetch('api/invite-users' + restParams, {
982
+ method: 'POST',
983
+ headers: { 'Content-Type': 'application/json' },
984
+ body: JSON.stringify(params)
985
+ }))
986
+ .then(checkResponse)
987
+ .then((response) => {
988
+ if (!onlyCheck) {
989
+ showToast('Users successfully updated');
990
+ hideConfirmUsersBlock();
991
+ closeModal(permissions);
992
+ return;
993
+ }
994
+
995
+ prepareUsersConfirmBlock(response.data);
996
+ })
997
+ .catch(e => {
998
+ catchRejection(e, e?.error || 'Can\'t invite users')
999
+ })
1000
+ .finally(() => unsetLoader('#permissions-modal'));
1001
+ }
1002
+
1003
+ function prepareUsersConfirmBlock(usersData) {
1004
+ showConfirmUsersBlock();
1005
+
1006
+ const organizationInvite = document.querySelector('.organization-invite');
1007
+ const projectInvite = document.querySelector('.project-invite');
1008
+ const applicationCredentialsInvite = document.querySelector('.application-credentials-invite');
1009
+
1010
+ const grantedElement = '<crowdin-p>&horbar;</crowdin-p>';
1011
+ const willGrantedElement = '<span class="badge badge-will-be-granted">Will Be Granted</span>';
1012
+ const notAvailableElement = '<span class="badge badge-not-available">Action Required</span>';
1013
+
1014
+ // only in enterprise
1015
+ if (organizationInvite) {
1016
+ const organizationWillGrantElement = `<span class="badge badge-will-be-granted">Will Be Registered</span>`;
1017
+
1018
+ processUsersWhoWillBeInvited(organizationInvite, usersData.usersWhoWillBeInvitedToOrganization, organizationWillGrantElement, grantedElement);
1019
+ }
1020
+
1021
+ processUsersWhoWillBeInvited(projectInvite, usersData.usersWhoWillBeInvitedToProject, willGrantedElement, grantedElement);
1022
+ processUsersWhoWillBeInvited(applicationCredentialsInvite, usersData.usersWhoNotIssetInIntegration, willGrantedElement, grantedElement);
1023
+
1024
+ processUsersWhoWillBeInvitedApplicationSettings(usersData, willGrantedElement, grantedElement, notAvailableElement);
1025
+ }
1026
+
1027
+ function processUsersWhoWillBeInvited(element, users, grantMessage, alreadyGrantedMessage) {
1028
+ const tooltip = element.querySelector('.status');
1029
+ const userList = element.querySelector('.affected-users');
1030
+
1031
+ if (users.length) {
1032
+ tooltip.innerHTML = grantMessage;
1033
+
1034
+ let affectedUsers = '<ul>';
1035
+ for (const user of users) {
1036
+ affectedUsers += `<li><crowdin-p>${user.name}</crowdin-p></li>`;
1037
+ }
1038
+ affectedUsers += '</ul>';
1039
+ userList.innerHTML = affectedUsers;
1040
+ } else {
1041
+ tooltip.innerHTML = alreadyGrantedMessage;
1042
+ userList.innerHTML = alreadyGrantedMessage;
1043
+ }
1044
+ }
1045
+
1046
+ function processUsersWhoWillBeInvitedApplicationSettings(usersData, willGrantedElement, grantedElement, notAvailableElement) {
1047
+ const applicationSettingsInvite = document.querySelector('.application-settings-invite');
1048
+
1049
+ const tooltip = applicationSettingsInvite.querySelector('.status');
1050
+ const description = applicationSettingsInvite.querySelector('.permission-description');
1051
+ const userList = applicationSettingsInvite.querySelector('.affected-users');
1052
+
1053
+ let descriptionMessage = 'This can be configured in organization settings (Apps section).';
1054
+ description.classList.remove('text-warning');
1055
+ description.innerText = descriptionMessage;
1056
+
1057
+ let affectedUsers = '<ul>';
1058
+
1059
+ if (!usersData.editApplicationAvailable) {
1060
+ descriptionMessage += ' The application doesn\'t have permission to update this setting.';
1061
+ descriptionMessage += usersData.isAdmin ? ' Please reinstall the app.' : ' Please ask the organization admin to reinstall the app.';
1062
+
1063
+ description.classList.add('text-warning');
1064
+ description.innerText = descriptionMessage;
1065
+
1066
+ tooltip.innerHTML = notAvailableElement;
1067
+ for (const user of usersData.usersWhoNotIssetInApplicationInstallation) {
1068
+ affectedUsers += `<li><crowdin-p>${user.name}</crowdin-p></li>`;
1069
+ }
1070
+ userList.innerHTML = affectedUsers + '</ul>';
1071
+ } else if (!usersData.usersWhoNotIssetInApplicationInstallation.length) {
1072
+ tooltip.innerHTML = grantedElement;
1073
+ userList.innerHTML = grantedElement;
1074
+ } else {
1075
+ if (usersData.isAdmin) {
1076
+ tooltip.innerHTML = willGrantedElement;
1077
+ for (const user of usersData.usersWhoNotIssetInApplicationInstallation) {
1078
+ affectedUsers += `<li><crowdin-p>${user.name}</crowdin-p></li>`;
1079
+ }
1080
+ userList.innerHTML = affectedUsers + '</ul>';
1081
+ } else {
1082
+ descriptionMessage += ' Only organization admins have permission to update this setting.';
1083
+
1084
+ description.classList.add('text-warning');
1085
+ description.innerText = descriptionMessage;
1086
+
1087
+ tooltip.innerHTML = notAvailableElement;
1088
+ for (const user of usersData.usersWhoNotIssetInApplicationInstallation) {
1089
+ affectedUsers += `<li><crowdin-p>${user.name}</crowdin-p></li>`;
1090
+ }
1091
+ userList.innerHTML = affectedUsers + '</ul>';
1092
+ }
1093
+ }
1094
+ }
1095
+
1096
+ function setLoader(id) {
1097
+ const loader = document.querySelector(`${id} .loader`);
835
1098
  loader.classList.remove('hidden');
836
1099
  }
837
1100
 
838
- function unsetLoader() {
839
- const loader = document.querySelector('#settings-modal .loader');
1101
+ function unsetLoader(id) {
1102
+ const loader = document.querySelector(`${id} .loader`);
840
1103
  setTimeout(function() {
841
1104
  loader.classList.add('hidden');
842
1105
  }, 500)
@@ -845,6 +1108,8 @@
845
1108
  const settingsModal = undefined;
846
1109
  {{/if}}
847
1110
 
1111
+ const permissions = document.getElementById('permissions-modal');
1112
+
848
1113
  {{#if infoModal}}
849
1114
  const infoModal = document.getElementById('info-modal');
850
1115
  {{else}}
@@ -853,7 +1118,9 @@
853
1118
 
854
1119
  document.addEventListener('keydown', (event) => {
855
1120
  if (event.keyCode == 27) {
856
-
1121
+ if (users) {
1122
+ closeModal(permissions);
1123
+ }
857
1124
  if (infoModal) {
858
1125
  closeModal(infoModal);
859
1126
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crowdin/app-project-module",
3
- "version": "0.72.0",
3
+ "version": "0.73.1",
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",