diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 30bbda0a7ffe2413c48af6494aaba91a0975d981..3cc6a013323e220ff05e15c48aa4eac6885014a2 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -14,6 +14,8 @@ Added * GraphQL schema for Rooms * [Dev] UpdateIndicator Vue Component to display the status of interactive pages +* [Dev] DeleteDialog Vue Component to unify item deletion in the new frontend + Changed ~~~~~~~ @@ -31,6 +33,8 @@ Fixed * The `Stop Impersonation` button is not shown due to an oversee when changing the type of the whoAmI query to an object of UserType * Offline fallback page for legacy pages was misleading sometimes. * Route changes in the Legacy-Component iframe didn't trigger a scroll to the top +* Query strings did not get passed when navigating legacy pages inside of the SPA. +* Retry button on error 500 page did not trigger a reload of the page. * When the Celery worker wasn't able to execute all tasks in time, notifications were sent multiple times. `3.0b3`_ - 2023-03-19 diff --git a/aleksis/core/frontend/components/LegacyBaseTemplate.vue b/aleksis/core/frontend/components/LegacyBaseTemplate.vue index 84f7e2e12be16f868c731b06f11ad0e4d2557ef4..790fc10c592bac5acc1e3bbadabcae63e065804f 100644 --- a/aleksis/core/frontend/components/LegacyBaseTemplate.vue +++ b/aleksis/core/frontend/components/LegacyBaseTemplate.vue @@ -59,13 +59,14 @@ export default { const location = this.$refs.contentIFrame.contentWindow.location; const url = new URL(location); const path = url.pathname.replace(/^\/django/, ""); + const pathWithQueryString = path + encodeURI(url.search); const routePath = path.charAt(path.length - 1) === "/" && this.$route.path.charAt(path.length - 1) !== "/" ? this.$route.path + "/" : this.$route.path; if (path !== routePath) { - this.$router.push(path); + this.$router.push(pathWithQueryString); } // Show loader if iframe starts to change its content, even if the $route stays the same diff --git a/aleksis/core/frontend/components/generic/dialogs/DeleteDialog.vue b/aleksis/core/frontend/components/generic/dialogs/DeleteDialog.vue new file mode 100644 index 0000000000000000000000000000000000000000..a56e3960cd1ae29e60f966a44ae69adeb0332949 --- /dev/null +++ b/aleksis/core/frontend/components/generic/dialogs/DeleteDialog.vue @@ -0,0 +1,136 @@ +<template> + <ApolloMutation + v-if="dialogOpen" + :mutation="gqlMutation" + :variables="{ id: item.id }" + :update="update" + @done="close(true)" + > + <template #default="{ mutate, loading, error }"> + <v-dialog v-model="dialogOpen" max-width="500px"> + <v-card> + <v-card-title class="text-h5"> + <slot name="title"> + {{ $t("actions.confirm_deletion") }} + </slot> + </v-card-title> + <v-card-text> + <slot name="body"> + <p class="text-body-1">{{ nameOfObject }}</p> + </slot> + </v-card-text> + <v-card-actions> + <v-spacer></v-spacer> + <v-btn text @click="close(false)" :disabled="loading"> + <slot name="cancelContent"> + {{ $t("actions.cancel") }} + </slot> + </v-btn> + <v-btn + color="error" + text + @click="mutate" + :loading="loading" + :disabled="loading" + > + <slot name="deleteContent"> + {{ $t("actions.delete") }} + </slot> + </v-btn> + </v-card-actions> + </v-card> + </v-dialog> + <v-snackbar :value="error !== null"> + {{ error }} + + <template #action="{ attrs }"> + <v-btn color="primary" text v-bind="attrs" @click="error = null" icon> + <v-icon>$close</v-icon> + </v-btn> + </template> + </v-snackbar> + </template> + </ApolloMutation> +</template> + +<script> +export default { + name: "DeleteDialog", + computed: { + nameOfObject() { + return this.itemAttribute in this.item || {} + ? this.item[this.itemAttribute] + : this.item.toString(); + }, + dialogOpen: { + get() { + return this.value; + }, + + set(val) { + this.$emit("input", val); + }, + }, + }, + methods: { + update(store) { + if (!this.gqlQuery) { + // There is no GraphQL query to update + return; + } + + // Read the data from cache for query + const storedData = store.readQuery({ query: this.gqlQuery }); + + if (!storedData) { + // There are no data in the cache yet + return; + } + + const storedDataKey = Object.keys(storedData)[0]; + + // Remove item from stored data + const index = storedData[storedDataKey].findIndex( + (m) => m.id === this.item.id + ); + storedData[storedDataKey].splice(index, 1); + + // Write data back to the cache + store.writeQuery({ query: this.gqlQuery, data: storedData }); + }, + close(success) { + this.$emit("input", false); + if (success) { + this.$emit("success"); + } else { + this.$emit("cancel"); + } + }, + }, + props: { + value: { + type: Boolean, + required: true, + }, + item: { + type: Object, + required: false, + default: () => ({}), + }, + itemAttribute: { + type: String, + required: false, + default: "name", + }, + gqlMutation: { + type: Object, + required: true, + }, + gqlQuery: { + type: Object, + required: false, + default: null, + }, + }, +}; +</script> diff --git a/aleksis/core/frontend/messages/en.json b/aleksis/core/frontend/messages/en.json index ff9693d369fea8f9ce44e07d5cc1dd4b89aae1aa..5235e23265346a5842cccf793e67987459a5b40f 100644 --- a/aleksis/core/frontend/messages/en.json +++ b/aleksis/core/frontend/messages/en.json @@ -75,7 +75,12 @@ "back": "Back", "search": "Search", "edit": "Edit", - "close": "Close" + "close": "Close", + "cancel": "Cancel", + "confirm_deletion": "Are you sure you want to delete this item?", + "delete": "Delete", + "stop_editing": "Stop editing", + "save": "Save" }, "administration": { "backend_admin": { diff --git a/aleksis/core/schema/base.py b/aleksis/core/schema/base.py index e927dadccaaab7b2bf39c2110286970222f70f5b..e3478a34ff62be4859d8d9e88a8d73322796ad82 100644 --- a/aleksis/core/schema/base.py +++ b/aleksis/core/schema/base.py @@ -1,3 +1,6 @@ +from django.db.models import Model +from django.core.exceptions import PermissionDenied + import graphene from graphene_django import DjangoObjectType @@ -24,3 +27,23 @@ class FieldFileType(graphene.ObjectType): def resolve_absolute_url(root, info, **kwargs): return info.context.build_absolute_uri(root.url) if root else "" + + +class DeleteMutation(graphene.Mutation): + """Mutation to delete an object.""" + + klass: Model = None + permission_required: str = "" + ok = graphene.Boolean() + + class Arguments: + id = graphene.ID() # noqa + + @classmethod + def mutate(cls, root, info, **kwargs): + obj = cls.klass.objects.get(pk=kwargs["id"]) + if info.context.user.has_perm(cls.permission_required, obj): + obj.delete() + return cls(ok=True) + else: + raise PermissionDenied() diff --git a/aleksis/core/templates/500.html b/aleksis/core/templates/500.html index d008cd5405f4e5eee73ddf175cab87057e563785..a2ea5c902dba89f34241403e85d0e8af8d8f9a5d 100644 --- a/aleksis/core/templates/500.html +++ b/aleksis/core/templates/500.html @@ -16,7 +16,7 @@ {% endblocktrans %} </p> {% include "core/partials/admins_list.html" %} - <a href="javascript:window.location.reload()" class="btn green waves-effect waves-light"> + <a onClick="window.location.reload();" class="btn secondary waves-effect waves-light"> <i class="material-icons left">refresh</i> {% trans "Retry" %} </a> diff --git a/pyproject.toml b/pyproject.toml index 9a3318eaef990a7445fd14bcbabad0885b1ac6b2..e4de1fd18c27222094738b4ff926335dc9f9a3c0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -100,7 +100,7 @@ django-favicon-plus-reloaded = "^1.1.5" django-health-check = "^3.12.1" psutil = "^5.7.0" celery-progress = "^0.1.0" -django-cachalot = "^2.3.2" +django-cachalot = "^2.5.3" django-prometheus = "^2.1.0" django-model-utils = "^4.0.0" bs4 = "^0.0.1"