mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-05-30 19:54:44 -04:00
refactor: unify recipe-organizer components (#1340)
* use generic context menu * implement organizer stores * add basic organizer types * refactor selectors to apply for all organizers * remove legacy organizer composables
This commit is contained in:
parent
bc175d4ca9
commit
12f480eb75
@ -5,8 +5,8 @@
|
|||||||
<v-select v-model="inputEntryType" :items="MEAL_TYPE_OPTIONS" label="Meal Type"></v-select>
|
<v-select v-model="inputEntryType" :items="MEAL_TYPE_OPTIONS" label="Meal Type"></v-select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<RecipeCategoryTagSelector v-model="inputCategories" />
|
<RecipeOrganizerSelector v-model="inputCategories" selector-type="categories" />
|
||||||
<RecipeCategoryTagSelector v-model="inputTags" :tag-selector="true" />
|
<RecipeOrganizerSelector v-model="inputTags" selector-type="tags" />
|
||||||
|
|
||||||
{{ inputDay === "unset" ? "This rule will apply to all days" : `This rule applies on ${inputDay}s` }}
|
{{ inputDay === "unset" ? "This rule will apply to all days" : `This rule applies on ${inputDay}s` }}
|
||||||
{{ inputEntryType === "unset" ? "for all meal types" : ` and for ${inputEntryType} meal types` }}
|
{{ inputEntryType === "unset" ? "for all meal types" : ` and for ${inputEntryType} meal types` }}
|
||||||
@ -15,7 +15,8 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, computed } from "@nuxtjs/composition-api";
|
import { defineComponent, computed } from "@nuxtjs/composition-api";
|
||||||
import RecipeCategoryTagSelector from "~/components/Domain/Recipe/RecipeCategoryTagSelector.vue";
|
import RecipeOrganizerSelector from "~/components/Domain/Recipe/RecipeOrganizerSelector.vue";
|
||||||
|
import { RecipeTag, RecipeCategory } from "~/types/api-types/group";
|
||||||
|
|
||||||
const MEAL_TYPE_OPTIONS = [
|
const MEAL_TYPE_OPTIONS = [
|
||||||
{ text: "Breakfast", value: "breakfast" },
|
{ text: "Breakfast", value: "breakfast" },
|
||||||
@ -38,7 +39,7 @@ const MEAL_DAY_OPTIONS = [
|
|||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
RecipeCategoryTagSelector,
|
RecipeOrganizerSelector,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
day: {
|
day: {
|
||||||
@ -50,11 +51,11 @@ export default defineComponent({
|
|||||||
default: "unset",
|
default: "unset",
|
||||||
},
|
},
|
||||||
categories: {
|
categories: {
|
||||||
type: Array,
|
type: Array as () => RecipeCategory[],
|
||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
tags: {
|
tags: {
|
||||||
type: Array,
|
type: Array as () => RecipeTag[],
|
||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
showHelp: {
|
showHelp: {
|
||||||
|
@ -1,111 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<slot>
|
|
||||||
<v-btn icon class="mt-n1" @click="dialog = true">
|
|
||||||
<v-icon :color="color">{{ $globals.icons.create }}</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</slot>
|
|
||||||
<v-dialog v-model="dialog" width="500">
|
|
||||||
<v-card>
|
|
||||||
<v-app-bar dense dark color="primary mb-2">
|
|
||||||
<v-icon large left class="mt-1">
|
|
||||||
{{ $globals.icons.tags }}
|
|
||||||
</v-icon>
|
|
||||||
|
|
||||||
<v-toolbar-title class="headline">
|
|
||||||
{{ title }}
|
|
||||||
</v-toolbar-title>
|
|
||||||
|
|
||||||
<v-spacer></v-spacer>
|
|
||||||
</v-app-bar>
|
|
||||||
<v-card-title> </v-card-title>
|
|
||||||
<v-form @submit.prevent="select">
|
|
||||||
<v-card-text>
|
|
||||||
<v-text-field
|
|
||||||
v-model="itemName"
|
|
||||||
dense
|
|
||||||
:label="inputLabel"
|
|
||||||
:rules="[rules.required]"
|
|
||||||
autofocus
|
|
||||||
></v-text-field>
|
|
||||||
</v-card-text>
|
|
||||||
<v-card-actions>
|
|
||||||
<BaseButton cancel @click="dialog = false" />
|
|
||||||
<v-spacer></v-spacer>
|
|
||||||
<BaseButton type="submit" create :disabled="!itemName" />
|
|
||||||
</v-card-actions>
|
|
||||||
</v-form>
|
|
||||||
</v-card>
|
|
||||||
</v-dialog>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { computed, defineComponent, reactive, toRefs, watch } from "@nuxtjs/composition-api";
|
|
||||||
import { useUserApi } from "~/composables/api";
|
|
||||||
|
|
||||||
const CREATED_ITEM_EVENT = "created-item";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
props: {
|
|
||||||
value: {
|
|
||||||
type: String,
|
|
||||||
default: "",
|
|
||||||
},
|
|
||||||
color: {
|
|
||||||
type: String,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
tagDialog: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup(props, context) {
|
|
||||||
const title = computed(() => props.tagDialog ? "Create a Tag" : "Create a Category");
|
|
||||||
const inputLabel = computed(() => props.tagDialog ? "Tag Name" : "Category Name");
|
|
||||||
|
|
||||||
const rules = {
|
|
||||||
required: (val: string) => !!val || "A Name is Required",
|
|
||||||
};
|
|
||||||
|
|
||||||
const state = reactive({
|
|
||||||
dialog: false,
|
|
||||||
itemName: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(() => state.dialog, (val: boolean) => {
|
|
||||||
if (!val) state.itemName = "";
|
|
||||||
});
|
|
||||||
|
|
||||||
const api = useUserApi();
|
|
||||||
async function select() {
|
|
||||||
const newItem = await (async () => {
|
|
||||||
if (props.tagDialog) {
|
|
||||||
const { data } = await api.tags.createOne({ name: state.itemName });
|
|
||||||
return data;
|
|
||||||
} else {
|
|
||||||
const { data } = await api.categories.createOne({ name: state.itemName });
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
console.log(newItem);
|
|
||||||
|
|
||||||
context.emit(CREATED_ITEM_EVENT, newItem);
|
|
||||||
state.dialog = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return {
|
|
||||||
...toRefs(state),
|
|
||||||
title,
|
|
||||||
inputLabel,
|
|
||||||
rules,
|
|
||||||
select,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style></style>
|
|
@ -1,164 +0,0 @@
|
|||||||
//TODO: Prevent fetching Categories/Tags multiple time when selector is on page multiple times
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<v-autocomplete
|
|
||||||
v-model="selected"
|
|
||||||
:items="activeItems"
|
|
||||||
:value="value"
|
|
||||||
:label="inputLabel"
|
|
||||||
chips
|
|
||||||
deletable-chips
|
|
||||||
:dense="dense"
|
|
||||||
item-text="name"
|
|
||||||
persistent-hint
|
|
||||||
multiple
|
|
||||||
:hide-details="hideDetails"
|
|
||||||
:hint="hint"
|
|
||||||
:solo="solo"
|
|
||||||
:return-object="returnObject"
|
|
||||||
:prepend-inner-icon="$globals.icons.tags"
|
|
||||||
v-bind="$attrs"
|
|
||||||
@input="emitChange"
|
|
||||||
>
|
|
||||||
<template #selection="data">
|
|
||||||
<v-chip
|
|
||||||
v-if="showSelected"
|
|
||||||
:key="data.index"
|
|
||||||
:small="dense"
|
|
||||||
class="ma-1"
|
|
||||||
:input-value="data.selected"
|
|
||||||
close
|
|
||||||
label
|
|
||||||
color="accent"
|
|
||||||
dark
|
|
||||||
@click:close="removeByIndex(data.index)"
|
|
||||||
>
|
|
||||||
{{ data.item.name || data.item }}
|
|
||||||
</v-chip>
|
|
||||||
</template>
|
|
||||||
<template #append-outer>
|
|
||||||
<RecipeCategoryTagDialog v-if="showAdd" :tag-dialog="tagSelector" @created-item="pushToItem" />
|
|
||||||
</template>
|
|
||||||
</v-autocomplete>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { computed, defineComponent, onMounted, reactive, toRefs, useContext, watch } from "@nuxtjs/composition-api";
|
|
||||||
import RecipeCategoryTagDialog from "./RecipeCategoryTagDialog.vue";
|
|
||||||
import { useTags, useCategories } from "~/composables/recipes";
|
|
||||||
import { RecipeCategory, RecipeTag } from "~/types/api-types/user";
|
|
||||||
|
|
||||||
const MOUNTED_EVENT = "mounted";
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
components: {
|
|
||||||
RecipeCategoryTagDialog,
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
value: {
|
|
||||||
type: Array as () => (RecipeTag | RecipeCategory | string)[],
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
solo: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
dense: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
returnObject: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
tagSelector: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
hint: {
|
|
||||||
type: String,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
showAdd: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
showLabel: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
showSelected: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
hideDetails: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
setup(props, context) {
|
|
||||||
const { allTags, useAsyncGetAll: getAllTags } = useTags();
|
|
||||||
const { allCategories, useAsyncGetAll: getAllCategories } = useCategories();
|
|
||||||
getAllCategories();
|
|
||||||
getAllTags();
|
|
||||||
|
|
||||||
const state = reactive({
|
|
||||||
selected: props.value,
|
|
||||||
});
|
|
||||||
watch(
|
|
||||||
() => props.value,
|
|
||||||
(val) => {
|
|
||||||
state.selected = val;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const { i18n } = useContext();
|
|
||||||
const inputLabel = computed(() => {
|
|
||||||
if (!props.showLabel) return null;
|
|
||||||
return props.tagSelector ? i18n.t("tag.tags") : i18n.t("recipe.categories");
|
|
||||||
});
|
|
||||||
|
|
||||||
const activeItems = computed(() => {
|
|
||||||
let itemObjects: RecipeTag[] | RecipeCategory[] | null;
|
|
||||||
if (props.tagSelector) itemObjects = allTags.value;
|
|
||||||
else {
|
|
||||||
itemObjects = allCategories.value;
|
|
||||||
}
|
|
||||||
if (props.returnObject) return itemObjects;
|
|
||||||
else {
|
|
||||||
return itemObjects?.map((x: RecipeTag | RecipeCategory) => x.name);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function emitChange() {
|
|
||||||
context.emit("input", state.selected);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO Is this needed?
|
|
||||||
onMounted(() => {
|
|
||||||
context.emit(MOUNTED_EVENT);
|
|
||||||
});
|
|
||||||
|
|
||||||
function removeByIndex(index: number) {
|
|
||||||
state.selected.splice(index, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function pushToItem(createdItem: RecipeTag | RecipeCategory) {
|
|
||||||
// TODO: Remove excessive get calls
|
|
||||||
getAllCategories();
|
|
||||||
getAllTags();
|
|
||||||
state.selected.push(createdItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...toRefs(state),
|
|
||||||
inputLabel,
|
|
||||||
activeItems,
|
|
||||||
emitChange,
|
|
||||||
removeByIndex,
|
|
||||||
pushToItem,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
@ -1,207 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="text-center">
|
|
||||||
<BaseDialog
|
|
||||||
v-model="ItemDeleteDialog"
|
|
||||||
:title="`Delete ${itemName}`"
|
|
||||||
color="error"
|
|
||||||
:icon="$globals.icons.alertCircle"
|
|
||||||
@confirm="deleteItem()"
|
|
||||||
>
|
|
||||||
<v-card-text> Are you sure you want to delete this {{ itemName }}? </v-card-text>
|
|
||||||
</BaseDialog>
|
|
||||||
<v-menu
|
|
||||||
offset-y
|
|
||||||
left
|
|
||||||
:bottom="!menuTop"
|
|
||||||
:nudge-bottom="!menuTop ? '5' : '0'"
|
|
||||||
:top="menuTop"
|
|
||||||
:nudge-top="menuTop ? '5' : '0'"
|
|
||||||
allow-overflow
|
|
||||||
close-delay="125"
|
|
||||||
open-on-hover
|
|
||||||
content-class="d-print-none"
|
|
||||||
>
|
|
||||||
<template #activator="{ on, attrs }">
|
|
||||||
<v-btn :fab="fab" :small="fab" :color="color" :icon="!fab" dark v-bind="attrs" v-on="on" @click.prevent>
|
|
||||||
<v-icon>{{ icon }}</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
</template>
|
|
||||||
<v-list dense>
|
|
||||||
<v-list-item v-for="(item, index) in menuItems" :key="index" @click="contextMenuEventHandler(item.event)">
|
|
||||||
<v-list-item-icon>
|
|
||||||
<v-icon :color="item.color">
|
|
||||||
{{ item.icon }}
|
|
||||||
</v-icon>
|
|
||||||
</v-list-item-icon>
|
|
||||||
<v-list-item-title>{{ item.title }}</v-list-item-title>
|
|
||||||
</v-list-item>
|
|
||||||
</v-list>
|
|
||||||
</v-menu>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, reactive, toRefs, useContext } from "@nuxtjs/composition-api";
|
|
||||||
import colors from "vuetify/lib/util/colors";
|
|
||||||
import { useUserApi } from "~/composables/api";
|
|
||||||
|
|
||||||
export interface ContextMenuIncludes {
|
|
||||||
delete: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ContextMenuItem {
|
|
||||||
title: string;
|
|
||||||
icon: string;
|
|
||||||
color: string | undefined;
|
|
||||||
event: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ItemTypes = {
|
|
||||||
tag: "tags",
|
|
||||||
category: "categories",
|
|
||||||
tool: "tools",
|
|
||||||
};
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
props: {
|
|
||||||
itemType: {
|
|
||||||
type: String as () => string,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
useItems: {
|
|
||||||
type: Object as () => ContextMenuIncludes,
|
|
||||||
default: () => ({
|
|
||||||
delete: true,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
// Append items are added at the end of the useItems list
|
|
||||||
appendItems: {
|
|
||||||
type: Array as () => ContextMenuItem[],
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
// Append items are added at the beginning of the useItems list
|
|
||||||
leadingItems: {
|
|
||||||
type: Array as () => ContextMenuItem[],
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
menuTop: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
fab: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
color: {
|
|
||||||
type: String,
|
|
||||||
default: colors.grey.darken2,
|
|
||||||
},
|
|
||||||
slug: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
menuIcon: {
|
|
||||||
type: String,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
name: {
|
|
||||||
required: true,
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
id: {
|
|
||||||
required: true,
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup(props, context) {
|
|
||||||
const api = useUserApi();
|
|
||||||
|
|
||||||
const state = reactive({
|
|
||||||
ItemDeleteDialog: false,
|
|
||||||
loading: false,
|
|
||||||
menuItems: [] as ContextMenuItem[],
|
|
||||||
itemName: "tag",
|
|
||||||
});
|
|
||||||
|
|
||||||
const { i18n, $globals } = useContext();
|
|
||||||
|
|
||||||
let apiRoute = "tags" as "tags" | "categories" | "tools";
|
|
||||||
|
|
||||||
switch (props.itemType) {
|
|
||||||
case ItemTypes.tag:
|
|
||||||
state.itemName = "tag";
|
|
||||||
apiRoute = "tags";
|
|
||||||
break;
|
|
||||||
case ItemTypes.category:
|
|
||||||
state.itemName = "category";
|
|
||||||
apiRoute = "categories";
|
|
||||||
break;
|
|
||||||
case ItemTypes.tool:
|
|
||||||
state.itemName = "tool";
|
|
||||||
apiRoute = "tools";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===========================================================================
|
|
||||||
// Context Menu Setup
|
|
||||||
|
|
||||||
const defaultItems: { [key: string]: ContextMenuItem } = {
|
|
||||||
delete: {
|
|
||||||
title: i18n.t("general.delete") as string,
|
|
||||||
icon: $globals.icons.delete,
|
|
||||||
color: undefined,
|
|
||||||
event: "delete",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get Default Menu Items Specified in Props
|
|
||||||
for (const [key, value] of Object.entries(props.useItems)) {
|
|
||||||
if (value) {
|
|
||||||
const item = defaultItems[key];
|
|
||||||
if (item) {
|
|
||||||
state.menuItems.push(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add leading and Apppending Items
|
|
||||||
state.menuItems = [...props.leadingItems, ...state.menuItems, ...props.appendItems];
|
|
||||||
|
|
||||||
const icon = props.menuIcon || $globals.icons.dotsVertical;
|
|
||||||
|
|
||||||
async function deleteItem() {
|
|
||||||
await api[apiRoute].deleteOne(props.id);
|
|
||||||
context.emit("delete", props.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: Print is handled as an event in the parent component
|
|
||||||
const eventHandlers: { [key: string]: () => void } = {
|
|
||||||
delete: () => {
|
|
||||||
state.ItemDeleteDialog = true;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
function contextMenuEventHandler(eventKey: string) {
|
|
||||||
const handler = eventHandlers[eventKey];
|
|
||||||
|
|
||||||
if (handler && typeof handler === "function") {
|
|
||||||
handler();
|
|
||||||
state.loading = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
context.emit(eventKey);
|
|
||||||
state.loading = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...toRefs(state),
|
|
||||||
contextMenuEventHandler,
|
|
||||||
deleteItem,
|
|
||||||
icon,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
@ -1,123 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div v-if="items">
|
|
||||||
<v-app-bar color="transparent" flat class="mt-n1 rounded">
|
|
||||||
<v-icon large left>
|
|
||||||
{{ icon }}
|
|
||||||
</v-icon>
|
|
||||||
<v-toolbar-title class="headline"> {{ headline }} </v-toolbar-title>
|
|
||||||
<v-spacer></v-spacer>
|
|
||||||
</v-app-bar>
|
|
||||||
<section v-for="(itms, key, idx) in itemsSorted" :key="'header' + idx" :class="idx === 1 ? null : 'my-4'">
|
|
||||||
<BaseCardSectionTitle :title="key"> </BaseCardSectionTitle>
|
|
||||||
<v-row>
|
|
||||||
<v-col v-for="(item, index) in itms" :key="'cat' + index" cols="12" :sm="12" :md="6" :lg="4" :xl="3">
|
|
||||||
<v-card class="left-border" hover :to="`/recipes/${itemType}/${item.slug}`">
|
|
||||||
<v-card-actions>
|
|
||||||
<v-icon>
|
|
||||||
{{ icon }}
|
|
||||||
</v-icon>
|
|
||||||
<v-card-title class="py-1">
|
|
||||||
{{ item.name }}
|
|
||||||
</v-card-title>
|
|
||||||
<v-spacer></v-spacer>
|
|
||||||
<RecipeCategoryTagToolContextMenu
|
|
||||||
:id="item.id"
|
|
||||||
:item-type="itemType"
|
|
||||||
:slug="item.slug"
|
|
||||||
:name="item.name"
|
|
||||||
:use-items="{
|
|
||||||
delete: true,
|
|
||||||
}"
|
|
||||||
@delete="$emit('delete', item.id)"
|
|
||||||
/>
|
|
||||||
</v-card-actions>
|
|
||||||
</v-card>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { defineComponent, reactive, toRefs, useContext, computed, useMeta } from "@nuxtjs/composition-api";
|
|
||||||
import RecipeCategoryTagToolContextMenu from "./RecipeCategoryTagToolContextMenu.vue";
|
|
||||||
|
|
||||||
type ItemType = "tags" | "categories" | "tools";
|
|
||||||
|
|
||||||
const ItemTypes = {
|
|
||||||
tag: "tags",
|
|
||||||
category: "categories",
|
|
||||||
tool: "tools",
|
|
||||||
};
|
|
||||||
|
|
||||||
interface GenericItem {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
slug: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
components: { RecipeCategoryTagToolContextMenu },
|
|
||||||
props: {
|
|
||||||
itemType: {
|
|
||||||
type: String as () => ItemType,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
items: {
|
|
||||||
type: Array as () => GenericItem[],
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
setup(props) {
|
|
||||||
const { i18n, $globals } = useContext();
|
|
||||||
|
|
||||||
const state = reactive({
|
|
||||||
headline: "tags",
|
|
||||||
icon: $globals.icons.tags,
|
|
||||||
});
|
|
||||||
|
|
||||||
switch (props.itemType) {
|
|
||||||
case ItemTypes.tag:
|
|
||||||
state.headline = i18n.t("tag.tags") as string;
|
|
||||||
break;
|
|
||||||
case ItemTypes.category:
|
|
||||||
state.headline = i18n.t("category.categories") as string;
|
|
||||||
break;
|
|
||||||
case ItemTypes.tool:
|
|
||||||
state.headline = i18n.t("tool.tools") as string;
|
|
||||||
state.icon = $globals.icons.potSteam;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
useMeta(() => ({
|
|
||||||
title: state.headline,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const itemsSorted = computed(() => {
|
|
||||||
const byLetter: { [key: string]: Array<GenericItem> } = {};
|
|
||||||
|
|
||||||
if (!props.items) return byLetter;
|
|
||||||
|
|
||||||
props.items.forEach((item) => {
|
|
||||||
const letter = item.name[0].toUpperCase();
|
|
||||||
if (!byLetter[letter]) {
|
|
||||||
byLetter[letter] = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
byLetter[letter].push(item);
|
|
||||||
});
|
|
||||||
|
|
||||||
return byLetter;
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
...toRefs(state),
|
|
||||||
itemsSorted,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
// Needed for useMeta
|
|
||||||
head: {},
|
|
||||||
});
|
|
||||||
</script>
|
|
152
frontend/components/Domain/Recipe/RecipeOrganizerDialog.vue
Normal file
152
frontend/components/Domain/Recipe/RecipeOrganizerDialog.vue
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<v-dialog v-model="dialog" width="500">
|
||||||
|
<v-card>
|
||||||
|
<v-app-bar dense dark color="primary mb-2">
|
||||||
|
<v-icon large left class="mt-1">
|
||||||
|
{{ itemType === Organizer.Tool ? $globals.icons.potSteam : $globals.icons.tags }}
|
||||||
|
</v-icon>
|
||||||
|
|
||||||
|
<v-toolbar-title class="headline">
|
||||||
|
{{ properties.title }}
|
||||||
|
</v-toolbar-title>
|
||||||
|
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
</v-app-bar>
|
||||||
|
<v-card-title> </v-card-title>
|
||||||
|
<v-form @submit.prevent="select">
|
||||||
|
<v-card-text>
|
||||||
|
<v-text-field
|
||||||
|
v-model="name"
|
||||||
|
dense
|
||||||
|
:label="properties.label"
|
||||||
|
:rules="[rules.required]"
|
||||||
|
autofocus
|
||||||
|
></v-text-field>
|
||||||
|
<v-checkbox v-if="itemType === Organizer.Tool" v-model="onHand" label="On Hand"></v-checkbox>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<BaseButton cancel @click="dialog = false" />
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<BaseButton type="submit" create :disabled="!name" />
|
||||||
|
</v-card-actions>
|
||||||
|
</v-form>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { computed, defineComponent, reactive, toRefs, watch } from "@nuxtjs/composition-api";
|
||||||
|
import { useUserApi } from "~/composables/api";
|
||||||
|
import { useCategoryStore, useTagStore, useToolStore } from "~/composables/store";
|
||||||
|
import { RecipeOrganizer, Organizer } from "~/types/recipe/organizers";
|
||||||
|
|
||||||
|
const CREATED_ITEM_EVENT = "created-item";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
tagDialog: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
itemType: {
|
||||||
|
type: String as () => RecipeOrganizer,
|
||||||
|
default: "category",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props, context) {
|
||||||
|
const state = reactive({
|
||||||
|
name: "",
|
||||||
|
onHand: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const dialog = computed({
|
||||||
|
get() {
|
||||||
|
return props.value;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
context.emit("input", value);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.value,
|
||||||
|
(val: boolean) => {
|
||||||
|
if (!val) state.name = "";
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const userApi = useUserApi();
|
||||||
|
|
||||||
|
const store = (() => {
|
||||||
|
switch (props.itemType) {
|
||||||
|
case Organizer.Tag:
|
||||||
|
return useTagStore();
|
||||||
|
case Organizer.Tool:
|
||||||
|
return useToolStore();
|
||||||
|
default:
|
||||||
|
return useCategoryStore();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
const properties = computed(() => {
|
||||||
|
switch (props.itemType) {
|
||||||
|
case Organizer.Tag:
|
||||||
|
return {
|
||||||
|
title: "Create a Tag",
|
||||||
|
label: "Tag Name",
|
||||||
|
api: userApi.tags,
|
||||||
|
};
|
||||||
|
case Organizer.Tool:
|
||||||
|
return {
|
||||||
|
title: "Create a Tool",
|
||||||
|
label: "Tool Name",
|
||||||
|
api: userApi.tools,
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
title: "Create a Category",
|
||||||
|
label: "Category Name",
|
||||||
|
api: userApi.categories,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
required: (val: string) => !!val || "A Name is Required",
|
||||||
|
};
|
||||||
|
|
||||||
|
async function select() {
|
||||||
|
if (store) {
|
||||||
|
// @ts-ignore - only property really required is the name
|
||||||
|
await store.actions.createOne({ name: state.name });
|
||||||
|
}
|
||||||
|
|
||||||
|
const newItem = store.items.value.find((item) => item.name === state.name);
|
||||||
|
|
||||||
|
context.emit(CREATED_ITEM_EVENT, newItem);
|
||||||
|
dialog.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
Organizer,
|
||||||
|
...toRefs(state),
|
||||||
|
dialog,
|
||||||
|
properties,
|
||||||
|
rules,
|
||||||
|
select,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style></style>
|
139
frontend/components/Domain/Recipe/RecipeOrganizerPage.vue
Normal file
139
frontend/components/Domain/Recipe/RecipeOrganizerPage.vue
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="items">
|
||||||
|
<RecipeOrganizerDialog v-model="dialog" :item-type="itemType" />
|
||||||
|
|
||||||
|
<BaseDialog
|
||||||
|
v-if="deleteTarget"
|
||||||
|
v-model="deleteDialog"
|
||||||
|
:title="`Delete ${deleteTarget.name}`"
|
||||||
|
color="error"
|
||||||
|
:icon="$globals.icons.alertCircle"
|
||||||
|
@confirm="deleteOne()"
|
||||||
|
>
|
||||||
|
<v-card-text> Are you sure you want to delete this {{ deleteTarget.name }}? </v-card-text>
|
||||||
|
</BaseDialog>
|
||||||
|
<v-app-bar color="transparent" flat class="mt-n1 rounded align-center">
|
||||||
|
<v-icon large left>
|
||||||
|
{{ icon }}
|
||||||
|
</v-icon>
|
||||||
|
<v-toolbar-title class="headline">
|
||||||
|
<slot name="title">
|
||||||
|
{{ headline }}
|
||||||
|
</slot>
|
||||||
|
</v-toolbar-title>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<BaseButton create @click="dialog = true" />
|
||||||
|
</v-app-bar>
|
||||||
|
<section v-for="(itms, key, idx) in itemsSorted" :key="'header' + idx" :class="idx === 1 ? null : 'my-4'">
|
||||||
|
<BaseCardSectionTitle :title="key"> </BaseCardSectionTitle>
|
||||||
|
<v-row>
|
||||||
|
<v-col v-for="(item, index) in itms" :key="'cat' + index" cols="12" :sm="12" :md="6" :lg="4" :xl="3">
|
||||||
|
<v-card class="left-border" hover :to="`/recipes/${itemType}/${item.slug}`">
|
||||||
|
<v-card-actions>
|
||||||
|
<v-icon>
|
||||||
|
{{ icon }}
|
||||||
|
</v-icon>
|
||||||
|
<v-card-title class="py-1">
|
||||||
|
{{ item.name }}
|
||||||
|
</v-card-title>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<ContextMenu :items="[presets.delete]" @delete="confirmDelete(item)" />
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, computed, ref } from "@nuxtjs/composition-api";
|
||||||
|
import { useContextPresets } from "~/composables/use-context-presents";
|
||||||
|
import RecipeOrganizerDialog from "~/components/Domain/Recipe/RecipeOrganizerDialog.vue";
|
||||||
|
import { RecipeOrganizer } from "~/types/recipe/organizers";
|
||||||
|
|
||||||
|
interface GenericItem {
|
||||||
|
id?: string;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
RecipeOrganizerDialog,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
items: {
|
||||||
|
type: Array as () => GenericItem[],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
itemType: {
|
||||||
|
type: String as () => RecipeOrganizer,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
setup(props, { emit }) {
|
||||||
|
// =================================================================
|
||||||
|
// Sorted Items
|
||||||
|
const itemsSorted = computed(() => {
|
||||||
|
const byLetter: { [key: string]: Array<GenericItem> } = {};
|
||||||
|
|
||||||
|
if (!props.items) return byLetter;
|
||||||
|
|
||||||
|
props.items.forEach((item) => {
|
||||||
|
const letter = item.name[0].toUpperCase();
|
||||||
|
if (!byLetter[letter]) {
|
||||||
|
byLetter[letter] = [];
|
||||||
|
}
|
||||||
|
byLetter[letter].push(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const key in byLetter) {
|
||||||
|
byLetter[key] = byLetter[key].sort((a, b) => {
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return byLetter;
|
||||||
|
});
|
||||||
|
|
||||||
|
// =================================================================
|
||||||
|
// Context Menu
|
||||||
|
const presets = useContextPresets();
|
||||||
|
|
||||||
|
const deleteTarget = ref<GenericItem | null>(null);
|
||||||
|
const deleteDialog = ref(false);
|
||||||
|
|
||||||
|
function confirmDelete(item: GenericItem) {
|
||||||
|
deleteTarget.value = item;
|
||||||
|
deleteDialog.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteOne() {
|
||||||
|
if (!deleteTarget.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
emit("delete", deleteTarget.value.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const dialog = ref(false);
|
||||||
|
|
||||||
|
return {
|
||||||
|
dialog,
|
||||||
|
confirmDelete,
|
||||||
|
deleteOne,
|
||||||
|
deleteDialog,
|
||||||
|
deleteTarget,
|
||||||
|
presets,
|
||||||
|
itemsSorted,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
// Needed for useMeta
|
||||||
|
head: {},
|
||||||
|
});
|
||||||
|
</script>
|
@ -1,14 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-autocomplete
|
<v-autocomplete
|
||||||
v-model="selected"
|
v-model="selected"
|
||||||
:items="items"
|
:items="storeItem"
|
||||||
:value="value"
|
:value="value"
|
||||||
:label="label"
|
:label="label"
|
||||||
chips
|
chips
|
||||||
deletable-chips
|
deletable-chips
|
||||||
item-text="name"
|
item-text="name"
|
||||||
multiple
|
multiple
|
||||||
:prepend-inner-icon="$globals.icons.tags"
|
:prepend-inner-icon="selectorType === Organizer.Tool ? $globals.icons.potSteam : $globals.icons.tags"
|
||||||
return-object
|
return-object
|
||||||
v-bind="inputAttrs"
|
v-bind="inputAttrs"
|
||||||
>
|
>
|
||||||
@ -17,6 +17,7 @@
|
|||||||
:key="data.index"
|
:key="data.index"
|
||||||
class="ma-1"
|
class="ma-1"
|
||||||
:input-value="data.selected"
|
:input-value="data.selected"
|
||||||
|
small
|
||||||
close
|
close
|
||||||
label
|
label
|
||||||
color="accent"
|
color="accent"
|
||||||
@ -26,41 +27,55 @@
|
|||||||
{{ data.item.name || data.item }}
|
{{ data.item.name || data.item }}
|
||||||
</v-chip>
|
</v-chip>
|
||||||
</template>
|
</template>
|
||||||
|
<template v-if="showAdd" #append-outer>
|
||||||
|
<v-btn icon @click="dialog = true">
|
||||||
|
<v-icon>
|
||||||
|
{{ $globals.icons.create }}
|
||||||
|
</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<RecipeOrganizerDialog v-model="dialog" :item-type="selectorType" @created-item="appendCreated" />
|
||||||
|
</template>
|
||||||
</v-autocomplete>
|
</v-autocomplete>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, useContext } from "@nuxtjs/composition-api";
|
import { defineComponent, ref, useContext } from "@nuxtjs/composition-api";
|
||||||
import { computed, onMounted } from "vue-demi";
|
import { computed, onMounted } from "vue-demi";
|
||||||
|
import RecipeOrganizerDialog from "./RecipeOrganizerDialog.vue";
|
||||||
import { RecipeCategory, RecipeTag } from "~/types/api-types/user";
|
import { RecipeCategory, RecipeTag } from "~/types/api-types/user";
|
||||||
import { RecipeTool } from "~/types/api-types/admin";
|
import { RecipeTool } from "~/types/api-types/admin";
|
||||||
|
import { useTagStore } from "~/composables/store/use-tag-store";
|
||||||
type OrganizerType = "tag" | "category" | "tool";
|
import { useCategoryStore, useToolStore } from "~/composables/store";
|
||||||
|
import { Organizer, RecipeOrganizer } from "~/types/recipe/organizers";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
RecipeOrganizerDialog,
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
value: {
|
value: {
|
||||||
type: Array as () => (RecipeTag | RecipeCategory | RecipeTool)[] | undefined,
|
type: Array as () => (RecipeTag | RecipeCategory | RecipeTool | string)[] | undefined,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* The type of organizer to use.
|
* The type of organizer to use.
|
||||||
*/
|
*/
|
||||||
selectorType: {
|
selectorType: {
|
||||||
type: String as () => OrganizerType,
|
type: String as () => RecipeOrganizer,
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* List of items that are available to be chosen from
|
|
||||||
*/
|
|
||||||
items: {
|
|
||||||
type: Array as () => (RecipeTag | RecipeCategory | RecipeTool)[],
|
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
inputAttrs: {
|
inputAttrs: {
|
||||||
type: Object as () => Record<string, any>,
|
type: Object as () => Record<string, any>,
|
||||||
default: () => ({}),
|
default: () => ({}),
|
||||||
},
|
},
|
||||||
|
returnObject: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
showAdd: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
setup(props, context) {
|
setup(props, context) {
|
||||||
@ -81,27 +96,62 @@ export default defineComponent({
|
|||||||
|
|
||||||
const label = computed(() => {
|
const label = computed(() => {
|
||||||
switch (props.selectorType) {
|
switch (props.selectorType) {
|
||||||
case "tag":
|
case Organizer.Tag:
|
||||||
return i18n.t("tag.tags");
|
return i18n.t("tag.tags");
|
||||||
case "category":
|
case Organizer.Category:
|
||||||
return i18n.t("category.categories");
|
return i18n.t("category.categories");
|
||||||
case "tool":
|
case Organizer.Tool:
|
||||||
return "Tools";
|
return i18n.t("tool.tools");
|
||||||
default:
|
default:
|
||||||
return "Organizer";
|
return "Organizer";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ===========================================================================
|
||||||
|
// Store & Items Setup
|
||||||
|
|
||||||
|
const store = (() => {
|
||||||
|
switch (props.selectorType) {
|
||||||
|
case Organizer.Tag:
|
||||||
|
return useTagStore();
|
||||||
|
case Organizer.Tool:
|
||||||
|
return useToolStore();
|
||||||
|
default:
|
||||||
|
return useCategoryStore();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
const items = computed(() => {
|
||||||
|
if (!props.returnObject) {
|
||||||
|
return store.items.value.map((item) => item.name);
|
||||||
|
}
|
||||||
|
return store.items.value;
|
||||||
|
});
|
||||||
|
|
||||||
function removeByIndex(index: number) {
|
function removeByIndex(index: number) {
|
||||||
if (selected.value === undefined) {
|
if (selected.value === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newSelected = selected.value.filter((_, i) => i !== index);
|
const newSelected = selected.value.filter((_, i) => i !== index);
|
||||||
selected.value = [...newSelected];
|
selected.value = [...newSelected];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function appendCreated(item: RecipeTag | RecipeCategory | RecipeTool) {
|
||||||
|
console.log(item);
|
||||||
|
if (selected.value === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
selected.value = [...selected.value, item];
|
||||||
|
}
|
||||||
|
|
||||||
|
const dialog = ref(false);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
Organizer,
|
||||||
|
appendCreated,
|
||||||
|
dialog,
|
||||||
|
storeItem: items,
|
||||||
label,
|
label,
|
||||||
selected,
|
selected,
|
||||||
removeByIndex,
|
removeByIndex,
|
||||||
|
56
frontend/components/global/ContextMenu.vue
Normal file
56
frontend/components/global/ContextMenu.vue
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<template>
|
||||||
|
<v-menu
|
||||||
|
offset-y
|
||||||
|
left
|
||||||
|
:bottom="!menuTop"
|
||||||
|
:nudge-bottom="!menuTop ? '5' : '0'"
|
||||||
|
:top="menuTop"
|
||||||
|
:nudge-top="menuTop ? '5' : '0'"
|
||||||
|
allow-overflow
|
||||||
|
close-delay="125"
|
||||||
|
open-on-hover
|
||||||
|
content-class="d-print-none"
|
||||||
|
>
|
||||||
|
<template #activator="{ on, attrs }">
|
||||||
|
<v-btn :fab="fab" :small="fab" :color="color" :icon="!fab" dark v-bind="attrs" v-on="on" @click.prevent>
|
||||||
|
<v-icon>{{ $globals.icons.dotsVertical }}</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
<v-list dense>
|
||||||
|
<v-list-item v-for="(item, index) in items" :key="index" @click="$emit(item.event)">
|
||||||
|
<v-list-item-icon>
|
||||||
|
<v-icon :color="item.color ? item.color : undefined">
|
||||||
|
{{ item.icon }}
|
||||||
|
</v-icon>
|
||||||
|
</v-list-item-icon>
|
||||||
|
<v-list-item-title>{{ item.title }}</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-menu>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from "@nuxtjs/composition-api";
|
||||||
|
import { ContextMenuItem } from "~/composables/use-context-presents";
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
items: {
|
||||||
|
type: Array as () => ContextMenuItem[],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
menuTop: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
fab: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
type: String,
|
||||||
|
default: "grey darken-2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
@ -3,7 +3,7 @@ import { useAsyncKey } from "../use-utils";
|
|||||||
import { BaseCRUDAPI } from "~/api/_base";
|
import { BaseCRUDAPI } from "~/api/_base";
|
||||||
|
|
||||||
type BoundT = {
|
type BoundT = {
|
||||||
id: string | number;
|
id?: string | number;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface StoreActions<T extends BoundT> {
|
interface StoreActions<T extends BoundT> {
|
||||||
@ -29,7 +29,12 @@ export function useStoreActions<T extends BoundT>(
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
const allItems = useAsync(async () => {
|
const allItems = useAsync(async () => {
|
||||||
const { data } = await api.getAll();
|
const { data } = await api.getAll();
|
||||||
return data;
|
|
||||||
|
if (allRef) {
|
||||||
|
allRef.value = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data ?? [];
|
||||||
}, useAsyncKey());
|
}, useAsyncKey());
|
||||||
|
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
@ -73,8 +78,8 @@ export function useStoreActions<T extends BoundT>(
|
|||||||
|
|
||||||
async function deleteOne(id: string | number) {
|
async function deleteOne(id: string | number) {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
const { data } = await api.deleteOne(id);
|
const { response } = await api.deleteOne(id);
|
||||||
if (data && allRef?.value) {
|
if (response && allRef?.value) {
|
||||||
refresh();
|
refresh();
|
||||||
}
|
}
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
export { useFraction } from "./use-fraction";
|
export { useFraction } from "./use-fraction";
|
||||||
export { useRecipe } from "./use-recipe";
|
export { useRecipe } from "./use-recipe";
|
||||||
export { useRecipes, recentRecipes, allRecipes, useLazyRecipes, useSorter } from "./use-recipes";
|
export { useRecipes, recentRecipes, allRecipes, useLazyRecipes, useSorter } from "./use-recipes";
|
||||||
export { useTags, useCategories, allCategories, allTags } from "./use-tags-categories";
|
|
||||||
export { parseIngredientText } from "./use-recipe-ingredients";
|
export { parseIngredientText } from "./use-recipe-ingredients";
|
||||||
export { useRecipeSearch } from "./use-recipe-search";
|
export { useRecipeSearch } from "./use-recipe-search";
|
||||||
export { useTools } from "./use-recipe-tools";
|
export { useTools } from "./use-recipe-tools";
|
||||||
|
@ -1,65 +0,0 @@
|
|||||||
import { Ref, ref, useAsync } from "@nuxtjs/composition-api";
|
|
||||||
import { useUserApi } from "../api";
|
|
||||||
import { useAsyncKey } from "../use-utils";
|
|
||||||
import { CategoriesAPI } from "~/api/class-interfaces/organizer-categories";
|
|
||||||
import { TagsAPI } from "~/api/class-interfaces/organizer-tags";
|
|
||||||
import { RecipeTag, RecipeCategory } from "~/types/api-types/recipe";
|
|
||||||
|
|
||||||
export const allCategories = ref<RecipeCategory[] | null>([]);
|
|
||||||
export const allTags = ref<RecipeTag[] | null>([]);
|
|
||||||
|
|
||||||
function baseTagsCategories(
|
|
||||||
reference: Ref<RecipeCategory[] | null> | Ref<RecipeTag[] | null>,
|
|
||||||
api: TagsAPI | CategoriesAPI
|
|
||||||
) {
|
|
||||||
function useAsyncGetAll() {
|
|
||||||
useAsync(async () => {
|
|
||||||
await refreshItems();
|
|
||||||
}, useAsyncKey());
|
|
||||||
}
|
|
||||||
|
|
||||||
async function refreshItems() {
|
|
||||||
const { data } = await api.getAll();
|
|
||||||
// @ts-ignore hotfix
|
|
||||||
reference.value = data;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createOne(payload: { name: string }) {
|
|
||||||
const { data } = await api.createOne(payload);
|
|
||||||
if (data) {
|
|
||||||
refreshItems();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteOne(slug: string) {
|
|
||||||
const { data } = await api.deleteOne(slug);
|
|
||||||
if (data) {
|
|
||||||
refreshItems();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateOne(slug: string, payload: { name: string }) {
|
|
||||||
// @ts-ignore // TODO: Fix Typescript Issue - Unsure how to fix this while also keeping mixins
|
|
||||||
const { data } = await api.updateOne(slug, payload);
|
|
||||||
if (data) {
|
|
||||||
refreshItems();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { useAsyncGetAll, refreshItems, createOne, deleteOne, updateOne };
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useTags = function () {
|
|
||||||
const api = useUserApi();
|
|
||||||
return {
|
|
||||||
allTags,
|
|
||||||
...baseTagsCategories(allTags, api.tags),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
export const useCategories = function () {
|
|
||||||
const api = useUserApi();
|
|
||||||
return {
|
|
||||||
allCategories,
|
|
||||||
...baseTagsCategories(allCategories, api.categories),
|
|
||||||
};
|
|
||||||
};
|
|
@ -1,3 +1,6 @@
|
|||||||
export { useFoodStore, useFoodData } from "./use-food-store";
|
export { useFoodStore, useFoodData } from "./use-food-store";
|
||||||
export { useUnitStore, useUnitData } from "./use-unit-store";
|
export { useUnitStore, useUnitData } from "./use-unit-store";
|
||||||
export { useLabelStore, useLabelData } from "./use-label-store";
|
export { useLabelStore, useLabelData } from "./use-label-store";
|
||||||
|
export { useToolStore, useToolData } from "./use-tool-store";
|
||||||
|
export { useCategoryStore, useCategoryData } from "./use-category-store";
|
||||||
|
export { useTagStore, useTagData } from "./use-tag-store";
|
||||||
|
47
frontend/composables/store/use-category-store.ts
Normal file
47
frontend/composables/store/use-category-store.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { reactive, ref, Ref } from "@nuxtjs/composition-api";
|
||||||
|
import { useStoreActions } from "../partials/use-actions-factory";
|
||||||
|
import { useUserApi } from "~/composables/api";
|
||||||
|
import { RecipeCategory } from "~/types/api-types/admin";
|
||||||
|
|
||||||
|
const categoryStore: Ref<RecipeCategory[]> = ref([]);
|
||||||
|
|
||||||
|
export function useCategoryData() {
|
||||||
|
const data = reactive({
|
||||||
|
id: "",
|
||||||
|
name: "",
|
||||||
|
slug: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
data.id = "";
|
||||||
|
data.name = "";
|
||||||
|
data.slug = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
reset,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCategoryStore() {
|
||||||
|
const api = useUserApi();
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
const actions = {
|
||||||
|
...useStoreActions<RecipeCategory>(api.categories, categoryStore, loading),
|
||||||
|
flushStore() {
|
||||||
|
categoryStore.value = [];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!categoryStore.value || categoryStore.value?.length === 0) {
|
||||||
|
actions.getAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: categoryStore,
|
||||||
|
actions,
|
||||||
|
loading,
|
||||||
|
};
|
||||||
|
}
|
47
frontend/composables/store/use-tag-store.ts
Normal file
47
frontend/composables/store/use-tag-store.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { reactive, ref, Ref } from "@nuxtjs/composition-api";
|
||||||
|
import { useStoreActions } from "../partials/use-actions-factory";
|
||||||
|
import { useUserApi } from "~/composables/api";
|
||||||
|
import { RecipeTag } from "~/types/api-types/admin";
|
||||||
|
|
||||||
|
const items: Ref<RecipeTag[]> = ref([]);
|
||||||
|
|
||||||
|
export function useTagData() {
|
||||||
|
const data = reactive({
|
||||||
|
id: "",
|
||||||
|
name: "",
|
||||||
|
slug: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
data.id = "";
|
||||||
|
data.name = "";
|
||||||
|
data.slug = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
reset,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTagStore() {
|
||||||
|
const api = useUserApi();
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
const actions = {
|
||||||
|
...useStoreActions<RecipeTag>(api.tags, items, loading),
|
||||||
|
flushStore() {
|
||||||
|
items.value = [];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!items.value || items.value?.length === 0) {
|
||||||
|
actions.getAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
items,
|
||||||
|
actions,
|
||||||
|
loading,
|
||||||
|
};
|
||||||
|
}
|
49
frontend/composables/store/use-tool-store.ts
Normal file
49
frontend/composables/store/use-tool-store.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { reactive, ref, Ref } from "@nuxtjs/composition-api";
|
||||||
|
import { useStoreActions } from "../partials/use-actions-factory";
|
||||||
|
import { useUserApi } from "~/composables/api";
|
||||||
|
import { RecipeTool } from "~/types/api-types/recipe";
|
||||||
|
|
||||||
|
const toolStore: Ref<RecipeTool[]> = ref([]);
|
||||||
|
|
||||||
|
export function useToolData() {
|
||||||
|
const data = reactive({
|
||||||
|
id: "",
|
||||||
|
name: "",
|
||||||
|
slug: undefined,
|
||||||
|
onHand: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
data.id = "";
|
||||||
|
data.name = "";
|
||||||
|
data.slug = undefined;
|
||||||
|
data.onHand = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
reset,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useToolStore() {
|
||||||
|
const api = useUserApi();
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
const actions = {
|
||||||
|
...useStoreActions<RecipeTool>(api.tools, toolStore, loading),
|
||||||
|
flushStore() {
|
||||||
|
toolStore.value = [];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!toolStore.value || toolStore.value?.length === 0) {
|
||||||
|
actions.getAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: toolStore,
|
||||||
|
actions,
|
||||||
|
loading,
|
||||||
|
};
|
||||||
|
}
|
30
frontend/composables/use-context-presents.ts
Normal file
30
frontend/composables/use-context-presents.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { useContext } from "@nuxtjs/composition-api";
|
||||||
|
|
||||||
|
export interface ContextMenuItem {
|
||||||
|
title: string;
|
||||||
|
icon: string;
|
||||||
|
event: string;
|
||||||
|
color?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useContextPresets(): { [key: string]: ContextMenuItem } {
|
||||||
|
const { $globals, i18n } = useContext();
|
||||||
|
|
||||||
|
return {
|
||||||
|
delete: {
|
||||||
|
title: i18n.tc("general.delete"),
|
||||||
|
icon: $globals.icons.delete,
|
||||||
|
event: "delete",
|
||||||
|
},
|
||||||
|
edit: {
|
||||||
|
title: i18n.tc("general.edit"),
|
||||||
|
icon: $globals.icons.edit,
|
||||||
|
event: "edit",
|
||||||
|
},
|
||||||
|
save: {
|
||||||
|
title: i18n.tc("general.save"),
|
||||||
|
icon: $globals.icons.save,
|
||||||
|
event: "save",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
@ -36,14 +36,9 @@
|
|||||||
<v-card-text v-if="cookbooks">
|
<v-card-text v-if="cookbooks">
|
||||||
<v-text-field v-model="cookbooks[index].name" label="Cookbook Name"></v-text-field>
|
<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>
|
<v-textarea v-model="cookbooks[index].description" auto-grow :rows="2" label="Description"></v-textarea>
|
||||||
<RecipeOrganizerSelector
|
<RecipeOrganizerSelector v-model="cookbooks[index].categories" selector-type="categories" />
|
||||||
v-model="cookbooks[index].categories"
|
<RecipeOrganizerSelector v-model="cookbooks[index].tags" selector-type="tags" />
|
||||||
:items="allCategories || []"
|
<RecipeOrganizerSelector v-model="cookbooks[index].tools" selector-type="tools" />
|
||||||
selector-type="category"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<RecipeOrganizerSelector v-model="cookbooks[index].tags" :items="allTags || []" selector-type="tag" />
|
|
||||||
<RecipeOrganizerSelector v-model="cookbooks[index].tools" :items="tools || []" selector-type="tool" />
|
|
||||||
<v-switch v-model="cookbooks[index].public" hide-details single-line>
|
<v-switch v-model="cookbooks[index].public" hide-details single-line>
|
||||||
<template #label>
|
<template #label>
|
||||||
Public Cookbook
|
Public Cookbook
|
||||||
@ -102,26 +97,15 @@ import { defineComponent } from "@nuxtjs/composition-api";
|
|||||||
import draggable from "vuedraggable";
|
import draggable from "vuedraggable";
|
||||||
import { useCookbooks } from "@/composables/use-group-cookbooks";
|
import { useCookbooks } from "@/composables/use-group-cookbooks";
|
||||||
import RecipeOrganizerSelector from "~/components/Domain/Recipe/RecipeOrganizerSelector.vue";
|
import RecipeOrganizerSelector from "~/components/Domain/Recipe/RecipeOrganizerSelector.vue";
|
||||||
import { useCategories, useTags, useTools } from "~/composables/recipes";
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: { draggable, RecipeOrganizerSelector },
|
components: { draggable, RecipeOrganizerSelector },
|
||||||
setup() {
|
setup() {
|
||||||
const { cookbooks, actions } = useCookbooks();
|
const { cookbooks, actions } = useCookbooks();
|
||||||
|
|
||||||
const { tools } = useTools();
|
|
||||||
const { allCategories, useAsyncGetAll: getAllCategories } = useCategories();
|
|
||||||
const { allTags, useAsyncGetAll: getAllTags } = useTags();
|
|
||||||
|
|
||||||
getAllCategories();
|
|
||||||
getAllTags();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
allCategories,
|
|
||||||
allTags,
|
|
||||||
cookbooks,
|
cookbooks,
|
||||||
actions,
|
actions,
|
||||||
tools,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
head() {
|
head() {
|
||||||
|
@ -22,10 +22,10 @@
|
|||||||
@submit="dialog.callback"
|
@submit="dialog.callback"
|
||||||
>
|
>
|
||||||
<v-card-text v-if="dialog.mode == MODES.tag">
|
<v-card-text v-if="dialog.mode == MODES.tag">
|
||||||
<RecipeCategoryTagSelector v-model="toSetTags" :tag-selector="true" />
|
<RecipeOrganizerSelector v-model="toSetTags" selector-type="tags" />
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-card-text v-else-if="dialog.mode == MODES.category">
|
<v-card-text v-else-if="dialog.mode == MODES.category">
|
||||||
<RecipeCategoryTagSelector v-model="toSetCategories" />
|
<RecipeOrganizerSelector v-model="toSetCategories" selector-type="categories" />
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-card-text v-else-if="dialog.mode == MODES.delete">
|
<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>
|
<p class="h4">Are you sure you want to delete the following recipes? This action cannot be undone.</p>
|
||||||
@ -149,7 +149,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, reactive, ref, useContext, onMounted } from "@nuxtjs/composition-api";
|
import { defineComponent, reactive, ref, useContext, onMounted } from "@nuxtjs/composition-api";
|
||||||
import RecipeDataTable from "~/components/Domain/Recipe/RecipeDataTable.vue";
|
import RecipeDataTable from "~/components/Domain/Recipe/RecipeDataTable.vue";
|
||||||
import RecipeCategoryTagSelector from "~/components/Domain/Recipe/RecipeCategoryTagSelector.vue";
|
import RecipeOrganizerSelector from "~/components/Domain/Recipe/RecipeOrganizerSelector.vue";
|
||||||
import { useUserApi } from "~/composables/api";
|
import { useUserApi } from "~/composables/api";
|
||||||
import { useRecipes, allRecipes } from "~/composables/recipes";
|
import { useRecipes, allRecipes } from "~/composables/recipes";
|
||||||
import { Recipe } from "~/types/api-types/recipe";
|
import { Recipe } from "~/types/api-types/recipe";
|
||||||
@ -165,7 +165,7 @@ const MODES = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: { RecipeDataTable, RecipeCategoryTagSelector, GroupExportData },
|
components: { RecipeDataTable, RecipeOrganizerSelector, GroupExportData },
|
||||||
scrollToTop: true,
|
scrollToTop: true,
|
||||||
setup() {
|
setup() {
|
||||||
const { getAllRecipes, refreshRecipes } = useRecipes(true, true);
|
const { getAllRecipes, refreshRecipes } = useRecipes(true, true);
|
||||||
|
@ -239,7 +239,7 @@
|
|||||||
hide-details
|
hide-details
|
||||||
class="pt-0 my-auto py-auto"
|
class="pt-0 my-auto py-auto"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
@change="updateTool(recipe.tools[index])"
|
@change="toolStore.actions.updateOne(recipe.tools[index])"
|
||||||
>
|
>
|
||||||
</v-checkbox>
|
</v-checkbox>
|
||||||
<v-list-item-content>
|
<v-list-item-content>
|
||||||
@ -256,12 +256,12 @@
|
|||||||
</v-card-title>
|
</v-card-title>
|
||||||
<v-divider class="mx-2"></v-divider>
|
<v-divider class="mx-2"></v-divider>
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<RecipeCategoryTagSelector
|
<RecipeOrganizerSelector
|
||||||
v-if="form"
|
v-if="form"
|
||||||
v-model="recipe.recipeCategory"
|
v-model="recipe.recipeCategory"
|
||||||
:return-object="true"
|
:return-object="true"
|
||||||
:show-add="true"
|
:show-add="true"
|
||||||
:show-label="false"
|
selector-type="categories"
|
||||||
/>
|
/>
|
||||||
<RecipeChips v-else :items="recipe.recipeCategory" />
|
<RecipeChips v-else :items="recipe.recipeCategory" />
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
@ -274,13 +274,12 @@
|
|||||||
</v-card-title>
|
</v-card-title>
|
||||||
<v-divider class="mx-2"></v-divider>
|
<v-divider class="mx-2"></v-divider>
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<RecipeCategoryTagSelector
|
<RecipeOrganizerSelector
|
||||||
v-if="form"
|
v-if="form"
|
||||||
v-model="recipe.tags"
|
v-model="recipe.tags"
|
||||||
:return-object="true"
|
:return-object="true"
|
||||||
:show-add="true"
|
:show-add="true"
|
||||||
:tag-selector="true"
|
selector-type="tags"
|
||||||
:show-label="false"
|
|
||||||
/>
|
/>
|
||||||
<RecipeChips v-else :items="recipe.tags" url-prefix="tags" />
|
<RecipeChips v-else :items="recipe.tags" url-prefix="tags" />
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
@ -291,7 +290,7 @@
|
|||||||
<v-card-title class="py-2"> Required Tools </v-card-title>
|
<v-card-title class="py-2"> Required Tools </v-card-title>
|
||||||
<v-divider class="mx-2"></v-divider>
|
<v-divider class="mx-2"></v-divider>
|
||||||
<v-card-text class="pt-0">
|
<v-card-text class="pt-0">
|
||||||
<RecipeTools v-model="recipe.tools" :edit="form" />
|
<RecipeOrganizerSelector v-model="recipe.tools" selector-type="tools" />
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
|
|
||||||
@ -344,12 +343,12 @@
|
|||||||
</v-card-title>
|
</v-card-title>
|
||||||
<v-divider class="mx-2"></v-divider>
|
<v-divider class="mx-2"></v-divider>
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<RecipeCategoryTagSelector
|
<RecipeOrganizerSelector
|
||||||
v-if="form"
|
v-if="form"
|
||||||
v-model="recipe.recipeCategory"
|
v-model="recipe.recipeCategory"
|
||||||
:return-object="true"
|
:return-object="true"
|
||||||
:show-add="true"
|
:show-add="true"
|
||||||
:show-label="false"
|
selector-type="categories"
|
||||||
/>
|
/>
|
||||||
<RecipeChips v-else :items="recipe.recipeCategory" />
|
<RecipeChips v-else :items="recipe.recipeCategory" />
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
@ -362,14 +361,14 @@
|
|||||||
</v-card-title>
|
</v-card-title>
|
||||||
<v-divider class="mx-2"></v-divider>
|
<v-divider class="mx-2"></v-divider>
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<RecipeCategoryTagSelector
|
<RecipeOrganizerSelector
|
||||||
v-if="form"
|
v-if="form"
|
||||||
v-model="recipe.tags"
|
v-model="recipe.tags"
|
||||||
:return-object="true"
|
:return-object="true"
|
||||||
:show-add="true"
|
:show-add="true"
|
||||||
:tag-selector="true"
|
selector-type="tags"
|
||||||
:show-label="false"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<RecipeChips v-else :items="recipe.tags" url-prefix="tags" />
|
<RecipeChips v-else :items="recipe.tags" url-prefix="tags" />
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
@ -484,7 +483,7 @@ import VueMarkdown from "@adapttive/vue-markdown";
|
|||||||
import draggable from "vuedraggable";
|
import draggable from "vuedraggable";
|
||||||
import { invoke, until, useWakeLock } from "@vueuse/core";
|
import { invoke, until, useWakeLock } from "@vueuse/core";
|
||||||
import { onUnmounted } from "vue-demi";
|
import { onUnmounted } from "vue-demi";
|
||||||
import RecipeCategoryTagSelector from "@/components/Domain/Recipe/RecipeCategoryTagSelector.vue";
|
import RecipeOrganizerSelector from "@/components/Domain/Recipe/RecipeOrganizerSelector.vue";
|
||||||
import RecipeDialogBulkAdd from "@/components/Domain/Recipe//RecipeDialogBulkAdd.vue";
|
import RecipeDialogBulkAdd from "@/components/Domain/Recipe//RecipeDialogBulkAdd.vue";
|
||||||
import { useUserApi, useStaticRoutes } from "~/composables/api";
|
import { useUserApi, useStaticRoutes } from "~/composables/api";
|
||||||
import { validators } from "~/composables/use-validators";
|
import { validators } from "~/composables/use-validators";
|
||||||
@ -503,9 +502,10 @@ import RecipeIngredientEditor from "~/components/Domain/Recipe/RecipeIngredientE
|
|||||||
import RecipePrintView from "~/components/Domain/Recipe/RecipePrintView.vue";
|
import RecipePrintView from "~/components/Domain/Recipe/RecipePrintView.vue";
|
||||||
import RecipeTools from "~/components/Domain/Recipe/RecipeTools.vue";
|
import RecipeTools from "~/components/Domain/Recipe/RecipeTools.vue";
|
||||||
import RecipeComments from "~/components/Domain/Recipe/RecipeComments.vue";
|
import RecipeComments from "~/components/Domain/Recipe/RecipeComments.vue";
|
||||||
import { Recipe, RecipeTool } from "~/types/api-types/recipe";
|
import { Recipe } from "~/types/api-types/recipe";
|
||||||
import { uuid4, deepCopy } from "~/composables/use-utils";
|
import { uuid4, deepCopy } from "~/composables/use-utils";
|
||||||
import { useRouteQuery } from "~/composables/use-router";
|
import { useRouteQuery } from "~/composables/use-router";
|
||||||
|
import { useToolStore } from "~/composables/store";
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
@ -516,7 +516,7 @@ export default defineComponent({
|
|||||||
return import(/* webpackChunkName: "RecipeAssets" */ "~/components/Domain/Recipe/RecipeAssets.vue");
|
return import(/* webpackChunkName: "RecipeAssets" */ "~/components/Domain/Recipe/RecipeAssets.vue");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
RecipeCategoryTagSelector,
|
RecipeOrganizerSelector,
|
||||||
RecipeChips,
|
RecipeChips,
|
||||||
RecipeComments,
|
RecipeComments,
|
||||||
RecipeDialogBulkAdd,
|
RecipeDialogBulkAdd,
|
||||||
@ -758,18 +758,7 @@ export default defineComponent({
|
|||||||
// ===============================================================
|
// ===============================================================
|
||||||
// Recipe Tools
|
// Recipe Tools
|
||||||
|
|
||||||
async function updateTool(tool: RecipeTool) {
|
const toolStore = useToolStore();
|
||||||
if (tool.id === undefined) return;
|
|
||||||
|
|
||||||
const { response } = await api.tools.updateOne(tool.id, tool);
|
|
||||||
|
|
||||||
if (response?.status === 200) {
|
|
||||||
console.log("Update Successful");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===============================================================
|
|
||||||
// Recipe API Extras
|
|
||||||
|
|
||||||
const apiNewKey = ref("");
|
const apiNewKey = ref("");
|
||||||
|
|
||||||
@ -864,13 +853,13 @@ export default defineComponent({
|
|||||||
deleteRecipe,
|
deleteRecipe,
|
||||||
printRecipe,
|
printRecipe,
|
||||||
closeEditor,
|
closeEditor,
|
||||||
updateTool,
|
|
||||||
updateRecipe,
|
updateRecipe,
|
||||||
uploadImage,
|
uploadImage,
|
||||||
validators,
|
validators,
|
||||||
recipeImage,
|
recipeImage,
|
||||||
addIngredient,
|
addIngredient,
|
||||||
removeApiExtra,
|
removeApiExtra,
|
||||||
|
toolStore,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
head: {},
|
head: {},
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div flat>
|
<div>
|
||||||
<v-card-title class="headline"> Recipe Bulk Importer </v-card-title>
|
<v-card-title class="headline"> Recipe Bulk Importer </v-card-title>
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
The Bulk recipe importer allows you to import multiple recipes at once by queing the sites on the backend and
|
The Bulk recipe importer allows you to import multiple recipes at once by queing the sites on the backend and
|
||||||
@ -38,8 +38,7 @@
|
|||||||
<v-col cols="12" xs="12" sm="6">
|
<v-col cols="12" xs="12" sm="6">
|
||||||
<RecipeOrganizerSelector
|
<RecipeOrganizerSelector
|
||||||
v-model="bulkUrls[idx].categories"
|
v-model="bulkUrls[idx].categories"
|
||||||
:items="allCategories || []"
|
selector-type="categories"
|
||||||
selector-type="category"
|
|
||||||
:input-attrs="{
|
:input-attrs="{
|
||||||
filled: true,
|
filled: true,
|
||||||
singleLine: true,
|
singleLine: true,
|
||||||
@ -54,8 +53,7 @@
|
|||||||
<v-col cols="12" xs="12" sm="6">
|
<v-col cols="12" xs="12" sm="6">
|
||||||
<RecipeOrganizerSelector
|
<RecipeOrganizerSelector
|
||||||
v-model="bulkUrls[idx].tags"
|
v-model="bulkUrls[idx].tags"
|
||||||
:items="allTags || []"
|
selector-type="tags"
|
||||||
selector-type="tag"
|
|
||||||
:input-attrs="{
|
:input-attrs="{
|
||||||
filled: true,
|
filled: true,
|
||||||
singleLine: true,
|
singleLine: true,
|
||||||
@ -109,7 +107,6 @@ import { whenever } from "@vueuse/shared";
|
|||||||
import { useUserApi } from "~/composables/api";
|
import { useUserApi } from "~/composables/api";
|
||||||
import { alert } from "~/composables/use-toast";
|
import { alert } from "~/composables/use-toast";
|
||||||
import RecipeOrganizerSelector from "~/components/Domain/Recipe/RecipeOrganizerSelector.vue";
|
import RecipeOrganizerSelector from "~/components/Domain/Recipe/RecipeOrganizerSelector.vue";
|
||||||
import { useCategories, useTags } from "~/composables/recipes";
|
|
||||||
import { ReportSummary } from "~/types/api-types/reports";
|
import { ReportSummary } from "~/types/api-types/reports";
|
||||||
import RecipeDialogBulkAdd from "~/components/Domain/Recipe/RecipeDialogBulkAdd.vue";
|
import RecipeDialogBulkAdd from "~/components/Domain/Recipe/RecipeDialogBulkAdd.vue";
|
||||||
|
|
||||||
@ -152,12 +149,6 @@ export default defineComponent({
|
|||||||
fetchReports();
|
fetchReports();
|
||||||
}
|
}
|
||||||
|
|
||||||
const { allTags, useAsyncGetAll: getAllTags } = useTags();
|
|
||||||
const { allCategories, useAsyncGetAll: getAllCategories } = useCategories();
|
|
||||||
|
|
||||||
getAllTags();
|
|
||||||
getAllCategories();
|
|
||||||
|
|
||||||
// =========================================================
|
// =========================================================
|
||||||
// Reports
|
// Reports
|
||||||
|
|
||||||
@ -189,8 +180,6 @@ export default defineComponent({
|
|||||||
assignUrls,
|
assignUrls,
|
||||||
reports,
|
reports,
|
||||||
deleteReport,
|
deleteReport,
|
||||||
allTags,
|
|
||||||
allCategories,
|
|
||||||
bulkCreate,
|
bulkCreate,
|
||||||
bulkUrls,
|
bulkUrls,
|
||||||
lockBulkImport,
|
lockBulkImport,
|
||||||
|
@ -1,44 +1,36 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-container>
|
<v-container>
|
||||||
<RecipeCategoryTagToolPage v-if="categories" :items="categories" item-type="categories" @delete="removeCat" />
|
<RecipeOrganizerPage
|
||||||
|
v-if="items"
|
||||||
|
:items="items"
|
||||||
|
:icon="$globals.icons.tags"
|
||||||
|
item-type="categories"
|
||||||
|
@delete="actions.deleteOne"
|
||||||
|
>
|
||||||
|
<template #title> {{ $tc("category.categories") }} </template>
|
||||||
|
</RecipeOrganizerPage>
|
||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, useAsync } from "@nuxtjs/composition-api";
|
import { defineComponent } from "@nuxtjs/composition-api";
|
||||||
import RecipeCategoryTagToolPage from "~/components/Domain/Recipe/RecipeCategoryTagToolPage.vue";
|
import RecipeOrganizerPage from "~/components/Domain/Recipe/RecipeOrganizerPage.vue";
|
||||||
import { useUserApi } from "~/composables/api";
|
import { useCategoryStore } from "~/composables/store";
|
||||||
import { useAsyncKey } from "~/composables/use-utils";
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
RecipeCategoryTagToolPage,
|
RecipeOrganizerPage,
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const userApi = useUserApi();
|
const { items, actions } = useCategoryStore();
|
||||||
const categories = useAsync(async () => {
|
|
||||||
const { data } = await userApi.categories.getAll();
|
|
||||||
|
|
||||||
if (data) {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
}, useAsyncKey());
|
|
||||||
|
|
||||||
function removeCat(id: string) {
|
|
||||||
if (categories.value) {
|
|
||||||
for (let i = 0; i < categories.value.length; i++) {
|
|
||||||
if (categories.value[i].id === id) {
|
|
||||||
categories.value.splice(i, 1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
categories,
|
items,
|
||||||
removeCat,
|
actions,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
head: {
|
||||||
|
title: "Tags",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,44 +1,36 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-container>
|
<v-container>
|
||||||
<RecipeCategoryTagToolPage v-if="tools" :items="tools" item-type="tags" @delete="removeTag" />
|
<RecipeOrganizerPage
|
||||||
|
v-if="items"
|
||||||
|
:items="items"
|
||||||
|
:icon="$globals.icons.tags"
|
||||||
|
item-type="tags"
|
||||||
|
@delete="actions.deleteOne"
|
||||||
|
>
|
||||||
|
<template #title> Tags </template>
|
||||||
|
</RecipeOrganizerPage>
|
||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, useAsync } from "@nuxtjs/composition-api";
|
import { defineComponent } from "@nuxtjs/composition-api";
|
||||||
import RecipeCategoryTagToolPage from "~/components/Domain/Recipe/RecipeCategoryTagToolPage.vue";
|
import RecipeOrganizerPage from "~/components/Domain/Recipe/RecipeOrganizerPage.vue";
|
||||||
import { useUserApi } from "~/composables/api";
|
import { useTagStore } from "~/composables/store";
|
||||||
import { useAsyncKey } from "~/composables/use-utils";
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
RecipeCategoryTagToolPage,
|
RecipeOrganizerPage,
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const userApi = useUserApi();
|
const { items, actions } = useTagStore();
|
||||||
const tools = useAsync(async () => {
|
|
||||||
const { data } = await userApi.tags.getAll();
|
|
||||||
|
|
||||||
if (data) {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
}, useAsyncKey());
|
|
||||||
|
|
||||||
function removeTag(id: string) {
|
|
||||||
if (tools.value) {
|
|
||||||
for (let i = 0; i < tools.value.length; i++) {
|
|
||||||
if (tools.value[i].id === id) {
|
|
||||||
tools.value.splice(i, 1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tools,
|
items,
|
||||||
removeTag,
|
actions,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
head: {
|
||||||
|
title: "Tags",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,44 +1,38 @@
|
|||||||
<template>
|
<template>
|
||||||
<v-container>
|
<v-container>
|
||||||
<RecipeCategoryTagToolPage v-if="tools" :items="tools" item-type="tools" @delete="removeTool" />
|
<RecipeOrganizerPage
|
||||||
|
v-if="tools"
|
||||||
|
:icon="$globals.icons.potSteam"
|
||||||
|
:items="tools"
|
||||||
|
item-type="tools"
|
||||||
|
@delete="actions.deleteOne"
|
||||||
|
>
|
||||||
|
<template #title> Tools </template>
|
||||||
|
</RecipeOrganizerPage>
|
||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, useAsync } from "@nuxtjs/composition-api";
|
import { defineComponent, ref } from "@nuxtjs/composition-api";
|
||||||
import RecipeCategoryTagToolPage from "~/components/Domain/Recipe/RecipeCategoryTagToolPage.vue";
|
import RecipeOrganizerPage from "~/components/Domain/Recipe/RecipeOrganizerPage.vue";
|
||||||
import { useUserApi } from "~/composables/api";
|
import { useToolStore } from "~/composables/store";
|
||||||
import { useAsyncKey } from "~/composables/use-utils";
|
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
RecipeCategoryTagToolPage,
|
RecipeOrganizerPage,
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const userApi = useUserApi();
|
const toolStore = useToolStore();
|
||||||
const tools = useAsync(async () => {
|
const dialog = ref(false);
|
||||||
const { data } = await userApi.tools.getAll();
|
|
||||||
|
|
||||||
if (data) {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
}, useAsyncKey());
|
|
||||||
|
|
||||||
function removeTool(id: string) {
|
|
||||||
if (tools.value) {
|
|
||||||
for (let i = 0; i < tools.value.length; i++) {
|
|
||||||
if (tools.value[i].id === id) {
|
|
||||||
tools.value.splice(i, 1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tools,
|
dialog,
|
||||||
removeTool,
|
tools: toolStore.items,
|
||||||
|
actions: toolStore.actions,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
head: {
|
||||||
|
title: "Tools",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -35,23 +35,30 @@
|
|||||||
<v-expand-transition>
|
<v-expand-transition>
|
||||||
<v-row v-show="state" dense class="my-0 dense flex-row align-center justify-space-around">
|
<v-row v-show="state" dense class="my-0 dense flex-row align-center justify-space-around">
|
||||||
<v-col cols="12" class="d-flex flex-wrap flex-md-nowrap justify-center" style="gap: 0.8rem">
|
<v-col cols="12" class="d-flex flex-wrap flex-md-nowrap justify-center" style="gap: 0.8rem">
|
||||||
<RecipeCategoryTagSelector
|
<RecipeOrganizerSelector
|
||||||
v-model="includeCategories"
|
v-model="includeCategories"
|
||||||
hide-details
|
:input-attrs="{
|
||||||
:solo="true"
|
solo: true,
|
||||||
:dense="false"
|
hideDetails: true,
|
||||||
|
dense: false,
|
||||||
|
}"
|
||||||
|
:show-add="false"
|
||||||
:return-object="false"
|
:return-object="false"
|
||||||
|
selector-type="categories"
|
||||||
/>
|
/>
|
||||||
<RecipeSearchFilterSelector class="mb-1" @update="updateCatParams" />
|
<RecipeSearchFilterSelector class="mb-1" @update="updateCatParams" />
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col cols="12" class="d-flex flex-wrap flex-md-nowrap justify-center" style="gap: 0.8rem">
|
<v-col cols="12" class="d-flex flex-wrap flex-md-nowrap justify-center" style="gap: 0.8rem">
|
||||||
<RecipeCategoryTagSelector
|
<RecipeOrganizerSelector
|
||||||
v-model="includeTags"
|
v-model="includeTags"
|
||||||
hide-details
|
:input-attrs="{
|
||||||
:solo="true"
|
solo: true,
|
||||||
:dense="false"
|
hideDetails: true,
|
||||||
|
dense: false,
|
||||||
|
}"
|
||||||
|
:show-add="false"
|
||||||
:return-object="false"
|
:return-object="false"
|
||||||
:tag-selector="true"
|
selector-type="tags"
|
||||||
/>
|
/>
|
||||||
<RecipeSearchFilterSelector class="mb-1" @update="updateTagParams" />
|
<RecipeSearchFilterSelector class="mb-1" @update="updateTagParams" />
|
||||||
</v-col>
|
</v-col>
|
||||||
@ -106,7 +113,7 @@
|
|||||||
import Fuse from "fuse.js";
|
import Fuse from "fuse.js";
|
||||||
import { defineComponent, toRefs, computed, reactive } from "@nuxtjs/composition-api";
|
import { defineComponent, toRefs, computed, reactive } from "@nuxtjs/composition-api";
|
||||||
import RecipeSearchFilterSelector from "~/components/Domain/Recipe/RecipeSearchFilterSelector.vue";
|
import RecipeSearchFilterSelector from "~/components/Domain/Recipe/RecipeSearchFilterSelector.vue";
|
||||||
import RecipeCategoryTagSelector from "~/components/Domain/Recipe/RecipeCategoryTagSelector.vue";
|
import RecipeOrganizerSelector from "~/components/Domain/Recipe/RecipeOrganizerSelector.vue";
|
||||||
import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue";
|
import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue";
|
||||||
import { useRecipes, allRecipes } from "~/composables/recipes";
|
import { useRecipes, allRecipes } from "~/composables/recipes";
|
||||||
import { RecipeSummary } from "~/types/api-types/recipe";
|
import { RecipeSummary } from "~/types/api-types/recipe";
|
||||||
@ -121,7 +128,7 @@ interface GenericFilter {
|
|||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: {
|
components: {
|
||||||
RecipeCategoryTagSelector,
|
RecipeOrganizerSelector,
|
||||||
RecipeSearchFilterSelector,
|
RecipeSearchFilterSelector,
|
||||||
RecipeCardSection,
|
RecipeCardSection,
|
||||||
},
|
},
|
||||||
|
7
frontend/types/recipe/organizers.ts
Normal file
7
frontend/types/recipe/organizers.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export type RecipeOrganizer = "categories" | "tags" | "tools";
|
||||||
|
|
||||||
|
export enum Organizer {
|
||||||
|
Category = "categories",
|
||||||
|
Tag = "tags",
|
||||||
|
Tool = "tools",
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user