diff --git a/api/src/Doctrine/Filter/ContentNodeCampFilter.php b/api/src/Doctrine/Filter/ContentNodeCampFilter.php new file mode 100644 index 0000000000..552eaa49c5 --- /dev/null +++ b/api/src/Doctrine/Filter/ContentNodeCampFilter.php @@ -0,0 +1,85 @@ + self::CAMP_QUERY_NAME, + 'type' => Type::BUILTIN_TYPE_STRING, + 'required' => false, + ]; + + return $description; + } + + protected function filterProperty( + string $property, + $value, + QueryBuilder $queryBuilder, + QueryNameGeneratorInterface $queryNameGenerator, + string $resourceClass, + ?Operation $operation = null, + array $context = [] + ): void { + if (ContentNode::class !== $resourceClass && !is_subclass_of($resourceClass, ContentNode::class)) { + throw new \Exception("ContentNodeCampFilter can only be applied to entities of type ContentNode (received: {$resourceClass})."); + } + + if (self::CAMP_QUERY_NAME !== $property) { + return; + } + + // load camp from query parameter value + $camp = $this->iriConverter->getResourceFromIri($value); + + // generate alias to avoid interference with other filters + $campParameterName = $queryNameGenerator->generateParameterName($property); + $periodJoinAlias = $queryNameGenerator->generateJoinAlias('period'); + $activityJoinAlias = $queryNameGenerator->generateJoinAlias('activity'); + $scheduleEntryJoinAlias = $queryNameGenerator->generateJoinAlias('scheduleEntry'); + + $rootAlias = $queryBuilder->getRootAliases()[0]; + + $rootQry = $queryBuilder->getEntityManager()->createQueryBuilder(); + $rootQry + ->select("identity({$activityJoinAlias}.rootContentNode)") + ->from(Activity::class, $activityJoinAlias) + ->join("{$activityJoinAlias}.scheduleEntries", $scheduleEntryJoinAlias) + ->join("{$scheduleEntryJoinAlias}.period", $periodJoinAlias) + ->where($queryBuilder->expr()->eq("{$periodJoinAlias}.camp", ":{$campParameterName}")) + ; + + $queryBuilder->andWhere($queryBuilder->expr()->in("{$rootAlias}.root", $rootQry->getDQL())); + $queryBuilder->setParameter($campParameterName, $camp); + } +} diff --git a/api/src/Doctrine/Filter/ContentNodePeriodFilter.php b/api/src/Doctrine/Filter/ContentNodePeriodFilter.php index b9f42f4155..3786f99323 100644 --- a/api/src/Doctrine/Filter/ContentNodePeriodFilter.php +++ b/api/src/Doctrine/Filter/ContentNodePeriodFilter.php @@ -52,7 +52,7 @@ protected function filterProperty( ?Operation $operation = null, array $context = [] ): void { - if (ContentNode::class !== $resourceClass) { + if (ContentNode::class !== $resourceClass && !is_subclass_of($resourceClass, ContentNode::class)) { throw new \Exception("ContentNodePeriodFilter can only be applied to entities of type ContentNode (received: {$resourceClass})."); } diff --git a/api/src/Entity/ChecklistItem.php b/api/src/Entity/ChecklistItem.php index 08bbd0165e..a08a45e681 100644 --- a/api/src/Entity/ChecklistItem.php +++ b/api/src/Entity/ChecklistItem.php @@ -122,6 +122,8 @@ class ChecklistItem extends BaseEntity implements BelongsToCampInterface, CopyFr /** * All ChecklistNodes that have selected this ChecklistItem. */ + #[ApiProperty(example: '["/checklist_items/1a2b3c4d"]')] + #[Groups(['read'])] #[Assert\Count( exactly: 0, exactMessage: 'It\'s not possible to delete a checklist item as long as checklist nodes are referencing it.', diff --git a/api/src/Entity/ContentNode.php b/api/src/Entity/ContentNode.php index 6130076daa..736104c7d0 100644 --- a/api/src/Entity/ContentNode.php +++ b/api/src/Entity/ContentNode.php @@ -7,6 +7,7 @@ use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\GetCollection; +use App\Doctrine\Filter\ContentNodeCampFilter; use App\Doctrine\Filter\ContentNodePeriodFilter; use App\Entity\ContentNode\ColumnLayout; use App\InputFilter; @@ -44,6 +45,7 @@ order: ['root.id', 'parent.id', 'slot', 'position'] )] #[ApiFilter(filterClass: SearchFilter::class, properties: ['contentType', 'root'])] +#[ApiFilter(filterClass: ContentNodeCampFilter::class)] #[ApiFilter(filterClass: ContentNodePeriodFilter::class)] #[ORM\Entity(repositoryClass: ContentNodeRepository::class)] #[ORM\InheritanceType('SINGLE_TABLE')] diff --git a/api/src/Entity/ContentNode/ChecklistNode.php b/api/src/Entity/ContentNode/ChecklistNode.php index f2ea286b6c..e7f3a8d58e 100644 --- a/api/src/Entity/ContentNode/ChecklistNode.php +++ b/api/src/Entity/ContentNode/ChecklistNode.php @@ -9,7 +9,6 @@ use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\Patch; use ApiPlatform\Metadata\Post; -use App\Entity\Checklist; use App\Entity\ChecklistItem; use App\Entity\ContentNode; use App\Repository\ChecklistNodeRepository; diff --git a/api/src/Entity/ContentType.php b/api/src/Entity/ContentType.php index 82be444e94..b54e625b53 100644 --- a/api/src/Entity/ContentType.php +++ b/api/src/Entity/ContentType.php @@ -26,7 +26,7 @@ normalizationContext: ['groups' => ['read']], order: ['name' => 'ASC'] )] -#[ApiFilter(filterClass: SearchFilter::class, properties: ['categories'])] +#[ApiFilter(filterClass: SearchFilter::class, properties: ['name', 'categories'])] #[ORM\Entity] class ContentType extends BaseEntity { /** diff --git a/api/tests/Api/ChecklistItems/CreateChecklistItemTest.php b/api/tests/Api/ChecklistItems/CreateChecklistItemTest.php index 484ce6f84e..37796a451a 100644 --- a/api/tests/Api/ChecklistItems/CreateChecklistItemTest.php +++ b/api/tests/Api/ChecklistItems/CreateChecklistItemTest.php @@ -275,7 +275,7 @@ public function getExampleReadPayload($attributes = [], $except = []) { ChecklistItem::class, Get::class, $attributes, - ['parent', 'checklist'], + ['parent', 'checklist', 'checklistNodes'], $except ); } diff --git a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set checklist_items__1.json b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set checklist_items__1.json index 8d60707fb2..cbfeee7a1a 100644 --- a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set checklist_items__1.json +++ b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetCollectionMatchesStructure with data set checklist_items__1.json @@ -6,6 +6,7 @@ "checklist": { "href": "escaped_value" }, + "checklistNodes": [], "children": [], "parent": "escaped_value", "self": { @@ -21,6 +22,7 @@ "checklist": { "href": "escaped_value" }, + "checklistNodes": [], "children": [], "parent": "escaped_value", "self": { @@ -36,8 +38,11 @@ "checklist": { "href": "escaped_value" }, + "checklistNodes": [], "children": [], - "parent": "escaped_value", + "parent": { + "href": "escaped_value" + }, "self": { "href": "escaped_value" } @@ -51,10 +56,13 @@ "checklist": { "href": "escaped_value" }, - "children": [], - "parent": { - "href": "escaped_value" - }, + "checklistNodes": [], + "children": [ + { + "href": "escaped_value" + } + ], + "parent": "escaped_value", "self": { "href": "escaped_value" } @@ -68,12 +76,15 @@ "checklist": { "href": "escaped_value" }, + "checklistNodes": [], "children": [ { "href": "escaped_value" } ], - "parent": "escaped_value", + "parent": { + "href": "escaped_value" + }, "self": { "href": "escaped_value" } @@ -87,14 +98,13 @@ "checklist": { "href": "escaped_value" }, - "children": [ + "checklistNodes": [ { "href": "escaped_value" } ], - "parent": { - "href": "escaped_value" - }, + "children": [], + "parent": "escaped_value", "self": { "href": "escaped_value" } diff --git a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetItemMatchesStructure with data set checklist_items__1.json b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetItemMatchesStructure with data set checklist_items__1.json index 8a41227692..24fc1459c1 100644 --- a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetItemMatchesStructure with data set checklist_items__1.json +++ b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testGetItemMatchesStructure with data set checklist_items__1.json @@ -3,6 +3,11 @@ "checklist": { "href": "escaped_value" }, + "checklistNodes": [ + { + "href": "escaped_value" + } + ], "children": [], "parent": "escaped_value", "self": { diff --git a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testOpenApiSpecMatchesSnapshot__1.yml b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testOpenApiSpecMatchesSnapshot__1.yml index af498183ce..27900e3b79 100644 --- a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testOpenApiSpecMatchesSnapshot__1.yml +++ b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testOpenApiSpecMatchesSnapshot__1.yml @@ -7925,6 +7925,14 @@ components: example: /checklists/1a2b3c4d format: iri-reference type: string + checklistNodes: + description: 'All ChecklistNodes that have selected this ChecklistItem.' + example: '["/checklist_items/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + type: array children: description: 'All ChecklistItems that are direct children of this ChecklistItem.' example: '["/checklist_items/1a2b3c4d"]' @@ -7962,6 +7970,7 @@ components: type: string required: - checklist + - checklistNodes - children - position - text @@ -8068,12 +8077,15 @@ components: properties: checklist: properties: { data: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object } } + checklistNodes: + properties: { data: { items: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object }, type: array } } children: properties: { data: { items: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object }, type: array } } parent: properties: { data: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object } } required: - checklist + - checklistNodes - children type: object type: @@ -8094,6 +8106,8 @@ components: $ref: '#/components/schemas/ChecklistItem.jsonapi' - $ref: '#/components/schemas/ChecklistItem.jsonapi' + - + $ref: '#/components/schemas/ChecklistItem.jsonapi' readOnly: true type: array type: object @@ -8117,6 +8131,14 @@ components: example: /checklists/1a2b3c4d format: iri-reference type: string + checklistNodes: + description: 'All ChecklistNodes that have selected this ChecklistItem.' + example: '["/checklist_items/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + type: array children: description: 'All ChecklistItems that are direct children of this ChecklistItem.' example: '["/checklist_items/1a2b3c4d"]' @@ -8154,6 +8176,7 @@ components: type: string required: - checklist + - checklistNodes - children - position - text @@ -8237,6 +8260,14 @@ components: example: /checklists/1a2b3c4d format: iri-reference type: string + checklistNodes: + description: 'All ChecklistNodes that have selected this ChecklistItem.' + example: '["/checklist_items/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + type: array children: description: 'All ChecklistItems that are direct children of this ChecklistItem.' example: '["/checklist_items/1a2b3c4d"]' @@ -8274,6 +8305,7 @@ components: type: string required: - checklist + - checklistNodes - children - position - text @@ -25478,6 +25510,18 @@ paths: description: 'Retrieves the collection of ChecklistNode resources.' operationId: api_content_nodechecklist_nodes_get_collection parameters: + - + allowEmptyValue: true + allowReserved: false + deprecated: false + description: '' + explode: false + in: query + name: camp + required: false + schema: + type: string + style: form - allowEmptyValue: true allowReserved: false @@ -25765,6 +25809,18 @@ paths: description: 'Retrieves the collection of ColumnLayout resources.' operationId: api_content_nodecolumn_layouts_get_collection parameters: + - + allowEmptyValue: true + allowReserved: false + deprecated: false + description: '' + explode: false + in: query + name: camp + required: false + schema: + type: string + style: form - allowEmptyValue: true allowReserved: false @@ -26052,6 +26108,18 @@ paths: description: 'Retrieves the collection of MaterialNode resources.' operationId: api_content_nodematerial_nodes_get_collection parameters: + - + allowEmptyValue: true + allowReserved: false + deprecated: false + description: '' + explode: false + in: query + name: camp + required: false + schema: + type: string + style: form - allowEmptyValue: true allowReserved: false @@ -26339,6 +26407,18 @@ paths: description: 'Retrieves the collection of MultiSelect resources.' operationId: api_content_nodemulti_selects_get_collection parameters: + - + allowEmptyValue: true + allowReserved: false + deprecated: false + description: '' + explode: false + in: query + name: camp + required: false + schema: + type: string + style: form - allowEmptyValue: true allowReserved: false @@ -26626,6 +26706,18 @@ paths: description: 'Retrieves the collection of ResponsiveLayout resources.' operationId: api_content_noderesponsive_layouts_get_collection parameters: + - + allowEmptyValue: true + allowReserved: false + deprecated: false + description: '' + explode: false + in: query + name: camp + required: false + schema: + type: string + style: form - allowEmptyValue: true allowReserved: false @@ -26913,6 +27005,18 @@ paths: description: 'Retrieves the collection of SingleText resources.' operationId: api_content_nodesingle_texts_get_collection parameters: + - + allowEmptyValue: true + allowReserved: false + deprecated: false + description: '' + explode: false + in: query + name: camp + required: false + schema: + type: string + style: form - allowEmptyValue: true allowReserved: false @@ -27200,6 +27304,18 @@ paths: description: 'Retrieves the collection of Storyboard resources.' operationId: api_content_nodestoryboards_get_collection parameters: + - + allowEmptyValue: true + allowReserved: false + deprecated: false + description: '' + explode: false + in: query + name: camp + required: false + schema: + type: string + style: form - allowEmptyValue: true allowReserved: false @@ -27487,6 +27603,18 @@ paths: description: 'Retrieves the collection of ContentNode resources.' operationId: api_content_nodes_get_collection parameters: + - + allowEmptyValue: true + allowReserved: false + deprecated: false + description: '' + explode: false + in: query + name: camp + required: false + schema: + type: string + style: form - allowEmptyValue: true allowReserved: false @@ -27612,6 +27740,18 @@ paths: schema: type: string style: form + - + allowEmptyValue: true + allowReserved: false + deprecated: false + description: '' + explode: false + in: query + name: name + required: false + schema: + type: string + style: form - allowEmptyValue: true allowReserved: false @@ -27626,6 +27766,20 @@ paths: type: string type: array style: form + - + allowEmptyValue: true + allowReserved: false + deprecated: false + description: '' + explode: true + in: query + name: 'name[]' + required: false + schema: + items: + type: string + type: array + style: form responses: 200: content: diff --git a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testRootEndpointMatchesSnapshot__1.json b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testRootEndpointMatchesSnapshot__1.json index 2c2b7a3616..29e9562464 100644 --- a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testRootEndpointMatchesSnapshot__1.json +++ b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testRootEndpointMatchesSnapshot__1.json @@ -29,7 +29,7 @@ "templated": true }, "checklistNodes": { - "href": "\/content_node\/checklist_nodes{\/id}{?contentType,contentType[],root,root[],period}", + "href": "\/content_node\/checklist_nodes{\/id}{?contentType,contentType[],root,root[],camp,period}", "templated": true }, "checklists": { @@ -37,15 +37,15 @@ "templated": true }, "columnLayouts": { - "href": "\/content_node\/column_layouts{\/id}{?contentType,contentType[],root,root[],period}", + "href": "\/content_node\/column_layouts{\/id}{?contentType,contentType[],root,root[],camp,period}", "templated": true }, "contentNodes": { - "href": "\/content_nodes{?contentType,contentType[],root,root[],period}", + "href": "\/content_nodes{?contentType,contentType[],root,root[],camp,period}", "templated": true }, "contentTypes": { - "href": "\/content_types{\/id}{?categories,categories[]}", + "href": "\/content_types{\/id}{?name,name[],categories,categories[]}", "templated": true }, "dayResponsibles": { @@ -72,11 +72,11 @@ "templated": true }, "materialNodes": { - "href": "\/content_node\/material_nodes{\/id}{?contentType,contentType[],root,root[],period}", + "href": "\/content_node\/material_nodes{\/id}{?contentType,contentType[],root,root[],camp,period}", "templated": true }, "multiSelects": { - "href": "\/content_node\/multi_selects{\/id}{?contentType,contentType[],root,root[],period}", + "href": "\/content_node\/multi_selects{\/id}{?contentType,contentType[],root,root[],camp,period}", "templated": true }, "oauthCevidb": { @@ -116,7 +116,7 @@ "templated": true }, "responsiveLayouts": { - "href": "\/content_node\/responsive_layouts{\/id}{?contentType,contentType[],root,root[],period}", + "href": "\/content_node\/responsive_layouts{\/id}{?contentType,contentType[],root,root[],camp,period}", "templated": true }, "scheduleEntries": { @@ -127,11 +127,11 @@ "href": "\/" }, "singleTexts": { - "href": "\/content_node\/single_texts{\/id}{?contentType,contentType[],root,root[],period}", + "href": "\/content_node\/single_texts{\/id}{?contentType,contentType[],root,root[],camp,period}", "templated": true }, "storyboards": { - "href": "\/content_node\/storyboards{\/id}{?contentType,contentType[],root,root[],period}", + "href": "\/content_node\/storyboards{\/id}{?contentType,contentType[],root,root[],camp,period}", "templated": true }, "users": { diff --git a/frontend/src/components/checklist/ChecklistItemTree.vue b/frontend/src/components/checklist/ChecklistItemTree.vue new file mode 100644 index 0000000000..5a27e5038c --- /dev/null +++ b/frontend/src/components/checklist/ChecklistItemTree.vue @@ -0,0 +1,161 @@ + + + + + diff --git a/frontend/src/locales/de.json b/frontend/src/locales/de.json index ceb229f1ec..afc69528af 100644 --- a/frontend/src/locales/de.json +++ b/frontend/src/locales/de.json @@ -664,6 +664,9 @@ "reminderLockedMove": "Ziehen zum Verschieben ist nur im entsperrten Modus möglich.", "title": "Grobprogramm" }, + "checklist": { + "title": "Checklist-Übersicht" + }, "dashboard": { "activities": "Aktivitäten", "columns": { @@ -707,6 +710,7 @@ "navTopbar": { "admin": "Admin", "campIsLoading": "Lager wird geladen", + "checklist": "Checklist", "material": "Material", "print": "Drucken", "program": "Programm", diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index fc0b43ba26..d82891fdbf 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -705,6 +705,7 @@ "desktop": { "navTopbar": { "admin": "Admin", + "checklist": "Checklist", "campIsLoading": "Camp is loading", "material": "Materials", "print": "Print", diff --git a/frontend/src/router.js b/frontend/src/router.js index cdfaa88e2d..0bc45d6df2 100644 --- a/frontend/src/router.js +++ b/frontend/src/router.js @@ -292,6 +292,11 @@ export default new Router({ redirectToPeriod(to, from, next, 'camp/period/program') }, }, + { + path: 'checklist', + name: 'camp/checklist', + component: () => import('./views/camp/Checklist.vue'), + }, { path: 'story/period/:periodId/:periodTitle?', name: 'camp/period/story', @@ -711,6 +716,7 @@ export function checklistFromRoute(route) { function getContentLayout(route) { switch (route.name) { + case 'camp/checklist': case 'camp/period/program': case 'camp/admin/print': case 'camp/admin/activity/category': diff --git a/frontend/src/views/camp/Checklist.vue b/frontend/src/views/camp/Checklist.vue new file mode 100644 index 0000000000..a23d6c29ba --- /dev/null +++ b/frontend/src/views/camp/Checklist.vue @@ -0,0 +1,74 @@ + + + diff --git a/frontend/src/views/camp/navigation/desktop/NavTopbar.vue b/frontend/src/views/camp/navigation/desktop/NavTopbar.vue index b0d5737d47..9f798eb852 100644 --- a/frontend/src/views/camp/navigation/desktop/NavTopbar.vue +++ b/frontend/src/views/camp/navigation/desktop/NavTopbar.vue @@ -13,6 +13,12 @@ $tc('views.camp.navigation.desktop.navTopbar.program') }} + + mdi-clipboard-list-outline + {{ + $tc('views.camp.navigation.desktop.navTopbar.checklist') + }} + mdi-book-open-variant {{ @@ -68,6 +74,9 @@ export default { } }, computed: { + hasChecklist() { + return this.camp.checklists().items.length > 0 + }, helpLink() { return getEnv().HELP_LINK },