Skip to content

Commit

Permalink
Implement pasting categories
Browse files Browse the repository at this point in the history
  • Loading branch information
carlobeltrame committed Mar 26, 2024
1 parent 39adba8 commit ab23fb2
Show file tree
Hide file tree
Showing 6 changed files with 254 additions and 6 deletions.
7 changes: 7 additions & 0 deletions api/src/Entity/Category.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,13 @@ class Category extends BaseEntity implements BelongsToCampInterface, CopyFromPro
#[ORM\OneToMany(targetEntity: Activity::class, mappedBy: 'category', orphanRemoval: true)]
public Collection $activities;

/**
* Copy contents from this source category or activity.
*/
#[ApiProperty(example: '/categories/1a2b3c4d')]
#[Groups(['create'])]
public Category|Activity|null $copyCategorySource;

/**
* The id of the category that was used as a template for creating this category. Internal for now, is
* not published through the API.
Expand Down
7 changes: 7 additions & 0 deletions api/src/State/CategoryCreateProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use App\Entity\ContentNode\ColumnLayout;
use App\Entity\ContentType;
use App\State\Util\AbstractPersistProcessor;
use App\Util\EntityMap;
use Doctrine\ORM\EntityManagerInterface;

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

if (isset($data->copyCategorySource)) {
// CopyActivity Source is set -> copy it's content (rootContentNode)
$entityMap = new EntityMap();
$rootContentNode->copyFromPrototype($data->copyCategorySource->getRootContentNode(), $entityMap);
}

return $data;
}
}
217 changes: 216 additions & 1 deletion frontend/src/components/campAdmin/DialogCategoryCreate.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,90 @@
<slot name="activator" v-bind="scope" />
</template>

<dialog-category-form :camp="camp" :is-new="true" :category="entityData" />
<template #moreActions>
<CopyCategoryInfoDialog @closed="refreshCopyCategorySource">
<template #activator="{ on }">
<v-btn v-show="clipboardPermission === 'prompt'" v-on="on">
<v-icon left>mdi-information-outline</v-icon>
{{
$tc('components.campAdmin.dialogCategoryCreate.copyPasteCategoryOrActivity')
}}
</v-btn>
</template>
</CopyCategoryInfoDialog>
</template>

<div v-if="hasCopyCategorySource">
<div class="mb-8">
<div v-if="!clipboardAccessDenied">
{{ $tc('components.campAdmin.dialogCategoryCreate.clipboard') }}
<div style="float: right">
<small>
<a
href="#"
style="color: inherit; text-decoration: none"
@click="clearClipboard"
>
{{ $tc('components.campAdmin.dialogCategoryCreate.clearClipboard') }}
</a>
</small>
</div>
</div>
<v-list-item
class="ec-copy-source rounded-xl blue-grey lighten-5 blue-grey--text text--darken-4 mt-1"
>
<v-list-item-avatar>
<v-icon color="blue-grey">mdi-clipboard-check-outline</v-icon>
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title>
<CategoryChip :category="copyCategorySourceCategory" class="mx-1" dense />
{{ copyCategorySource.title }}
</v-list-item-title>
<v-list-item-subtitle>
{{ copyCategorySource.camp().title }}
</v-list-item-subtitle>
</v-list-item-content>
<v-list-item-action>
<e-checkbox
v-model="copyContent"
:label="$tc('components.campAdmin.dialogCategoryCreate.copyContent')"
/>
</v-list-item-action>
</v-list-item>
</div>
</div>
<dialog-category-form :camp="camp" :is-new="true" :category="entityData">
<template v-if="clipboardAccessDenied" #textFieldTitleAppend>
<PopoverPrompt
v-model="copyCategorySourceUrlShowPopover"
icon="mdi-content-paste"
:title="$tc('components.campAdmin.dialogCategoryCreate.pasteCategory')"
>
<template #activator="scope">
<v-btn
:title="$tc('components.campAdmin.dialogCategoryCreate.pasteCategory')"
text
class="v-btn--has-bg"
height="56"
v-on="scope.on"
>
<v-progress-circular v-if="copyCategorySourceUrlLoading" indeterminate />
<v-icon v-else>mdi-content-paste</v-icon>
</v-btn>
</template>
{{ $tc('components.campAdmin.dialogCategoryCreate.copySourceInfo') }}
<e-text-field
v-model="copyCategorySourceUrl"
:label="
$tc('components.campAdmin.dialogCategoryCreate.copyCategoryOrActivity')
"
style="margin-bottom: 12px"
autofocus
/>
</PopoverPrompt>
</template>
</dialog-category-form>
</dialog-form>
</template>

Expand All @@ -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,
},
Expand All @@ -39,30 +129,155 @@ 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() {
const createdCategory = await this.create()
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()
},
},
}
</script>
Expand Down
14 changes: 9 additions & 5 deletions frontend/src/components/campAdmin/DialogCategoryForm.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
<template>
<div>
<e-text-field
v-model="localCategory.short"
:name="$tc('entity.category.fields.short')"
vee-rules="required"
/>
<div class="e-form-container d-flex gap-2">
<e-text-field
v-model="localCategory.short"
:name="$tc('entity.category.fields.short')"
vee-rules="required"
class="flex-grow-1"
/>
<slot name="textFieldTitleAppend" />
</div>

<e-text-field
v-model="localCategory.name"
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
},
"campCategories": {
"create": "Block-Kategorie erstellen",
"pasteCategory": "Kopierte Kategorie einfügen",
"title": "Block-Kategorien"
},
"campConditionalFields": {
Expand Down Expand Up @@ -118,6 +119,13 @@
"title": "Status bearbeiten"
},
"dialogCategoryCreate": {
"clearClipboard": "Zwischenablage leeren",
"clipboard": "Zwischenablage",
"copyCategoryOrActivity": "Kategorie oder Aktivität kopieren",
"copyContent": "Inhalt kopieren",
"copyPasteCategoryOrActivity": "Kategorie oder Aktivität 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"
},
"dialogMaterialListCreate": {
Expand Down
7 changes: 7 additions & 0 deletions frontend/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
},
"campCategories": {
"create": "Create activity category",
"pasteCategory": "Paste category",
"title": "Activity categories"
},
"campConditionalFields": {
Expand Down Expand Up @@ -118,6 +119,12 @@
"title": "Edit activity state"
},
"dialogCategoryCreate": {
"clearClipboard": "Clear clipboard",
"clipboard": "Clipboard",
"copyCategoryOrActivity": "Copy category or activity",
"copyContent": "Copy content",
"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"
},
"dialogMaterialListCreate": {
Expand Down

0 comments on commit ab23fb2

Please sign in to comment.