Skip to content

Commit b3b30a0

Browse files
committed
Implement pasting categories
1 parent 9634cae commit b3b30a0

File tree

6 files changed

+254
-6
lines changed

6 files changed

+254
-6
lines changed

api/src/Entity/Category.php

+7
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,13 @@ class Category extends BaseEntity implements BelongsToCampInterface, CopyFromPro
107107
#[ORM\OneToMany(targetEntity: Activity::class, mappedBy: 'category', orphanRemoval: true)]
108108
public Collection $activities;
109109

110+
/**
111+
* Copy contents from this source category or activity.
112+
*/
113+
#[ApiProperty(example: '/categories/1a2b3c4d')]
114+
#[Groups(['create'])]
115+
public null|Activity|Category $copyCategorySource;
116+
110117
/**
111118
* The id of the category that was used as a template for creating this category. Internal for now, is
112119
* not published through the API.

api/src/State/CategoryCreateProcessor.php

+7
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use App\Entity\ContentNode\ColumnLayout;
99
use App\Entity\ContentType;
1010
use App\State\Util\AbstractPersistProcessor;
11+
use App\Util\EntityMap;
1112
use Doctrine\ORM\EntityManagerInterface;
1213

1314
/**
@@ -35,6 +36,12 @@ public function onBefore($data, Operation $operation, array $uriVariables = [],
3536
$rootContentNode->data = ['columns' => [['slot' => '1', 'width' => 12]]];
3637
$data->setRootContentNode($rootContentNode);
3738

39+
if (isset($data->copyCategorySource)) {
40+
// CopyActivity Source is set -> copy it's content (rootContentNode)
41+
$entityMap = new EntityMap();
42+
$rootContentNode->copyFromPrototype($data->copyCategorySource->getRootContentNode(), $entityMap);
43+
}
44+
3845
return $data;
3946
}
4047
}

frontend/src/components/campAdmin/DialogCategoryCreate.vue

+216-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,90 @@
1414
<slot name="activator" v-bind="scope" />
1515
</template>
1616

17-
<dialog-category-form :camp="camp" :is-new="true" :category="entityData" />
17+
<template #moreActions>
18+
<CopyCategoryInfoDialog @closed="refreshCopyCategorySource">
19+
<template #activator="{ on }">
20+
<v-btn v-show="clipboardPermission === 'prompt'" v-on="on">
21+
<v-icon left>mdi-information-outline</v-icon>
22+
{{
23+
$tc('components.campAdmin.dialogCategoryCreate.copyPasteCategoryOrActivity')
24+
}}
25+
</v-btn>
26+
</template>
27+
</CopyCategoryInfoDialog>
28+
</template>
29+
30+
<div v-if="hasCopyCategorySource">
31+
<div class="mb-8">
32+
<div v-if="!clipboardAccessDenied">
33+
{{ $tc('components.campAdmin.dialogCategoryCreate.clipboard') }}
34+
<div style="float: right">
35+
<small>
36+
<a
37+
href="#"
38+
style="color: inherit; text-decoration: none"
39+
@click="clearClipboard"
40+
>
41+
{{ $tc('components.campAdmin.dialogCategoryCreate.clearClipboard') }}
42+
</a>
43+
</small>
44+
</div>
45+
</div>
46+
<v-list-item
47+
class="ec-copy-source rounded-xl blue-grey lighten-5 blue-grey--text text--darken-4 mt-1"
48+
>
49+
<v-list-item-avatar>
50+
<v-icon color="blue-grey">mdi-clipboard-check-outline</v-icon>
51+
</v-list-item-avatar>
52+
<v-list-item-content>
53+
<v-list-item-title>
54+
<CategoryChip :category="copyCategorySourceCategory" class="mx-1" dense />
55+
{{ copyCategorySource.title }}
56+
</v-list-item-title>
57+
<v-list-item-subtitle>
58+
{{ copyCategorySource.camp().title }}
59+
</v-list-item-subtitle>
60+
</v-list-item-content>
61+
<v-list-item-action>
62+
<e-checkbox
63+
v-model="copyContent"
64+
:label="$tc('components.campAdmin.dialogCategoryCreate.copyContent')"
65+
/>
66+
</v-list-item-action>
67+
</v-list-item>
68+
</div>
69+
</div>
70+
<dialog-category-form :camp="camp" :is-new="true" :category="entityData">
71+
<template v-if="clipboardAccessDenied" #textFieldTitleAppend>
72+
<PopoverPrompt
73+
v-model="copyCategorySourceUrlShowPopover"
74+
icon="mdi-content-paste"
75+
:title="$tc('components.campAdmin.dialogCategoryCreate.pasteCategory')"
76+
>
77+
<template #activator="scope">
78+
<v-btn
79+
:title="$tc('components.campAdmin.dialogCategoryCreate.pasteCategory')"
80+
text
81+
class="v-btn--has-bg"
82+
height="56"
83+
v-on="scope.on"
84+
>
85+
<v-progress-circular v-if="copyCategorySourceUrlLoading" indeterminate />
86+
<v-icon v-else>mdi-content-paste</v-icon>
87+
</v-btn>
88+
</template>
89+
{{ $tc('components.campAdmin.dialogCategoryCreate.copySourceInfo') }}
90+
<e-text-field
91+
v-model="copyCategorySourceUrl"
92+
:label="
93+
$tc('components.campAdmin.dialogCategoryCreate.copyCategoryOrActivity')
94+
"
95+
style="margin-bottom: 12px"
96+
autofocus
97+
/>
98+
</PopoverPrompt>
99+
</template>
100+
</dialog-category-form>
18101
</dialog-form>
19102
</template>
20103

@@ -23,10 +106,17 @@ import { categoryRoute } from '@/router.js'
23106
import DialogForm from '@/components/dialog/DialogForm.vue'
24107
import DialogBase from '@/components/dialog/DialogBase.vue'
25108
import DialogCategoryForm from './DialogCategoryForm.vue'
109+
import PopoverPrompt from '../prompt/PopoverPrompt.vue'
110+
import router from '../../router.js'
111+
import CategoryChip from '../generic/CategoryChip.vue'
112+
import CopyCategoryInfoDialog from '../category/CopyCategoryInfoDialog.vue'
26113
27114
export default {
28115
name: 'DialogCategoryCreate',
29116
components: {
117+
CopyCategoryInfoDialog,
118+
CategoryChip,
119+
PopoverPrompt,
30120
DialogCategoryForm,
31121
DialogForm,
32122
},
@@ -39,30 +129,155 @@ export default {
39129
entityProperties: ['camp', 'short', 'name', 'color', 'numberingStyle'],
40130
embeddedCollections: ['preferredContentTypes'],
41131
entityUri: '/categories',
132+
clipboardPermission: 'unknown',
133+
copyCategorySource: null,
134+
copyCategorySourceUrl: null,
135+
copyCategorySourceUrlLoading: false,
136+
copyCategorySourceUrlShowPopover: false,
42137
}
43138
},
139+
computed: {
140+
clipboardAccessDenied() {
141+
return (
142+
this.clipboardPermission === 'unaccessable' ||
143+
this.clipboardPermission === 'denied'
144+
)
145+
},
146+
hasCopyCategorySource() {
147+
return this.copyCategorySource != null && this.copyCategorySource._meta.self != null
148+
},
149+
copyContent: {
150+
get() {
151+
return this.entityData.copyCategorySource != null
152+
},
153+
set(val) {
154+
if (val) {
155+
this.entityData.copyCategorySource = this.copyCategorySource._meta.self
156+
this.entityData.short = this.copyCategorySourceCategory.short
157+
this.entityData.name = this.copyCategorySourceCategory.name
158+
this.entityData.color = this.copyCategorySourceCategory.color
159+
this.entityData.numberingStyle = this.copyCategorySourceCategory.numberingStyle
160+
} else {
161+
this.entityData.copyCategorySource = null
162+
}
163+
},
164+
},
165+
copyCategorySourceCategory() {
166+
if (!this.hasCopyCategorySource) return null
167+
return this.copyCategorySource.short
168+
? this.copyCategorySource
169+
: this.copyCategorySource.category()
170+
},
171+
},
44172
watch: {
45173
showDialog: function (showDialog) {
46174
if (showDialog) {
175+
this.refreshCopyCategorySource()
47176
this.setEntityData({
48177
camp: this.camp._meta.self,
49178
short: '',
50179
name: '',
51180
color: '#000000',
52181
numberingStyle: '1',
182+
copyCategorySource: null,
53183
})
54184
} else {
55185
// clear form on exit
56186
this.clearEntityData()
187+
this.copyCategorySource = null
188+
this.copyCategorySourceUrl = null
57189
}
58190
},
191+
copyCategorySourceUrl: function (url) {
192+
this.copyCategorySourceUrlLoading = true
193+
194+
this.getCopyCategorySource(url).then(
195+
(categoryOrActivityProxy) => {
196+
if (categoryOrActivityProxy != null) {
197+
categoryOrActivityProxy._meta.load.then(
198+
async (categoryOrActivity) => {
199+
if (!categoryOrActivity.short) {
200+
await categoryOrActivity.category()._meta.load
201+
}
202+
this.copyCategorySource = categoryOrActivity
203+
this.copyContent = true
204+
this.copyCategorySourceUrlLoading = false
205+
},
206+
() => {
207+
this.copyCategorySourceUrlLoading = false
208+
}
209+
)
210+
} else {
211+
this.copyCategorySource = null
212+
this.copyContent = false
213+
this.copyCategorySourceUrlLoading = false
214+
}
215+
216+
// if Paste-Popover is shown, close it now
217+
if (this.copyCategorySourceUrlShowPopover) {
218+
this.$nextTick(() => {
219+
this.copyCategorySourceUrlShowPopover = false
220+
})
221+
}
222+
},
223+
() => {
224+
this.copyCategorySourceUrlLoading = false
225+
}
226+
)
227+
},
59228
},
60229
methods: {
61230
async createCategory() {
62231
const createdCategory = await this.create()
63232
await this.api.reload(this.camp.categories())
64233
this.$router.push(categoryRoute(this.camp, createdCategory, { new: true }))
65234
},
235+
refreshCopyCategorySource() {
236+
navigator.permissions.query({ name: 'clipboard-read' }).then(
237+
(p) => {
238+
this.clipboardPermission = p.state
239+
this.copyCategorySource = null
240+
241+
if (p.state === 'granted') {
242+
navigator.clipboard
243+
.readText()
244+
.then(async (url) => {
245+
this.copyCategorySource = await (
246+
await this.getCopyCategorySource(url)
247+
)?._meta.load
248+
})
249+
.catch(() => {
250+
this.clipboardPermission = 'unaccessable'
251+
console.warn('clipboard permission not requestable')
252+
})
253+
}
254+
},
255+
() => {
256+
this.clipboardPermission = 'unaccessable'
257+
console.warn('clipboard permission not requestable')
258+
}
259+
)
260+
},
261+
async getCopyCategorySource(url) {
262+
if (url?.startsWith(window.location.origin)) {
263+
url = url.substring(window.location.origin.length)
264+
const match = router.matcher.match(url)
265+
266+
if (match.name === 'activity') {
267+
const scheduleEntry = await this.api
268+
.get()
269+
.scheduleEntries({ id: match.params['scheduleEntryId'] })
270+
return await scheduleEntry.activity()
271+
} else if (match.name === 'admin/activity/category') {
272+
return await this.api.get().categories({ id: match.params['categoryId'] })
273+
}
274+
}
275+
return null
276+
},
277+
async clearClipboard() {
278+
await navigator.clipboard.writeText('')
279+
this.refreshCopyCategorySource()
280+
},
66281
},
67282
}
68283
</script>

frontend/src/components/campAdmin/DialogCategoryForm.vue

+9-5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
<template>
22
<div>
3-
<e-text-field
4-
v-model="localCategory.short"
5-
:name="$tc('entity.category.fields.short')"
6-
vee-rules="required"
7-
/>
3+
<div class="e-form-container d-flex gap-2">
4+
<e-text-field
5+
v-model="localCategory.short"
6+
:name="$tc('entity.category.fields.short')"
7+
vee-rules="required"
8+
class="flex-grow-1"
9+
/>
10+
<slot name="textFieldTitleAppend" />
11+
</div>
812

913
<e-text-field
1014
v-model="localCategory.name"

frontend/src/locales/de.json

+8
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
},
6666
"campCategories": {
6767
"create": "Block-Kategorie erstellen",
68+
"pasteCategory": "Kopierte Kategorie einfügen",
6869
"title": "Block-Kategorien"
6970
},
7071
"campConditionalFields": {
@@ -118,6 +119,13 @@
118119
"title": "Status bearbeiten"
119120
},
120121
"dialogCategoryCreate": {
122+
"clearClipboard": "Zwischenablage leeren",
123+
"clipboard": "Zwischenablage",
124+
"copyCategoryOrActivity": "Kategorie oder Aktivität kopieren",
125+
"copyContent": "Inhalt kopieren",
126+
"copyPasteCategoryOrActivity": "Kategorie oder Aktivität kopieren & einfügen",
127+
"copySourceInfo": "Hier kannst du die URL einer Block-Kategorie oder einer Aktivität einfügen um dessen Inhalte zu kopieren.",
128+
"pasteCategory": "Kopierte Kategorie oder Aktivität einfügen",
121129
"title": "Block-Kategorie erstellen"
122130
},
123131
"dialogMaterialListCreate": {

frontend/src/locales/en.json

+7
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
},
6666
"campCategories": {
6767
"create": "Create activity category",
68+
"pasteCategory": "Paste category",
6869
"title": "Activity categories"
6970
},
7071
"campConditionalFields": {
@@ -118,6 +119,12 @@
118119
"title": "Edit activity state"
119120
},
120121
"dialogCategoryCreate": {
122+
"clearClipboard": "Clear clipboard",
123+
"clipboard": "Clipboard",
124+
"copyCategoryOrActivity": "Copy category or activity",
125+
"copyContent": "Copy content",
126+
"copySourceInfo": "Here you can paste the URL of a category or an activity to copy its contents.",
127+
"pasteCategory": "paste category or activity",
121128
"title": "Create activity category"
122129
},
123130
"dialogMaterialListCreate": {

0 commit comments

Comments
 (0)