Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions api/src/Entity/Activity.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use App\Repository\ActivityRepository;
use App\State\ActivityCreateProcessor;
use App\State\ActivityRemoveProcessor;
use App\State\ActivityResetProcessor;
use App\Validator\AssertBelongsToSameCamp;
use App\Validator\AssertLastCollectionItemIsNotDeleted;
use Doctrine\Common\Collections\ArrayCollection;
Expand All @@ -24,6 +25,7 @@
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Annotation\SerializedName;
use Symfony\Component\Validator\Constraints as Assert;
use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation;

/**
* A piece of programme that will be carried out once or multiple times in a camp.
Expand All @@ -39,6 +41,14 @@
security: 'is_granted("CAMP_MEMBER", object) or is_granted("CAMP_MANAGER", object)',
validationContext: ['groups' => ['Default', 'update']]
),
new Patch(
processor: ActivityResetProcessor::class,
security: 'is_granted("CAMP_MANAGER", object)',
uriTemplate: 'activities/{id}/reset_contents',
denormalizationContext: ['groups' => ['reset_contents']],
openapi: new OpenApiOperation(summary: 'Delete all programme content from this activity and replace it with a copy of the content of the connected category.'),
validationContext: ['groups' => ['Default', 'reset_contents']]
),
new Delete(
processor: ActivityRemoveProcessor::class,
security: 'is_granted("CAMP_MEMBER", object) or is_granted("CAMP_MANAGER", object)'
Expand Down
60 changes: 60 additions & 0 deletions api/src/State/ActivityResetProcessor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

namespace App\State;

use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use App\Entity\Activity;
use App\Entity\ContentNode\ColumnLayout;
use App\Entity\ContentType;
use App\State\Util\AbstractPersistProcessor;
use App\Util\EntityMap;
use Doctrine\ORM\EntityManagerInterface;

/**
* @template-extends AbstractPersistProcessor<Activity>
*/
class ActivityResetProcessor extends AbstractPersistProcessor {
public function __construct(
ProcessorInterface $decorated,
private EntityManagerInterface $em
) {
parent::__construct($decorated);
}

/**
* @param Activity $data
*/
public function onBefore($data, Operation $operation, array $uriVariables = [], array $context = []): Activity {
// @phpstan-ignore nullsafe.neverNull
if (!isset($data->category?->rootContentNode)) {
throw new \UnexpectedValueException('Property rootContentNode of provided category is null. Object of type '.ColumnLayout::class.' expected.');
}
if (!$data->category->rootContentNode instanceof ColumnLayout) {
throw new \UnexpectedValueException('Property rootContentNode of provided category is of wrong type. Object of type '.ColumnLayout::class.' expected.');
}

// Delete the old content
$this->em->remove($data->rootContentNode);

// Copy content from the category
$targetCamp = $data->category->camp;
$rootContentNodePrototype = $data->category->rootContentNode;

$rootContentNode = new ColumnLayout();
$rootContentNode->contentType = $this->em
->getRepository(ContentType::class)
->findOneBy(['name' => 'ColumnLayout'])
;
$data->setRootContentNode($rootContentNode);

$entityMap = new EntityMap($targetCamp);
$rootContentNode->copyFromPrototype($rootContentNodePrototype, $entityMap);

return $data;
}

private function deleteContent(Activity $activity): void {

}
}
3 changes: 3 additions & 0 deletions frontend/src/components/category/CategoryTemplate.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
<template #categoryShort>
<CategoryChip :category="category" dense />
</template>
<template #applyToActivities>
<DialogApplyCategoryLayoutToActivities :category="category" :camp="camp" />
</template>
<template #br><br /></template>
</i18n>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
<template>
<DetailPane
v-if="isContributor"
v-model="dialogOpen"
max-width="900px"
:title="$tc('components.category.dialogApplyCategoryLayoutToActivities.title')"
icon="mdi-file-replace-outline"
:cancel-action="close"
:submit-action="apply"
submit-color="error"
submit-icon="mdi-bomb"
:submit-label="
$tc(
'components.category.dialogApplyCategoryLayoutToActivities.confirmApply',
selectedActivities.length
)
"
:submit-enabled="selectedActivities.length > 0"
>
<template #activator="{ on, attrs }">
<v-btn elevation="0" color="danger" :disabled="!isManager" v-bind="attrs" v-on="on">
<v-icon left>mdi-file-replace-outline</v-icon>
{{
$tc(
'components.category.dialogApplyCategoryLayoutToActivities.replaceActivityContents'
)
}}
</v-btn>
</template>

<p>
{{ $tc('components.category.dialogApplyCategoryLayoutToActivities.description') }}
</p>
<p>
<v-icon color="grey lighten-1">mdi-alert</v-icon>
{{ $tc('components.category.dialogApplyCategoryLayoutToActivities.risk') }}
</p>

<v-list two-line subheader>
<v-list-item>
<v-list-item-content>
<v-list-item-title>
<i18n
class="font-weight-medium"
path="components.category.dialogApplyCategoryLayoutToActivities.templateForNewActivities"
>
<template #categoryShort
><CategoryChip :category="category" dense
/></template>
</i18n>
</v-list-item-title>
<v-list-item-subtitle class="whitespace-normal">
{{ contentsSummary(category) }}
</v-list-item-subtitle>
</v-list-item-content>
<v-list-item-action class="justify-start">
<v-list-item-action-text>
{{
$tc('components.category.dialogApplyCategoryLayoutToActivities.lastChanged')
}}
</v-list-item-action-text>
<v-list-item-action-text>{{ updateTime(category) }}</v-list-item-action-text>
</v-list-item-action>
</v-list-item>
</v-list>

<v-divider></v-divider>

<v-list two-line>
<v-subheader class="justify-center black--text">
<v-icon>mdi-arrow-down-thick</v-icon>
{{
$tc(
'components.category.dialogApplyCategoryLayoutToActivities.applyToActivities'
)
}}
<v-icon>mdi-arrow-down-thick</v-icon>
</v-subheader>
</v-list>

<v-divider></v-divider>

<v-list two-line>
<v-list-item-group v-model="selectedActivities" active-class="red--text" multiple>
<template v-for="activity in activities">
<v-list-item :key="activity._meta.self">
<template #default="{ active }">
<v-list-item-action>
<v-checkbox :input-value="active" color="primary"></v-checkbox>
</v-list-item-action>
<v-list-item-content>
<v-list-item-title>
<CategoryChip
:category="activity.category()"
class="mr-1 flex-shrink-0"
dense
/>
{{ scheduleEntryNumbers(activity) }}
{{ activity.title }}
</v-list-item-title>
<v-list-item-subtitle class="whitespace-normal">
{{ contentsSummary(activity) }}
</v-list-item-subtitle>
</v-list-item-content>

<v-list-item-action class="justify-start">
<v-list-item-action-text>
{{
$tc(
'components.category.dialogApplyCategoryLayoutToActivities.lastChanged'
)
}}
</v-list-item-action-text>
<v-list-item-action-text>
{{ updateTime(activity) }}
</v-list-item-action-text>
</v-list-item-action>
</template>
</v-list-item>
</template>
</v-list-item-group>
</v-list>

<template #actions>
<v-progress-linear
:active="progress !== null"
:value="progress"
absolute
bottom
color="red"
></v-progress-linear>
</template>
</DetailPane>
</template>
<script>
import DetailPane from '@/components/generic/DetailPane.vue'
import { campRoleMixin } from '../../mixins/campRoleMixin.js'
import CategoryChip from '../generic/CategoryChip.vue'
import camelCase from 'lodash-es/camelCase.js'
import { errorToMultiLineToast } from '../toast/toasts.js'

export default {
name: 'DialogApplyCategoryLayoutToActivities',
components: { CategoryChip, DetailPane },
mixins: [campRoleMixin],
props: {
category: { type: Object, required: true },
camp: { type: Object, required: true },
},
data() {
return {
dialogOpen: false,
selectedActivities: [],
progress: null,
}
},
computed: {
activities() {
return this.camp
.activities()
.items.filter(
(activity) => activity.category()._meta.self === this.category._meta.self
)
},
},
methods: {
camelCase,
scheduleEntryNumbers(activity) {
if (this.category.numberingStyle === '-') return ''
return activity
.scheduleEntries()
.items.map((scheduleEntry) => scheduleEntry.number)
.join(', ')
},
contentsSummary(activityOrCategory) {
return (
activityOrCategory
.contentNodes()
.items.map((item) =>
this.$tc('contentNode.' + camelCase(item.contentTypeName) + '.name')
)
.join(', ') ||
this.$tc('components.category.dialogApplyCategoryLayoutToActivities.noContent')
)
},
updateTime(activityOrCategory) {

Check failure on line 186 in frontend/src/components/category/DialogApplyCategoryLayoutToActivities.vue

View workflow job for this annotation

GitHub Actions / Lint: Frontend (ESLint)

'activityOrCategory' is defined but never used. Allowed unused args must match /^_$/u
return this.$date().format(this.$tc('global.datetime.dateTimeLong'))
},
async apply() {
console.log('you did it!', this.selectedActivities)
this.progress = 0
const step = 100 / this.selectedActivities.length
const selected = [...this.selectedActivities]
for (const index of selected) {
const activity = this.activities[index]

await this.api
.href(this.api.get(), 'activities', {
id: activity.id,
action: 'reset_contents',
})
.then((url) => this.api.patch(url, {}))
.catch((e) => {
this.$toast.error(errorToMultiLineToast(e))
})
.finally(() => {
this.progress += step
this.selectedActivities = this.selectedActivities.splice(1)
activity.$reload()
})
}
},
close() {
this.dialogOpen = false
this.selectedActivities = []
},
},
}
</script>
<style scoped>
:deep(.v-chip__content) {
width: 100%;
}
</style>
13 changes: 12 additions & 1 deletion frontend/src/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -211,9 +211,20 @@
"category": {
"categoryTemplate": {
"contents": "Inhalte",
"createLayoutHelp": "Hier kannst du die Vorlage für neue {categoryShort}-Blöcke definieren.{br}Blockinhalt & Layout bereits erstellter {categoryShort}-Blöcke, werden nicht angepasst.",
"createLayoutHelp": "Hier kannst du die Vorlage für neue {categoryShort}-Blöcke definieren.{br}Blockinhalt & Layout bereits erstellter {categoryShort}-Blöcke, werden nicht automatisch angepasst. {applyToActivities}",
"layout": "Layout",
"noTemplate": "Keine Vorlage"
},
"dialogApplyCategoryLayoutToActivities": {
"applyToActivities": "Layout und Inhalte der folgenden Aktivitäten überschreiben",
"confirmApply": "Aktivitäten überschreiben | 1 Aktivität überschreiben | {count} Aktivitäten überschreiben",
"description": "Du kannst das Layout und die Inhalte von {categoryShort} auf Aktivitäten übertragen.",
"lastChanged": "Zuletzt geändert",
"noContent": "Kein Inhalt",
"replaceActivityContents": "Aktivitäten überschreiben",
"risk": "Jegliche bereits erfasste Inhalte in der Aktivität gehen dabei unwiderruflich verloren. Fahre nur fort wenn du dir sicher bist.",
"templateForNewActivities": "Vorlage für neue {categoryShort}-Blöcke",
"title": "Auf Aktivitäten übertragen"
}
},
"checklist": {
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@
"category": {
"categoryTemplate": {
"contents": "Contents",
"createLayoutHelp": "Here you can define the template for new {categoryShort} activities.{br}The content & layout of already created {categoryShort} activities will not be adjusted.",
"createLayoutHelp": "Here you can define the template for new {categoryShort} activities.{br}The content & layout of already created {categoryShort} activities will not be automatically adjusted. {applyToActivities}",
"layout": "Layout",
"noTemplate": "No template"
}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@
"category": {
"categoryTemplate": {
"contents": "Contenus",
"createLayoutHelp": "Ici, vous pouvez définir le modèle pour de nouvelles activités {categoryShort}.{br}Le contenu et la mise en page des activités {categoryShort} déjà créées ne seront pas ajustés.",
"createLayoutHelp": "Ici, vous pouvez définir le modèle pour de nouvelles activités {categoryShort}.{br}Le contenu et la mise en page des activités {categoryShort} déjà créées ne seront pas ajustés. {applyToActivities}",
"layout": "Layout",
"noTemplate": "Pas de modèle"
}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/locales/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@
"category": {
"categoryTemplate": {
"contents": "Contenuti",
"createLayoutHelp": "Qui puoi definire il modello per le nuove attività {categoryShort}.{br}Il contenuto e il layout delle attività {categoryShort} già create non verranno modificati.",
"createLayoutHelp": "Qui puoi definire il modello per le nuove attività {categoryShort}.{br}Il contenuto e il layout delle attività {categoryShort} già create non verranno modificati. {applyToActivities}",
"layout": "Layout",
"noTemplate": "Nessun modello"
}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/locales/rm.json
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@
"category": {
"categoryTemplate": {
"contents": "Cuntegns",
"createLayoutHelp": "Qua pudais vus definir il project per novas activitads da {categoryShort}.{br}Il cuntegn ed il layout ch'èn gia vegnidas fatgas da {categoryShort} na vegnan betg adattads.",
"createLayoutHelp": "Qua pudais vus definir il project per novas activitads da {categoryShort}.{br}Il cuntegn ed il layout ch'èn gia vegnidas fatgas da {categoryShort} na vegnan betg adattads. {applyToActivities}",
"layout": "Layout",
"noTemplate": "Nagin model"
}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/views/camp/category/Category.vue
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
</v-expansion-panel-header>
<v-expansion-panel-content>
<CategoryProperties
:key="category"
:key="category._meta.self"
:category="category"
:disabled="!isManager"
/>
Expand Down
Loading