diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 07e472b0..9b878b56 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -7,7 +7,7 @@ on: branches: - "*" jobs: - e2e: + e2e-instant: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 @@ -15,7 +15,25 @@ jobs: working-directory: ./e2e run: | make up - make test + make test-instant + e2e-verified: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Run automation + working-directory: ./e2e + run: | + make up + make test-verified + e2e-copy-source-text: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Run automation + working-directory: ./e2e + run: | + make up + make test-copy-source-text tests-php-72: runs-on: ubuntu-latest steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index c22139eb..57c3805b 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## 3.4.3 - 2023-02-24 +### Fixed +- Multilingual draft publishing issue +- Failed jobs aren't able to be deleted ([github issue](https://github.com/lilt/craft-lilt-plugin/issues/90)) + ## 3.4.2 - 2023-02-21 ### Changed - Retry logic for queues diff --git a/composer.json b/composer.json index a79d8ae2..f9a55ee6 100755 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "lilt/craft-lilt-plugin", "description": "The Lilt plugin makes it easy for you to send content to Lilt for translation right from within Craft CMS.", "type": "craft-plugin", - "version": "3.4.2", + "version": "3.4.3", "keywords": [ "craft", "cms", diff --git a/e2e/Makefile b/e2e/Makefile index 90687fae..7967bf45 100644 --- a/e2e/Makefile +++ b/e2e/Makefile @@ -43,3 +43,27 @@ test: --env-file .env.test \ --entrypoint=cypress \ cypress/included:12.2.0 run --headless --config-file /e2e/cypress.config.js --record false + +test-instant: + docker run -u root -t -v ${PWD}:/e2e -w /e2e --env CYPRESS_CACHE_FOLDER=${CYPRESS_CACHE_FOLDER} node:18.12.1 npm install + docker run -u root -t -v ${PWD}:/e2e -w /e2e --env CYPRESS_CACHE_FOLDER=${CYPRESS_CACHE_FOLDER} \ + --network e2e-network \ + --env-file .env.test \ + --entrypoint=cypress \ + cypress/included:12.2.0 run --headless --config-file /e2e/cypress.config.js --spec cypress/e2e/jobs/instant/*.js --record false + +test-verified: + docker run -u root -t -v ${PWD}:/e2e -w /e2e --env CYPRESS_CACHE_FOLDER=${CYPRESS_CACHE_FOLDER} node:18.12.1 npm install + docker run -u root -t -v ${PWD}:/e2e -w /e2e --env CYPRESS_CACHE_FOLDER=${CYPRESS_CACHE_FOLDER} \ + --network e2e-network \ + --env-file .env.test \ + --entrypoint=cypress \ + cypress/included:12.2.0 run --headless --config-file /e2e/cypress.config.js --spec cypress/e2e/jobs/verified/*.js --record false + +test-copy-source-text: + docker run -u root -t -v ${PWD}:/e2e -w /e2e --env CYPRESS_CACHE_FOLDER=${CYPRESS_CACHE_FOLDER} node:18.12.1 npm install + docker run -u root -t -v ${PWD}:/e2e -w /e2e --env CYPRESS_CACHE_FOLDER=${CYPRESS_CACHE_FOLDER} \ + --network e2e-network \ + --env-file .env.test \ + --entrypoint=cypress \ + cypress/included:12.2.0 run --headless --config-file /e2e/cypress.config.js --spec cypress/e2e/jobs/copy-source-text-flow/*.js --record false diff --git a/e2e/cypress/e2e/jobs/verified/success-path-multiple-bulk-publishing.cy.js b/e2e/cypress/e2e/jobs/verified/success-path-multiple-bulk-publishing.cy.js new file mode 100644 index 00000000..120f613d --- /dev/null +++ b/e2e/cypress/e2e/jobs/verified/success-path-multiple-bulk-publishing.cy.js @@ -0,0 +1,71 @@ +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('with copy slug disabled & enable after publish disabled', () => { + const {jobTitle, slug} = generateJobData(); + + cy.verifiedFlow({ + slug, + entryLabel, + jobTitle, + entryId, + copySlug: false, + enableAfterPublish: false, + languages: ['de', 'es', 'uk'], + batchPublishing: true, + }); + }); + + it('with copy slug disabled & enable after publish enabled', () => { + const {jobTitle, slug} = generateJobData(); + + cy.verifiedFlow({ + slug, + entryLabel, + jobTitle, + entryId, + copySlug: false, + enableAfterPublish: true, + languages: ['de', 'es', 'uk'], + batchPublishing: true, + }); + }); + + it('with copy slug enabled & enable after publish disabled', () => { + const {jobTitle, slug} = generateJobData(); + + cy.verifiedFlow({ + slug, + entryLabel, + jobTitle, + entryId, + copySlug: true, + enableAfterPublish: false, + languages: ['de', 'es', 'uk'], + batchPublishing: true, + }); + }); + + it('with copy slug enabled & enable after publish enabled', () => { + const {jobTitle, slug} = generateJobData(); + + cy.log(`Job title: ${jobTitle}`); + cy.log(`Slug: ${slug}`); + + cy.verifiedFlow({ + slug, + entryLabel, + jobTitle, + entryId, + copySlug: true, + enableAfterPublish: true, + languages: ['de', 'es', 'uk'], + batchPublishing: true, + }); + }); + }); diff --git a/e2e/cypress/e2e/jobs/verified/success-path-multiple-single-publishing.cy.js b/e2e/cypress/e2e/jobs/verified/success-path-multiple-single-publishing.cy.js new file mode 100644 index 00000000..c482fdd4 --- /dev/null +++ b/e2e/cypress/e2e/jobs/verified/success-path-multiple-single-publishing.cy.js @@ -0,0 +1,71 @@ +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('with copy slug disabled & enable after publish disabled', () => { + const {jobTitle, slug} = generateJobData(); + + cy.verifiedFlow({ + slug, + entryLabel, + jobTitle, + entryId, + copySlug: false, + enableAfterPublish: false, + languages: ['de', 'es', 'uk'], + batchPublishing: false, + }); + }); + + it('with copy slug disabled & enable after publish enabled', () => { + const {jobTitle, slug} = generateJobData(); + + cy.verifiedFlow({ + slug, + entryLabel, + jobTitle, + entryId, + copySlug: false, + enableAfterPublish: true, + languages: ['de', 'es', 'uk'], + batchPublishing: false, + }); + }); + + it('with copy slug enabled & enable after publish disabled', () => { + const {jobTitle, slug} = generateJobData(); + + cy.verifiedFlow({ + slug, + entryLabel, + jobTitle, + entryId, + copySlug: true, + enableAfterPublish: false, + languages: ['de', 'es', 'uk'], + batchPublishing: false, + }); + }); + + it('with copy slug enabled & enable after publish enabled', () => { + const {jobTitle, slug} = generateJobData(); + + cy.log(`Job title: ${jobTitle}`); + cy.log(`Slug: ${slug}`); + + cy.verifiedFlow({ + slug, + entryLabel, + jobTitle, + entryId, + copySlug: true, + enableAfterPublish: true, + languages: ['de', 'es', 'uk'], + batchPublishing: false, + }); + }); + }); diff --git a/e2e/cypress/e2e/jobs/verified/success-path-single.cy.js b/e2e/cypress/e2e/jobs/verified/success-path-single.cy.js new file mode 100644 index 00000000..d1d66d9c --- /dev/null +++ b/e2e/cypress/e2e/jobs/verified/success-path-single.cy.js @@ -0,0 +1,67 @@ +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('with copy slug disabled & enable after publish disabled', () => { + const {jobTitle, slug} = generateJobData(); + + cy.verifiedFlow({ + slug, + entryLabel, + jobTitle, + entryId, + copySlug: false, + enableAfterPublish: false, + languages: ["de"], + }) + }); + + it('with copy slug disabled & enable after publish enabled', () => { + const {jobTitle, slug} = generateJobData(); + + cy.verifiedFlow({ + slug, + entryLabel, + jobTitle, + entryId, + copySlug: false, + enableAfterPublish: true, + languages: ["de"] + }) + }); + + it('with copy slug enabled & enable after publish disabled', () => { + const {jobTitle, slug} = generateJobData(); + + cy.verifiedFlow({ + slug, + entryLabel, + jobTitle, + entryId, + copySlug: true, + enableAfterPublish: false, + languages: ["de"] + }) + }); + + it('with copy slug enabled & enable after publish enabled', () => { + const {jobTitle, slug} = generateJobData(); + + cy.log(`Job title: ${jobTitle}`) + cy.log(`Slug: ${slug}`) + + cy.verifiedFlow({ + slug, + entryLabel, + jobTitle, + entryId, + copySlug: true, + enableAfterPublish: true, + languages: ["de"] + }) + }); + }); diff --git a/e2e/cypress/support/commands.js b/e2e/cypress/support/commands.js index 3c56e2e5..862d2a5a 100644 --- a/e2e/cypress/support/commands.js +++ b/e2e/cypress/support/commands.js @@ -362,8 +362,10 @@ Cypress.Commands.add('publishTranslation', (jobTitle, language) => { cy.get('#translations-publish-action').click(); }); - cy.wait(10000); //delay for publishing - cy.waitForJobStatus('complete'); + cy. + get('#notifications .notification.notice'). + invoke('text'). + should('contain', 'Translation(s) published'); }); /** @@ -391,7 +393,11 @@ Cypress.Commands.add('publishTranslations', (jobTitle, languages) => { cy.get('#translations-publish-action').click(); - cy.wait(10000); //delay for publishing + cy. + get('#notifications .notification.notice'). + invoke('text'). + should('contain', 'Translation(s) published'); + cy.waitForJobStatus('complete'); }); @@ -413,7 +419,11 @@ Cypress.Commands.add('publishJob', cy.assertDraftSlugValue(copySlug, slug, language); cy.publishTranslation(jobTitle, language); + } + cy.waitForJobStatus('complete'); + + for (const language of languages) { cy.assertAfterPublish(copySlug, slug, entryId, language, enableAfterPublish); } diff --git a/e2e/cypress/support/e2e.js b/e2e/cypress/support/e2e.js index b05ebdd7..cb36ac91 100644 --- a/e2e/cypress/support/e2e.js +++ b/e2e/cypress/support/e2e.js @@ -1,5 +1,6 @@ import './commands' import './flow/instant' +import './flow/verified' beforeEach(() => { cy.wrap(Cypress.session.clearAllSavedSessions()) diff --git a/e2e/cypress/support/flow/verified.js b/e2e/cypress/support/flow/verified.js new file mode 100644 index 00000000..625bb780 --- /dev/null +++ b/e2e/cypress/support/flow/verified.js @@ -0,0 +1,297 @@ +import {siteLanguages} from '../parameters'; +import mockServer from 'mockserver-client'; + +/** + * @memberof cy + * @method verifiedFlow + * @param {object} options + * @returns undefined + */ +Cypress.Commands.add('verifiedFlow', ({ + slug, + entryLabel, + jobTitle, + copySlug = false, + enableAfterPublish = false, + languages = ['de'], + batchPublishing = false, //publish all translations at once with publish button + entryId = 24, +}) => { + const isMockserverEnabled = Cypress.env('MOCKSERVER_ENABLED'); + + cy.releaseQueueManager(); + cy.resetEntryTitle(entryId, entryLabel); + + cy.setConfigurationOption('enableEntries', enableAfterPublish); + cy.setConfigurationOption('copySlug', copySlug); + + if (copySlug) { + // update slug on entry and enable slug copy option + cy.setEntrySlug(slug, entryId); + } + + cy.disableEntry(slug, entryId); + + // create job + cy.createJob(jobTitle, 'verified', languages); + + // send job for translation + cy.get('#lilt-btn-create-new-job').click(); + cy.get('#translations-list').should('be.visible'); + + let mockServerClient = mockServer.mockServerClient( + Cypress.env('MOCKSERVER_HOST'), Cypress.env('MOCKSERVER_PORT')); + + if (isMockserverEnabled) { + cy.wrap(mockServerClient.reset()); + + cy.wrap(mockServerClient.mockAnyResponse({ + 'httpRequest': { + 'method': 'GET', 'path': '/settings', + }, 'httpResponse': { + 'statusCode': 200, 'body': JSON.stringify({ + 'project_prefix': 'Project Prefix From Response', + 'project_name_template': 'Project Name Template From Response', + 'lilt_translation_workflow': 'INSTANT', + }), + }, 'times': { + 'unlimited': true, + }, + })); + + cy.wrap(mockServerClient.mockAnyResponse({ + 'httpRequest': { + 'method': 'POST', 'path': '/jobs', 'headers': [ + { + 'name': 'Authorization', 'values': ['Bearer this_is_apy_key'], + }], 'body': { + 'project_prefix': jobTitle, 'lilt_translation_workflow': 'VERIFIED', + }, + }, 'httpResponse': { + 'statusCode': 200, 'body': JSON.stringify({ + 'id': 777, + 'status': 'draft', + 'errorMsg': '', + 'createdAt': '2019-08-24T14:15:22Z', + 'updatedAt': '2019-08-24T14:15:22Z', + }), + }, 'times': { + 'remainingTimes': 1, 'unlimited': false, + }, + })); + + cy.wrap(mockServerClient.mockAnyResponse({ + 'httpRequest': { + 'method': 'POST', 'path': '/jobs/777/start', 'headers': [ + { + 'name': 'Authorization', 'values': ['Bearer this_is_apy_key'], + }], + }, 'httpResponse': { + 'statusCode': 200, + }, 'times': { + 'remainingTimes': 1, 'unlimited': false, + }, + })); + + for (const language of languages) { + cy.wrap(mockServerClient.mockAnyResponse({ + 'httpRequest': { + 'method': 'POST', 'path': '/jobs/777/files', 'queryStringParameters': [ + { + 'name': 'trglang', 'values': [language], + }, { + 'name': 'srclang', 'values': ['en'], + }, { + 'name': 'name', 'values': ['element_[\\d]+_.*\\.json\\+html'], + }, { + 'name': 'due', 'values': [''], + }], 'headers': [ + { + 'name': 'Authorization', 'values': ['Bearer this_is_apy_key'], + }], // TODO: expectation request body + // 'body': translationBody, + }, 'httpResponse': { + 'statusCode': 200, + }, 'times': { + 'remainingTimes': 1, 'unlimited': false, + }, + })); + } + } + + cy. + get('#status-value'). + invoke('text'). + should('contain', 'In Progress'); + + cy.waitForTranslationDrafts(); + + if (isMockserverEnabled) { + cy.wrap(mockServerClient.mockAnyResponse({ + 'httpRequest': { + 'method': 'GET', 'path': '/jobs/777', 'headers': [ + { + 'name': 'Authorization', 'values': ['Bearer this_is_apy_key'], + }], + }, 'httpResponse': { + 'statusCode': 200, 'body': JSON.stringify({ + 'id': 777, + 'status': 'complete', + 'errorMsg': '', + 'createdAt': '2019-08-24T14:15:22Z', + 'updatedAt': '2019-08-24T14:15:22Z', + }), + }, 'times': { + 'remainingTimes': 1, 'unlimited': false, + }, + })); + + let translationsResult = []; + + for (const language of languages) { + const siteId = siteLanguages[language]; + const translationId = 777000 + siteId; + + const translationResult = { + 'createdAt': '2022-05-29T11:31:58', + 'errorMsg': null, + 'id': translationId, + 'name': '777_element_' + 24 + '_slug_for_' + language + '.json+html', + 'status': 'export_complete', + 'trgLang': language, + 'trgLocale': '', + 'updatedAt': '2022-06-02T23:01:42', + }; + translationsResult.push(translationResult); + + cy.wrap(mockServerClient.mockAnyResponse({ + 'httpRequest': { + 'method': 'GET', + 'path': `/translations/${translationId}`, + 'headers': [ + { + 'name': 'Authorization', + 'values': ['Bearer this_is_apy_key'], + }], + }, 'httpResponse': { + 'statusCode': 200, 'body': JSON.stringify(translationResult), + }, 'times': { + 'remainingTimes': 1, 'unlimited': false, + }, + })); + } + + cy.wrap(mockServerClient.mockAnyResponse({ + 'httpRequest': { + 'method': 'GET', 'path': '/translations', 'headers': [ + { + 'name': 'Authorization', 'values': ['Bearer this_is_apy_key'], + }], 'queryStringParameters': [ + { + 'name': 'limit', 'values': ['100'], + }, { + 'name': 'start', 'values': ['00'], + }, { + 'name': 'job_id', 'values': ['777'], + }], + }, 'httpResponse': { + 'statusCode': 200, 'body': JSON.stringify({ + 'limit': 100, 'start': 0, 'results': translationsResult, + }), + }, 'times': { + 'remainingTimes': 1, 'unlimited': false, + }, + })); + + for (const language of languages) { + cy.get( + `#translations-list th[data-title="Title"] div.element[data-target-site-language="${language}"]`). + invoke('attr', 'data-translated-draft-id'). + then(async translatedDraftId => { + const siteId = siteLanguages[language]; + const translationId = 777000 + siteId; + + let expectedSourceContent = {}; + expectedSourceContent[translatedDraftId] = {'title': `Translated ${language}: The Future of Augmented Reality`}; + + + cy.wrap(mockServerClient.mockAnyResponse({ + 'httpRequest': { + 'method': 'GET', + 'path': `/translations/${translationId}/download`, + 'headers': [ + { + 'name': 'Authorization', 'values': ['Bearer this_is_apy_key'], + }], + }, 'httpResponse': { + 'statusCode': 200, 'body': JSON.stringify(expectedSourceContent), + }, 'times': { + 'remainingTimes': 1, 'unlimited': false, + }, + })); + }); + } + } + +//wait for job to be in status ready-for-review + cy.waitForJobStatus('ready-for-review'); + +//assert all the values + cy. + get('#status-value'). + invoke('text'). + should('contain', 'Ready for review'); + + cy.get('#translations-list th[data-title="Title"] div.element'). + invoke('attr', 'data-type'). + should('equal', 'lilthq\\craftliltplugin\\elements\\Translation'); + + cy.get('#translations-list th[data-title="Title"] div.element'). + invoke('attr', 'data-status'). + should('equal', 'ready-for-review'); + + cy.get('#translations-list th[data-title="Title"] div.element'). + invoke('attr', 'data-label'). + should('equal', 'The Future of Augmented Reality'); + + cy.get('#translations-list th[data-title="Title"] div.element'). + invoke('attr', 'title'). + should('equal', 'The Future of Augmented Reality – Happy Lager (en)'); + + cy.get('#author-label').invoke('text').should('equal', 'Author'); + + cy.get('#meta-settings-source-site'). + invoke('text'). + should('equal', 'en'); + + for (const language of languages) { + cy.get( + `#meta-settings-target-sites .target-languages-list span[data-language="${language}"]`). + should('be.visible'); + } + + cy.get('#meta-settings-translation-workflow'). + invoke('text'). + should('equal', 'Verified'); + + cy.get('#meta-settings-job-id'). + invoke('text'). + then((createdJobId) => { + const appUrl = Cypress.env('APP_URL'); + cy.url(). + should('equal', + `${appUrl}/admin/craft-lilt-plugin/job/edit/${createdJobId}`); + }); + + if (batchPublishing) { + cy.publishJobBatch({ + languages, jobTitle, copySlug, slug, entryId, enableAfterPublish, + }); + + return; + } + + cy.publishJob({ + languages, jobTitle, copySlug, slug, entryId, enableAfterPublish, + }); +}); diff --git a/src/controllers/translation/PostTranslationPublishController.php b/src/controllers/translation/PostTranslationPublishController.php index cb148e62..de0055b2 100644 --- a/src/controllers/translation/PostTranslationPublishController.php +++ b/src/controllers/translation/PostTranslationPublishController.php @@ -10,9 +10,11 @@ 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\SettingRecord; use lilthq\craftliltplugin\records\TranslationRecord; use Throwable; use yii\web\Response; @@ -46,6 +48,8 @@ public function actionInvoke(): Response $translation->translatedDraftId, $translation->targetSiteId ); + + $this->mergeCanonicalForAllDrafts($translation->jobId, $translation); } $updated = TranslationRecord::updateAll( @@ -53,10 +57,6 @@ public function actionInvoke(): Response ['id' => $translationIds] ); - if ($updated !== 1) { - //TODO: handle when we update more then one row - } - if ($updated) { foreach ($translations as $translation) { Craftliltplugin::getInstance()->jobLogsRepository->create( @@ -77,4 +77,58 @@ public function actionInvoke(): Response 'success' => $updated === 1 ]); } + + /** + * @param $translations + * @param TranslationRecord $translation + * @return ElementInterface|null + */ + private function mergeCanonicalForAllDrafts( + int $jobId, + TranslationRecord $translation + ): void { + $translationsToUpdate = TranslationRecord::findAll( + [ + 'jobId' => $jobId, + 'status' => [TranslationRecord::STATUS_READY_FOR_REVIEW, TranslationRecord::STATUS_READY_TO_PUBLISH] + ] + ); + + foreach ($translationsToUpdate as $translationToUpdate) { + $draftElement = Craft::$app->elements->getElementById( + $translationToUpdate->translatedDraftId, + null, + $translation->targetSiteId + ); + + if (!$draftElement) { + throw new \RuntimeException('Draft not found'); + } + + Craftliltplugin::getInstance()->createDraftHandler->markFieldsAsChanged( + $draftElement->getCanonical() + ); + $attributes = ['title']; + + $copyEntriesSlugFromSourceToTarget = SettingRecord::findOne( + ['name' => 'copy_entries_slug_from_source_to_target'] + ); + $isCopySlugEnabled = (bool) ($copyEntriesSlugFromSourceToTarget->value ?? false); + + if ($isCopySlugEnabled) { + $attributes[] = 'slug'; + } + Craftliltplugin::getInstance()->createDraftHandler->upsertChangedAttributes( + $draftElement->getCanonical(false), + $attributes + ); + + Craftliltplugin::getInstance()->publishDraftsHandler->mergeCanonicalChanges( + $draftElement + ); + + Craft::$app->elements->invalidateCachesForElement($draftElement->getCanonical()); + Craft::$app->elements->invalidateCachesForElement($draftElement); + } + } } diff --git a/src/elements/Job.php b/src/elements/Job.php index 5bd32189..86e32c30 100644 --- a/src/elements/Job.php +++ b/src/elements/Job.php @@ -70,9 +70,11 @@ public function beforeDelete(): bool $translationRecords = TranslationRecord::findAll(['jobId' => $this->id]); array_map(static function (TranslationRecord $t) { - Craft::$app->elements->deleteElementById( - $t->translatedDraftId - ); + if ($t->translatedDraftId !== null) { + Craft::$app->elements->deleteElementById( + $t->translatedDraftId + ); + } }, $translationRecords); JobRecord::deleteAll(['id' => $this->id]); diff --git a/src/services/appliers/ElementTranslatableContentApplier.php b/src/services/appliers/ElementTranslatableContentApplier.php index c9c2e77b..225761b3 100644 --- a/src/services/appliers/ElementTranslatableContentApplier.php +++ b/src/services/appliers/ElementTranslatableContentApplier.php @@ -129,9 +129,14 @@ public function apply(TranslationApplyCommand $translationApplyCommand): Element $draftElement->setIsFresh(); } - Craft::$app->elements->saveElement( - $draftElement - ); + Craft::$app + ->elements + ->saveElement( + $draftElement, + true, + false, + false + ); return $draftElement; } @@ -151,7 +156,6 @@ public function createDraftElement( TranslationApplyCommand $translationApplyCommand, array $newAttributes ): ElementInterface { - /** Element will be created from original one, we can't create draft from draft */ $createFrom = $translationApplyCommand->getElement()->getIsDraft() ? Craft::$app->elements->getElementById( $translationApplyCommand->getElement()->getCanonicalId() diff --git a/src/services/appliers/field/AbstractContentApplier.php b/src/services/appliers/field/AbstractContentApplier.php index a995721c..da76af84 100644 --- a/src/services/appliers/field/AbstractContentApplier.php +++ b/src/services/appliers/field/AbstractContentApplier.php @@ -9,7 +9,6 @@ use craft\base\FieldInterface; use craft\elements\MatrixBlock; use craft\errors\InvalidFieldException; -use lilthq\craftliltplugin\records\I18NRecord; use verbb\supertable\elements\SuperTableBlockElement; abstract class AbstractContentApplier @@ -30,9 +29,15 @@ protected function forceSave(ApplyContentCommand $command): ?bool // @since In craft only from 3.7.14 $command->getElement()->setIsFresh(); } - $success = Craft::$app->elements->saveElement( - $command->getElement() - ); + + $success = Craft::$app + ->elements + ->saveElement( + $command->getElement(), + true, + false, + false + ); Craft::$app->elements->invalidateCachesForElement($command->getElement()); @@ -60,9 +65,8 @@ protected function getFieldKey(FieldInterface $field): string } /** - * @throws InvalidFieldException - * * @return mixed + * @throws InvalidFieldException */ protected function getOriginalFieldSerializedValue(ApplyContentCommand $command) { diff --git a/src/services/handlers/CreateDraftHandler.php b/src/services/handlers/CreateDraftHandler.php index 88e7e006..ec382b22 100644 --- a/src/services/handlers/CreateDraftHandler.php +++ b/src/services/handlers/CreateDraftHandler.php @@ -12,6 +12,7 @@ use craft\db\Table as DbTable; use craft\elements\db\ElementQuery; use craft\elements\Entry; +use craft\elements\MatrixBlock; use craft\errors\ElementNotFoundException; use craft\errors\InvalidFieldException; use craft\helpers\Db; @@ -42,7 +43,7 @@ public function create( * Element will be created from original one, we can't create draft from draft * @var Entry $createFrom */ - $createFrom = $element ? Craft::$app->elements->getElementById( + $createFrom = $element->getIsDraft() ? Craft::$app->elements->getElementById( $element->getCanonicalId() ) : $element; @@ -78,11 +79,22 @@ public function create( foreach ($fields as $field) { if (get_class($field) === CraftliltpluginParameters::CRAFT_FIELDS_MATRIX) { - $draft->setFieldValue($field->handle, $draft->getFieldValue($field->handle)); + $blocksQuery = $draft->getFieldValue($field->handle); + + /** + * @var MatrixBlock[] $blocks + */ + $blocks = $blocksQuery->all(); Craft::$app->matrix->duplicateBlocks($field, $createFrom, $draft, false, false); Craft::$app->matrix->saveField($field, $draft); + foreach ($blocks as $block) { + if ($block instanceof MatrixBlock) { + Craft::$app->getElements()->deleteElement($block, true); + } + } + continue; } @@ -96,8 +108,6 @@ public function create( ); $draft->duplicateOf = $element; - $draft->mergingCanonicalChanges = true; - $draft->afterPropagate(true); $copyEntriesSlugFromSourceToTarget = SettingRecord::findOne( ['name' => 'copy_entries_slug_from_source_to_target'] @@ -108,7 +118,7 @@ public function create( $draft->slug = $element->slug; } - Craft::$app->elements->saveElement($draft); + Craft::$app->elements->saveElement($draft, true, false, false); $this->markFieldsAsChanged($draft); @@ -126,7 +136,7 @@ public function create( * @throws InvalidFieldException * @throws \yii\db\Exception */ - private function markFieldsAsChanged(ElementInterface $element): void + public function markFieldsAsChanged(ElementInterface $element): void { $fieldLayout = $element->getFieldLayout(); $fields = $fieldLayout ? $fieldLayout->getFields() : []; @@ -190,7 +200,7 @@ private function upsertChangedFields(ElementInterface $element, FieldInterface $ ); } - private function upsertChangedAttributes(ElementInterface $element, array $attributes): void + public function upsertChangedAttributes(ElementInterface $element, array $attributes): void { $userId = Craft::$app->getUser()->getId(); $timestamp = Db::prepareDateForDb(new DateTime()); diff --git a/src/services/handlers/PublishDraftHandler.php b/src/services/handlers/PublishDraftHandler.php index 2a2a3d39..ce17bc6a 100644 --- a/src/services/handlers/PublishDraftHandler.php +++ b/src/services/handlers/PublishDraftHandler.php @@ -2,7 +2,7 @@ /** * @link https://github.com/lilt - * @copyright Copyright (c) 2022 Lilt Devs + * @copyright Copyright (c) 2023 Lilt Devs */ declare(strict_types=1); @@ -10,6 +10,7 @@ namespace lilthq\craftliltplugin\services\handlers; use Craft; +use craft\base\ElementInterface; use craft\services\Drafts as DraftRepository; use lilthq\craftliltplugin\records\SettingRecord; use Throwable; @@ -33,26 +34,27 @@ public function __invoke(int $draftId, int $targetSiteId): void ); if (!$draftElement) { - //TODO: published already or what? Why we are here? return; } $enableEntriesForTargetSitesRecord = SettingRecord::findOne(['name' => 'enable_entries_for_target_sites']); - $enableEntriesForTargetSites = (bool) ($enableEntriesForTargetSitesRecord->value + $enableEntriesForTargetSites = (bool)($enableEntriesForTargetSitesRecord->value ?? false); - if (method_exists($draftElement, 'setIsFresh')) { - $draftElement->setIsFresh(); - } - - Craft::$app->getElements()->saveElement($draftElement); - $element = $this->draftRepository->applyDraft($draftElement); if ($enableEntriesForTargetSites && !$draftElement->getEnabledForSite($targetSiteId)) { $element->setEnabledForSite([$targetSiteId => true]); } - Craft::$app->getElements()->saveElement($element); + Craft::$app->getElements()->saveElement($element, true, false, false); Craft::$app->getElements()->invalidateCachesForElement($element); } + + public function mergeCanonicalChanges(ElementInterface $draftElement): void + { + Craft::$app->getElements()->mergeCanonicalChanges($draftElement); + + Craft::$app->getElements()->saveElement($draftElement, true, false); + Craft::$app->getElements()->invalidateCachesForElement($draftElement); + } }