Skip to content
Snippets Groups Projects
Verified Commit 9b74b44b authored by Jonathan Weth's avatar Jonathan Weth :keyboard:
Browse files

Merge branch 'master' into calendar-object-feeds

parents 24208c38 1b6bbe92
No related branches found
No related tags found
1 merge request!1148Calendar events and iCal feeds
Showing
with 1505 additions and 94 deletions
......@@ -9,12 +9,73 @@ and this project adheres to `Semantic Versioning`_.
Unreleased
----------
Changes
=======
The "managed models" feature is mandatory for all models derived from `ExtensibleModel`
and requires creating a migration for all downstream models to add the respective
field.
Added
~~~~~
* Frontend for managing rooms.
* Introduce Holiday model to track information about holidays.
* [Dev] Components for implementing standard CRUD operations in new frontend.
* [Dev] Options for filtering and sorting of GraphQL queries at the server.
* [Dev] Managed models for instances handled by other apps.
* [Dev] Upload slot sytem for out-of-band uploads in GraphQL clients
Changes
Changed
~~~~~~~
* Management of school terms was migrated to new frontend.
Fixed
~~~~~
* [Docker] The build could silently continue even if frontend bundling failed, resulting
in an incomplete AlekSIS frontend app.
* GraphQL mutations did not return errors in case of exceptions.
* Rendering of "simple" PDF templates failed when used with S3 storage.
`3.1.2`_ - 2023-07-05
---------------------
Changed
~~~~~~~
* uWSGI is now installed together with AlekSIS-Core per default.
Fixed
~~~~~
* Notifications were not properly shown in the frontend.
* [Dev] Log levels were not correctly propagated to all loggers
* [Dev] Log format did not contain all essential information
* When navigating from legacy to legacy page, the latter would reload once for no reason.
* The oauth authorization page was not accessible when the service worker was active.
* [Docker] Clear obsolete bundle parts when adding apps using ONBUILD
* Extensible forms that used a subset of fields did not render properly
`3.1.1`_ - 2023-07-01
---------------------
Fixed
~~~~~
* Progress page didn't work properly.
* About page failed to load for apps with an unknown licence.
* QUeries for persons with partial permissions failed.
* Some pages couldn't be scrolled when a task progress popup was open.
* Notification query failed on admin users without persons.
* Querying for notification caused unnecessary database requests.
* Loading bar didn't disappear on some pages after loading was finished.
* Support newer versions of django-oauth-toolkit.
`3.1`_ - 2023-05-30
-------------------
Changed
~~~~~~~
* The frontend is now able to display headings in the main toolbar.
......@@ -22,16 +83,16 @@ Changes
Fixed
~~~~~
* Default translations from vuetify were not loaded.
* Default translations from Vuetify were not loaded.
* Browser locale was not the default locale in the entire frontend.
* In some cases, some items in the sidenav menu were not shown due to its height being higher than the visible page area.
* The search bar in the sidenav menu is shown even though the user has no permission to see it.
* Add permission check to accept invitation menu point in order to hide it when this feature is disabled.
* In some cases, items in the sidenav menu were not shown.
* The search bar in the sidenav menu was shown even though the user had no permission to see it.
* Accept invitation menu item was shown when the invitation feature was disabled.
* Metrics endpoint for Prometheus was at the wrong URL.
* Polling behavior of the whoAmI and permission queries was fixed.
* Polling behavior of the whoAmI and permission queries was improved.
* Confirmation e-mail contained a wrong link.
`3.0`_ - 2022-05-11
`3.0`_ - 2023-05-11
-------------------
Added
......@@ -152,13 +213,13 @@ Changed
* Show languages in local language
* Rewrite of frontend (base template) using Vuetify
* Frontend bundling migrated from Webpack to Vite (cf. installation docs)
* [Dev] The runuwsgi dev server now starts a Vite dev server with HMR in the
background
* Frontend bundling migrated from Webpack to Vite (cf. installation docs)
* [Dev] The runuwsgi dev server now starts a Vite dev server with HMR in the
background
* OIDC scope "profile" now exposes the avatar instead of the official photo
* Based on Django 4.0
* Use built-in Redis cache backend
* Introduce PBKDF2-SHA1 password hashing
* Use built-in Redis cache backend
* Introduce PBKDF2-SHA1 password hashing
* Persistent database connections are now health-checked as to not fail
requests
* [Dev] The undocumented field `check` on `DataCheckResult` was renamed to `data_check`
......@@ -184,8 +245,8 @@ Removed
* iCal feed URLs for birthdays (will be reintroduced later)
* [Dev] Django debug toolbar
* It caused major performance issues and is not useful with the new
frontend anymore
* It caused major performance issues and is not useful with the new
frontend anymore
`2.12.3`_ - 2023-03-07
----------------------
......@@ -336,9 +397,7 @@ Fixed
* The menu button used to be displayed twice on smaller screens.
* The icons were loaded from external servers instead from local server.
* Weekdays were not translated if system locales were missing
* Added locales-all to base image and note to docs
* Added locales-all to base image and note to docs
* The icons in the account menu were still the old ones.
* Due to a merge error, the once removed account menu in the sidenav appeared again.
* Scheduled notifications were shown on dashboard before time.
......@@ -542,11 +601,9 @@ Changed
* Configuration files are now deep merged by default
* Improvements for shell_plus module loading
* core.Group model now takes precedence over auth.Group
* Name collisions are resolved by prefixing with the app label
* Apps can extend SHELL_PLUS_APP_PREFIXES and SHELL_PLUS_DONT_LOAD
* core.Group model now takes precedence over auth.Group
* Name collisions are resolved by prefixing with the app label
* Apps can extend SHELL_PLUS_APP_PREFIXES and SHELL_PLUS_DONT_LOAD
* [Docker] Base image now contains curl, grep, less, sed, and pspg
* Views raising a 404 error can now customise the message that is displayed on the error page
* OpenID Connect is enabled by default now, without RSA support
......@@ -1120,50 +1177,53 @@ Fixed
.. _Keep a Changelog: https://keepachangelog.com/en/1.0.0/
.. _Semantic Versioning: https://semver.org/spec/v2.0.0.html
.. _1.0a1: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/1.0a1
.. _1.0a2: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/1.0a2
.. _1.0a4: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/1.0a4
.. _2.0a1: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.0a1
.. _2.0a2: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.0a2
.. _2.0b0: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.0b0
.. _2.0b1: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.0b1
.. _2.0b2: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.0b2
.. _2.0rc1: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.0rc1
.. _2.0rc2: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.0rc2
.. _2.0rc3: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.0rc3
.. _2.0rc4: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.0rc4
.. _2.0rc5: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.0rc5
.. _2.0rc6: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.0rc6
.. _2.0rc7: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.0rc7
.. _2.0: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.0
.. _2.1: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.1
.. _2.1.1: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.1.1
.. _2.2: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.2
.. _2.2.1: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.2.1
.. _2.3: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.3
.. _2.3.1: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.3.1
.. _2.4: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.4
.. _2.5: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.5
.. _2.6: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.6
.. _2.7: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.7
.. _2.7.1: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.7.1
.. _2.7.2: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.7.2
.. _2.7.3: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.7.3
.. _2.7.4: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.7.4
.. _2.8: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.8
.. _2.8.1: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.8.1
.. _2.9: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.9
.. _2.10: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.10
.. _2.10.1: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.10.1
.. _2.10.2: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.10.2
.. _2.11: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.11
.. _2.11.1: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.11.1
.. _2.12: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.12
.. _2.12.1: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.12.1
.. _2.12.2: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.12.2
.. _2.12.3: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/2.12.3
.. _3.0b0: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/3.0b0
.. _3.0b1: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/3.0b1
.. _3.0b2: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/3.0b2
.. _3.0b3: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/3.0b3
.. _3.0: https://edugit.org/AlekSIS/Official/AlekSIS/-/tags/3.0
.. _1.0a1: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/1.0a1
.. _1.0a2: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/1.0a2
.. _1.0a4: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/1.0a4
.. _2.0a1: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.0a1
.. _2.0a2: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.0a2
.. _2.0b0: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.0b0
.. _2.0b1: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.0b1
.. _2.0b2: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.0b2
.. _2.0rc1: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.0rc1
.. _2.0rc2: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.0rc2
.. _2.0rc3: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.0rc3
.. _2.0rc4: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.0rc4
.. _2.0rc5: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.0rc5
.. _2.0rc6: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.0rc6
.. _2.0rc7: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.0rc7
.. _2.0: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.0
.. _2.1: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.1
.. _2.1.1: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.1.1
.. _2.2: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.2
.. _2.2.1: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.2.1
.. _2.3: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.3
.. _2.3.1: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.3.1
.. _2.4: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.4
.. _2.5: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.5
.. _2.6: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.6
.. _2.7: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.7
.. _2.7.1: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.7.1
.. _2.7.2: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.7.2
.. _2.7.3: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.7.3
.. _2.7.4: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.7.4
.. _2.8: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.8
.. _2.8.1: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.8.1
.. _2.9: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.9
.. _2.10: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.10
.. _2.10.1: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.10.1
.. _2.10.2: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.10.2
.. _2.11: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.11
.. _2.11.1: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.11.1
.. _2.12: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.12
.. _2.12.1: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.12.1
.. _2.12.2: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.12.2
.. _2.12.3: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/2.12.3
.. _3.0b0: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/3.0b0
.. _3.0b1: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/3.0b1
.. _3.0b2: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/3.0b2
.. _3.0b3: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/3.0b3
.. _3.0: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/3.0
.. _3.1: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/3.1
.. _3.1.1: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/3.1.1
.. _3.1.2: https://edugit.org/AlekSIS/official/AlekSIS-Core/-/tags/3.1.2
......@@ -126,7 +126,7 @@ ONBUILD RUN set -e; \
eatmydata pip install $APPS; \
fi; \
eatmydata aleksis-admin vite build; \
eatmydata aleksis-admin collectstatic --no-input; \
eatmydata aleksis-admin collectstatic --no-input --clear; \
rm -rf /usr/local/share/.cache; \
eatmydata apt-get remove --purge -y yarnpkg $BUILD_DEPS; \
eatmydata apt-get autoremove --purge -y; \
......
......@@ -32,7 +32,6 @@ from .models import (
OAuthApplication,
Person,
PersonInvitation,
SchoolTerm,
)
from .registries import (
group_preferences_registry,
......@@ -379,16 +378,6 @@ class EditGroupTypeForm(forms.ModelForm):
fields = ["name", "description"]
class SchoolTermForm(ExtensibleForm):
"""Form for managing school years."""
layout = Layout("name", Row("date_start", "date_end"))
class Meta:
model = SchoolTerm
fields = ["name", "date_start", "date_end"]
class DashboardWidgetOrderForm(ExtensibleForm):
pk = forms.ModelChoiceField(
queryset=None,
......
......@@ -33,6 +33,10 @@ const dateTimeFormats = {
hour: "numeric",
minute: "numeric",
},
shortTime: {
hour: "numeric",
minute: "numeric",
},
},
de: {
short: {
......@@ -67,6 +71,10 @@ const dateTimeFormats = {
hour: "numeric",
minute: "numeric",
},
shortTime: {
hour: "numeric",
minute: "numeric",
},
},
};
......
......@@ -10,9 +10,10 @@ const vuetifyOpts = {
icons: {
iconfont: "mdi", // default - only for display purposes
values: {
cancel: "mdi-close-circle-outline",
delete: "mdi-close-circle-outline",
success: "mdi-check-circle-outline",
cancel: "mdi-close",
delete: "mdi-close", // Not a trashcan due to vuetify using this icon inside chips for closing etc.
deleteContent: "mdi-delete-outline",
success: "mdi-check",
info: "mdi-information-outline",
warning: "mdi-alert-outline",
error: "mdi-alert-octagon-outline",
......@@ -22,6 +23,11 @@ const vuetifyOpts = {
checkboxIndeterminate: "mdi-minus-box-outline",
edit: "mdi-pencil-outline",
preferences: "mdi-cog-outline",
save: "mdi-content-save-outline",
search: "mdi-magnify",
filterEmpty: "mdi-filter-outline",
filterSet: "mdi-filter",
send: "mdi-send-outline",
},
},
};
......
......@@ -21,10 +21,9 @@
</message-box>
<iframe
v-else
:src="'/django' + $route.path + queryString"
:src="iFrameSrc"
:height="iFrameHeight + 'px'"
class="iframe-fullsize"
@load="load"
ref="contentIFrame"
></iframe>
</template>
......@@ -41,6 +40,7 @@ export default {
data: function () {
return {
iFrameHeight: 0,
iFrameSrc: undefined,
};
},
computed: {
......@@ -53,13 +53,16 @@ export default {
},
},
methods: {
getIFrameURL() {
const location = this.$refs.contentIFrame.contentWindow.location;
const url = new URL(location);
return url;
},
/** Handle iframe data after inner page loaded */
load() {
// Write new location of iframe back to Vue Router
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 path = this.getIFrameURL().pathname.replace(/^\/django/, "");
const pathWithQueryString = path + encodeURI(this.getIFrameURL().search);
const routePath =
path.charAt(path.length - 1) === "/" &&
this.$route.path.charAt(path.length - 1) !== "/"
......@@ -71,7 +74,9 @@ export default {
// Show loader if iframe starts to change its content, even if the $route stays the same
this.$refs.contentIFrame.contentWindow.onpagehide = () => {
this.$root.contentLoading = true;
if (this.$root.isLegacyBaseTemplate) {
this.$root.contentLoading = true;
}
};
// Write title of iframe to SPA window
......@@ -101,15 +106,36 @@ export default {
},
},
watch: {
$route() {
$route(newRoute) {
// Show loading animation once route changes
this.$root.contentLoading = true;
// Only reload iFrame content when navigation comes from outsite the iFrame
const path = this.getIFrameURL().pathname.replace(/^\/django/, "");
const routePath =
path.charAt(path.length - 1) === "/" &&
newRoute.path.charAt(path.length - 1) !== "/"
? newRoute.path + "/"
: newRoute.path;
if (path !== routePath) {
this.$refs.contentIFrame.contentWindow.location =
"/django" + this.$route.path + this.queryString;
} else {
this.$root.contentLoading = false;
}
// Scroll to top only when route changes to not affect form submits etc.
// A small duration to avoid flashing of the UI
this.$vuetify.goTo(0, { duration: 10 });
},
},
mounted() {
this.$refs.contentIFrame.addEventListener("load", (e) => {
this.load();
});
this.iFrameSrc = "/django" + this.$route.path + this.queryString;
},
name: "LegacyBaseTemplate",
};
</script>
......
......@@ -315,4 +315,9 @@ export default {
};
</script>
<style scoped></style>
<style>
div[aria-required="true"] .v-input .v-label::after {
content: " *";
color: red;
}
</style>
<template>
<v-bottom-sheet :value="show" persistent hide-overlay max-width="400px">
<v-bottom-sheet
:value="show"
persistent
hide-overlay
max-width="400px"
ref="sheet"
>
<v-expansion-panels accordion v-model="open">
<v-expansion-panel>
<v-expansion-panel-header color="primary" class="white--text px-4">
......@@ -33,6 +39,13 @@ export default {
data() {
return { open: 0 };
},
mounted() {
// Vuetify uses the hideScroll method to disable scrolling by setting an event listener
// to the window. As event listeners can only be removed by referencing the listener
// method and because vuetify this method is called on every state change of the dialog,
// we simply replace the method in this component instance
this.$refs.sheet.hideScroll = this.$refs.sheet.showScroll;
},
computed: {
show() {
return this.celeryProgressByUser && this.celeryProgressByUser.length > 0;
......
This diff is collapsed.
<script setup>
import CreateButton from "./buttons/CreateButton.vue";
import DialogObjectForm from "./dialogs/DialogObjectForm.vue";
</script>
<template>
<v-card>
<v-data-iterator
:items="items"
:items-per-page="itemsPerPage"
:loading="$apollo.queries.items.loading"
hide-default-footer
>
<template #loading>
<slot name="loading">
<v-skeleton-loader
type="card-heading, list-item-avatar-two-line@3, actions"
/>
</slot>
</template>
<template #no-data>
<v-card-text>{{ $t(noItemsI18nKey) }}</v-card-text>
</template>
<template #header>
<v-card-title>{{ title }}</v-card-title>
</template>
<template #default="props">
<slot
v-if="items.length"
name="iteratorContent"
:items="props.items"
:editing-enabled="editingEnabled"
:deletion-enabled="deletionEnabled"
:handle-edit="handleEdit"
:handle-delete="handleDelete"
>
<v-list>
<template v-for="(item, index) in items">
<v-list-item :key="item.id">
<v-list-item-avatar>
<slot
name="listIteratorItemAvatar"
:item="item"
:index="index"
/>
</v-list-item-avatar>
<v-list-item-content>
<slot
name="listIteratorItemContent"
:item="item"
:index="index"
>
<v-list-item-title>
{{ item.name }}
</v-list-item-title>
</slot>
</v-list-item-content>
<v-list-item-action>
<v-btn
v-if="editingEnabled && item.canEdit"
icon
@click="handleEdit(item)"
>
<v-icon>mdi-pencil-outline</v-icon>
</v-btn>
<v-btn
v-if="deletionEnabled && item.canDelete"
icon
@click="handleDelete(item)"
>
<v-icon>mdi-delete-outline</v-icon>
</v-btn>
</v-list-item-action>
</v-list-item>
<v-divider
v-if="index < items.length - 1"
:key="index"
inset
></v-divider>
</template>
</v-list>
</slot>
</template>
<template #footer>
<v-card-actions>
<slot
v-if="creatingEnabled || editingEnabled"
name="createComponent"
:attrs="{
value: objectFormModel,
defaultItem: defaultItem,
editItem: editItem,
gqlCreateMutation: gqlCreateMutation,
gqlPatchMutation: gqlPatchMutation,
isCreate: isCreate,
fields: fields,
getCreateData: getCreateData,
createItemI18nKey: createItemI18nKey,
}"
:on="{
input: (i) => (objectFormModel = i),
cancel: () => (objectFormModel = false),
save: handleCreateDone,
error: handleError,
}"
>
<dialog-object-form
v-model="objectFormModel"
:get-create-data="getCreateData"
:get-patch-data="getPatchData"
:default-item="defaultItem"
:edit-item="editItem"
:force-model-item-update="true"
:gql-create-mutation="gqlCreateMutation"
:gql-patch-mutation="gqlPatchMutation"
:is-create="isCreate"
:fields="fields"
:create-item-i18n-key="createItemI18nKey"
@cancel="objectFormModel = false"
@save="handleCreateDone"
@error="handleError"
>
<template #activator="{ props }" v-if="creatingEnabled">
<create-button
@click="handleCreate"
:disabled="objectFormModel"
/>
</template>
<template
v-for="field in fields"
#[formFieldSlotName(field)]="{ item, isCreate, on, attrs }"
>
<slot
:name="formFieldSlotName(field)"
:attrs="attrs"
:on="on"
:item="item"
:is-create="isCreate"
/>
</template>
</dialog-object-form>
</slot>
</v-card-actions>
</template>
</v-data-iterator>
</v-card>
</template>
<script>
export default {
name: "ObjectCRUDList",
props: {
titleI18nKey: {
type: String,
required: false,
default: "",
},
titleString: {
type: String,
required: false,
default: "",
},
noItemsI18nKey: {
type: String,
required: true,
},
createItemI18nKey: {
type: String,
required: false,
default: "actions.create",
},
fields: {
type: Array,
required: false,
default: undefined,
},
defaultItem: {
type: Object,
required: false,
default: undefined,
},
getGqlData: {
type: Function,
required: false,
default: (data) => data.items,
},
gqlQuery: {
type: Object,
required: true,
},
gqlVariables: {
type: Object,
required: false,
default: undefined,
},
gqlCreateMutation: {
type: Object,
required: false,
default: undefined,
},
gqlPatchMutation: {
type: Object,
required: false,
default: undefined,
},
gqlDeleteMutation: {
type: Object,
required: false,
default: undefined,
},
getCreateData: {
type: Function,
required: false,
default: (item) => item,
},
getPatchData: {
type: Function,
required: false,
default: (item) => {
let { id, __typename, ...patchItem } = item;
return patchItem;
},
},
itemsPerPage: {
type: Number,
required: false,
default: 5,
},
},
components: {
CreateButton,
},
data() {
return {
objectFormModel: false,
editItem: undefined,
isCreate: true,
};
},
apollo: {
items() {
return {
query: this.gqlQuery,
variables() {
if (this.gqlVariables) {
return this.gqlVariables;
}
return {};
},
error: function (error) {
this.handleError(error);
},
update(data) {
return this.getGqlData(data);
},
};
},
},
methods: {
handleCreate() {
this.editItem = undefined;
this.isCreate = true;
this.objectFormModel = true;
},
handleEdit(item) {
if (!item || !this.editingEnabled) {
return;
}
this.editItem = item;
this.isCreate = false;
this.objectFormModel = true;
},
handleDelete() {},
handleCreateDone() {
this.$apollo.queries.items.refetch();
},
handleError(error) {
console.error(error);
let snackbarText = "";
if (error instanceof String) {
// error is a translation key or simply a string
snackbarText = this.$t(error);
} else if (error instanceof Object && error.message) {
snackbarText = error.message;
} else {
snackbarText = this.$t("graphql.snackbar_error_message");
}
this.$root.snackbarItems.push({
id: crypto.randomUUID(),
timeout: 5000,
messageKey: snackbarText,
color: "error",
});
},
formFieldSlotName(headerEntry) {
return headerEntry.value + ".field";
},
},
computed: {
creatingEnabled() {
return this.gqlCreateMutation && this.fields && this.defaultItem;
},
editingEnabled() {
return (
this.gqlPatchMutation &&
this.fields &&
this.items &&
this.items.some((i) => i.canEdit)
);
},
deletionEnabled() {
return (
this.gqlDeleteMutation &&
this.items &&
this.items.some((i) => i.canDelete)
);
},
title() {
return this.titleI18nKey ? this.$t(this.titleI18nKey) : this.titleString;
},
},
};
</script>
<style scoped></style>
<template>
<v-btn v-bind="{ ...$props, ...$attrs }" @click="$emit('click', $event)">
<slot>
<v-icon v-if="iconText" left>{{ iconText }}</v-icon>
<span v-t="i18nKey" />
</slot>
</v-btn>
</template>
<script>
export default {
name: "BaseButton",
inheritAttrs: true,
extends: "v-btn",
props: {
i18nKey: {
type: String,
required: true,
},
iconText: {
type: String,
required: false,
default: undefined,
},
},
};
</script>
<style scoped></style>
<script>
import SecondaryActionButton from "./SecondaryActionButton.vue";
export default {
name: "CancelButton",
extends: SecondaryActionButton,
props: {
iconText: {
type: String,
required: false,
default: "$cancel",
},
i18nKey: {
type: String,
required: false,
default: "actions.cancel",
},
color: {
type: String,
required: false,
default: "error",
},
},
};
</script>
<script>
import PrimaryActionButton from "./PrimaryActionButton.vue";
export default {
name: "CreateButton",
extends: PrimaryActionButton,
props: {
iconText: {
type: String,
required: false,
default: "$plus",
},
i18nKey: {
type: String,
required: false,
default: "actions.create",
},
},
};
</script>
<script>
import PrimaryActionButton from "./PrimaryActionButton.vue";
export default {
name: "DeleteButton",
extends: PrimaryActionButton,
props: {
iconText: {
type: String,
required: false,
default: "$delete",
},
i18nKey: {
type: String,
required: false,
default: "actions.delete",
},
},
};
</script>
<script>
import PrimaryActionButton from "./PrimaryActionButton.vue";
export default {
name: "EditButton",
extends: PrimaryActionButton,
props: {
iconText: {
type: String,
required: false,
default: "$edit",
},
i18nKey: {
type: String,
required: false,
default: "actions.edit",
},
},
};
</script>
<template>
<secondary-action-button
v-bind="$attrs"
v-on="$listeners"
:i18n-key="i18nKey"
>
<v-icon v-if="icon" left>{{ icon }}</v-icon>
<v-badge color="secondary" :value="numFilters" :content="numFilters" inline>
<span v-t="i18nKey" />
</v-badge>
<v-btn
icon
@click.stop="$emit('clear')"
small
v-if="numFilters"
class="mr-n1"
>
<v-icon>$clear</v-icon>
</v-btn>
</secondary-action-button>
</template>
<script>
import SecondaryActionButton from "./SecondaryActionButton.vue";
export default {
name: "FilterButton",
components: { SecondaryActionButton },
extends: SecondaryActionButton,
computed: {
icon() {
return this.hasFilters || this.numFilters > 0
? "$filterSet"
: "$filterEmpty";
},
},
props: {
i18nKey: {
type: String,
required: false,
default: "actions.filter",
},
hasFilters: {
type: Boolean,
required: false,
default: false,
},
numFilters: {
type: Number,
required: false,
default: 0,
},
},
};
</script>
<script>
import BaseButton from "./BaseButton.vue";
export default {
name: "PrimaryActionButton",
components: { BaseButton },
extends: BaseButton,
props: {
i18nKey: {
type: String,
required: true,
},
color: {
type: String,
required: false,
default: "primary",
},
},
};
</script>
<script>
import PrimaryActionButton from "./PrimaryActionButton.vue";
export default {
name: "SaveButton",
extends: PrimaryActionButton,
props: {
iconText: {
type: String,
required: false,
default: "$save",
},
i18nKey: {
type: String,
required: false,
default: "actions.save",
},
},
};
</script>
<script>
import BaseButton from "./BaseButton.vue";
export default {
name: "SecondaryActionButton",
components: { BaseButton },
extends: BaseButton,
props: {
i18nKey: {
type: String,
required: true,
},
color: {
type: String,
required: false,
default: "secondary",
},
outlined: {
type: Boolean,
required: false,
default: true,
},
},
};
</script>
<template>
<v-snackbar v-bind="$attrs" v-on="$listeners">
<slot />
<template #action="{ attrs }">
<v-btn v-bind="attrs" @click="close()" icon>
<v-icon>$close</v-icon>
</v-btn>
</template>
</v-snackbar>
</template>
<script>
export default {
name: "ClosableSnackbar",
extends: "v-snackbar",
methods: {
close() {
this.$emit("input", false);
},
},
};
</script>
<style scoped></style>
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