From 88ca5e4bcd98190a10ca6a0f24eb5332747bb225 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 23 Mar 2024 10:27:12 +0000 Subject: [PATCH 01/12] chore(deps): update dependency autoprefixer to v10.4.19 --- frontend/package-lock.json | 10 +++++----- frontend/package.json | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index fe7911f133..1045c82ac9 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -81,7 +81,7 @@ "@vue/babel-preset-app": "5.0.8", "@vue/eslint-config-prettier": "9.0.0", "@vue/test-utils": "1.3.6", - "autoprefixer": "10.4.18", + "autoprefixer": "10.4.19", "babel-plugin-require-context-hook": "1.0.0", "eslint": "8.57.0", "eslint-config-prettier": "9.1.0", @@ -4914,9 +4914,9 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/autoprefixer": { - "version": "10.4.18", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.18.tgz", - "integrity": "sha512-1DKbDfsr6KUElM6wg+0zRNkB/Q7WcKYAaK+pzXn+Xqmszm/5Xa9coeNdtP88Vi+dPzZnMjhge8GIV49ZQkDa+g==", + "version": "10.4.19", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", + "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==", "dev": true, "funding": [ { @@ -4934,7 +4934,7 @@ ], "dependencies": { "browserslist": "^4.23.0", - "caniuse-lite": "^1.0.30001591", + "caniuse-lite": "^1.0.30001599", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", "picocolors": "^1.0.0", diff --git a/frontend/package.json b/frontend/package.json index 3735d6b12e..456259fd4a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -93,7 +93,7 @@ "@vue/babel-preset-app": "5.0.8", "@vue/eslint-config-prettier": "9.0.0", "@vue/test-utils": "1.3.6", - "autoprefixer": "10.4.18", + "autoprefixer": "10.4.19", "babel-plugin-require-context-hook": "1.0.0", "eslint": "8.57.0", "eslint-config-prettier": "9.1.0", From 5b13d395deb00129c4a58d0630672841392db6e2 Mon Sep 17 00:00:00 2001 From: Carlo Beltrame Date: Tue, 26 Mar 2024 02:46:42 +0100 Subject: [PATCH 02/12] Autofocus and autoselect title of new activity --- frontend/src/components/program/DialogActivityCreate.vue | 4 ++-- frontend/src/components/program/DialogActivityForm.vue | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/program/DialogActivityCreate.vue b/frontend/src/components/program/DialogActivityCreate.vue index 4eb8ea8a49..a154044ed2 100644 --- a/frontend/src/components/program/DialogActivityCreate.vue +++ b/frontend/src/components/program/DialogActivityCreate.vue @@ -71,7 +71,7 @@ - + @@ -23,10 +106,17 @@ import { categoryRoute } from '@/router.js' import DialogForm from '@/components/dialog/DialogForm.vue' import DialogBase from '@/components/dialog/DialogBase.vue' import DialogCategoryForm from './DialogCategoryForm.vue' +import PopoverPrompt from '../prompt/PopoverPrompt.vue' +import router from '../../router.js' +import CategoryChip from '../generic/CategoryChip.vue' +import CopyCategoryInfoDialog from '../category/CopyCategoryInfoDialog.vue' export default { name: 'DialogCategoryCreate', components: { + CopyCategoryInfoDialog, + CategoryChip, + PopoverPrompt, DialogCategoryForm, DialogForm, }, @@ -39,23 +129,102 @@ export default { entityProperties: ['camp', 'short', 'name', 'color', 'numberingStyle'], embeddedCollections: ['preferredContentTypes'], entityUri: '/categories', + clipboardPermission: 'unknown', + copyCategorySource: null, + copyCategorySourceUrl: null, + copyCategorySourceUrlLoading: false, + copyCategorySourceUrlShowPopover: false, } }, + computed: { + clipboardAccessDenied() { + return ( + this.clipboardPermission === 'unaccessable' || + this.clipboardPermission === 'denied' + ) + }, + hasCopyCategorySource() { + return this.copyCategorySource != null && this.copyCategorySource._meta.self != null + }, + copyContent: { + get() { + return this.entityData.copyCategorySource != null + }, + set(val) { + if (val) { + this.entityData.copyCategorySource = this.copyCategorySource._meta.self + this.entityData.short = this.copyCategorySourceCategory.short + this.entityData.name = this.copyCategorySourceCategory.name + this.entityData.color = this.copyCategorySourceCategory.color + this.entityData.numberingStyle = this.copyCategorySourceCategory.numberingStyle + } else { + this.entityData.copyCategorySource = null + } + }, + }, + copyCategorySourceCategory() { + if (!this.hasCopyCategorySource) return null + return this.copyCategorySource.short + ? this.copyCategorySource + : this.copyCategorySource.category() + }, + }, watch: { showDialog: function (showDialog) { if (showDialog) { + this.refreshCopyCategorySource() this.setEntityData({ camp: this.camp._meta.self, short: '', name: '', color: '#000000', numberingStyle: '1', + copyCategorySource: null, }) } else { // clear form on exit this.clearEntityData() + this.copyCategorySource = null + this.copyCategorySourceUrl = null } }, + copyCategorySourceUrl: function (url) { + this.copyCategorySourceUrlLoading = true + + this.getCopyCategorySource(url).then( + (categoryOrActivityProxy) => { + if (categoryOrActivityProxy != null) { + categoryOrActivityProxy._meta.load.then( + async (categoryOrActivity) => { + if (!categoryOrActivity.short) { + await categoryOrActivity.category()._meta.load + } + this.copyCategorySource = categoryOrActivity + this.copyContent = true + this.copyCategorySourceUrlLoading = false + }, + () => { + this.copyCategorySourceUrlLoading = false + } + ) + } else { + this.copyCategorySource = null + this.copyContent = false + this.copyCategorySourceUrlLoading = false + } + + // if Paste-Popover is shown, close it now + if (this.copyCategorySourceUrlShowPopover) { + this.$nextTick(() => { + this.copyCategorySourceUrlShowPopover = false + }) + } + }, + () => { + this.copyCategorySourceUrlLoading = false + } + ) + }, }, methods: { async createCategory() { @@ -63,6 +232,52 @@ export default { await this.api.reload(this.camp.categories()) this.$router.push(categoryRoute(this.camp, createdCategory, { new: true })) }, + refreshCopyCategorySource() { + navigator.permissions.query({ name: 'clipboard-read' }).then( + (p) => { + this.clipboardPermission = p.state + this.copyCategorySource = null + + if (p.state === 'granted') { + navigator.clipboard + .readText() + .then(async (url) => { + this.copyCategorySource = await ( + await this.getCopyCategorySource(url) + )?._meta.load + }) + .catch(() => { + this.clipboardPermission = 'unaccessable' + console.warn('clipboard permission not requestable') + }) + } + }, + () => { + this.clipboardPermission = 'unaccessable' + console.warn('clipboard permission not requestable') + } + ) + }, + async getCopyCategorySource(url) { + if (url?.startsWith(window.location.origin)) { + url = url.substring(window.location.origin.length) + const match = router.matcher.match(url) + + if (match.name === 'activity') { + const scheduleEntry = await this.api + .get() + .scheduleEntries({ id: match.params['scheduleEntryId'] }) + return await scheduleEntry.activity() + } else if (match.name === 'admin/activity/category') { + return await this.api.get().categories({ id: match.params['categoryId'] }) + } + } + return null + }, + async clearClipboard() { + await navigator.clipboard.writeText('') + this.refreshCopyCategorySource() + }, }, } diff --git a/frontend/src/components/campAdmin/DialogCategoryForm.vue b/frontend/src/components/campAdmin/DialogCategoryForm.vue index 8d480df706..b82c4847c7 100644 --- a/frontend/src/components/campAdmin/DialogCategoryForm.vue +++ b/frontend/src/components/campAdmin/DialogCategoryForm.vue @@ -1,10 +1,14 @@ diff --git a/frontend/src/locales/de.json b/frontend/src/locales/de.json index 1bd2d80529..8946d9fc27 100644 --- a/frontend/src/locales/de.json +++ b/frontend/src/locales/de.json @@ -123,7 +123,7 @@ "clipboard": "Zwischenablage", "copyCategoryOrActivity": "Kategorie oder Aktivität kopieren", "copyContent": "Inhalt kopieren", - "copyPasteCategoryOrActivity": "Kategorie oder Aktivität kopieren & einfügen", + "copyPasteCategory": "Kategorie kopieren & einfügen", "copySourceInfo": "Hier kannst du die URL einer Block-Kategorie oder einer Aktivität einfügen um dessen Inhalte zu kopieren.", "pasteCategory": "Kopierte Kategorie oder Aktivität einfügen", "title": "Block-Kategorie erstellen" diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index 1175a99b91..af4a2ff40f 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -123,6 +123,7 @@ "clipboard": "Clipboard", "copyCategoryOrActivity": "Copy category or activity", "copyContent": "Copy content", + "copyPasteCategory": "Copy & paste category", "copySourceInfo": "Here you can paste the URL of a category or an activity to copy its contents.", "pasteCategory": "paste category or activity", "title": "Create activity category" From b2614bf165f841dce56a307eb4c6392c71fff71a Mon Sep 17 00:00:00 2001 From: Carlo Beltrame Date: Wed, 27 Mar 2024 17:40:05 +0100 Subject: [PATCH 10/12] Adjust and extend tests --- .../Api/Categories/CreateCategoryTest.php | 101 +++++++++++++++++- ...est__testOpenApiSpecMatchesSnapshot__1.yml | 27 ++++- .../campAdmin/DialogCategoryCreate.vue | 4 +- 3 files changed, 124 insertions(+), 8 deletions(-) diff --git a/api/tests/Api/Categories/CreateCategoryTest.php b/api/tests/Api/Categories/CreateCategoryTest.php index 5433282fb1..14a0b76f42 100644 --- a/api/tests/Api/Categories/CreateCategoryTest.php +++ b/api/tests/Api/Categories/CreateCategoryTest.php @@ -99,10 +99,10 @@ public function testCreateCategoryCreatesNewColumnLayoutAsRootContentNode() { $this->assertResponseStatusCodeSame(201); $newestColumnLayout = $this->getEntityManager()->getRepository(ContentNode::class) - ->findBy(['contentType' => static::$fixtures['contentTypeColumnLayout']], ['createTime' => 'DESC'])[0] + ->findBy(['contentType' => static::$fixtures['contentTypeColumnLayout'], 'instanceName' => null], ['createTime' => 'DESC'], 1)[0] ; $this->assertJsonContains(['_links' => [ - 'rootContentNode' => ['href' => '/content_node/column_layouts/'.$newestColumnLayout->getId()], + 'rootContentNode' => ['href' => $this->getIriFor($newestColumnLayout)], ]]); } @@ -456,6 +456,102 @@ public function testCreateCategoryValidatesInvalidNumberingStyle() { ]); } + public function testCreateCategoryFromCopySourceValidatesAccess() { + static::createClientWithCredentials(['email' => static::$fixtures['user8memberOnlyInCamp2']->getEmail()])->request( + 'POST', + '/categories', + ['json' => $this->getExampleWritePayload( + [ + 'camp' => $this->getIriFor('camp2'), + 'copyCategorySource' => $this->getIriFor('category1'), + ] + )] + ); + + // No Access on category1 -> BadRequest + $this->assertResponseStatusCodeSame(400); + } + + public function testCreateCategoryFromCopySourceWithinSameCamp() { + static::createClientWithCredentials()->request( + 'POST', + '/categories', + ['json' => $this->getExampleWritePayload( + [ + 'camp' => $this->getIriFor('camp1'), + 'copyCategorySource' => $this->getIriFor('category1'), + ], + )] + ); + + // Category created + $this->assertResponseStatusCodeSame(201); + } + + public function testCreateCategoryFromCopySourceAcrossCamp() { + static::createClientWithCredentials()->request( + 'POST', + '/categories', + ['json' => $this->getExampleWritePayload( + [ + 'camp' => $this->getIriFor('camp2'), + 'copyCategorySource' => $this->getIriFor('category1'), + ], + )] + ); + + // Category created + $this->assertResponseStatusCodeSame(201); + } + + public function testCreateCategoryFromCopySourceActivityValidatesAccess() { + static::createClientWithCredentials(['email' => static::$fixtures['user8memberOnlyInCamp2']->getEmail()])->request( + 'POST', + '/categories', + ['json' => $this->getExampleWritePayload( + [ + 'camp' => $this->getIriFor('camp2'), + 'copyCategorySource' => $this->getIriFor('activity1'), + ] + )] + ); + + // No Access on activity1 -> BadRequest + $this->assertResponseStatusCodeSame(400); + } + + public function testCreateCategoryFromCopySourceActivityWithinSameCamp() { + static::createClientWithCredentials()->request( + 'POST', + '/categories', + ['json' => $this->getExampleWritePayload( + [ + 'camp' => $this->getIriFor('camp1'), + 'copyCategorySource' => $this->getIriFor('activity1'), + ], + )] + ); + + // Category created + $this->assertResponseStatusCodeSame(201); + } + + public function testCreateCategoryFromCopySourceActivityAcrossCamp() { + static::createClientWithCredentials()->request( + 'POST', + '/categories', + ['json' => $this->getExampleWritePayload( + [ + 'camp' => $this->getIriFor('camp2'), + 'copyCategorySource' => $this->getIriFor('activity1'), + ], + )] + ); + + // Category created + $this->assertResponseStatusCodeSame(201); + } + /** * @throws RedirectionExceptionInterface * @throws DecodingExceptionInterface @@ -488,6 +584,7 @@ public function getExampleWritePayload($attributes = [], $except = []) { Category::class, Post::class, array_merge([ + 'copyCategorySource' => null, 'camp' => $this->getIriFor('camp1'), 'preferredContentTypes' => [$this->getIriFor('contentTypeSafetyConcept')], ], $attributes), diff --git a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testOpenApiSpecMatchesSnapshot__1.yml b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testOpenApiSpecMatchesSnapshot__1.yml index 60be7ed441..6eeed510f5 100644 --- a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testOpenApiSpecMatchesSnapshot__1.yml +++ b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testOpenApiSpecMatchesSnapshot__1.yml @@ -362,7 +362,7 @@ components: format: iri-reference type: string copyActivitySource: - description: 'Copy Contents from this Source-Activity.' + description: 'Copy contents from this source activity.' example: /activities/1a2b3c4d format: iri-reference type: @@ -1168,7 +1168,7 @@ components: format: iri-reference type: string copyActivitySource: - description: 'Copy Contents from this Source-Activity.' + description: 'Copy contents from this source activity.' example: /activities/1a2b3c4d format: iri-reference type: @@ -1607,7 +1607,7 @@ components: format: iri-reference type: string copyActivitySource: - description: 'Copy Contents from this Source-Activity.' + description: 'Copy contents from this source activity.' example: /activities/1a2b3c4d format: iri-reference type: @@ -7938,6 +7938,13 @@ components: maxLength: 8 pattern: '^(#[0-9a-zA-Z]{6})$' type: string + copyCategorySource: + description: 'Copy contents from this source category or activity.' + example: /categories/1a2b3c4d + format: iri-reference + type: + - 'null' + - string name: description: 'The full name of the category.' example: Lagersport @@ -8690,6 +8697,13 @@ components: maxLength: 8 pattern: '^(#[0-9a-zA-Z]{6})$' type: string + copyCategorySource: + description: 'Copy contents from this source category or activity.' + example: /categories/1a2b3c4d + format: iri-reference + type: + - 'null' + - string name: description: 'The full name of the category.' example: Lagersport @@ -9062,6 +9076,13 @@ components: maxLength: 8 pattern: '^(#[0-9a-zA-Z]{6})$' type: string + copyCategorySource: + description: 'Copy contents from this source category or activity.' + example: /categories/1a2b3c4d + format: iri-reference + type: + - 'null' + - string name: description: 'The full name of the category.' example: Lagersport diff --git a/frontend/src/components/campAdmin/DialogCategoryCreate.vue b/frontend/src/components/campAdmin/DialogCategoryCreate.vue index 5f8f589f5f..3566695929 100644 --- a/frontend/src/components/campAdmin/DialogCategoryCreate.vue +++ b/frontend/src/components/campAdmin/DialogCategoryCreate.vue @@ -19,9 +19,7 @@ From 70b01ebbf0edc554a354f147e108f645957d6324 Mon Sep 17 00:00:00 2001 From: Carlo Beltrame Date: Sat, 30 Mar 2024 20:17:20 +0100 Subject: [PATCH 11/12] Minor fixes from review --- frontend/src/components/campAdmin/DialogCategoryCreate.vue | 7 +++---- frontend/src/components/program/DialogActivityCreate.vue | 5 ++--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/campAdmin/DialogCategoryCreate.vue b/frontend/src/components/campAdmin/DialogCategoryCreate.vue index 3566695929..ae78f22109 100644 --- a/frontend/src/components/campAdmin/DialogCategoryCreate.vue +++ b/frontend/src/components/campAdmin/DialogCategoryCreate.vue @@ -105,7 +105,7 @@ import DialogForm from '@/components/dialog/DialogForm.vue' import DialogBase from '@/components/dialog/DialogBase.vue' import DialogCategoryForm from './DialogCategoryForm.vue' import PopoverPrompt from '../prompt/PopoverPrompt.vue' -import router from '../../router.js' +import router from '@/router.js' import CategoryChip from '../generic/CategoryChip.vue' import CopyCategoryInfoDialog from '../category/CopyCategoryInfoDialog.vue' @@ -240,9 +240,8 @@ export default { navigator.clipboard .readText() .then(async (url) => { - this.copyCategorySource = await ( - await this.getCopyCategorySource(url) - )?._meta.load + const copyCategorySource = await this.getCopyCategorySource(url) + this.copyCategorySource = await copyCategorySource?._meta.load }) .catch(() => { this.clipboardPermission = 'unaccessable' diff --git a/frontend/src/components/program/DialogActivityCreate.vue b/frontend/src/components/program/DialogActivityCreate.vue index b9242887cc..f9f67373e5 100644 --- a/frontend/src/components/program/DialogActivityCreate.vue +++ b/frontend/src/components/program/DialogActivityCreate.vue @@ -258,9 +258,8 @@ export default { navigator.clipboard .readText() .then(async (url) => { - this.copyActivitySource = await ( - await this.getCopyActivitySource(url) - )?._meta.load + const copyActivitySource = await this.getCopyActivitySource(url) + this.copyActivitySource = await copyActivitySource?._meta.load }) .catch(() => { this.clipboardPermission = 'unaccessable' From ccfdcfcbbfaec9ecd2991a6348dd20a0106c025e Mon Sep 17 00:00:00 2001 From: Carlo Beltrame Date: Sat, 30 Mar 2024 21:17:40 +0100 Subject: [PATCH 12/12] Update snapshot --- ...seSnapshotTest__testOpenApiSpecMatchesSnapshot__1.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testOpenApiSpecMatchesSnapshot__1.yml b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testOpenApiSpecMatchesSnapshot__1.yml index 6eeed510f5..87e6a210a3 100644 --- a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testOpenApiSpecMatchesSnapshot__1.yml +++ b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testOpenApiSpecMatchesSnapshot__1.yml @@ -759,7 +759,7 @@ components: format: iri-reference type: string copyActivitySource: - description: 'Copy Contents from this Source-Activity.' + description: 'Copy contents from this source activity.' example: /activities/1a2b3c4d format: iri-reference type: @@ -8303,6 +8303,13 @@ components: maxLength: 8 pattern: '^(#[0-9a-zA-Z]{6})$' type: string + copyCategorySource: + description: 'Copy contents from this source category or activity.' + example: /categories/1a2b3c4d + format: iri-reference + type: + - 'null' + - string name: description: 'The full name of the category.' example: Lagersport