diff --git a/aleksis/core/frontend/collections.js b/aleksis/core/frontend/collections.js index b25b6e2350efcbe3a621c950fda632e8f875ea9d..98cf666d98980464b8bd66393b62db26cb73025c 100644 --- a/aleksis/core/frontend/collections.js +++ b/aleksis/core/frontend/collections.js @@ -13,6 +13,22 @@ export const collections = [ }, ], }, + { + name: "groupActions", + type: Object, + }, + { + name: "personWidgets", + type: Object, + }, ]; -export const collectionItems = {}; +export const collectionItems = { + coreGroupActions: [ + { + key: "core-delete-group-action", + component: () => import("./components/group/actions/DeleteGroup.vue"), + isActive: (group) => group.canDelete || false, + }, + ], +}; diff --git a/aleksis/core/frontend/components/generic/ButtonMenu.vue b/aleksis/core/frontend/components/generic/ButtonMenu.vue index 431407d27ff78654c13aa548d27132dd74b5c0dd..f1ef728503695747b278003d85e405a76787a65d 100644 --- a/aleksis/core/frontend/components/generic/ButtonMenu.vue +++ b/aleksis/core/frontend/components/generic/ButtonMenu.vue @@ -1,5 +1,9 @@ <template> - <v-menu transition="slide-y-transition" offset-y> + <v-menu + transition="slide-y-transition" + offset-y + :close-on-content-click="closeOnContentClick" + > <template #activator="{ on, attrs }"> <slot name="activator" v-bind="{ on, attrs }"> <v-btn outlined text v-bind="attrs" v-on="on"> @@ -31,6 +35,11 @@ export default { required: false, default: "", }, + closeOnContentClick: { + type: Boolean, + required: false, + default: true, + }, }, }; </script> diff --git a/aleksis/core/frontend/components/generic/InlineCRUDList.vue b/aleksis/core/frontend/components/generic/InlineCRUDList.vue index 3341bfb546ba63d65a3f64996099f4c8e0f06760..24a4af0fc0e50cab24ce2d2ad0e81dd428845cb6 100644 --- a/aleksis/core/frontend/components/generic/InlineCRUDList.vue +++ b/aleksis/core/frontend/components/generic/InlineCRUDList.vue @@ -55,6 +55,10 @@ <slot name="additionalActions" /> </template> + <template #actions="actions"> + <slot name="actions" v-bind="actions" /> + </template> + <!-- customizable headers --> <template v-for="(_header, idx) in $attrs.headers" diff --git a/aleksis/core/frontend/components/generic/chips/CounterChip.vue b/aleksis/core/frontend/components/generic/chips/CounterChip.vue index 04ae1b4766781e9f8a108f95373a4622ad51fa54..c7e39eb26678e564b44f6639063ad4c0f8658afb 100644 --- a/aleksis/core/frontend/components/generic/chips/CounterChip.vue +++ b/aleksis/core/frontend/components/generic/chips/CounterChip.vue @@ -1,7 +1,7 @@ <template> <v-chip v-bind="$attrs" v-on="$listeners"> <v-avatar :left="!onlyShowCount" v-if="count !== null"> - {{ count }} + {{ $n(count) }} </v-avatar> <slot v-if="!onlyShowCount" /> </v-chip> diff --git a/aleksis/core/frontend/components/group/GroupActions.vue b/aleksis/core/frontend/components/group/GroupActions.vue index 72d21f44bd1b1b049608cad2fade2fd0a5381170..b9f5d606999822f4fbd4bde66dfcb7a0de18de11 100644 --- a/aleksis/core/frontend/components/group/GroupActions.vue +++ b/aleksis/core/frontend/components/group/GroupActions.vue @@ -6,52 +6,32 @@ :to="{ name: 'core.editGroup', params: { id: group.id } }" /> - <delete-button - v-if="group.canDelete" - @click="showDeleteConfirm = true" - outlined - text - color="error" - /> - - <delete-dialog - v-model="showDeleteConfirm" - :gql-delete-mutation="deleteMutation" - item-attribute="name" - :items="[group]" - @save=" - $router.push({ - name: 'core.groups', - }) - " - > - <template #title> - {{ $t("group.confirm_delete") }} - </template> - </delete-dialog> + <button-menu :close-on-content-click="false" v-if="actions.length"> + <component + :is="action.component" + v-for="action in actions" + :key="action.key" + :group="group" + /> + </button-menu> </div> </template> <script> -import { deleteGroups } from "./groups.graphql"; -import DeleteDialog from "../generic/dialogs/DeleteDialog.vue"; -import DeleteButton from "../generic/buttons/DeleteButton.vue"; import EditButton from "../generic/buttons/EditButton.vue"; +import { collections } from "aleksisAppImporter"; +import groupActionsMixin from "./actions/groupActionsMixin"; export default { name: "GroupActions", - components: { EditButton, DeleteButton, DeleteDialog }, - props: { - group: { - type: Object, - required: true, + components: { EditButton }, + mixins: [groupActionsMixin], + computed: { + actions() { + return collections.coreGroupActions.items.filter((action) => + action.isActive.call(this, this.group), + ); }, }, - data() { - return { - showDeleteConfirm: false, - deleteMutation: deleteGroups, - }; - }, }; </script> diff --git a/aleksis/core/frontend/components/group/actions/DeleteGroup.vue b/aleksis/core/frontend/components/group/actions/DeleteGroup.vue new file mode 100644 index 0000000000000000000000000000000000000000..dc18a0a001b7956cf0050f84be783b796e055ab9 --- /dev/null +++ b/aleksis/core/frontend/components/group/actions/DeleteGroup.vue @@ -0,0 +1,48 @@ +<script> +import { deleteGroups } from "../groups.graphql"; +import DeleteDialog from "../../generic/dialogs/DeleteDialog.vue"; +import groupActionsMixin from "./groupActionsMixin"; + +export default { + name: "DeleteGroup", + components: { DeleteDialog }, + mixins: [groupActionsMixin], + data() { + return { + showDeleteConfirm: false, + deleteMutation: deleteGroups, + }; + }, +}; +</script> + +<template> + <v-list-item @click="showDeleteConfirm = true" class="error--text"> + <v-list-item-icon> + <v-icon color="error">$deleteContent</v-icon> + </v-list-item-icon> + <v-list-item-content> + <v-list-item-title> + {{ $t("actions.delete") }} + </v-list-item-title> + </v-list-item-content> + + <delete-dialog + v-model="showDeleteConfirm" + :gql-delete-mutation="deleteMutation" + item-attribute="name" + :items="[group]" + @save=" + $router.push({ + name: 'core.groups', + }) + " + > + <template #title> + {{ $t("group.confirm_delete") }} + </template> + </delete-dialog> + </v-list-item> +</template> + +<style scoped></style> diff --git a/aleksis/core/frontend/components/group/actions/groupActionsMixin.js b/aleksis/core/frontend/components/group/actions/groupActionsMixin.js new file mode 100644 index 0000000000000000000000000000000000000000..831d2514ff93792f7b005526042c1007fec10987 --- /dev/null +++ b/aleksis/core/frontend/components/group/actions/groupActionsMixin.js @@ -0,0 +1,8 @@ +export default { + props: { + group: { + type: Object, + required: true, + }, + }, +}; diff --git a/aleksis/core/frontend/components/person/PersonOverview.vue b/aleksis/core/frontend/components/person/PersonOverview.vue index 90933bf625804e9ab0a6811d843f15e451d392f5..ac23625d38dcd34af93011bf6365721b3bf8df51 100644 --- a/aleksis/core/frontend/components/person/PersonOverview.vue +++ b/aleksis/core/frontend/components/person/PersonOverview.vue @@ -228,15 +228,56 @@ lg="4" v-if="person.memberOf.length || person.ownerOf.length" > - <v-card v-if="person.memberOf.length" class="mb-6"> + <v-card> <v-card-title>{{ $t("group.title_plural") }}</v-card-title> - <group-collection :groups="person.memberOf" /> - </v-card> - <v-card v-if="person.ownerOf.length"> - <v-card-title>{{ $t("group.ownership") }}</v-card-title> - <group-collection :groups="person.ownerOf" /> + <v-list-group + :disabled="person.memberOf.length === 0" + :append-icon="person.memberOf.length === 0 ? null : undefined" + > + <template #activator> + <v-list-item-icon> + <v-icon>mdi-account-group-outline</v-icon> + </v-list-item-icon> + <v-list-item-title>{{ + $tc("group.member_of_n", person.memberOf.length) + }}</v-list-item-title> + </template> + <group-collection :groups="person.memberOf" dense /> + </v-list-group> + <v-list-group + :disabled="person.ownerOf.length === 0" + :append-icon="person.ownerOf.length === 0 ? null : undefined" + > + <template #activator> + <v-list-item-icon> + <v-icon>mdi-account-tie-hat-outline</v-icon> + </v-list-item-icon> + <v-list-item-title>{{ + $tc("group.owner_of_n", person.ownerOf.length) + }}</v-list-item-title> + </template> + <group-collection :groups="person.ownerOf" dense /> + </v-list-group> </v-card> </v-col> + + <template v-for="widget in widgets"> + <v-col + v-if="widget.shouldDisplay(person, currentSchoolTerm)" + v-bind="widget.colProps" + :key="widget.key" + > + <!-- Props defined in aleksis/core/frontend/mixins/personOverviewCardMixin.js --> + <component + :is="widget.component" + :person="person" + :school-term="currentSchoolTerm" + :maximized="widgetSlug === widget.key" + @maximize="maximizeWidget(widget.key)" + @minimize="minimizeWidgets()" + /> + </v-col> + </template> </v-row> </detail-view> </template> @@ -251,8 +292,11 @@ import PersonActions from "./PersonActions.vue"; import PersonAvatarClickbox from "./PersonAvatarClickbox.vue"; import PersonCollection from "./PersonCollection.vue"; +import gqlCurrentSchoolTerm from "../school_term/currentSchoolTerm.graphql"; import gqlPersonOverview from "./personOverview.graphql"; +import { collections } from "aleksisAppImporter"; + export default { name: "PersonOverview", components: { @@ -263,9 +307,15 @@ export default { PersonAvatarClickbox, PersonCollection, }, + apollo: { + currentSchoolTerm: { + query: gqlCurrentSchoolTerm, + }, + }, data() { return { query: gqlPersonOverview, + currentSchoolTerm: null, }; }, props: { @@ -274,6 +324,46 @@ export default { required: false, default: null, }, + widgetSlug: { + type: String, + required: false, + default: "default", + }, + }, + methods: { + maximizeWidget(slug) { + if (this.widgetSlug !== slug) { + if (this.id) { + this.$router.push({ + name: "core.personByIdWithSlug", + params: { id: this.id, widgetSlug: slug }, + }); + } else { + this.$router.push({ + name: "core.personWithSlug", + params: { widgetSlug: slug }, + }); + } + } + }, + minimizeWidgets() { + if (this.id) { + this.$router.push({ + name: "core.personByIdWithSlug", + params: { id: this.id, widgetSlug: "default" }, + }); + } else { + this.$router.push({ + name: "core.personWithSlug", + params: { widgetSlug: "default" }, + }); + } + }, + }, + computed: { + widgets() { + return collections.corePersonWidgets.items; + }, }, }; </script> diff --git a/aleksis/core/frontend/components/school_term/currentSchoolTerm.graphql b/aleksis/core/frontend/components/school_term/currentSchoolTerm.graphql new file mode 100644 index 0000000000000000000000000000000000000000..82a7741f54770fc1a7edb7d6ce5ebcf74ffbb1f5 --- /dev/null +++ b/aleksis/core/frontend/components/school_term/currentSchoolTerm.graphql @@ -0,0 +1,10 @@ +query currentSchoolTerm { + currentSchoolTerm { + id + name + dateStart + dateEnd + canEdit + canDelete + } +} diff --git a/aleksis/core/frontend/messages/de.json b/aleksis/core/frontend/messages/de.json index 2b65e8f31ea122a0821dbc994c90a30e6662b84c..475c0ac5a0e05f7d27485b758f5a035bd51fdec7 100644 --- a/aleksis/core/frontend/messages/de.json +++ b/aleksis/core/frontend/messages/de.json @@ -246,6 +246,8 @@ "ownership": "Gruppen-Eigentümerschaft", "parent_groups": "Übergeordnete Gruppen", "parent_groups_n": "Keine übergeordneten Gruppen | {n} übergeordnete Gruppe | {n} übergeordnete Gruppen", + "member_of_n": "Keine Gruppenmitgliedschaften | Mitglied in einer Gruppe | Mitglied in {n} Gruppen", + "owner_of_n": "Keine Gruppeneigentümerschaften | Besitzt eine Gruppe | Besitzt {n} Gruppen", "properties": "Eigenschaften", "short_name": "Kurzname", "statistics": { diff --git a/aleksis/core/frontend/messages/en.json b/aleksis/core/frontend/messages/en.json index 6b581d999247c077890c5eb95d5590ceb6c7254e..bfbcb76074e92a1a14e11e02a7b6ea3bdbe814b0 100644 --- a/aleksis/core/frontend/messages/en.json +++ b/aleksis/core/frontend/messages/en.json @@ -198,6 +198,8 @@ "child_groups_n": "No Child Groups | {n} Child Group | {n} Child Groups", "parent_groups": "Parent Groups", "parent_groups_n": "No Parent Groups | {n} Parent Group | {n} Parent Groups", + "member_of_n": "Not member in any group | Member of {n} group | Member of {n} groups", + "owner_of_n": "Not owner of any group | Owner of {n} group | Owner of {n} groups", "confirm_delete": "Do you really want to delete this group?", "statistics": { "title": "Statistics", diff --git a/aleksis/core/frontend/mixins/personOverviewCardMixin.js b/aleksis/core/frontend/mixins/personOverviewCardMixin.js index 1e00f2c5eaa027a9f29b48a62090d70089901e79..7da808c34ac839d8e2ce7170d960b957019d2a61 100644 --- a/aleksis/core/frontend/mixins/personOverviewCardMixin.js +++ b/aleksis/core/frontend/mixins/personOverviewCardMixin.js @@ -18,5 +18,19 @@ export default { required: false, default: null, }, + /** + * Whether the current widget is maximized + */ + maximized: { + type: Boolean, + required: false, + default: false, + }, + emits: [ + // When this is fired, the component can assume that the `maximized` prop will soon turn true + "maximize", + // Use this to signify a wanted closure + "minimize", + ], }, }; diff --git a/aleksis/core/frontend/routes.js b/aleksis/core/frontend/routes.js index d4efe17320770bfef7d469614a97e46807069d80..0a8b2528d3c8a4f1b4786986a7adb0d3bf970f16 100644 --- a/aleksis/core/frontend/routes.js +++ b/aleksis/core/frontend/routes.js @@ -157,6 +157,15 @@ const routes = [ }, name: "core.invitePerson", }, + { + path: "/persons/:id(\\d+)/:widgetSlug([^\\s!?\\/*#|]+)", + component: () => import("./components/person/PersonOverview.vue"), + props: true, + name: "core.personByIdWithSlug", + meta: { + titleKey: "person.page_title", + }, + }, { path: "/groups", component: () => import("./components/LegacyBaseTemplate.vue"), @@ -623,9 +632,6 @@ const routes = [ { path: "/person/", component: () => import("./components/person/PersonOverview.vue"), - props: { - byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true, - }, name: "core.person", meta: { inAccountMenu: true, @@ -635,6 +641,15 @@ const routes = [ permission: "core.view_account_rule", }, }, + { + path: "/person/:widgetSlug([^\\s!?\\/*#|]+)/", + component: () => import("./components/person/PersonOverview.vue"), + props: true, + name: "core.personWithSlug", + meta: { + permission: "core.view_account_rule", + }, + }, { path: "/preferences/person/", component: () => import("./components/LegacyBaseTemplate.vue"), diff --git a/aleksis/core/schema/__init__.py b/aleksis/core/schema/__init__.py index d545cedb4d3f690f2b9bf11528c6cae569e92620..2132d599fd96c0c6e8f9f41880a0609253eccd46 100644 --- a/aleksis/core/schema/__init__.py +++ b/aleksis/core/schema/__init__.py @@ -18,6 +18,7 @@ from ..models import ( PDFFile, Person, Room, + SchoolTerm, TaskUserAssignment, ) from ..util.apps import AppConfig @@ -115,6 +116,7 @@ class Query(graphene.ObjectType): room_by_id = graphene.Field(RoomType, id=graphene.ID()) school_terms = FilterOrderList(SchoolTermType) + current_school_term = graphene.Field(SchoolTermType) holidays = FilterOrderList(HolidayType) calendar = graphene.Field(CalendarBaseType) @@ -284,6 +286,13 @@ class Query(graphene.ObjectType): def resolve_calendar(root, info, **kwargs): return True + @staticmethod + def resolve_current_school_term(root, info, **kwargs): + if not has_person(info.context.user): + return None + + return SchoolTerm.current + class Mutation(graphene.ObjectType): delete_persons = PersonBatchDeleteMutation.Field()