diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 1872640a..16f5e5fb 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -17,10 +17,15 @@ jobs: "cypress/e2e/jobs/copy-source-text-flow/filters.cy.js", "cypress/e2e/jobs/copy-source-text-flow/success-path-multiple.cy.js", "cypress/e2e/jobs/copy-source-text-flow/success-path-single.cy.js", + "cypress/e2e/jobs/copy-source-text-flow/success-path-multiple-async-publishing.cy.js" + "cypress/e2e/jobs/copy-source-text-flow/success-path-single-async-publishing.cy.js" "cypress/e2e/jobs/instant/success-path-multiple.cy.js", "cypress/e2e/jobs/instant/success-path-multiple-copy-slug.cy.js", "cypress/e2e/jobs/instant/success-path-multiple-copy-slug-and-enable-after-publish.cy.js", "cypress/e2e/jobs/instant/success-path-multiple-enable-after-publish.cy.js", + "cypress/e2e/jobs/instant/success-path-single.cy.js", + "cypress/e2e/jobs/instant/success-path-multiple-async-publish.cy.js", + "cypress/e2e/jobs/instant/success-path-single-async-publishing.cy.js", "cypress/e2e/jobs/verified/success-path-multiple-bulk-publishing.cy.js", "cypress/e2e/jobs/verified/success-path-multiple-bulk-publishing-copy-slug.cy.js", "cypress/e2e/jobs/verified/success-path-multiple-bulk-publishing-copy-slug-and-enable-after-publish.cy.js", @@ -30,7 +35,9 @@ jobs: "cypress/e2e/jobs/verified/success-path-multiple-single-publishing-copy-slug-and-enable-after-publish.cy.js", "cypress/e2e/jobs/verified/success-path-multiple-single-publishing-enable-after-publish.cy.js", "cypress/e2e/jobs/verified/success-path-single.cy.js", - "cypress/e2e/jobs/instant/success-path-single.cy.js", + "cypress/e2e/jobs/verified/success-path-multiple-bulk-async-publishing.cy.js", + "cypress/e2e/jobs/verified/success-path-multiple-single-async-publishing.cy.js", + "cypress/e2e/jobs/verified/success-path-single-async-publishing.cy.js" ] runs-on: ubuntu-latest steps: diff --git a/e2e/cypress.config.js b/e2e/cypress.config.js index 2650bff6..929efd68 100644 --- a/e2e/cypress.config.js +++ b/e2e/cypress.config.js @@ -10,4 +10,10 @@ export default defineConfig({ // implement node event listeners here }, }, + retries: { + // Configure retry attempts for `cypress run` + runMode: 2, + // Configure retry attempts for `cypress open` + openMode: 0 + } }); diff --git a/e2e/cypress/e2e/jobs/copy-source-text-flow/success-path-multiple-async-publishing.cy.js b/e2e/cypress/e2e/jobs/copy-source-text-flow/success-path-multiple-async-publishing.cy.js new file mode 100644 index 00000000..f89706f6 --- /dev/null +++ b/e2e/cypress/e2e/jobs/copy-source-text-flow/success-path-multiple-async-publishing.cy.js @@ -0,0 +1,67 @@ +const {generateJobData} = require('../../../support/job/generator.js'); + +describe( + '[Copy Source Text] Success path for job with multiple target languages', + () => { + const entryLabel = 'The Future of Augmented Reality'; + + it('with copy slug disabled & enable after publish disabled', () => { + const {jobTitle, slug} = generateJobData(); + + cy.copySourceTextFlow({ + slug, + entryLabel, + jobTitle, + copySlug: false, + enableAfterPublish: false, + languages: ['de', 'es', 'uk'], + batchPublishing: true, + publishTranslationsAsync: true + }) + }); + + it('with copy slug disabled & enable after publish enabled', () => { + const {jobTitle, slug} = generateJobData(); + + cy.copySourceTextFlow({ + slug, + entryLabel, + jobTitle, + copySlug: false, + enableAfterPublish: true, + languages: ['de', 'es', 'uk'], + batchPublishing: true, + publishTranslationsAsync: true + }) + }); + + it('with copy slug enabled & enable after publish disabled', () => { + const {jobTitle, slug} = generateJobData(); + + cy.copySourceTextFlow({ + slug, + entryLabel, + jobTitle, + copySlug: true, + enableAfterPublish: false, + languages: ['de', 'es', 'uk'], + batchPublishing: true, + publishTranslationsAsync: true + }) + }); + + it('with copy slug enabled & enable after publish enabled', () => { + const {jobTitle, slug} = generateJobData(); + + cy.copySourceTextFlow({ + slug, + entryLabel, + jobTitle, + copySlug: true, + enableAfterPublish: true, + languages: ['de', 'es', 'uk'], + batchPublishing: true, + publishTranslationsAsync: true + }) + }); + }); diff --git a/e2e/cypress/e2e/jobs/copy-source-text-flow/success-path-single-async-publishing.cy.js b/e2e/cypress/e2e/jobs/copy-source-text-flow/success-path-single-async-publishing.cy.js new file mode 100644 index 00000000..3068916a --- /dev/null +++ b/e2e/cypress/e2e/jobs/copy-source-text-flow/success-path-single-async-publishing.cy.js @@ -0,0 +1,63 @@ +const {generateJobData} = require('../../../support/job/generator.js'); + +describe( + '[Copy Source Text] Success path for job with one target language', + () => { + const entryLabel = 'The Future of Augmented Reality'; + + it('with copy slug disabled & enable after publish disabled', () => { + const {jobTitle, slug} = generateJobData(); + + cy.copySourceTextFlow({ + slug, + entryLabel, + jobTitle, + copySlug: false, + enableAfterPublish: false, + languages: ["de"], + publishTranslationsAsync: true + }) + }); + + it('with copy slug disabled & enable after publish enabled', () => { + const {jobTitle, slug} = generateJobData(); + + cy.copySourceTextFlow({ + slug, + entryLabel, + jobTitle, + copySlug: false, + enableAfterPublish: true, + languages: ["de"], + publishTranslationsAsync: true + }) + }); + + it('with copy slug enabled & enable after publish disabled', () => { + const {jobTitle, slug} = generateJobData(); + + cy.copySourceTextFlow({ + slug, + entryLabel, + jobTitle, + copySlug: true, + enableAfterPublish: false, + languages: ["de"], + publishTranslationsAsync: true + }) + }); + + it('with copy slug enabled & enable after publish enabled', () => { + const {jobTitle, slug} = generateJobData(); + + cy.copySourceTextFlow({ + slug, + entryLabel, + jobTitle, + copySlug: true, + enableAfterPublish: true, + languages: ["de"], + publishTranslationsAsync: true + }) + }); + }); diff --git a/e2e/cypress/e2e/jobs/instant/success-path-multiple-async-publish.cy.js b/e2e/cypress/e2e/jobs/instant/success-path-multiple-async-publish.cy.js new file mode 100644 index 00000000..07cb044e --- /dev/null +++ b/e2e/cypress/e2e/jobs/instant/success-path-multiple-async-publish.cy.js @@ -0,0 +1,25 @@ +const {generateJobData} = require('../../../support/job/generator.js'); + +describe( + '[Instant] Success path for job with multiple target languages', + () => { + const entryLabel = 'The Future of Augmented Reality'; + const entryId = 24; + + it('async publishing', () => { + const {jobTitle, slug} = generateJobData(); + + cy.instantFlow({ + slug, + entryLabel, + jobTitle, + entryId, + copySlug: false, + enableAfterPublish: false, + languages: ['de', 'es', 'uk'], + batchPublishing: true, + publishTranslationsAsync: true + }); + }); + + }); diff --git a/e2e/cypress/e2e/jobs/instant/success-path-single-async-publishing.cy.js b/e2e/cypress/e2e/jobs/instant/success-path-single-async-publishing.cy.js new file mode 100644 index 00000000..7baa06ff --- /dev/null +++ b/e2e/cypress/e2e/jobs/instant/success-path-single-async-publishing.cy.js @@ -0,0 +1,23 @@ +const {generateJobData} = require('../../../support/job/generator.js'); + +describe( + '[Instant] Success path for job with single target language', + () => { + const entryLabel = 'The Future of Augmented Reality'; + const entryId = 24; + + it('async publishing', () => { + const {jobTitle, slug} = generateJobData(); + + cy.instantFlow({ + slug, + entryLabel, + jobTitle, + entryId, + copySlug: false, + enableAfterPublish: false, + languages: ["de"], + publishTranslationsAsync: true + }) + }); + }); diff --git a/e2e/cypress/e2e/jobs/verified/success-path-multiple-bulk-async-publishing.cy.js b/e2e/cypress/e2e/jobs/verified/success-path-multiple-bulk-async-publishing.cy.js new file mode 100644 index 00000000..352a5fba --- /dev/null +++ b/e2e/cypress/e2e/jobs/verified/success-path-multiple-bulk-async-publishing.cy.js @@ -0,0 +1,25 @@ +const {generateJobData} = require('../../../support/job/generator.js'); + +describe( + '[Verified] Success path for job with multiple target languages with bulk publishing', + () => { + const entryLabel = 'The Future of Augmented Reality'; + const entryId = 24; + + it('async publishing', () => { + const {jobTitle, slug} = generateJobData(); + + cy.verifiedFlow({ + slug, + entryLabel, + jobTitle, + entryId, + copySlug: false, + enableAfterPublish: false, + languages: ['de', 'es', 'uk'], + batchPublishing: true, + publishTranslationsAsync: true + }); + }); + + }); diff --git a/e2e/cypress/e2e/jobs/verified/success-path-multiple-single-async-publishing.cy.js b/e2e/cypress/e2e/jobs/verified/success-path-multiple-single-async-publishing.cy.js new file mode 100644 index 00000000..d8304847 --- /dev/null +++ b/e2e/cypress/e2e/jobs/verified/success-path-multiple-single-async-publishing.cy.js @@ -0,0 +1,24 @@ +const {generateJobData} = require('../../../support/job/generator.js'); + +describe( + '[Verified] Success path for job with multiple target languages with single publishing', + () => { + const entryLabel = 'The Future of Augmented Reality'; + const entryId = 24; + + it('async publishing', () => { + const {jobTitle, slug} = generateJobData(); + + cy.verifiedFlow({ + slug, + entryLabel, + jobTitle, + entryId, + copySlug: false, + enableAfterPublish: false, + languages: ['de', 'es', 'uk'], + batchPublishing: false, + publishTranslationsAsync: true + }); + }); + }); diff --git a/e2e/cypress/e2e/jobs/verified/success-path-single-async-publishing.cy.js b/e2e/cypress/e2e/jobs/verified/success-path-single-async-publishing.cy.js new file mode 100644 index 00000000..e0bc4125 --- /dev/null +++ b/e2e/cypress/e2e/jobs/verified/success-path-single-async-publishing.cy.js @@ -0,0 +1,23 @@ +const {generateJobData} = require('../../../support/job/generator.js'); + +describe( + '[Verified] Success path for job with single target language', + () => { + const entryLabel = 'The Future of Augmented Reality'; + const entryId = 24; + + it('async publishing', () => { + const {jobTitle, slug} = generateJobData(); + + cy.verifiedFlow({ + slug, + entryLabel, + jobTitle, + entryId, + copySlug: false, + enableAfterPublish: false, + languages: ["de"], + publishTranslationsAsync: true + }) + }); + }); diff --git a/e2e/cypress/support/commands.js b/e2e/cypress/support/commands.js index 8e1f086e..dd918c84 100644 --- a/e2e/cypress/support/commands.js +++ b/e2e/cypress/support/commands.js @@ -575,18 +575,22 @@ Cypress.Commands.add('assertEntryContent', * @param {object} options * @returns undefined */ -Cypress.Commands.add('copySourceTextFlow', ({ - slug, - entryLabel, - jobTitle, - copySlug = false, - enableAfterPublish = false, - languages = ['de'], - batchPublishing = false, //publish all translations at once with publish button - entryId = 24, - }) => { +Cypress.Commands.add( + 'copySourceTextFlow', + ({ + slug, + entryLabel, + jobTitle, + copySlug = false, + enableAfterPublish = false, + languages = ['de'], + batchPublishing = false, //publish all translations at once with publish button + entryId = 24, + publishTranslationsAsync = true, + }) => { cy.setConfigurationOption('enableEntries', enableAfterPublish); cy.setConfigurationOption('copySlug', copySlug); + cy.setConfigurationOption('publishTranslationsAsync', publishTranslationsAsync); if (copySlug) { // update slug on entry and enable slug copy option diff --git a/e2e/cypress/support/flow/instant.js b/e2e/cypress/support/flow/instant.js index 413775a5..284825c1 100644 --- a/e2e/cypress/support/flow/instant.js +++ b/e2e/cypress/support/flow/instant.js @@ -17,6 +17,7 @@ Cypress.Commands.add('instantFlow', ({ batchPublishing = false, //publish all translations at once with publish button entryId = 24, splitSend = true, + publishTranslationsAsync = false, }) => { const isMockserverEnabled = Cypress.env('MOCKSERVER_ENABLED'); @@ -27,6 +28,7 @@ Cypress.Commands.add('instantFlow', ({ cy.setConfigurationOption('enableEntries', enableAfterPublish); cy.setConfigurationOption('copySlug', copySlug); cy.setConfigurationOption('splitSend', splitSend); + cy.setConfigurationOption('publishTranslationsAsync', publishTranslationsAsync); if (copySlug) { // update slug on entry and enable slug copy option diff --git a/e2e/cypress/support/flow/verified.js b/e2e/cypress/support/flow/verified.js index 233f8aff..a7a39202 100644 --- a/e2e/cypress/support/flow/verified.js +++ b/e2e/cypress/support/flow/verified.js @@ -18,6 +18,7 @@ Cypress.Commands.add('verifiedFlow', ({ batchPublishing = false, //publish all translations at once with publish button entryId = 24, splitSend = true, + publishTranslationsAsync = true, }) => { const isMockserverEnabled = Cypress.env('MOCKSERVER_ENABLED'); @@ -28,6 +29,7 @@ Cypress.Commands.add('verifiedFlow', ({ cy.setConfigurationOption('enableEntries', enableAfterPublish); cy.setConfigurationOption('copySlug', copySlug); cy.setConfigurationOption('splitSend', splitSend); + cy.setConfigurationOption('publishTranslationsAsync', publishTranslationsAsync); if (copySlug) { // update slug on entry and enable slug copy option diff --git a/src/Craftliltplugin.php b/src/Craftliltplugin.php index abe9a4ec..280608b8 100644 --- a/src/Craftliltplugin.php +++ b/src/Craftliltplugin.php @@ -34,6 +34,7 @@ use lilthq\craftliltplugin\services\handlers\CreateTranslationsHandler; use lilthq\craftliltplugin\services\handlers\EditJobHandler; use lilthq\craftliltplugin\services\handlers\LoadI18NHandler; +use lilthq\craftliltplugin\services\handlers\PublishDraftAsyncHandler; use lilthq\craftliltplugin\services\handlers\PublishDraftHandler; use lilthq\craftliltplugin\services\handlers\RefreshJobStatusHandler; use lilthq\craftliltplugin\services\handlers\SendJobToLiltConnectorHandler; @@ -90,6 +91,7 @@ * @property SendTranslationToLiltConnectorHandler $sendTranslationToLiltConnectorHandler * @property SyncJobFromLiltConnectorHandler $syncJobFromLiltConnectorHandler * @property PublishDraftHandler $publishDraftsHandler + * @property PublishDraftAsyncHandler $publishDraftsHandlerAsync * @property Configuration $connectorConfiguration * @property JobsApi $connectorJobsApi * @property TranslationsApi $connectorTranslationsApi diff --git a/src/assets/resources/entry-edit.js b/src/assets/resources/entry-edit.js index 35e41d25..d97ad58f 100644 --- a/src/assets/resources/entry-edit.js +++ b/src/assets/resources/entry-edit.js @@ -13,6 +13,7 @@ CraftliltPlugin.EntryEditWarning = Garnish.Base.extend({ 'statuses[1]': ['in-progress'], 'statuses[2]': ['ready-for-review'], 'statuses[3]': ['ready-to-publish'], + 'statuses[4]': ['publishing'], }); const container = jQuery('
'). diff --git a/src/assets/resources/job-translation-review.js b/src/assets/resources/job-translation-review.js index 2f401acf..d6ecadc1 100644 --- a/src/assets/resources/job-translation-review.js +++ b/src/assets/resources/job-translation-review.js @@ -43,12 +43,14 @@ CraftliltPlugin.TranslationReview = Garnish.Base.extend({ const translationIsReviewed = translationRow.data('is-reviewed'); const translationIsPublished = translationRow.data('is-published'); const translationTitle = translationRow.data('title'); + const translationStatus = translationRow.data('status'); return { translationId, translationTitle, translationIsPublished, translationIsReviewed, + translationStatus, }; }, loadTranslationData: function(translationId) { @@ -114,7 +116,7 @@ CraftliltPlugin.TranslationReview = Garnish.Base.extend({ }); const { - translationIsReviewed, translationIsPublished, + translationIsReviewed, translationIsPublished, translationStatus } = this.getTranslationData(translationId); if (translationIsReviewed === 1) { @@ -128,6 +130,12 @@ CraftliltPlugin.TranslationReview = Garnish.Base.extend({ } else { this.$modalFooterButtonsPublish.removeClass('disabled'); } + + if (translationStatus === "publishing") { + this.$modalFooterButtonsPublish.addClass('disabled'); + } else { + this.$modalFooterButtonsPublish.removeClass('disabled'); + } }, showMultiModal: function(translationIds) { if (translationIds.length === 1) { @@ -599,6 +607,7 @@ $(document).ready(function() { const status = $(this).find('span.translation-status').data('status'); if (status === 'published' || status === 'failed' || status === 'new' || + status === 'publishing' || status === 'in-progress') { disabledIds.push($(this).data('id')); } else { diff --git a/src/controllers/PostConfigurationController.php b/src/controllers/PostConfigurationController.php index 53b3526d..746a5222 100644 --- a/src/controllers/PostConfigurationController.php +++ b/src/controllers/PostConfigurationController.php @@ -120,6 +120,17 @@ public function actionInvoke(): Response (string)$queueDisableAutomaticSync ); + // publishTranslationsAsync + $publishTranslationsAsync = $request->getBodyParam('publishTranslationsAsync'); + if (empty($publishTranslationsAsync)) { + $publishTranslationsAsync = 0; + } + + Craftliltplugin::getInstance()->settingsRepository->save( + SettingsRepository::PUBLISH_TRANSLATIONS_ASYNC, + (string)$publishTranslationsAsync + ); + $settingsRequest = new SettingsRequest(); $settingsRequest->setProjectPrefix( $request->getBodyParam('projectPrefix') diff --git a/src/controllers/translation/PostTranslationPublishController.php b/src/controllers/translation/PostTranslationPublishController.php index 2192371e..393670f8 100644 --- a/src/controllers/translation/PostTranslationPublishController.php +++ b/src/controllers/translation/PostTranslationPublishController.php @@ -10,11 +10,13 @@ namespace lilthq\craftliltplugin\controllers\translation; use Craft; -use craft\base\ElementInterface; use lilthq\craftliltplugin\controllers\job\AbstractJobController; use lilthq\craftliltplugin\Craftliltplugin; -use lilthq\craftliltplugin\elements\Translation; use lilthq\craftliltplugin\records\TranslationRecord; +use lilthq\craftliltplugin\services\handlers\commands\PublishDraftCommand; +use lilthq\craftliltplugin\services\handlers\PublishDraftAsyncHandler; +use lilthq\craftliltplugin\services\handlers\PublishDraftHandler; +use lilthq\craftliltplugin\services\repositories\SettingsRepository; use Throwable; use yii\web\Response; @@ -42,36 +44,41 @@ public function actionInvoke(): Response return (new Response())->setStatusCode(404); } + $publishHandler = $this->getPublishHandler(); + foreach ($translations as $translation) { - Craftliltplugin::getInstance()->publishDraftsHandler->__invoke( - $translation->translatedDraftId, - $translation->targetSiteId + $publishHandler->__invoke( + new PublishDraftCommand( + $translation->translatedDraftId, + $translation->targetSiteId, + $translation->jobId, + $translation->id + ) ); } - $updated = TranslationRecord::updateAll( - ['status' => TranslationRecord::STATUS_PUBLISHED], - ['id' => $translationIds] + Craftliltplugin::getInstance()->refreshJobStatusHandler->__invoke( + $translations[0]->jobId ); - if ($updated) { - foreach ($translations as $translation) { - Craftliltplugin::getInstance()->jobLogsRepository->create( - $translation->jobId, - Craft::$app->getUser()->getId(), - sprintf('Translation (id: %d) published', $translation->id) - ); - } + return $this->asJson([ + 'success' => true + ]); + } - Craftliltplugin::getInstance()->refreshJobStatusHandler->__invoke( - $translations[0]->jobId - ); + /** + * @return PublishDraftAsyncHandler|PublishDraftHandler + */ + private function getPublishHandler() + { + if ( + Craftliltplugin::getInstance() + ->settingsRepository + ->getBool(SettingsRepository::PUBLISH_TRANSLATIONS_ASYNC) + ) { + return Craftliltplugin::getInstance()->publishDraftsHandlerAsync; } - Craft::$app->elements->invalidateCachesForElementType(Translation::class); - - return $this->asJson([ - 'success' => $updated === 1 - ]); + return Craftliltplugin::getInstance()->publishDraftsHandler; } } diff --git a/src/elements/Job.php b/src/elements/Job.php index 540a694b..a2b124c6 100644 --- a/src/elements/Job.php +++ b/src/elements/Job.php @@ -37,6 +37,7 @@ class Job extends Element public const STATUS_NEW = 'new'; public const STATUS_DRAFT = 'draft'; public const STATUS_IN_PROGRESS = 'in-progress'; + public const STATUS_PUBLISHING = 'publishing'; public const STATUS_READY_FOR_REVIEW = 'ready-for-review'; public const STATUS_READY_TO_PUBLISH = 'ready-to-publish'; public const STATUS_COMPLETE = 'complete'; @@ -228,6 +229,7 @@ public static function statuses(): array self::STATUS_NEW => ['label' => 'New', 'color' => 'orange'], self::STATUS_DRAFT => ['label' => 'Draft', 'color' => ''], self::STATUS_IN_PROGRESS => ['label' => 'In Progress', 'color' => 'blue'], + self::STATUS_PUBLISHING => ['label' => 'Publishing', 'color' => 'blue'], self::STATUS_READY_FOR_REVIEW => ['label' => 'Ready for review', 'color' => 'yellow'], self::STATUS_READY_TO_PUBLISH => ['label' => 'Ready to publish', 'color' => 'purple'], self::STATUS_COMPLETE => ['label' => 'Complete', 'color' => 'green'], @@ -296,6 +298,16 @@ protected static function defineSources(string $context = null): array ], 'defaultSort' => ['dateCreated', 'desc'] ], + [ + 'key' => 'publishing', + 'label' => 'Publishing', + 'criteria' => [ + 'status' => [ + self::STATUS_PUBLISHING + ] + ], + 'defaultSort' => ['dateCreated', 'desc'] + ], [ 'key' => 'ready-for-review', 'label' => 'Ready for review', diff --git a/src/elements/Translation.php b/src/elements/Translation.php index 4451c105..5b870ba7 100644 --- a/src/elements/Translation.php +++ b/src/elements/Translation.php @@ -104,6 +104,7 @@ public static function statuses(): array { return [ TranslationRecord::STATUS_IN_PROGRESS => ['label' => 'In Progress', 'color' => 'blue'], + TranslationRecord::STATUS_PUBLISHING => ['label' => 'Publishing', 'color' => 'blue'], TranslationRecord::STATUS_READY_FOR_REVIEW => ['label' => 'Ready for review', 'color' => 'yellow'], TranslationRecord::STATUS_READY_TO_PUBLISH => ['label' => 'Ready to publish', 'color' => 'purple'], TranslationRecord::STATUS_PUBLISHED => ['label' => 'Published', 'color' => 'green'], diff --git a/src/modules/FetchInstantJobTranslationsFromConnector.php b/src/modules/FetchInstantJobTranslationsFromConnector.php index 82331967..5dafd266 100644 --- a/src/modules/FetchInstantJobTranslationsFromConnector.php +++ b/src/modules/FetchInstantJobTranslationsFromConnector.php @@ -111,4 +111,14 @@ public function canRetry($attempt, $error): bool { return $attempt < self::RETRY_COUNT; } + + public static function getDelay(): int + { + $envDelay = getenv('CRAFT_LILT_PLUGIN_QUEUE_DELAY_IN_SECONDS'); + if (!empty($envDelay) || $envDelay === '0') { + return (int)$envDelay; + } + + return self::DELAY_IN_SECONDS; + } } diff --git a/src/modules/FetchVerifiedJobTranslationsFromConnector.php b/src/modules/FetchVerifiedJobTranslationsFromConnector.php index a96430be..335c539f 100644 --- a/src/modules/FetchVerifiedJobTranslationsFromConnector.php +++ b/src/modules/FetchVerifiedJobTranslationsFromConnector.php @@ -183,6 +183,16 @@ function (TranslationResponse $translationResponse) use ($job, $unprocessedTrans $mutex->release($mutexKey); } + public static function getDelay(): int + { + $envDelay = getenv('CRAFT_LILT_PLUGIN_QUEUE_DELAY_IN_SECONDS'); + if (!empty($envDelay) || $envDelay === '0') { + return (int)$envDelay; + } + + return self::DELAY_IN_SECONDS; + } + /** * @inheritdoc */ diff --git a/src/modules/PublishTranslation.php b/src/modules/PublishTranslation.php new file mode 100644 index 00000000..f8f20606 --- /dev/null +++ b/src/modules/PublishTranslation.php @@ -0,0 +1,148 @@ +getCommand(); + if (empty($command)) { + return; + } + + Craftliltplugin::getInstance()->publishDraftsHandler->__invoke( + $this->getPublishDraftCommand() + ); + + Craftliltplugin::getInstance()->refreshJobStatusHandler->__invoke( + $this->jobId + ); + + $this->markAsDone($queue); + $this->release(); + } + + private function getPublishDraftCommand(): PublishDraftCommand + { + return new PublishDraftCommand( + $this->draftId, + $this->targetSiteId, + $this->jobId, + $this->translationId + ); + } + + /** + * @inheritdoc + */ + protected function defaultDescription(): ?string + { + return Craft::t( + 'app', + sprintf( + 'Publish translation draft: %d', + $this->translationId + ) + ); + } + + /** + * @param $queue + * @return void + */ + private function markAsDone($queue): void + { + $this->setProgress( + $queue, + 1, + Craft::t( + 'app', + 'Sending translation for jobId: {jobId} to lilt platform done', + [ + 'jobId' => $this->jobId, + ] + ) + ); + } + + public function canRetry(): bool + { + return $this->attempt < self::RETRY_COUNT; + } + + public function getRetryJob(): BaseJob + { + return new self([ + 'jobId' => $this->jobId, + 'translationId' => $this->translationId, + 'draftId' => $this->draftId, + 'targetSiteId' => $this->targetSiteId, + 'attempt' => $this->attempt + 1 + ]); + } + + protected function getMutexKey(): string + { + return join('_', [ + __CLASS__, + __FUNCTION__, + $this->jobId, + $this->translationId, + $this->targetSiteId, + $this->draftId, + $this->attempt + ]); + } + + public static function getDelay(): int + { + $envDelay = getenv('CRAFT_LILT_PLUGIN_QUEUE_DELAY_IN_SECONDS'); + if (!empty($envDelay) || $envDelay === '0') { + return (int)$envDelay; + } + + return self::DELAY_IN_SECONDS; + } +} diff --git a/src/records/TranslationRecord.php b/src/records/TranslationRecord.php index dd0517b4..fc65986a 100644 --- a/src/records/TranslationRecord.php +++ b/src/records/TranslationRecord.php @@ -33,6 +33,7 @@ class TranslationRecord extends ActiveRecord public const STATUS_READY_FOR_REVIEW = 'ready-for-review'; public const STATUS_READY_TO_PUBLISH = 'ready-to-publish'; public const STATUS_IN_PROGRESS = 'in-progress'; + public const STATUS_PUBLISHING = 'publishing'; public const STATUS_PUBLISHED = 'published'; public const STATUS_FAILED = 'failed'; public const STATUS_NEEDS_ATTENTION = 'needs-attention'; diff --git a/src/services/ServiceInitializer.php b/src/services/ServiceInitializer.php index 8206d499..610bdac3 100644 --- a/src/services/ServiceInitializer.php +++ b/src/services/ServiceInitializer.php @@ -5,7 +5,6 @@ namespace lilthq\craftliltplugin\services; use Craft; -use fruitstudios\linkit\fields\LinkitField; use GuzzleHttp\Client; use LiltConnectorSDK\Api\JobsApi; use LiltConnectorSDK\Api\SettingsApi; @@ -34,6 +33,7 @@ use lilthq\craftliltplugin\services\handlers\field\copier\SuperTableFieldCopier; use lilthq\craftliltplugin\services\handlers\field\CopyFieldsHandler; use lilthq\craftliltplugin\services\handlers\LoadI18NHandler; +use lilthq\craftliltplugin\services\handlers\PublishDraftAsyncHandler; use lilthq\craftliltplugin\services\handlers\PublishDraftHandler; use lilthq\craftliltplugin\services\handlers\RefreshJobStatusHandler; use lilthq\craftliltplugin\services\handlers\SendJobToLiltConnectorHandler; @@ -270,6 +270,11 @@ function () { 'class' => PublishDraftHandler::class, 'draftRepository' => Craft::$app->getDrafts(), ], + 'publishDraftsHandlerAsync' => + [ + 'class' => PublishDraftAsyncHandler::class, + 'translationRepository' => $pluginInstance->translationRepository + ], 'connectorTranslationRepository' => [ 'class' => ConnectorTranslationRepository::class, diff --git a/src/services/handlers/CreateDraftHandler.php b/src/services/handlers/CreateDraftHandler.php index 45ee93b6..7e038388 100644 --- a/src/services/handlers/CreateDraftHandler.php +++ b/src/services/handlers/CreateDraftHandler.php @@ -18,9 +18,9 @@ use lilthq\craftliltplugin\Craftliltplugin; use lilthq\craftliltplugin\datetime\DateTime; use lilthq\craftliltplugin\parameters\CraftliltpluginParameters; -use lilthq\craftliltplugin\records\SettingRecord; use lilthq\craftliltplugin\services\handlers\commands\CreateDraftCommand; use lilthq\craftliltplugin\services\handlers\field\CopyFieldsHandler; +use lilthq\craftliltplugin\services\repositories\SettingsRepository; use Throwable; use yii\base\Exception; @@ -95,10 +95,9 @@ public function create( ); } - $copyEntriesSlugFromSourceToTarget = SettingRecord::findOne( - ['name' => 'copy_entries_slug_from_source_to_target'] - ); - $isCopySlugEnabled = (bool)($copyEntriesSlugFromSourceToTarget->value ?? false); + $isCopySlugEnabled = Craftliltplugin::getInstance() + ->settingsRepository + ->getBool(SettingsRepository::COPY_ENTRIES_SLUG_FROM_SOURCE_TO_TARGET); if ($isCopySlugEnabled) { $draft->slug = $element->slug; diff --git a/src/services/handlers/PublishDraftAsyncHandler.php b/src/services/handlers/PublishDraftAsyncHandler.php new file mode 100644 index 00000000..d66ba4fa --- /dev/null +++ b/src/services/handlers/PublishDraftAsyncHandler.php @@ -0,0 +1,50 @@ + $command->getJobId(), + 'translationId' => $command->getTranslationId(), + 'targetSiteId' => $command->getTargetSiteId(), + 'draftId' => $command->getDraftId(), + ] + )), + PublishTranslation::PRIORITY, + PublishTranslation::DELAY_IN_SECONDS + ); + + + $this->translationRepository->updateTranslationStatusById( + $command->getTranslationId(), + TranslationRecord::STATUS_PUBLISHING + ); + + Craft::$app->getElements()->invalidateCachesForElementType(Translation::class); + } +} diff --git a/src/services/handlers/PublishDraftHandler.php b/src/services/handlers/PublishDraftHandler.php index 0557fd7d..a662a853 100644 --- a/src/services/handlers/PublishDraftHandler.php +++ b/src/services/handlers/PublishDraftHandler.php @@ -13,9 +13,12 @@ use craft\base\ElementInterface; use craft\errors\InvalidElementException; use craft\services\Drafts as DraftRepository; +use lilthq\craftliltplugin\Craftliltplugin; +use lilthq\craftliltplugin\elements\Translation; use lilthq\craftliltplugin\parameters\CraftliltpluginParameters; use lilthq\craftliltplugin\records\SettingRecord; use lilthq\craftliltplugin\records\TranslationRecord; +use lilthq\craftliltplugin\services\handlers\commands\PublishDraftCommand; use Throwable; use yii\base\Exception; @@ -29,12 +32,12 @@ class PublishDraftHandler /** * @throws Throwable */ - public function __invoke(int $draftId, int $targetSiteId): void + public function __invoke(PublishDraftCommand $command): void { $draftElement = Craft::$app->elements->getElementById( - $draftId, + $command->getDraftId(), null, - $targetSiteId + $command->getTargetSiteId() ); if (!$draftElement) { @@ -46,7 +49,7 @@ public function __invoke(int $draftId, int $targetSiteId): void class_exists('verbb\supertable\SuperTable') || class_exists('benf\neo\Plugin') ) { - $translation = TranslationRecord::findOne(['translatedDraftId' => $draftId]); + $translation = TranslationRecord::findOne(['translatedDraftId' => $command->getDraftId()]); $translations = TranslationRecord::findAll( [ 'jobId' => $translation->jobId, @@ -56,7 +59,7 @@ class_exists('verbb\supertable\SuperTable') foreach ($translations as $translation) { $draftElementLanguageToUpdate = Craft::$app->elements->getElementById( - $draftId, + $command->getDraftId(), null, $translation->targetSiteId ); @@ -98,7 +101,7 @@ class_exists('verbb\supertable\SuperTable') $neoPluginInstance = call_user_func(['benf\neo\Plugin', 'getInstance']); // Get the Neo plugin Fields service - /** @var \benf\neo\services\Fields $neoPluginFieldsService */ + /** @var \benf\neo\services\Fields $neoPluginFieldsService */ $neoPluginFieldsService = $neoPluginInstance->get('fields'); // Clear current neo field value @@ -126,12 +129,28 @@ class_exists('verbb\supertable\SuperTable') ?? false); $element = $this->apply($draftElement); - if ($enableEntriesForTargetSites && !$draftElement->getEnabledForSite($targetSiteId)) { - $element->setEnabledForSite([$targetSiteId => true]); + if ($enableEntriesForTargetSites && !$draftElement->getEnabledForSite($command->getTargetSiteId())) { + $element->setEnabledForSite([$command->getTargetSiteId() => true]); } Craft::$app->getElements()->saveElement($element, true, false, false); Craft::$app->getElements()->invalidateCachesForElement($element); + + // finish publishing + $updated = TranslationRecord::updateAll( + ['status' => TranslationRecord::STATUS_PUBLISHED], + ['id' => $command->getTranslationId()] + ); + + Craft::$app->getElements()->invalidateCachesForElementType(Translation::class); + + if ($updated) { + Craftliltplugin::getInstance()->jobLogsRepository->create( + $command->getJobId(), + Craft::$app->getUser()->getId(), + sprintf('Translation (id: %d) published', $command->getTranslationId()) + ); + } } // copied from \craft\controllers\EntryRevisionsController::actionPublishDraft diff --git a/src/services/handlers/PublishDraftHandlerInterface.php b/src/services/handlers/PublishDraftHandlerInterface.php new file mode 100644 index 00000000..fd57708d --- /dev/null +++ b/src/services/handlers/PublishDraftHandlerInterface.php @@ -0,0 +1,12 @@ + $jobId]); if (!$jobRecord) { - return; + return false; } $translations = Craftliltplugin::getInstance()->translationRepository->findByJobId($jobId); @@ -34,7 +34,7 @@ public function __invoke(int $jobId): void }, $translations) ); - if ($uniqueStatuses === [TranslationRecord::STATUS_PUBLISHED]) { + if ($uniqueStatuses === [TranslationRecord::STATUS_PUBLISHED] && $jobRecord->status != Job::STATUS_COMPLETE) { $jobRecord->status = Job::STATUS_COMPLETE; $jobRecord->save(); @@ -43,9 +43,32 @@ public function __invoke(int $jobId): void Craft::$app->getUser()->getId(), 'Job published' ); + + Craft::$app->elements->invalidateCachesForElementType( + Job::class + ); + + return true; + } + + if ( + $uniqueStatuses === [TranslationRecord::STATUS_PUBLISHING] + && $jobRecord->status != Job::STATUS_PUBLISHING + ) { + $jobRecord->status = Job::STATUS_PUBLISHING; + $jobRecord->save(); + + Craft::$app->elements->invalidateCachesForElementType( + Job::class + ); + + return true; } - if ($uniqueStatuses === [TranslationRecord::STATUS_READY_TO_PUBLISH]) { + if ( + $uniqueStatuses === [TranslationRecord::STATUS_READY_TO_PUBLISH] + && $jobRecord->status != Job::STATUS_READY_TO_PUBLISH + ) { $jobRecord->status = Job::STATUS_READY_TO_PUBLISH; $jobRecord->save(); @@ -54,10 +77,15 @@ public function __invoke(int $jobId): void Craft::$app->getUser()->getId(), 'Job reviewed' ); + + Craft::$app->elements->invalidateCachesForElementType( + Job::class + ); + + return true; } - Craft::$app->elements->invalidateCachesForElementType( - Job::class - ); + + return false; } } diff --git a/src/services/handlers/commands/PublishDraftCommand.php b/src/services/handlers/commands/PublishDraftCommand.php new file mode 100644 index 00000000..0264baed --- /dev/null +++ b/src/services/handlers/commands/PublishDraftCommand.php @@ -0,0 +1,65 @@ +draftId = $draftId; + $this->targetSiteId = $targetSiteId; + $this->jobId = $jobId; + $this->translationId = $translationId; + } + + public function getDraftId(): int + { + return $this->draftId; + } + + public function getTargetSiteId(): int + { + return $this->targetSiteId; + } + + public function getJobId(): int + { + return $this->jobId; + } + + public function getTranslationId(): int + { + return $this->translationId; + } +} diff --git a/src/services/listeners/AfterErrorListener.php b/src/services/listeners/AfterErrorListener.php index ed68a432..ff3c0235 100644 --- a/src/services/listeners/AfterErrorListener.php +++ b/src/services/listeners/AfterErrorListener.php @@ -19,6 +19,7 @@ use lilthq\craftliltplugin\modules\FetchJobStatusFromConnector; use lilthq\craftliltplugin\modules\FetchTranslationFromConnector; use lilthq\craftliltplugin\modules\FetchVerifiedJobTranslationsFromConnector; +use lilthq\craftliltplugin\modules\PublishTranslation; use lilthq\craftliltplugin\modules\SendJobToConnector; use lilthq\craftliltplugin\modules\SendTranslationToConnector; use lilthq\craftliltplugin\records\JobRecord; @@ -35,6 +36,7 @@ class AfterErrorListener implements ListenerInterface FetchTranslationFromConnector::class, SendJobToConnector::class, SendTranslationToConnector::class, + PublishTranslation::class, ]; public function register(): void diff --git a/src/services/repositories/JobRepository.php b/src/services/repositories/JobRepository.php index f634ec60..374c794f 100644 --- a/src/services/repositories/JobRepository.php +++ b/src/services/repositories/JobRepository.php @@ -27,6 +27,15 @@ public function findByIds(array $ids): array return Job::findAll(['id' => $ids]); } + public function updateJobStatusById(int $id, string $status): bool + { + return JobRecord::updateAll([ + "status" => $status, + ], [ + "id" => $id + ]) > 0; + } + public function saveJob(Job $job): bool { $jobRecord = new JobRecord(); diff --git a/src/services/repositories/SettingsRepository.php b/src/services/repositories/SettingsRepository.php index cb49ffa7..2ff36895 100644 --- a/src/services/repositories/SettingsRepository.php +++ b/src/services/repositories/SettingsRepository.php @@ -20,6 +20,7 @@ class SettingsRepository public const QUEUE_EACH_TRANSLATION_FILE_SEPARATELY = 'queue_each_translation_file_separately'; public const QUEUE_DISABLE_AUTOMATIC_SYNC = 'queue_disable_automatic_sync'; public const QUEUE_MANAGER_EXECUTED_AT = 'queue_manager_executed_at'; + public const PUBLISH_TRANSLATIONS_ASYNC = 'publish_translations_async'; public const IGNORE_DROPDOWNS = 'ignore_dropdowns'; @@ -56,6 +57,19 @@ public function isQueueEachTranslationFileSeparately(): bool return (bool)$settingValue->value; } + public function getBool(string $name): bool + { + $tableSchema = Craft::$app->getDb()->schema->getTableSchema(CraftliltpluginParameters::SETTINGS_TABLE_NAME); + if ($tableSchema === null) { + return false; + } + + $queueDisableAutomaticSync = SettingRecord::findOne( + ['name' => $name] + ); + return (bool) ($queueDisableAutomaticSync->value ?? false); + } + public function get(string $name): ?string { $tableSchema = Craft::$app->getDb()->schema->getTableSchema(CraftliltpluginParameters::SETTINGS_TABLE_NAME); diff --git a/src/services/repositories/TranslationRepository.php b/src/services/repositories/TranslationRepository.php index 7e5dcc61..f8347036 100644 --- a/src/services/repositories/TranslationRepository.php +++ b/src/services/repositories/TranslationRepository.php @@ -203,4 +203,13 @@ public function findOneById(int $id): ?TranslationModel $translationRecord->toArray() ); } + + public function updateTranslationStatusById(int $id, string $status): bool + { + return TranslationRecord::updateAll([ + "status" => $status, + ], [ + "id" => $id + ]) > 0; + } } diff --git a/src/templates/_components/translation/_elements.twig b/src/templates/_components/translation/_elements.twig index c2fc79c0..2560ae68 100644 --- a/src/templates/_components/translation/_elements.twig +++ b/src/templates/_components/translation/_elements.twig @@ -1,6 +1,7 @@ {% import '_includes/forms' as forms %} {% set isJobInProgress = (constant('STATUS_IN_PROGRESS', element) is same as element.getStatus()) %} +{% set isJobPublishing = (constant('STATUS_PUBLISHING', element) is same as element.getStatus()) %}