@crowdin/app-project-module 0.38.0 → 0.39.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.
@@ -10,14 +10,26 @@ 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 logger_1 = require("../../util/logger");
13
14
  function handle(integration) {
14
15
  return (0, util_1.runAsyncWrapper)((req, res) => __awaiter(this, void 0, void 0, function* () {
15
16
  const fileId = Number(req.params.fileId || req.body.fileId);
16
17
  req.logInfo(`Loading translation progress for file ${fileId}`);
17
18
  if (integration.getFileProgress) {
18
- const progress = yield integration.getFileProgress(req.crowdinContext.jwtPayload.context.project_id, req.crowdinApiClient, fileId);
19
- req.logInfo(`Translation progress for file ${fileId} ${JSON.stringify(progress, null, 2)}`);
20
- res.send(progress);
19
+ try {
20
+ const progress = yield integration.getFileProgress(req.crowdinContext.jwtPayload.context.project_id, req.crowdinApiClient, fileId);
21
+ req.logInfo(`Translation progress for file ${fileId} ${JSON.stringify(progress, null, 2)}`);
22
+ res.send(progress);
23
+ }
24
+ catch (e) {
25
+ yield (0, logger_1.handleUserError)({
26
+ action: 'Get Crowdin files progress',
27
+ error: e,
28
+ crowdinId: req.crowdinContext.crowdinId,
29
+ clientId: req.crowdinContext.clientId,
30
+ });
31
+ throw e;
32
+ }
21
33
  }
22
34
  else {
23
35
  res.send({});
@@ -11,17 +11,29 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  const util_1 = require("../../util");
13
13
  const defaults_1 = require("../../util/defaults");
14
+ const logger_1 = require("../../util/logger");
14
15
  function handle(config, integration) {
15
16
  return (0, util_1.runAsyncWrapper)((req, res) => __awaiter(this, void 0, void 0, function* () {
16
17
  req.logInfo('Loading crowdin files');
17
18
  if (integration.getCrowdinFiles) {
18
19
  const rootFolder = yield (0, defaults_1.getRootFolder)(config, integration, req.crowdinApiClient, req.crowdinContext.jwtPayload.context.project_id);
19
20
  req.logInfo(`Loading files ${rootFolder ? `from folder ${rootFolder.id}` : 'from root'}`);
20
- const files = integration.getCrowdinFiles
21
- ? yield integration.getCrowdinFiles(req.crowdinContext.jwtPayload.context.project_id, req.crowdinApiClient, rootFolder, req.integrationSettings)
22
- : [];
23
- req.logInfo(`Returning ${files.length} files`);
24
- res.send(files);
21
+ try {
22
+ const files = integration.getCrowdinFiles
23
+ ? yield integration.getCrowdinFiles(req.crowdinContext.jwtPayload.context.project_id, req.crowdinApiClient, rootFolder, req.integrationSettings)
24
+ : [];
25
+ req.logInfo(`Returning ${files.length} files`);
26
+ res.send(files);
27
+ }
28
+ catch (e) {
29
+ yield (0, logger_1.handleUserError)({
30
+ action: 'Get Crowdin files',
31
+ error: e,
32
+ crowdinId: req.crowdinContext.crowdinId,
33
+ clientId: req.crowdinContext.clientId,
34
+ });
35
+ throw e;
36
+ }
25
37
  }
26
38
  else {
27
39
  res.send([]);
@@ -11,6 +11,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  const util_1 = require("../../util");
13
13
  const defaults_1 = require("../../util/defaults");
14
+ const logger_1 = require("../../util/logger");
14
15
  function handle(config, integration) {
15
16
  return (0, util_1.runAsyncWrapper)((req, res) => __awaiter(this, void 0, void 0, function* () {
16
17
  var _a, _b;
@@ -25,10 +26,21 @@ function handle(config, integration) {
25
26
  if (((_b = config.api) === null || _b === void 0 ? void 0 : _b.default) && req.body.files) {
26
27
  req.body = req.body.files;
27
28
  }
28
- const result = yield integration.updateCrowdin(projectId, req.crowdinApiClient, req.integrationCredentials, req.body, rootFolder, req.integrationSettings, uploadTranslations);
29
29
  let message;
30
- if ((0, util_1.isExtendedResultType)(result)) {
31
- message = result.message;
30
+ try {
31
+ const result = yield integration.updateCrowdin(projectId, req.crowdinApiClient, req.integrationCredentials, req.body, rootFolder, req.integrationSettings, uploadTranslations);
32
+ if ((0, util_1.isExtendedResultType)(result)) {
33
+ message = result.message;
34
+ }
35
+ }
36
+ catch (e) {
37
+ yield (0, logger_1.handleUserError)({
38
+ action: 'Sync files to Crowdin',
39
+ error: e,
40
+ crowdinId: req.crowdinContext.crowdinId,
41
+ clientId: req.crowdinContext.clientId,
42
+ });
43
+ throw e;
32
44
  }
33
45
  res.send({ message });
34
46
  }));
@@ -10,21 +10,33 @@ 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 logger_1 = require("../../util/logger");
13
14
  function handle(integration) {
14
15
  return (0, util_1.runAsyncWrapper)((req, res) => __awaiter(this, void 0, void 0, function* () {
15
16
  const { parent_id: parentId, search, page } = req.query;
16
17
  req.logInfo('Recieved request to get integration data');
17
- const result = yield integration.getIntegrationFiles(req.integrationCredentials, req.integrationSettings, parentId, search, page);
18
18
  let message;
19
19
  let stopPagination;
20
20
  let files;
21
- if ((0, util_1.isExtendedResultType)(result)) {
22
- files = result.data;
23
- message = result.message;
24
- stopPagination = result.stopPagination;
21
+ try {
22
+ const result = yield integration.getIntegrationFiles(req.integrationCredentials, req.integrationSettings, parentId, search, page);
23
+ if ((0, util_1.isExtendedResultType)(result)) {
24
+ files = result.data;
25
+ message = result.message;
26
+ stopPagination = result.stopPagination;
27
+ }
28
+ else {
29
+ files = result;
30
+ }
25
31
  }
26
- else {
27
- files = result;
32
+ catch (e) {
33
+ yield (0, logger_1.handleUserError)({
34
+ action: 'Get External Service data',
35
+ error: e,
36
+ crowdinId: req.crowdinContext.crowdinId,
37
+ clientId: req.crowdinContext.clientId,
38
+ });
39
+ throw e;
28
40
  }
29
41
  req.logInfo(`Integration data response ${JSON.stringify(files, null, 2)}`);
30
42
  res.send({ data: files, message, stopPagination });
@@ -11,12 +11,24 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  const storage_1 = require("../../storage");
13
13
  const util_1 = require("../../util");
14
+ const logger_1 = require("../../util/logger");
14
15
  function handle(config, integration) {
15
16
  return (0, util_1.runAsyncWrapper)((req, res) => __awaiter(this, void 0, void 0, function* () {
16
17
  req.logInfo('Received integration login request');
17
18
  if (integration.checkConnection) {
18
- req.logInfo('Checking the integration credentials');
19
- yield integration.checkConnection(req.body.credentials);
19
+ try {
20
+ req.logInfo('Checking the integration credentials');
21
+ yield integration.checkConnection(req.body.credentials);
22
+ }
23
+ catch (e) {
24
+ yield (0, logger_1.handleUserError)({
25
+ action: 'External Service login',
26
+ error: e,
27
+ crowdinId: req.crowdinContext.crowdinId,
28
+ clientId: req.crowdinContext.clientId,
29
+ });
30
+ throw e;
31
+ }
20
32
  }
21
33
  const existing = yield (0, storage_1.getStorage)().getIntegrationCredentials(req.crowdinContext.clientId);
22
34
  if (!!existing) {
@@ -13,12 +13,24 @@ const storage_1 = require("../../storage");
13
13
  const util_1 = require("../../util");
14
14
  const connection_1 = require("../../util/connection");
15
15
  const webhooks_1 = require("../../util/webhooks");
16
+ const logger_1 = require("../../util/logger");
16
17
  function handle(config, integration) {
17
18
  return (0, util_1.runAsyncWrapper)((req, res) => __awaiter(this, void 0, void 0, function* () {
18
19
  req.logInfo('Received integration logout request');
19
20
  if (integration.onLogout) {
20
- req.logInfo('Invoking onLogout hook');
21
- yield integration.onLogout(req.crowdinContext.jwtPayload.context.project_id, req.crowdinApiClient, req.integrationCredentials, req.integrationSettings);
21
+ try {
22
+ req.logInfo('Invoking onLogout hook');
23
+ yield integration.onLogout(req.crowdinContext.jwtPayload.context.project_id, req.crowdinApiClient, req.integrationCredentials, req.integrationSettings);
24
+ }
25
+ catch (e) {
26
+ yield (0, logger_1.handleUserError)({
27
+ action: 'External Service logout',
28
+ error: e,
29
+ crowdinId: req.crowdinContext.crowdinId,
30
+ clientId: req.crowdinContext.clientId,
31
+ });
32
+ throw e;
33
+ }
22
34
  }
23
35
  req.logInfo(`Deleting integration credentials for ${req.crowdinContext.clientId} client`);
24
36
  yield (0, storage_1.getStorage)().deleteIntegrationCredentials(req.crowdinContext.clientId);
@@ -11,6 +11,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  const util_1 = require("../../util");
13
13
  const defaults_1 = require("../../util/defaults");
14
+ const logger_1 = require("../../util/logger");
14
15
  function handle(config, integration) {
15
16
  return (0, util_1.runAsyncWrapper)((req, res) => __awaiter(this, void 0, void 0, function* () {
16
17
  var _a;
@@ -23,10 +24,21 @@ function handle(config, integration) {
23
24
  if (((_a = config.api) === null || _a === void 0 ? void 0 : _a.default) && req.body.files) {
24
25
  req.body = req.body.files;
25
26
  }
26
- const result = yield integration.updateIntegration(req.crowdinContext.jwtPayload.context.project_id, req.crowdinApiClient, req.integrationCredentials, req.body, rootFolder, req.integrationSettings);
27
27
  let message;
28
- if ((0, util_1.isExtendedResultType)(result)) {
29
- message = result.message;
28
+ try {
29
+ const result = yield integration.updateIntegration(req.crowdinContext.jwtPayload.context.project_id, req.crowdinApiClient, req.integrationCredentials, req.body, rootFolder, req.integrationSettings);
30
+ if ((0, util_1.isExtendedResultType)(result)) {
31
+ message = result.message;
32
+ }
33
+ }
34
+ catch (e) {
35
+ yield (0, logger_1.handleUserError)({
36
+ action: 'Sync files to External Service',
37
+ error: e,
38
+ crowdinId: req.crowdinContext.crowdinId,
39
+ clientId: req.crowdinContext.clientId,
40
+ });
41
+ throw e;
30
42
  }
31
43
  res.send({ message });
32
44
  }));
@@ -0,0 +1,3 @@
1
+ /// <reference types="qs" />
2
+ import { Response } from 'express';
3
+ export default function handle(): (req: import("../models").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,40 @@
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 storage_1 = require("../storage");
13
+ const util_1 = require("../util");
14
+ function handle() {
15
+ return (0, util_1.runAsyncWrapper)((req, res) => __awaiter(this, void 0, void 0, function* () {
16
+ var _a;
17
+ let userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
18
+ if (req.crowdinContext.jwtPayload.context.user_id) {
19
+ userTimezone = (yield req.crowdinApiClient.usersApi.getUserInfo(req.crowdinContext.jwtPayload.context.user_id)).data.timezone;
20
+ }
21
+ req.logInfo(`Loading user errors for crowdinId ${req.crowdinContext.crowdinId} and integrationId ${req.crowdinContext.clientId}`);
22
+ const userErrors = (_a = (yield (0, storage_1.getStorage)().getAllUserErrors(req.crowdinContext.crowdinId, req.crowdinContext.clientId))) === null || _a === void 0 ? void 0 : _a.map((userError) => {
23
+ const date = new Date(+userError.createdAt);
24
+ // Format the date as 'MMM DD, YYYY HH:mm'
25
+ const formattedDate = new Intl.DateTimeFormat('en-US', {
26
+ year: 'numeric',
27
+ month: 'short',
28
+ day: 'numeric',
29
+ hour: '2-digit',
30
+ minute: '2-digit',
31
+ hour12: false,
32
+ timeZone: userTimezone,
33
+ }).format(date);
34
+ return Object.assign(Object.assign({}, userError), { createdAt: formattedDate });
35
+ });
36
+ req.logInfo(`Returning ${userErrors === null || userErrors === void 0 ? void 0 : userErrors.length} user errors`);
37
+ res.send(userErrors);
38
+ }));
39
+ }
40
+ exports.default = handle;
package/out/index.js CHANGED
@@ -67,6 +67,7 @@ const sync_settings_save_1 = __importDefault(require("./handlers/integration/syn
67
67
  const manifest_1 = __importDefault(require("./handlers/manifest"));
68
68
  const subscription_paid_1 = __importDefault(require("./handlers/subscription-paid"));
69
69
  const uninstall_1 = __importDefault(require("./handlers/uninstall"));
70
+ const user_errors_1 = __importDefault(require("./handlers/user-errors"));
70
71
  const crowdin_client_1 = __importStar(require("./middlewares/crowdin-client"));
71
72
  const integration_credentials_1 = __importDefault(require("./middlewares/integration-credentials"));
72
73
  const json_response_1 = __importDefault(require("./middlewares/json-response"));
@@ -184,6 +185,7 @@ function addCrowdinEndpoints(app, clientConfig) {
184
185
  (0, webhooks_1.listenQueueMessage)(config, integrationLogic, integrationLogic.webhooks.queueUrl, config.identifier);
185
186
  }
186
187
  }
188
+ app.get('/api/user-errors', json_response_1.default, (0, crowdin_client_1.default)(config), (0, integration_credentials_1.default)(config, integrationLogic), (0, user_errors_1.default)());
187
189
  }
188
190
  if (config.customFileFormat) {
189
191
  (0, defaults_1.applyFileProcessorsModuleDefaults)(config, config.customFileFormat);
@@ -860,6 +860,15 @@ export interface IntegrationWebhooks {
860
860
  crowdinId: string;
861
861
  provider: Provider;
862
862
  }
863
+ export interface UserErrors {
864
+ id: number;
865
+ action: string;
866
+ message: string;
867
+ data: any;
868
+ createdAt: string;
869
+ crowdinId: string;
870
+ integrationId?: string;
871
+ }
863
872
  export interface ImagePath {
864
873
  /**
865
874
  * path to app logo (e.g. {@example join(__dirname, 'logo.png')})
@@ -161,4 +161,52 @@
161
161
 
162
162
  #form .MuiButtonBase-root[type="submit"]:hover {
163
163
  background: rgba(236, 239, 241, .54);
164
- }
164
+ }
165
+
166
+ #buttons {
167
+ display: flex;
168
+ justify-content: space-between;
169
+ width: 100%;
170
+ }
171
+
172
+ #buttons crowdin-button:nth-of-type(2) {
173
+ margin-right: auto;
174
+ }
175
+
176
+ #user-errors-table {
177
+ margin-top: 24px;
178
+ display: block;
179
+ }
180
+
181
+ .error-detail-table {
182
+ height: auto;
183
+ max-height: 579px;
184
+ margin: -24px;
185
+ }
186
+
187
+ .error-detail-table table {
188
+ border-spacing: unset;
189
+ width: 100%;
190
+ }
191
+
192
+ .error-detail-table tr:first-child td {
193
+ padding-top: 24px;
194
+ border-top-right-radius: 12px;
195
+ }
196
+
197
+ .error-detail-table tr td:first-child {
198
+ color: #263238;
199
+ font-weight: 500;
200
+ min-width: 200px;
201
+ vertical-align: top;
202
+ }
203
+
204
+ .error-detail-table tr td:last-child {
205
+ background: #f6f6f6;
206
+ color: rgba(38,50,56,.87);
207
+ word-break: break-all;
208
+ }
209
+
210
+ .error-detail-table tr td {
211
+ padding: 8px 24px;
212
+ }
@@ -1,4 +1,4 @@
1
- import { Config, CrowdinCredentials, IntegrationCredentials, IntegrationFilesSnapshot, IntegrationSyncSettings, IntegrationWebhooks, Provider } from '../models';
1
+ import { Config, CrowdinCredentials, IntegrationCredentials, IntegrationFilesSnapshot, IntegrationSyncSettings, IntegrationWebhooks, Provider, UserErrors } from '../models';
2
2
  export interface Storage {
3
3
  migrate(): Promise<void>;
4
4
  saveCrowdinCredentials(credentials: CrowdinCredentials): Promise<void>;
@@ -29,6 +29,9 @@ export interface Storage {
29
29
  getWebhooks(fileId: string, integrationId: string, crowdinId: string, provider: Provider): Promise<IntegrationWebhooks | undefined>;
30
30
  saveWebhooks(fileId: string, integrationId: string, crowdinId: string, provider: Provider): Promise<void>;
31
31
  deleteWebhooks(fileIds: any[], integrationId: string, crowdinId: string, provider: Provider): Promise<void>;
32
+ getAllUserErrors(crowdinId: string, integrationId?: string): Promise<UserErrors[] | undefined>;
33
+ saveUserError(action: string, message: string, data: any, createdAt: string, crowdinId: string, integrationId?: string): Promise<void>;
34
+ deleteUserErrors(date: string, crowdinId: string, integrationId?: string): Promise<void>;
32
35
  }
33
36
  export declare function initialize(config: Config): Promise<void>;
34
37
  export declare function getStorage(): Storage;
@@ -1,5 +1,5 @@
1
1
  import { Storage } from '.';
2
- import { CrowdinCredentials, IntegrationCredentials, IntegrationFilesSnapshot, IntegrationSyncSettings, IntegrationWebhooks } from '../models';
2
+ import { CrowdinCredentials, IntegrationCredentials, IntegrationFilesSnapshot, IntegrationSyncSettings, IntegrationWebhooks, UserErrors } from '../models';
3
3
  export interface MySQLStorageConfig {
4
4
  uri?: string;
5
5
  host?: string;
@@ -46,4 +46,7 @@ export declare class MySQLStorage implements Storage {
46
46
  getWebhooks(fileId: string, integrationId: string, crowdinId: string, provider: string): Promise<IntegrationWebhooks | undefined>;
47
47
  saveWebhooks(fileId: string, integrationId: string, crowdinId: string, provider: string): Promise<void>;
48
48
  deleteWebhooks(fileIds: any[], integrationId: string, crowdinId: string, provider: string): Promise<void>;
49
+ getAllUserErrors(crowdinId: string, integrationId?: string): Promise<UserErrors[] | undefined>;
50
+ saveUserError(action: string, message: string, data: any, createdAt: string, crowdinId: string, integrationId?: string): Promise<void>;
51
+ deleteUserErrors(createdAt: string, crowdinId: string, integrationId?: string): Promise<void>;
49
52
  }
@@ -124,6 +124,18 @@ class MySQLStorage {
124
124
  provider varchar(255) not null
125
125
  )
126
126
  `);
127
+ yield connection.execute(`
128
+ create table if not exists user_errors
129
+ (
130
+ id int auto_increment primary key,
131
+ action varchar(255) not null,
132
+ message varchar(255) not null,
133
+ data text,
134
+ created_at varchar(255) not null,
135
+ crowdin_id varchar(255) not null,
136
+ integration_id varchar(255)
137
+ )
138
+ `);
127
139
  });
128
140
  }
129
141
  saveCrowdinCredentials(credentials) {
@@ -187,6 +199,7 @@ class MySQLStorage {
187
199
  yield connection.execute('DELETE FROM app_metadata WHERE crowdin_id = ?', [id]);
188
200
  yield connection.execute('DELETE FROM files_snapshot WHERE crowdin_id = ?', [id]);
189
201
  yield connection.execute('DELETE FROM webhooks WHERE crowdin_id = ?', [id]);
202
+ yield connection.execute('DELETE FROM user_errors WHERE crowdin_id = ?', [id]);
190
203
  }));
191
204
  });
192
205
  }
@@ -249,6 +262,7 @@ class MySQLStorage {
249
262
  yield connection.execute('DELETE FROM sync_settings where crowdin_id = ?', [crowdinId]);
250
263
  yield connection.execute('DELETE FROM files_snapshot where crowdin_id = ?', [crowdinId]);
251
264
  yield connection.execute('DELETE FROM webhooks where crowdin_id = ?', [crowdinId]);
265
+ yield connection.execute('DELETE FROM user_errors where crowdin_id = ?', [crowdinId]);
252
266
  }));
253
267
  });
254
268
  }
@@ -380,5 +394,40 @@ class MySQLStorage {
380
394
  yield this.executeQuery((connection) => connection.execute(`DELETE FROM webhooks WHERE file_id IN (${placeholders}) AND integration_id = ? AND crowdin_id = ? AND provider = ?`, [...fileIds, integrationId, crowdinId, provider]));
381
395
  });
382
396
  }
397
+ getAllUserErrors(crowdinId, integrationId) {
398
+ return __awaiter(this, void 0, void 0, function* () {
399
+ yield this.dbPromise;
400
+ return this.executeQuery((connection) => __awaiter(this, void 0, void 0, function* () {
401
+ let whereIntegrationCondition = 'integration_id is NULL';
402
+ const params = [crowdinId];
403
+ if (integrationId) {
404
+ whereIntegrationCondition = 'integration_id = ?';
405
+ params.push(integrationId);
406
+ }
407
+ const [rows] = yield connection.execute(`SELECT id, action, message, data, created_at as "createdAt" FROM user_errors WHERE crowdin_id = ? AND ${whereIntegrationCondition}`, params);
408
+ return rows || [];
409
+ }));
410
+ });
411
+ }
412
+ saveUserError(action, message, data, createdAt, crowdinId, integrationId) {
413
+ return __awaiter(this, void 0, void 0, function* () {
414
+ yield this.dbPromise;
415
+ yield this.executeQuery((connection) => connection.execute('INSERT INTO user_errors(action, message, data, created_at, integration_id, crowdin_id) VALUES (?, ?, ?, ?, ?, ?)', [action, message, data, createdAt, integrationId, crowdinId]));
416
+ });
417
+ }
418
+ deleteUserErrors(createdAt, crowdinId, integrationId) {
419
+ return __awaiter(this, void 0, void 0, function* () {
420
+ yield this.dbPromise;
421
+ yield this.executeQuery((connection) => {
422
+ let whereIntegrationCondition = 'integration_id is NULL';
423
+ const params = [createdAt, crowdinId];
424
+ if (integrationId) {
425
+ whereIntegrationCondition = 'integration_id = ?';
426
+ params.push(integrationId);
427
+ }
428
+ return connection.execute(`DELETE FROM user_errors WHERE created_at < ? AND crowdin_id = ? AND ${whereIntegrationCondition}`, params);
429
+ });
430
+ });
431
+ }
383
432
  }
384
433
  exports.MySQLStorage = MySQLStorage;
@@ -1,6 +1,6 @@
1
1
  import { Client } from 'pg';
2
2
  import { Storage } from '.';
3
- import { CrowdinCredentials, IntegrationCredentials, IntegrationFilesSnapshot, IntegrationSyncSettings, IntegrationWebhooks } from '../models';
3
+ import { CrowdinCredentials, IntegrationCredentials, IntegrationFilesSnapshot, IntegrationSyncSettings, IntegrationWebhooks, UserErrors } from '../models';
4
4
  export interface PostgreStorageConfig {
5
5
  host?: string;
6
6
  connectionString?: string;
@@ -51,4 +51,7 @@ export declare class PostgreStorage implements Storage {
51
51
  getWebhooks(fileId: string, integrationId: string, crowdinId: string, provider: string): Promise<IntegrationWebhooks | undefined>;
52
52
  saveWebhooks(fileId: string, integrationId: string, crowdinId: string, provider: string): Promise<void>;
53
53
  deleteWebhooks(fileIds: any[], integrationId: string, crowdinId: string, provider: string): Promise<void>;
54
+ getAllUserErrors(crowdinId: string, integrationId?: string): Promise<UserErrors[] | undefined>;
55
+ saveUserError(action: string, message: string, data: any, createdAt: string, crowdinId: string, integrationId?: string): Promise<void>;
56
+ deleteUserErrors(createdAt: string, crowdinId: string, integrationId?: string): Promise<void>;
54
57
  }
@@ -138,6 +138,18 @@ class PostgreStorage {
138
138
  provider varchar not null
139
139
  )
140
140
  `);
141
+ yield client.query(`
142
+ create table if not exists user_errors
143
+ (
144
+ id serial primary key,
145
+ action varchar not null,
146
+ message varchar not null,
147
+ data varchar,
148
+ created_at varchar not null,
149
+ crowdin_id varchar not null,
150
+ integration_id varchar
151
+ )
152
+ `);
141
153
  });
142
154
  }
143
155
  saveCrowdinCredentials(credentials) {
@@ -201,6 +213,7 @@ class PostgreStorage {
201
213
  yield client.query('DELETE FROM files_snapshot WHERE crowdin_id = $1', [id]);
202
214
  yield client.query('DELETE FROM app_metadata WHERE crowdin_id = $1', [id]);
203
215
  yield client.query('DELETE FROM webhooks WHERE crowdin_id = $1', [id]);
216
+ yield client.query('DELETE FROM user_errors WHERE crowdin_id = $1', [id]);
204
217
  }));
205
218
  });
206
219
  }
@@ -263,6 +276,7 @@ class PostgreStorage {
263
276
  yield client.query('DELETE FROM sync_settings where crowdin_id = $1', [crowdinId]);
264
277
  yield client.query('DELETE FROM files_snapshot where crowdin_id = $1', [crowdinId]);
265
278
  yield client.query('DELETE FROM webhooks where crowdin_id = $1', [crowdinId]);
279
+ yield client.query('DELETE FROM user_errors where crowdin_id = $1', [crowdinId]);
266
280
  }));
267
281
  });
268
282
  }
@@ -395,5 +409,40 @@ class PostgreStorage {
395
409
  yield this.executeQuery((client) => client.query(`DELETE FROM webhooks WHERE file_id IN ${placeholders} AND integration_id = $${++index} AND crowdin_id = $${++index} AND provider = $${++index}`, [...fileIds, integrationId, crowdinId, provider]));
396
410
  });
397
411
  }
412
+ getAllUserErrors(crowdinId, integrationId) {
413
+ return __awaiter(this, void 0, void 0, function* () {
414
+ yield this.dbPromise;
415
+ return this.executeQuery((client) => __awaiter(this, void 0, void 0, function* () {
416
+ let whereIntegrationCondition = 'integration_id is NULL';
417
+ const params = [crowdinId];
418
+ if (integrationId) {
419
+ whereIntegrationCondition = 'integration_id = $2';
420
+ params.push(integrationId);
421
+ }
422
+ const res = yield client.query(`SELECT id, action, message, data, created_at as "createdAt", integration_id as "integrationId", crowdin_id as "crowdinId" FROM user_errors WHERE crowdin_id = $1 AND ${whereIntegrationCondition}`, params);
423
+ return (res === null || res === void 0 ? void 0 : res.rows) || [];
424
+ }));
425
+ });
426
+ }
427
+ saveUserError(action, message, data, createdAt, crowdinId, integrationId) {
428
+ return __awaiter(this, void 0, void 0, function* () {
429
+ yield this.dbPromise;
430
+ yield this.executeQuery((client) => client.query('INSERT INTO user_errors(action, message, data, created_at, integration_id, crowdin_id) VALUES ($1, $2, $3, $4, $5, $6)', [action, message, data, createdAt, integrationId, crowdinId]));
431
+ });
432
+ }
433
+ deleteUserErrors(createdAt, crowdinId, integrationId) {
434
+ return __awaiter(this, void 0, void 0, function* () {
435
+ yield this.dbPromise;
436
+ yield this.executeQuery((client) => {
437
+ let whereIntegrationCondition = 'integration_id is NULL';
438
+ const params = [createdAt, crowdinId];
439
+ if (integrationId) {
440
+ whereIntegrationCondition = 'integration_id = $3';
441
+ params.push(integrationId);
442
+ }
443
+ return client.query(`DELETE FROM user_errors WHERE created_at < $1 AND crowdin_id = $2 AND ${whereIntegrationCondition}`, params);
444
+ });
445
+ });
446
+ }
398
447
  }
399
448
  exports.PostgreStorage = PostgreStorage;
@@ -1,5 +1,5 @@
1
1
  import { Storage } from '.';
2
- import { CrowdinCredentials, IntegrationCredentials, IntegrationFilesSnapshot, IntegrationSyncSettings, IntegrationWebhooks } from '../models';
2
+ import { CrowdinCredentials, IntegrationCredentials, IntegrationFilesSnapshot, IntegrationSyncSettings, IntegrationWebhooks, UserErrors } from '../models';
3
3
  export interface SQLiteStorageConfig {
4
4
  dbFolder: string;
5
5
  }
@@ -45,4 +45,7 @@ export declare class SQLiteStorage implements Storage {
45
45
  getWebhooks(fileId: string, integrationId: string, crowdinId: string, provider: string): Promise<IntegrationWebhooks | undefined>;
46
46
  saveWebhooks(fileId: string, integrationId: string, crowdinId: string, provider: string): Promise<void>;
47
47
  deleteWebhooks(fileIds: any[], integrationId: string, crowdinId: string, provider: string): Promise<void>;
48
+ getAllUserErrors(crowdinId: string, integrationId?: string): Promise<UserErrors[]>;
49
+ saveUserError(action: string, message: string, data: any, createdAt: string, crowdinId: string, integrationId?: string): Promise<void>;
50
+ deleteUserErrors(createAt: string, crowdinId: string, integrationId?: string): Promise<void>;
48
51
  }
@@ -195,6 +195,18 @@ class SQLiteStorage {
195
195
  crowdin_id varchar not null,
196
196
  provider varchar not null
197
197
  );
198
+ `, []);
199
+ yield this._run(`
200
+ create table if not exists user_errors
201
+ (
202
+ id integer not null primary key autoincrement,
203
+ action varchar not null,
204
+ message varchar not null,
205
+ data varchar null,
206
+ created_at varchar not null,
207
+ crowdin_id varchar not null,
208
+ integration_id varchar null
209
+ );
198
210
  `, []);
199
211
  this._res && this._res();
200
212
  // TODO: temporary code
@@ -251,6 +263,7 @@ class SQLiteStorage {
251
263
  yield this.run('DELETE FROM app_metadata WHERE crowdin_id = ?', [id]);
252
264
  yield this.run('DELETE FROM files_snapshot WHERE crowdin_id = ?', [id]);
253
265
  yield this.run('DELETE FROM webhooks WHERE crowdin_id = ?', [id]);
266
+ yield this.run('DELETE FROM user_errors WHERE crowdin_id = ?', [id]);
254
267
  });
255
268
  }
256
269
  saveIntegrationCredentials(id, credentials, crowdinId) {
@@ -291,6 +304,7 @@ class SQLiteStorage {
291
304
  yield this.run('DELETE FROM sync_settings where crowdin_id = ?', [crowdinId]);
292
305
  yield this.run('DELETE FROM files_snapshot where crowdin_id = ?', [crowdinId]);
293
306
  yield this.run('DELETE FROM webhooks where crowdin_id = ?', [crowdinId]);
307
+ yield this.run('DELETE FROM user_errors where crowdin_id = ?', [crowdinId]);
294
308
  });
295
309
  }
296
310
  saveMetadata(id, metadata, crowdinId) {
@@ -389,5 +403,28 @@ class SQLiteStorage {
389
403
  return this.run(`DELETE FROM webhooks WHERE file_id IN (${placeholders}) AND integration_id = ? AND crowdin_id = ? AND provider = ?`, [...fileIds, integrationId, crowdinId, provider]);
390
404
  });
391
405
  }
406
+ getAllUserErrors(crowdinId, integrationId) {
407
+ let whereIntegrationCondition = 'integration_id is NULL';
408
+ const params = [crowdinId];
409
+ if (integrationId) {
410
+ whereIntegrationCondition = 'integration_id = ?';
411
+ params.push(integrationId);
412
+ }
413
+ return this.each(`SELECT id, action, message, data, created_at as createdAt, integration_id as integrationId, crowdin_id as crowdinId FROM user_errors WHERE crowdin_id = ? AND ${whereIntegrationCondition}`, params);
414
+ }
415
+ saveUserError(action, message, data, createdAt, crowdinId, integrationId) {
416
+ return this.run('INSERT INTO user_errors(action, message, data, created_at, integration_id, crowdin_id) VALUES (?, ?, ?, ?, ?, ?)', [action, message, data, createdAt, integrationId, crowdinId]);
417
+ }
418
+ deleteUserErrors(createAt, crowdinId, integrationId) {
419
+ return __awaiter(this, void 0, void 0, function* () {
420
+ let whereIntegrationCondition = 'integration_id is NULL';
421
+ const params = [createAt, crowdinId];
422
+ if (integrationId) {
423
+ whereIntegrationCondition = 'integration_id = ?';
424
+ params.push(integrationId);
425
+ }
426
+ return this.run(`DELETE FROM user_errors WHERE created_at < ? AND crowdin_id = ? AND ${whereIntegrationCondition}`, params);
427
+ });
428
+ }
392
429
  }
393
430
  exports.SQLiteStorage = SQLiteStorage;
@@ -20,6 +20,7 @@ interface CustomAxiosError extends AxiosError {
20
20
  }
21
21
  export declare class AppModuleError extends Error {
22
22
  data: any;
23
+ appData: any;
23
24
  isAxiosError: boolean;
24
25
  constructor(error: CustomAxiosError | Error | string, data?: any);
25
26
  }
@@ -27,4 +28,10 @@ export declare class AppModuleAggregateError extends Error {
27
28
  errors: Error[] | AppModuleError[];
28
29
  constructor(errors: Error[] | AppModuleError[], message: string);
29
30
  }
31
+ export declare function handleUserError({ action, error, crowdinId, clientId, }: {
32
+ action: string;
33
+ error: Error | string;
34
+ crowdinId: string;
35
+ clientId?: string;
36
+ }): Promise<void>;
30
37
  export {};
@@ -22,9 +22,19 @@ var __importStar = (this && this.__importStar) || function (mod) {
22
22
  __setModuleDefault(result, mod);
23
23
  return result;
24
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
+ };
25
34
  Object.defineProperty(exports, "__esModule", { value: true });
26
- exports.AppModuleAggregateError = exports.AppModuleError = exports.getErrorMessage = exports.isAxiosError = exports.logError = exports.log = exports.withContextError = exports.withContext = exports.prepareContext = exports.initialize = void 0;
35
+ exports.handleUserError = exports.AppModuleAggregateError = exports.AppModuleError = exports.getErrorMessage = exports.isAxiosError = exports.logError = exports.log = exports.withContextError = exports.withContext = exports.prepareContext = exports.initialize = void 0;
27
36
  const logsFormatter = __importStar(require("@crowdin/logs-formatter"));
37
+ const storage_1 = require("../storage");
28
38
  let logConfig;
29
39
  let onError;
30
40
  function initialize(config) {
@@ -139,10 +149,10 @@ function errorOutputByType(error) {
139
149
  status: ((_j = error.response) === null || _j === void 0 ? void 0 : _j.status) || ((_k = error.data) === null || _k === void 0 ? void 0 : _k.response.status),
140
150
  }
141
151
  : {};
142
- console.error(message, { attributes: { request, response } }, { backtrace: error.stack });
152
+ console.error(message, { attributes: JSON.stringify({ request, response }) }, { backtrace: error.stack });
143
153
  }
144
154
  else {
145
- console.error(message, error.data ? { attributes: error.data } : '', error.stack ? { backtrace: error.stack } : '');
155
+ console.error(message, error.data ? { attributes: typeof error.data === 'object' ? JSON.stringify(error.data) : error.data } : '', error.stack ? { backtrace: error.stack } : '');
146
156
  }
147
157
  }
148
158
  function isAxiosError(e) {
@@ -168,8 +178,11 @@ class AppModuleError extends Error {
168
178
  super(typeof error === 'string' ? error : error.message);
169
179
  this.isAxiosError = false;
170
180
  this.name = 'AppModuleError';
171
- this.data = typeof error === 'string' ? Object.assign({}, data) : Object.assign(Object.assign({}, error), data);
172
181
  this.stack = typeof error === 'string' ? this.stack : error.stack;
182
+ this.appData = data;
183
+ if (typeof error !== 'string') {
184
+ this.data = Object.assign({}, error);
185
+ }
173
186
  if (isAxiosError(error)) {
174
187
  this.isAxiosError = true;
175
188
  }
@@ -184,3 +197,76 @@ class AppModuleAggregateError extends Error {
184
197
  }
185
198
  }
186
199
  exports.AppModuleAggregateError = AppModuleAggregateError;
200
+ function storeUserError({ action, error, crowdinId, clientId, }) {
201
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w;
202
+ return __awaiter(this, void 0, void 0, function* () {
203
+ const data = {};
204
+ if (isAxiosError(error)) {
205
+ data.requestParams = {
206
+ method: ((_a = error === null || error === void 0 ? void 0 : error.request) === null || _a === void 0 ? void 0 : _a.method) || ((_c = (_b = error === null || error === void 0 ? void 0 : error.data) === null || _b === void 0 ? void 0 : _b.request) === null || _c === void 0 ? void 0 : _c.method),
207
+ protocol: ((_d = error === null || error === void 0 ? void 0 : error.request) === null || _d === void 0 ? void 0 : _d.protocol) || ((_f = (_e = error === null || error === void 0 ? void 0 : error.data) === null || _e === void 0 ? void 0 : _e.request) === null || _f === void 0 ? void 0 : _f.protocol),
208
+ host: ((_g = error === null || error === void 0 ? void 0 : error.request) === null || _g === void 0 ? void 0 : _g.host) || ((_j = (_h = error === null || error === void 0 ? void 0 : error.data) === null || _h === void 0 ? void 0 : _h.request) === null || _j === void 0 ? void 0 : _j.host),
209
+ path: ((_k = error === null || error === void 0 ? void 0 : error.request) === null || _k === void 0 ? void 0 : _k.path) || ((_m = (_l = error === null || error === void 0 ? void 0 : error.data) === null || _l === void 0 ? void 0 : _l.request) === null || _m === void 0 ? void 0 : _m.path),
210
+ };
211
+ data.responseData = {
212
+ data: ((_o = error === null || error === void 0 ? void 0 : error.response) === null || _o === void 0 ? void 0 : _o.data) || ((_q = (_p = error === null || error === void 0 ? void 0 : error.data) === null || _p === void 0 ? void 0 : _p.response) === null || _q === void 0 ? void 0 : _q.data),
213
+ status: ((_r = error === null || error === void 0 ? void 0 : error.response) === null || _r === void 0 ? void 0 : _r.status) || ((_t = (_s = error === null || error === void 0 ? void 0 : error.data) === null || _s === void 0 ? void 0 : _s.response) === null || _t === void 0 ? void 0 : _t.status),
214
+ statusText: ((_u = error === null || error === void 0 ? void 0 : error.response) === null || _u === void 0 ? void 0 : _u.statusText) || ((_w = (_v = error === null || error === void 0 ? void 0 : error.data) === null || _v === void 0 ? void 0 : _v.response) === null || _w === void 0 ? void 0 : _w.statusText),
215
+ };
216
+ }
217
+ if (error instanceof AppModuleError && error.appData) {
218
+ data.appData = error.appData;
219
+ }
220
+ yield (0, storage_1.getStorage)().saveUserError(action, error.message, JSON.stringify(data), `${Date.now()}`, crowdinId, clientId);
221
+ });
222
+ }
223
+ function clearOldUserErrors(crowdinId, clientId) {
224
+ const date = new Date();
225
+ date.setMonth(date.getMonth() - 1); // previous month
226
+ (0, storage_1.getStorage)().deleteUserErrors(`${date.getTime()}`, crowdinId, clientId);
227
+ }
228
+ function mergeAppModuleAggregateErrors(errors) {
229
+ const result = [];
230
+ const mergedData = {};
231
+ for (const errorItem of errors) {
232
+ if (errorItem instanceof AppModuleError) {
233
+ if (typeof errorItem.appData === 'object' &&
234
+ errorItem.appData !== null &&
235
+ !Array.isArray(errorItem.appData)) {
236
+ if (!mergedData[errorItem.message]) {
237
+ mergedData[errorItem.message] = {};
238
+ }
239
+ for (const key in errorItem.appData) {
240
+ mergedData[errorItem.message][key] = (mergedData[errorItem.message][key] || []).concat(errorItem.appData[key]);
241
+ }
242
+ }
243
+ else if (errorItem.appData) {
244
+ mergedData[errorItem.message] = (mergedData[errorItem.message] || []).concat(errorItem.appData);
245
+ }
246
+ continue;
247
+ }
248
+ result.push(errorItem);
249
+ }
250
+ for (const key in mergedData) {
251
+ result.push(new AppModuleError(key, mergedData[key]));
252
+ }
253
+ return result;
254
+ }
255
+ function handleUserError({ action, error, crowdinId, clientId, }) {
256
+ return __awaiter(this, void 0, void 0, function* () {
257
+ if (typeof error === 'string') {
258
+ return;
259
+ }
260
+ if (error instanceof AppModuleAggregateError) {
261
+ const mergedErrors = mergeAppModuleAggregateErrors(error.errors);
262
+ for (const key in mergedErrors) {
263
+ yield handleUserError({ action, error: mergedErrors[key], crowdinId, clientId });
264
+ }
265
+ }
266
+ else {
267
+ yield storeUserError({ action, error, crowdinId, clientId });
268
+ clearOldUserErrors(crowdinId, clientId);
269
+ }
270
+ });
271
+ }
272
+ exports.handleUserError = handleUserError;
@@ -33,7 +33,9 @@
33
33
  </div>
34
34
  </crowdin-alert>
35
35
  {{/if}}
36
- <div>
36
+ <div id="buttons">
37
+ <crowdin-button id="show-integration-btn" class="hidden" icon-before="arrow_back" onclick="showIntegration();">Integration</crowdin-button>
38
+ <crowdin-button id="show-error-logs-btn" icon-before="list" onclick="showErrorLogs();">Error logs</crowdin-button>
37
39
  {{#if infoModal}}
38
40
  <crowdin-button icon-before="info" onclick="infoModal.open();">{{infoModal.title}}</crowdin-button>
39
41
  {{/if}}
@@ -87,6 +89,16 @@
87
89
  {{/if}}
88
90
  >
89
91
  </crowdin-simple-integration>
92
+ <div id="user-errors" class="hidden">
93
+ <crowdin-alert title="Error Logs">This table displays the most recent error logs from the past month. Logs older than one month will be automatically deleted.</crowdin-alert>
94
+ <crowdin-show-as-table
95
+ is-loading
96
+ id="user-errors-table"
97
+ is-searchable
98
+ total-records="25"
99
+ search-placeholder="Search something"
100
+ ></crowdin-show-as-table>
101
+ </div>
90
102
  </div>
91
103
  <crowdin-toasts></crowdin-toasts>
92
104
  <crowdin-modal id="subscription-modal" modal-width="50" close-button="false">
@@ -256,6 +268,13 @@
256
268
  </div>
257
269
  </crowdin-modal>
258
270
  {{/or}}
271
+
272
+ <crowdin-modal
273
+ id="user-error-detail"
274
+ close-button-title="Close"
275
+ close-button="true"
276
+ >
277
+ </crowdin-modal>
259
278
  </body>
260
279
  <script type="text/javascript">
261
280
  document.body.addEventListener('refreshFilesList', (e) => {
@@ -491,95 +510,94 @@
491
510
  }
492
511
 
493
512
  {{#if configurationFields}}
494
- const settingsModal = document.getElementById('settings-modal');
495
- const settingsSaveBtn = document.getElementById('settings-save-btn');
496
- let config = JSON.parse('{{{config}}}');
497
-
498
- function triggerEvent(el, type) {
499
- const e = document.createEvent('HTMLEvents');
500
- e.initEvent(type, false, true);
501
- el.dispatchEvent(e);
502
- }
513
+ const settingsModal = document.getElementById('settings-modal');
514
+ const settingsSaveBtn = document.getElementById('settings-save-btn');
515
+ let config = JSON.parse('{{{config}}}');
516
+
517
+ function triggerEvent(el, type) {
518
+ const e = document.createEvent('HTMLEvents');
519
+ e.initEvent(type, false, true);
520
+ el.dispatchEvent(e);
521
+ }
503
522
 
504
- function fillSettingsForm() {
505
- Object.entries(config).forEach(([key, value]) => {
506
- const el = document.getElementById(`${key}-settings`);
507
- if (el && (value || el.tagName.toLowerCase() === 'crowdin-checkbox')) {
508
- if (el.tagName.toLowerCase() === 'crowdin-select') {
509
- if (el.hasAttribute('is-multi')) {
510
- el.value = JSON.stringify(value);
523
+ function fillSettingsForm() {
524
+ Object.entries(config).forEach(([key, value]) => {
525
+ const el = document.getElementById(`${key}-settings`);
526
+ if (el && (value || el.tagName.toLowerCase() === 'crowdin-checkbox')) {
527
+ if (el.tagName.toLowerCase() === 'crowdin-select') {
528
+ if (el.hasAttribute('is-multi')) {
529
+ el.value = JSON.stringify(value);
530
+ } else {
531
+ el.value = JSON.stringify([value]);
532
+ }
533
+ } else if (el.tagName.toLowerCase() === 'crowdin-checkbox') {
534
+ el.checked = !!value;
511
535
  } else {
512
- el.value = JSON.stringify([value]);
536
+ el.value = value;
513
537
  }
514
- } else if (el.tagName.toLowerCase() === 'crowdin-checkbox') {
515
- el.checked = !!value;
516
- } else {
517
- el.value = value;
518
- }
519
538
 
520
- triggerEvent(el, 'change');
521
- }
522
- });
523
- }
524
-
525
- function saveSettings() {
526
- setLoader();
527
- const settingsElements = Array.from(document.getElementById('modal-content').children);
528
- const tags = ['crowdin-checkbox', 'crowdin-select', 'crowdin-input'];
529
- const configReq = {};
530
- settingsElements
531
- .filter(e => tags.includes(e.tagName.toLowerCase()))
532
- .forEach(e => {
533
- const key = e.getAttribute('key');
534
- let value;
535
- if (e.tagName.toLowerCase() === 'crowdin-select') {
536
- value = JSON.parse(e.value);
537
- if (!e.hasAttribute('is-multi')) {
538
- value = value.length > 0 ? value[0] : undefined;
539
- }
540
- } else if (e.tagName.toLowerCase() === 'crowdin-checkbox') {
541
- value = !!e.checked;
542
- } else {
543
- value = e.value;
539
+ triggerEvent(el, 'change');
544
540
  }
545
- configReq[key] = value;
546
541
  });
547
- settingsSaveBtn.setAttribute('disabled', true);
548
- checkOrigin()
549
- .then(restParams => fetch('api/settings' + restParams, {
550
- method: 'POST',
551
- headers: { 'Content-Type': 'application/json' },
552
- body: JSON.stringify({ config: configReq })
553
- }))
554
- .then(checkResponse)
555
- .then(() => {
556
- showToast('Settings successfully saved');
557
- config = configReq;
558
- })
559
- .catch(e => catchRejection(e, 'Can\'t save settings'))
560
- .finally(() => {
561
- unsetLoader();
562
- settingsSaveBtn.removeAttribute('disabled');
563
- settingsModal.close();
564
- {{#if reloadOnConfigSave}}
565
- getIntegrationData(true);
566
- getCrowdinData();
567
- {{/if}}
568
- });
569
- }
542
+ }
570
543
 
571
- function setLoader() {
572
- const loader = document.querySelector('#settings-modal .loader');
573
- loader.classList.remove('hidden');
574
- }
544
+ function saveSettings() {
545
+ setLoader();
546
+ const settingsElements = Array.from(document.getElementById('modal-content').children);
547
+ const tags = ['crowdin-checkbox', 'crowdin-select', 'crowdin-input'];
548
+ const configReq = {};
549
+ settingsElements
550
+ .filter(e => tags.includes(e.tagName.toLowerCase()))
551
+ .forEach(e => {
552
+ const key = e.getAttribute('key');
553
+ let value;
554
+ if (e.tagName.toLowerCase() === 'crowdin-select') {
555
+ value = JSON.parse(e.value);
556
+ if (!e.hasAttribute('is-multi')) {
557
+ value = value.length > 0 ? value[0] : undefined;
558
+ }
559
+ } else if (e.tagName.toLowerCase() === 'crowdin-checkbox') {
560
+ value = !!e.checked;
561
+ } else {
562
+ value = e.value;
563
+ }
564
+ configReq[key] = value;
565
+ });
566
+ settingsSaveBtn.setAttribute('disabled', true);
567
+ checkOrigin()
568
+ .then(restParams => fetch('api/settings' + restParams, {
569
+ method: 'POST',
570
+ headers: { 'Content-Type': 'application/json' },
571
+ body: JSON.stringify({ config: configReq })
572
+ }))
573
+ .then(checkResponse)
574
+ .then(() => {
575
+ showToast('Settings successfully saved');
576
+ config = configReq;
577
+ })
578
+ .catch(e => catchRejection(e, 'Can\'t save settings'))
579
+ .finally(() => {
580
+ unsetLoader();
581
+ settingsSaveBtn.removeAttribute('disabled');
582
+ settingsModal.close();
583
+ {{#if reloadOnConfigSave}}
584
+ getIntegrationData(true);
585
+ getCrowdinData();
586
+ {{/if}}
587
+ });
588
+ }
575
589
 
576
- function unsetLoader() {
577
- const loader = document.querySelector('#settings-modal .loader');
578
- setTimeout(function() {
579
- loader.classList.add('hidden');
580
- }, 500)
581
- }
590
+ function setLoader() {
591
+ const loader = document.querySelector('#settings-modal .loader');
592
+ loader.classList.remove('hidden');
593
+ }
582
594
 
595
+ function unsetLoader() {
596
+ const loader = document.querySelector('#settings-modal .loader');
597
+ setTimeout(function() {
598
+ loader.classList.add('hidden');
599
+ }, 500)
600
+ }
583
601
  {{else}}
584
602
  const settingsModal = undefined;
585
603
  {{/if}}
@@ -807,6 +825,95 @@
807
825
  const notice = document.getElementById('notice');
808
826
  checkAlert(notice, 'notice');
809
827
  {{/if}}
828
+
829
+ function showErrorLogs() {
830
+ document.getElementById('show-error-logs-btn').classList.add('hidden');
831
+ document.getElementById('show-integration-btn').classList.remove('hidden');
832
+
833
+ appComponent.classList.add('hidden');
834
+ document.getElementById('user-errors').classList.remove('hidden');
835
+
836
+ checkOrigin()
837
+ .then(restParams => fetch('api/user-errors' + restParams))
838
+ .then(checkResponse)
839
+ .then((res) => {
840
+ const table = document.getElementById('user-errors-table');
841
+
842
+ const clickRow = (field, index, item) => {
843
+ const modal = document.getElementById('user-error-detail');
844
+ modal.open();
845
+ modal.innerHTML = getUserErrorDetail(item);
846
+ };
847
+
848
+ table.setTableConfig && table.setTableConfig({
849
+ defaultSortingColumn: "createdAt",
850
+ defaultSortingDir: "desc",
851
+ headerFields: [
852
+ {
853
+ field: "createdAt",
854
+ name: "Date",
855
+ isSortable: true,
856
+ clickFn: clickRow,
857
+ },
858
+ {
859
+ field: "action",
860
+ name: "Action",
861
+ clickFn: clickRow,
862
+ },
863
+ {
864
+ field: "message",
865
+ name: "Message",
866
+ clickFn: clickRow,
867
+ }
868
+ ],
869
+ });
870
+
871
+ table.setTableData(JSON.stringify(res));
872
+ table.setAttribute(`is-loading`, false);
873
+ })
874
+ .catch(e => catchRejection(e, 'Can\'t fetch error logs'));
875
+ }
876
+
877
+ function showIntegration() {
878
+ document.getElementById('show-error-logs-btn').classList.remove('hidden');
879
+ document.getElementById('show-integration-btn').classList.add('hidden');
880
+
881
+ appComponent.classList.remove('hidden');
882
+ document.getElementById('user-errors').classList.add('hidden');
883
+ }
884
+
885
+ function getUserErrorDetail(error) {
886
+ const data = JSON.parse(error.data);
887
+
888
+ let html = '<div class="error-detail-table"><table>';
889
+ html += `<tr><td>Action</td><td>${error.action}</td></tr>`;
890
+ html += `<tr><td>Message</td><td>${error.message}</td></tr>`;
891
+ html += `<tr><td>Date/time</td><td>${error.createdAt}</td></tr>`;
892
+
893
+ if (data.requestParams) {
894
+ html += `<tr><td>Method</td><td>${data.requestParams.method}</td></tr>`;
895
+ }
896
+
897
+ if (data.responseData) {
898
+ html += `<tr><td>Response Data</td><td><pre>${JSON.stringify(data.responseData, null, 2)}</pre></td></tr>`;
899
+ }
900
+
901
+ if (data.appData) {
902
+ if (Array.isArray(data.appData)) {
903
+ html += `<tr><td>App Data</td><td><pre>${JSON.stringify(data.appData, null, 2)}</pre></td></tr>`;
904
+ } else if (typeof data.appData === 'object') {
905
+ for (const key in data.appData) {
906
+ html += `<tr><td>${key.charAt(0).toUpperCase() + key.slice(1)}</td><td><pre>${JSON.stringify(data.appData[key], null, 2)}</pre></td></tr>`;
907
+ }
908
+ } else {
909
+ html += `<tr><td>App Data</td><td>${data.appData}</td></tr>`;
910
+ }
911
+ }
912
+
913
+ html += '</table></div>';
914
+
915
+ return html;
916
+ }
810
917
  </script>
811
918
 
812
919
  </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crowdin/app-project-module",
3
- "version": "0.38.0",
3
+ "version": "0.39.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",
@@ -17,7 +17,7 @@
17
17
  "@aws-sdk/client-s3": "^3.423.0",
18
18
  "@aws-sdk/s3-request-presigner": "^3.423.0",
19
19
  "@crowdin/crowdin-apps-functions": "0.6.0",
20
- "@crowdin/logs-formatter": "^2.0.5",
20
+ "@crowdin/logs-formatter": "^2.0.6",
21
21
  "@godaddy/terminus": "^4.12.1",
22
22
  "@types/pg": "^8.10.3",
23
23
  "amqplib": "^0.10.3",