Skip to content
Snippets Groups Projects
Commit 22f2aeac authored by Jonathan Weth's avatar Jonathan Weth :keyboard:
Browse files

Merge branch '1109-more-extensible-person-and-group-detail-page' into 'master'

Resolve "More extensible person and group detail page"

Closes #1109

See merge request !1596
parents 7c984bb4 1aecd6db
No related branches found
No related tags found
1 merge request!1596Resolve "More extensible person and group detail page"
Pipeline #193336 failed
Showing
with 256 additions and 49 deletions
......@@ -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,
},
],
};
<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>
......
......@@ -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"
......
<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>
......
......@@ -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>
<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>
export default {
props: {
group: {
type: Object,
required: true,
},
},
};
......@@ -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>
......
query currentSchoolTerm {
currentSchoolTerm {
id
name
dateStart
dateEnd
canEdit
canDelete
}
}
......@@ -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": {
......
......@@ -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",
......
......@@ -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",
],
},
};
......@@ -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"),
......
......@@ -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()
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment