Skip to content

Commit

Permalink
Merge pull request #6023 from usu/feat/checklist-admin
Browse files Browse the repository at this point in the history
feat(frontend): admin interface for checklists
  • Loading branch information
manuelmeister authored Sep 28, 2024
2 parents bc1f39f + fe818f0 commit b91075b
Show file tree
Hide file tree
Showing 40 changed files with 723 additions and 502 deletions.
20 changes: 15 additions & 5 deletions api/src/Entity/ChecklistItem.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,19 @@
#[ApiResource(
operations: [
new Get(
security: 'is_granted("CAMP_COLLABORATOR", object) or is_granted("CAMP_IS_PROTOTYPE", object)'
security: 'is_granted("CHECKLIST_IS_PROTOTYPE", object) or
is_granted("CAMP_IS_PROTOTYPE", object) or
is_granted("CAMP_COLLABORATOR", object)
'
),
new Patch(
security: 'is_granted("CAMP_MEMBER", object) or is_granted("CAMP_MANAGER", object)'
security: '(is_granted("CHECKLIST_IS_PROTOTYPE", object) and is_granted("ROLE_ADMIN")) or
(is_granted("CAMP_MEMBER", object) or is_granted("CAMP_MANAGER", object))
'
),
new Delete(
security: 'is_granted("CAMP_MEMBER", object) or is_granted("CAMP_MANAGER", object)',
security: '(is_granted("CHECKLIST_IS_PROTOTYPE", object) and is_granted("ROLE_ADMIN")) or
(is_granted("CAMP_MEMBER", object) or is_granted("CAMP_MANAGER", object))',
validate: true,
validationContext: ['groups' => ['delete']],
),
Expand All @@ -47,15 +53,19 @@
),
new Post(
denormalizationContext: ['groups' => ['write', 'create']],
securityPostDenormalize: 'is_granted("CAMP_MEMBER", object) or is_granted("CAMP_MANAGER", object) or object.checklist === null'
securityPostDenormalize: '(is_granted("CHECKLIST_IS_PROTOTYPE", object) and is_granted("ROLE_ADMIN")) or
(!is_granted("CHECKLIST_IS_PROTOTYPE", object) and (is_granted("CAMP_MEMBER", object) or is_granted("CAMP_MANAGER", object) or object.checklist === null))
'
),
new GetCollection(
uriTemplate: self::CHECKLIST_SUBRESOURCE_URI_TEMPLATE,
uriVariables: [
'checklistId' => new Link(
fromClass: Checklist::class,
toProperty: 'checklist',
security: 'is_granted("CAMP_COLLABORATOR", checklist) or is_granted("CAMP_IS_PROTOTYPE", checklist)'
security: 'is_granted("CHECKLIST_IS_PROTOTYPE", checklist) or
is_granted("CAMP_IS_PROTOTYPE", checklist) or
is_granted("CAMP_COLLABORATOR", checklist)'
),
],
),
Expand Down
19 changes: 15 additions & 4 deletions api/src/Security/Voter/ChecklistIsPrototypeVoter.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,33 @@
namespace App\Security\Voter;

use App\Entity\Checklist;
use App\Entity\ChecklistItem;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;

/**
* @extends Voter<string,Checklist>
* @extends Voter<string,Checklist|ChecklistItem>
*/
class ChecklistIsPrototypeVoter extends Voter {
public function __construct() {}

protected function supports($attribute, $subject): bool {
return 'CHECKLIST_IS_PROTOTYPE' === $attribute && $subject instanceof Checklist;
return 'CHECKLIST_IS_PROTOTYPE' === $attribute
&& ($subject instanceof Checklist || $subject instanceof ChecklistItem);
}

protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool {
/** @var Checklist $checklist */
$checklist = $subject;
if ($subject instanceof Checklist) {
$checklist = $subject;
}

if ($subject instanceof ChecklistItem) {
$checklist = $subject->checklist;
}

if (!$checklist) {
return false;
}

return $checklist->isPrototype;
}
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/campAdmin/DialogCategoryCreate.vue
Original file line number Diff line number Diff line change
Expand Up @@ -263,12 +263,12 @@ export default {
url = url.substring(window.location.origin.length)
const match = router.matcher.match(url)
if (match.name === 'activity') {
if (match.name === 'camp/activity') {
const scheduleEntry = await this.api
.get()
.scheduleEntries({ id: match.params['scheduleEntryId'] })
return await scheduleEntry.activity()
} else if (match.name === 'admin/activity/category') {
} else if (match.name === 'camp/admin/activity/category') {
return await this.api.get().categories({ id: match.params['categoryId'] })
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
>
<router-link
:to="{
name: 'activity',
name: 'camp/activity',
params: {
campId: camp.id,
scheduleEntryId: scheduleEntry.id,
Expand Down
10 changes: 6 additions & 4 deletions frontend/src/components/checklist/ChecklistCreate.vue
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,12 @@ export default {
}
},
props: {
camp: { type: Object, required: true },
camp: { type: Object, required: false, default: null },
checklistCollection: { type: Object, required: true },
},
data() {
return {
entityProperties: ['camp', 'name', 'copyChecklistSource'],
entityProperties: ['camp', 'name', 'copyChecklistSource', 'isPrototype'],
entityUri: '',
}
},
Expand All @@ -76,9 +77,10 @@ export default {
showDialog: function (showDialog) {
if (showDialog) {
this.setEntityData({
camp: this.camp._meta.self,
camp: this.camp?._meta.self,
name: '',
copyChecklistSource: null,
isPrototype: this.camp ? false : true,
})
} else {
// clear form on exit
Expand All @@ -92,7 +94,7 @@ export default {
methods: {
createChecklist() {
return this.create().then(() => {
this.api.reload(this.camp.checklists())
this.api.reload(this.checklistCollection)
})
},
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,43 +1,34 @@
<template>
<v-container fluid>
<content-card
v-if="checklist"
:key="checklist._meta.self"
class="ec-checklist"
toolbar
back
:title="checklist.name"
>
<template #title-actions>
<ChecklistItemCreate :checklist="checklist" />
</template>
<v-list>
<SortableChecklist :parent="null" :checklist="checklist" />
</v-list>
</content-card>
</v-container>
<content-card
v-if="checklist"
:key="checklist._meta.self"
class="ec-checklist"
toolbar
back
:title="checklist.name"
>
<template #title-actions>
<ChecklistItemCreate :checklist="checklist" />
</template>
<v-list>
<SortableChecklist :parent="null" :checklist="checklist" />
</v-list>
</content-card>
</template>

<script>
import ContentCard from '@/components/layout/ContentCard.vue'
import { campRoleMixin } from '@/mixins/campRoleMixin.js'
import ChecklistItemCreate from '@/components/checklist/ChecklistItemCreate.vue'
import SortableChecklist from '@/components/checklist/SortableChecklist.vue'
export default {
name: 'Category',
name: 'ChecklistDetail',
components: {
SortableChecklist,
ChecklistItemCreate,
ContentCard,
},
mixins: [campRoleMixin],
props: {
camp: {
type: Object,
default: null,
required: false,
},
checklist: {
type: Object,
default: null,
Expand Down
55 changes: 55 additions & 0 deletions frontend/src/components/checklist/ChecklistOverview.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<template>
<content-card :title="$tc('entity.checklist.name', 2)" toolbar>
<template #title-actions>
<ChecklistCreate :camp="camp" :checklist-collection="checklistCollection" />
</template>
<v-card-text>
<v-list class="mx-n2 py-0">
<v-list-item
v-for="checklist in checklists"
:key="checklist._meta.self"
:to="checklistRoute(camp, checklist)"
class="px-2 rounded"
>
<v-list-item-content>
<v-list-item-title>
{{ checklist.name }}
</v-list-item-title>
</v-list-item-content>

<v-list-item-action style="display: inline">
<v-item-group>
<ButtonEdit color="primary--text" text class="my-n1 v-btn--has-bg" />
</v-item-group>
</v-list-item-action>
</v-list-item>
</v-list>
</v-card-text>
</content-card>
</template>

<script>
import ContentCard from '@/components/layout/ContentCard.vue'
import ChecklistCreate from '@/components/checklist/ChecklistCreate.vue'
import { checklistRoute } from '@/router.js'
import ButtonEdit from '@/components/buttons/ButtonEdit.vue'
export default {
name: 'ChecklistOverview',
components: {
ButtonEdit,
ChecklistCreate,
ContentCard,
},
props: {
camp: { type: Object, required: false, default: null },
checklistCollection: { type: Object, required: true },
},
computed: {
checklists() {
return this.checklistCollection.items
},
},
methods: { checklistRoute },
}
</script>
2 changes: 1 addition & 1 deletion frontend/src/components/dashboard/ActivityRow.vue
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ export default {
},
routerLink() {
return {
name: 'activity',
name: 'camp/activity',
params: {
campId: this.scheduleEntry.period().camp().id,
scheduleEntryId: this.scheduleEntry.id,
Expand Down
16 changes: 16 additions & 0 deletions frontend/src/components/navigation/UserMeta.vue
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,17 @@
<v-badge inline bordered color="#f00" :content="invitationCount" />
</v-list-item-action-text>
</v-list-item>
<v-list-item
v-if="isAdmin"
block
tag="li"
exact
:to="{ name: 'admin/debug' }"
@click="open = false"
>
<v-icon left>mdi-coffee</v-icon>
<span>{{ $tc('components.navigation.userMeta.admin') }}</span>
</v-list-item>
<v-list-item
v-if="!$vuetify.breakpoint.lgAndUp"
block
Expand Down Expand Up @@ -134,6 +145,7 @@
import UserAvatar from '../user/UserAvatar.vue'
import { mapGetters } from 'vuex'
import { getEnv } from '@/environment.js'
import { isAdmin } from '@/plugins/auth'
export default {
name: 'UserMeta',
Expand All @@ -158,6 +170,7 @@ export default {
return {
open: false,
logoutInProgress: false,
isAdmin: false,
}
},
computed: {
Expand All @@ -182,6 +195,9 @@ export default {
)
},
},
mounted() {
this.isAdmin = isAdmin()
},
methods: {
async logout() {
this.logoutInProgress = true
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/program/DialogActivityCreate.vue
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ export default {
url = url.substring(window.location.origin.length)
const match = router.matcher.match(url)
if (match.name === 'activity') {
if (match.name === 'camp/activity') {
const scheduleEntry = await this.api
.get()
.scheduleEntries({ id: match.params['scheduleEntryId'] })
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/story/StoryDay.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
</span>
<router-link
:to="{
name: 'activity',
name: 'camp/activity',
params: {
campId: day.period().camp().id,
scheduleEntryId: scheduleEntry.id,
Expand Down
Loading

0 comments on commit b91075b

Please sign in to comment.