@crowdin/app-project-module 0.26.6 → 0.28.0-10

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.
Files changed (37) hide show
  1. package/CONTRIBUTING.md +19 -1
  2. package/README.md +1 -855
  3. package/out/handlers/crowdin-file-progress.d.ts +2 -2
  4. package/out/handlers/crowdin-file-progress.js +9 -4
  5. package/out/handlers/crowdin-update.js +4 -3
  6. package/out/handlers/crowdin-webhook.d.ts +4 -0
  7. package/out/handlers/crowdin-webhook.js +43 -0
  8. package/out/handlers/form-data-display.d.ts +3 -0
  9. package/out/handlers/form-data-display.js +46 -0
  10. package/out/handlers/form-data-save.d.ts +3 -0
  11. package/out/handlers/form-data-save.js +56 -0
  12. package/out/handlers/integration-logout.js +4 -0
  13. package/out/handlers/integration-webhook.d.ts +4 -0
  14. package/out/handlers/integration-webhook.js +39 -0
  15. package/out/handlers/main.js +7 -1
  16. package/out/handlers/settings-save.d.ts +2 -2
  17. package/out/handlers/settings-save.js +8 -3
  18. package/out/handlers/uninstall.js +4 -0
  19. package/out/index.js +50 -10
  20. package/out/middlewares/render-ui-module.d.ts +4 -0
  21. package/out/middlewares/render-ui-module.js +33 -0
  22. package/out/models/index.d.ts +79 -7
  23. package/out/models/index.js +13 -1
  24. package/out/static/css/styles.css +96 -0
  25. package/out/static/js/dependent.js +307 -0
  26. package/out/static/js/form.js +115 -0
  27. package/out/static/js/main.js +11 -1
  28. package/out/util/cron.js +9 -10
  29. package/out/util/defaults.js +55 -12
  30. package/out/util/index.js +5 -1
  31. package/out/util/webhooks.d.ts +29 -0
  32. package/out/util/webhooks.js +308 -0
  33. package/out/views/form.handlebars +29 -0
  34. package/out/views/login.handlebars +84 -16
  35. package/out/views/main.handlebars +171 -88
  36. package/out/views/partials/head.handlebars +5 -4
  37. package/package.json +37 -23
@@ -1,4 +1,4 @@
1
- import Crowdin, { LanguagesModel, SourceFilesModel, SourceStringsModel } from '@crowdin/crowdin-api-client';
1
+ import Crowdin, { LanguagesModel, SourceFilesModel, SourceStringsModel, TranslationStatusModel } from '@crowdin/crowdin-api-client';
2
2
  import { JwtPayload, VerifyOptions } from '@crowdin/crowdin-apps-functions';
3
3
  import { Request } from 'express';
4
4
  import { MySQLStorageConfig } from '../storage/mysql';
@@ -187,7 +187,7 @@ export interface IntegrationLogic {
187
187
  /**
188
188
  * function to update crowdin files (e.g. pull integration data to crowdin source files)
189
189
  */
190
- updateCrowdin: (projectId: number, client: Crowdin, apiCredentials: any, request: IntegrationFile[], appRootFolder?: SourceFilesModel.Directory, config?: any) => Promise<void | ExtendedResult<void>>;
190
+ updateCrowdin: (projectId: number, client: Crowdin, apiCredentials: any, request: IntegrationFile[], appRootFolder?: SourceFilesModel.Directory, config?: any, uploadTranslations?: boolean) => Promise<void | ExtendedResult<void>>;
191
191
  /**
192
192
  * function to update integration content (e.g. load crowdin translations and push them to integration service)
193
193
  */
@@ -235,8 +235,22 @@ export interface IntegrationLogic {
235
235
  * Enable integration next page event
236
236
  */
237
237
  integrationPagination?: boolean;
238
+ /**
239
+ * Enable the option to upload translations to crowdin that are already present in the integration.
240
+ */
241
+ uploadTranslations?: boolean;
242
+ /**
243
+ * function to get crowdin file translation progress
244
+ */
245
+ getFileProgress?: (projectId: number, client: Crowdin, fileId: number) => Promise<{
246
+ [key: number]: TranslationStatusModel.LanguageProgress[];
247
+ }>;
248
+ /**
249
+ * Register Crowdin webhook to get notified when translations are ready
250
+ */
251
+ webhooks?: Webhooks;
238
252
  }
239
- export declare type FormEntity = FormField | FormDelimeter;
253
+ export type FormEntity = FormField | FormDelimeter;
240
254
  export interface FormDelimeter {
241
255
  label: string;
242
256
  }
@@ -358,7 +372,7 @@ export interface FormField {
358
372
  helpText?: string;
359
373
  helpTextHtml?: string;
360
374
  label: string;
361
- type?: 'text' | 'password' | 'checkbox' | 'select';
375
+ type?: 'text' | 'password' | 'checkbox' | 'select' | 'textarea' | 'file';
362
376
  defaultValue?: any;
363
377
  /**
364
378
  * only for select
@@ -375,19 +389,27 @@ export interface FormField {
375
389
  label: string;
376
390
  value: string;
377
391
  }[];
392
+ /**
393
+ * only for type file
394
+ */
395
+ accept?: string;
396
+ /**
397
+ * field dependency settings
398
+ */
399
+ dependencySettings?: string;
378
400
  }
379
401
  export interface ExtendedResult<T> {
380
402
  data?: T;
381
403
  message?: string;
382
404
  stopPagination?: boolean;
383
405
  }
384
- export declare type TreeItem = File | Folder;
406
+ export type TreeItem = File | Folder;
385
407
  /**
386
408
  * 0 - folder
387
409
  * 1 - file
388
410
  * 2 - branch
389
411
  */
390
- declare type IntegrationTreeElementType = '0' | '1' | '2';
412
+ type IntegrationTreeElementType = '0' | '1' | '2';
391
413
  export interface File {
392
414
  id: string;
393
415
  name: string;
@@ -574,10 +596,29 @@ export interface CustomMTRequest {
574
596
  strings: string[];
575
597
  }
576
598
  export interface UiModule {
599
+ /**
600
+ * Form schema for react-jsonschema-doc to be used as front-end
601
+ * https://rjsf-team.github.io/react-jsonschema-form/docs
602
+ */
603
+ formSchema?: object;
604
+ /**
605
+ * URL to custom endpoint that can be used instead of default one to save form data.
606
+ * Endpoint should accept POST requests.
607
+ */
608
+ formPostDataUrl?: string;
609
+ /**
610
+ * URL to custom endpoint that can be used instead of default one to retrieve form data.
611
+ * Endpoint should accept GET requests.
612
+ */
613
+ formGetDataUrl?: string;
614
+ /**
615
+ * Additional attributes for react-jsonschema-doc
616
+ */
617
+ formUiSchema?: object;
577
618
  /**
578
619
  * path to ui folder (e.g. {@example join(__dirname, 'public')})
579
620
  */
580
- uiPath: string;
621
+ uiPath?: string;
581
622
  /**
582
623
  * page name (default index.html)
583
624
  */
@@ -637,4 +678,35 @@ export interface Pricing {
637
678
  cachingSeconds?: number;
638
679
  infoDisplayDaysThreshold?: number;
639
680
  }
681
+ export interface Webhooks {
682
+ crowdinWebhookUrl?: string;
683
+ integrationWebhookUrl?: string;
684
+ urlParam?: string;
685
+ crowdinWebhooks?: (client: Crowdin, projectId: number, available: boolean, config?: any) => Promise<void>;
686
+ integrationWebhooks?: (apiCredentials: any, urlParam: string, available: boolean, config?: any, syncSettings?: any) => Promise<void>;
687
+ crowdinWebhookInterceptor?: (projectId: number, client: Crowdin, appRootFolder?: SourceFilesModel.Directory, config?: any, syncSettings?: any, webhookRequest?: any) => Promise<UpdateIntegrationRequest>;
688
+ integrationWebhookInterceptor?: (projectId: number, client: Crowdin, apiCredentials: any, appRootFolder?: SourceFilesModel.Directory, config?: any, syncSettings?: any, webhookRequest?: any) => Promise<IntegrationFile[]>;
689
+ queueUrl: string;
690
+ }
691
+ export declare enum SyncCondition {
692
+ ALL = 0,
693
+ TRANSLATED = 1,
694
+ APPROVED = 2
695
+ }
696
+ export declare enum SyncType {
697
+ NONE = 0,
698
+ SCHEDULE = 1,
699
+ WEBHOOKS = 2
700
+ }
701
+ export type Payload = {
702
+ event: string;
703
+ projectId: string;
704
+ language: string;
705
+ fileId: string;
706
+ };
707
+ export type WebhookUrlParams = {
708
+ projectId: number;
709
+ crowdinId: string;
710
+ clientId: string;
711
+ };
640
712
  export {};
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.EditorPanelsMode = exports.ProcessFileJobType = exports.SubscriptionInfoType = exports.AccountType = exports.Scope = exports.AuthenticationType = void 0;
3
+ exports.SyncType = exports.SyncCondition = exports.EditorPanelsMode = exports.ProcessFileJobType = exports.SubscriptionInfoType = exports.AccountType = exports.Scope = exports.AuthenticationType = void 0;
4
4
  var AuthenticationType;
5
5
  (function (AuthenticationType) {
6
6
  AuthenticationType["CODE"] = "authorization_code";
@@ -47,3 +47,15 @@ var EditorPanelsMode;
47
47
  EditorPanelsMode["TRANSLATE"] = "TRANSLATE";
48
48
  EditorPanelsMode["PROOFREAD"] = "proofread";
49
49
  })(EditorPanelsMode = exports.EditorPanelsMode || (exports.EditorPanelsMode = {}));
50
+ var SyncCondition;
51
+ (function (SyncCondition) {
52
+ SyncCondition[SyncCondition["ALL"] = 0] = "ALL";
53
+ SyncCondition[SyncCondition["TRANSLATED"] = 1] = "TRANSLATED";
54
+ SyncCondition[SyncCondition["APPROVED"] = 2] = "APPROVED";
55
+ })(SyncCondition = exports.SyncCondition || (exports.SyncCondition = {}));
56
+ var SyncType;
57
+ (function (SyncType) {
58
+ SyncType[SyncType["NONE"] = 0] = "NONE";
59
+ SyncType[SyncType["SCHEDULE"] = 1] = "SCHEDULE";
60
+ SyncType[SyncType["WEBHOOKS"] = 2] = "WEBHOOKS";
61
+ })(SyncType = exports.SyncType || (exports.SyncType = {}));
@@ -55,4 +55,100 @@
55
55
 
56
56
  .m-0 {
57
57
  margin: 0;
58
+ }
59
+
60
+ .info-text {
61
+ max-width: 800px;
62
+ }
63
+
64
+ #translation-info {
65
+ margin: 12px 0 12px 0;
66
+ }
67
+
68
+ .dismiss-alert {
69
+ position: absolute;
70
+ top: 0;
71
+ right: 0;
72
+ }
73
+ .file-field {
74
+ font-family: Roboto,‘Segoe UI’,-apple-system,BlinkMacSystemFont,‘Helvetica Neue’,Arial,sans-serif;
75
+ -moz-osx-font-smoothing: grayscale;
76
+ -webkit-font-smoothing: antialiased;
77
+ font-size: .875rem;
78
+ font-weight: 400;
79
+ line-height: 1.5;
80
+ -ms-text-size-adjust: 100%;
81
+ text-rendering: optimizeLegibility;
82
+ color: rgba(38,50,56,.87);
83
+ }
84
+
85
+ .file-field .help-text {
86
+ font-size: .75rem;
87
+ color: rgba(38,50,56,.54);
88
+ }
89
+
90
+ .file-field .upload {
91
+ margin-top: 8px;
92
+ display: flex;
93
+ align-items: center;
94
+ justify-content: space-between;
95
+ }
96
+
97
+ .file-field .uploaded-file {
98
+ font-style: italic;
99
+ display: flex;
100
+ align-items: center;
101
+ }
102
+
103
+ .loader {
104
+ background: rgba(255, 255, 255, 0.3);
105
+ position: absolute;
106
+ top: 0;
107
+ left: 0;
108
+ width: 100%;
109
+ height: 100%;
110
+ z-index: 4;
111
+ }
112
+
113
+ .loader crowdin-progress-indicator {
114
+ position: absolute;
115
+ top: calc(50% - 20px);
116
+ left: calc(50% - 20px);
117
+ }
118
+
119
+ .hidden {
120
+ width:0 !important;
121
+ height: 0 !important;
122
+ opacity: 0 !important;
123
+ z-index: -1 !important;
124
+ display: block !important;
125
+ overflow: hidden !important;
126
+ }
127
+
128
+ [data-dependency]:not(.dependency-show):not(input) {
129
+ display : none;
130
+ }
131
+
132
+
133
+ #form-loading {
134
+ position: absolute;
135
+ top: 0;
136
+ bottom: 0;
137
+ left: 0;
138
+ right: 0;
139
+ display: flex;
140
+ justify-content: center;
141
+ align-items: center;
142
+ background: rgb(251 251 251 / 70%);
143
+ }
144
+
145
+ #form .MuiButtonBase-root[type="submit"] {
146
+ background: #fff;
147
+ color: #263238;
148
+ box-shadow: none;
149
+ border: 1px solid rgba(38,50,56,.24);
150
+ }
151
+
152
+ #form .MuiButtonBase-root[type="submit"]:hover {
153
+ background: rgba(236, 239, 241, .54);
58
154
  }
@@ -0,0 +1,307 @@
1
+ document.addEventListener('DOMContentLoaded', () => {
2
+ const fields = document.querySelectorAll('[data-dependency]');
3
+
4
+ if (fields.length > 0) {
5
+ fields.forEach((field) => {
6
+ const conditionString = field.dataset['dependency'].replace(/'/g, '"');
7
+ const conditions = JSON.parse(conditionString);
8
+ action(field, conditions);
9
+
10
+ const success = check(conditions);
11
+ showHide(field, success);
12
+ });
13
+ }
14
+ });
15
+
16
+ function action(field, conditions) {
17
+ conditions.forEach((rules) => {
18
+ for (const [selector, rule] of Object.entries(rules)) {
19
+ ['input', 'change'].forEach((event) => {
20
+ const element = document.querySelector(selector);
21
+ if (element) {
22
+ element.addEventListener(event, () => {
23
+ const success = check(conditions);
24
+ showHide(field, success);
25
+ });
26
+ }
27
+ });
28
+ }
29
+ });
30
+ }
31
+
32
+ function showHide(element, success) {
33
+ if (success) {
34
+ element.classList.remove('dependency-show');
35
+ element.classList.add('dependency-show');
36
+ return true;
37
+ } else {
38
+ element.classList.remove('dependency-show');
39
+ return false;
40
+ }
41
+ }
42
+
43
+ function check(conditions) {
44
+ return conditions.every((conditionObj) => {
45
+ const selectors = Object.keys(conditionObj);
46
+ return selectors.every((selector) => {
47
+ const condition = conditionObj[selector];
48
+ return decision(selector, condition);
49
+ });
50
+ });
51
+ }
52
+
53
+ function decision(selector, condition) {
54
+ const type = condition['type'];
55
+ const currentValue = getValue(selector);
56
+
57
+ let checkValue = typeof condition['value'] === 'undefined' ? false : condition['value'];
58
+
59
+ let minValue = typeof condition['min'] === 'undefined' ? false : parseInt(condition['min']);
60
+ let maxValue = typeof condition['max'] === 'undefined' ? false : parseInt(condition['max']);
61
+
62
+ const allowEmpty = typeof condition['empty'] === 'undefined' ? false : condition['empty'];
63
+ const isEmpty = !allowEmpty && currentValue.length < 1;
64
+
65
+ const likeSelector = typeof condition['like'] === 'undefined' ? false : condition['like'];
66
+ const likeSelectorValue = getValue(likeSelector);
67
+
68
+ const regExpPattern = typeof condition['pattern'] === 'undefined' ? false : condition['pattern'];
69
+ const regExpModifier = typeof condition['modifier'] === 'undefined' ? 'gi' : condition['modifier'];
70
+ const sign = typeof condition['sign'] === 'undefined' ? false : condition['sign'];
71
+ const strict = typeof condition['strict'] === 'undefined' ? false : condition['strict'];
72
+
73
+ const emptyTypes = ['empty', 'blank'];
74
+ const notEmptyTypes = ['!empty', 'notEmpty', 'not-empty', 'notempty'];
75
+
76
+ const equalTypes = ['equal', '=', '==', '==='];
77
+ const notEqualTypes = ['!equal', '!=', '!==', '!===', 'notEqual', 'not-equal', 'notequal'];
78
+
79
+ const regularExpressionTypes = ['regexp', 'exp', 'expression', 'match'];
80
+
81
+ // if empty return true
82
+ if (emptyTypes.includes(type)) {
83
+ return currentValue.length < 1;
84
+ }
85
+
86
+ // if not empty return true
87
+ if (notEmptyTypes.includes(type)) {
88
+ return currentValue.length > 0;
89
+ }
90
+
91
+ // if equal return true
92
+ if (equalTypes.includes(type)) {
93
+ if (isEmpty) {
94
+ return false;
95
+ }
96
+
97
+ // Match two selector value/s
98
+ if (likeSelector) {
99
+ if (strict) {
100
+ return likeSelectorValue.every((value) => {
101
+ return currentValue.includes(value);
102
+ });
103
+ } else {
104
+ return likeSelectorValue.some((value) => {
105
+ return currentValue.includes(value);
106
+ });
107
+ }
108
+ }
109
+
110
+ // Match pre-defined value/s
111
+ if (strict) {
112
+ if (checkValue && Array.isArray(checkValue)) {
113
+ return checkValue.every((value) => {
114
+ return currentValue.includes(value);
115
+ });
116
+ }
117
+
118
+ if (checkValue && !Array.isArray(checkValue)) {
119
+ return currentValue.includes(checkValue);
120
+ }
121
+ } else {
122
+ if (checkValue && Array.isArray(checkValue)) {
123
+ return checkValue.some((value) => {
124
+ return currentValue.includes(value);
125
+ });
126
+ }
127
+
128
+ if (checkValue && !Array.isArray(checkValue)) {
129
+ return currentValue.includes(checkValue);
130
+ }
131
+ }
132
+ }
133
+
134
+ // if not equal return true
135
+ if (notEqualTypes.includes(type)) {
136
+ if (isEmpty) {
137
+ return false;
138
+ }
139
+
140
+ // Match two selector value/s
141
+ if (likeSelector) {
142
+ if (strict) {
143
+ return likeSelectorValue.every((value) => {
144
+ return !currentValue.includes(value);
145
+ });
146
+ } else {
147
+ return likeSelectorValue.some((value) => {
148
+ return !currentValue.includes(value);
149
+ });
150
+ }
151
+ }
152
+
153
+ // Match pre-defined value/s
154
+ if (strict) {
155
+ if (checkValue && Array.isArray(checkValue)) {
156
+ return checkValue.every((value) => {
157
+ return !currentValue.includes(value);
158
+ });
159
+ }
160
+
161
+ if (checkValue && !Array.isArray(checkValue)) {
162
+ return !currentValue.includes(checkValue);
163
+ }
164
+ } else {
165
+ if (checkValue && Array.isArray(checkValue)) {
166
+ return checkValue.some((value) => {
167
+ return !currentValue.includes(value);
168
+ });
169
+ }
170
+
171
+ if (checkValue && !Array.isArray(checkValue)) {
172
+ return !currentValue.includes(checkValue);
173
+ }
174
+ }
175
+ }
176
+
177
+ // if regexp match
178
+ if (regularExpressionTypes.includes(type) && regExpPattern) {
179
+ if (isEmpty) {
180
+ return false;
181
+ }
182
+
183
+ const exp = new RegExp(regExpPattern, regExpModifier);
184
+ return currentValue.every((value) => {
185
+ return exp.test(value);
186
+ });
187
+ }
188
+
189
+ // if length
190
+ if ('length' === type) {
191
+ if (isEmpty) {
192
+ return false;
193
+ }
194
+
195
+ if (checkValue && Array.isArray(checkValue)) {
196
+ minValue = parseInt(checkValue[0]);
197
+ maxValue = typeof checkValue[1] === 'undefined' ? false : parseInt(checkValue[1]);
198
+ }
199
+
200
+ if (checkValue && !Array.isArray(checkValue)) {
201
+ minValue = parseInt(checkValue);
202
+ maxValue = false;
203
+ }
204
+
205
+ return currentValue.every((value) => {
206
+ if (!maxValue) {
207
+ return value.length >= minValue;
208
+ }
209
+
210
+ if (!minValue) {
211
+ return value.length <= maxValue;
212
+ }
213
+
214
+ return value.length >= minValue && value.length <= maxValue;
215
+ });
216
+ }
217
+
218
+ // if range
219
+ if ('range' === type) {
220
+ if (isEmpty) {
221
+ return false;
222
+ }
223
+
224
+ if (checkValue && Array.isArray(checkValue)) {
225
+ minValue = parseInt(checkValue[0]);
226
+ maxValue = typeof checkValue[1] === 'undefined' ? false : parseInt(checkValue[1]);
227
+ }
228
+
229
+ return currentValue.every((value) => {
230
+ if (!maxValue) {
231
+ return parseInt(value) > minValue;
232
+ }
233
+
234
+ if (!minValue) {
235
+ return parseInt(value) < maxValue;
236
+ }
237
+
238
+ return parseInt(value) > minValue && parseInt(value) < maxValue;
239
+ });
240
+ }
241
+
242
+ // if compare
243
+ if ('compare' === type && sign && checkValue) {
244
+ if (isEmpty) {
245
+ return false;
246
+ }
247
+
248
+ checkValue = parseInt(checkValue);
249
+
250
+ switch (sign) {
251
+ case '<':
252
+ return currentValue.every((value) => {
253
+ return parseInt(value) < checkValue;
254
+ });
255
+ break;
256
+
257
+ case '<=':
258
+ return currentValue.every((value) => {
259
+ return parseInt(value) <= checkValue;
260
+ });
261
+ break;
262
+
263
+ case '>':
264
+ return currentValue.every((value) => {
265
+ return parseInt(value) > checkValue;
266
+ });
267
+ break;
268
+
269
+ case '>=':
270
+ return currentValue.every((value) => {
271
+ return parseInt(value) >= checkValue;
272
+ });
273
+ break;
274
+
275
+ case '=':
276
+ case '==':
277
+ return currentValue.every((value) => {
278
+ return parseInt(value) === checkValue;
279
+ });
280
+ break;
281
+ }
282
+ }
283
+ }
284
+
285
+ function getValue(selector) {
286
+ const values = [];
287
+ if (selector && document.querySelector(selector)) {
288
+ const inputType = document.querySelector(selector).tagName.toLowerCase();``
289
+
290
+ let currentSelector = selector;
291
+
292
+ if ('crowdin-select' === inputType) {
293
+ currentSelector = `${selector} option:checked`;
294
+ }
295
+
296
+ if ('crowdin-checkbox' === inputType) {
297
+ currentSelector = `${selector}:not(input)`;
298
+ }
299
+
300
+ document.querySelectorAll(`${currentSelector}`).forEach((element) => {
301
+ const value = element.value || element.checked;
302
+ values.push(value);
303
+ });
304
+ }
305
+
306
+ return values.filter((value) => value !== '');
307
+ }