@crowdin/app-project-module 0.79.1 → 0.80.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.
package/out/index.js CHANGED
@@ -191,7 +191,7 @@ function addCrowdinEndpoints(app, clientConfig) {
191
191
  webhooks.register({ config, app });
192
192
  workflowStepType.register({ config, app });
193
193
  addFormSchema({ config, app });
194
- return Object.assign(Object.assign({}, exports.metadataStore), { establishCrowdinConnection: (authRequest, moduleKey) => {
194
+ return Object.assign(Object.assign({}, exports.metadataStore), { storage: storage.getStorage(), establishCrowdinConnection: (authRequest, moduleKey) => {
195
195
  let jwtToken = '';
196
196
  if (typeof authRequest === 'string') {
197
197
  jwtToken = authRequest;
@@ -8,13 +8,25 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  step((generator = generator.apply(thisArg, _arguments || [])).next());
9
9
  });
10
10
  };
11
+ var __rest = (this && this.__rest) || function (s, e) {
12
+ var t = {};
13
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
14
+ t[p] = s[p];
15
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
16
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
17
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
18
+ t[p[i]] = s[p[i]];
19
+ }
20
+ return t;
21
+ };
11
22
  Object.defineProperty(exports, "__esModule", { value: true });
12
23
  const util_1 = require("../../../util");
13
24
  const logger_1 = require("../../../util/logger");
14
25
  function handle(aiPromptProvider) {
15
26
  return (0, util_1.runAsyncWrapper)((req, res) => __awaiter(this, void 0, void 0, function* () {
16
27
  try {
17
- const compiledPrompt = yield aiPromptProvider.compile(req.body.options, req.body.payload, req.crowdinApiClient, req.crowdinContext);
28
+ const _a = req.body, { options, payload } = _a, body = __rest(_a, ["options", "payload"]);
29
+ const compiledPrompt = yield aiPromptProvider.compile(options, payload, req.crowdinApiClient, req.crowdinContext, body);
18
30
  res.send({ data: { content: compiledPrompt } });
19
31
  }
20
32
  catch (e) {
@@ -20,7 +20,7 @@ function register({ config, app }) {
20
20
  checkSubscriptionExpiration: true,
21
21
  moduleKey: config.aiPromptProvider.key,
22
22
  }), (0, compile_1.default)(config.aiPromptProvider));
23
- if (config.aiPromptProvider.formSchema) {
23
+ if (config.aiPromptProvider.formSchema || config.aiPromptProvider.uiPath) {
24
24
  app.use('/prompt-provider/settings', (0, ui_module_1.default)({ config, allowUnauthorized: true, moduleType: config.aiPromptProvider.key }), (0, render_ui_module_1.default)(config.aiPromptProvider));
25
25
  }
26
26
  app.get((0, util_1.getLogoUrl)(config.aiPromptProvider, '/ai-prompt-provider'), (req, res) => { var _a; return res.sendFile(((_a = config.aiPromptProvider) === null || _a === void 0 ? void 0 : _a.imagePath) || config.imagePath); });
@@ -4,5 +4,5 @@ export interface AiPromptProviderModule extends UiModule {
4
4
  /**
5
5
  * generates prompt text based on provided options
6
6
  */
7
- compile: (options: any, payload: any, client: Crowdin, context: CrowdinContextInfo) => Promise<string>;
7
+ compile: (options: any, payload: any, client: Crowdin, context: CrowdinContextInfo, requestData: any) => Promise<string>;
8
8
  }
@@ -240,7 +240,7 @@ function handle(config) {
240
240
  // prevent possible overrides of the other modules
241
241
  config.aiPromptProvider = Object.assign(Object.assign({}, config.aiPromptProvider), { key: config.identifier + '-ai-prompt-provider' });
242
242
  modules['ai-prompt-provider'] = [
243
- Object.assign({ key: config.aiPromptProvider.key, name: config.aiPromptProvider.name || config.name, logo: (0, util_1.getLogoUrl)(config.aiPromptProvider, '/ai-prompt-provider'), compileUrl: '/prompt-provider/compile' }, (!!config.aiPromptProvider.formSchema
243
+ Object.assign({ key: config.aiPromptProvider.key, name: config.aiPromptProvider.name || config.name, logo: (0, util_1.getLogoUrl)(config.aiPromptProvider, '/ai-prompt-provider'), compileUrl: '/prompt-provider/compile' }, (config.aiPromptProvider.formSchema || config.aiPromptProvider.uiPath
244
244
  ? {
245
245
  configuratorUrl: '/prompt-provider/settings/' + (config.aiPromptProvider.fileName || 'index.html'),
246
246
  }
@@ -68,6 +68,17 @@ export interface Storage {
68
68
  saveUnsyncedFiles(params: UnsyncedFiles): Promise<void>;
69
69
  getUnsyncedFiles(params: GetUnsyncedFiles): Promise<UnsyncedFiles | undefined>;
70
70
  updateUnsyncedFiles(params: UnsyncedFiles): Promise<void>;
71
+ registerCustomTable(tableName: string, schema: Record<string, string>): Promise<void>;
72
+ insertRecord(tableName: string, data: Record<string, any>): Promise<void>;
73
+ selectRecords(tableName: string, options?: {
74
+ columns?: string[];
75
+ whereClause?: string;
76
+ orderBy?: string;
77
+ limit?: number;
78
+ distinct?: boolean;
79
+ }, params?: any[]): Promise<any[]>;
80
+ updateRecord(tableName: string, data: Record<string, any>, whereClause: string, params?: any[]): Promise<void>;
81
+ deleteRecord(tableName: string, whereClause: string, params?: any[]): Promise<void>;
71
82
  }
72
83
  export declare function initialize(config: Config | UnauthorizedConfig): Promise<void>;
73
84
  export declare function getStorage(): Storage;
@@ -84,4 +84,15 @@ export declare class MySQLStorage implements Storage {
84
84
  saveUnsyncedFiles({ integrationId, crowdinId, files }: UnsyncedFiles): Promise<void>;
85
85
  updateUnsyncedFiles({ integrationId, crowdinId, files }: UnsyncedFiles): Promise<void>;
86
86
  getUnsyncedFiles({ integrationId, crowdinId }: GetUnsyncedFiles): Promise<UnsyncedFiles | undefined>;
87
+ registerCustomTable(tableName: string, schema: Record<string, string>): Promise<void>;
88
+ insertRecord(tableName: string, data: Record<string, any>): Promise<void>;
89
+ selectRecords(tableName: string, options?: {
90
+ columns?: string[];
91
+ whereClause?: string;
92
+ orderBy?: string;
93
+ limit?: number;
94
+ distinct?: boolean;
95
+ }, params?: any[]): Promise<any[]>;
96
+ updateRecord(tableName: string, data: Record<string, any>, whereClause: string, params?: any[]): Promise<void>;
97
+ deleteRecord(tableName: string, whereClause: string, params?: any[]): Promise<void>;
87
98
  }
@@ -433,9 +433,17 @@ class MySQLStorage {
433
433
  }
434
434
  deleteWebhooks(fileIds, integrationId, crowdinId, provider) {
435
435
  return __awaiter(this, void 0, void 0, function* () {
436
+ if (!fileIds.length) {
437
+ return;
438
+ }
436
439
  yield this.dbPromise;
437
440
  const placeholders = fileIds.map(() => '?').join(',');
438
- 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]));
441
+ yield this.executeQuery((connection) => connection.execute(`DELETE
442
+ FROM webhooks
443
+ WHERE file_id IN (${placeholders})
444
+ AND integration_id = ?
445
+ AND crowdin_id = ?
446
+ AND provider = ?`, [...fileIds, integrationId, crowdinId, provider]));
439
447
  });
440
448
  }
441
449
  getAllUserErrors(crowdinId, integrationId) {
@@ -448,7 +456,9 @@ class MySQLStorage {
448
456
  whereIntegrationCondition = 'integration_id = ?';
449
457
  params.push(integrationId);
450
458
  }
451
- const [rows] = yield connection.execute(`SELECT id, action, message, data, created_at as "createdAt" FROM user_errors WHERE crowdin_id = ? AND ${whereIntegrationCondition}`, params);
459
+ const [rows] = yield connection.execute(`SELECT id, action, message, data, created_at as "createdAt"
460
+ FROM user_errors
461
+ WHERE crowdin_id = ? AND ${whereIntegrationCondition}`, params);
452
462
  return rows || [];
453
463
  }));
454
464
  });
@@ -469,7 +479,11 @@ class MySQLStorage {
469
479
  whereIntegrationCondition = 'integration_id = ?';
470
480
  params.push(integrationId);
471
481
  }
472
- return connection.execute(`DELETE FROM user_errors WHERE created_at < ? AND crowdin_id = ? AND ${whereIntegrationCondition}`, params);
482
+ return connection.execute(`DELETE
483
+ FROM user_errors
484
+ WHERE created_at < ?
485
+ AND crowdin_id = ?
486
+ AND ${whereIntegrationCondition}`, params);
473
487
  });
474
488
  });
475
489
  }
@@ -522,7 +536,7 @@ class MySQLStorage {
522
536
  yield this.executeQuery((connection) => connection.execute(`
523
537
  INSERT INTO job(id, integration_id, crowdin_id, type, payload, title, created_at)
524
538
  VALUES (?, ?, ?, ?, ?, ?, ?)
525
- `, [id, integrationId, crowdinId, type, payload, title, Date.now().toString()]));
539
+ `, [id, integrationId, crowdinId, type, payload, title, Date.now().toString()]));
526
540
  return id;
527
541
  });
528
542
  }
@@ -564,7 +578,7 @@ class MySQLStorage {
564
578
  UPDATE job
565
579
  SET ${updateFields.join(', ')}
566
580
  WHERE id = ?
567
- `, updateParams));
581
+ `, updateParams));
568
582
  });
569
583
  }
570
584
  getJob({ id }) {
@@ -572,11 +586,23 @@ class MySQLStorage {
572
586
  yield this.dbPromise;
573
587
  return this.executeQuery((connection) => __awaiter(this, void 0, void 0, function* () {
574
588
  const [rows] = yield connection.execute(`
575
- SELECT id, integration_id as "integrationId", crowdin_id as "crowdinId", type, payload, progress, status,
576
- title, info, data, attempt, created_at as "createdAt", updated_at as "updatedAt", finished_at as "finishedAt"
577
- FROM job
578
- WHERE id = ?
579
- `, [id]);
589
+ SELECT id,
590
+ integration_id as "integrationId",
591
+ crowdin_id as "crowdinId",
592
+ type,
593
+ payload,
594
+ progress,
595
+ status,
596
+ title,
597
+ info,
598
+ data,
599
+ attempt,
600
+ created_at as "createdAt",
601
+ updated_at as "updatedAt",
602
+ finished_at as "finishedAt"
603
+ FROM job
604
+ WHERE id = ?
605
+ `, [id]);
580
606
  return (rows || [])[0];
581
607
  }));
582
608
  });
@@ -586,11 +612,25 @@ class MySQLStorage {
586
612
  yield this.dbPromise;
587
613
  return this.executeQuery((connection) => __awaiter(this, void 0, void 0, function* () {
588
614
  const [rows] = yield connection.execute(`
589
- SELECT id, integration_id as "integrationId", crowdin_id as "crowdinId", type, payload, progress, status,
590
- title, info, data, attempt, created_at as "createdAt", updated_at as "updatedAt", finished_at as "finishedAt"
591
- FROM job
592
- WHERE integration_id = ? AND crowdin_id = ? AND finished_at is NULL
593
- `, [integrationId, crowdinId]);
615
+ SELECT id,
616
+ integration_id as "integrationId",
617
+ crowdin_id as "crowdinId",
618
+ type,
619
+ payload,
620
+ progress,
621
+ status,
622
+ title,
623
+ info,
624
+ data,
625
+ attempt,
626
+ created_at as "createdAt",
627
+ updated_at as "updatedAt",
628
+ finished_at as "finishedAt"
629
+ FROM job
630
+ WHERE integration_id = ?
631
+ AND crowdin_id = ?
632
+ AND finished_at is NULL
633
+ `, [integrationId, crowdinId]);
594
634
  return rows || [];
595
635
  }));
596
636
  });
@@ -606,11 +646,24 @@ class MySQLStorage {
606
646
  yield this.dbPromise;
607
647
  return this.executeQuery((connection) => __awaiter(this, void 0, void 0, function* () {
608
648
  const [rows] = yield connection.execute(`
609
- SELECT id, integration_id as "integrationId", crowdin_id as "crowdinId", type, payload, progress, status,
610
- title, info, data, attempt, created_at as "createdAt", updated_at as "updatedAt", finished_at as "finishedAt"
611
- FROM job
612
- WHERE status IN (?, ?) AND finished_at is NULL
613
- `, [types_1.JobStatus.IN_PROGRESS, types_1.JobStatus.CREATED]);
649
+ SELECT id,
650
+ integration_id as "integrationId",
651
+ crowdin_id as "crowdinId",
652
+ type,
653
+ payload,
654
+ progress,
655
+ status,
656
+ title,
657
+ info,
658
+ data,
659
+ attempt,
660
+ created_at as "createdAt",
661
+ updated_at as "updatedAt",
662
+ finished_at as "finishedAt"
663
+ FROM job
664
+ WHERE status IN (?, ?)
665
+ AND finished_at is NULL
666
+ `, [types_1.JobStatus.IN_PROGRESS, types_1.JobStatus.CREATED]);
614
667
  return rows || [];
615
668
  }));
616
669
  });
@@ -621,7 +674,7 @@ class MySQLStorage {
621
674
  yield this.executeQuery((connection) => connection.execute(`
622
675
  INSERT INTO translation_file_cache(integration_id, crowdin_id, file_id, language_id, etag)
623
676
  VALUES (?, ?, ?, ?, ?)
624
- `, [integrationId, crowdinId, fileId, languageId, etag]));
677
+ `, [integrationId, crowdinId, fileId, languageId, etag]));
625
678
  });
626
679
  }
627
680
  getFileTranslationCache({ integrationId, crowdinId, fileId, }) {
@@ -629,10 +682,16 @@ class MySQLStorage {
629
682
  yield this.dbPromise;
630
683
  return this.executeQuery((connection) => __awaiter(this, void 0, void 0, function* () {
631
684
  const [rows] = yield connection.execute(`
632
- SELECT integration_id as "integrationId", crowdin_id as "crowdinId", file_id as "fileId", language_id as "languageId", etag
633
- FROM translation_file_cache
634
- WHERE integration_id = ? AND crowdin_id = ? AND file_id = ?
635
- `, [integrationId, crowdinId, fileId]);
685
+ SELECT integration_id as "integrationId",
686
+ crowdin_id as "crowdinId",
687
+ file_id as "fileId",
688
+ language_id as "languageId",
689
+ etag
690
+ FROM translation_file_cache
691
+ WHERE integration_id = ?
692
+ AND crowdin_id = ?
693
+ AND file_id = ?
694
+ `, [integrationId, crowdinId, fileId]);
636
695
  return rows || [];
637
696
  }));
638
697
  });
@@ -642,10 +701,17 @@ class MySQLStorage {
642
701
  yield this.dbPromise;
643
702
  return this.executeQuery((connection) => __awaiter(this, void 0, void 0, function* () {
644
703
  const [rows] = yield connection.execute(`
645
- SELECT integration_id as "integrationId", crowdin_id as "crowdinId", file_id as "fileId", language_id as "languageId", etag
646
- FROM translation_file_cache
647
- WHERE integration_id = ? AND crowdin_id = ? AND file_id = ? AND language_id = ?
648
- `, [integrationId, crowdinId, fileId, languageId]);
704
+ SELECT integration_id as "integrationId",
705
+ crowdin_id as "crowdinId",
706
+ file_id as "fileId",
707
+ language_id as "languageId",
708
+ etag
709
+ FROM translation_file_cache
710
+ WHERE integration_id = ?
711
+ AND crowdin_id = ?
712
+ AND file_id = ?
713
+ AND language_id = ?
714
+ `, [integrationId, crowdinId, fileId, languageId]);
649
715
  return (rows || [])[0];
650
716
  }));
651
717
  });
@@ -656,8 +722,11 @@ class MySQLStorage {
656
722
  yield this.executeQuery((connection) => connection.execute(`
657
723
  UPDATE translation_file_cache
658
724
  SET etag = ?
659
- WHERE integration_id = ? AND crowdin_id = ? AND file_id = ? AND language_id = ?
660
- `, [etag, integrationId, crowdinId, fileId, languageId]));
725
+ WHERE integration_id = ?
726
+ AND crowdin_id = ?
727
+ AND file_id = ?
728
+ AND language_id = ?
729
+ `, [etag, integrationId, crowdinId, fileId, languageId]));
661
730
  });
662
731
  }
663
732
  saveUnsyncedFiles({ integrationId, crowdinId, files }) {
@@ -666,7 +735,7 @@ class MySQLStorage {
666
735
  yield this.executeQuery((connection) => connection.execute(`
667
736
  INSERT INTO unsynced_files(integration_id, crowdin_id, files)
668
737
  VALUES (?, ?, ?,)
669
- `, [integrationId, crowdinId, files]));
738
+ `, [integrationId, crowdinId, files]));
670
739
  });
671
740
  }
672
741
  updateUnsyncedFiles({ integrationId, crowdinId, files }) {
@@ -675,8 +744,9 @@ class MySQLStorage {
675
744
  yield this.executeQuery((connection) => connection.execute(`
676
745
  UPDATE unsynced_files
677
746
  SET files = ?
678
- WHERE integration_id = ? AND crowdin_id = ?
679
- `, [files, integrationId, crowdinId]));
747
+ WHERE integration_id = ?
748
+ AND crowdin_id = ?
749
+ `, [files, integrationId, crowdinId]));
680
750
  });
681
751
  }
682
752
  getUnsyncedFiles({ integrationId, crowdinId }) {
@@ -684,13 +754,83 @@ class MySQLStorage {
684
754
  yield this.dbPromise;
685
755
  return this.executeQuery((connection) => __awaiter(this, void 0, void 0, function* () {
686
756
  const [rows] = yield connection.execute(`
687
- SELECT fileIds
688
- FROM unsynced_files
689
- WHERE integration_id = ? AND crowdin_id = ?
690
- `, [integrationId, crowdinId]);
757
+ SELECT fileIds
758
+ FROM unsynced_files
759
+ WHERE integration_id = ?
760
+ AND crowdin_id = ?
761
+ `, [integrationId, crowdinId]);
691
762
  return (rows || [])[0];
692
763
  }));
693
764
  });
694
765
  }
766
+ registerCustomTable(tableName, schema) {
767
+ return __awaiter(this, void 0, void 0, function* () {
768
+ const columns = Object.entries(schema)
769
+ .map(([col, def]) => `${col} ${def}`)
770
+ .join(', ');
771
+ const query = `CREATE TABLE IF NOT EXISTS ${tableName}
772
+ (
773
+ ${columns}
774
+ );`;
775
+ yield this.executeQuery((connection) => __awaiter(this, void 0, void 0, function* () {
776
+ yield connection.execute(query);
777
+ return;
778
+ }));
779
+ });
780
+ }
781
+ insertRecord(tableName, data) {
782
+ return __awaiter(this, void 0, void 0, function* () {
783
+ const columns = Object.keys(data).join(', ');
784
+ const placeholders = Object.keys(data)
785
+ .map(() => '?')
786
+ .join(', ');
787
+ const values = Object.values(data);
788
+ const query = `INSERT INTO ${tableName} (${columns})
789
+ VALUES (${placeholders});`;
790
+ yield this.executeQuery((connection) => __awaiter(this, void 0, void 0, function* () {
791
+ yield connection.execute(query, values);
792
+ return;
793
+ }));
794
+ });
795
+ }
796
+ selectRecords(tableName, options = {}, params = []) {
797
+ var _a;
798
+ return __awaiter(this, void 0, void 0, function* () {
799
+ const columns = ((_a = options.columns) === null || _a === void 0 ? void 0 : _a.length) ? options.columns.join(', ') : '*';
800
+ const distinctKeyword = options.distinct ? 'DISTINCT ' : '';
801
+ const whereClause = options.whereClause ? ` ${options.whereClause}` : '';
802
+ const orderByClause = options.orderBy ? ` ORDER BY ${options.orderBy}` : '';
803
+ const limitClause = options.limit ? ` LIMIT ${options.limit}` : '';
804
+ const query = `SELECT ${distinctKeyword}${columns} FROM ${tableName}${whereClause}${orderByClause}${limitClause};`;
805
+ return this.executeQuery((connection) => __awaiter(this, void 0, void 0, function* () {
806
+ const [rows] = yield connection.execute(query, params);
807
+ return rows;
808
+ }));
809
+ });
810
+ }
811
+ updateRecord(tableName, data, whereClause, params = []) {
812
+ return __awaiter(this, void 0, void 0, function* () {
813
+ const setClause = Object.keys(data)
814
+ .map((key) => `${key} = ?`)
815
+ .join(', ');
816
+ const values = Object.values(data);
817
+ const query = `UPDATE ${tableName}
818
+ SET ${setClause} ${whereClause};`;
819
+ yield this.executeQuery((connection) => __awaiter(this, void 0, void 0, function* () {
820
+ yield connection.execute(query, [...values, ...params]);
821
+ return;
822
+ }));
823
+ });
824
+ }
825
+ deleteRecord(tableName, whereClause, params = []) {
826
+ return __awaiter(this, void 0, void 0, function* () {
827
+ const query = `DELETE
828
+ FROM ${tableName} ${whereClause};`;
829
+ yield this.executeQuery((connection) => __awaiter(this, void 0, void 0, function* () {
830
+ yield connection.execute(query, params);
831
+ return;
832
+ }));
833
+ });
834
+ }
695
835
  }
696
836
  exports.MySQLStorage = MySQLStorage;
@@ -99,5 +99,16 @@ export declare class PostgreStorage implements Storage {
99
99
  saveUnsyncedFiles({ integrationId, crowdinId, files }: UnsyncedFiles): Promise<void>;
100
100
  updateUnsyncedFiles({ integrationId, crowdinId, files }: UnsyncedFiles): Promise<void>;
101
101
  getUnsyncedFiles({ integrationId, crowdinId }: GetUnsyncedFiles): Promise<UnsyncedFiles | undefined>;
102
+ registerCustomTable(tableName: string, schema: Record<string, string>): Promise<void>;
103
+ insertRecord(tableName: string, data: Record<string, any>): Promise<void>;
104
+ selectRecords(tableName: string, options?: {
105
+ columns?: string[];
106
+ whereClause?: string;
107
+ orderBy?: string;
108
+ limit?: number;
109
+ distinct?: boolean;
110
+ }, params?: any[]): Promise<any[]>;
111
+ updateRecord(tableName: string, data: Record<string, any>, whereClause: string, params?: any[]): Promise<void>;
112
+ deleteRecord(tableName: string, whereClause: string, params?: any[]): Promise<void>;
102
113
  }
103
114
  export {};
@@ -516,6 +516,9 @@ class PostgreStorage {
516
516
  }
517
517
  deleteWebhooks(fileIds, integrationId, crowdinId, provider) {
518
518
  return __awaiter(this, void 0, void 0, function* () {
519
+ if (!fileIds.length) {
520
+ return;
521
+ }
519
522
  let index = 0;
520
523
  const placeholders = fileIds.map(() => `$${++index}`).join(',');
521
524
  yield this.dbPromise;
@@ -813,5 +816,69 @@ class PostgreStorage {
813
816
  }));
814
817
  });
815
818
  }
819
+ registerCustomTable(tableName, schema) {
820
+ return __awaiter(this, void 0, void 0, function* () {
821
+ const columns = Object.entries(schema)
822
+ .map(([col, def]) => `"${col}" ${def}`)
823
+ .join(', ');
824
+ const query = `CREATE TABLE IF NOT EXISTS "${tableName}" (${columns});`;
825
+ yield this.executeQuery((client) => __awaiter(this, void 0, void 0, function* () {
826
+ yield client.query(query);
827
+ return;
828
+ }));
829
+ });
830
+ }
831
+ insertRecord(tableName, data) {
832
+ return __awaiter(this, void 0, void 0, function* () {
833
+ const columns = Object.keys(data)
834
+ .map((col) => `"${col}"`)
835
+ .join(', ');
836
+ const placeholders = Object.keys(data)
837
+ .map((_, i) => `$${i + 1}`)
838
+ .join(', ');
839
+ const values = Object.values(data);
840
+ const query = `INSERT INTO "${tableName}" (${columns}) VALUES (${placeholders});`;
841
+ yield this.executeQuery((client) => __awaiter(this, void 0, void 0, function* () {
842
+ yield client.query(query, values);
843
+ return;
844
+ }));
845
+ });
846
+ }
847
+ selectRecords(tableName, options = {}, params = []) {
848
+ var _a;
849
+ return __awaiter(this, void 0, void 0, function* () {
850
+ const columns = ((_a = options.columns) === null || _a === void 0 ? void 0 : _a.length) ? options.columns.join(', ') : '*';
851
+ const distinctKeyword = options.distinct ? 'DISTINCT ' : '';
852
+ const whereClause = options.whereClause ? ` ${options.whereClause}` : '';
853
+ const orderByClause = options.orderBy ? ` ORDER BY ${options.orderBy}` : '';
854
+ const limitClause = options.limit ? ` LIMIT ${options.limit}` : '';
855
+ const query = `SELECT ${distinctKeyword}${columns} FROM ${tableName}${whereClause}${orderByClause}${limitClause};`;
856
+ return this.executeQuery((client) => __awaiter(this, void 0, void 0, function* () {
857
+ const res = yield client.query(query, params);
858
+ return res.rows;
859
+ }));
860
+ });
861
+ }
862
+ updateRecord(tableName, data, whereClause, params = []) {
863
+ return __awaiter(this, void 0, void 0, function* () {
864
+ const keys = Object.keys(data);
865
+ const setClause = keys.map((key, index) => `"${key}" = $${index + 1}`).join(', ');
866
+ const values = Object.values(data);
867
+ const query = `UPDATE "${tableName}" SET ${setClause} ${whereClause};`;
868
+ yield this.executeQuery((client) => __awaiter(this, void 0, void 0, function* () {
869
+ yield client.query(query, [...values, ...params]);
870
+ return;
871
+ }));
872
+ });
873
+ }
874
+ deleteRecord(tableName, whereClause, params = []) {
875
+ return __awaiter(this, void 0, void 0, function* () {
876
+ const query = `DELETE FROM ${tableName} ${whereClause};`;
877
+ yield this.executeQuery((client) => __awaiter(this, void 0, void 0, function* () {
878
+ yield client.query(query, params);
879
+ return;
880
+ }));
881
+ });
882
+ }
816
883
  }
817
884
  exports.PostgreStorage = PostgreStorage;
@@ -87,4 +87,15 @@ export declare class SQLiteStorage implements Storage {
87
87
  saveUnsyncedFiles({ integrationId, crowdinId, files }: UnsyncedFiles): Promise<void>;
88
88
  updateUnsyncedFiles({ integrationId, crowdinId, files }: UnsyncedFiles): Promise<void>;
89
89
  getUnsyncedFiles({ integrationId, crowdinId }: GetUnsyncedFiles): Promise<UnsyncedFiles | undefined>;
90
+ registerCustomTable(tableName: string, schema: Record<string, string>): Promise<void>;
91
+ insertRecord(tableName: string, data: Record<string, any>): Promise<void>;
92
+ selectRecords(tableName: string, options?: {
93
+ columns?: string[];
94
+ whereClause?: string;
95
+ orderBy?: string;
96
+ limit?: number;
97
+ distinct?: boolean;
98
+ }, params?: any[]): Promise<any[]>;
99
+ updateRecord(tableName: string, data: Record<string, any>, whereClause: string, params?: any[]): Promise<void>;
100
+ deleteRecord(tableName: string, whereClause: string, params?: any[]): Promise<void>;
90
101
  }
@@ -489,6 +489,9 @@ class SQLiteStorage {
489
489
  }
490
490
  deleteWebhooks(fileIds, integrationId, crowdinId, provider) {
491
491
  return __awaiter(this, void 0, void 0, function* () {
492
+ if (!fileIds.length) {
493
+ return;
494
+ }
492
495
  const placeholders = fileIds.map(() => '?').join(',');
493
496
  return this.run(`DELETE FROM webhooks WHERE file_id IN (${placeholders}) AND integration_id = ? AND crowdin_id = ? AND provider = ?`, [...fileIds, integrationId, crowdinId, provider]);
494
497
  });
@@ -691,5 +694,53 @@ class SQLiteStorage {
691
694
  }
692
695
  });
693
696
  }
697
+ registerCustomTable(tableName, schema) {
698
+ return __awaiter(this, void 0, void 0, function* () {
699
+ const columns = Object.entries(schema)
700
+ .map(([col, def]) => `${col} ${def}`)
701
+ .join(', ');
702
+ const query = `CREATE TABLE IF NOT EXISTS ${tableName} (${columns});`;
703
+ yield this.run(query, []);
704
+ });
705
+ }
706
+ insertRecord(tableName, data) {
707
+ return __awaiter(this, void 0, void 0, function* () {
708
+ const columns = Object.keys(data).join(', ');
709
+ const placeholders = Object.keys(data)
710
+ .map(() => '?')
711
+ .join(', ');
712
+ const values = Object.values(data);
713
+ const query = `INSERT INTO ${tableName} (${columns}) VALUES (${placeholders});`;
714
+ return this.run(query, values);
715
+ });
716
+ }
717
+ selectRecords(tableName, options = {}, params = []) {
718
+ var _a;
719
+ return __awaiter(this, void 0, void 0, function* () {
720
+ const columns = ((_a = options.columns) === null || _a === void 0 ? void 0 : _a.length) ? options.columns.join(', ') : '*';
721
+ const distinctKeyword = options.distinct ? 'DISTINCT ' : '';
722
+ const whereClause = options.whereClause ? ` ${options.whereClause}` : '';
723
+ const orderByClause = options.orderBy ? ` ORDER BY ${options.orderBy}` : '';
724
+ const limitClause = options.limit ? ` LIMIT ${options.limit}` : '';
725
+ const query = `SELECT ${distinctKeyword}${columns} FROM ${tableName}${whereClause}${orderByClause}${limitClause};`;
726
+ return this.each(query, params);
727
+ });
728
+ }
729
+ updateRecord(tableName, data, whereClause, params = []) {
730
+ return __awaiter(this, void 0, void 0, function* () {
731
+ const setClause = Object.keys(data)
732
+ .map((key) => `${key} = ?`)
733
+ .join(', ');
734
+ const values = Object.values(data);
735
+ const query = `UPDATE ${tableName} SET ${setClause} ${whereClause};`;
736
+ yield this.run(query, [...values, ...params]);
737
+ });
738
+ }
739
+ deleteRecord(tableName, whereClause, params = []) {
740
+ return __awaiter(this, void 0, void 0, function* () {
741
+ const query = `DELETE FROM ${tableName} ${whereClause};`;
742
+ yield this.run(query, params);
743
+ });
744
+ }
694
745
  }
695
746
  exports.SQLiteStorage = SQLiteStorage;
package/out/types.d.ts CHANGED
@@ -7,6 +7,7 @@ import { CustomSpellcheckerModule } from './modules/custom-spell-check/types';
7
7
  import { EditorPanels } from './modules/editor-right-panel/types';
8
8
  import { CustomFileFormatLogic, FilePostExportLogic, FilePostImportLogic, FilePreExportLogic, FilePreImportLogic } from './modules/file-processing/types';
9
9
  import { IntegrationLogic } from './modules/integration/types';
10
+ import { Storage } from './storage';
10
11
  import { MySQLStorageConfig } from './storage/mysql';
11
12
  import { PostgreStorageConfig } from './storage/postgre';
12
13
  import { ApiModule } from './modules/api/types';
@@ -353,6 +354,7 @@ export interface CrowdinAppUtilities extends CrowdinMetadataStore {
353
354
  client: Crowdin;
354
355
  extra: Record<string, any>;
355
356
  }>;
357
+ storage: Storage;
356
358
  }
357
359
  export interface CrowdinMetadataStore {
358
360
  saveMetadata: (id: string, metadata: any, crowdinId?: string) => Promise<void>;
@@ -919,6 +919,186 @@
919
919
  }
920
920
  }
921
921
 
922
+ function hideConfirmUsersBlock() {
923
+ const confirmUsersBlock = document.querySelector('.confirm-users-block');
924
+ confirmUsersBlock.classList.add('hidden');
925
+
926
+ const permissionsModal = document.getElementById('permissions-modal');
927
+ permissionsModal.setAttribute('body-overflow-unset', true)
928
+ }
929
+
930
+ function showPermissionsDialog() {
931
+ hideConfirmUsersBlock();
932
+ openModal(permissions)
933
+ setLoader('#permissions-modal');
934
+ const select = document.getElementById('users');
935
+
936
+ select.value = '[]';
937
+
938
+ checkOrigin()
939
+ .then(restParams => fetch('api/users' + restParams))
940
+ .then(checkResponse)
941
+ .then((res) => {
942
+ let userOptions = res.data.users.map(user => `<option value="${user.id}">${sanitizeHTML(user.name)}</option>`).join('');
943
+ select.innerHTML = userOptions;
944
+ select.value = JSON.stringify(res.data.managers);
945
+ })
946
+ .catch(e => catchRejection(e, 'Can\'t fetch users'))
947
+ .finally(() => unsetLoader('#permissions-modal'));
948
+ }
949
+
950
+ function showConfirmUsersBlock() {
951
+ const confirmUsersBlock = document.querySelector('.confirm-users-block');
952
+ confirmUsersBlock.classList.remove('hidden');
953
+
954
+ const permissionsModal = document.getElementById('permissions-modal');
955
+ permissionsModal.removeAttribute('body-overflow-unset')
956
+ }
957
+
958
+ async function inviteUsers(onlyCheck = true) {
959
+ setLoader('#permissions-modal');
960
+
961
+ const select = document.getElementById('users');
962
+
963
+ if (onlyCheck && select.value === '[]') {
964
+ hideConfirmUsersBlock();
965
+ unsetLoader('#permissions-modal');
966
+ return;
967
+ }
968
+
969
+ const params = {
970
+ users: JSON.parse(select.value),
971
+ onlyCheck,
972
+ };
973
+
974
+ checkOrigin()
975
+ .then(restParams => fetch('api/invite-users' + restParams, {
976
+ method: 'POST',
977
+ headers: { 'Content-Type': 'application/json' },
978
+ body: JSON.stringify(params)
979
+ }))
980
+ .then(checkResponse)
981
+ .then((response) => {
982
+ if (!onlyCheck) {
983
+ showToast('Users successfully updated');
984
+ hideConfirmUsersBlock();
985
+ closeModal(permissions);
986
+ return;
987
+ }
988
+
989
+ prepareUsersConfirmBlock(response.data);
990
+ })
991
+ .catch(e => {
992
+ catchRejection(e, e?.error || 'Can\'t invite users')
993
+ })
994
+ .finally(() => unsetLoader('#permissions-modal'));
995
+ }
996
+
997
+ function prepareUsersConfirmBlock(usersData) {
998
+ showConfirmUsersBlock();
999
+
1000
+ const organizationInvite = document.querySelector('.organization-invite');
1001
+ const projectInvite = document.querySelector('.project-invite');
1002
+ const applicationCredentialsInvite = document.querySelector('.application-credentials-invite');
1003
+
1004
+ const grantedElement = '<crowdin-p>&horbar;</crowdin-p>';
1005
+ const willGrantedElement = '<span class="badge badge-will-be-granted">Will Be Granted</span>';
1006
+ const notAvailableElement = '<span class="badge badge-not-available">Action Required</span>';
1007
+
1008
+ // only in enterprise
1009
+ if (organizationInvite) {
1010
+ const organizationWillGrantElement = `<span class="badge badge-will-be-granted">Will Be Registered</span>`;
1011
+
1012
+ processUsersWhoWillBeInvited(organizationInvite, usersData.usersWhoWillBeInvitedToOrganization, organizationWillGrantElement, grantedElement);
1013
+ }
1014
+
1015
+ processUsersWhoWillBeInvited(projectInvite, usersData.usersWhoWillBeInvitedToProject, willGrantedElement, grantedElement);
1016
+ processUsersWhoWillBeInvited(applicationCredentialsInvite, usersData.usersWhoNotIssetInIntegration, willGrantedElement, grantedElement);
1017
+
1018
+ processUsersWhoWillBeInvitedApplicationSettings(usersData, willGrantedElement, grantedElement, notAvailableElement);
1019
+ }
1020
+
1021
+ function processUsersWhoWillBeInvited(element, users, grantMessage, alreadyGrantedMessage) {
1022
+ const tooltip = element.querySelector('.status');
1023
+ const userList = element.querySelector('.affected-users');
1024
+
1025
+ if (users.length) {
1026
+ tooltip.innerHTML = grantMessage;
1027
+
1028
+ let affectedUsers = '<ul>';
1029
+ for (const user of users) {
1030
+ affectedUsers += `<li><crowdin-p>${sanitizeHTML(user.name)}</crowdin-p></li>`;
1031
+ }
1032
+ affectedUsers += '</ul>';
1033
+ userList.innerHTML = affectedUsers;
1034
+ } else {
1035
+ tooltip.innerHTML = alreadyGrantedMessage;
1036
+ userList.innerHTML = alreadyGrantedMessage;
1037
+ }
1038
+ }
1039
+
1040
+ function processUsersWhoWillBeInvitedApplicationSettings(usersData, willGrantedElement, grantedElement, notAvailableElement) {
1041
+ const applicationSettingsInvite = document.querySelector('.application-settings-invite');
1042
+
1043
+ const tooltip = applicationSettingsInvite.querySelector('.status');
1044
+ const description = applicationSettingsInvite.querySelector('.permission-description');
1045
+ const userList = applicationSettingsInvite.querySelector('.affected-users');
1046
+
1047
+ let descriptionMessage = 'This can be configured in organization settings (Apps section).';
1048
+ description.classList.remove('text-warning');
1049
+ description.innerText = descriptionMessage;
1050
+
1051
+ let affectedUsers = '<ul>';
1052
+
1053
+ if (!usersData.editApplicationAvailable) {
1054
+ descriptionMessage += ' The application doesn\'t have permission to update this setting.';
1055
+ descriptionMessage += usersData.isAdmin ? ' Please reinstall the app.' : ' Please ask the organization admin to reinstall the app.';
1056
+
1057
+ description.classList.add('text-warning');
1058
+ description.innerText = descriptionMessage;
1059
+
1060
+ tooltip.innerHTML = notAvailableElement;
1061
+ for (const user of usersData.usersWhoNotIssetInApplicationInstallation) {
1062
+ affectedUsers += `<li><crowdin-p>${sanitizeHTML(user.name)}</crowdin-p></li>`;
1063
+ }
1064
+ userList.innerHTML = affectedUsers + '</ul>';
1065
+ } else if (!usersData.usersWhoNotIssetInApplicationInstallation.length) {
1066
+ tooltip.innerHTML = grantedElement;
1067
+ userList.innerHTML = grantedElement;
1068
+ } else {
1069
+ if (usersData.isAdmin) {
1070
+ tooltip.innerHTML = willGrantedElement;
1071
+ for (const user of usersData.usersWhoNotIssetInApplicationInstallation) {
1072
+ affectedUsers += `<li><crowdin-p>${sanitizeHTML(user.name)}</crowdin-p></li>`;
1073
+ }
1074
+ userList.innerHTML = affectedUsers + '</ul>';
1075
+ } else {
1076
+ descriptionMessage += ' Only organization admins have permission to update this setting.';
1077
+
1078
+ description.classList.add('text-warning');
1079
+ description.innerText = descriptionMessage;
1080
+
1081
+ tooltip.innerHTML = notAvailableElement;
1082
+ for (const user of usersData.usersWhoNotIssetInApplicationInstallation) {
1083
+ affectedUsers += `<li><crowdin-p>${sanitizeHTML(user.name)}</crowdin-p></li>`;
1084
+ }
1085
+ userList.innerHTML = affectedUsers + '</ul>';
1086
+ }
1087
+ }
1088
+ }
1089
+
1090
+ function setLoader(id) {
1091
+ const loader = document.querySelector(`${id} .loader`);
1092
+ loader.classList.remove('hidden');
1093
+ }
1094
+
1095
+ function unsetLoader(id) {
1096
+ const loader = document.querySelector(`${id} .loader`);
1097
+ setTimeout(function() {
1098
+ loader.classList.add('hidden');
1099
+ }, 500)
1100
+ }
1101
+
922
1102
  {{#if configurationFields}}
923
1103
  const settingsModal = document.getElementById('settings-modal');
924
1104
  const settingsSaveBtn = document.getElementById('settings-save-btn');
@@ -998,186 +1178,6 @@
998
1178
  {{/if}}
999
1179
  });
1000
1180
  }
1001
-
1002
- function showConfirmUsersBlock() {
1003
- const confirmUsersBlock = document.querySelector('.confirm-users-block');
1004
- confirmUsersBlock.classList.remove('hidden');
1005
-
1006
- const permissionsModal = document.getElementById('permissions-modal');
1007
- permissionsModal.removeAttribute('body-overflow-unset')
1008
- }
1009
-
1010
- function hideConfirmUsersBlock() {
1011
- const confirmUsersBlock = document.querySelector('.confirm-users-block');
1012
- confirmUsersBlock.classList.add('hidden');
1013
-
1014
- const permissionsModal = document.getElementById('permissions-modal');
1015
- permissionsModal.setAttribute('body-overflow-unset', true)
1016
- }
1017
-
1018
- function showPermissionsDialog() {
1019
- hideConfirmUsersBlock();
1020
- openModal(permissions)
1021
- setLoader('#permissions-modal');
1022
- const select = document.getElementById('users');
1023
-
1024
- select.value = '[]';
1025
-
1026
- checkOrigin()
1027
- .then(restParams => fetch('api/users' + restParams))
1028
- .then(checkResponse)
1029
- .then((res) => {
1030
- let userOptions = res.data.users.map(user => `<option value="${user.id}">${sanitizeHTML(user.name)}</option>`).join('');
1031
- select.innerHTML = userOptions;
1032
- select.value = JSON.stringify(res.data.managers);
1033
- })
1034
- .catch(e => catchRejection(e, 'Can\'t fetch users'))
1035
- .finally(() => unsetLoader('#permissions-modal'));
1036
- }
1037
-
1038
- async function inviteUsers(onlyCheck = true) {
1039
- setLoader('#permissions-modal');
1040
-
1041
- const select = document.getElementById('users');
1042
-
1043
- if (onlyCheck && select.value === '[]') {
1044
- hideConfirmUsersBlock();
1045
- unsetLoader('#permissions-modal');
1046
- return;
1047
- }
1048
-
1049
- const params = {
1050
- users: JSON.parse(select.value),
1051
- onlyCheck,
1052
- };
1053
-
1054
- checkOrigin()
1055
- .then(restParams => fetch('api/invite-users' + restParams, {
1056
- method: 'POST',
1057
- headers: { 'Content-Type': 'application/json' },
1058
- body: JSON.stringify(params)
1059
- }))
1060
- .then(checkResponse)
1061
- .then((response) => {
1062
- if (!onlyCheck) {
1063
- showToast('Users successfully updated');
1064
- hideConfirmUsersBlock();
1065
- closeModal(permissions);
1066
- return;
1067
- }
1068
-
1069
- prepareUsersConfirmBlock(response.data);
1070
- })
1071
- .catch(e => {
1072
- catchRejection(e, e?.error || 'Can\'t invite users')
1073
- })
1074
- .finally(() => unsetLoader('#permissions-modal'));
1075
- }
1076
-
1077
- function prepareUsersConfirmBlock(usersData) {
1078
- showConfirmUsersBlock();
1079
-
1080
- const organizationInvite = document.querySelector('.organization-invite');
1081
- const projectInvite = document.querySelector('.project-invite');
1082
- const applicationCredentialsInvite = document.querySelector('.application-credentials-invite');
1083
-
1084
- const grantedElement = '<crowdin-p>&horbar;</crowdin-p>';
1085
- const willGrantedElement = '<span class="badge badge-will-be-granted">Will Be Granted</span>';
1086
- const notAvailableElement = '<span class="badge badge-not-available">Action Required</span>';
1087
-
1088
- // only in enterprise
1089
- if (organizationInvite) {
1090
- const organizationWillGrantElement = `<span class="badge badge-will-be-granted">Will Be Registered</span>`;
1091
-
1092
- processUsersWhoWillBeInvited(organizationInvite, usersData.usersWhoWillBeInvitedToOrganization, organizationWillGrantElement, grantedElement);
1093
- }
1094
-
1095
- processUsersWhoWillBeInvited(projectInvite, usersData.usersWhoWillBeInvitedToProject, willGrantedElement, grantedElement);
1096
- processUsersWhoWillBeInvited(applicationCredentialsInvite, usersData.usersWhoNotIssetInIntegration, willGrantedElement, grantedElement);
1097
-
1098
- processUsersWhoWillBeInvitedApplicationSettings(usersData, willGrantedElement, grantedElement, notAvailableElement);
1099
- }
1100
-
1101
- function processUsersWhoWillBeInvited(element, users, grantMessage, alreadyGrantedMessage) {
1102
- const tooltip = element.querySelector('.status');
1103
- const userList = element.querySelector('.affected-users');
1104
-
1105
- if (users.length) {
1106
- tooltip.innerHTML = grantMessage;
1107
-
1108
- let affectedUsers = '<ul>';
1109
- for (const user of users) {
1110
- affectedUsers += `<li><crowdin-p>${sanitizeHTML(user.name)}</crowdin-p></li>`;
1111
- }
1112
- affectedUsers += '</ul>';
1113
- userList.innerHTML = affectedUsers;
1114
- } else {
1115
- tooltip.innerHTML = alreadyGrantedMessage;
1116
- userList.innerHTML = alreadyGrantedMessage;
1117
- }
1118
- }
1119
-
1120
- function processUsersWhoWillBeInvitedApplicationSettings(usersData, willGrantedElement, grantedElement, notAvailableElement) {
1121
- const applicationSettingsInvite = document.querySelector('.application-settings-invite');
1122
-
1123
- const tooltip = applicationSettingsInvite.querySelector('.status');
1124
- const description = applicationSettingsInvite.querySelector('.permission-description');
1125
- const userList = applicationSettingsInvite.querySelector('.affected-users');
1126
-
1127
- let descriptionMessage = 'This can be configured in organization settings (Apps section).';
1128
- description.classList.remove('text-warning');
1129
- description.innerText = descriptionMessage;
1130
-
1131
- let affectedUsers = '<ul>';
1132
-
1133
- if (!usersData.editApplicationAvailable) {
1134
- descriptionMessage += ' The application doesn\'t have permission to update this setting.';
1135
- descriptionMessage += usersData.isAdmin ? ' Please reinstall the app.' : ' Please ask the organization admin to reinstall the app.';
1136
-
1137
- description.classList.add('text-warning');
1138
- description.innerText = descriptionMessage;
1139
-
1140
- tooltip.innerHTML = notAvailableElement;
1141
- for (const user of usersData.usersWhoNotIssetInApplicationInstallation) {
1142
- affectedUsers += `<li><crowdin-p>${sanitizeHTML(user.name)}</crowdin-p></li>`;
1143
- }
1144
- userList.innerHTML = affectedUsers + '</ul>';
1145
- } else if (!usersData.usersWhoNotIssetInApplicationInstallation.length) {
1146
- tooltip.innerHTML = grantedElement;
1147
- userList.innerHTML = grantedElement;
1148
- } else {
1149
- if (usersData.isAdmin) {
1150
- tooltip.innerHTML = willGrantedElement;
1151
- for (const user of usersData.usersWhoNotIssetInApplicationInstallation) {
1152
- affectedUsers += `<li><crowdin-p>${sanitizeHTML(user.name)}</crowdin-p></li>`;
1153
- }
1154
- userList.innerHTML = affectedUsers + '</ul>';
1155
- } else {
1156
- descriptionMessage += ' Only organization admins have permission to update this setting.';
1157
-
1158
- description.classList.add('text-warning');
1159
- description.innerText = descriptionMessage;
1160
-
1161
- tooltip.innerHTML = notAvailableElement;
1162
- for (const user of usersData.usersWhoNotIssetInApplicationInstallation) {
1163
- affectedUsers += `<li><crowdin-p>${sanitizeHTML(user.name)}</crowdin-p></li>`;
1164
- }
1165
- userList.innerHTML = affectedUsers + '</ul>';
1166
- }
1167
- }
1168
- }
1169
-
1170
- function setLoader(id) {
1171
- const loader = document.querySelector(`${id} .loader`);
1172
- loader.classList.remove('hidden');
1173
- }
1174
-
1175
- function unsetLoader(id) {
1176
- const loader = document.querySelector(`${id} .loader`);
1177
- setTimeout(function() {
1178
- loader.classList.add('hidden');
1179
- }, 500)
1180
- }
1181
1181
  {{else}}
1182
1182
  const settingsModal = undefined;
1183
1183
  {{/if}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crowdin/app-project-module",
3
- "version": "0.79.1",
3
+ "version": "0.80.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",