From a080b5a5e3a66fec9b4f048f9afead06213f013e Mon Sep 17 00:00:00 2001 From: dante di domenico Date: Tue, 13 Aug 2024 11:26:11 +0200 Subject: [PATCH 1/4] feat: clone translations --- locales/default.pot | 5 +- locales/en_US/default.po | 5 +- locales/it_IT/default.po | 5 +- resources/js/app/app.js | 19 +++-- resources/js/app/components/dialog/dialog.js | 27 ++++--- src/Controller/Component/CloneComponent.php | 43 ++++++++++- src/Controller/ModulesController.php | 1 + .../Controller/BaseControllerTest.php | 20 ++++++ .../Component/CloneComponentTest.php | 71 +++++++++++++++++-- 9 files changed, 171 insertions(+), 25 deletions(-) diff --git a/locales/default.pot b/locales/default.pot index 5800d682a..b654257f4 100644 --- a/locales/default.pot +++ b/locales/default.pot @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: BEdita 4 \n" -"POT-Creation-Date: 2024-07-10 10:08:33 \n" +"POT-Creation-Date: 2024-08-13 09:24:59 \n" "MIME-Version: 1.0 \n" "Content-Transfer-Encoding: 8bit \n" "Language-Team: BEdita I18N & I10N Team \n" @@ -1136,6 +1136,9 @@ msgstr "" msgid "Clone relations" msgstr "" +msgid "Clone translations" +msgstr "" + msgid "If you confirm, this data will be gone forever. Are you sure?" msgstr "" diff --git a/locales/en_US/default.po b/locales/en_US/default.po index ef3d5108c..ba12e50b3 100644 --- a/locales/en_US/default.po +++ b/locales/en_US/default.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: BEdita Manager \n" -"POT-Creation-Date: 2024-07-10 10:08:33 \n" +"POT-Creation-Date: 2024-08-13 09:24:59 \n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: BEdita I18N & I10N Team \n" @@ -1139,6 +1139,9 @@ msgstr "" msgid "Clone relations" msgstr "" +msgid "Clone translations" +msgstr "" + msgid "If you confirm, this data will be gone forever. Are you sure?" msgstr "" diff --git a/locales/it_IT/default.po b/locales/it_IT/default.po index ccb4344d2..f850c9cfa 100644 --- a/locales/it_IT/default.po +++ b/locales/it_IT/default.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: BEdita Manager \n" -"POT-Creation-Date: 2024-07-10 10:08:33 \n" +"POT-Creation-Date: 2024-08-13 09:24:59 \n" "PO-Revision-Date: \n" "Last-Translator: \n" "Language-Team: BEdita I18N & I10N Team \n" @@ -1144,6 +1144,9 @@ msgstr "copia" msgid "Clone relations" msgstr "Clona le relazioni" +msgid "Clone translations" +msgstr "Clona le traduzioni" + msgid "If you confirm, this data will be gone forever. Are you sure?" msgstr "Questo dato verrĂ  eliminato definitivamente. Vuoi proseguire?" diff --git a/resources/js/app/app.js b/resources/js/app/app.js index ef2aff383..64fad2a26 100644 --- a/resources/js/app/app.js +++ b/resources/js/app/app.js @@ -216,12 +216,13 @@ const _vueInstance = new Vue({ const title = document.getElementById('title').value || t('Untitled'); const msg = t`Please insert a new title on "${title}" clone`; const defaultTitle = title + '-' + t`copy`; - const confirmCallback = (cloneTitle, cloneRelations, dialog, unique) => { - let query = `?title=${cloneTitle || defaultTitle}`; - for (const uitem of unique) { + const confirmCallback = (dialog, options) => { + let query = `?title=${options?.title || defaultTitle}`; + for (const uitem of options?.unique || []) { query += `&${uitem.field}=${uitem.value}`; } - query += `&cloneRelations=${cloneRelations || false}`; + query += `&cloneRelations=${options?.relations || false}`; + query += `&cloneTranslations=${options?.translations || false}`; const origin = window.location.origin; const path = window.location.pathname.replace('/view/', '/clone/'); const url = `${origin}${path}${query}`; @@ -239,9 +240,13 @@ const _vueInstance = new Vue({ value: document.getElementById(field).value + '-' + t`copy`, }); } - const options = { checkLabel: t`Clone relations`, checkValue: false, unique: uniqueOptions }; - - prompt(msg, defaultTitle, confirmCallback, document.body, options); + prompt(msg, defaultTitle, confirmCallback, document.body, { + checks: [ + { name: 'relations', label: t`Clone relations`, value: false }, + { name: 'translations', label: t`Clone translations`, value: false } + ], + unique: uniqueOptions + }); }, /** diff --git a/resources/js/app/components/dialog/dialog.js b/resources/js/app/components/dialog/dialog.js index 19f650ae0..70e663eff 100644 --- a/resources/js/app/components/dialog/dialog.js +++ b/resources/js/app/components/dialog/dialog.js @@ -33,16 +33,18 @@ export const Dialog = Vue.extend({ -
- - -
+
@@ -70,13 +72,21 @@ export const Dialog = Vue.extend({ confirmCallback: this.hide, cancelMessage: false, inputValue: '', - checkValue: '', - checkLabel: '', + checks: [], loading: false, unique: [], }; }, methods: { + callbackOptions(title, unique) { + const relations = this.checks?.filter(c => c?.name === 'relations')?.[0]?.value || false; + const translations = this.checks?.filter(c => c?.name === 'translations')?.[0]?.value || false; + + return {title, relations, translations, unique}; + }, + checkname(index) { + return `check_${index}`; + }, show(message, headerText = this.dialogType, root = document.body) { this.headerText = headerText; this.message = message; @@ -126,8 +136,7 @@ export const Dialog = Vue.extend({ prompt(message, defaultValue, confirmCallback, root = document.body, options = {}) { this.dialogType = 'prompt'; this.inputValue = defaultValue || ''; - this.checkValue = options?.checkValue || ''; - this.checkLabel = options?.checkLabel || ''; + this.checks = options.checks || []; this.unique = options?.unique || []; this.confirmCallback = confirmCallback; this.cancelMessage = t`cancel`; diff --git a/src/Controller/Component/CloneComponent.php b/src/Controller/Component/CloneComponent.php index 0d4f5855c..a2982e537 100644 --- a/src/Controller/Component/CloneComponent.php +++ b/src/Controller/Component/CloneComponent.php @@ -114,9 +114,21 @@ function ($key) use ($reset) { */ public function queryCloneRelations(): bool { - $cloneRelations = $this->getController()->getRequest()->getQuery('cloneRelations'); + return $this->queryClone('cloneRelations'); + } + + /** + * Get the value of query $param. + * Return true when $param is not false. + * + * @param string $param The query parameter + * @return bool + */ + public function queryClone(string $param): bool + { + $clone = $this->getController()->getRequest()->getQuery($param); - return filter_var($cloneRelations, FILTER_VALIDATE_BOOLEAN) !== false; + return filter_var($clone, FILTER_VALIDATE_BOOLEAN) !== false; } /** @@ -143,6 +155,33 @@ public function relations(array $source, string $destinationId): bool return true; } + /** + * Clone translations from source object $source to destination object ID $destinationId. + * + * @param array $source The source object + * @param string $destinationId The destination ID + * @return bool + */ + public function translations(array $source, string $destinationId): bool + { + if (!$this->queryClone('cloneTranslations')) { + return false; + } + $sourceId = (string)Hash::get($source, 'data.id'); + $response = $this->apiClient->get(sprintf('/translations?filter[object_id]=%s', $sourceId)); + $responseData = (array)Hash::get($response, 'data'); + foreach ($responseData as $translation) { + $this->apiClient->save('translations', [ + 'lang' => (string)Hash::get($translation, 'attributes.lang'), + 'object_id' => $destinationId, + 'status' => (string)Hash::get($translation, 'attributes.status'), + 'translated_fields' => (array)Hash::get($translation, 'attributes.translated_fields'), + ]); + } + + return true; + } + /** * Filter relationships, remove not allowed 'children', 'parents', 'translations' * diff --git a/src/Controller/ModulesController.php b/src/Controller/ModulesController.php index 44904eb83..db7e342c5 100644 --- a/src/Controller/ModulesController.php +++ b/src/Controller/ModulesController.php @@ -361,6 +361,7 @@ public function clone($id): ?Response $save = $this->apiClient->save($this->objectType, $attributes); $destination = (string)Hash::get($save, 'data.id'); $this->Clone->relations($source, $destination); + $this->Clone->translations($source, $destination); $id = $destination; } catch (BEditaClientException $e) { $this->log($e->getMessage(), LogLevel::ERROR); diff --git a/tests/TestCase/Controller/BaseControllerTest.php b/tests/TestCase/Controller/BaseControllerTest.php index aa8fbae12..ae6128096 100644 --- a/tests/TestCase/Controller/BaseControllerTest.php +++ b/tests/TestCase/Controller/BaseControllerTest.php @@ -156,6 +156,26 @@ protected function createTestObject(): array return $o; } + /** + * Create a object for test purposes (if not available already) + * + * @return array + */ + public function createTestObjectWithTranslation(): array + { + $o = $this->getTestObject(); + if ($o == null) { + // create document + $response = $this->client->save('documents', ['title' => 'translations controller test document', 'uname' => $this->uname]); + $o = $response['data']; + + // create translation + $this->client->save('translations', ['object_id' => $o['id'], 'status' => 'draft', 'lang' => 'it', 'translated_fields' => ['title' => 'Titolo di test']]); + } + + return $o; + } + /** * Create a folder for test purposes (if not available already) * diff --git a/tests/TestCase/Controller/Component/CloneComponentTest.php b/tests/TestCase/Controller/Component/CloneComponentTest.php index 95c6f1dd7..78e22b510 100644 --- a/tests/TestCase/Controller/Component/CloneComponentTest.php +++ b/tests/TestCase/Controller/Component/CloneComponentTest.php @@ -132,13 +132,14 @@ public function relationsProvider(): array * @return void * @dataProvider relationsProvider() * @covers ::queryCloneRelations() + * @covers ::queryClone() */ public function testQueryCloneRelations(bool $expected): void { $this->prepareClone($expected); $this->setupApi(); - $actual = $this->Clone->queryCloneRelations(); - static::assertSame($expected, $actual); + static::assertSame($expected, $this->Clone->queryCloneRelations()); + static::assertSame($expected, $this->Clone->queryClone('cloneRelations')); } /** @@ -374,13 +375,75 @@ public function testStream(): void static::assertArrayNotHasKey('id', $attributes); } - private function prepareClone(bool $cloneRelations): void + /** + * Data provider for `testTranslations` + * + * @return array + */ + public function translationsProvider(): array + { + return [ + 'do not clone translations' => [false, false], + 'clone translations' => [true, true], + ]; + } + + /** + * Test `translations` method + * + * @param bool $clone Clone translations + * @param bool $expected Expected result + * @return void + * @dataProvider translationsProvider() + */ + public function testTranslations(bool $clone, bool $expected): void + { + $this->prepareClone($clone); + $destinationId = ''; + $source = []; + $expectedTranslation = []; + if ($clone) { + $this->setupApi(); + $this->Clone->startup(); + $response = $this->client->save('documents', ['title' => 'translations clone test document']); + $destinationId = (string)Hash::get($response, 'data.id'); + $source['data'] = (array)$this->createTestObjectWithTranslation(); + $sourceId = (string)Hash::get($source, 'data.id'); + $response = $this->client->get(sprintf('/translations?filter[object_id]=%s', $sourceId)); + $data = (array)Hash::get($response, 'data.0.attributes'); + $expectedTranslation = [ + 'lang' => (string)Hash::get($data, 'lang'), + 'object_id' => $destinationId, + 'status' => (string)Hash::get($data, 'status'), + 'translated_fields' => (array)Hash::get($data, 'translated_fields'), + ]; + } + $actual = $this->Clone->translations($source, $destinationId); + static::assertSame($expected, $actual); + if (!empty($expectedTranslation)) { + $response = $this->client->get(sprintf('/translations?filter[object_id]=%s', $destinationId)); + $data = (array)Hash::get($response, 'data.0.attributes'); + static::assertSame($expectedTranslation, [ + 'lang' => (string)Hash::get($data, 'lang'), + 'object_id' => $destinationId, + 'status' => (string)Hash::get($data, 'status'), + 'translated_fields' => (array)Hash::get($data, 'translated_fields'), + ]); + } + } + + /** + * Prepare clone component for test. + * + * @return void + */ + private function prepareClone(bool $clone): void { $config = [ 'environment' => [ 'REQUEST_METHOD' => 'POST', ], - 'query' => compact('cloneRelations'), + 'query' => ['cloneRelations' => $clone, 'cloneTranslations' => $clone], ]; $request = new ServerRequest($config); $this->controller = new Controller($request); From 3863752838b7661f1b55e28a225a720b2c1c907d Mon Sep 17 00:00:00 2001 From: dante di domenico Date: Tue, 13 Aug 2024 11:37:58 +0200 Subject: [PATCH 2/4] fix: createTestObjectWithTranslation --- tests/TestCase/Controller/BaseControllerTest.php | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/tests/TestCase/Controller/BaseControllerTest.php b/tests/TestCase/Controller/BaseControllerTest.php index ae6128096..8abbd9bb4 100644 --- a/tests/TestCase/Controller/BaseControllerTest.php +++ b/tests/TestCase/Controller/BaseControllerTest.php @@ -163,17 +163,10 @@ protected function createTestObject(): array */ public function createTestObjectWithTranslation(): array { - $o = $this->getTestObject(); - if ($o == null) { - // create document - $response = $this->client->save('documents', ['title' => 'translations controller test document', 'uname' => $this->uname]); - $o = $response['data']; + $response = $this->client->save('documents', ['title' => 'test document with translation']); + $this->client->save('translations', ['object_id' => $response['data']['id'], 'status' => 'draft', 'lang' => 'it', 'translated_fields' => ['title' => 'Titolo di test']]); - // create translation - $this->client->save('translations', ['object_id' => $o['id'], 'status' => 'draft', 'lang' => 'it', 'translated_fields' => ['title' => 'Titolo di test']]); - } - - return $o; + return $response['data']; } /** From 00f12eb51da8300c5b2affa8deebf8b48cbc5730 Mon Sep 17 00:00:00 2001 From: dante di domenico Date: Wed, 14 Aug 2024 12:42:08 +0200 Subject: [PATCH 3/4] chore fix: use find instead of filter --- resources/js/app/components/dialog/dialog.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/js/app/components/dialog/dialog.js b/resources/js/app/components/dialog/dialog.js index 70e663eff..b57e446de 100644 --- a/resources/js/app/components/dialog/dialog.js +++ b/resources/js/app/components/dialog/dialog.js @@ -79,8 +79,8 @@ export const Dialog = Vue.extend({ }, methods: { callbackOptions(title, unique) { - const relations = this.checks?.filter(c => c?.name === 'relations')?.[0]?.value || false; - const translations = this.checks?.filter(c => c?.name === 'translations')?.[0]?.value || false; + const relations = this.checks?.find(c => c?.name === 'relations')?.value || false; + const translations = this.checks?.find(c => c?.name === 'translations')?.value || false; return {title, relations, translations, unique}; }, From 750c2872eee1071c3013cd5566b68facc24a8ae7 Mon Sep 17 00:00:00 2001 From: dante di domenico Date: Wed, 14 Aug 2024 12:43:17 +0200 Subject: [PATCH 4/4] chore feat: update .gitignore with css bundle files --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 5ca85b404..ebb1cde3c 100644 --- a/.gitignore +++ b/.gitignore @@ -74,6 +74,7 @@ bundle-report.*.html /webroot/css/date-input* /webroot/css/email-input* /webroot/css/flash-message* +/webroot/css/folder-picker* /webroot/css/import-result* /webroot/css/index-cell* /webroot/css/labels-form* @@ -84,10 +85,12 @@ bundle-report.*.html /webroot/css/object-property* /webroot/css/objects-history* /webroot/css/permission* +/webroot/css/placeholder-list* /webroot/css/property-view* /webroot/css/roles-list-view* /webroot/css/secret* /webroot/css/show-hide* +/webroot/css/system-info* /webroot/css/tag-form* /webroot/css/tag-picker* /webroot/css/thumbnail*