1
0
Fork 0
mirror of https://github.com/mealie-recipes/mealie.git synced 2025-08-02 20:15:24 +02:00

Feature/event notifications (#399)

* additional server events

* sort 'recent recipes' by updated

* remove duplicate code

* fixes #396

* set color

* consolidate tag/category pages

* set colors

* list unorganized recipes

* cleanup old code

* remove flash message, switch to global snackbar

* cancel to close

* cleanup

* notifications first pass

* test notification

* complete notification feature

* use background tasks

* add url param

* update documentation

Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
Hayden 2021-05-08 18:29:31 -08:00 committed by GitHub
parent 8923c1ecf8
commit 14b6ab7ec7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 875 additions and 355 deletions

View file

@ -0,0 +1 @@
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 245 240"><style>.st0{fill:#7289DA;}</style><path class="st0" d="M104.4 103.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1.1-6.1-4.5-11.1-10.2-11.1zM140.9 103.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1s-4.5-11.1-10.2-11.1z"/><path class="st0" d="M189.5 20h-134C44.2 20 35 29.2 35 40.6v135.2c0 11.4 9.2 20.6 20.5 20.6h113.4l-5.3-18.5 12.8 11.9 12.1 11.2 21.5 19V40.6c0-11.4-9.2-20.6-20.5-20.6zm-38.6 130.6s-3.6-4.3-6.6-8.1c13.1-3.7 18.1-11.9 18.1-11.9-4.1 2.7-8 4.6-11.5 5.9-5 2.1-9.8 3.5-14.5 4.3-9.6 1.8-18.4 1.3-25.9-.1-5.7-1.1-10.6-2.7-14.7-4.3-2.3-.9-4.8-2-7.3-3.4-.3-.2-.6-.3-.9-.5-.2-.1-.3-.2-.4-.3-1.8-1-2.8-1.7-2.8-1.7s4.8 8 17.5 11.8c-3 3.8-6.7 8.3-6.7 8.3-22.1-.7-30.5-15.2-30.5-15.2 0-32.2 14.4-58.3 14.4-58.3 14.4-10.8 28.1-10.5 28.1-10.5l1 1.2c-18 5.2-26.3 13.1-26.3 13.1s2.2-1.2 5.9-2.9c10.7-4.7 19.2-6 22.7-6.3.6-.1 1.1-.2 1.7-.2 6.1-.8 13-1 20.2-.2 9.5 1.1 19.7 3.9 30.1 9.6 0 0-7.9-7.5-24.9-12.7l1.4-1.6s13.7-.3 28.1 10.5c0 0 14.4 26.1 14.4 58.3 0 0-8.5 14.5-30.6 15.2z"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="602px" height="602px" viewBox="57 57 602 602" version="1.1" xmlns="http://www.w3.org/2000/svg">
<g id="layer1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" transform="translate(58.964119, 58.887520)" opacity="0.91">
<ellipse style="fill: rgb(36, 157, 241); fill-rule: evenodd; stroke: rgb(255, 255, 255); stroke-width: 0;" transform="matrix(-0.674571, 0.73821, -0.73821, -0.674571, 556.833239, 241.613465)" cx="216.308" cy="152.076" rx="296.855" ry="296.855"/>
<path d="M 280.949 172.514 L 355.429 162.714 L 282.909 326.374 L 282.909 326.374 C 295.649 325.394 308.142 321.067 320.389 313.394 L 320.389 313.394 L 320.389 313.394 C 332.642 305.714 343.916 296.077 354.209 284.484 L 354.209 284.484 L 354.209 284.484 C 364.496 272.884 373.396 259.981 380.909 245.774 L 380.909 245.774 L 380.909 245.774 C 388.422 231.561 393.812 217.594 397.079 203.874 L 397.079 203.874 L 397.079 203.874 C 399.039 195.381 399.939 187.214 399.779 179.374 L 399.779 179.374 L 399.779 179.374 C 399.612 171.534 397.569 164.674 393.649 158.794 L 393.649 158.794 L 393.649 158.794 C 389.729 152.914 383.766 148.177 375.759 144.584 L 375.759 144.584 L 375.759 144.584 C 367.759 140.991 356.899 139.194 343.179 139.194 L 343.179 139.194 L 343.179 139.194 C 327.172 139.194 311.409 141.807 295.889 147.034 L 295.889 147.034 L 295.889 147.034 C 280.376 152.261 266.002 159.857 252.769 169.824 L 252.769 169.824 L 252.769 169.824 C 239.542 179.784 228.029 192.197 218.229 207.064 L 218.229 207.064 L 218.229 207.064 C 208.429 221.924 201.406 238.827 197.159 257.774 L 197.159 257.774 L 197.159 257.774 C 195.526 263.981 194.546 268.961 194.219 272.714 L 194.219 272.714 L 194.219 272.714 C 193.892 276.474 193.812 279.577 193.979 282.024 L 193.979 282.024 L 193.979 282.024 C 194.139 284.477 194.462 286.357 194.949 287.664 L 194.949 287.664 L 194.949 287.664 C 195.442 288.971 195.852 290.277 196.179 291.584 L 196.179 291.584 L 196.179 291.584 C 179.519 291.584 167.349 288.234 159.669 281.534 L 159.669 281.534 L 159.669 281.534 C 151.996 274.841 150.119 263.164 154.039 246.504 L 154.039 246.504 L 154.039 246.504 C 157.959 229.191 166.862 212.694 180.749 197.014 L 180.749 197.014 L 180.749 197.014 C 194.629 181.334 211.122 167.531 230.229 155.604 L 230.229 155.604 L 230.229 155.604 C 249.342 143.684 270.249 134.214 292.949 127.194 L 292.949 127.194 L 292.949 127.194 C 315.656 120.167 337.789 116.654 359.349 116.654 L 359.349 116.654 L 359.349 116.654 C 378.296 116.654 394.219 119.347 407.119 124.734 L 407.119 124.734 L 407.119 124.734 C 420.026 130.127 430.072 137.234 437.259 146.054 L 437.259 146.054 L 437.259 146.054 C 444.446 154.874 448.936 165.164 450.729 176.924 L 450.729 176.924 L 450.729 176.924 C 452.529 188.684 451.959 200.934 449.019 213.674 L 449.019 213.674 L 449.019 213.674 C 445.426 229.027 438.646 244.464 428.679 259.984 L 428.679 259.984 L 428.679 259.984 C 418.719 275.497 406.226 289.544 391.199 302.124 L 391.199 302.124 L 391.199 302.124 C 376.172 314.697 358.939 324.904 339.499 332.744 L 339.499 332.744 L 339.499 332.744 C 320.066 340.584 299.406 344.504 277.519 344.504 L 277.519 344.504 L 275.069 344.504 L 212.839 484.154 L 142.279 484.154 L 280.949 172.514 Z" transform="matrix(1, 0, 0, 1, 0, 0)" style="fill: rgb(255, 255, 255); fill-rule: nonzero; white-space: pre;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View file

@ -1,5 +1,6 @@
import { baseURL } from "./api-utils";
import { apiReq } from "./api-utils";
import i18n from "@/i18n.js";
const prefix = baseURL + "about";
@ -12,6 +13,10 @@ const aboutURLs = {
statistics: `${prefix}/statistics`,
events: `${prefix}/events`,
event: id => `${prefix}/events/${id}`,
allNotifications: `${prefix}/events/notifications`,
testNotifications: `${prefix}/events/notifications/test`,
notification: id => `${prefix}/events/notifications/${id}`,
};
export const aboutAPI = {
@ -27,6 +32,39 @@ export const aboutAPI = {
const resposne = await apiReq.delete(aboutURLs.events);
return resposne.data;
},
async allEventNotifications() {
const response = await apiReq.get(aboutURLs.allNotifications);
return response.data;
},
async createNotification(data) {
const response = await apiReq.post(aboutURLs.allNotifications, data);
return response.data;
},
async deleteNotification(id) {
const response = await apiReq.delete(aboutURLs.notification(id));
return response.data;
},
async testNotificationByID(id) {
const response = await apiReq.post(
aboutURLs.testNotifications,
{ id: id },
() => i18n.t("events.something-went-wrong"),
() => i18n.t("events.test-message-sent")
);
return response.data;
},
async testNotificationByURL(url) {
const response = await apiReq.post(
aboutURLs.testNotifications,
{ test_url: url },
() => i18n.t("events.something-went-wrong"),
() => i18n.t("events.test-message-sent")
);
return response.data;
},
// async getAppInfo() {
// const response = await apiReq.get(aboutURLs.version);
// return response.data;

View file

@ -71,7 +71,7 @@ export const userAPI = {
},
delete(id) {
return apiReq.delete(usersURLs.userID(id), null, deleteErrorText, function() {
return apiReq.delete(usersURLs.userID(id), null, deleteErrorText, () => {
return i18n.t("user.user-deleted");
});
},

View file

@ -2,7 +2,7 @@
<div>
<slot name="open" v-bind="{ open }"> </slot>
<v-dialog v-model="dialog" :width="modalWidth + 'px'" :content-class="top ? 'top-dialog' : undefined">
<v-card class="pb-10" height="100%">
<v-card height="100%">
<v-app-bar dark :color="color" class="mt-n1 mb-0">
<v-icon large left>
{{ titleIcon }}
@ -11,7 +11,9 @@
<v-spacer></v-spacer>
</v-app-bar>
<v-progress-linear class="mt-1" v-if="loading" indeterminate color="primary"></v-progress-linear>
<slot> </slot>
<slot v-bind="{ submitEvent }"> </slot>
<v-card-actions>
<slot name="card-actions">
<v-btn text color="grey" @click="dialog = false">
@ -22,13 +24,15 @@
<v-btn color="error" text @click="deleteEvent" v-if="$listeners.delete">
{{ $t("general.delete") }}
</v-btn>
<v-btn color="success" @click="submitEvent">
<v-btn color="success" type="submit" @click="submitEvent">
{{ submitText }}
</v-btn>
</slot>
</v-card-actions>
<slot name="below-actions"> </slot>
<div class="pb-4" v-if="$slots['below-actions']">
<slot name="below-actions"> </slot>
</div>
</v-card>
</v-dialog>
</div>
@ -59,6 +63,9 @@ export default {
submitText: {
default: () => i18n.t("general.create"),
},
keepOpen: {
default: false,
},
},
data() {
return {
@ -68,7 +75,7 @@ export default {
},
computed: {
determineClose() {
return this.submitted && !this.loading;
return this.submitted && !this.loading && !this.keepOpen;
},
},
watch: {
@ -82,6 +89,7 @@ export default {
},
methods: {
submitEvent() {
console.log("Submit");
this.$emit("submit");
this.submitted = true;
},

View file

@ -1,6 +1,10 @@
<template>
<div class="text-center ma-2">
<v-snackbar v-model="snackbar.open" top :color="snackbar.color" timeout="3500">
<v-icon dark left>
{{ icon }}
</v-icon>
{{ snackbar.title }}
{{ snackbar.text }}
@ -25,6 +29,18 @@ export default {
return this.$store.getters.getSnackbar;
},
},
icon() {
switch (this.snackbar.color) {
case "error":
return "mdi-alert";
case "success":
return "mdi-checkbox-marked-circle";
case "info":
return "mdi-information";
default:
return "mdi-bell-alert";
}
},
},
};
</script>

View file

@ -31,6 +31,16 @@
"category-updated": "Category updated",
"uncategorized-count": "Uncategorized {count}"
},
"events": {
"notification": "Notification",
"apprise-url": "Apprise URL",
"subscribed-events": "Subscribed Events",
"scheduled": "Scheduled",
"database": "Database",
"test-message-sent": "Test Message Sent",
"something-went-wrong": "Something Went Wrong!",
"new-notification-form-description": "Mealie uses the Apprise library to generate notifications. They offer many options for services to use for notifications. Refer to their wiki for a comprehensive guide on how to create the URL for your service. If available, selecting the type of your notification may include extra features."
},
"general": {
"apply": "Apply",
"cancel": "Cancel",
@ -55,6 +65,7 @@
"file-uploaded": "File uploaded",
"filter": "Filter",
"friday": "Friday",
"general": "General",
"get": "Get",
"image": "Image",
"image-upload-failed": "Image upload failed",
@ -70,6 +81,7 @@
"random": "Random",
"rating": "Rating",
"recent": "Recent",
"recipe": "Recipe",
"recipes": "Recipes",
"rename-object": "Rename {0}",
"reset": "Reset",
@ -88,6 +100,8 @@
"thursday": "Thursday",
"token": "Token",
"tuesday": "Tuesday",
"type": "Type",
"test": "Test",
"update": "Update",
"updated": "Updated",
"upload": "Upload",

View file

@ -1,6 +1,6 @@
<template>
<div>
<base-dialog
<BaseDialog
ref="assignDialog"
title-icon="mdi-tag"
color="primary"
@ -33,7 +33,7 @@
:single-column="true"
/>
</template>
</base-dialog>
</BaseDialog>
<v-btn @click="openDialog" small color="success">
{{ $t("settings.toolbox.bulk-assign") }}

View file

@ -0,0 +1,236 @@
<template>
<div>
<v-card outlined class="mt-n1">
<v-card-actions>
<v-spacer></v-spacer>
<BaseDialog
:keep-open="keepDialogOpen"
title-icon="mdi-bell-alert"
:title="$t('general.new') + ' ' + $t('events.notification')"
@submit="createNotification"
>
<template v-slot:open="{ open }">
<v-btn small color="info" @click="open">
<v-icon left>
mdi-plus
</v-icon>
{{ $t("events.notification") }}
</v-btn>
</template>
<template v-slot:default>
<v-card-text class="mt-2">
{{ $t("events.new-notification-form-description") }}
<div class="d-flex justify-space-around mt-1 mb-3">
<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>
<v-form ref="notificationForm">
<v-select
:label="$t('general.type')"
:rules="[existsRule]"
:items="notificationTypes"
item-value="text"
v-model="newNotification.type"
>
</v-select>
<v-text-field :rules="[existsRule]" :label="$t('general.name')" v-model="newNotification.name">
</v-text-field>
<v-text-field
required
:rules="[existsRule]"
:label="$t('events.apprise-url')"
v-model="newNotification.notificationUrl"
>
</v-text-field>
<v-btn class="d-flex ml-auto" small color="info" @click="testByURL(newNotification.notificationUrl)">
<v-icon left> mdi-test-tube</v-icon>
{{ $t("general.test") }}
</v-btn>
<v-subheader class="pa-0 mb-0">
{{ $t("events.subscribed-events") }}
</v-subheader>
<v-row class="mt-1">
<v-col cols="3" v-for="(item, key, index) in newNotificationOptions" :key="index">
<v-checkbox class="my-n3 py-0" v-model="newNotificationOptions[key]" :label="key"> </v-checkbox>
</v-col>
</v-row>
</v-form>
</v-card-text>
</template>
</BaseDialog>
</v-card-actions>
<v-simple-table>
<template v-slot:default>
<thead>
<tr>
<th class="text-center">
{{ $t("general.type") }}
</th>
<th class="text-center">
{{ $t("general.name") }}
</th>
<th class="text-center">
{{ $t("general.general") }}
</th>
<th class="text-center">
{{ $t("general.recipe") }}
</th>
<th class="text-center">
{{ $t("events.database") }}
</th>
<th class="text-center">
{{ $t("events.scheduled") }}
</th>
<th class="text-center">
{{ $t("settings.migrations") }}
</th>
<th class="text-center">
{{ $t("group.group") }}
</th>
<th class="text-center">
{{ $t("user.user") }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in notifications" :key="index">
<td>
<v-avatar size="35" class="ma-1" :color="getIcon(item.type).icon ? 'primary' : undefined">
<v-icon dark v-if="getIcon(item.type).icon"> {{ getIcon(item.type).icon }}</v-icon>
<v-img v-else :src="getIcon(item.type).image"> </v-img>
</v-avatar>
{{ item.type }}
</td>
<td>
{{ item.name }}
</td>
<td class="text-center">
<v-icon color="success"> {{ item.general ? "mdi-check" : "" }} </v-icon>
</td>
<td class="text-center">
<v-icon color="success"> {{ item.recipe ? "mdi-check" : "" }} </v-icon>
</td>
<td class="text-center">
<v-icon color="success"> {{ item.backup ? "mdi-check" : "" }} </v-icon>
</td>
<td class="text-center">
<v-icon color="success"> {{ item.scheduled ? "mdi-check" : "" }} </v-icon>
</td>
<td class="text-center">
<v-icon color="success"> {{ item.migration ? "mdi-check" : "" }} </v-icon>
</td>
<td class="text-center">
<v-icon color="success"> {{ item.group ? "mdi-check" : "" }} </v-icon>
</td>
<td class="text-center">
<v-icon color="success"> {{ item.user ? "mdi-check" : "" }} </v-icon>
</td>
<td>
<v-btn class="mx-1" small color="error" @click="deleteNotification(item.id)">
<v-icon> mdi-delete </v-icon>
{{ $t("general.delete") }}
</v-btn>
<v-btn small color="info" @click="testByID(item.id)">
<v-icon left> mdi-test-tube</v-icon>
{{ $t("general.test") }}
</v-btn>
</td>
</tr>
</tbody>
</template>
</v-simple-table>
</v-card>
</div>
</template>
<script>
import BaseDialog from "@/components/UI/Dialogs/BaseDialog";
import { api } from "@/api";
import { validators } from "@/mixins/validators";
export default {
components: {
BaseDialog,
},
mixins: [validators],
data() {
return {
keepDialogOpen: false,
notifications: [],
newNotification: {
type: "General",
name: "",
notificationUrl: "",
},
newNotificationOptions: {
general: true,
recipe: true,
backup: true,
scheduled: true,
migration: true,
group: true,
user: true,
},
notificationTypes: [
{
text: "General",
icon: "mdi-bell-alert",
},
{
text: "Discord",
image: "./static/discord.svg",
},
{
text: "Gotify",
image: "./static/gotify.png",
},
{
text: "Home Assistant",
image: "./static/home-assistant.png",
},
{
text: "Pushover",
image: "./static/pushover.svg",
},
],
};
},
mounted() {
this.getAllNotifications();
},
methods: {
getIcon(textValue) {
return this.notificationTypes.find(x => x.text === textValue);
},
async getAllNotifications() {
this.notifications = await api.about.allEventNotifications();
},
async createNotification() {
if (this.$refs.notificationForm.validate()) {
this.keepDialogOpen = false;
await api.about.createNotification({ ...this.newNotification, ...this.newNotificationOptions });
this.getAllNotifications();
} else {
this.keepDialogOpen = true;
}
},
async deleteNotification(id) {
await api.about.deleteNotification(id);
this.getAllNotifications();
},
async testByID(id) {
await api.about.testNotificationByID(id);
},
async testByURL(url) {
await api.about.testNotificationByURL(url);
},
},
};
</script>

View file

@ -4,6 +4,10 @@
<v-tabs v-model="tab" background-color="primary" centered dark icons-and-text>
<v-tabs-slider></v-tabs-slider>
<v-tab href="#event-notifications">
Notify
<v-icon>mdi-bell-alert</v-icon>
</v-tab>
<v-tab href="#category-editor">
{{ $t("recipe.categories") }}
<v-icon>mdi-tag-multiple-outline</v-icon>
@ -20,20 +24,23 @@
</v-tabs>
<v-tabs-items v-model="tab">
<v-tab-item value="event-notifications"> <EventNotification /></v-tab-item>
<v-tab-item value="category-editor"> <CategoryTagEditor :is-tags="false"/></v-tab-item>
<v-tab-item value="tag-editor"> <CategoryTagEditor :is-tags="true" /> </v-tab-item>
<v-tab-item value="organize"> <RecipeOrganizer :is-tags="true" /> </v-tab-item>
<v-tab-item value="organize"> <RecipeOrganizer /> </v-tab-item>
</v-tabs-items>
</v-card>
</div>
</template>
<script>
import EventNotification from "./EventNotification";
import CategoryTagEditor from "./CategoryTagEditor";
import RecipeOrganizer from "./RecipeOrganizer";
export default {
components: {
CategoryTagEditor,
EventNotification,
RecipeOrganizer,
},
computed: {