mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-08-02 20:15:24 +02:00
Reorganize Group/User Page Routes (#1084)
* Consolidate group routes * Update doc migration link
This commit is contained in:
parent
20822ee808
commit
e743d2c66b
15 changed files with 23 additions and 23 deletions
86
frontend/pages/group/cookbooks.vue
Normal file
86
frontend/pages/group/cookbooks.vue
Normal file
|
@ -0,0 +1,86 @@
|
|||
<template>
|
||||
<v-container class="narrow-container">
|
||||
<BasePageTitle divider>
|
||||
<template #header>
|
||||
<v-img max-height="100" max-width="100" :src="require('~/static/svgs/manage-cookbooks.svg')"></v-img>
|
||||
</template>
|
||||
<template #title> Cookbooks </template>
|
||||
Arrange and edit your cookbooks here.
|
||||
</BasePageTitle>
|
||||
|
||||
<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 left-border rounded">
|
||||
<v-expansion-panel-header disable-icon-rotate class="headline">
|
||||
<div class="d-flex align-center">
|
||||
<v-icon large left>
|
||||
{{ $globals.icons.pages }}
|
||||
</v-icon>
|
||||
{{ cookbook.name }}
|
||||
</div>
|
||||
<template #actions>
|
||||
<v-icon class="handle">
|
||||
{{ $globals.icons.arrowUpDown }}
|
||||
</v-icon>
|
||||
<v-btn icon small class="ml-2">
|
||||
<v-icon>
|
||||
{{ $globals.icons.edit }}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-expansion-panel-header>
|
||||
<v-expansion-panel-content>
|
||||
<v-card-text>
|
||||
<v-text-field v-model="cookbooks[index].name" label="Cookbook Name"></v-text-field>
|
||||
<v-textarea v-model="cookbooks[index].description" auto-grow :rows="2" label="Description"></v-textarea>
|
||||
<DomainRecipeCategoryTagSelector v-model="cookbooks[index].categories" />
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<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(cookbook.id)"
|
||||
@save="actions.updateOne(cookbook)"
|
||||
/>
|
||||
</v-card-actions>
|
||||
</v-expansion-panel-content>
|
||||
</v-expansion-panel>
|
||||
</draggable>
|
||||
</v-expansion-panels>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "@nuxtjs/composition-api";
|
||||
import draggable from "vuedraggable";
|
||||
import { useCookbooks } from "@/composables/use-group-cookbooks";
|
||||
|
||||
export default defineComponent({
|
||||
components: { draggable },
|
||||
setup() {
|
||||
const { cookbooks, actions } = useCookbooks();
|
||||
|
||||
return {
|
||||
cookbooks,
|
||||
actions,
|
||||
};
|
||||
},
|
||||
head() {
|
||||
return {
|
||||
title: this.$t("settings.pages") as string,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
306
frontend/pages/group/data/migrations.vue
Normal file
306
frontend/pages/group/data/migrations.vue
Normal file
|
@ -0,0 +1,306 @@
|
|||
<template>
|
||||
<v-container>
|
||||
<BasePageTitle divider>
|
||||
<template #header>
|
||||
<v-img
|
||||
max-height="200"
|
||||
max-width="200"
|
||||
class="mb-2"
|
||||
:src="require('~/static/svgs/manage-data-migrations.svg')"
|
||||
></v-img>
|
||||
</template>
|
||||
<template #title> Recipe Data Migrations</template>
|
||||
Recipes can be migrated from another supported application to Mealie. This is a great way to get started with
|
||||
Mealie.
|
||||
</BasePageTitle>
|
||||
<v-container>
|
||||
<BaseCardSectionTitle title="New Migration"> </BaseCardSectionTitle>
|
||||
<v-card outlined :loading="loading">
|
||||
<v-card-title> Choose Migration Type </v-card-title>
|
||||
<v-card-text v-if="content" class="pb-0">
|
||||
<div class="mb-2">
|
||||
<BaseOverflowButton v-model="migrationType" mode="model" :items="items" />
|
||||
</div>
|
||||
{{ content.text }}
|
||||
<v-treeview v-if="content.tree" dense :items="content.tree">
|
||||
<template #prepend="{ item }">
|
||||
<v-icon> {{ item.icon }}</v-icon>
|
||||
</template>
|
||||
</v-treeview>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-title class="mt-0"> Upload File </v-card-title>
|
||||
<v-card-text>
|
||||
<AppButtonUpload
|
||||
accept=".zip"
|
||||
class="mb-2"
|
||||
:post="false"
|
||||
file-name="file"
|
||||
:text-btn="false"
|
||||
@uploaded="setFileObject"
|
||||
/>
|
||||
{{ fileObject.name || "No file selected" }}
|
||||
</v-card-text>
|
||||
|
||||
<v-card-text>
|
||||
<v-checkbox v-model="addMigrationTag">
|
||||
<template #label>
|
||||
Tag all recipes with <b class="mx-1"> {{ migrationType }} </b> tag
|
||||
</template>
|
||||
</v-checkbox>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions class="justify-end">
|
||||
<BaseButton :disabled="!fileObject.name" submit @click="startMigration">
|
||||
{{ $t("general.submit") }}</BaseButton
|
||||
>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-container>
|
||||
<v-container>
|
||||
<BaseCardSectionTitle title="Previous Migrations"> </BaseCardSectionTitle>
|
||||
<ReportTable :items="reports" @delete="deleteReport" />
|
||||
</v-container>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive, toRefs, useContext, computed, onMounted } from "@nuxtjs/composition-api";
|
||||
import { SupportedMigration } from "~/api/class-interfaces/group-migrations";
|
||||
import { ReportSummary } from "~/api/class-interfaces/group-reports";
|
||||
import { MenuItem } from "~/components/global/BaseOverflowButton.vue";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
|
||||
const MIGRATIONS = {
|
||||
nextcloud: "nextcloud",
|
||||
chowdown: "chowdown",
|
||||
paprika: "paprika",
|
||||
mealie: "mealie_alpha",
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const { $globals } = useContext();
|
||||
|
||||
const api = useUserApi();
|
||||
|
||||
const state = reactive({
|
||||
addMigrationTag: false,
|
||||
loading: false,
|
||||
treeState: true,
|
||||
migrationType: MIGRATIONS.nextcloud as SupportedMigration,
|
||||
fileObject: {} as File,
|
||||
reports: [] as ReportSummary[],
|
||||
});
|
||||
|
||||
const items: MenuItem[] = [
|
||||
{
|
||||
text: "Nextcloud",
|
||||
value: MIGRATIONS.nextcloud,
|
||||
},
|
||||
{
|
||||
text: "Chowdown",
|
||||
value: MIGRATIONS.chowdown,
|
||||
},
|
||||
{
|
||||
text: "Paprika",
|
||||
value: MIGRATIONS.paprika,
|
||||
},
|
||||
{
|
||||
text: "Mealie",
|
||||
value: MIGRATIONS.mealie,
|
||||
},
|
||||
];
|
||||
|
||||
const _content = {
|
||||
[MIGRATIONS.nextcloud]: {
|
||||
text: "Nextcloud recipes can be imported from a zip file that contains the data stored in Nextcloud. See the example folder structure below to ensure your recipes are able to be imported.",
|
||||
tree: [
|
||||
{
|
||||
id: 1,
|
||||
icon: $globals.icons.zip,
|
||||
name: "nextcloud.zip",
|
||||
children: [
|
||||
{
|
||||
id: 2,
|
||||
name: "Recipe 1",
|
||||
icon: $globals.icons.folderOutline,
|
||||
children: [
|
||||
{ id: 3, name: "recipe.json", icon: $globals.icons.codeJson },
|
||||
{ id: 4, name: "full.jpg", icon: $globals.icons.fileImage },
|
||||
{ id: 5, name: "thumb.jpg", icon: $globals.icons.fileImage },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: "Recipe 2",
|
||||
icon: $globals.icons.folderOutline,
|
||||
children: [
|
||||
{ id: 7, name: "recipe.json", icon: $globals.icons.codeJson },
|
||||
{ id: 8, name: "full.jpg", icon: $globals.icons.fileImage },
|
||||
{ id: 9, name: "thumb.jpg", icon: $globals.icons.fileImage },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
[MIGRATIONS.chowdown]: {
|
||||
text: "Mealie natively supports the chowdown repository format. Download the code repository as a .zip file and upload it below",
|
||||
tree: [
|
||||
{
|
||||
id: 1,
|
||||
icon: $globals.icons.zip,
|
||||
name: "nextcloud.zip",
|
||||
children: [
|
||||
{
|
||||
id: 2,
|
||||
name: "Recipe 1",
|
||||
icon: $globals.icons.folderOutline,
|
||||
children: [
|
||||
{ id: 3, name: "recipe.json", icon: $globals.icons.codeJson },
|
||||
{ id: 4, name: "full.jpg", icon: $globals.icons.fileImage },
|
||||
{ id: 5, name: "thumb.jpg", icon: $globals.icons.fileImage },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: "Recipe 2",
|
||||
icon: $globals.icons.folderOutline,
|
||||
children: [
|
||||
{ id: 7, name: "recipe.json", icon: $globals.icons.codeJson },
|
||||
{ id: 8, name: "full.jpg", icon: $globals.icons.fileImage },
|
||||
{ id: 9, name: "thumb.jpg", icon: $globals.icons.fileImage },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
[MIGRATIONS.paprika]: {
|
||||
text: "Mealie can import recipes from the Paprika application. Export your recipes from paprika, rename the export extension to .zip and upload it below.",
|
||||
tree: false,
|
||||
},
|
||||
[MIGRATIONS.mealie]: {
|
||||
text: "Mealie can import recipes from the Mealie application from a pre v1.0 release. Export your recipes from your old instance, and upload the zip file below. Note that only recipes can be imported from the export.",
|
||||
tree: [
|
||||
{
|
||||
id: 1,
|
||||
icon: $globals.icons.zip,
|
||||
name: "mealie.zip",
|
||||
children: [
|
||||
{
|
||||
id: 2,
|
||||
name: "recipes",
|
||||
icon: $globals.icons.folderOutline,
|
||||
children: [
|
||||
{
|
||||
id: 3,
|
||||
name: "recipe-name",
|
||||
icon: $globals.icons.folderOutline,
|
||||
children: [
|
||||
{ id: 4, name: "recipe-name.json", icon: $globals.icons.codeJson },
|
||||
{
|
||||
id: 5,
|
||||
name: "images",
|
||||
icon: $globals.icons.folderOutline,
|
||||
children: [
|
||||
{ id: 6, name: "original.webp", icon: $globals.icons.codeJson },
|
||||
{ id: 7, name: "full.jpg", icon: $globals.icons.fileImage },
|
||||
{ id: 8, name: "thumb.jpg", icon: $globals.icons.fileImage },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: "recipe-name-1",
|
||||
icon: $globals.icons.folderOutline,
|
||||
children: [
|
||||
{ id: 10, name: "recipe-name-1.json", icon: $globals.icons.codeJson },
|
||||
{
|
||||
id: 11,
|
||||
name: "images",
|
||||
icon: $globals.icons.folderOutline,
|
||||
children: [
|
||||
{ id: 12, name: "original.webp", icon: $globals.icons.codeJson },
|
||||
{ id: 13, name: "full.jpg", icon: $globals.icons.fileImage },
|
||||
{ id: 14, name: "thumb.jpg", icon: $globals.icons.fileImage },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
function setFileObject(fileObject: File) {
|
||||
state.fileObject = fileObject;
|
||||
}
|
||||
|
||||
async function startMigration() {
|
||||
state.loading = true;
|
||||
const payload = {
|
||||
addMigrationTag: state.addMigrationTag,
|
||||
migrationType: state.migrationType,
|
||||
archive: state.fileObject,
|
||||
};
|
||||
|
||||
const { data } = await api.groupMigration.startMigration(payload);
|
||||
|
||||
state.loading = false;
|
||||
|
||||
if (data) {
|
||||
state.reports.unshift(data);
|
||||
}
|
||||
}
|
||||
|
||||
async function getMigrationReports() {
|
||||
const { data } = await api.groupReports.getAll("migration");
|
||||
|
||||
if (data) {
|
||||
state.reports = data;
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteReport(id: string) {
|
||||
await api.groupReports.deleteOne(id);
|
||||
getMigrationReports();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getMigrationReports();
|
||||
});
|
||||
|
||||
const content = computed(() => {
|
||||
const data = _content[state.migrationType];
|
||||
|
||||
if (data) {
|
||||
return data;
|
||||
} else {
|
||||
return {
|
||||
text: "",
|
||||
tree: false,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
items,
|
||||
content,
|
||||
setFileObject,
|
||||
deleteReport,
|
||||
startMigration,
|
||||
getMigrationReports,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
388
frontend/pages/group/data/recipes.vue
Normal file
388
frontend/pages/group/data/recipes.vue
Normal file
|
@ -0,0 +1,388 @@
|
|||
<template>
|
||||
<v-container fluid>
|
||||
<!-- Export Purge Confirmation Dialog -->
|
||||
<BaseDialog
|
||||
v-model="purgeExportsDialog"
|
||||
title="Purge Exports"
|
||||
color="error"
|
||||
:icon="$globals.icons.alertCircle"
|
||||
@confirm="purgeExports()"
|
||||
>
|
||||
<v-card-text> Are you sure you want to delete all export data? </v-card-text>
|
||||
</BaseDialog>
|
||||
|
||||
<!-- Base Dialog Object -->
|
||||
<BaseDialog
|
||||
ref="domDialog"
|
||||
v-model="dialog.state"
|
||||
width="650px"
|
||||
:icon="dialog.icon"
|
||||
:title="dialog.title"
|
||||
submit-text="Submit"
|
||||
@submit="dialog.callback"
|
||||
>
|
||||
<v-card-text v-if="dialog.mode == MODES.tag">
|
||||
<RecipeCategoryTagSelector v-model="toSetTags" :tag-selector="true" />
|
||||
</v-card-text>
|
||||
<v-card-text v-else-if="dialog.mode == MODES.category">
|
||||
<RecipeCategoryTagSelector v-model="toSetCategories" />
|
||||
</v-card-text>
|
||||
<v-card-text v-else-if="dialog.mode == MODES.delete">
|
||||
<p class="h4">Are you sure you want to delete the following recipes? This action cannot be undone.</p>
|
||||
<v-card outlined>
|
||||
<v-virtual-scroll height="400" item-height="25" :items="selected">
|
||||
<template #default="{ item }">
|
||||
<v-list-item class="pb-2">
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>{{ item.name }}</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-virtual-scroll>
|
||||
</v-card>
|
||||
</v-card-text>
|
||||
<v-card-text v-else-if="dialog.mode == MODES.export">
|
||||
<p class="h4">The following recipes ({{ selected.length }}) will be exported.</p>
|
||||
<v-card outlined>
|
||||
<v-virtual-scroll height="400" item-height="25" :items="selected">
|
||||
<template #default="{ item }">
|
||||
<v-list-item class="pb-2">
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>{{ item.name }}</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-virtual-scroll>
|
||||
</v-card>
|
||||
</v-card-text>
|
||||
</BaseDialog>
|
||||
<BasePageTitle divider>
|
||||
<template #header>
|
||||
<v-img max-height="125" max-width="125" :src="require('~/static/svgs/manage-recipes.svg')"></v-img>
|
||||
</template>
|
||||
<template #title> Data Management </template>
|
||||
</BasePageTitle>
|
||||
|
||||
<section>
|
||||
<!-- Recipe Data Table -->
|
||||
<BaseCardSectionTitle :icon="$globals.icons.primary" section title="Recipe Data">
|
||||
Use this section to manage the data associated with your recipes. You can perform several bulk actions on your
|
||||
recipes including exporting, deleting, tagging, and assigning categories.
|
||||
</BaseCardSectionTitle>
|
||||
<v-card-actions class="mt-n5 mb-1">
|
||||
<v-menu offset-y bottom nudge-bottom="6" :close-on-content-click="false">
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn color="accent" class="mr-1" dark v-bind="attrs" v-on="on">
|
||||
<v-icon left>
|
||||
{{ $globals.icons.cog }}
|
||||
</v-icon>
|
||||
Columns
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-card-title class="py-2">
|
||||
<div>Recipe Columns</div>
|
||||
</v-card-title>
|
||||
<v-divider class="mx-2"></v-divider>
|
||||
<v-card-text class="mt-n5">
|
||||
<v-checkbox
|
||||
v-for="(itemValue, key) in headers"
|
||||
:key="key"
|
||||
v-model="headers[key]"
|
||||
dense
|
||||
flat
|
||||
inset
|
||||
:label="headerLabels[key]"
|
||||
hide-details
|
||||
></v-checkbox>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
<BaseOverflowButton
|
||||
:disabled="selected.length < 1"
|
||||
mode="event"
|
||||
color="info"
|
||||
:items="actions"
|
||||
@export-selected="openDialog(MODES.export)"
|
||||
@tag-selected="openDialog(MODES.tag)"
|
||||
@categorize-selected="openDialog(MODES.category)"
|
||||
@delete-selected="openDialog(MODES.delete)"
|
||||
>
|
||||
</BaseOverflowButton>
|
||||
|
||||
<p v-if="selected.length > 0" class="text-caption my-auto ml-5">Selected: {{ selected.length }}</p>
|
||||
</v-card-actions>
|
||||
<v-card>
|
||||
<RecipeDataTable v-model="selected" :loading="loading" :recipes="allRecipes" :show-headers="headers" />
|
||||
<v-card-actions class="justify-end">
|
||||
<BaseButton color="info">
|
||||
<template #icon>
|
||||
{{ $globals.icons.database }}
|
||||
</template>
|
||||
Import
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
color="info"
|
||||
@click="
|
||||
selectAll();
|
||||
openDialog(MODES.export);
|
||||
"
|
||||
>
|
||||
<template #icon>
|
||||
{{ $globals.icons.database }}
|
||||
</template>
|
||||
Export All
|
||||
</BaseButton>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</section>
|
||||
|
||||
<section class="mt-10">
|
||||
<!-- Downloads Data Table -->
|
||||
<BaseCardSectionTitle :icon="$globals.icons.database" section title="Data Exports">
|
||||
This section provides links to available exports that are ready to download. These exports do expire, so be sure
|
||||
to grab them while they're still available.
|
||||
</BaseCardSectionTitle>
|
||||
<v-card-actions class="mt-n5 mb-1">
|
||||
<BaseButton delete @click="purgeExportsDialog = true"> </BaseButton>
|
||||
</v-card-actions>
|
||||
<v-card>
|
||||
<GroupExportData :exports="groupExports" />
|
||||
</v-card>
|
||||
</section>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive, ref, useContext, onMounted } from "@nuxtjs/composition-api";
|
||||
import RecipeDataTable from "~/components/Domain/Recipe/RecipeDataTable.vue";
|
||||
import RecipeCategoryTagSelector from "~/components/Domain/Recipe/RecipeCategoryTagSelector.vue";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { useRecipes, allRecipes } from "~/composables/recipes";
|
||||
import { Recipe } from "~/types/api-types/recipe";
|
||||
import GroupExportData from "~/components/Domain/Group/GroupExportData.vue";
|
||||
import { GroupDataExport } from "~/api/class-interfaces/recipe-bulk-actions";
|
||||
import { MenuItem } from "~/components/global/BaseOverflowButton.vue";
|
||||
|
||||
const MODES = {
|
||||
tag: "tag",
|
||||
category: "category",
|
||||
export: "export",
|
||||
delete: "delete",
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
components: { RecipeDataTable, RecipeCategoryTagSelector, GroupExportData },
|
||||
scrollToTop: true,
|
||||
setup() {
|
||||
const { getAllRecipes, refreshRecipes } = useRecipes(true, true);
|
||||
|
||||
const { $globals } = useContext();
|
||||
|
||||
const selected = ref<Recipe[]>([]);
|
||||
|
||||
function resetAll() {
|
||||
selected.value = [];
|
||||
toSetTags.value = [];
|
||||
toSetCategories.value = [];
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
const headers = reactive({
|
||||
id: false,
|
||||
owner: false,
|
||||
tags: true,
|
||||
tools: true,
|
||||
categories: true,
|
||||
recipeYield: false,
|
||||
dateAdded: false,
|
||||
});
|
||||
|
||||
const headerLabels = {
|
||||
id: "Id",
|
||||
owner: "Owner",
|
||||
tags: "Tags",
|
||||
categories: "Categories",
|
||||
tools: "Tools",
|
||||
recipeYield: "Recipe Yield",
|
||||
dateAdded: "Date Added",
|
||||
};
|
||||
|
||||
const actions: MenuItem[] = [
|
||||
{
|
||||
icon: $globals.icons.database,
|
||||
text: "Export",
|
||||
event: "export-selected",
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.tags,
|
||||
text: "Tag",
|
||||
event: "tag-selected",
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.tags,
|
||||
text: "Categorize",
|
||||
event: "categorize-selected",
|
||||
},
|
||||
{
|
||||
icon: $globals.icons.delete,
|
||||
text: "Delete",
|
||||
event: "delete-selected",
|
||||
},
|
||||
];
|
||||
|
||||
const api = useUserApi();
|
||||
const loading = ref(false);
|
||||
|
||||
// ===============================================================
|
||||
// Group Exports
|
||||
|
||||
const purgeExportsDialog = ref(false);
|
||||
|
||||
async function purgeExports() {
|
||||
await api.bulk.purgeExports();
|
||||
refreshExports();
|
||||
}
|
||||
|
||||
const groupExports = ref<GroupDataExport[]>([]);
|
||||
|
||||
async function refreshExports() {
|
||||
const { data } = await api.bulk.fetchExports();
|
||||
|
||||
if (data) {
|
||||
groupExports.value = data;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await refreshExports();
|
||||
});
|
||||
// ===============================================================
|
||||
// All Recipes
|
||||
|
||||
function selectAll() {
|
||||
selected.value = allRecipes.value;
|
||||
}
|
||||
|
||||
async function exportSelected() {
|
||||
loading.value = true;
|
||||
const { data } = await api.bulk.bulkExport({
|
||||
recipes: selected.value.map((x: Recipe) => x.slug ?? ""),
|
||||
exportType: "json",
|
||||
});
|
||||
|
||||
if (data) {
|
||||
console.log(data);
|
||||
}
|
||||
|
||||
resetAll();
|
||||
refreshExports();
|
||||
}
|
||||
|
||||
const toSetTags = ref([]);
|
||||
|
||||
async function tagSelected() {
|
||||
loading.value = true;
|
||||
|
||||
const recipes = selected.value.map((x: Recipe) => x.slug ?? "");
|
||||
await api.bulk.bulkTag({ recipes, tags: toSetTags.value });
|
||||
await refreshRecipes();
|
||||
resetAll();
|
||||
}
|
||||
|
||||
const toSetCategories = ref([]);
|
||||
|
||||
async function categorizeSelected() {
|
||||
loading.value = true;
|
||||
|
||||
const recipes = selected.value.map((x: Recipe) => x.slug ?? "");
|
||||
await api.bulk.bulkCategorize({ recipes, categories: toSetCategories.value });
|
||||
await refreshRecipes();
|
||||
resetAll();
|
||||
}
|
||||
|
||||
async function deleteSelected() {
|
||||
loading.value = true;
|
||||
|
||||
const recipes = selected.value.map((x: Recipe) => x.slug ?? "");
|
||||
|
||||
const { response, data } = await api.bulk.bulkDelete({ recipes });
|
||||
|
||||
console.log(response, data);
|
||||
|
||||
await refreshRecipes();
|
||||
resetAll();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Dialog Management
|
||||
|
||||
const dialog = reactive({
|
||||
state: false,
|
||||
title: "Tag Recipes",
|
||||
mode: MODES.tag,
|
||||
tag: "",
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
callback: () => {},
|
||||
icon: $globals.icons.tags,
|
||||
});
|
||||
|
||||
function openDialog(mode: string) {
|
||||
const titles = {
|
||||
[MODES.tag]: "Tag Recipes",
|
||||
[MODES.category]: "Categorize Recipes",
|
||||
[MODES.export]: "Export Recipes",
|
||||
[MODES.delete]: "Delete Recipes",
|
||||
};
|
||||
|
||||
const callbacks = {
|
||||
[MODES.tag]: tagSelected,
|
||||
[MODES.category]: categorizeSelected,
|
||||
[MODES.export]: exportSelected,
|
||||
[MODES.delete]: deleteSelected,
|
||||
};
|
||||
|
||||
const icons = {
|
||||
[MODES.tag]: $globals.icons.tags,
|
||||
[MODES.category]: $globals.icons.tags,
|
||||
[MODES.export]: $globals.icons.database,
|
||||
[MODES.delete]: $globals.icons.delete,
|
||||
};
|
||||
|
||||
dialog.mode = mode;
|
||||
dialog.title = titles[mode];
|
||||
dialog.callback = callbacks[mode];
|
||||
dialog.icon = icons[mode];
|
||||
dialog.state = true;
|
||||
}
|
||||
|
||||
return {
|
||||
selectAll,
|
||||
loading,
|
||||
actions,
|
||||
allRecipes,
|
||||
categorizeSelected,
|
||||
deleteSelected,
|
||||
dialog,
|
||||
exportSelected,
|
||||
getAllRecipes,
|
||||
headerLabels,
|
||||
headers,
|
||||
MODES,
|
||||
openDialog,
|
||||
selected,
|
||||
tagSelected,
|
||||
toSetCategories,
|
||||
toSetTags,
|
||||
groupExports,
|
||||
purgeExportsDialog,
|
||||
purgeExports,
|
||||
};
|
||||
},
|
||||
head() {
|
||||
return {
|
||||
title: "Recipe Data",
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
76
frontend/pages/group/data/reports/_id.vue
Normal file
76
frontend/pages/group/data/reports/_id.vue
Normal file
|
@ -0,0 +1,76 @@
|
|||
<template>
|
||||
<v-container>
|
||||
<BasePageTitle divider>
|
||||
<template #header>
|
||||
<v-img max-height="200" max-width="200" class="mb-2" :src="require('~/static/svgs/data-reports.svg')"></v-img>
|
||||
</template>
|
||||
<template #title> Recipe Data Migrations</template>
|
||||
Recipes can be migrated from another supported application to Mealie. This is a great way to get started with
|
||||
Mealie.
|
||||
</BasePageTitle>
|
||||
<v-container v-if="report">
|
||||
<BaseCardSectionTitle :title="report.name"> </BaseCardSectionTitle>
|
||||
|
||||
<v-card-text> Report Id: {{ id }} </v-card-text>
|
||||
|
||||
<v-data-table :headers="itemHeaders" :items="report.entries" :items-per-page="50" show-expand>
|
||||
<template #item.success="{ item }">
|
||||
<v-icon :color="item.success ? 'success' : 'error'">
|
||||
{{ item.success ? $globals.icons.checkboxMarkedCircle : $globals.icons.close }}
|
||||
</v-icon>
|
||||
</template>
|
||||
<template #item.timestamp="{ item }">
|
||||
{{ $d(Date.parse(item.timestamp), "short") }}
|
||||
</template>
|
||||
<template #expanded-item="{ headers, item }">
|
||||
<td class="pa-6" :colspan="headers.length">{{ item.exception }}</td>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-container>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, useRoute, reactive, toRefs, onMounted } from "@nuxtjs/composition-api";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const route = useRoute();
|
||||
const id = route.value.params.id;
|
||||
|
||||
const api = useUserApi();
|
||||
|
||||
const state = reactive({
|
||||
report: {},
|
||||
});
|
||||
|
||||
async function getReport() {
|
||||
const { data } = await api.groupReports.getOne(id);
|
||||
|
||||
if (data) {
|
||||
state.report = data;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await getReport();
|
||||
});
|
||||
|
||||
const itemHeaders = [
|
||||
{ text: "Success", value: "success" },
|
||||
{ text: "Message", value: "message" },
|
||||
{ text: "Timestamp", value: "timestamp" },
|
||||
];
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
id,
|
||||
itemHeaders,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
129
frontend/pages/group/index.vue
Normal file
129
frontend/pages/group/index.vue
Normal file
|
@ -0,0 +1,129 @@
|
|||
<template>
|
||||
<v-container class="narrow-container">
|
||||
<BasePageTitle class="mb-5">
|
||||
<template #header>
|
||||
<v-img max-height="100" max-width="100" :src="require('~/static/svgs/manage-group-settings.svg')"></v-img>
|
||||
</template>
|
||||
<template #title> Group Settings </template>
|
||||
These items are shared within your group. Editing one of them will change it for the whole group!
|
||||
</BasePageTitle>
|
||||
|
||||
<section v-if="group">
|
||||
<BaseCardSectionTitle class="mt-10" title="Group Preferences"></BaseCardSectionTitle>
|
||||
<v-checkbox
|
||||
v-model="group.preferences.privateGroup"
|
||||
class="mt-n4"
|
||||
label="Private Group"
|
||||
@change="groupActions.updatePreferences()"
|
||||
></v-checkbox>
|
||||
<v-select
|
||||
v-model="group.preferences.firstDayOfWeek"
|
||||
:prepend-icon="$globals.icons.calendarWeekBegin"
|
||||
:items="allDays"
|
||||
item-text="name"
|
||||
item-value="value"
|
||||
:label="$t('settings.first-day-of-week')"
|
||||
@change="groupActions.updatePreferences()"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section v-if="group">
|
||||
<BaseCardSectionTitle class="mt-10" title="Default Recipe Preferences">
|
||||
These are the default settings when a new recipe is created in your group. These can be changed for indivdual
|
||||
recipes in the recipe settings menu.
|
||||
</BaseCardSectionTitle>
|
||||
|
||||
<v-checkbox
|
||||
v-model="group.preferences.recipePublic"
|
||||
class="mt-n4"
|
||||
label="Allow users outside of your group to see your recipes"
|
||||
@change="groupActions.updatePreferences()"
|
||||
></v-checkbox>
|
||||
<v-checkbox
|
||||
v-model="group.preferences.recipeShowNutrition"
|
||||
class="mt-n4"
|
||||
label="Show nutrition information"
|
||||
@change="groupActions.updatePreferences()"
|
||||
></v-checkbox>
|
||||
<v-checkbox
|
||||
v-model="group.preferences.recipeShowAssets"
|
||||
class="mt-n4"
|
||||
label="Show recipe assets"
|
||||
@change="groupActions.updatePreferences()"
|
||||
></v-checkbox>
|
||||
<v-checkbox
|
||||
v-model="group.preferences.recipeLandscapeView"
|
||||
class="mt-n4"
|
||||
label="Default to landscape view"
|
||||
@change="groupActions.updatePreferences()"
|
||||
></v-checkbox>
|
||||
<v-checkbox
|
||||
v-model="group.preferences.recipeDisableComments"
|
||||
class="mt-n4"
|
||||
label="Disable users from commenting on recipes"
|
||||
@change="groupActions.updatePreferences()"
|
||||
></v-checkbox>
|
||||
<v-checkbox
|
||||
v-model="group.preferences.recipeDisableAmount"
|
||||
class="mt-n4"
|
||||
label="Disable organizing recipe ingredients by units and food"
|
||||
@change="groupActions.updatePreferences()"
|
||||
></v-checkbox>
|
||||
</section>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, useContext } from "@nuxtjs/composition-api";
|
||||
import { useGroupSelf } from "~/composables/use-groups";
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const { group, actions: groupActions } = useGroupSelf();
|
||||
|
||||
const { i18n } = useContext();
|
||||
|
||||
const allDays = [
|
||||
{
|
||||
name: i18n.t("general.sunday"),
|
||||
value: 0,
|
||||
},
|
||||
{
|
||||
name: i18n.t("general.monday"),
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
name: i18n.t("general.tuesday"),
|
||||
value: 2,
|
||||
},
|
||||
{
|
||||
name: i18n.t("general.wednesday"),
|
||||
value: 3,
|
||||
},
|
||||
{
|
||||
name: i18n.t("general.thursday"),
|
||||
value: 4,
|
||||
},
|
||||
{
|
||||
name: i18n.t("general.friday"),
|
||||
value: 5,
|
||||
},
|
||||
{
|
||||
name: i18n.t("general.saturday"),
|
||||
value: 6,
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
group,
|
||||
groupActions,
|
||||
allDays,
|
||||
};
|
||||
},
|
||||
head() {
|
||||
return {
|
||||
title: this.$t("group.group") as string,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
120
frontend/pages/group/members.vue
Normal file
120
frontend/pages/group/members.vue
Normal file
|
@ -0,0 +1,120 @@
|
|||
<template>
|
||||
<v-container>
|
||||
<BasePageTitle divider>
|
||||
<template #header>
|
||||
<v-img max-height="125" max-width="125" :src="require('~/static/svgs/manage-members.svg')"></v-img>
|
||||
</template>
|
||||
<template #title> Manage Members </template>
|
||||
Manage the permissions of the members in your groups. <b> Manage </b> allows the user to access the
|
||||
data-management page <b> Invite </b> allows the user to generate invitation links for other users. Group owners
|
||||
cannot change their own permissions.
|
||||
</BasePageTitle>
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="members || []"
|
||||
item-key="id"
|
||||
class="elevation-0"
|
||||
hide-default-footer
|
||||
disable-pagination
|
||||
>
|
||||
<template #item.avatar="{ item }">
|
||||
<UserAvatar :user-id="item.id" />
|
||||
</template>
|
||||
<template #item.admin="{ item }">
|
||||
{{ item.admin ? "Admin" : "User" }}
|
||||
</template>
|
||||
<template #item.manage="{ item }">
|
||||
<div class="d-flex justify-center">
|
||||
<v-checkbox
|
||||
v-model="item.canManage"
|
||||
:disabled="item.id === $auth.user.id || item.admin"
|
||||
class=""
|
||||
style="max-width: 30px"
|
||||
@change="setPermissions(item)"
|
||||
></v-checkbox>
|
||||
</div>
|
||||
</template>
|
||||
<template #item.organize="{ item }">
|
||||
<div class="d-flex justify-center">
|
||||
<v-checkbox
|
||||
v-model="item.canOrganize"
|
||||
:disabled="item.id === $auth.user.id || item.admin"
|
||||
class=""
|
||||
style="max-width: 30px"
|
||||
@change="setPermissions(item)"
|
||||
></v-checkbox>
|
||||
</div>
|
||||
</template>
|
||||
<template #item.invite="{ item }">
|
||||
<div class="d-flex justify-center">
|
||||
<v-checkbox
|
||||
v-model="item.canInvite"
|
||||
:disabled="item.id === $auth.user.id || item.admin"
|
||||
class=""
|
||||
style="max-width: 30px"
|
||||
@change="setPermissions(item)"
|
||||
></v-checkbox>
|
||||
</div>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, onMounted, useContext } from "@nuxtjs/composition-api";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { UserOut } from "~/types/api-types/user";
|
||||
import UserAvatar from "~/components/Domain/User/UserAvatar.vue";
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
UserAvatar,
|
||||
},
|
||||
setup() {
|
||||
const api = useUserApi();
|
||||
|
||||
const { i18n } = useContext();
|
||||
|
||||
const members = ref<UserOut[] | null[]>([]);
|
||||
|
||||
const headers = [
|
||||
{ text: "", value: "avatar", sortable: false, align: "center" },
|
||||
{ text: i18n.t("user.username"), value: "username" },
|
||||
{ text: i18n.t("user.full-name"), value: "fullName" },
|
||||
{ text: i18n.t("user.admin"), value: "admin" },
|
||||
{ text: "Manage", value: "manage", sortable: false, align: "center" },
|
||||
{ text: "Organize", value: "organize", sortable: false, align: "center" },
|
||||
{ text: "Invite", value: "invite", sortable: false, align: "center" },
|
||||
];
|
||||
|
||||
async function refreshMembers() {
|
||||
const { data } = await api.groups.fetchMembers();
|
||||
if (data) {
|
||||
members.value = data;
|
||||
}
|
||||
}
|
||||
|
||||
async function setPermissions(user: UserOut) {
|
||||
const payload = {
|
||||
userId: user.id,
|
||||
canInvite: user.canInvite,
|
||||
canManage: user.canManage,
|
||||
canOrganize: user.canOrganize,
|
||||
};
|
||||
|
||||
await api.groups.setMemberPermissions(payload);
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await refreshMembers();
|
||||
});
|
||||
|
||||
return { members, headers, setPermissions };
|
||||
},
|
||||
head() {
|
||||
return {
|
||||
title: "Members",
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
307
frontend/pages/group/notifiers.vue
Normal file
307
frontend/pages/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" title="New Notification" @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>
|
85
frontend/pages/group/webhooks.vue
Normal file
85
frontend/pages/group/webhooks.vue
Normal file
|
@ -0,0 +1,85 @@
|
|||
<template>
|
||||
<v-container class="narrow-container">
|
||||
<BasePageTitle divider>
|
||||
<template #header>
|
||||
<v-img max-height="125" max-width="125" :src="require('~/static/svgs/manage-webhooks.svg')"></v-img>
|
||||
</template>
|
||||
<template #title> Webhooks </template>
|
||||
The webhooks defined below will be executed when a meal is defined for the day. At the scheduled time the webhooks
|
||||
will be sent with the data from the recipe that is scheduled for the day
|
||||
</BasePageTitle>
|
||||
|
||||
<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 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">
|
||||
{{ $globals.icons.webhook }}
|
||||
</v-icon>
|
||||
{{ webhook.name }} - {{ webhook.time }}
|
||||
</div>
|
||||
<template #actions>
|
||||
<v-btn small icon class="ml-2">
|
||||
<v-icon>
|
||||
{{ $globals.icons.edit }}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-expansion-panel-header>
|
||||
<v-expansion-panel-content>
|
||||
<v-card-text>
|
||||
<v-switch v-model="webhook.enabled" label="Enabled"></v-switch>
|
||||
<v-text-field v-model="webhook.name" label="Webhook Name"></v-text-field>
|
||||
<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 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>
|
||||
</v-expansion-panels>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "@nuxtjs/composition-api";
|
||||
import { useGroupWebhooks } from "~/composables/use-group-webhooks";
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const { actions, webhooks } = useGroupWebhooks();
|
||||
|
||||
return {
|
||||
webhooks,
|
||||
actions,
|
||||
};
|
||||
},
|
||||
head() {
|
||||
return {
|
||||
title: this.$t("settings.webhooks.webhooks") as string,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
Loading…
Add table
Add a link
Reference in a new issue