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:
Hayden 2022-06-03 20:12:32 -08:00 committed by GitHub
parent bc175d4ca9
commit 12f480eb75
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 719 additions and 857 deletions

View file

@ -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: {

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View 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>

View 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>

View file

@ -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,

View 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>

View file

@ -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;

View file

@ -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";

View file

@ -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),
};
};

View file

@ -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";

View 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,
};
}

View 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,
};
}

View 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,
};
}

View 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",
},
};
}

View file

@ -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() {

View file

@ -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);

View file

@ -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: {},

View file

@ -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,

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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,
},

View file

@ -0,0 +1,7 @@
export type RecipeOrganizer = "categories" | "tags" | "tools";
export enum Organizer {
Category = "categories",
Tag = "tags",
Tool = "tools",
}