@crowdin/app-project-module 0.50.0 → 0.51.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.
@@ -0,0 +1,1158 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ {{> head}}
4
+
5
+ <body>
6
+ <div class="i_w">
7
+ <div class="top">
8
+ {{#if notice}}
9
+ <crowdin-alert
10
+ id="notice"
11
+ title="{{notice.title}}"
12
+ {{#unless notice.icon}}
13
+ no-icon="true"
14
+ {{/unless}}
15
+ {{#if notice.type}}
16
+ type={{notice.type}}
17
+ {{/if}}
18
+ style="display: none; margin-bottom: 12px;"
19
+ >
20
+ <div class="box-center">
21
+ <p class="m-0">{{{notice.content}}}</p>
22
+ </div>
23
+ {{#if notice.close}}
24
+ <crowdin-button onclick="closeAlert(this, 'notice')" class="dismiss-alert" icon>close</crowdin-button>
25
+ {{/if}}
26
+ </crowdin-alert>
27
+ {{/if}}
28
+ {{#if checkSubscription}}
29
+ <crowdin-alert id="subscription-info" no-icon="true" type="warning" style="display: none; margin-bottom: 12px;">
30
+ <div class="box-center">
31
+ <p class="m-0"></p>
32
+ <crowdin-button class="ml-1" primary onclick="window.open(subscriptionLink,'_blank')">Subscribe</crowdin-button>
33
+ </div>
34
+ </crowdin-alert>
35
+ {{/if}}
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>
39
+ {{#if infoModal}}
40
+ <crowdin-button icon-before="info" onclick="infoModal.open();">{{infoModal.title}}</crowdin-button>
41
+ {{/if}}
42
+ {{#if configurationFields}}
43
+ <crowdin-button icon-before="settings" onclick="settingsModal.open();fillSettingsForm();">Settings</crowdin-button>
44
+ {{/if}}
45
+ <crowdin-button icon-before="account_circle" onclick="integrationLogout()">Log out</crowdin-button>
46
+ </div>
47
+ {{#if uploadTranslations}}
48
+ <crowdin-alert id="translation-info" no-icon="true" style="display: none;">
49
+ <div class="box-center">
50
+ <p class="info-text">
51
+ We recommend importing existing translations into your Crowdin project for newly uploaded source content.
52
+ Please note that translations imported for non-key-value formats may require additional review.
53
+ Read more about <crowdin-a href="https://support.crowdin.com/uploading-translations/" target="_blank">Uploading Translations</crowdin-a>.
54
+ </p>
55
+ </div>
56
+ <crowdin-button onclick="closeAlert(this)" class="dismiss-alert" icon>close</crowdin-button>
57
+ </crowdin-alert>
58
+ {{/if}}
59
+ </div>
60
+ <crowdin-simple-integration
61
+ async-progress
62
+ {{#if syncNewElements.crowdin}}
63
+ skip-crowdin-auto-schedule
64
+ {{/if}}
65
+ {{#if syncNewElements.integration}}
66
+ skip-integration-auto-schedule
67
+ {{/if}}
68
+ {{#or withCronSync.crowdin webhooks.crowdin}}
69
+ crowdin-schedule="true"
70
+ {{/or}}
71
+ {{#or withCronSync.integration webhooks.integration}}
72
+ integration-schedule="true"
73
+ {{/or}}
74
+ {{#if integrationOneLevelFetching}}
75
+ integration-one-level-fetching="true"
76
+ {{/if}}
77
+ {{#if integrationSearchListener}}
78
+ allow-filter-change-listener="true"
79
+ {{/if}}
80
+ {{#if integrationPagination}}
81
+ integration-load-more-files="true"
82
+ {{/if}}
83
+ integration-name="{{name}}"
84
+ integration-logo="logo.png"
85
+ {{#if uploadTranslations}}
86
+ integration-button-menu-items='[{"label":"Upload Translations", "action":"uploadTranslations"}]'
87
+ {{/if}}
88
+ {{#if filtering.crowdinLanguages}}
89
+ crowdin-filter
90
+ {{/if}}
91
+ >
92
+ </crowdin-simple-integration>
93
+ <div id="user-errors" class="hidden">
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
+ <crowdin-alert>This table displays the most recent error logs from the past month. Logs older than one month will be automatically deleted.</crowdin-alert>
102
+ </div>
103
+ </div>
104
+ <crowdin-toasts></crowdin-toasts>
105
+ <crowdin-async-progress
106
+ cancelAsyncAction=""
107
+ ></crowdin-async-progress>
108
+ <crowdin-modal id="subscription-modal" modal-width="50" close-button="false">
109
+ <div>
110
+ <crowdin-alert type="warning">Subscribe to continue using the {{name}} app.</crowdin-alert>
111
+ </div>
112
+ <div slot="footer">
113
+ <crowdin-button primary onclick="window.open(subscriptionLink,'_blank')">Subscribe</crowdin-button>
114
+ <crowdin-button outlined onclick="window.open('https://crowdin.com/contacts','_blank')">Contact us</crowdin-button>
115
+ <crowdin-button class="ml-10" secondary onclick="integrationLogout()">Log out</crowdin-button>
116
+ </div>
117
+ </crowdin-modal>
118
+ {{#if infoModal}}
119
+ <crowdin-modal id="info-modal" modal-width="50" modal-title="{{infoModal.title}}" close-button-title="Close">
120
+ <div>
121
+ {{{infoModal.content}}}
122
+ </div>
123
+ </crowdin-modal>
124
+ {{/if}}
125
+ {{#if configurationFields}}
126
+ <crowdin-modal
127
+ id="settings-modal"
128
+ body-overflow-unset="{{#checkLength configurationFields 3}}false{{else}}true{{/checkLength}}"
129
+ modal-width="50"
130
+ modal-title="Settings"
131
+ close-button-title="Close"
132
+ >
133
+ <div class="loader hidden">
134
+ <crowdin-progress-indicator></crowdin-progress-indicator>
135
+ </div>
136
+ <div id="modal-content">
137
+ {{#each configurationFields}}
138
+ {{#if key}}
139
+ {{#ifeq type "checkbox"}}
140
+ <crowdin-checkbox
141
+ id="{{key}}-settings"
142
+ key="{{key}}"
143
+ label="{{label}}"
144
+ value="false"
145
+ use-switch
146
+ {{#if helpText}}
147
+ help-text="{{helpText}}"
148
+ {{/if}}
149
+ {{#if helpTextHtml}}
150
+ help-text-html="{{helpTextHtml}}"
151
+ {{/if}}
152
+ {{#ifeq defaultValue true}}
153
+ checked="{{defaultValue}}"
154
+ {{/ifeq}}
155
+ {{#if dependencySettings}}
156
+ data-dependency="{{dependencySettings}}"
157
+ {{/if}}
158
+ >
159
+ </crowdin-checkbox>
160
+ {{/ifeq}}
161
+ {{#ifeq type "select"}}
162
+ <crowdin-select
163
+ {{#if isMulti}}
164
+ is-multi
165
+ close-on-select="false"
166
+ {{/if}}
167
+ {{#if isSearchable}}
168
+ is-searchable
169
+ {{/if}}
170
+ id="{{key}}-settings"
171
+ key="{{key}}"
172
+ label="{{label}}"
173
+ {{#if helpText}}
174
+ help-text="{{helpText}}"
175
+ {{/if}}
176
+ {{#if helpTextHtml}}
177
+ help-text-html="{{helpTextHtml}}"
178
+ {{/if}}
179
+ {{#if dependencySettings}}
180
+ data-dependency="{{dependencySettings}}"
181
+ {{/if}}
182
+ >
183
+ {{#each options}}
184
+ <option
185
+ {{#if ../defaultValue}}
186
+ {{#ifeq ../defaultValue value}} selected {{/ifeq}}
187
+ {{#in ../defaultValue value}} selected {{/in}}
188
+ {{/if}}
189
+ value="{{value}}">{{label}}
190
+ </option>
191
+ {{/each}}
192
+ </crowdin-select>
193
+ {{/ifeq}}
194
+ {{#ifeq type "text"}}
195
+ <crowdin-input
196
+ id="{{key}}-settings"
197
+ label="{{label}}"
198
+ key="{{key}}"
199
+ with-fixed-height
200
+ {{#if helpText}}
201
+ help-text="{{helpText}}"
202
+ {{/if}}
203
+ {{#if helpTextHtml}}
204
+ help-text-html="{{helpTextHtml}}"
205
+ {{/if}}
206
+ {{#if dependencySettings}}
207
+ data-dependency="{{dependencySettings}}"
208
+ {{/if}}
209
+ value="{{#if defaultValue}}{{defaultValue}}{{/if}}"
210
+ >
211
+ </crowdin-input>
212
+ {{/ifeq}}
213
+ {{#ifeq type "textarea"}}
214
+ <crowdin-textarea
215
+ id="{{key}}-settings"
216
+ label="{{label}}"
217
+ key="{{key}}"
218
+ {{#if helpText}}
219
+ help-text="{{helpText}}"
220
+ {{/if}}
221
+ {{#if helpTextHtml}}
222
+ help-text-html="{{helpTextHtml}}"
223
+ {{/if}}
224
+ {{#if dependencySettings}}
225
+ data-dependency="{{dependencySettings}}"
226
+ {{/if}}
227
+ value="{{#if defaultValue}}{{defaultValue}}{{/if}}">
228
+ </crowdin-textarea>
229
+ {{/ifeq}}
230
+ {{else}}
231
+ {{#if labelHtml}}
232
+ <crowdin-p
233
+ {{#if dependencySettings}}
234
+ data-dependency="{{dependencySettings}}"
235
+ {{/if}}
236
+ >
237
+ {{{labelHtml}}}
238
+ </crowdin-p>
239
+ {{else}}
240
+ <crowdin-p
241
+ {{#if dependencySettings}}
242
+ data-dependency="{{dependencySettings}}"
243
+ {{/if}}
244
+ >
245
+ {{label}}
246
+ </crowdin-p>
247
+ {{/if}}
248
+ {{/if}}
249
+ <div
250
+ style="padding: 8px"
251
+ {{#if dependencySettings}}
252
+ data-dependency="{{dependencySettings}}"
253
+ {{/if}}
254
+ ></div>
255
+ {{/each}}
256
+ </div>
257
+ <div slot="footer">
258
+ <crowdin-button id="settings-save-btn" outlined onclick="saveSettings()">Save</crowdin-button>
259
+ </div>
260
+ </crowdin-modal>
261
+ {{/if}}
262
+ {{#or syncNewElements.crowdin syncNewElements.integration}}
263
+ <crowdin-modal
264
+ id="confirm-schedule-modal"
265
+ modal-width="50"
266
+ modal-title="Synchronization options"
267
+ close-button-title="Close"
268
+ close-button="true"
269
+ body-overflow-unset
270
+ >
271
+ <crowdin-checkbox
272
+ id="selected-files"
273
+ name="selected-files"
274
+ label="Selected files"
275
+ class="hydrated"
276
+ onchange="onChangeAutoSynchronizationOptions(this)"
277
+ >
278
+ </crowdin-checkbox>
279
+ <crowdin-checkbox
280
+ id="new-files"
281
+ name="new-files"
282
+ label="New files"
283
+ class="hydrated"
284
+ onchange="onChangeAutoSynchronizationOptions(this)"
285
+ >
286
+ </crowdin-checkbox>
287
+ <div slot="footer">
288
+ <crowdin-button outlined id="save-schedule-sync" onclick="saveScheduleSync()">Save</crowdin-button>
289
+ </div>
290
+ </crowdin-modal>
291
+ {{/or}}
292
+
293
+ <crowdin-modal
294
+ id="user-error-detail"
295
+ close-button-title="Close"
296
+ close-button="true"
297
+ >
298
+ </crowdin-modal>
299
+ </body>
300
+ <script type="text/javascript">
301
+ document.body.addEventListener('refreshFilesList', (e) => {
302
+ if (e.detail.refreshIntegration) {
303
+ getIntegrationData(true);
304
+ } else if (e.detail.refreshCrowdin) {
305
+ getCrowdinData();
306
+ }
307
+ });
308
+ document.body.addEventListener('crowdinFilesFolderToggled', (event) => {
309
+ if (event.detail.componentId === 'crowdin-files' && event.detail.isOpen && event.detail.type === '1') {
310
+ getFileProgress(event.detail.id);
311
+ }
312
+ {{#if integrationOneLevelFetching}}
313
+ if (event.detail.componentId === 'integration-files' && event.detail.isOpen) {
314
+ getIntegrationData(false, event.detail.id);
315
+ }
316
+ {{/if}}
317
+ });
318
+ document.body.addEventListener('uploadFilesToCrowdin', uploadFilesToCrowdin);
319
+ document.body.addEventListener('uploadFilesToIntegration', uploadFilesToIntegration);
320
+ document.body.addEventListener('cancelAsyncAction', (e) => {
321
+ cancelJob(e.detail);
322
+ });
323
+ {{#if integrationSearchListener}}
324
+ document.body.addEventListener("integrationFilterChange", (event) => {
325
+ getIntegrationData(false, 0, event.detail);
326
+ })
327
+ {{/if}}
328
+ const appComponent = document.querySelector('crowdin-simple-integration');
329
+ const subscriptionModal = document.getElementById('subscription-modal');
330
+
331
+ const folderType = '0';
332
+ const fileType = '1';
333
+ const branchType = '2';
334
+
335
+ const JOB_TYPE = {
336
+ updateCrowdin: 'updateCrowdin',
337
+ updateIntegration: 'updateIntegration',
338
+ integrationSyncSettingsSave: 'integrationSyncSettingsSave',
339
+ crowdinSyncSettingsSave: 'crowdinSyncSettingsSave',
340
+ };
341
+
342
+ const JOB_STATUS = {
343
+ created: 'created',
344
+ inProgress: 'inProgress',
345
+ failed: 'failed',
346
+ canceled: 'canceled',
347
+ finished: 'finished',
348
+ };
349
+
350
+ const silentJobs = [
351
+ JOB_TYPE.integrationSyncSettingsSave,
352
+ JOB_TYPE.crowdinSyncSettingsSave,
353
+ ];
354
+
355
+ const asyncJobs = {};
356
+
357
+ let project = {};
358
+ let crowdinData = [];
359
+
360
+ getCrowdinData();
361
+ getIntegrationData();
362
+ getActiveJobs();
363
+
364
+ function integrationLogout() {
365
+ checkOrigin()
366
+ .then(queryParams => fetch(`api/logout${queryParams}`, { method: 'POST' }))
367
+ .then(checkResponse)
368
+ .then(reloadLocation)
369
+ .then(localStorage.removeItem('revised_{{name}}'))
370
+ .catch(e => catchRejection(e, 'Looks like you are not logged in'));
371
+ }
372
+
373
+ function getCrowdinData() {
374
+ appComponent.setAttribute('is-crowdin-loading', true);
375
+ checkOrigin()
376
+ .then(restParams => fetch('api/crowdin/files' + restParams))
377
+ .then(checkResponse)
378
+ .then((res) => {
379
+ const tree = res.map(e => {
380
+ const item = {
381
+ parent_id: e.parentId ? e.parentId : '0',
382
+ name: e.name,
383
+ id: e.id,
384
+ customContent: e.customContent,
385
+ labels: e.labels,
386
+ };
387
+ if (e.type) {
388
+ item.type = e.type;
389
+ item.node_type = fileType;
390
+ } else {
391
+ item.node_type = e.nodeType || folderType;
392
+ }
393
+ return item;
394
+ });
395
+ crowdinData = tree;
396
+ appComponent.setCrowdinFilesData(tree);
397
+ return checkOrigin();
398
+ })
399
+ .then(restParams => fetch('api/crowdin/project' + restParams))
400
+ .then(checkResponse)
401
+ .then((res) => {
402
+ project = res;
403
+ const languagesSorted = project.targetLanguages.sort((a, b) => a.name.localeCompare(b.name));
404
+
405
+ if (project.inContext) {
406
+ languagesSorted.push({...project.inContextPseudoLanguage, inContext: true});
407
+ }
408
+
409
+ appComponent.setCrowdinLanguagesData(languagesSorted)
410
+ })
411
+ {{#or withCronSync webhooks}}
412
+ .then(() => getSyncSettings('crowdin'))
413
+ {{/or}}
414
+ .catch(e => catchRejection(e, 'Can\'t fetch Crowdin data'))
415
+ .finally(() => (appComponent.setAttribute('is-crowdin-loading', false)));
416
+ }
417
+
418
+ function getIntegrationData(hardReload = false, parentId = '', search = '', page = 0) {
419
+ appComponent.setAttribute('is-integration-loading', true);
420
+ checkOrigin()
421
+ .then(restParams => fetch(`api/integration/data${restParams}&parent_id=${encodeURIComponent(parentId)}&search=${encodeURIComponent(search)}&page=${page}`))
422
+ .then(checkResponse)
423
+ .then((res) => {
424
+ const files = res.data;
425
+ const stopPagination = res.stopPagination;
426
+ const tree = files.map(e => {
427
+ const item = {
428
+ parent_id: e.parentId ? e.parentId : '0',
429
+ name: e.name,
430
+ id: e.id,
431
+ customContent: e.customContent,
432
+ labels: e.labels,
433
+ };
434
+ if (e.type) {
435
+ item.type = e.type;
436
+ item.node_type = fileType;
437
+ } else {
438
+ item.node_type = e.nodeType || folderType;
439
+ }
440
+ return item;
441
+ });
442
+
443
+ const appIntegrationFiles = appComponent.querySelector('#integration-files');
444
+ if (hardReload) {
445
+ appComponent.setIntegrationFilesData(tree);
446
+ } else if (tree.length) {
447
+ appComponent.pushIntegrationFilesData(tree).then(() => {
448
+ appIntegrationFiles.getSelected().then(selection => {
449
+ selection = selection?.filter((node) => node);
450
+ if (!selection?.length) {
451
+ return;
452
+ }
453
+
454
+ const selectedIds = selection.map(({id}) => id.toString());
455
+ tree.forEach((node) => {
456
+ selectedIds.includes(node.parent_id.toString()) && selectedIds.push(node.id.toString());
457
+ });
458
+ appIntegrationFiles.setSelected(selectedIds);
459
+ });
460
+ });
461
+ }
462
+ {{#if integrationPagination}}
463
+ if (stopPagination) {
464
+ appIntegrationFiles.setAttribute('load-more-disabled', true);
465
+ } else {
466
+ appIntegrationFiles.setAttribute('load-more-disabled', false);
467
+ }
468
+ {{/if}}
469
+ if (search) {
470
+ const openIds = files.filter(e => !e.type).map(e => e.id);
471
+ appComponent.setIntegrationOpenedFolders(openIds);
472
+ }
473
+ })
474
+ {{#or withCronSync webhooks}}
475
+ .then(() => getSyncSettings('integration'))
476
+ {{/or}}
477
+ .catch(e => catchRejection(e, 'Can\'t fetch {{name}} templates'))
478
+ .finally(() => (appComponent.setAttribute('is-integration-loading', false)));
479
+ }
480
+
481
+ function getSyncSettings(provider) {
482
+ checkOrigin()
483
+ .then(restParams => fetch(`/api/sync-settings/${provider}` + restParams))
484
+ .then(checkResponse)
485
+ .then((res) => {
486
+ if (provider === 'crowdin') {
487
+ {{#if syncNewElements.crowdin}}
488
+ appComponent.setCrowdinScheduleSync(res, true, true);
489
+ {{else}}
490
+ appComponent.setCrowdinScheduleSync(res);
491
+ {{/if}}
492
+ } else {
493
+ {{#if syncNewElements.integration}}
494
+ appComponent.setIntegrationScheduleSync(res, true, true);
495
+ {{else}}
496
+ appComponent.setIntegrationScheduleSync(res);
497
+ {{/if}}
498
+
499
+ }
500
+ })
501
+ .catch(e => catchRejection(e, 'Can\'t fetch file progress'));
502
+ }
503
+
504
+ function getFileProgress(fileId) {
505
+ checkOrigin()
506
+ .then(restParams => fetch(`api/crowdin/file-progress/${fileId}` + restParams))
507
+ .then(checkResponse)
508
+ .then((res) => (appComponent.addCrowdinFileProgress(res)))
509
+ .catch(e => catchRejection(e, 'Can\'t fetch file progress'));
510
+ }
511
+
512
+ function uploadFilesToCrowdin(event) {
513
+ let files = [];
514
+ let uploadTranslations = false;
515
+ if (event.detail.action === 'uploadTranslations') {
516
+ files = event.detail.files;
517
+ uploadTranslations = true;
518
+ } else {
519
+ files = event.detail;
520
+ }
521
+
522
+ if (event.detail.length === 0) {
523
+ showToast('Select templates which will be pushed to Crowdin');
524
+ return;
525
+ }
526
+ appComponent.setAttribute('is-to-crowdin-process', true);
527
+ let req;
528
+ {{#if integrationOneLevelFetching}}
529
+ req = files.map(f => ({
530
+ name: f.name,
531
+ id: f.id,
532
+ type: f.type,
533
+ parentId: f.parent_id,
534
+ nodeType: f.node_type
535
+ }));
536
+ {{else}}
537
+ req = files.filter(f => f.node_type !== folderType).map(f => ({
538
+ name: f.name,
539
+ id: f.id,
540
+ type: f.type,
541
+ parentId: f.parent_id
542
+ }));
543
+ {{/if}}
544
+ checkOrigin()
545
+ .then(restParams => fetch(`api/crowdin/update${restParams}&uploadTranslations=${uploadTranslations}`, {
546
+ method: 'POST',
547
+ headers: { 'Content-Type': 'application/json' },
548
+ body: JSON.stringify(req)
549
+ }))
550
+ .then(checkResponse)
551
+ .then((res) => {
552
+ checkJob({
553
+ jobId: res?.jobId,
554
+ jobType: JOB_TYPE.updateCrowdin,
555
+ })
556
+ })
557
+ .catch(e => catchRejection(e, 'Can\'t upload templates to Crowdin'))
558
+ }
559
+
560
+ function getActiveJobs() {
561
+ checkOrigin()
562
+ .then(restParams => fetch(`api/jobs${restParams}`))
563
+ .then(checkResponse)
564
+ .then((jobs) => {
565
+ if (!Array.isArray(jobs)) {
566
+ return;
567
+ }
568
+
569
+ jobs.forEach((job) => {
570
+ switch (job.type) {
571
+ case JOB_TYPE.updateCrowdin:
572
+ appComponent.setAttribute('is-to-crowdin-process', true);
573
+ break;
574
+ case JOB_TYPE.updateIntegration:
575
+ appComponent.setAttribute('is-to-integration-process', true);
576
+ break;
577
+ case JOB_TYPE.crowdinSyncSettingsSave:
578
+ appComponent.setAttribute(`is-crowdin-sync-settings-in-progress`, true)
579
+ break;
580
+ case JOB_TYPE.integrationSyncSettingsSave:
581
+ appComponent.setAttribute(`is-integration-sync-settings-in-progress`, true)
582
+ break;
583
+ default:
584
+ }
585
+
586
+ checkJob({
587
+ jobId: job.id,
588
+ jobType: job.type,
589
+ })
590
+ })
591
+ })
592
+ .catch(e => catchRejection(e, 'Sync status check failed'))
593
+ }
594
+
595
+ function cancelJob(jobId) {
596
+ if (asyncJobs[jobId]?.isFailed) {
597
+ return;
598
+ }
599
+
600
+ checkOrigin()
601
+ .then(restParams => fetch('api/jobs' + restParams + '&job_id=' + jobId, {
602
+ method: 'DELETE',
603
+ headers: { 'Content-Type': 'application/json' },
604
+ }))
605
+ .then(checkResponse)
606
+ .then(() => showToast('Cancellation…'))
607
+ .catch(e => catchRejection(e, 'Sync cancellation failed'));
608
+ }
609
+
610
+ function checkJob({jobId, jobType, onSuccess, onError, onFinally}) {
611
+ switch (jobType) {
612
+ case JOB_TYPE.updateCrowdin:
613
+ if (!onSuccess) {
614
+ onSuccess = ((job) => {
615
+ getCrowdinData();
616
+ });
617
+ }
618
+
619
+ if (!onFinally) {
620
+ onFinally = (() => appComponent.setAttribute('is-to-crowdin-process', false));
621
+ }
622
+ break;
623
+ case JOB_TYPE.updateIntegration:
624
+ if (!onFinally) {
625
+ onFinally = (() => appComponent.setAttribute('is-to-integration-process', false));
626
+ }
627
+ break;
628
+ case JOB_TYPE.crowdinSyncSettingsSave:
629
+ if (!onFinally) {
630
+ onFinally = (() => appComponent.setAttribute(`is-crowdin-sync-settings-in-progress`, false));
631
+ }
632
+ break;
633
+ case JOB_TYPE.integrationSyncSettingsSave:
634
+ if (!onFinally) {
635
+ onFinally = (() => appComponent.setAttribute(`is-integration-sync-settings-in-progress`, false));
636
+ }
637
+ break;
638
+ default:
639
+ }
640
+
641
+ checkOrigin()
642
+ .then(restParams => fetch('api/jobs' + restParams + '&job_id=' + jobId))
643
+ .then(checkResponse)
644
+ .then((job) => {
645
+ const isFailed = [JOB_STATUS.failed, JOB_STATUS.canceled].includes(job.status);
646
+ const progress = isFailed || JOB_STATUS.finished === job.status ? 100 : job.progress;
647
+ const info = JOB_STATUS.canceled === job.status ? `Cancelled\n${job.info || ''}` : job.info;
648
+
649
+ !silentJobs.find((type) => type === jobType) && pushJobs([ {
650
+ id: job.id,
651
+ title: job.title,
652
+ progress,
653
+ info,
654
+ isFailed,
655
+ } ]);
656
+
657
+ if (isFailed) {
658
+ onError && onError();
659
+ } else if ([JOB_STATUS.created, JOB_STATUS.inProgress].includes(job.status) && job.progress < 100) {
660
+ setTimeout(() => {
661
+ checkJob({jobId, jobType, onSuccess, onError, onFinally});
662
+ }, {{asyncProgress.checkInterval}});
663
+ return;
664
+ } else {
665
+ onSuccess && onSuccess(job);
666
+ }
667
+
668
+ onFinally && onFinally(job);
669
+ })
670
+ .catch((e) => {
671
+ onFinally && onFinally();
672
+
673
+ !silentJobs.find((type) => type === jobType) && pushJobs([ {
674
+ id: jobId,
675
+ isFailed: true,
676
+ } ]);
677
+
678
+ showToast('Sync status check failed');
679
+ });
680
+ }
681
+
682
+ function pushJobs(jobs) {
683
+ const el = document.querySelector('crowdin-async-progress');
684
+ if (el && el.pushJobs) {
685
+ el.pushJobs(jobs);
686
+ jobs.forEach((job) => (asyncJobs[job.id] = job));
687
+ } else {
688
+ setTimeout(() => {
689
+ pushJobs(jobs);
690
+ }, 300);
691
+ }
692
+ }
693
+
694
+ function uploadFilesToIntegration(e) {
695
+ if (Object.keys(e.detail).length === 0) {
696
+ showToast('Select files which will be uploaded to {{name}}');
697
+ return;
698
+ }
699
+ appComponent.setAttribute('is-to-integration-process', true);
700
+ const req = {};
701
+ Object.keys(e.detail)
702
+ .filter(id => crowdinData.find(c => c.id === id).node_type === fileType)
703
+ .forEach(id => (req[id] = e.detail[id]));
704
+ checkOrigin()
705
+ .then(restParams => fetch('api/integration/update' + restParams, {
706
+ method: 'POST',
707
+ headers: { 'Content-Type': 'application/json' },
708
+ body: JSON.stringify(req)
709
+ }))
710
+ .then(checkResponse)
711
+ .then((res) => {
712
+ checkJob({
713
+ jobId: res?.jobId,
714
+ jobType: JOB_TYPE.updateIntegration,
715
+ })
716
+ })
717
+ .catch(e => catchRejection(e, 'Can\'t upload files to {{name}}'))
718
+ }
719
+
720
+ {{#if configurationFields}}
721
+ const settingsModal = document.getElementById('settings-modal');
722
+ const settingsSaveBtn = document.getElementById('settings-save-btn');
723
+ let config = JSON.parse('{{{config}}}');
724
+
725
+ function triggerEvent(el, type) {
726
+ const e = document.createEvent('HTMLEvents');
727
+ e.initEvent(type, false, true);
728
+ el.dispatchEvent(e);
729
+ }
730
+
731
+ function fillSettingsForm() {
732
+ Object.entries(config).forEach(([key, value]) => {
733
+ const el = document.getElementById(`${key}-settings`);
734
+ if (el && (value || el.tagName.toLowerCase() === 'crowdin-checkbox')) {
735
+ if (el.tagName.toLowerCase() === 'crowdin-select') {
736
+ if (el.hasAttribute('is-multi')) {
737
+ el.value = JSON.stringify(value);
738
+ } else {
739
+ el.value = JSON.stringify([value]);
740
+ }
741
+ } else if (el.tagName.toLowerCase() === 'crowdin-checkbox') {
742
+ el.checked = !!value;
743
+ } else {
744
+ el.value = value;
745
+ }
746
+
747
+ triggerEvent(el, 'change');
748
+ }
749
+ });
750
+ }
751
+
752
+ function saveSettings() {
753
+ setLoader();
754
+ const settingsElements = Array.from(document.getElementById('modal-content').children);
755
+ const tags = ['crowdin-checkbox', 'crowdin-select', 'crowdin-input'];
756
+ const configReq = {};
757
+ settingsElements
758
+ .filter(e => tags.includes(e.tagName.toLowerCase()))
759
+ .forEach(e => {
760
+ const key = e.getAttribute('key');
761
+ let value;
762
+ if (e.tagName.toLowerCase() === 'crowdin-select') {
763
+ value = JSON.parse(e.value);
764
+ if (!e.hasAttribute('is-multi')) {
765
+ value = value.length > 0 ? value[0] : undefined;
766
+ }
767
+ } else if (e.tagName.toLowerCase() === 'crowdin-checkbox') {
768
+ value = !!e.checked;
769
+ } else {
770
+ value = e.value;
771
+ }
772
+ configReq[key] = value;
773
+ });
774
+ settingsSaveBtn.setAttribute('disabled', true);
775
+ checkOrigin()
776
+ .then(restParams => fetch('api/settings' + restParams, {
777
+ method: 'POST',
778
+ headers: { 'Content-Type': 'application/json' },
779
+ body: JSON.stringify({ config: configReq })
780
+ }))
781
+ .then(checkResponse)
782
+ .then(() => {
783
+ showToast('Settings successfully saved');
784
+ config = configReq;
785
+ })
786
+ .catch(e => catchRejection(e, 'Can\'t save settings'))
787
+ .finally(() => {
788
+ unsetLoader();
789
+ settingsSaveBtn.removeAttribute('disabled');
790
+ settingsModal.close();
791
+ {{#if reloadOnConfigSave}}
792
+ getIntegrationData(true);
793
+ getCrowdinData();
794
+ {{/if}}
795
+ });
796
+ }
797
+
798
+ function setLoader() {
799
+ const loader = document.querySelector('#settings-modal .loader');
800
+ loader.classList.remove('hidden');
801
+ }
802
+
803
+ function unsetLoader() {
804
+ const loader = document.querySelector('#settings-modal .loader');
805
+ setTimeout(function() {
806
+ loader.classList.add('hidden');
807
+ }, 500)
808
+ }
809
+ {{else}}
810
+ const settingsModal = undefined;
811
+ {{/if}}
812
+
813
+ {{#if infoModal}}
814
+ const infoModal = document.getElementById('info-modal');
815
+ {{else}}
816
+ const infoModal = undefined;
817
+ {{/if}}
818
+
819
+ document.addEventListener('keydown', (event) => {
820
+ if (event.keyCode == 27) {
821
+
822
+ if (infoModal) {
823
+ infoModal.close();
824
+ }
825
+ if (settingsModal) {
826
+ settingsModal.close();
827
+ }
828
+ }
829
+ });
830
+
831
+ {{#if integrationPagination}}
832
+ document.body.addEventListener('fileListEnd', (e) => {
833
+ getIntegrationData(false, 0, '', e.detail.page)
834
+ })
835
+ {{/if}}
836
+
837
+ {{#or withCronSync webhooks}}
838
+ const scheduleModal = document.getElementById('confirm-schedule-modal');
839
+ let syncData;
840
+
841
+ document.body.addEventListener('integrationScheduleSync', setIntegrationScheduleSync);
842
+ document.body.addEventListener('crowdinScheduleSync', setCrowdinScheduleSync);
843
+ document.body.addEventListener('crowdinDisableSync', disableCrowdinSync);
844
+ document.body.addEventListener('integrationDisableSync', disableIntegrationSync);
845
+
846
+ function getIntegrationFoldersToExpand(allData, syncedFiles) {
847
+ return Array.isArray(allData)
848
+ ? allData.filter(
849
+ node => (node.node_type === folderType && !syncedFiles.find((syncedFile) => syncedFile.parent_id === node.id))
850
+ ).map(node => ({
851
+ ...node,
852
+ nodeType: node.node_type,
853
+ parentId: node.parent_id,
854
+ }))
855
+ : [];
856
+ }
857
+
858
+ async function saveScheduleSync() {
859
+ const newFile = scheduleModal.querySelector('#new-files').checked || false;
860
+ const selectedFiles = scheduleModal.querySelector('#selected-files').checked || false;
861
+
862
+ const type = scheduleModal.getAttribute('data-type');
863
+
864
+ if (type === 'crowdin') {
865
+ appComponent.setCrowdinScheduleSync(syncData, newFile, selectedFiles);
866
+ const syncedFiles = await appComponent.getCrowdinScheduleSync(true);
867
+ appComponent.setAttribute('is-crowdin-sync-settings-in-progress', true);
868
+ updateSyncSettings(syncedFiles, 'schedule', 'crowdin');
869
+ } else if (type === 'integration') {
870
+ appComponent.setIntegrationScheduleSync(syncData, newFile, selectedFiles);
871
+ const syncedFiles = await appComponent.getIntegrationScheduleSync(true);
872
+ appComponent.setAttribute('is-integration-sync-settings-in-progress', true);
873
+ {{#if integrationOneLevelFetching}}
874
+ updateSyncSettings(
875
+ syncedFiles,
876
+ 'schedule',
877
+ 'integration',
878
+ selectedFiles ? getIntegrationFoldersToExpand(syncData, syncedFiles) : [],
879
+ );
880
+ {{else}}
881
+ updateSyncSettings(syncedFiles, 'schedule', 'integration');
882
+ {{/if}}
883
+ }
884
+
885
+ scheduleModal.close();
886
+ }
887
+
888
+ function openScheduleModal(type) {
889
+ const newFile = scheduleModal.querySelector('#new-files')
890
+ const selectedFiles = scheduleModal.querySelector('#selected-files');
891
+
892
+ newFile.checked = false;
893
+ newFile.value = false;
894
+ selectedFiles.checked = true;
895
+ selectedFiles.value = true;
896
+ scheduleModal.querySelector('#save-schedule-sync').setAttribute('disabled', false);
897
+ scheduleModal.setAttribute('data-type', type);
898
+ scheduleModal.open();
899
+ }
900
+
901
+ function onChangeAutoSynchronizationOptions() {
902
+ const newFiles = document.getElementById('new-files').checked || false;
903
+ const selectedFiles = document.getElementById('selected-files').checked || false;
904
+ const buttonSaveScheduleSync = document.getElementById('save-schedule-sync');
905
+
906
+ if (newFiles || selectedFiles) {
907
+ buttonSaveScheduleSync.removeAttribute('disabled');
908
+ } else {
909
+ buttonSaveScheduleSync.setAttribute('disabled', true);
910
+ }
911
+ }
912
+
913
+ async function hasFolder(selectedFiles) {
914
+ let isFolder;
915
+ if (Array.isArray(selectedFiles)) {
916
+ isFolder = selectedFiles.find((file) => file.node_type === folderType || file.node_type === branchType);
917
+ } else {
918
+ const files = await appComponent.getCrowdinFilesData();
919
+ const folders = files.filter((file) => file.node_type === folderType || file.node_type === branchType);
920
+ isFolder = folders.find((folder) => selectedFiles.hasOwnProperty(folder.id));
921
+ }
922
+
923
+ return isFolder !== undefined;
924
+ }
925
+
926
+ async function setIntegrationScheduleSync(e) {
927
+ if (e.detail.length === 0) {
928
+ showToast('Select templates which will be pushed to Crowdin');
929
+ return;
930
+ }
931
+
932
+ {{#if syncNewElements.integration}}
933
+ syncData = e.detail;
934
+ const isFolder = await hasFolder(e.detail);
935
+ if (isFolder) {
936
+ openScheduleModal('integration');
937
+ return;
938
+ }
939
+
940
+ appComponent.setIntegrationScheduleSync(e.detail);
941
+ const syncedFiles = await appComponent.getIntegrationScheduleSync(true);
942
+ {{else}}
943
+ const syncedFiles = await appComponent.getIntegrationScheduleSync();
944
+ {{/if}}
945
+
946
+ appComponent.setAttribute('is-integration-sync-settings-in-progress', true);
947
+ {{#if integrationOneLevelFetching}}
948
+ updateSyncSettings(syncedFiles, 'schedule', 'integration', getIntegrationFoldersToExpand(e.detail, syncedFiles));
949
+ {{else}}
950
+ updateSyncSettings(syncedFiles, 'schedule', 'integration');
951
+ {{/if}}
952
+ }
953
+
954
+ async function setCrowdinScheduleSync(e) {
955
+ if (e.detail.length === 0) {
956
+ showToast('Select templates which will be pushed to Crowdin');
957
+ return;
958
+ }
959
+
960
+ {{#if syncNewElements.crowdin}}
961
+ syncData = e.detail;
962
+ const isFolder = await hasFolder(e.detail);
963
+ if (isFolder) {
964
+ openScheduleModal('crowdin');
965
+ return;
966
+ }
967
+
968
+ appComponent.setCrowdinScheduleSync(e.detail);
969
+ const syncedFiles = await appComponent.getCrowdinScheduleSync(true);
970
+ {{else}}
971
+ const syncedFiles = await appComponent.getCrowdinScheduleSync();
972
+ {{/if}}
973
+ appComponent.setAttribute('is-crowdin-sync-settings-in-progress', true);
974
+ updateSyncSettings(syncedFiles, 'schedule', 'crowdin');
975
+ }
976
+
977
+ async function disableIntegrationSync(e) {
978
+ if (e.detail.length === 0) {
979
+ showToast('Select templates which will be pushed to Crowdin');
980
+ return;
981
+ }
982
+ appComponent.setAttribute('is-integration-sync-settings-in-progress', true);
983
+ {{#if syncNewElements.integration}}
984
+ const syncedFiles = await appComponent.getIntegrationScheduleSync(true);
985
+ {{else}}
986
+ const syncedFiles = await appComponent.getIntegrationScheduleSync();
987
+ {{/if}}
988
+
989
+ updateSyncSettings(syncedFiles, 'schedule', 'integration');
990
+ }
991
+
992
+ async function disableCrowdinSync(e) {
993
+ if (e.detail.length === 0) {
994
+ showToast('Select templates which will be pushed to Crowdin');
995
+ return;
996
+ }
997
+ appComponent.setAttribute('is-crowdin-sync-settings-in-progress', true);
998
+ {{#if syncNewElements.crowdin}}
999
+ const syncedFiles = await appComponent.getCrowdinScheduleSync(true);
1000
+ {{else}}
1001
+ const syncedFiles = await appComponent.getCrowdinScheduleSync();
1002
+ {{/if}}
1003
+
1004
+ updateSyncSettings(syncedFiles, 'schedule', 'crowdin');
1005
+ }
1006
+
1007
+ function updateSyncSettings(files, type, provider, expandIntegrationFolders = []) {
1008
+ checkOrigin()
1009
+ .then(restParams => fetch('api/sync-settings' + restParams, {
1010
+ method: 'POST',
1011
+ headers: { 'Content-Type': 'application/json' },
1012
+ body: JSON.stringify({ files, type, provider, expandIntegrationFolders })
1013
+ }))
1014
+ .then(checkResponse)
1015
+ .then((res) => {
1016
+ checkJob({
1017
+ jobId: res?.jobId,
1018
+ jobType: JOB_TYPE[`${provider}SyncSettingsSave`],
1019
+ })
1020
+ })
1021
+ .catch(e => catchRejection(e, 'Can\'t save schedule sync settings'));
1022
+ }
1023
+ {{/or}}
1024
+
1025
+ {{#if checkSubscription}}
1026
+ const subscriptionInfo = document.getElementById('subscription-info');
1027
+ const subscriptionInfoText = subscriptionInfo.getElementsByTagName('p')[0];
1028
+ function getSubscriptionInfo() {
1029
+ checkOrigin()
1030
+ .then(restParams => fetch('api/subscription-info' + restParams))
1031
+ .then(checkResponse)
1032
+ .then((res) => {
1033
+ if (res.showInfo) {
1034
+ subscriptionLink = res.subscribeLink;
1035
+ subscriptionInfoText.textContent = `Your trial expires in ${res.daysLeft} days.`
1036
+ subscriptionInfo.style.display = 'block';
1037
+ }
1038
+ });
1039
+ }
1040
+
1041
+ getSubscriptionInfo();
1042
+ {{/if}}
1043
+
1044
+ function checkAlert(alert, suffix) {
1045
+ const name = suffix ?? '{{name}}';
1046
+ const revised = localStorage.getItem(`revised_${name}`) === '1';
1047
+ if (!revised) {
1048
+ alert.style.display = 'block';
1049
+ }
1050
+ }
1051
+ function closeAlert(el, suffix) {
1052
+ const name = suffix ?? '{{name}}';
1053
+ const alert = el.closest('crowdin-alert');
1054
+ alert.style.display = 'none';
1055
+ localStorage.setItem(`revised_${name}`, 1);
1056
+ }
1057
+
1058
+ {{#if uploadTranslations}}
1059
+ const translationInfo = document.getElementById('translation-info');
1060
+ checkAlert(translationInfo);
1061
+ {{/if}}
1062
+
1063
+ {{#if notice}}
1064
+ const notice = document.getElementById('notice');
1065
+ checkAlert(notice, 'notice');
1066
+ {{/if}}
1067
+
1068
+ function showErrorLogs() {
1069
+ document.getElementById('show-error-logs-btn').classList.add('hidden');
1070
+ document.getElementById('show-integration-btn').classList.remove('hidden');
1071
+
1072
+ appComponent.classList.add('hidden');
1073
+ document.getElementById('user-errors').classList.remove('hidden');
1074
+
1075
+ checkOrigin()
1076
+ .then(restParams => fetch('api/user-errors' + restParams))
1077
+ .then(checkResponse)
1078
+ .then((res) => {
1079
+ const table = document.getElementById('user-errors-table');
1080
+
1081
+ const clickRow = (field, index, item) => {
1082
+ const modal = document.getElementById('user-error-detail');
1083
+ modal.open();
1084
+ modal.innerHTML = getUserErrorDetail(item);
1085
+ };
1086
+
1087
+ table.setTableConfig && table.setTableConfig({
1088
+ defaultSortingColumn: "createdAt",
1089
+ defaultSortingDir: "desc",
1090
+ headerFields: [
1091
+ {
1092
+ field: "createdAt",
1093
+ name: "Date",
1094
+ isSortable: true,
1095
+ clickFn: clickRow,
1096
+ },
1097
+ {
1098
+ field: "action",
1099
+ name: "Action",
1100
+ clickFn: clickRow,
1101
+ },
1102
+ {
1103
+ field: "message",
1104
+ name: "Message",
1105
+ clickFn: clickRow,
1106
+ }
1107
+ ],
1108
+ });
1109
+
1110
+ table.setTableData(JSON.stringify(res));
1111
+ table.setAttribute(`is-loading`, false);
1112
+ })
1113
+ .catch(e => catchRejection(e, 'Can\'t fetch error logs'));
1114
+ }
1115
+
1116
+ function showIntegration() {
1117
+ document.getElementById('show-error-logs-btn').classList.remove('hidden');
1118
+ document.getElementById('show-integration-btn').classList.add('hidden');
1119
+
1120
+ appComponent.classList.remove('hidden');
1121
+ document.getElementById('user-errors').classList.add('hidden');
1122
+ }
1123
+
1124
+ function getUserErrorDetail(error) {
1125
+ const data = JSON.parse(error.data);
1126
+
1127
+ let html = '<div class="error-detail-table"><table>';
1128
+ html += `<tr><td>Action</td><td>${error.action}</td></tr>`;
1129
+ html += `<tr><td>Message</td><td>${error.message}</td></tr>`;
1130
+ html += `<tr><td>Date/time</td><td>${error.createdAt}</td></tr>`;
1131
+
1132
+ if (data.requestParams) {
1133
+ html += `<tr><td>Method</td><td>${data.requestParams.method}</td></tr>`;
1134
+ }
1135
+
1136
+ if (data.responseData) {
1137
+ html += `<tr><td>Response Data</td><td><pre>${JSON.stringify(data.responseData, null, 2)}</pre></td></tr>`;
1138
+ }
1139
+
1140
+ if (data.appData) {
1141
+ if (Array.isArray(data.appData)) {
1142
+ html += `<tr><td>App Data</td><td><pre>${JSON.stringify(data.appData, null, 2)}</pre></td></tr>`;
1143
+ } else if (typeof data.appData === 'object') {
1144
+ for (const key in data.appData) {
1145
+ html += `<tr><td>${key.charAt(0).toUpperCase() + key.slice(1)}</td><td><pre>${JSON.stringify(data.appData[key], null, 2)}</pre></td></tr>`;
1146
+ }
1147
+ } else {
1148
+ html += `<tr><td>App Data</td><td>${data.appData}</td></tr>`;
1149
+ }
1150
+ }
1151
+
1152
+ html += '</table></div>';
1153
+
1154
+ return html;
1155
+ }
1156
+ </script>
1157
+
1158
+ </html>