1
0
Fork 0
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:
Miroito 2022-03-23 04:54:16 +01:00 committed by GitHub
parent 20822ee808
commit e743d2c66b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 23 additions and 23 deletions

View file

@ -1,86 +0,0 @@
<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>

View file

@ -1,306 +0,0 @@
<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>

View file

@ -1,388 +0,0 @@
<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>

View file

@ -1,76 +0,0 @@
<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>

View file

@ -1,129 +0,0 @@
<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>

View file

@ -1,120 +0,0 @@
<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>

View file

@ -1,307 +0,0 @@
<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>

View file

@ -1,85 +0,0 @@
<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>

View file

@ -112,7 +112,7 @@
</v-icon>
Back to Profile
</v-btn>
<v-btn outlined class="rounded-xl my-1 mx-1" to="/user/group"> Looking for Privacy Settings? </v-btn>
<v-btn outlined class="rounded-xl my-1 mx-1" to="/group"> Looking for Privacy Settings? </v-btn>
</div>
</section>
</v-container>

View file

@ -73,7 +73,7 @@
<v-row tag="section">
<v-col cols="12" sm="12" md="6">
<UserProfileLinkCard
:link="{ text: 'Group Settings', to: '/user/group' }"
:link="{ text: 'Group Settings', to: '/group' }"
:image="require('~/static/svgs/manage-group-settings.svg')"
>
<template #title> Group Settings </template>
@ -82,7 +82,7 @@
</v-col>
<v-col cols="12" sm="12" md="6">
<UserProfileLinkCard
:link="{ text: 'Manage Cookbooks', to: '/user/group/cookbooks' }"
:link="{ text: 'Manage Cookbooks', to: '/group/cookbooks' }"
:image="require('~/static/svgs/manage-cookbooks.svg')"
>
<template #title> Cookbooks </template>
@ -91,7 +91,7 @@
</v-col>
<v-col v-if="user.advanced" cols="12" sm="12" md="6">
<UserProfileLinkCard
:link="{ text: 'Manage Webhooks', to: '/user/group/webhooks' }"
:link="{ text: 'Manage Webhooks', to: '/group/webhooks' }"
:image="require('~/static/svgs/manage-webhooks.svg')"
>
<template #title> Webhooks </template>
@ -100,7 +100,7 @@
</v-col>
<v-col v-if="user.advanced" cols="12" sm="12" md="6">
<UserProfileLinkCard
:link="{ text: 'Manage Notifiers', to: '/user/group/notifiers' }"
:link="{ text: 'Manage Notifiers', to: '/group/notifiers' }"
:image="require('~/static/svgs/manage-notifiers.svg')"
>
<template #title> Notifiers </template>
@ -109,7 +109,7 @@
</v-col>
<v-col v-if="user.canManage" cols="12" sm="12" md="6">
<UserProfileLinkCard
:link="{ text: 'Manage Members', to: '/user/group/members' }"
:link="{ text: 'Manage Members', to: '/group/members' }"
:image="require('~/static/svgs/manage-members.svg')"
>
<template #title> Members </template>
@ -118,7 +118,7 @@
</v-col>
<v-col v-if="user.advanced" cols="12" sm="12" md="6">
<UserProfileLinkCard
:link="{ text: 'Manage Recipe Data', to: '/user/group/data/recipes' }"
:link="{ text: 'Manage Recipe Data', to: '/group/data/recipes' }"
:image="require('~/static/svgs/manage-recipes.svg')"
>
<template #title> Recipe Data </template>
@ -136,7 +136,7 @@
</v-col>
<v-col v-if="user.advanced" cols="12" sm="12" md="6">
<UserProfileLinkCard
:link="{ text: 'Manage Data Migrations', to: '/user/group/data/migrations' }"
:link="{ text: 'Manage Data Migrations', to: '/group/data/migrations' }"
:image="require('~/static/svgs/manage-data-migrations.svg')"
>
<template #title> Data Migrations </template>