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
26 changed files with 719 additions and 857 deletions
|
@ -5,8 +5,8 @@
|
|||
<v-select v-model="inputEntryType" :items="MEAL_TYPE_OPTIONS" label="Meal Type"></v-select>
|
||||
</div>
|
||||
|
||||
<RecipeCategoryTagSelector v-model="inputCategories" />
|
||||
<RecipeCategoryTagSelector v-model="inputTags" :tag-selector="true" />
|
||||
<RecipeOrganizerSelector v-model="inputCategories" selector-type="categories" />
|
||||
<RecipeOrganizerSelector v-model="inputTags" selector-type="tags" />
|
||||
|
||||
{{ 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` }}
|
||||
|
@ -15,7 +15,8 @@
|
|||
|
||||
<script lang="ts">
|
||||
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 = [
|
||||
{ text: "Breakfast", value: "breakfast" },
|
||||
|
@ -38,7 +39,7 @@ const MEAL_DAY_OPTIONS = [
|
|||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
RecipeCategoryTagSelector,
|
||||
RecipeOrganizerSelector,
|
||||
},
|
||||
props: {
|
||||
day: {
|
||||
|
@ -50,11 +51,11 @@ export default defineComponent({
|
|||
default: "unset",
|
||||
},
|
||||
categories: {
|
||||
type: Array,
|
||||
type: Array as () => RecipeCategory[],
|
||||
default: () => [],
|
||||
},
|
||||
tags: {
|
||||
type: Array,
|
||||
type: Array as () => RecipeTag[],
|
||||
default: () => [],
|
||||
},
|
||||
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>
|
||||
<v-autocomplete
|
||||
v-model="selected"
|
||||
:items="items"
|
||||
:items="storeItem"
|
||||
:value="value"
|
||||
:label="label"
|
||||
chips
|
||||
deletable-chips
|
||||
item-text="name"
|
||||
multiple
|
||||
:prepend-inner-icon="$globals.icons.tags"
|
||||
:prepend-inner-icon="selectorType === Organizer.Tool ? $globals.icons.potSteam : $globals.icons.tags"
|
||||
return-object
|
||||
v-bind="inputAttrs"
|
||||
>
|
||||
|
@ -17,6 +17,7 @@
|
|||
:key="data.index"
|
||||
class="ma-1"
|
||||
:input-value="data.selected"
|
||||
small
|
||||
close
|
||||
label
|
||||
color="accent"
|
||||
|
@ -26,41 +27,55 @@
|
|||
{{ data.item.name || data.item }}
|
||||
</v-chip>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<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 RecipeOrganizerDialog from "./RecipeOrganizerDialog.vue";
|
||||
import { RecipeCategory, RecipeTag } from "~/types/api-types/user";
|
||||
import { RecipeTool } from "~/types/api-types/admin";
|
||||
|
||||
type OrganizerType = "tag" | "category" | "tool";
|
||||
import { useTagStore } from "~/composables/store/use-tag-store";
|
||||
import { useCategoryStore, useToolStore } from "~/composables/store";
|
||||
import { Organizer, RecipeOrganizer } from "~/types/recipe/organizers";
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
RecipeOrganizerDialog,
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: Array as () => (RecipeTag | RecipeCategory | RecipeTool)[] | undefined,
|
||||
type: Array as () => (RecipeTag | RecipeCategory | RecipeTool | string)[] | undefined,
|
||||
required: true,
|
||||
},
|
||||
/**
|
||||
* The type of organizer to use.
|
||||
*/
|
||||
selectorType: {
|
||||
type: String as () => OrganizerType,
|
||||
required: true,
|
||||
},
|
||||
/**
|
||||
* List of items that are available to be chosen from
|
||||
*/
|
||||
items: {
|
||||
type: Array as () => (RecipeTag | RecipeCategory | RecipeTool)[],
|
||||
type: String as () => RecipeOrganizer,
|
||||
required: true,
|
||||
},
|
||||
inputAttrs: {
|
||||
type: Object as () => Record<string, any>,
|
||||
default: () => ({}),
|
||||
},
|
||||
returnObject: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
showAdd: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
|
||||
setup(props, context) {
|
||||
|
@ -81,27 +96,62 @@ export default defineComponent({
|
|||
|
||||
const label = computed(() => {
|
||||
switch (props.selectorType) {
|
||||
case "tag":
|
||||
case Organizer.Tag:
|
||||
return i18n.t("tag.tags");
|
||||
case "category":
|
||||
case Organizer.Category:
|
||||
return i18n.t("category.categories");
|
||||
case "tool":
|
||||
return "Tools";
|
||||
case Organizer.Tool:
|
||||
return i18n.t("tool.tools");
|
||||
default:
|
||||
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) {
|
||||
if (selected.value === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newSelected = selected.value.filter((_, i) => i !== index);
|
||||
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 {
|
||||
Organizer,
|
||||
appendCreated,
|
||||
dialog,
|
||||
storeItem: items,
|
||||
label,
|
||||
selected,
|
||||
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";
|
||||
|
||||
type BoundT = {
|
||||
id: string | number;
|
||||
id?: string | number;
|
||||
};
|
||||
|
||||
interface StoreActions<T extends BoundT> {
|
||||
|
@ -29,7 +29,12 @@ export function useStoreActions<T extends BoundT>(
|
|||
loading.value = true;
|
||||
const allItems = useAsync(async () => {
|
||||
const { data } = await api.getAll();
|
||||
return data;
|
||||
|
||||
if (allRef) {
|
||||
allRef.value = data;
|
||||
}
|
||||
|
||||
return data ?? [];
|
||||
}, useAsyncKey());
|
||||
|
||||
loading.value = false;
|
||||
|
@ -73,8 +78,8 @@ export function useStoreActions<T extends BoundT>(
|
|||
|
||||
async function deleteOne(id: string | number) {
|
||||
loading.value = true;
|
||||
const { data } = await api.deleteOne(id);
|
||||
if (data && allRef?.value) {
|
||||
const { response } = await api.deleteOne(id);
|
||||
if (response && allRef?.value) {
|
||||
refresh();
|
||||
}
|
||||
loading.value = false;
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
export { useFraction } from "./use-fraction";
|
||||
export { useRecipe } from "./use-recipe";
|
||||
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 { useRecipeSearch } from "./use-recipe-search";
|
||||
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 { useUnitStore, useUnitData } from "./use-unit-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-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>
|
||||
<RecipeOrganizerSelector
|
||||
v-model="cookbooks[index].categories"
|
||||
:items="allCategories || []"
|
||||
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" />
|
||||
<RecipeOrganizerSelector v-model="cookbooks[index].categories" selector-type="categories" />
|
||||
<RecipeOrganizerSelector v-model="cookbooks[index].tags" selector-type="tags" />
|
||||
<RecipeOrganizerSelector v-model="cookbooks[index].tools" selector-type="tools" />
|
||||
<v-switch v-model="cookbooks[index].public" hide-details single-line>
|
||||
<template #label>
|
||||
Public Cookbook
|
||||
|
@ -102,26 +97,15 @@ import { defineComponent } from "@nuxtjs/composition-api";
|
|||
import draggable from "vuedraggable";
|
||||
import { useCookbooks } from "@/composables/use-group-cookbooks";
|
||||
import RecipeOrganizerSelector from "~/components/Domain/Recipe/RecipeOrganizerSelector.vue";
|
||||
import { useCategories, useTags, useTools } from "~/composables/recipes";
|
||||
|
||||
export default defineComponent({
|
||||
components: { draggable, RecipeOrganizerSelector },
|
||||
setup() {
|
||||
const { cookbooks, actions } = useCookbooks();
|
||||
|
||||
const { tools } = useTools();
|
||||
const { allCategories, useAsyncGetAll: getAllCategories } = useCategories();
|
||||
const { allTags, useAsyncGetAll: getAllTags } = useTags();
|
||||
|
||||
getAllCategories();
|
||||
getAllTags();
|
||||
|
||||
return {
|
||||
allCategories,
|
||||
allTags,
|
||||
cookbooks,
|
||||
actions,
|
||||
tools,
|
||||
};
|
||||
},
|
||||
head() {
|
||||
|
|
|
@ -22,10 +22,10 @@
|
|||
@submit="dialog.callback"
|
||||
>
|
||||
<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-else-if="dialog.mode == MODES.category">
|
||||
<RecipeCategoryTagSelector v-model="toSetCategories" />
|
||||
<RecipeOrganizerSelector v-model="toSetCategories" selector-type="categories" />
|
||||
</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>
|
||||
|
@ -149,7 +149,7 @@
|
|||
<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 RecipeOrganizerSelector from "~/components/Domain/Recipe/RecipeOrganizerSelector.vue";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { useRecipes, allRecipes } from "~/composables/recipes";
|
||||
import { Recipe } from "~/types/api-types/recipe";
|
||||
|
@ -165,7 +165,7 @@ const MODES = {
|
|||
};
|
||||
|
||||
export default defineComponent({
|
||||
components: { RecipeDataTable, RecipeCategoryTagSelector, GroupExportData },
|
||||
components: { RecipeDataTable, RecipeOrganizerSelector, GroupExportData },
|
||||
scrollToTop: true,
|
||||
setup() {
|
||||
const { getAllRecipes, refreshRecipes } = useRecipes(true, true);
|
||||
|
|
|
@ -239,7 +239,7 @@
|
|||
hide-details
|
||||
class="pt-0 my-auto py-auto"
|
||||
color="secondary"
|
||||
@change="updateTool(recipe.tools[index])"
|
||||
@change="toolStore.actions.updateOne(recipe.tools[index])"
|
||||
>
|
||||
</v-checkbox>
|
||||
<v-list-item-content>
|
||||
|
@ -256,12 +256,12 @@
|
|||
</v-card-title>
|
||||
<v-divider class="mx-2"></v-divider>
|
||||
<v-card-text>
|
||||
<RecipeCategoryTagSelector
|
||||
<RecipeOrganizerSelector
|
||||
v-if="form"
|
||||
v-model="recipe.recipeCategory"
|
||||
:return-object="true"
|
||||
:show-add="true"
|
||||
:show-label="false"
|
||||
selector-type="categories"
|
||||
/>
|
||||
<RecipeChips v-else :items="recipe.recipeCategory" />
|
||||
</v-card-text>
|
||||
|
@ -274,13 +274,12 @@
|
|||
</v-card-title>
|
||||
<v-divider class="mx-2"></v-divider>
|
||||
<v-card-text>
|
||||
<RecipeCategoryTagSelector
|
||||
<RecipeOrganizerSelector
|
||||
v-if="form"
|
||||
v-model="recipe.tags"
|
||||
:return-object="true"
|
||||
:show-add="true"
|
||||
:tag-selector="true"
|
||||
:show-label="false"
|
||||
selector-type="tags"
|
||||
/>
|
||||
<RecipeChips v-else :items="recipe.tags" url-prefix="tags" />
|
||||
</v-card-text>
|
||||
|
@ -291,7 +290,7 @@
|
|||
<v-card-title class="py-2"> Required Tools </v-card-title>
|
||||
<v-divider class="mx-2"></v-divider>
|
||||
<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>
|
||||
|
||||
|
@ -344,12 +343,12 @@
|
|||
</v-card-title>
|
||||
<v-divider class="mx-2"></v-divider>
|
||||
<v-card-text>
|
||||
<RecipeCategoryTagSelector
|
||||
<RecipeOrganizerSelector
|
||||
v-if="form"
|
||||
v-model="recipe.recipeCategory"
|
||||
:return-object="true"
|
||||
:show-add="true"
|
||||
:show-label="false"
|
||||
selector-type="categories"
|
||||
/>
|
||||
<RecipeChips v-else :items="recipe.recipeCategory" />
|
||||
</v-card-text>
|
||||
|
@ -362,14 +361,14 @@
|
|||
</v-card-title>
|
||||
<v-divider class="mx-2"></v-divider>
|
||||
<v-card-text>
|
||||
<RecipeCategoryTagSelector
|
||||
<RecipeOrganizerSelector
|
||||
v-if="form"
|
||||
v-model="recipe.tags"
|
||||
:return-object="true"
|
||||
:show-add="true"
|
||||
:tag-selector="true"
|
||||
:show-label="false"
|
||||
selector-type="tags"
|
||||
/>
|
||||
|
||||
<RecipeChips v-else :items="recipe.tags" url-prefix="tags" />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
@ -484,7 +483,7 @@ import VueMarkdown from "@adapttive/vue-markdown";
|
|||
import draggable from "vuedraggable";
|
||||
import { invoke, until, useWakeLock } from "@vueuse/core";
|
||||
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 { useUserApi, useStaticRoutes } from "~/composables/api";
|
||||
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 RecipeTools from "~/components/Domain/Recipe/RecipeTools.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 { useRouteQuery } from "~/composables/use-router";
|
||||
import { useToolStore } from "~/composables/store";
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
|
@ -516,7 +516,7 @@ export default defineComponent({
|
|||
return import(/* webpackChunkName: "RecipeAssets" */ "~/components/Domain/Recipe/RecipeAssets.vue");
|
||||
}
|
||||
},
|
||||
RecipeCategoryTagSelector,
|
||||
RecipeOrganizerSelector,
|
||||
RecipeChips,
|
||||
RecipeComments,
|
||||
RecipeDialogBulkAdd,
|
||||
|
@ -758,18 +758,7 @@ export default defineComponent({
|
|||
// ===============================================================
|
||||
// Recipe Tools
|
||||
|
||||
async function updateTool(tool: RecipeTool) {
|
||||
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 toolStore = useToolStore();
|
||||
|
||||
const apiNewKey = ref("");
|
||||
|
||||
|
@ -864,13 +853,13 @@ export default defineComponent({
|
|||
deleteRecipe,
|
||||
printRecipe,
|
||||
closeEditor,
|
||||
updateTool,
|
||||
updateRecipe,
|
||||
uploadImage,
|
||||
validators,
|
||||
recipeImage,
|
||||
addIngredient,
|
||||
removeApiExtra,
|
||||
toolStore,
|
||||
};
|
||||
},
|
||||
head: {},
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div>
|
||||
<div flat>
|
||||
<div>
|
||||
<v-card-title class="headline"> Recipe Bulk Importer </v-card-title>
|
||||
<v-card-text>
|
||||
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">
|
||||
<RecipeOrganizerSelector
|
||||
v-model="bulkUrls[idx].categories"
|
||||
:items="allCategories || []"
|
||||
selector-type="category"
|
||||
selector-type="categories"
|
||||
:input-attrs="{
|
||||
filled: true,
|
||||
singleLine: true,
|
||||
|
@ -54,8 +53,7 @@
|
|||
<v-col cols="12" xs="12" sm="6">
|
||||
<RecipeOrganizerSelector
|
||||
v-model="bulkUrls[idx].tags"
|
||||
:items="allTags || []"
|
||||
selector-type="tag"
|
||||
selector-type="tags"
|
||||
:input-attrs="{
|
||||
filled: true,
|
||||
singleLine: true,
|
||||
|
@ -109,7 +107,6 @@ import { whenever } from "@vueuse/shared";
|
|||
import { useUserApi } from "~/composables/api";
|
||||
import { alert } from "~/composables/use-toast";
|
||||
import RecipeOrganizerSelector from "~/components/Domain/Recipe/RecipeOrganizerSelector.vue";
|
||||
import { useCategories, useTags } from "~/composables/recipes";
|
||||
import { ReportSummary } from "~/types/api-types/reports";
|
||||
import RecipeDialogBulkAdd from "~/components/Domain/Recipe/RecipeDialogBulkAdd.vue";
|
||||
|
||||
|
@ -152,12 +149,6 @@ export default defineComponent({
|
|||
fetchReports();
|
||||
}
|
||||
|
||||
const { allTags, useAsyncGetAll: getAllTags } = useTags();
|
||||
const { allCategories, useAsyncGetAll: getAllCategories } = useCategories();
|
||||
|
||||
getAllTags();
|
||||
getAllCategories();
|
||||
|
||||
// =========================================================
|
||||
// Reports
|
||||
|
||||
|
@ -189,8 +180,6 @@ export default defineComponent({
|
|||
assignUrls,
|
||||
reports,
|
||||
deleteReport,
|
||||
allTags,
|
||||
allCategories,
|
||||
bulkCreate,
|
||||
bulkUrls,
|
||||
lockBulkImport,
|
||||
|
|
|
@ -1,44 +1,36 @@
|
|||
<template>
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, useAsync } from "@nuxtjs/composition-api";
|
||||
import RecipeCategoryTagToolPage from "~/components/Domain/Recipe/RecipeCategoryTagToolPage.vue";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { useAsyncKey } from "~/composables/use-utils";
|
||||
import { defineComponent } from "@nuxtjs/composition-api";
|
||||
import RecipeOrganizerPage from "~/components/Domain/Recipe/RecipeOrganizerPage.vue";
|
||||
import { useCategoryStore } from "~/composables/store";
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
RecipeCategoryTagToolPage,
|
||||
RecipeOrganizerPage,
|
||||
},
|
||||
setup() {
|
||||
const userApi = useUserApi();
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const { items, actions } = useCategoryStore();
|
||||
|
||||
return {
|
||||
categories,
|
||||
removeCat,
|
||||
items,
|
||||
actions,
|
||||
};
|
||||
},
|
||||
head: {
|
||||
title: "Tags",
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -1,44 +1,36 @@
|
|||
<template>
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, useAsync } from "@nuxtjs/composition-api";
|
||||
import RecipeCategoryTagToolPage from "~/components/Domain/Recipe/RecipeCategoryTagToolPage.vue";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { useAsyncKey } from "~/composables/use-utils";
|
||||
import { defineComponent } from "@nuxtjs/composition-api";
|
||||
import RecipeOrganizerPage from "~/components/Domain/Recipe/RecipeOrganizerPage.vue";
|
||||
import { useTagStore } from "~/composables/store";
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
RecipeCategoryTagToolPage,
|
||||
RecipeOrganizerPage,
|
||||
},
|
||||
setup() {
|
||||
const userApi = useUserApi();
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const { items, actions } = useTagStore();
|
||||
|
||||
return {
|
||||
tools,
|
||||
removeTag,
|
||||
items,
|
||||
actions,
|
||||
};
|
||||
},
|
||||
head: {
|
||||
title: "Tags",
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -1,44 +1,38 @@
|
|||
<template>
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, useAsync } from "@nuxtjs/composition-api";
|
||||
import RecipeCategoryTagToolPage from "~/components/Domain/Recipe/RecipeCategoryTagToolPage.vue";
|
||||
import { useUserApi } from "~/composables/api";
|
||||
import { useAsyncKey } from "~/composables/use-utils";
|
||||
import { defineComponent, ref } from "@nuxtjs/composition-api";
|
||||
import RecipeOrganizerPage from "~/components/Domain/Recipe/RecipeOrganizerPage.vue";
|
||||
import { useToolStore } from "~/composables/store";
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
RecipeCategoryTagToolPage,
|
||||
RecipeOrganizerPage,
|
||||
},
|
||||
setup() {
|
||||
const userApi = useUserApi();
|
||||
const tools = useAsync(async () => {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const toolStore = useToolStore();
|
||||
const dialog = ref(false);
|
||||
|
||||
return {
|
||||
tools,
|
||||
removeTool,
|
||||
dialog,
|
||||
tools: toolStore.items,
|
||||
actions: toolStore.actions,
|
||||
};
|
||||
},
|
||||
head: {
|
||||
title: "Tools",
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -35,23 +35,30 @@
|
|||
<v-expand-transition>
|
||||
<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">
|
||||
<RecipeCategoryTagSelector
|
||||
<RecipeOrganizerSelector
|
||||
v-model="includeCategories"
|
||||
hide-details
|
||||
:solo="true"
|
||||
:dense="false"
|
||||
:input-attrs="{
|
||||
solo: true,
|
||||
hideDetails: true,
|
||||
dense: false,
|
||||
}"
|
||||
:show-add="false"
|
||||
:return-object="false"
|
||||
selector-type="categories"
|
||||
/>
|
||||
<RecipeSearchFilterSelector class="mb-1" @update="updateCatParams" />
|
||||
</v-col>
|
||||
<v-col cols="12" class="d-flex flex-wrap flex-md-nowrap justify-center" style="gap: 0.8rem">
|
||||
<RecipeCategoryTagSelector
|
||||
<RecipeOrganizerSelector
|
||||
v-model="includeTags"
|
||||
hide-details
|
||||
:solo="true"
|
||||
:dense="false"
|
||||
:input-attrs="{
|
||||
solo: true,
|
||||
hideDetails: true,
|
||||
dense: false,
|
||||
}"
|
||||
:show-add="false"
|
||||
:return-object="false"
|
||||
:tag-selector="true"
|
||||
selector-type="tags"
|
||||
/>
|
||||
<RecipeSearchFilterSelector class="mb-1" @update="updateTagParams" />
|
||||
</v-col>
|
||||
|
@ -106,7 +113,7 @@
|
|||
import Fuse from "fuse.js";
|
||||
import { defineComponent, toRefs, computed, reactive } from "@nuxtjs/composition-api";
|
||||
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 { useRecipes, allRecipes } from "~/composables/recipes";
|
||||
import { RecipeSummary } from "~/types/api-types/recipe";
|
||||
|
@ -121,7 +128,7 @@ interface GenericFilter {
|
|||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
RecipeCategoryTagSelector,
|
||||
RecipeOrganizerSelector,
|
||||
RecipeSearchFilterSelector,
|
||||
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…
Reference in a new issue