@crowdin/app-project-module 0.102.2 → 0.104.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
@@ -88,6 +88,7 @@ const webhooks = __importStar(require("./modules/webhooks"));
88
88
  const workflowStepType = __importStar(require("./modules/workflow-step-type"));
89
89
  const aiRequestProcessors = __importStar(require("./modules/ai-request-processors"));
90
90
  const automationAction = __importStar(require("./modules/automation-action"));
91
+ const authGuard = __importStar(require("./modules/auth-guard"));
91
92
  const subscription_1 = require("./util/subscription");
92
93
  var types_2 = require("./types");
93
94
  Object.defineProperty(exports, "ProjectPermissions", { enumerable: true, get: function () { return types_2.ProjectPermissions; } });
@@ -222,6 +223,7 @@ function addCrowdinEndpoints(app, clientConfig) {
222
223
  workflowStepType.register({ config, app });
223
224
  aiRequestProcessors.register({ config, app });
224
225
  automationAction.register({ config, app });
226
+ authGuard.register({ config, app });
225
227
  addFormSchema({ config, app });
226
228
  return Object.assign(Object.assign({}, exports.metadataStore), { storage: storage.getStorage(), establishCrowdinConnection: (authRequest, moduleKey) => {
227
229
  let jwtToken = '';
@@ -1,8 +1,17 @@
1
1
  import { CrowdinContextInfo, UiModule } from '../../types';
2
2
  import Crowdin from '@crowdin/crowdin-api-client';
3
+ export declare enum PromptActions {
4
+ PRE_TRANSLATE = "pre_translate",
5
+ ASSIST = "assist",
6
+ QA_CHECK = "qa_check"
7
+ }
3
8
  export interface AiPromptProviderModule extends UiModule {
4
9
  /**
5
10
  * generates prompt text based on provided options
6
11
  */
7
12
  compile: (options: any, payload: any, client: Crowdin, context: CrowdinContextInfo, requestData: any) => Promise<string>;
13
+ /**
14
+ * Represents a collection of supported actions. If not provided, all actions are supported.
15
+ */
16
+ actions?: PromptActions[] | `custom:${string}`[];
8
17
  }
@@ -1,2 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PromptActions = void 0;
4
+ var PromptActions;
5
+ (function (PromptActions) {
6
+ PromptActions["PRE_TRANSLATE"] = "pre_translate";
7
+ PromptActions["ASSIST"] = "assist";
8
+ PromptActions["QA_CHECK"] = "qa_check";
9
+ })(PromptActions = exports.PromptActions || (exports.PromptActions = {}));
@@ -0,0 +1,5 @@
1
+ /// <reference types="qs" />
2
+ import { Response } from 'express';
3
+ import { CrowdinClientRequest } from '../../../types';
4
+ import { AuthGuardModule } from '../types';
5
+ export default function verifyHandler(authGuardModules: AuthGuardModule[]): (req: CrowdinClientRequest | import("express").Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>, res: Response<any, Record<string, any>>, next: Function) => void;
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ const util_1 = require("../../../util");
13
+ function verifyHandler(authGuardModules) {
14
+ return (0, util_1.runAsyncWrapper)((req, res) => __awaiter(this, void 0, void 0, function* () {
15
+ const { userId, organizationId, ipAddress, moduleKey, code } = req.body;
16
+ req.logInfo(`Auth guard verification request for module: ${moduleKey}`);
17
+ // Find the auth-guard module by key
18
+ const module = authGuardModules.find((m) => m.key === moduleKey);
19
+ if (!module) {
20
+ req.logError(`Auth guard module not found: ${moduleKey}`);
21
+ return res.status(404).json({
22
+ error: `Auth guard module not found: ${moduleKey}`,
23
+ });
24
+ }
25
+ try {
26
+ // Call the user-provided verification logic
27
+ const result = yield module.verify({
28
+ client: req.crowdinApiClient,
29
+ context: req.crowdinContext,
30
+ userId,
31
+ organizationId,
32
+ ipAddress,
33
+ moduleKey,
34
+ code,
35
+ });
36
+ if (!result || typeof result.success !== 'boolean') {
37
+ req.logError('Invalid response from auth guard verification function');
38
+ return res.status(500).json({
39
+ success: false,
40
+ message: 'Invalid response from verification function',
41
+ });
42
+ }
43
+ // Return the result (always HTTP 200, success field indicates verification result)
44
+ req.logInfo(`Auth guard verification ${result.success ? 'succeeded' : 'failed'} for module: ${moduleKey}`);
45
+ return res.status(200).json(result);
46
+ }
47
+ catch (error) {
48
+ req.logError(`Auth guard verification error: ${error.message || error}`);
49
+ // Return HTTP 500 for internal errors
50
+ return res.status(500).json({
51
+ success: false,
52
+ message: 'Internal server error during verification',
53
+ });
54
+ }
55
+ }));
56
+ }
57
+ exports.default = verifyHandler;
@@ -0,0 +1,6 @@
1
+ import { Express } from 'express';
2
+ import { Config } from '../../types';
3
+ export declare function register({ config, app }: {
4
+ config: Config;
5
+ app: Express;
6
+ }): void;
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.register = void 0;
7
+ const crowdin_client_1 = __importDefault(require("../../middlewares/crowdin-client"));
8
+ const json_response_1 = __importDefault(require("../../middlewares/json-response"));
9
+ const render_ui_module_1 = __importDefault(require("../../middlewares/render-ui-module"));
10
+ const ui_module_1 = __importDefault(require("../../middlewares/ui-module"));
11
+ const verify_1 = __importDefault(require("./handlers/verify"));
12
+ function register({ config, app }) {
13
+ if (!config.authGuard) {
14
+ return;
15
+ }
16
+ // Normalize to array
17
+ const authGuardModules = Array.isArray(config.authGuard) ? config.authGuard : [config.authGuard];
18
+ if (!authGuardModules.length) {
19
+ return;
20
+ }
21
+ // Register settings UI modules if provided
22
+ authGuardModules.forEach((module, index) => {
23
+ if (module.settingsUiModule) {
24
+ const moduleKey = module.key || `${config.identifier}-auth-guard-${index}`;
25
+ app.use(`/auth-guard/${moduleKey}/settings`, (0, ui_module_1.default)({ config, moduleType: moduleKey }), (0, render_ui_module_1.default)(module.settingsUiModule));
26
+ }
27
+ });
28
+ // Register verification endpoint
29
+ app.post('/auth-guard/verify', json_response_1.default, (0, crowdin_client_1.default)({
30
+ config,
31
+ optional: false,
32
+ checkSubscriptionExpiration: false,
33
+ moduleKey: authGuardModules.map((m, i) => m.key || `${config.identifier}-auth-guard-${i}`),
34
+ }), (0, verify_1.default)(authGuardModules));
35
+ }
36
+ exports.register = register;
@@ -0,0 +1,101 @@
1
+ import Crowdin from '@crowdin/crowdin-api-client';
2
+ import { CrowdinContextInfo, Environments, ModuleKey, UiModule } from '../../types';
3
+ /**
4
+ * Auth Guard verification type
5
+ */
6
+ export type AuthGuardVerificationType = 'direct' | 'redirect' | 'iframe';
7
+ /**
8
+ * Auth Guard module options
9
+ */
10
+ export interface AuthGuardOptions {
11
+ /**
12
+ * Verification type: direct (backend-only), redirect (external page), or iframe (embedded iframe)
13
+ * @default 'direct'
14
+ */
15
+ type?: AuthGuardVerificationType;
16
+ /**
17
+ * Whether to apply this check to organization administrators
18
+ * @default false
19
+ */
20
+ applyToAdmin?: boolean;
21
+ /**
22
+ * User-facing URL for redirect and iframe types
23
+ * Required if type is not 'direct'
24
+ */
25
+ url?: string;
26
+ }
27
+ /**
28
+ * Request body for auth guard verification
29
+ */
30
+ export interface AuthGuardVerifyRequest {
31
+ /**
32
+ * User ID
33
+ */
34
+ userId: number;
35
+ /**
36
+ * Organization ID
37
+ */
38
+ organizationId: number;
39
+ /**
40
+ * User's IP address
41
+ */
42
+ ipAddress: string;
43
+ /**
44
+ * Module key to identify which auth-guard module to invoke
45
+ */
46
+ moduleKey: string;
47
+ /**
48
+ * Verification code (provided for redirect/iframe types after user interaction)
49
+ */
50
+ code?: string;
51
+ }
52
+ /**
53
+ * Response for auth guard verification
54
+ */
55
+ export interface AuthGuardVerifyResponse {
56
+ /**
57
+ * Whether verification was successful
58
+ */
59
+ success: boolean;
60
+ /**
61
+ * Optional error or informational message
62
+ */
63
+ message?: string;
64
+ }
65
+ /**
66
+ * Auth Guard module configuration
67
+ */
68
+ export interface AuthGuardModule extends ModuleKey, Environments {
69
+ /**
70
+ * Module name displayed to users during verification
71
+ */
72
+ name?: string;
73
+ /**
74
+ * Module description (primarily for iframe type)
75
+ */
76
+ description?: string;
77
+ /**
78
+ * Endpoint URL for verification (relative to baseUrl or absolute)
79
+ */
80
+ url?: string;
81
+ /**
82
+ * Module configuration options
83
+ */
84
+ options?: AuthGuardOptions;
85
+ /**
86
+ * Settings UI module for configuration
87
+ */
88
+ settingsUiModule?: UiModule;
89
+ /**
90
+ * Verification logic function
91
+ */
92
+ verify: (params: {
93
+ client?: Crowdin;
94
+ context?: CrowdinContextInfo;
95
+ userId: number;
96
+ organizationId: number;
97
+ ipAddress: string;
98
+ moduleKey: string;
99
+ code?: string;
100
+ }) => Promise<AuthGuardVerifyResponse>;
101
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -16,8 +16,8 @@ const types_1 = require("../types");
16
16
  const storage_1 = require("../../../storage");
17
17
  function handle(integration) {
18
18
  return (0, util_1.runAsyncWrapper)((req, res) => __awaiter(this, void 0, void 0, function* () {
19
- var _a, _b, _c, _d, _e, _f, _g, _h, _j;
20
- const { parent_id: parentId, search, page } = req.query;
19
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
20
+ const { parent_id: parentId, search, page, paginationData } = req.query;
21
21
  req.logInfo('Received request to get integration data');
22
22
  let message;
23
23
  let stopPagination;
@@ -25,7 +25,8 @@ function handle(integration) {
25
25
  const { crowdinId, clientId } = req.crowdinContext;
26
26
  try {
27
27
  const appSettings = req.integrationSettings;
28
- const result = yield integration.getIntegrationFiles(req.integrationCredentials, appSettings, parentId, search, page);
28
+ const parsedPaginationData = paginationData ? JSON.parse(paginationData) : undefined;
29
+ const result = yield integration.getIntegrationFiles(req.integrationCredentials, appSettings, parentId, search, page, parsedPaginationData);
29
30
  if ((0, files_1.isExtendedResultType)(result)) {
30
31
  files = result.data || [];
31
32
  message = result.message;
@@ -46,20 +47,22 @@ function handle(integration) {
46
47
  const needsFileStatus = ((_e = (_d = integration.filtering) === null || _d === void 0 ? void 0 : _d.integrationFileStatus) === null || _e === void 0 ? void 0 : _e.isNew) ||
47
48
  ((_g = (_f = integration.filtering) === null || _f === void 0 ? void 0 : _f.integrationFileStatus) === null || _g === void 0 ? void 0 : _g.isUpdated) ||
48
49
  ((_j = (_h = integration.filtering) === null || _h === void 0 ? void 0 : _h.integrationFileStatus) === null || _j === void 0 ? void 0 : _j.notSynced) ||
50
+ ((_l = (_k = integration.filtering) === null || _k === void 0 ? void 0 : _k.integrationFileStatus) === null || _l === void 0 ? void 0 : _l.synced) ||
49
51
  integration.forcePushSources === true;
50
52
  if (needsFileStatus) {
51
53
  const syncedData = yield (0, storage_1.getStorage)().getSyncedData(clientId, crowdinId, types_1.Provider.INTEGRATION);
52
54
  const syncedFiles = (syncedData === null || syncedData === void 0 ? void 0 : syncedData.files) ? JSON.parse(syncedData.files) : [];
53
55
  const lastSyncTimestamp = (syncedData === null || syncedData === void 0 ? void 0 : syncedData.updatedAt) ? Number(syncedData.updatedAt) : null;
54
56
  files = files.map((file) => {
55
- var _a, _b, _c, _d, _e, _f;
57
+ var _a, _b, _c, _d, _e, _f, _g, _h;
56
58
  if (!file.type) {
57
59
  return file;
58
60
  }
59
61
  const syncedFile = syncedFiles.find((syncedItem) => syncedItem.id === file.id);
60
62
  const notSynced = ((_b = (_a = integration.filtering) === null || _a === void 0 ? void 0 : _a.integrationFileStatus) === null || _b === void 0 ? void 0 : _b.notSynced) === true ? !syncedFile : false;
63
+ const synced = ((_d = (_c = integration.filtering) === null || _c === void 0 ? void 0 : _c.integrationFileStatus) === null || _d === void 0 ? void 0 : _d.synced) === true ? !!syncedFile : false;
61
64
  const fileSyncTimestamp = (syncedFile === null || syncedFile === void 0 ? void 0 : syncedFile.syncedAt) ? Number(syncedFile.syncedAt) : lastSyncTimestamp;
62
- const isNew = ((_d = (_c = integration.filtering) === null || _c === void 0 ? void 0 : _c.integrationFileStatus) === null || _d === void 0 ? void 0 : _d.isNew) === true
65
+ const isNew = ((_f = (_e = integration.filtering) === null || _e === void 0 ? void 0 : _e.integrationFileStatus) === null || _f === void 0 ? void 0 : _f.isNew) === true
63
66
  ? !syncedFile &&
64
67
  fileSyncTimestamp &&
65
68
  file.createdAt &&
@@ -67,7 +70,7 @@ function handle(integration) {
67
70
  ? new Date(file.createdAt).getTime()
68
71
  : file.createdAt) > fileSyncTimestamp
69
72
  : false;
70
- const shouldCalculateIsUpdated = ((_f = (_e = integration.filtering) === null || _e === void 0 ? void 0 : _e.integrationFileStatus) === null || _f === void 0 ? void 0 : _f.isUpdated) === true ||
73
+ const shouldCalculateIsUpdated = ((_h = (_g = integration.filtering) === null || _g === void 0 ? void 0 : _g.integrationFileStatus) === null || _h === void 0 ? void 0 : _h.isUpdated) === true ||
71
74
  integration.forcePushSources === true;
72
75
  const isUpdated = shouldCalculateIsUpdated
73
76
  ? syncedFile &&
@@ -78,7 +81,8 @@ function handle(integration) {
78
81
  : false;
79
82
  return Object.assign(Object.assign({}, file), { isNew,
80
83
  notSynced,
81
- isUpdated });
84
+ isUpdated,
85
+ synced });
82
86
  });
83
87
  }
84
88
  }
@@ -26,7 +26,7 @@ function checkAutoSyncSettings(integration, appSettings, provider) {
26
26
  }
27
27
  function handle(config, integration) {
28
28
  return (0, util_1.runAsyncWrapper)((req, res) => __awaiter(this, void 0, void 0, function* () {
29
- const { files, provider, expandIntegrationFolders } = req.body;
29
+ const { files, provider, expandIntegrationFolders, hasLoadMoreElements } = req.body;
30
30
  if (req.isApiCall) {
31
31
  if (!files || !provider) {
32
32
  return res.status(400).json({
@@ -54,9 +54,12 @@ function handle(config, integration) {
54
54
  client: req.crowdinApiClient,
55
55
  jobType: types_1.JobClientType.MANUAL,
56
56
  jobStoreType: integration.jobStoreType,
57
- jobCallback: () => __awaiter(this, void 0, void 0, function* () {
57
+ jobCallback: (job) => __awaiter(this, void 0, void 0, function* () {
58
58
  if (Array.isArray(expandIntegrationFolders) && expandIntegrationFolders.length) {
59
- const allFiles = (yield (0, files_1.expandFilesTree)(expandIntegrationFolders, req, integration)).map((node) => ({
59
+ const expandedFiles = hasLoadMoreElements
60
+ ? yield (0, files_1.expandFilesTreeWithPagination)(expandIntegrationFolders, req, integration, job)
61
+ : yield (0, files_1.expandFilesTree)(expandIntegrationFolders, req, integration, job);
62
+ const allFiles = expandedFiles.map((node) => ({
60
63
  id: node.id,
61
64
  name: node.name,
62
65
  // eslint-disable-next-line @typescript-eslint/camelcase
@@ -67,6 +70,7 @@ function handle(config, integration) {
67
70
  sync: false,
68
71
  type: node.type,
69
72
  }));
73
+ req.logInfo(`Expanded ${allFiles.length} files for auto-sync`);
70
74
  yield (0, cron_1.createOrUpdateSyncSettings)({
71
75
  req,
72
76
  files: (0, lodash_uniqby_1.default)([...files, ...allFiles], 'id'),
@@ -40,7 +40,7 @@ export interface IntegrationLogic extends ModuleKey {
40
40
  /**
41
41
  * function to get data from integration
42
42
  */
43
- getIntegrationFiles: (apiCredentials: any, config?: any, parentId?: any, search?: any, page?: any) => Promise<TreeItem[] | ExtendedResult<TreeItem[]>>;
43
+ getIntegrationFiles: (apiCredentials: any, config?: any, parentId?: any, search?: any, page?: any, paginationData?: any) => Promise<TreeItem[] | ExtendedResult<TreeItem[]>>;
44
44
  /**
45
45
  * function to update crowdin files (e.g. pull integration data to crowdin source files)
46
46
  */
@@ -146,6 +146,10 @@ export interface IntegrationLogic extends ModuleKey {
146
146
  * Enable file filtering by "notSynced" status
147
147
  */
148
148
  notSynced?: boolean;
149
+ /**
150
+ * Enable file filtering by "synced" status
151
+ */
152
+ synced?: boolean;
149
153
  };
150
154
  };
151
155
  /**
@@ -58,7 +58,7 @@ function getOauthRoute(integration) {
58
58
  }
59
59
  exports.getOauthRoute = getOauthRoute;
60
60
  function applyIntegrationModuleDefaults(config, integration) {
61
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
61
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
62
62
  if (!integration.getCrowdinFiles) {
63
63
  integration.getCrowdinFiles = (projectId, client, rootFolder, config, mode) => __awaiter(this, void 0, void 0, function* () {
64
64
  const allBranches = (yield client.sourceFilesApi.withFetchAll().listProjectBranches(projectId)).data.map((d) => d.data);
@@ -152,7 +152,7 @@ function applyIntegrationModuleDefaults(config, integration) {
152
152
  }
153
153
  const getUserSettings = integration.getConfiguration;
154
154
  integration.getConfiguration = (projectId, crowdinClient, integrationCredentials) => __awaiter(this, void 0, void 0, function* () {
155
- var _m, _o, _p;
155
+ var _p, _q, _r;
156
156
  let fields = [];
157
157
  const project = (yield crowdinClient.projectsGroupsApi.getProject(projectId));
158
158
  if (getUserSettings) {
@@ -171,7 +171,7 @@ function applyIntegrationModuleDefaults(config, integration) {
171
171
  if (integration.withCronSync || integration.webhooks) {
172
172
  const userSchedule = fields.find((field) => 'key' in field && field.key === 'schedule');
173
173
  if (userSchedule) {
174
- userSchedule.position = (_m = userSchedule.position) !== null && _m !== void 0 ? _m : 0;
174
+ userSchedule.position = (_p = userSchedule.position) !== null && _p !== void 0 ? _p : 0;
175
175
  userSchedule.category = types_1.DefaultCategory.SYNC;
176
176
  }
177
177
  else {
@@ -211,7 +211,7 @@ function applyIntegrationModuleDefaults(config, integration) {
211
211
  ],
212
212
  });
213
213
  }
214
- if ((_o = integration.syncNewElements) === null || _o === void 0 ? void 0 : _o.crowdin) {
214
+ if ((_q = integration.syncNewElements) === null || _q === void 0 ? void 0 : _q.crowdin) {
215
215
  defaultSettings.push({
216
216
  key: 'new-crowdin-files',
217
217
  label: 'Automatically sync new translations from Crowdin',
@@ -221,7 +221,7 @@ function applyIntegrationModuleDefaults(config, integration) {
221
221
  position: 1,
222
222
  });
223
223
  }
224
- if ((_p = integration.syncNewElements) === null || _p === void 0 ? void 0 : _p.integration) {
224
+ if ((_r = integration.syncNewElements) === null || _r === void 0 ? void 0 : _r.integration) {
225
225
  defaultSettings.push({
226
226
  key: 'new-integration-files',
227
227
  label: `Automatically sync new content from ${config.name}`,
@@ -350,6 +350,12 @@ function applyIntegrationModuleDefaults(config, integration) {
350
350
  label: 'Never Synced',
351
351
  });
352
352
  }
353
+ if ((_o = (_m = integration.filtering) === null || _m === void 0 ? void 0 : _m.integrationFileStatus) === null || _o === void 0 ? void 0 : _o.synced) {
354
+ filterItems.push({
355
+ value: 'synced',
356
+ label: 'Previously Synced',
357
+ });
358
+ }
353
359
  integration.filtering = Object.assign(Object.assign({}, (integration.filtering || {})), { integrationFilterConfig: filterItems.length > 1
354
360
  ? [
355
361
  {
@@ -5,6 +5,7 @@ import { SourceFilesModel } from '@crowdin/crowdin-api-client';
5
5
  export declare function skipFilesByRegex(files: TreeItem[] | undefined, skipIntegrationNodes?: SkipIntegrationNodes): TreeItem[];
6
6
  export declare function expandFilesTree(nodes: IntegrationFile[], req: IntegrationRequest, integration: IntegrationLogic, job?: JobClient): Promise<IntegrationFile[]>;
7
7
  export declare function attachFileStatus(files: any[], clientId: string, crowdinId: string, integration: IntegrationLogic): Promise<any[]>;
8
+ export declare function expandFilesTreeWithPagination(nodes: IntegrationFile[], req: IntegrationRequest, integration: IntegrationLogic, job?: JobClient): Promise<IntegrationFile[]>;
8
9
  export declare function isExtendedResultType<T>(data?: T | ExtendedResult<T>): data is ExtendedResult<T>;
9
10
  export declare function markUnsyncedFiles({ integrationId, crowdinId, client, files, }: {
10
11
  integrationId: string;
@@ -12,7 +12,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
12
12
  return (mod && mod.__esModule) ? mod : { "default": mod };
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.filterSyncedData = exports.updateSyncedData = exports.filterFilesByPath = exports.filterFilesByPatterns = exports.getParentFiles = exports.getParentPaths = exports.buildPath = exports.prepareSyncFiles = exports.uniqueFileLanguages = exports.filterLanguages = exports.getExcludedTargetLanguages = exports.markUnsyncedFiles = exports.isExtendedResultType = exports.attachFileStatus = exports.expandFilesTree = exports.skipFilesByRegex = void 0;
15
+ exports.filterSyncedData = exports.updateSyncedData = exports.filterFilesByPath = exports.filterFilesByPatterns = exports.getParentFiles = exports.getParentPaths = exports.buildPath = exports.prepareSyncFiles = exports.uniqueFileLanguages = exports.filterLanguages = exports.getExcludedTargetLanguages = exports.markUnsyncedFiles = exports.isExtendedResultType = exports.expandFilesTreeWithPagination = exports.attachFileStatus = exports.expandFilesTree = exports.skipFilesByRegex = void 0;
16
16
  const types_1 = require("../types");
17
17
  const types_2 = require("./types");
18
18
  const storage_1 = require("../../../storage");
@@ -38,7 +38,7 @@ function skipFilesByRegex(files, skipIntegrationNodes) {
38
38
  }
39
39
  exports.skipFilesByRegex = skipFilesByRegex;
40
40
  function expandFilesTree(nodes, req, integration, job) {
41
- var _a, _b, _c, _d, _e, _f, _g;
41
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
42
42
  return __awaiter(this, void 0, void 0, function* () {
43
43
  if (job && types_2.JobStatus.CANCELED === ((_a = (yield job.get())) === null || _a === void 0 ? void 0 : _a.status)) {
44
44
  throw new Error('Job canceled');
@@ -72,6 +72,7 @@ function expandFilesTree(nodes, req, integration, job) {
72
72
  const needsFileStatus = ((_c = (_b = integration.filtering) === null || _b === void 0 ? void 0 : _b.integrationFileStatus) === null || _c === void 0 ? void 0 : _c.isNew) ||
73
73
  ((_e = (_d = integration.filtering) === null || _d === void 0 ? void 0 : _d.integrationFileStatus) === null || _e === void 0 ? void 0 : _e.isUpdated) ||
74
74
  ((_g = (_f = integration.filtering) === null || _f === void 0 ? void 0 : _f.integrationFileStatus) === null || _g === void 0 ? void 0 : _g.notSynced) ||
75
+ ((_j = (_h = integration.filtering) === null || _h === void 0 ? void 0 : _h.integrationFileStatus) === null || _j === void 0 ? void 0 : _j.synced) ||
75
76
  integration.forcePushSources === true;
76
77
  if (needsFileStatus) {
77
78
  const { crowdinId, clientId } = req.crowdinContext;
@@ -82,11 +83,12 @@ function expandFilesTree(nodes, req, integration, job) {
82
83
  }
83
84
  exports.expandFilesTree = expandFilesTree;
84
85
  function attachFileStatus(files, clientId, crowdinId, integration) {
85
- var _a, _b, _c, _d, _e, _f;
86
+ var _a, _b, _c, _d, _e, _f, _g, _h;
86
87
  return __awaiter(this, void 0, void 0, function* () {
87
88
  const needsFileStatus = ((_b = (_a = integration.filtering) === null || _a === void 0 ? void 0 : _a.integrationFileStatus) === null || _b === void 0 ? void 0 : _b.isNew) ||
88
89
  ((_d = (_c = integration.filtering) === null || _c === void 0 ? void 0 : _c.integrationFileStatus) === null || _d === void 0 ? void 0 : _d.isUpdated) ||
89
90
  ((_f = (_e = integration.filtering) === null || _e === void 0 ? void 0 : _e.integrationFileStatus) === null || _f === void 0 ? void 0 : _f.notSynced) ||
91
+ ((_h = (_g = integration.filtering) === null || _g === void 0 ? void 0 : _g.integrationFileStatus) === null || _h === void 0 ? void 0 : _h.synced) ||
90
92
  integration.forcePushSources === true;
91
93
  if (!needsFileStatus) {
92
94
  return files;
@@ -95,21 +97,22 @@ function attachFileStatus(files, clientId, crowdinId, integration) {
95
97
  const syncedFiles = (syncedData === null || syncedData === void 0 ? void 0 : syncedData.files) ? JSON.parse(syncedData.files) : [];
96
98
  const lastSyncTimestamp = (syncedData === null || syncedData === void 0 ? void 0 : syncedData.updatedAt) ? Number(syncedData.updatedAt) : null;
97
99
  return files.map((file) => {
98
- var _a, _b, _c, _d, _e, _f;
100
+ var _a, _b, _c, _d, _e, _f, _g, _h;
99
101
  if (!file.type) {
100
102
  return file;
101
103
  }
102
104
  const syncedFile = syncedFiles.find((syncedItem) => syncedItem.id === file.id);
103
105
  const notSynced = ((_b = (_a = integration.filtering) === null || _a === void 0 ? void 0 : _a.integrationFileStatus) === null || _b === void 0 ? void 0 : _b.notSynced) === true ? !syncedFile : false;
106
+ const synced = ((_d = (_c = integration.filtering) === null || _c === void 0 ? void 0 : _c.integrationFileStatus) === null || _d === void 0 ? void 0 : _d.synced) === true ? !!syncedFile : false;
104
107
  const fileSyncTimestamp = (syncedFile === null || syncedFile === void 0 ? void 0 : syncedFile.syncedAt) ? Number(syncedFile.syncedAt) : lastSyncTimestamp;
105
- const isNew = ((_d = (_c = integration.filtering) === null || _c === void 0 ? void 0 : _c.integrationFileStatus) === null || _d === void 0 ? void 0 : _d.isNew) === true
108
+ const isNew = ((_f = (_e = integration.filtering) === null || _e === void 0 ? void 0 : _e.integrationFileStatus) === null || _f === void 0 ? void 0 : _f.isNew) === true
106
109
  ? !syncedFile &&
107
110
  fileSyncTimestamp &&
108
111
  file.createdAt &&
109
112
  (typeof file.createdAt === 'string' ? new Date(file.createdAt).getTime() : file.createdAt) >
110
113
  fileSyncTimestamp
111
114
  : false;
112
- const shouldCalculateIsUpdated = ((_f = (_e = integration.filtering) === null || _e === void 0 ? void 0 : _e.integrationFileStatus) === null || _f === void 0 ? void 0 : _f.isUpdated) === true || integration.forcePushSources === true;
115
+ const shouldCalculateIsUpdated = ((_h = (_g = integration.filtering) === null || _g === void 0 ? void 0 : _g.integrationFileStatus) === null || _h === void 0 ? void 0 : _h.isUpdated) === true || integration.forcePushSources === true;
113
116
  const isUpdated = file.isUpdated !== undefined
114
117
  ? file.isUpdated
115
118
  : shouldCalculateIsUpdated
@@ -121,11 +124,78 @@ function attachFileStatus(files, clientId, crowdinId, integration) {
121
124
  : false;
122
125
  return Object.assign(Object.assign({}, file), { isNew,
123
126
  notSynced,
127
+ synced,
124
128
  isUpdated });
125
129
  });
126
130
  });
127
131
  }
128
132
  exports.attachFileStatus = attachFileStatus;
133
+ function expandFilesTreeWithPagination(nodes, req, integration, job) {
134
+ var _a, _b;
135
+ return __awaiter(this, void 0, void 0, function* () {
136
+ if (job && types_2.JobStatus.CANCELED === ((_a = (yield job.get())) === null || _a === void 0 ? void 0 : _a.status)) {
137
+ throw new Error('Job canceled');
138
+ }
139
+ const files = nodes.filter((file) => file.nodeType === undefined || file.nodeType === '1');
140
+ const folders = nodes.filter((folder) => folder.nodeType === '0' && !nodes.find((node) => node.parentId === folder.id));
141
+ for (const folder of folders) {
142
+ const allFolderFiles = [];
143
+ let paginationData = undefined;
144
+ let hasMore = true;
145
+ let pageCount = 0;
146
+ while (hasMore) {
147
+ if (job && types_2.JobStatus.CANCELED === ((_b = (yield job.get())) === null || _b === void 0 ? void 0 : _b.status)) {
148
+ throw new Error('Job canceled');
149
+ }
150
+ pageCount++;
151
+ try {
152
+ if (job && pageCount > 1) {
153
+ yield job.update({
154
+ info: `Loading files from ${folder.name} (page ${pageCount})`,
155
+ });
156
+ }
157
+ const result = yield integration.getIntegrationFiles(req.integrationCredentials, req.integrationSettings, folder.id, '', 0, paginationData);
158
+ let integrationTreeItems = [];
159
+ if (isExtendedResultType(result)) {
160
+ integrationTreeItems = result.data || [];
161
+ if (result.stopPagination === true) {
162
+ hasMore = false;
163
+ }
164
+ }
165
+ else {
166
+ integrationTreeItems = result || [];
167
+ }
168
+ const loadMoreElement = integrationTreeItems.find((item) => item.nodeType === 'load-more');
169
+ const actualFiles = integrationTreeItems.filter((item) => item.nodeType !== 'load-more');
170
+ allFolderFiles.push(...actualFiles);
171
+ if (loadMoreElement && loadMoreElement.pagination) {
172
+ paginationData = loadMoreElement.pagination;
173
+ }
174
+ else if (!isExtendedResultType(result) || result.stopPagination !== false) {
175
+ hasMore = false;
176
+ }
177
+ }
178
+ catch (e) {
179
+ const errorMessage = e instanceof Error ? e.message : String(e);
180
+ if (job) {
181
+ const currentJob = yield job.get();
182
+ const currentErrors = (currentJob === null || currentJob === void 0 ? void 0 : currentJob.errors) ? JSON.parse(currentJob.errors) : [];
183
+ yield job.update({ errors: [...currentErrors, errorMessage] });
184
+ }
185
+ req.logError(`Error loading page ${pageCount} for folder ${folder.id}: ${errorMessage}`);
186
+ break;
187
+ }
188
+ }
189
+ const checkNodes = allFolderFiles
190
+ .filter((item) => item.id !== folder.id)
191
+ .map((item) => (Object.assign(Object.assign({}, item), { nodeType: item.nodeType || ('type' in item ? '1' : '0') })));
192
+ const expandedResult = yield expandFilesTreeWithPagination(checkNodes, req, integration, job);
193
+ files.push(...expandedResult);
194
+ }
195
+ return files;
196
+ });
197
+ }
198
+ exports.expandFilesTreeWithPagination = expandFilesTreeWithPagination;
129
199
  function isExtendedResultType(data) {
130
200
  const dataTyped = data;
131
201
  return !!dataTyped && !Array.isArray(dataTyped);
@@ -295,7 +295,11 @@ function handle(config) {
295
295
  // prevent possible overrides of the other modules
296
296
  config.aiPromptProvider = Object.assign(Object.assign({}, config.aiPromptProvider), { key: config.identifier + '-ai-prompt-provider' });
297
297
  modules['ai-prompt-provider'] = [
298
- 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
298
+ Object.assign(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.actions
299
+ ? {
300
+ actions: config.aiPromptProvider.actions,
301
+ }
302
+ : {})), (config.aiPromptProvider.formSchema || config.aiPromptProvider.uiPath
299
303
  ? {
300
304
  configuratorUrl: '/prompt-provider/settings/' + (config.aiPromptProvider.fileName || 'index.html'),
301
305
  }
@@ -407,6 +411,35 @@ function handle(config) {
407
411
  : {})), { validateSettingsUrl: (0, index_1.getAutomationActionUrl)('/automation-action/validate-settings', automationAction), executeUrl: (0, index_1.getAutomationActionUrl)('/automation-action/execute', automationAction), invocationWaitMode: automationAction.invocationWaitMode || 'sync' }), (uiModule ? { url: (0, index_1.getAutomationActionUrl)('/automation-action', automationAction) } : {})));
408
412
  }
409
413
  }
414
+ if (config.authGuard) {
415
+ // prevent possible overrides of the other modules
416
+ if (Array.isArray(config.authGuard)) {
417
+ config.authGuard = config.authGuard.map((authGuard) => (Object.assign({}, authGuard)));
418
+ }
419
+ else {
420
+ config.authGuard = Object.assign({}, config.authGuard);
421
+ }
422
+ const authGuards = Array.isArray(config.authGuard) ? config.authGuard : [config.authGuard];
423
+ modules['auth-guard'] = [];
424
+ for (let i = 0; i < authGuards.length; i++) {
425
+ if (!authGuards[i].key) {
426
+ authGuards[i].key = config.identifier + '-auth-guard-' + i;
427
+ }
428
+ const options = authGuards[i].options || {};
429
+ const manifestEntry = {
430
+ key: authGuards[i].key,
431
+ name: authGuards[i].name || config.name,
432
+ url: '/auth-guard/verify',
433
+ };
434
+ if (authGuards[i].description) {
435
+ manifestEntry.description = authGuards[i].description;
436
+ }
437
+ if (options.type || options.applyToAdmin !== undefined || options.url) {
438
+ manifestEntry.options = Object.assign(Object.assign(Object.assign({}, (options.type && { type: options.type })), (options.applyToAdmin !== undefined && { applyToAdmin: options.applyToAdmin })), (options.url && { url: options.url }));
439
+ }
440
+ modules['auth-guard'].push(manifestEntry);
441
+ }
442
+ }
410
443
  const events = {
411
444
  installed: '/installed',
412
445
  uninstall: '/uninstall',
@@ -415,10 +448,10 @@ function handle(config) {
415
448
  events['subscription_paid'] = '/subscription-paid';
416
449
  }
417
450
  return (_req, res) => {
418
- const manifest = Object.assign(Object.assign(Object.assign(Object.assign({ identifier: config.identifier, name: config.name, logo: (0, util_1.getLogoUrl)(), baseUrl: config.baseUrl, authentication: {
451
+ const manifest = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ identifier: config.identifier, name: config.name, logo: (0, util_1.getLogoUrl)(), baseUrl: config.baseUrl, authentication: {
419
452
  type: config.authenticationType || types_1.AuthenticationType.APP,
420
453
  clientId: config.clientId,
421
- }, restrictAiToSameApp: config.restrictAiToSameApp }, (config.agent && { agent: config.agent })), { events, scopes: config.scopes ? config.scopes : [types_1.Scope.PROJECTS] }), (config.defaultPermissions && { default_permissions: config.defaultPermissions })), { modules });
454
+ }, restrictAiToSameApp: config.restrictAiToSameApp }, (config.stringBasedAvailable !== undefined && { stringBasedAvailable: config.stringBasedAvailable })), (config.agent && { agent: config.agent })), { events, scopes: config.scopes ? config.scopes : [types_1.Scope.PROJECTS] }), (config.defaultPermissions && { default_permissions: config.defaultPermissions })), { modules });
422
455
  res.send(manifest);
423
456
  };
424
457
  }
package/out/types.d.ts CHANGED
@@ -20,6 +20,7 @@ import { Webhook } from './modules/webhooks/types';
20
20
  import { WorkflowStepTypeModule } from './modules/workflow-step-type/types';
21
21
  import { AiRequestProcessorModule, AiStreamProcessorModule } from './modules/ai-request-processors/types';
22
22
  import { AutomationActionModule } from './modules/automation-action/types';
23
+ import { AuthGuardModule } from './modules/auth-guard/types';
23
24
  export interface ClientConfig extends ImagePath {
24
25
  /**
25
26
  * Authentication Crowdin App type: "authorization_code", "crowdin_app", "crowdin_agent". Default: "crowdin_app"
@@ -264,10 +265,18 @@ export interface ClientConfig extends ImagePath {
264
265
  * property that tells backend that AiProvider and AiPromptProvider modules can cooperate only with each one
265
266
  */
266
267
  restrictAiToSameApp?: boolean;
268
+ /**
269
+ * Flag to indicate if the app works with string-based projects
270
+ */
271
+ stringBasedAvailable?: boolean;
267
272
  /**
268
273
  * Automation action module
269
274
  */
270
275
  automationAction?: AutomationActionModule | AutomationActionModule[];
276
+ /**
277
+ * Auth guard module for custom authentication/authorization checks
278
+ */
279
+ authGuard?: AuthGuardModule | AuthGuardModule[];
271
280
  }
272
281
  export interface Environments {
273
282
  environments?: Environment | Environment[];
@@ -453,6 +453,15 @@
453
453
  onchange="onChangeAutoSynchronizationOptions(this)"
454
454
  >
455
455
  </crowdin-checkbox>
456
+ <crowdin-checkbox
457
+ id="all-folder-files"
458
+ name="all-folder-files"
459
+ label="All files in the folder"
460
+ class="hydrated"
461
+ hidden
462
+ onchange="onChangeAutoSynchronizationOptions(this)"
463
+ >
464
+ </crowdin-checkbox>
456
465
  <crowdin-checkbox
457
466
  id="new-files"
458
467
  name="new-files"
@@ -499,8 +508,21 @@
499
508
  cancelJob(e.detail);
500
509
  });
501
510
  {{#if integrationSearchListener}}
502
- document.body.addEventListener("integrationFilterChange", (event) => {
503
- getIntegrationData({ search: event.detail });
511
+ document.body.addEventListener("integrationFilterChange", async (event) => {
512
+ const isEmptySearch = !event.detail || event.detail.trim() === '';
513
+
514
+ if (isEmptySearch && savedIntegrationData && hasLoadMoreElements) {
515
+ appComponent.setIntegrationFilesData(savedIntegrationData);
516
+ appComponent.setIntegrationOpenedFolders(currentOpenedFolders);
517
+ return;
518
+ }
519
+
520
+ if (!isEmptySearch && hasLoadMoreElements) {
521
+ savedIntegrationData = await appComponent.getIntegrationFilesData();
522
+ currentOpenedFolders = await appComponent.getIntegrationOpenedFolders();
523
+ }
524
+
525
+ getIntegrationData({ search: event.detail });
504
526
  })
505
527
  {{/if}}
506
528
  const appComponent = document.querySelector('crowdin-simple-integration');
@@ -535,6 +557,9 @@
535
557
  let project = {};
536
558
  let crowdinData = [];
537
559
  let fileToSync = [];
560
+ let hasLoadMoreElements = false;
561
+ let savedIntegrationData = null;
562
+ let currentOpenedFolders = null;
538
563
 
539
564
  getCrowdinData();
540
565
  getIntegrationData({});
@@ -628,10 +653,29 @@
628
653
  .finally(() => (appComponent.setAttribute('is-crowdin-loading', false)));
629
654
  }
630
655
 
631
- function getIntegrationData({ hardReload = false, parentId = '', search = '', page = 0, recursion = false } = {}, recursionState = null) {
632
- appComponent.setAttribute('is-integration-loading', true);
656
+ function getIntegrationData(
657
+ {
658
+ hardReload = false,
659
+ parentId = '',
660
+ search = '',
661
+ page = 0,
662
+ recursion = false,
663
+ paginationData,
664
+ } = {},
665
+ recursionState = null
666
+ ) {
667
+ if (!paginationData) {
668
+ appComponent.setAttribute('is-integration-loading', true);
669
+ }
670
+
633
671
  return checkOrigin()
634
- .then(restParams => fetch(`api/integration/data${restParams}&parent_id=${encodeURIComponent(parentId)}&search=${encodeURIComponent(search)}&page=${page}`))
672
+ .then(restParams => {
673
+ let url = `api/integration/data${restParams}&parent_id=${encodeURIComponent(parentId)}&search=${encodeURIComponent(search)}&page=${page}`;
674
+ if (paginationData) {
675
+ url += `&paginationData=${encodeURIComponent(JSON.stringify(paginationData))}`;
676
+ }
677
+ return fetch(url);
678
+ })
635
679
  .then(checkResponse)
636
680
  .then(async (res) => {
637
681
  const files = res.data;
@@ -645,11 +689,13 @@
645
689
  labels: e.labels,
646
690
  disabled: e.disabled,
647
691
  tooltip: e.tooltip,
692
+ pagination: e.pagination,
648
693
  };
649
694
  if (e.type) {
650
695
  item.isNew = e.isNew;
651
696
  item.isUpdated = e.isUpdated;
652
697
  item.notSynced = e.notSynced;
698
+ item.synced = e.synced;
653
699
  item.type = e.type;
654
700
  item.node_type = fileType;
655
701
  } else {
@@ -658,6 +704,10 @@
658
704
  return item;
659
705
  });
660
706
 
707
+ if ((!paginationData && !parentId && !search) || hardReload) {
708
+ hasLoadMoreElements = tree.some(item => item.node_type === 'load-more');
709
+ }
710
+
661
711
  const appIntegrationFiles = appComponent.querySelector('#integration-files');
662
712
  if (hardReload) {
663
713
  appComponent.setIntegrationFilesData(tree);
@@ -1496,6 +1546,18 @@
1496
1546
  })
1497
1547
  {{/if}}
1498
1548
 
1549
+ document.body.addEventListener('loadMoreClick', async (e) => {
1550
+ const loadMoreFile = e.detail;
1551
+ const fileComponent = document.getElementById('integration-files')
1552
+
1553
+ await fileComponent.setLoadMoreLoading(loadMoreFile.id, true);
1554
+
1555
+ getIntegrationData({ parentId: e.detail.parent_id, paginationData: e.detail.pagination })
1556
+ .finally(async () => {
1557
+ await fileComponent.setLoadMoreLoading(loadMoreFile.id, false);
1558
+ });
1559
+ });
1560
+
1499
1561
  {{#or withCronSync webhooks}}
1500
1562
  const scheduleModal = document.getElementById('confirm-schedule-modal');
1501
1563
  let syncData;
@@ -1520,6 +1582,7 @@
1520
1582
  async function saveScheduleSync() {
1521
1583
  const newFile = scheduleModal.querySelector('#new-files').checked || false;
1522
1584
  const selectedFiles = scheduleModal.querySelector('#selected-files').checked || false;
1585
+ const allFolderFiles = scheduleModal.querySelector('#all-folder-files').checked || false;
1523
1586
 
1524
1587
  const type = scheduleModal.getAttribute('data-type');
1525
1588
 
@@ -1530,7 +1593,11 @@
1530
1593
  appComponent.setAttribute('is-crowdin-sync-settings-in-progress', true);
1531
1594
  updateSyncSettings(syncedFiles, 'schedule', 'crowdin');
1532
1595
  } else if (type === 'integration') {
1533
- appComponent.setIntegrationScheduleSync(syncData, newFile, selectedFiles);
1596
+ {{#if syncNewElements.integration}}
1597
+ appComponent.setIntegrationScheduleSync(syncData, newFile, selectedFiles);
1598
+ {{else}}
1599
+ appComponent.setIntegrationScheduleSync(syncData);
1600
+ {{/if}}
1534
1601
  appComponent.setIntegrationFilesHasSyncOption(true);
1535
1602
  const syncedFiles = await appComponent.getIntegrationScheduleSync(true);
1536
1603
  appComponent.setAttribute('is-integration-sync-settings-in-progress', true);
@@ -1542,16 +1609,28 @@
1542
1609
  selectedFiles ? getIntegrationFoldersToExpand(syncData, syncedFiles) : [],
1543
1610
  );
1544
1611
  {{else}}
1545
- updateSyncSettings(syncedFiles, 'schedule', 'integration');
1612
+ if (allFolderFiles) {
1613
+ const integrationExpandedFolders = syncData.filter(file => file.node_type === folderType).map(file => ({
1614
+ ...file,
1615
+ nodeType: file.node_type,
1616
+ parentId: file.parent_id,
1617
+ }));
1618
+ updateSyncSettings(syncedFiles, 'schedule', 'integration', integrationExpandedFolders);
1619
+ } else {
1620
+ updateSyncSettings(syncedFiles, 'schedule', 'integration');
1621
+ }
1546
1622
  {{/if}}
1547
1623
  }
1548
1624
 
1549
1625
  closeModal(scheduleModal);
1550
1626
  }
1551
1627
 
1552
- async function openScheduleModal(type) {
1628
+ async function openScheduleModal(type, options = {}) {
1629
+ const { showAllFolderFiles = false, showNewFiles = true } = options;
1630
+
1553
1631
  const newFile = scheduleModal.querySelector('#new-files')
1554
1632
  const selectedFiles = scheduleModal.querySelector('#selected-files');
1633
+ const allFolderFiles = scheduleModal.querySelector('#all-folder-files');
1555
1634
  let newFileStatus = 'unChecked';
1556
1635
 
1557
1636
  const integrationSyncFolders = (await document.querySelector('crowdin-simple-integration').getIntegrationScheduleSync(true))
@@ -1577,18 +1656,33 @@
1577
1656
  }
1578
1657
  }
1579
1658
 
1580
- if (newFileStatus === 'checked') {
1581
- newFile[newFileStatus] = true;
1582
- newFile.value = true;
1583
- newFile.removeAttribute('clear-selection');
1584
- } else if (newFileStatus === 'clear-selection') {
1585
- newFile.checked = false;
1586
- newFile.value = false;
1587
- newFile.setAttribute(newFileStatus, '');
1659
+ if (showNewFiles) {
1660
+ newFile.removeAttribute('hidden');
1661
+ if (newFileStatus === 'checked') {
1662
+ newFile[newFileStatus] = true;
1663
+ newFile.value = true;
1664
+ newFile.removeAttribute('clear-selection');
1665
+ } else if (newFileStatus === 'clear-selection') {
1666
+ newFile.checked = false;
1667
+ newFile.value = false;
1668
+ newFile.setAttribute(newFileStatus, '');
1669
+ } else {
1670
+ newFile.checked = false;
1671
+ newFile.value = false;
1672
+ newFile.removeAttribute('clear-selection');
1673
+ }
1588
1674
  } else {
1675
+ newFile.setAttribute('hidden', '');
1589
1676
  newFile.checked = false;
1590
- newFile.value = false;
1591
- newFile.removeAttribute('clear-selection');
1677
+ }
1678
+
1679
+ if (showAllFolderFiles) {
1680
+ allFolderFiles.removeAttribute('hidden');
1681
+ allFolderFiles.checked = false;
1682
+ allFolderFiles.value = false;
1683
+ } else {
1684
+ allFolderFiles.setAttribute('hidden', '');
1685
+ allFolderFiles.checked = false;
1592
1686
  }
1593
1687
 
1594
1688
  selectedFiles.checked = true;
@@ -1604,9 +1698,10 @@
1604
1698
 
1605
1699
  const newFilesStatus = newFiles.checked || false;
1606
1700
  const selectedFiles = document.getElementById('selected-files').checked || false;
1701
+ const allFolderFiles = document.getElementById('all-folder-files').checked || false;
1607
1702
  const buttonSaveScheduleSync = document.getElementById('save-schedule-sync');
1608
1703
 
1609
- if (newFilesStatus || selectedFiles) {
1704
+ if (newFilesStatus || selectedFiles || allFolderFiles) {
1610
1705
  buttonSaveScheduleSync.removeAttribute('disabled');
1611
1706
  } else {
1612
1707
  buttonSaveScheduleSync.setAttribute('disabled', true);
@@ -1635,8 +1730,13 @@
1635
1730
  {{#if syncNewElements.integration}}
1636
1731
  syncData = e.detail;
1637
1732
  const isFolder = await hasFolder(e.detail);
1638
- if (isFolder && !newIntegrationFiles) {
1639
- await openScheduleModal('integration');
1733
+
1734
+ const shouldShowModal = isFolder && !newIntegrationFiles;
1735
+ const showNewFiles = true; // завжди показуємо New files коли є syncNewElements.integration
1736
+ const showAllFolderFiles = hasLoadMoreElements; // показуємо All files якщо є hasLoadMoreElements
1737
+
1738
+ if (shouldShowModal) {
1739
+ await openScheduleModal('integration', { showNewFiles, showAllFolderFiles });
1640
1740
  return;
1641
1741
  }
1642
1742
 
@@ -1644,14 +1744,33 @@
1644
1744
  appComponent.setIntegrationFilesHasSyncOption(true);
1645
1745
  const syncedFiles = await appComponent.getIntegrationScheduleSync(true);
1646
1746
  {{else}}
1747
+ syncData = e.detail;
1748
+ const isFolder = await hasFolder(e.detail);
1749
+
1750
+ if (hasLoadMoreElements && isFolder) {
1751
+ await openScheduleModal('integration', { showNewFiles: false, showAllFolderFiles: true });
1752
+ return;
1753
+ }
1754
+
1647
1755
  const syncedFiles = await appComponent.getIntegrationScheduleSync();
1648
1756
  {{/if}}
1649
1757
 
1650
1758
  appComponent.setAttribute('is-integration-sync-settings-in-progress', true);
1759
+
1651
1760
  {{#if integrationOneLevelFetching}}
1652
1761
  updateSyncSettings(syncedFiles, 'schedule', 'integration', getIntegrationFoldersToExpand(e.detail, syncedFiles));
1653
1762
  {{else}}
1654
- updateSyncSettings(syncedFiles, 'schedule', 'integration');
1763
+ if (hasLoadMoreElements) {
1764
+ const integrationExpandedFolders = e.detail.filter(file => file.node_type === folderType).map(file => ({
1765
+ ...file,
1766
+ nodeType: file.node_type,
1767
+ parentId: file.parent_id,
1768
+ }));
1769
+
1770
+ updateSyncSettings(syncedFiles, 'schedule', 'integration', integrationExpandedFolders);
1771
+ } else {
1772
+ updateSyncSettings(syncedFiles, 'schedule', 'integration');
1773
+ }
1655
1774
  {{/if}}
1656
1775
  }
1657
1776
 
@@ -1714,7 +1833,7 @@
1714
1833
  .then(restParams => fetch('api/sync-settings' + restParams, {
1715
1834
  method: 'POST',
1716
1835
  headers: { 'Content-Type': 'application/json' },
1717
- body: JSON.stringify({ files, type, provider, expandIntegrationFolders })
1836
+ body: JSON.stringify({ files, type, provider, expandIntegrationFolders, hasLoadMoreElements }),
1718
1837
  }))
1719
1838
  .then(checkResponse)
1720
1839
  .then((res) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crowdin/app-project-module",
3
- "version": "0.102.2",
3
+ "version": "0.104.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",