Feature/group based notifications (#918)

* fix group page

* setup group notification for backend

* update type generators

* script to auto-generate schema exports

* setup frontend CRUD interface

* remove old notifications UI

* drop old events api

* add test functionality

* update naming for fields

* add event dispatcher functionality

* bump to python 3.10

* bump python version

* purge old event code

* use-async apprise

* set mealie logo as image

* unify styles for buttons rows

* add links to banners
This commit is contained in:
Hayden 2022-01-09 21:04:24 -09:00 committed by GitHub
commit 190773c5d7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
74 changed files with 1992 additions and 1229 deletions

View file

@ -41,9 +41,6 @@
:search="search"
@click:row="handleRowClick"
>
<template #item.shoppingLists="{ item }">
{{ item.shoppingLists.length }}
</template>
<template #item.users="{ item }">
{{ item.users.length }}
</template>
@ -99,7 +96,6 @@ export default defineComponent({
{ text: i18n.t("general.name"), value: "name" },
{ text: i18n.t("user.total-users"), value: "users" },
{ text: i18n.t("user.webhooks-enabled"), value: "webhookEnable" },
{ text: i18n.t("shopping-list.shopping-lists"), value: "shoppingLists" },
{ text: i18n.t("general.delete"), value: "actions" },
],
updateMode: false,

View file

@ -1,24 +0,0 @@
<template>
<v-container fluid>
<BaseCardSectionTitle title="Manage Categories"> </BaseCardSectionTitle>
</v-container>
</template>
<script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api";
export default defineComponent({
layout: "admin",
setup() {
return {};
},
head() {
return {
title: this.$t("sidebar.categories") as string,
};
},
});
</script>
<style scoped>
</style>

View file

@ -1,226 +0,0 @@
<template>
<v-container fluid>
<BaseCardSectionTitle title="Event Notifications">
{{ $t("events.new-notification-form-description") }}
<div class="d-flex justify-space-around">
<a href="https://github.com/caronc/apprise/wiki" target="_blanks"> Apprise </a>
<a href="https://github.com/caronc/apprise/wiki/Notify_gotify" target="_blanks"> Gotify </a>
<a href="https://github.com/caronc/apprise/wiki/Notify_discord" target="_blanks"> Discord </a>
<a href="https://github.com/caronc/apprise/wiki/Notify_homeassistant" target="_blanks"> Home Assistant </a>
<a href="https://github.com/caronc/apprise/wiki/Notify_matrix" target="_blanks"> Matrix </a>
<a href="https://github.com/caronc/apprise/wiki/Notify_pushover" target="_blanks"> Pushover </a>
</div>
</BaseCardSectionTitle>
<BaseDialog
ref="domDeleteConfirmation"
:title="$t('settings.backup.delete-backup')"
color="error"
:icon="$globals.icons.alertCircle"
@confirm="deleteNotification()"
>
<v-card-text>
{{ $t("general.confirm-delete-generic") }}
</v-card-text>
</BaseDialog>
<v-toolbar color="background" flat class="justify-between">
<BaseDialog
:icon="$globals.icons.bellAlert"
:title="$t('general.new') + ' ' + $t('events.notification')"
@submit="createNotification"
>
<template #activator="{ open }">
<BaseButton @click="open"> {{ $t("events.notification") }}</BaseButton>
</template>
<v-card-text>
<v-select
v-model="createNotificationData.type"
:items="notificationTypes"
:label="$t('general.type')"
></v-select>
<v-text-field v-model="createNotificationData.name" :label="$t('general.name')"></v-text-field>
<v-text-field
v-model="createNotificationData.notificationUrl"
:label="$t('events.apprise-url')"
></v-text-field>
<BaseButton
class="d-flex ml-auto"
small
color="info"
@click="testByUrl(createNotificationData.notificationUrl)"
>
<template #icon> {{ $globals.icons.testTube }}</template>
{{ $t("general.test") }}
</BaseButton>
<p class="text-uppercase">{{ $t("events.subscribed-events") }}</p>
<div class="d-flex flex-wrap justify-center">
<v-checkbox
v-model="createNotificationData.general"
class="mb-n2 mt-n2 mx-2"
:label="$t('general.general')"
></v-checkbox>
<v-checkbox
v-model="createNotificationData.recipe"
class="mb-n2 mt-n2 mx-2"
:label="$t('general.recipe')"
></v-checkbox>
<v-checkbox
v-model="createNotificationData.backup"
class="mb-n2 mt-n2 mx-2"
:label="$t('settings.backup-and-exports')"
></v-checkbox>
<v-checkbox
v-model="createNotificationData.scheduled"
class="mb-n2 mt-n2 mx-2"
:label="$t('events.scheduled')"
></v-checkbox>
<v-checkbox
v-model="createNotificationData.migration"
class="mb-n2 mt-n2 mx-2"
:label="$t('settings.migrations')"
></v-checkbox>
<v-checkbox
v-model="createNotificationData.group"
class="mb-n2 mt-n2 mx-2"
:label="$t('group.group')"
></v-checkbox>
<v-checkbox
v-model="createNotificationData.user"
class="mb-n2 mt-n2 mx-2"
:label="$t('user.user')"
></v-checkbox>
</div>
</v-card-text>
</BaseDialog>
</v-toolbar>
<!-- Data Table -->
<v-data-table
:headers="headers"
:items="notifications || []"
class="elevation-0"
hide-default-footer
disable-pagination
>
<template v-for="boolHeader in headers" #[`item.${boolHeader.value}`]="{ item }">
<div :key="boolHeader.value">
<div v-if="boolHeader.value === 'type'">
{{ item[boolHeader.value] }}
</div>
<v-icon
v-else-if="item[boolHeader.value] === true || item[boolHeader.value] === false"
:color="item[boolHeader.value] ? 'success' : 'gray'"
>
{{ item[boolHeader.value] ? $globals.icons.check : $globals.icons.close }}
</v-icon>
<div v-else-if="boolHeader.value === 'actions'" class="d-flex">
<BaseButton
class="mr-1"
delete
x-small
minor
@click="
deleteTarget = item.id;
domDeleteConfirmation.open();
"
/>
<BaseButton edit x-small @click="testById(item.id)">
<template #icon>
{{ $globals.icons.testTube }}
</template>
{{ $t("general.test") }}
</BaseButton>
</div>
<div v-else>
{{ item[boolHeader.value] }}
</div>
</div>
</template>
</v-data-table>
</v-container>
</template>
<script lang="ts">
import { defineComponent, reactive, useContext, toRefs, ref } from "@nuxtjs/composition-api";
import { useNotifications } from "@/composables/use-notifications";
export default defineComponent({
layout: "admin",
setup() {
const { i18n } = useContext();
const state = reactive({
headers: [
{ text: i18n.t("general.type"), value: "type" },
{ text: i18n.t("general.name"), value: "name" },
{ text: i18n.t("general.general"), value: "general", align: "center" },
{ text: i18n.t("general.recipe"), value: "recipe", align: "center" },
{ text: i18n.t("events.database"), value: "backup", align: "center" },
{ text: i18n.t("events.scheduled"), value: "scheduled", align: "center" },
{ text: i18n.t("settings.migrations"), value: "migration", align: "center" },
{ text: i18n.t("group.group"), value: "group", align: "center" },
{ text: i18n.t("user.user"), value: "user", align: "center" },
{ text: "", value: "actions" },
],
keepDialogOpen: false,
notifications: [],
newNotification: {
type: "General",
name: "",
notificationUrl: "",
},
newNotificationOptions: {
general: true,
recipe: true,
backup: true,
scheduled: true,
migration: true,
group: true,
user: true,
},
});
const {
deleteNotification,
createNotification,
refreshNotifications,
notifications,
loading,
testById,
testByUrl,
createNotificationData,
notificationTypes,
deleteTarget,
} = useNotifications();
// API
const domDeleteConfirmation = ref(null);
return {
...toRefs(state),
domDeleteConfirmation,
notifications,
loading,
createNotificationData,
deleteNotification,
deleteTarget,
createNotification,
refreshNotifications,
testById,
testByUrl,
notificationTypes,
};
},
head() {
return {
title: this.$t("events.notification") as string,
};
},
});
</script>
<style scoped>
</style>

View file

@ -1,24 +0,0 @@
<template>
<v-container fluid>
<BaseCardSectionTitle title="Manage Tags"> </BaseCardSectionTitle>
</v-container>
</template>
<script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api";
export default defineComponent({
layout: "admin",
setup() {
return {};
},
head() {
return {
title: this.$t("sidebar.tags") as string,
};
},
});
</script>
<style scoped>
</style>

View file

@ -6,7 +6,7 @@
</template>
<template #title> {{ shoppingList.name }} </template>
</BasePageTitle>
<BannerExperimental />
<BannerExperimental issue="https://github.com/hay-kot/mealie/issues/916" />
<!-- Viewer -->
<section v-if="!edit" class="py-2">
<div v-if="!byLabel">

View file

@ -11,7 +11,7 @@
<BaseButton create @click="actions.createOne()" />
<v-expansion-panels class="mt-2">
<draggable v-model="cookbooks" handle=".handle" style="width: 100%" @change="actions.updateOrder()">
<v-expansion-panel v-for="(cookbook, index) in cookbooks" :key="index" class="my-2 my-border rounded">
<v-expansion-panel v-for="(cookbook, index) in cookbooks" :key="index" class="my-2 left-border rounded">
<v-expansion-panel-header disable-icon-rotate class="headline">
<div class="d-flex align-center">
<v-icon large left>
@ -23,8 +23,8 @@
<v-icon class="handle">
{{ $globals.icons.arrowUpDown }}
</v-icon>
<v-btn color="info" fab small class="ml-2">
<v-icon color="white">
<v-btn icon small class="ml-2">
<v-icon>
{{ $globals.icons.edit }}
</v-icon>
</v-btn>
@ -38,8 +38,22 @@
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<BaseButton delete @click="actions.deleteOne(cookbook.id)" />
<BaseButton save @click="actions.updateOne(cookbook)"> </BaseButton>
<BaseButtonGroup
:buttons="[
{
icon: $globals.icons.delete,
text: $t('general.delete'),
event: 'delete',
},
{
icon: $globals.icons.save,
text: $t('general.save'),
event: 'save',
},
]"
@delete="actions.deleteOne(webhook.id)"
@save="actions.updateOne(webhook)"
/>
</v-card-actions>
</v-expansion-panel-content>
</v-expansion-panel>
@ -70,9 +84,3 @@ export default defineComponent({
},
});
</script>
<style>
.my-border {
border-left: 5px solid var(--v-primary-base) !important;
}
</style>

View file

@ -0,0 +1,307 @@
<template>
<v-container class="narrow-container">
<BaseDialog
v-model="deleteDialog"
color="error"
:title="$t('general.confirm')"
:icon="$globals.icons.alertCircle"
@confirm="deleteNotifier(deleteTargetId)"
>
<v-card-text>
{{ $t("general.confirm-delete-generic") }}
</v-card-text>
</BaseDialog>
<BaseDialog v-model="createDialog" @submit="createNewNotifier">
<v-card-text>
<v-text-field v-model="createNotifierData.name" :label="$t('general.name')"></v-text-field>
<v-text-field v-model="createNotifierData.appriseUrl" :label="$t('events.apprise-url')"></v-text-field>
</v-card-text>
</BaseDialog>
<BasePageTitle divider>
<template #header>
<v-img max-height="125" max-width="125" :src="require('~/static/svgs/manage-notifiers.svg')"></v-img>
</template>
<template #title> Event Notifiers </template>
{{ $t("events.new-notification-form-description") }}
<div class="mt-3 d-flex justify-space-around">
<a href="https://github.com/caronc/apprise/wiki" target="_blanks"> Apprise </a>
<a href="https://github.com/caronc/apprise/wiki/Notify_gotify" target="_blanks"> Gotify </a>
<a href="https://github.com/caronc/apprise/wiki/Notify_discord" target="_blanks"> Discord </a>
<a href="https://github.com/caronc/apprise/wiki/Notify_homeassistant" target="_blanks"> Home Assistant </a>
<a href="https://github.com/caronc/apprise/wiki/Notify_matrix" target="_blanks"> Matrix </a>
<a href="https://github.com/caronc/apprise/wiki/Notify_pushover" target="_blanks"> Pushover </a>
</div>
</BasePageTitle>
<BannerExperimental issue="https://github.com/hay-kot/mealie/issues/833" />
<BaseButton create @click="createDialog = true" />
<v-expansion-panels v-if="notifiers" class="mt-2">
<v-expansion-panel v-for="(notifier, index) in notifiers" :key="index" class="my-2 left-border rounded">
<v-expansion-panel-header disable-icon-rotate class="headline">
<div class="d-flex align-center">
{{ notifier.name }}
</div>
<template #actions>
<v-btn icon class="ml-2">
<v-icon>
{{ $globals.icons.edit }}
</v-icon>
</v-btn>
</template>
</v-expansion-panel-header>
<v-expansion-panel-content>
<v-text-field v-model="notifiers[index].name" label="Name"></v-text-field>
<v-text-field v-model="notifiers[index].appriseUrl" label="Apprise URL (skipped in blank)"></v-text-field>
<v-checkbox v-model="notifiers[index].enabled" label="Enable Notifier" dense></v-checkbox>
<v-divider></v-divider>
<p class="pt-4">What events should this notifier subscribe to?</p>
<template v-for="(opt, idx) in optionsKeys">
<v-checkbox
v-if="!opt.divider"
:key="'option-' + idx"
v-model="notifiers[index].options[opt.key]"
hide-details
dense
:label="opt.text"
></v-checkbox>
<div v-else :key="'divider-' + idx" class="mt-4">
{{ opt.text }}
</div>
</template>
<v-card-actions class="py-0">
<v-spacer></v-spacer>
<BaseButtonGroup
:buttons="[
{
icon: $globals.icons.delete,
text: $t('general.delete'),
event: 'delete',
},
{
icon: $globals.icons.testTube,
text: $t('general.test'),
event: 'test',
},
{
icon: $globals.icons.save,
text: $t('general.save'),
event: 'save',
},
]"
@delete="openDelete(notifier)"
@save="saveNotifier(notifier)"
@test="testNotifier(notifier)"
/>
</v-card-actions>
</v-expansion-panel-content>
</v-expansion-panel>
</v-expansion-panels>
</v-container>
</template>
<script lang="ts">
import { defineComponent, useAsync, reactive, useContext, toRefs } from "@nuxtjs/composition-api";
import { useUserApi } from "~/composables/api";
import { useAsyncKey } from "~/composables/use-utils";
import { GroupEventNotifierCreate, GroupEventNotifierOut } from "~/types/api-types/group";
interface OptionKey {
text: string;
key: string;
}
interface OptionDivider {
divider: true;
text: string;
}
export default defineComponent({
setup() {
const api = useUserApi();
const state = reactive({
deleteDialog: false,
createDialog: false,
deleteTargetId: "",
});
const notifiers = useAsync(async () => {
const { data } = await api.groupEventNotifier.getAll();
return data ?? [];
}, useAsyncKey());
async function refreshNotifiers() {
const { data } = await api.groupEventNotifier.getAll();
notifiers.value = data ?? [];
}
const createNotifierData: GroupEventNotifierCreate = reactive({
name: "",
enabled: true,
appriseUrl: "",
});
async function createNewNotifier() {
await api.groupEventNotifier.createOne(createNotifierData);
refreshNotifiers();
}
function openDelete(notifier: GroupEventNotifierOut) {
state.deleteDialog = true;
state.deleteTargetId = notifier.id;
}
async function deleteNotifier(targetId: string) {
await api.groupEventNotifier.deleteOne(targetId);
refreshNotifiers();
state.deleteTargetId = "";
}
async function saveNotifier(notifier: GroupEventNotifierOut) {
await api.groupEventNotifier.updateOne(notifier.id, notifier);
refreshNotifiers();
}
async function testNotifier(notifier: GroupEventNotifierOut) {
await api.groupEventNotifier.test(notifier.id);
}
// ===============================================================
// Options Definitions
const { i18n } = useContext();
const optionsKeys: (OptionKey | OptionDivider)[] = [
{
divider: true,
text: "Recipe Events",
},
{
text: i18n.t("general.create") as string,
key: "recipeCreated",
},
{
text: i18n.t("general.update") as string,
key: "recipeUpdated",
},
{
text: i18n.t("general.delete") as string,
key: "recipeDeleted",
},
{
divider: true,
text: "User Events",
},
{
text: "When a new user joins your group",
key: "userSignup",
},
{
divider: true,
text: "Data Events",
},
{
text: "When a new data migration is completed",
key: "dataMigrations",
},
{
text: "When a data export is completed",
key: "dataExport",
},
{
text: "When a data import is completed",
key: "dataImport",
},
{
divider: true,
text: "Mealplan Events",
},
{
text: "When a user in your group creates a new mealplan",
key: "mealplanEntryCreated",
},
{
divider: true,
text: "Shopping List Events",
},
{
text: i18n.t("general.create") as string,
key: "shoppingListCreated",
},
{
text: i18n.t("general.update") as string,
key: "shoppingListUpdated",
},
{
text: i18n.t("general.delete") as string,
key: "shoppingListDeleted",
},
{
divider: true,
text: "Cookbook Events",
},
{
text: i18n.t("general.create") as string,
key: "cookbookCreated",
},
{
text: i18n.t("general.update") as string,
key: "cookbookUpdated",
},
{
text: i18n.t("general.delete") as string,
key: "cookbookDeleted",
},
{
divider: true,
text: "Tag Events",
},
{
text: i18n.t("general.create") as string,
key: "tagCreated",
},
{
text: i18n.t("general.update") as string,
key: "tagUpdated",
},
{
text: i18n.t("general.delete") as string,
key: "tagDeleted",
},
{
divider: true,
text: "Category Events",
},
{
text: i18n.t("general.create") as string,
key: "categoryCreated",
},
{
text: i18n.t("general.update") as string,
key: "categoryUpdated",
},
{
text: i18n.t("general.delete") as string,
key: "categoryDeleted",
},
];
return {
...toRefs(state),
openDelete,
optionsKeys,
notifiers,
createNotifierData,
deleteNotifier,
testNotifier,
saveNotifier,
createNewNotifier,
};
},
head: {
title: "Notifiers",
},
});
</script>

View file

@ -11,7 +11,7 @@
<BaseButton create @click="actions.createOne()" />
<v-expansion-panels class="mt-2">
<v-expansion-panel v-for="(webhook, index) in webhooks" :key="index" class="my-2 my-border rounded">
<v-expansion-panel v-for="(webhook, index) in webhooks" :key="index" class="my-2 left-border rounded">
<v-expansion-panel-header disable-icon-rotate class="headline">
<div class="d-flex align-center">
<v-icon large left :color="webhook.enabled ? 'info' : null">
@ -20,8 +20,8 @@
{{ webhook.name }} - {{ webhook.time }}
</div>
<template #actions>
<v-btn color="info" fab small class="ml-2">
<v-icon color="white">
<v-btn small icon class="ml-2">
<v-icon>
{{ $globals.icons.edit }}
</v-icon>
</v-btn>
@ -34,16 +34,28 @@
<v-text-field v-model="webhook.url" label="Webhook Url"></v-text-field>
<v-time-picker v-model="webhook.time" class="elevation-2" ampm-in-title format="ampm"></v-time-picker>
</v-card-text>
<v-card-actions>
<BaseButton secondary color="info">
<template #icon>
{{ $globals.icons.testTube }}
</template>
Test
</BaseButton>
<v-spacer></v-spacer>
<BaseButton delete @click="actions.deleteOne(webhook.id)" />
<BaseButton save @click="actions.updateOne(webhook)" />
<v-card-actions class="py-0 justify-end">
<BaseButtonGroup
:buttons="[
{
icon: $globals.icons.delete,
text: $t('general.delete'),
event: 'delete',
},
{
icon: $globals.icons.testTube,
text: $t('general.test'),
event: 'test',
},
{
icon: $globals.icons.save,
text: $t('general.save'),
event: 'save',
},
]"
@delete="actions.deleteOne(webhook.id)"
@save="actions.updateOne(webhook)"
/>
</v-card-actions>
</v-expansion-panel-content>
</v-expansion-panel>

View file

@ -98,6 +98,15 @@
Setup webhooks that trigger on days that you have have mealplan scheduled.
</UserProfileLinkCard>
</v-col>
<v-col v-if="user.advanced" cols="12" sm="12" md="6">
<UserProfileLinkCard
:link="{ text: 'Manage Notifiers', to: '/user/group/notifiers' }"
:image="require('~/static/svgs/manage-notifiers.svg')"
>
<template #title> Notifiers </template>
Setup email and push notifications that trigger on specific events.
</UserProfileLinkCard>
</v-col>
<v-col v-if="user.canManage" cols="12" sm="12" md="6">
<UserProfileLinkCard
:link="{ text: 'Manage Members', to: '/user/group/members' }"
@ -129,7 +138,7 @@
</section>
</v-container>
</template>
<script lang="ts">
import { computed, defineComponent, useContext, ref, toRefs, reactive } from "@nuxtjs/composition-api";
import UserProfileLinkCard from "@/components/Domain/User/UserProfileLinkCard.vue";
@ -218,4 +227,3 @@ export default defineComponent({
},
});
</script>