diff --git a/dev/src/lib/tests/collections/localized-posts-delete-404-files.test.ts b/dev/src/lib/tests/collections/localized-posts-delete-404-files.test.ts new file mode 100644 index 0000000..1f42958 --- /dev/null +++ b/dev/src/lib/tests/collections/localized-posts-delete-404-files.test.ts @@ -0,0 +1,98 @@ +import payload from 'payload'; +import { initPayloadTest } from '../helpers/config'; +import nock from 'nock'; +import { mockCrowdinClient } from 'plugin/src/lib/api/mock/crowdin-api-responses'; +import { pluginConfig } from '../helpers/plugin-config'; +import { getFilesByDocumentID, payloadCrowdinSyncTranslationsApi } from 'payload-crowdin-sync'; + +const pluginOptions = pluginConfig(); +const mockClient = mockCrowdinClient(pluginOptions); + +describe('Lexical editor with multiple blocks', () => { + beforeAll(async () => { + await initPayloadTest({}); + }); + + afterEach((done) => { + if (!nock.isDone()) { + throw new Error( + `Not all nock interceptors were used: ${JSON.stringify( + nock.pendingMocks() + )}` + ); + } + nock.cleanAll(); + done(); + }); + + afterAll(async () => { + if (typeof payload?.db?.destroy === 'function') { + await payload.db.destroy(payload); + } + }); + + it('removes CrowdinFile Payload documents if the Crowdin API responds with a 404', async () => { + nock('https://api.crowdin.com') + .post( + `/api/v2/projects/${pluginOptions.projectId}/directories` + ) + .twice() + .reply(200, mockClient.createDirectory({})) + .post( + `/api/v2/storages` + ) + .reply(200, mockClient.addStorage()) + .post( + `/api/v2/projects/${pluginOptions.projectId}/files` + ) + .reply( + 200, + mockClient.createFile({ + fileId: 94100, + }) + ) + // translation + .post( + `/api/v2/projects/${ + pluginOptions.projectId + }/translations/builds/files/${94100}`, + { + targetLanguageId: 'fr', + } + ) + .reply( + 404, + { + code: 404, + } + ) + + const post = await payload.create({ + collection: "localized-posts-with-condition", + data: { + title: "Test post", + translateWithCrowdin: true, + }, + }); + + const crowdinFiles = await getFilesByDocumentID(`${post.id}`, payload); + + expect(crowdinFiles.length).toEqual(1) + + const translationsApi = new payloadCrowdinSyncTranslationsApi( + pluginOptions, + payload + ); + + await translationsApi.updateTranslation({ + documentId: `${post.id}`, + collection: 'localized-posts-with-condition', + dryRun: false, + excludeLocales: ['de_DE'], + }); + + const refreshedCrowdinFiles = await getFilesByDocumentID(`${post.id}`, payload); + + expect(refreshedCrowdinFiles.length).toEqual(0) + }); +}); diff --git a/dev/src/lib/tests/collections/localized-posts-with-condition.test.ts b/dev/src/lib/tests/collections/localized-posts-with-condition.test.ts index 50ff188..18d6dd1 100644 --- a/dev/src/lib/tests/collections/localized-posts-with-condition.test.ts +++ b/dev/src/lib/tests/collections/localized-posts-with-condition.test.ts @@ -62,7 +62,7 @@ describe("Collection: Localized Posts With Conditon", () => { expect(Object.prototype.hasOwnProperty.call(result, 'crowdinArticleDirectory')).toBeFalsy(); }); - it("creates an article directory if the conditon is met", async () => { + it("creates an article directory if the condition is met", async () => { nock('https://api.crowdin.com') .post( `/api/v2/projects/${pluginOptions.projectId}/directories` diff --git a/plugin/src/lib/api/payload-crowdin-sync/files/by-document.ts b/plugin/src/lib/api/payload-crowdin-sync/files/by-document.ts index c8db225..8dc18ef 100644 --- a/plugin/src/lib/api/payload-crowdin-sync/files/by-document.ts +++ b/plugin/src/lib/api/payload-crowdin-sync/files/by-document.ts @@ -29,7 +29,7 @@ export class filesApiByDocument { directoryId?: number; document: Document articleDirectory: CrowdinArticleDirectory - collectionSlug: keyof Config['collections'] | "globals"; + collectionSlug: keyof Config['collections'] | keyof Config['globals']; global: boolean; pluginOptions: PluginOptions; req: PayloadRequest; @@ -44,7 +44,7 @@ export class filesApiByDocument { parent, }: { document: Document, - collectionSlug: keyof Config['collections'] | "globals", + collectionSlug: keyof Config['collections'] | keyof Config['globals'], global: boolean, pluginOptions: PluginOptions, req: PayloadRequest, diff --git a/plugin/src/lib/api/payload-crowdin-sync/translations.ts b/plugin/src/lib/api/payload-crowdin-sync/translations.ts index 15e3141..410be11 100644 --- a/plugin/src/lib/api/payload-crowdin-sync/translations.ts +++ b/plugin/src/lib/api/payload-crowdin-sync/translations.ts @@ -1,5 +1,6 @@ import crowdin, { Credentials, + CrowdinError, Translations, } from "@crowdin/crowdin-api-client"; import { Payload } from "payload"; @@ -73,6 +74,7 @@ export class payloadCrowdinSyncTranslationsApi { localeMap: PluginOptions["localeMap"]; sourceLocale: PluginOptions["sourceLocale"]; htmlToSlateConfig: PluginOptions["htmlToSlateConfig"]; + disableSelfClean?: PluginOptions["disableSelfClean"] constructor(pluginOptions: PluginOptions, payload: Payload) { // credentials @@ -88,6 +90,7 @@ export class payloadCrowdinSyncTranslationsApi { this.localeMap = pluginOptions.localeMap; this.sourceLocale = pluginOptions.sourceLocale; this.htmlToSlateConfig = pluginOptions.htmlToSlateConfig + this.disableSelfClean = pluginOptions.disableSelfClean } async updateTranslation({ @@ -376,13 +379,13 @@ export class payloadCrowdinSyncTranslationsApi { return; } try { - const response = await this.translationsApi.buildProjectFileTranslation( - this.projectId, - file.originalId as number, - { - targetLanguageId: this.localeMap[locale].crowdinId, - } - ); + const response = await this.buildTranslationFile({ + file, + locale + }) + if (!response) { + return + } const data = await this.getFileDataFromUrl(response.data.url); if (file.type === "html") { const allFields = collection ? collection.fields : fields @@ -458,6 +461,37 @@ export class payloadCrowdinSyncTranslationsApi { } } + private async buildTranslationFile({ + file, + locale + }: { + file: CrowdinFile, + locale: string + }) { + try { + const response = await this.translationsApi.buildProjectFileTranslation( + this.projectId, + file.originalId as number, + { + targetLanguageId: this.localeMap[locale].crowdinId, + } + ); + return response + } + catch (error: unknown) { + if (this.disableSelfClean) { + return undefined + } + if (error instanceof CrowdinError && error.code === 404) { + await this.payload.delete({ + id: file.id, + collection: "crowdin-files", + }) + } + return undefined + } + } + private async getBlockTranslations({ blockConfig, file, diff --git a/plugin/src/lib/plugin.ts b/plugin/src/lib/plugin.ts index b331d68..8df6220 100644 --- a/plugin/src/lib/plugin.ts +++ b/plugin/src/lib/plugin.ts @@ -100,6 +100,8 @@ export const crowdinSync = pluginCollectionAdmin: Joi.object(), tabbedUI: Joi.boolean(), lexicalBlockFolderPrefix: Joi.string(), + /** Prevent the plugin deleting Payload documents it has created in response to Crowdin API responses. */ + disableSelfClean: Joi.boolean(), }); const validate = schema.validate(pluginOptions); diff --git a/plugin/src/lib/types.ts b/plugin/src/lib/types.ts index 7c92f29..9338c18 100644 --- a/plugin/src/lib/types.ts +++ b/plugin/src/lib/types.ts @@ -45,6 +45,7 @@ export interface PluginOptions { pluginCollectionAdmin?: CollectionConfig["admin"]; tabbedUI?: boolean lexicalBlockFolderPrefix?: string + disableSelfClean?: boolean } export type FieldWithName = Field & { name: string };