mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-08-05 05:25:26 +02:00
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:
parent
50a341ed3f
commit
190773c5d7
74 changed files with 1992 additions and 1229 deletions
|
@ -1,41 +0,0 @@
|
|||
import { BaseCRUDAPI } from "../_base";
|
||||
|
||||
export type EventCategory = "general" | "recipe" | "backup" | "scheduled" | "migration" | "group" | "user";
|
||||
export type DeclaredTypes = "General" | "Discord" | "Gotify" | "Pushover" | "Home Assistant";
|
||||
export type GotifyPriority = "low" | "moderate" | "normal" | "high";
|
||||
|
||||
export interface EventNotification {
|
||||
id?: number;
|
||||
name?: string;
|
||||
type?: DeclaredTypes & string;
|
||||
general?: boolean;
|
||||
recipe?: boolean;
|
||||
backup?: boolean;
|
||||
scheduled?: boolean;
|
||||
migration?: boolean;
|
||||
group?: boolean;
|
||||
user?: boolean;
|
||||
}
|
||||
|
||||
export interface CreateEventNotification extends EventNotification {
|
||||
notificationUrl?: string;
|
||||
}
|
||||
|
||||
const prefix = "/api";
|
||||
|
||||
const routes = {
|
||||
aboutEventsNotifications: `${prefix}/about/events/notifications`,
|
||||
aboutEventsNotificationsTest: `${prefix}/about/events/notifications/test`,
|
||||
|
||||
aboutEventsNotificationsId: (id: number) => `${prefix}/about/events/notifications/${id}`,
|
||||
};
|
||||
|
||||
export class NotificationsAPI extends BaseCRUDAPI<EventNotification, CreateEventNotification> {
|
||||
baseRoute = routes.aboutEventsNotifications;
|
||||
itemRoute = routes.aboutEventsNotificationsId;
|
||||
/** Returns the Group Data for the Current User
|
||||
*/
|
||||
async testNotification(id: number | null = null, testUrl: string | null = null) {
|
||||
return await this.requests.post(routes.aboutEventsNotificationsTest, { id, testUrl });
|
||||
}
|
||||
}
|
18
frontend/api/class-interfaces/group-event-notifier.ts
Normal file
18
frontend/api/class-interfaces/group-event-notifier.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { BaseCRUDAPI } from "../_base";
|
||||
import { GroupEventNotifierCreate, GroupEventNotifierOut } from "~/types/api-types/group";
|
||||
|
||||
const prefix = "/api";
|
||||
|
||||
const routes = {
|
||||
eventNotifier: `${prefix}/groups/events/notifications`,
|
||||
eventNotifierId: (id: string | number) => `${prefix}/groups/events/notifications/${id}`,
|
||||
};
|
||||
|
||||
export class GroupEventNotifierApi extends BaseCRUDAPI<GroupEventNotifierOut, GroupEventNotifierCreate> {
|
||||
baseRoute = routes.eventNotifier;
|
||||
itemRoute = routes.eventNotifierId;
|
||||
|
||||
async test(itemId: string) {
|
||||
return await this.requests.post(`${this.baseRoute}/${itemId}/test`, {});
|
||||
}
|
||||
}
|
|
@ -7,7 +7,6 @@ import { UploadFile } from "./class-interfaces/upload";
|
|||
import { CategoriesAPI } from "./class-interfaces/categories";
|
||||
import { TagsAPI } from "./class-interfaces/tags";
|
||||
import { UtilsAPI } from "./class-interfaces/utils";
|
||||
import { NotificationsAPI } from "./class-interfaces/event-notifications";
|
||||
import { FoodAPI } from "./class-interfaces/recipe-foods";
|
||||
import { UnitAPI } from "./class-interfaces/recipe-units";
|
||||
import { CookbookAPI } from "./class-interfaces/group-cookbooks";
|
||||
|
@ -23,10 +22,10 @@ import { GroupMigrationApi } from "./class-interfaces/group-migrations";
|
|||
import { GroupReportsApi } from "./class-interfaces/group-reports";
|
||||
import { ShoppingApi } from "./class-interfaces/group-shopping-lists";
|
||||
import { MultiPurposeLabelsApi } from "./class-interfaces/group-multiple-purpose-labels";
|
||||
import { GroupEventNotifierApi } from "./class-interfaces/group-event-notifier";
|
||||
import { ApiRequestInstance } from "~/types/api";
|
||||
|
||||
class Api {
|
||||
// private static instance: Api;
|
||||
public recipes: RecipeAPI;
|
||||
public users: UserApi;
|
||||
public groups: GroupAPI;
|
||||
|
@ -35,7 +34,6 @@ class Api {
|
|||
public categories: CategoriesAPI;
|
||||
public tags: TagsAPI;
|
||||
public utils: UtilsAPI;
|
||||
public notifications: NotificationsAPI;
|
||||
public foods: FoodAPI;
|
||||
public units: UnitAPI;
|
||||
public cookbooks: CookbookAPI;
|
||||
|
@ -50,14 +48,10 @@ class Api {
|
|||
public tools: ToolsApi;
|
||||
public shopping: ShoppingApi;
|
||||
public multiPurposeLabels: MultiPurposeLabelsApi;
|
||||
// Utils
|
||||
public groupEventNotifier: GroupEventNotifierApi;
|
||||
public upload: UploadFile;
|
||||
|
||||
constructor(requests: ApiRequestInstance) {
|
||||
// if (Api.instance instanceof Api) {
|
||||
// return Api.instance;
|
||||
// }
|
||||
|
||||
// Recipes
|
||||
this.recipes = new RecipeAPI(requests);
|
||||
this.categories = new CategoriesAPI(requests);
|
||||
|
@ -84,7 +78,6 @@ class Api {
|
|||
// Admin
|
||||
this.events = new EventsAPI(requests);
|
||||
this.backups = new BackupAPI(requests);
|
||||
this.notifications = new NotificationsAPI(requests);
|
||||
|
||||
// Utils
|
||||
this.upload = new UploadFile(requests);
|
||||
|
@ -92,9 +85,9 @@ class Api {
|
|||
|
||||
this.email = new EmailAPI(requests);
|
||||
this.bulk = new BulkActionsAPI(requests);
|
||||
this.groupEventNotifier = new GroupEventNotifierApi(requests);
|
||||
|
||||
Object.freeze(this);
|
||||
// Api.instance = this;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -107,19 +107,43 @@
|
|||
|
||||
{{ $t("recipe.step-index", { step: index + 1 }) }}
|
||||
|
||||
<div class="ml-auto">
|
||||
<BaseOverflowButton
|
||||
v-if="edit"
|
||||
small
|
||||
mode="event"
|
||||
:items="actionEvents || []"
|
||||
@merge-above="mergeAbove(index - 1, index)"
|
||||
@toggle-section="toggleShowTitle(step.id)"
|
||||
@link-ingredients="openDialog(index, step.ingredientReferences, step.text)"
|
||||
>
|
||||
</BaseOverflowButton>
|
||||
</div>
|
||||
<v-icon v-if="edit" class="handle">{{ $globals.icons.arrowUpDown }}</v-icon>
|
||||
<template v-if="edit">
|
||||
<v-icon class="handle ml-auto mr-2">{{ $globals.icons.arrowUpDown }}</v-icon>
|
||||
<div>
|
||||
<BaseButtonGroup
|
||||
:buttons="[
|
||||
{
|
||||
icon: $globals.icons.dotsVertical,
|
||||
text: $t('general.delete'),
|
||||
children: [
|
||||
{
|
||||
text: 'Toggle Section',
|
||||
event: 'toggle-section',
|
||||
},
|
||||
{
|
||||
text: 'Link Ingredients',
|
||||
event: 'link-ingredients',
|
||||
},
|
||||
{
|
||||
text: 'Merge Above',
|
||||
event: 'merge-above',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: previewStates[index] ? $globals.icons.edit : $globals.icons.eye,
|
||||
text: previewStates[index] ? $t('general.edit') : 'Preview Markdown',
|
||||
event: 'preview-step',
|
||||
},
|
||||
]"
|
||||
@merge-above="mergeAbove(index - 1, index)"
|
||||
@toggle-section="toggleShowTitle(step.id)"
|
||||
@link-ingredients="openDialog(index, step.ingredientReferences, step.text)"
|
||||
@preview-step="togglePreviewState(index)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<v-fade-transition>
|
||||
<v-icon v-show="isChecked(index)" size="24" class="ml-auto" color="success">
|
||||
{{ $globals.icons.checkboxMarkedCircle }}
|
||||
|
@ -127,7 +151,11 @@
|
|||
</v-fade-transition>
|
||||
</v-card-title>
|
||||
<v-card-text v-if="edit">
|
||||
<MarkdownEditor v-model="value[index]['text']" />
|
||||
<MarkdownEditor
|
||||
v-model="value[index]['text']"
|
||||
:preview.sync="previewStates[index]"
|
||||
:display-preview="false"
|
||||
/>
|
||||
<div
|
||||
v-for="ing in step.ingredientReferences"
|
||||
:key="ing.referenceId"
|
||||
|
@ -410,7 +438,17 @@ export default defineComponent({
|
|||
}
|
||||
}
|
||||
|
||||
const previewStates = ref<boolean[]>([]);
|
||||
|
||||
function togglePreviewState(index: number) {
|
||||
const temp = [...previewStates.value];
|
||||
temp[index] = !temp[index];
|
||||
previewStates.value = temp;
|
||||
}
|
||||
|
||||
return {
|
||||
togglePreviewState,
|
||||
previewStates,
|
||||
...toRefs(state),
|
||||
actionEvents,
|
||||
activeRefs,
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<template>
|
||||
<v-card outlined nuxt :to="link.to" height="100%" class="d-flex flex-column">
|
||||
<div v-if="$vuetify.breakpoint.smAndDown" class="pa-2 mx-auto">
|
||||
<v-img max-width="150px" max-height="125" :src="image"></v-img>
|
||||
<v-img max-width="150px" max-height="125" :src="image" />
|
||||
</div>
|
||||
<div class="d-flex align-center justify-space-between">
|
||||
<div class="d-flex justify-space-between">
|
||||
<div>
|
||||
<v-card-title class="headline pb-0">
|
||||
<slot name="title"> </slot>
|
||||
|
@ -14,7 +14,7 @@
|
|||
</v-card-text>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="$vuetify.breakpoint.mdAndUp" class="px-10">
|
||||
<div v-if="$vuetify.breakpoint.mdAndUp" class="py-2 px-10 my-auto">
|
||||
<v-img max-width="150px" max-height="125" :src="image"></v-img>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -189,7 +189,7 @@ export default defineComponent({
|
|||
return {
|
||||
...toRefs(state),
|
||||
drawer,
|
||||
}
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -2,5 +2,21 @@
|
|||
<v-alert border="left" colored-border type="warning" elevation="2" :icon="$globals.icons.alert">
|
||||
<b>Experimental Feature</b>
|
||||
<div>This page contains experimental or still-baking features. Please excuse the mess.</div>
|
||||
<div v-if="issue != ''" class="py-2">
|
||||
<a :href="issue" target="_blank"> Track our progress here</a>
|
||||
</div>
|
||||
</v-alert>
|
||||
</template>
|
||||
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
props: {
|
||||
issue: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "",
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -1,17 +1,19 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-tabs v-model="tab" height="30px" class="my-1">
|
||||
<v-tab>
|
||||
<v-icon small left> {{ $globals.icons.edit }}</v-icon>
|
||||
Edit
|
||||
</v-tab>
|
||||
<v-tab>
|
||||
<v-icon small left> {{ $globals.icons.eye }}</v-icon>
|
||||
Preview
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
<div v-if="displayPreview" class="d-flex justify-end">
|
||||
<BaseButtonGroup
|
||||
:buttons="[
|
||||
{
|
||||
icon: previewState ? $globals.icons.edit : $globals.icons.eye,
|
||||
text: previewState ? $t('general.edit') : 'Preview Markdown',
|
||||
event: 'toggle',
|
||||
},
|
||||
]"
|
||||
@toggle="previewState = !previewState"
|
||||
/>
|
||||
</div>
|
||||
<v-textarea
|
||||
v-if="tab == 0"
|
||||
v-if="!previewState"
|
||||
v-model="inputVal"
|
||||
:class="label == '' ? '' : 'mt-5'"
|
||||
:label="label"
|
||||
|
@ -27,7 +29,7 @@
|
|||
// @ts-ignore
|
||||
import VueMarkdown from "@adapttive/vue-markdown";
|
||||
|
||||
import { defineComponent, reactive, toRefs, computed } from "@nuxtjs/composition-api";
|
||||
import { defineComponent, computed, ref } from "@nuxtjs/composition-api";
|
||||
|
||||
export default defineComponent({
|
||||
name: "MarkdownEditor",
|
||||
|
@ -43,10 +45,28 @@ export default defineComponent({
|
|||
type: String,
|
||||
default: "",
|
||||
},
|
||||
preview: {
|
||||
type: Boolean,
|
||||
default: undefined,
|
||||
},
|
||||
displayPreview: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
setup(props, context) {
|
||||
const state = reactive({
|
||||
tab: 0,
|
||||
const fallbackPreview = ref(false);
|
||||
const previewState = computed({
|
||||
get: () => {
|
||||
return props.preview ?? fallbackPreview.value;
|
||||
},
|
||||
set: (val) => {
|
||||
if (props.preview) {
|
||||
context.emit("input:preview", val);
|
||||
} else {
|
||||
fallbackPreview.value = val;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const inputVal = computed({
|
||||
|
@ -58,10 +78,11 @@ export default defineComponent({
|
|||
},
|
||||
});
|
||||
return {
|
||||
previewState,
|
||||
inputVal,
|
||||
...toRefs(state),
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
|
|
|
@ -1,89 +0,0 @@
|
|||
import { useAsync, ref } from "@nuxtjs/composition-api";
|
||||
import { useAsyncKey } from "./use-utils";
|
||||
import { CreateEventNotification } from "@/api/class-interfaces/event-notifications";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
|
||||
const notificationTypes = ["General", "Discord", "Gotify", "Pushover", "Home Assistant"];
|
||||
|
||||
export const useNotifications = function () {
|
||||
const api = useUserApi();
|
||||
const loading = ref(false);
|
||||
|
||||
const createNotificationData = ref<CreateEventNotification>({
|
||||
name: "",
|
||||
type: "General",
|
||||
general: true,
|
||||
recipe: true,
|
||||
backup: true,
|
||||
scheduled: true,
|
||||
migration: true,
|
||||
group: true,
|
||||
user: true,
|
||||
notificationUrl: "",
|
||||
});
|
||||
|
||||
const deleteTarget = ref(0);
|
||||
|
||||
function getNotifications() {
|
||||
loading.value = true;
|
||||
const notifications = useAsync(async () => {
|
||||
const { data } = await api.notifications.getAll();
|
||||
return data;
|
||||
}, useAsyncKey());
|
||||
loading.value = false;
|
||||
return notifications;
|
||||
}
|
||||
|
||||
async function refreshNotifications() {
|
||||
loading.value = true;
|
||||
const { data } = await api.notifications.getAll();
|
||||
if (data) {
|
||||
notifications.value = data;
|
||||
}
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
async function createNotification() {
|
||||
if (createNotificationData.value.name === "" || createNotificationData.value.notificationUrl === "") {
|
||||
return;
|
||||
}
|
||||
const { response } = await api.notifications.createOne(createNotificationData.value);
|
||||
|
||||
if (response?.status === 200) {
|
||||
refreshNotifications();
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteNotification() {
|
||||
const { response } = await api.notifications.deleteOne(deleteTarget.value);
|
||||
if (response?.status === 200) {
|
||||
refreshNotifications();
|
||||
}
|
||||
}
|
||||
|
||||
async function testById(id: number) {
|
||||
const { data } = await api.notifications.testNotification(id, null);
|
||||
console.log(data);
|
||||
}
|
||||
|
||||
async function testByUrl(testUrl: string) {
|
||||
const { data } = await api.notifications.testNotification(null, testUrl);
|
||||
console.log(data);
|
||||
}
|
||||
|
||||
const notifications = getNotifications();
|
||||
|
||||
return {
|
||||
createNotification,
|
||||
deleteNotification,
|
||||
refreshNotifications,
|
||||
getNotifications,
|
||||
testById,
|
||||
testByUrl,
|
||||
notifications,
|
||||
loading,
|
||||
createNotificationData,
|
||||
notificationTypes,
|
||||
deleteTarget,
|
||||
};
|
||||
};
|
|
@ -60,11 +60,6 @@ export default defineComponent({
|
|||
to: "/admin/toolbox",
|
||||
title: i18n.t("sidebar.toolbox"),
|
||||
children: [
|
||||
{
|
||||
icon: $globals.icons.bellAlert,
|
||||
to: "/admin/toolbox/notifications",
|
||||
title: i18n.t("events.notification"),
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.foods,
|
||||
to: "/admin/toolbox/foods",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
307
frontend/pages/user/group/notifiers.vue
Normal file
307
frontend/pages/user/group/notifiers.vue
Normal 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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
1
frontend/static/svgs/manage-notifiers.svg
Normal file
1
frontend/static/svgs/manage-notifiers.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 9.2 KiB |
|
@ -5,12 +5,211 @@
|
|||
/* Do not modify it by hand - just update the pydantic models and then re-run the script
|
||||
*/
|
||||
|
||||
export type SupportedMigrations = "nextcloud" | "chowdown" | "paprika" | "mealie_alpha";
|
||||
|
||||
export interface CreateGroupPreferences {
|
||||
privateGroup?: boolean;
|
||||
firstDayOfWeek?: number;
|
||||
recipePublic?: boolean;
|
||||
recipeShowNutrition?: boolean;
|
||||
recipeShowAssets?: boolean;
|
||||
recipeLandscapeView?: boolean;
|
||||
recipeDisableComments?: boolean;
|
||||
recipeDisableAmount?: boolean;
|
||||
groupId: string;
|
||||
}
|
||||
export interface CreateInviteToken {
|
||||
uses: number;
|
||||
}
|
||||
export interface CreateWebhook {
|
||||
enabled?: boolean;
|
||||
name?: string;
|
||||
url?: string;
|
||||
time?: string;
|
||||
}
|
||||
export interface DataMigrationCreate {
|
||||
sourceType: SupportedMigrations;
|
||||
}
|
||||
export interface EmailInitationResponse {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}
|
||||
export interface EmailInvitation {
|
||||
email: string;
|
||||
token: string;
|
||||
}
|
||||
export interface GroupAdminUpdate {
|
||||
id: string;
|
||||
name: string;
|
||||
preferences: UpdateGroupPreferences;
|
||||
}
|
||||
export interface UpdateGroupPreferences {
|
||||
privateGroup?: boolean;
|
||||
firstDayOfWeek?: number;
|
||||
recipePublic?: boolean;
|
||||
recipeShowNutrition?: boolean;
|
||||
recipeShowAssets?: boolean;
|
||||
recipeLandscapeView?: boolean;
|
||||
recipeDisableComments?: boolean;
|
||||
recipeDisableAmount?: boolean;
|
||||
}
|
||||
export interface GroupDataExport {
|
||||
id: string;
|
||||
groupId: string;
|
||||
name: string;
|
||||
filename: string;
|
||||
path: string;
|
||||
size: string;
|
||||
expires: string;
|
||||
}
|
||||
export interface GroupEventNotifierCreate {
|
||||
name: string;
|
||||
appriseUrl: string;
|
||||
}
|
||||
/**
|
||||
* These events are in-sync with the EventTypes found in the EventBusService.
|
||||
* If you modify this, make sure to update the EventBusService as well.
|
||||
*/
|
||||
export interface GroupEventNotifierOptions {
|
||||
recipeCreated?: boolean;
|
||||
recipeUpdated?: boolean;
|
||||
recipeDeleted?: boolean;
|
||||
userSignup?: boolean;
|
||||
dataMigrations?: boolean;
|
||||
dataExport?: boolean;
|
||||
dataImport?: boolean;
|
||||
mealplanEntryCreated?: boolean;
|
||||
shoppingListCreated?: boolean;
|
||||
shoppingListUpdated?: boolean;
|
||||
shoppingListDeleted?: boolean;
|
||||
cookbookCreated?: boolean;
|
||||
cookbookUpdated?: boolean;
|
||||
cookbookDeleted?: boolean;
|
||||
tagCreated?: boolean;
|
||||
tagUpdated?: boolean;
|
||||
tagDeleted?: boolean;
|
||||
categoryCreated?: boolean;
|
||||
categoryUpdated?: boolean;
|
||||
categoryDeleted?: boolean;
|
||||
}
|
||||
/**
|
||||
* These events are in-sync with the EventTypes found in the EventBusService.
|
||||
* If you modify this, make sure to update the EventBusService as well.
|
||||
*/
|
||||
export interface GroupEventNotifierOptionsOut {
|
||||
recipeCreated?: boolean;
|
||||
recipeUpdated?: boolean;
|
||||
recipeDeleted?: boolean;
|
||||
userSignup?: boolean;
|
||||
dataMigrations?: boolean;
|
||||
dataExport?: boolean;
|
||||
dataImport?: boolean;
|
||||
mealplanEntryCreated?: boolean;
|
||||
shoppingListCreated?: boolean;
|
||||
shoppingListUpdated?: boolean;
|
||||
shoppingListDeleted?: boolean;
|
||||
cookbookCreated?: boolean;
|
||||
cookbookUpdated?: boolean;
|
||||
cookbookDeleted?: boolean;
|
||||
tagCreated?: boolean;
|
||||
tagUpdated?: boolean;
|
||||
tagDeleted?: boolean;
|
||||
categoryCreated?: boolean;
|
||||
categoryUpdated?: boolean;
|
||||
categoryDeleted?: boolean;
|
||||
id: string;
|
||||
}
|
||||
/**
|
||||
* These events are in-sync with the EventTypes found in the EventBusService.
|
||||
* If you modify this, make sure to update the EventBusService as well.
|
||||
*/
|
||||
export interface GroupEventNotifierOptionsSave {
|
||||
recipeCreated?: boolean;
|
||||
recipeUpdated?: boolean;
|
||||
recipeDeleted?: boolean;
|
||||
userSignup?: boolean;
|
||||
dataMigrations?: boolean;
|
||||
dataExport?: boolean;
|
||||
dataImport?: boolean;
|
||||
mealplanEntryCreated?: boolean;
|
||||
shoppingListCreated?: boolean;
|
||||
shoppingListUpdated?: boolean;
|
||||
shoppingListDeleted?: boolean;
|
||||
cookbookCreated?: boolean;
|
||||
cookbookUpdated?: boolean;
|
||||
cookbookDeleted?: boolean;
|
||||
tagCreated?: boolean;
|
||||
tagUpdated?: boolean;
|
||||
tagDeleted?: boolean;
|
||||
categoryCreated?: boolean;
|
||||
categoryUpdated?: boolean;
|
||||
categoryDeleted?: boolean;
|
||||
notifierId: string;
|
||||
}
|
||||
export interface GroupEventNotifierOut {
|
||||
id: string;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
groupId: string;
|
||||
options: GroupEventNotifierOptionsOut;
|
||||
}
|
||||
export interface GroupEventNotifierPrivate {
|
||||
id: string;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
groupId: string;
|
||||
options: GroupEventNotifierOptionsOut;
|
||||
appriseUrl: string;
|
||||
}
|
||||
export interface GroupEventNotifierSave {
|
||||
name: string;
|
||||
appriseUrl: string;
|
||||
enabled?: boolean;
|
||||
groupId: string;
|
||||
options?: GroupEventNotifierOptions;
|
||||
}
|
||||
export interface GroupEventNotifierUpdate {
|
||||
name: string;
|
||||
appriseUrl?: string;
|
||||
enabled?: boolean;
|
||||
groupId: string;
|
||||
options?: GroupEventNotifierOptions;
|
||||
id: string;
|
||||
}
|
||||
export interface IngredientFood {
|
||||
name: string;
|
||||
description?: string;
|
||||
id: number;
|
||||
}
|
||||
export interface IngredientUnit {
|
||||
name: string;
|
||||
description?: string;
|
||||
fraction?: boolean;
|
||||
abbreviation?: string;
|
||||
id: number;
|
||||
}
|
||||
export interface MultiPurposeLabelSummary {
|
||||
name: string;
|
||||
groupId: string;
|
||||
id: string;
|
||||
}
|
||||
export interface ReadGroupPreferences {
|
||||
privateGroup?: boolean;
|
||||
firstDayOfWeek?: number;
|
||||
recipePublic?: boolean;
|
||||
recipeShowNutrition?: boolean;
|
||||
recipeShowAssets?: boolean;
|
||||
recipeLandscapeView?: boolean;
|
||||
recipeDisableComments?: boolean;
|
||||
recipeDisableAmount?: boolean;
|
||||
groupId: string;
|
||||
id: number;
|
||||
}
|
||||
export interface ReadInviteToken {
|
||||
token: string;
|
||||
usesLeft: number;
|
||||
groupId: string;
|
||||
}
|
||||
export interface ReadWebhook {
|
||||
enabled?: boolean;
|
||||
name?: string;
|
||||
|
@ -19,6 +218,11 @@ export interface ReadWebhook {
|
|||
groupId: string;
|
||||
id: number;
|
||||
}
|
||||
export interface SaveInviteToken {
|
||||
usesLeft: number;
|
||||
groupId: string;
|
||||
token: string;
|
||||
}
|
||||
export interface SaveWebhook {
|
||||
enabled?: boolean;
|
||||
name?: string;
|
||||
|
@ -26,3 +230,78 @@ export interface SaveWebhook {
|
|||
time?: string;
|
||||
groupId: string;
|
||||
}
|
||||
export interface SetPermissions {
|
||||
userId: string;
|
||||
canManage?: boolean;
|
||||
canInvite?: boolean;
|
||||
canOrganize?: boolean;
|
||||
}
|
||||
/**
|
||||
* Create Shopping List
|
||||
*/
|
||||
export interface ShoppingListCreate {
|
||||
name?: string;
|
||||
}
|
||||
export interface ShoppingListItemCreate {
|
||||
shoppingListId: string;
|
||||
checked?: boolean;
|
||||
position?: number;
|
||||
isFood?: boolean;
|
||||
note?: string;
|
||||
quantity?: number;
|
||||
unitId?: number;
|
||||
unit?: IngredientUnit;
|
||||
foodId?: number;
|
||||
food?: IngredientFood;
|
||||
recipeId?: number;
|
||||
labelId?: string;
|
||||
}
|
||||
export interface ShoppingListItemOut {
|
||||
shoppingListId: string;
|
||||
checked?: boolean;
|
||||
position?: number;
|
||||
isFood?: boolean;
|
||||
note?: string;
|
||||
quantity?: number;
|
||||
unitId?: number;
|
||||
unit?: IngredientUnit;
|
||||
foodId?: number;
|
||||
food?: IngredientFood;
|
||||
recipeId?: number;
|
||||
labelId?: string;
|
||||
id: string;
|
||||
label?: MultiPurposeLabelSummary;
|
||||
}
|
||||
/**
|
||||
* Create Shopping List
|
||||
*/
|
||||
export interface ShoppingListOut {
|
||||
name?: string;
|
||||
groupId: string;
|
||||
id: string;
|
||||
listItems?: ShoppingListItemOut[];
|
||||
}
|
||||
/**
|
||||
* Create Shopping List
|
||||
*/
|
||||
export interface ShoppingListSave {
|
||||
name?: string;
|
||||
groupId: string;
|
||||
}
|
||||
/**
|
||||
* Create Shopping List
|
||||
*/
|
||||
export interface ShoppingListSummary {
|
||||
name?: string;
|
||||
groupId: string;
|
||||
id: string;
|
||||
}
|
||||
/**
|
||||
* Create Shopping List
|
||||
*/
|
||||
export interface ShoppingListUpdate {
|
||||
name?: string;
|
||||
groupId: string;
|
||||
id: string;
|
||||
listItems?: ShoppingListItemOut[];
|
||||
}
|
||||
|
|
59
frontend/types/api-types/labels.ts
Normal file
59
frontend/types/api-types/labels.ts
Normal file
|
@ -0,0 +1,59 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
/* This file was automatically generated from pydantic models by running pydantic2ts.
|
||||
/* Do not modify it by hand - just update the pydantic models and then re-run the script
|
||||
*/
|
||||
|
||||
export interface IngredientFood {
|
||||
name: string;
|
||||
description?: string;
|
||||
id: number;
|
||||
}
|
||||
export interface MultiPurposeLabelCreate {
|
||||
name: string;
|
||||
}
|
||||
export interface MultiPurposeLabelOut {
|
||||
name: string;
|
||||
groupId: string;
|
||||
id: string;
|
||||
shoppingListItems?: ShoppingListItemOut[];
|
||||
foods?: IngredientFood[];
|
||||
}
|
||||
export interface ShoppingListItemOut {
|
||||
shoppingListId: string;
|
||||
checked?: boolean;
|
||||
position?: number;
|
||||
isFood?: boolean;
|
||||
note?: string;
|
||||
quantity?: number;
|
||||
unitId?: number;
|
||||
unit?: IngredientUnit;
|
||||
foodId?: number;
|
||||
food?: IngredientFood;
|
||||
recipeId?: number;
|
||||
labelId?: string;
|
||||
id: string;
|
||||
label?: MultiPurposeLabelSummary;
|
||||
}
|
||||
export interface IngredientUnit {
|
||||
name: string;
|
||||
description?: string;
|
||||
fraction?: boolean;
|
||||
abbreviation?: string;
|
||||
id: number;
|
||||
}
|
||||
export interface MultiPurposeLabelSummary {
|
||||
name: string;
|
||||
groupId: string;
|
||||
id: string;
|
||||
}
|
||||
export interface MultiPurposeLabelSave {
|
||||
name: string;
|
||||
groupId: string;
|
||||
}
|
||||
export interface MultiPurposeLabelUpdate {
|
||||
name: string;
|
||||
groupId: string;
|
||||
id: string;
|
||||
}
|
|
@ -5,13 +5,36 @@
|
|||
/* Do not modify it by hand - just update the pydantic models and then re-run the script
|
||||
*/
|
||||
|
||||
export type ExportTypes = "json";
|
||||
export type RegisteredParser = "nlp" | "brute";
|
||||
|
||||
export interface AssignCategories {
|
||||
recipes: string[];
|
||||
categories: CategoryBase[];
|
||||
}
|
||||
export interface CategoryBase {
|
||||
name: string;
|
||||
id: number;
|
||||
slug: string;
|
||||
}
|
||||
export interface AssignTags {
|
||||
recipes: string[];
|
||||
tags: TagBase[];
|
||||
}
|
||||
export interface TagBase {
|
||||
name: string;
|
||||
id: number;
|
||||
slug: string;
|
||||
}
|
||||
export interface BulkActionError {
|
||||
recipe: string;
|
||||
error: string;
|
||||
}
|
||||
export interface BulkActionsResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
errors?: BulkActionError[];
|
||||
}
|
||||
export interface CategoryIn {
|
||||
name: string;
|
||||
}
|
||||
|
@ -47,6 +70,13 @@ export interface CreateRecipeByUrl {
|
|||
export interface CreateRecipeByUrlBulk {
|
||||
imports: CreateRecipeBulk[];
|
||||
}
|
||||
export interface DeleteRecipes {
|
||||
recipes: string[];
|
||||
}
|
||||
export interface ExportRecipes {
|
||||
recipes: string[];
|
||||
exportType?: ExportTypes & string;
|
||||
}
|
||||
export interface IngredientConfidence {
|
||||
average?: number;
|
||||
comment?: number;
|
||||
|
@ -60,6 +90,12 @@ export interface IngredientFood {
|
|||
description?: string;
|
||||
id: number;
|
||||
}
|
||||
/**
|
||||
* A list of ingredient references.
|
||||
*/
|
||||
export interface IngredientReferences {
|
||||
referenceId?: string;
|
||||
}
|
||||
export interface IngredientRequest {
|
||||
parser?: RegisteredParser & string;
|
||||
ingredient: string;
|
||||
|
@ -141,12 +177,6 @@ export interface RecipeStep {
|
|||
text: string;
|
||||
ingredientReferences?: IngredientReferences[];
|
||||
}
|
||||
/**
|
||||
* A list of ingredient references.
|
||||
*/
|
||||
export interface IngredientReferences {
|
||||
referenceId?: string;
|
||||
}
|
||||
export interface RecipeSettings {
|
||||
public?: boolean;
|
||||
showNutrition?: boolean;
|
||||
|
@ -198,6 +228,30 @@ export interface RecipeCommentUpdate {
|
|||
id: string;
|
||||
text: string;
|
||||
}
|
||||
export interface RecipeShareToken {
|
||||
recipeId: number;
|
||||
expiresAt?: string;
|
||||
groupId: string;
|
||||
id: string;
|
||||
createdAt: string;
|
||||
recipe: Recipe;
|
||||
}
|
||||
export interface RecipeShareTokenCreate {
|
||||
recipeId: number;
|
||||
expiresAt?: string;
|
||||
}
|
||||
export interface RecipeShareTokenSave {
|
||||
recipeId: number;
|
||||
expiresAt?: string;
|
||||
groupId: string;
|
||||
}
|
||||
export interface RecipeShareTokenSummary {
|
||||
recipeId: number;
|
||||
expiresAt?: string;
|
||||
groupId: string;
|
||||
id: string;
|
||||
createdAt: string;
|
||||
}
|
||||
export interface RecipeSlug {
|
||||
slug: string;
|
||||
}
|
||||
|
@ -247,11 +301,6 @@ export interface RecipeToolResponse {
|
|||
recipes?: Recipe[];
|
||||
}
|
||||
export interface SlugResponse {}
|
||||
export interface TagBase {
|
||||
name: string;
|
||||
id: number;
|
||||
slug: string;
|
||||
}
|
||||
export interface TagIn {
|
||||
name: string;
|
||||
}
|
||||
|
|
|
@ -19,6 +19,19 @@ export interface CreateToken {
|
|||
userId: string;
|
||||
token: string;
|
||||
}
|
||||
export interface CreateUserRegistration {
|
||||
group?: string;
|
||||
groupToken?: string;
|
||||
email: string;
|
||||
username: string;
|
||||
password: string;
|
||||
passwordConfirm: string;
|
||||
advanced?: boolean;
|
||||
private?: boolean;
|
||||
}
|
||||
export interface ForgotPassword {
|
||||
email: string;
|
||||
}
|
||||
export interface GroupBase {
|
||||
name: string;
|
||||
}
|
||||
|
@ -28,7 +41,6 @@ export interface GroupInDB {
|
|||
categories?: CategoryBase[];
|
||||
webhooks?: unknown[];
|
||||
users?: UserOut[];
|
||||
shoppingLists?: ShoppingListOut[];
|
||||
preferences?: ReadGroupPreferences;
|
||||
}
|
||||
export interface UserOut {
|
||||
|
@ -52,18 +64,6 @@ export interface LongLiveTokenOut {
|
|||
id: number;
|
||||
createdAt: string;
|
||||
}
|
||||
export interface ShoppingListOut {
|
||||
name: string;
|
||||
group?: string;
|
||||
items: ListItem[];
|
||||
id: number;
|
||||
}
|
||||
export interface ListItem {
|
||||
title?: string;
|
||||
text?: string;
|
||||
quantity?: number;
|
||||
checked?: boolean;
|
||||
}
|
||||
export interface ReadGroupPreferences {
|
||||
privateGroup?: boolean;
|
||||
firstDayOfWeek?: number;
|
||||
|
@ -103,6 +103,11 @@ export interface PrivateUser {
|
|||
cacheKey: string;
|
||||
password: string;
|
||||
}
|
||||
export interface PrivatePasswordResetToken {
|
||||
userId: string;
|
||||
token: string;
|
||||
user: PrivateUser;
|
||||
}
|
||||
export interface RecipeSummary {
|
||||
id?: number;
|
||||
userId?: string;
|
||||
|
@ -166,6 +171,16 @@ export interface CreateIngredientFood {
|
|||
name: string;
|
||||
description?: string;
|
||||
}
|
||||
export interface ResetPassword {
|
||||
token: string;
|
||||
email: string;
|
||||
password: string;
|
||||
passwordConfirm: string;
|
||||
}
|
||||
export interface SavePasswordResetToken {
|
||||
userId: string;
|
||||
token: string;
|
||||
}
|
||||
export interface SignUpIn {
|
||||
name: string;
|
||||
admin: boolean;
|
||||
|
@ -231,3 +246,6 @@ export interface UserIn {
|
|||
canOrganize?: boolean;
|
||||
password: string;
|
||||
}
|
||||
export interface ValidateResetToken {
|
||||
token: string;
|
||||
}
|
||||
|
|
4
frontend/types/components.d.ts
vendored
4
frontend/types/components.d.ts
vendored
|
@ -5,7 +5,9 @@
|
|||
import BaseOverflowButton from "@/components/global/BaseOverflowButton.vue";
|
||||
import ReportTable from "@/components/global/ReportTable.vue";
|
||||
import AppToolbar from "@/components/global/AppToolbar.vue";
|
||||
import BaseButtonGroup from "@/components/global/BaseButtonGroup.vue";
|
||||
import BaseButton from "@/components/global/BaseButton.vue";
|
||||
import BannerExperimental from "@/components/global/BannerExperimental.vue";
|
||||
import BaseDialog from "@/components/global/BaseDialog.vue";
|
||||
import RecipeJsonEditor from "@/components/global/RecipeJsonEditor.vue";
|
||||
import BaseStatCard from "@/components/global/BaseStatCard.vue";
|
||||
|
@ -31,7 +33,9 @@ declare module "vue" {
|
|||
BaseOverflowButton: typeof BaseOverflowButton;
|
||||
ReportTable: typeof ReportTable;
|
||||
AppToolbar: typeof AppToolbar;
|
||||
BaseButtonGroup: typeof BaseButtonGroup;
|
||||
BaseButton: typeof BaseButton;
|
||||
BannerExperimental: typeof BannerExperimental;
|
||||
BaseDialog: typeof BaseDialog;
|
||||
RecipeJsonEditor: typeof RecipeJsonEditor;
|
||||
BaseStatCard: typeof BaseStatCard;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue