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> <v-select v-model="inputEntryType" :items="MEAL_TYPE_OPTIONS" label="Meal Type"></v-select>
</div> </div>
<RecipeCategoryTagSelector v-model="inputCategories" /> <RecipeOrganizerSelector v-model="inputCategories" selector-type="categories" />
<RecipeCategoryTagSelector v-model="inputTags" :tag-selector="true" /> <RecipeOrganizerSelector v-model="inputTags" selector-type="tags" />
{{ inputDay === "unset" ? "This rule will apply to all days" : `This rule applies on ${inputDay}s` }} {{ inputDay === "unset" ? "This rule will apply to all days" : `This rule applies on ${inputDay}s` }}
{{ inputEntryType === "unset" ? "for all meal types" : ` and for ${inputEntryType} meal types` }} {{ inputEntryType === "unset" ? "for all meal types" : ` and for ${inputEntryType} meal types` }}
@ -15,7 +15,8 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, computed } from "@nuxtjs/composition-api"; import { defineComponent, computed } from "@nuxtjs/composition-api";
import RecipeCategoryTagSelector from "~/components/Domain/Recipe/RecipeCategoryTagSelector.vue"; import RecipeOrganizerSelector from "~/components/Domain/Recipe/RecipeOrganizerSelector.vue";
import { RecipeTag, RecipeCategory } from "~/types/api-types/group";
const MEAL_TYPE_OPTIONS = [ const MEAL_TYPE_OPTIONS = [
{ text: "Breakfast", value: "breakfast" }, { text: "Breakfast", value: "breakfast" },
@ -38,7 +39,7 @@ const MEAL_DAY_OPTIONS = [
export default defineComponent({ export default defineComponent({
components: { components: {
RecipeCategoryTagSelector, RecipeOrganizerSelector,
}, },
props: { props: {
day: { day: {
@ -50,11 +51,11 @@ export default defineComponent({
default: "unset", default: "unset",
}, },
categories: { categories: {
type: Array, type: Array as () => RecipeCategory[],
default: () => [], default: () => [],
}, },
tags: { tags: {
type: Array, type: Array as () => RecipeTag[],
default: () => [], default: () => [],
}, },
showHelp: { showHelp: {

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> <template>
<v-autocomplete <v-autocomplete
v-model="selected" v-model="selected"
:items="items" :items="storeItem"
:value="value" :value="value"
:label="label" :label="label"
chips chips
deletable-chips deletable-chips
item-text="name" item-text="name"
multiple multiple
:prepend-inner-icon="$globals.icons.tags" :prepend-inner-icon="selectorType === Organizer.Tool ? $globals.icons.potSteam : $globals.icons.tags"
return-object return-object
v-bind="inputAttrs" v-bind="inputAttrs"
> >
@ -17,6 +17,7 @@
:key="data.index" :key="data.index"
class="ma-1" class="ma-1"
:input-value="data.selected" :input-value="data.selected"
small
close close
label label
color="accent" color="accent"
@ -26,41 +27,55 @@
{{ data.item.name || data.item }} {{ data.item.name || data.item }}
</v-chip> </v-chip>
</template> </template>
<template v-if="showAdd" #append-outer>
<v-btn icon @click="dialog = true">
<v-icon>
{{ $globals.icons.create }}
</v-icon>
</v-btn>
<RecipeOrganizerDialog v-model="dialog" :item-type="selectorType" @created-item="appendCreated" />
</template>
</v-autocomplete> </v-autocomplete>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, useContext } from "@nuxtjs/composition-api"; import { defineComponent, ref, useContext } from "@nuxtjs/composition-api";
import { computed, onMounted } from "vue-demi"; import { computed, onMounted } from "vue-demi";
import RecipeOrganizerDialog from "./RecipeOrganizerDialog.vue";
import { RecipeCategory, RecipeTag } from "~/types/api-types/user"; import { RecipeCategory, RecipeTag } from "~/types/api-types/user";
import { RecipeTool } from "~/types/api-types/admin"; import { RecipeTool } from "~/types/api-types/admin";
import { useTagStore } from "~/composables/store/use-tag-store";
type OrganizerType = "tag" | "category" | "tool"; import { useCategoryStore, useToolStore } from "~/composables/store";
import { Organizer, RecipeOrganizer } from "~/types/recipe/organizers";
export default defineComponent({ export default defineComponent({
components: {
RecipeOrganizerDialog,
},
props: { props: {
value: { value: {
type: Array as () => (RecipeTag | RecipeCategory | RecipeTool)[] | undefined, type: Array as () => (RecipeTag | RecipeCategory | RecipeTool | string)[] | undefined,
required: true, required: true,
}, },
/** /**
* The type of organizer to use. * The type of organizer to use.
*/ */
selectorType: { selectorType: {
type: String as () => OrganizerType, type: String as () => RecipeOrganizer,
required: true,
},
/**
* List of items that are available to be chosen from
*/
items: {
type: Array as () => (RecipeTag | RecipeCategory | RecipeTool)[],
required: true, required: true,
}, },
inputAttrs: { inputAttrs: {
type: Object as () => Record<string, any>, type: Object as () => Record<string, any>,
default: () => ({}), default: () => ({}),
}, },
returnObject: {
type: Boolean,
default: true,
},
showAdd: {
type: Boolean,
default: true,
},
}, },
setup(props, context) { setup(props, context) {
@ -81,27 +96,62 @@ export default defineComponent({
const label = computed(() => { const label = computed(() => {
switch (props.selectorType) { switch (props.selectorType) {
case "tag": case Organizer.Tag:
return i18n.t("tag.tags"); return i18n.t("tag.tags");
case "category": case Organizer.Category:
return i18n.t("category.categories"); return i18n.t("category.categories");
case "tool": case Organizer.Tool:
return "Tools"; return i18n.t("tool.tools");
default: default:
return "Organizer"; return "Organizer";
} }
}); });
// ===========================================================================
// Store & Items Setup
const store = (() => {
switch (props.selectorType) {
case Organizer.Tag:
return useTagStore();
case Organizer.Tool:
return useToolStore();
default:
return useCategoryStore();
}
})();
const items = computed(() => {
if (!props.returnObject) {
return store.items.value.map((item) => item.name);
}
return store.items.value;
});
function removeByIndex(index: number) { function removeByIndex(index: number) {
if (selected.value === undefined) { if (selected.value === undefined) {
return; return;
} }
const newSelected = selected.value.filter((_, i) => i !== index); const newSelected = selected.value.filter((_, i) => i !== index);
selected.value = [...newSelected]; selected.value = [...newSelected];
} }
function appendCreated(item: RecipeTag | RecipeCategory | RecipeTool) {
console.log(item);
if (selected.value === undefined) {
return;
}
selected.value = [...selected.value, item];
}
const dialog = ref(false);
return { return {
Organizer,
appendCreated,
dialog,
storeItem: items,
label, label,
selected, selected,
removeByIndex, removeByIndex,

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"; import { BaseCRUDAPI } from "~/api/_base";
type BoundT = { type BoundT = {
id: string | number; id?: string | number;
}; };
interface StoreActions<T extends BoundT> { interface StoreActions<T extends BoundT> {
@ -29,7 +29,12 @@ export function useStoreActions<T extends BoundT>(
loading.value = true; loading.value = true;
const allItems = useAsync(async () => { const allItems = useAsync(async () => {
const { data } = await api.getAll(); const { data } = await api.getAll();
return data;
if (allRef) {
allRef.value = data;
}
return data ?? [];
}, useAsyncKey()); }, useAsyncKey());
loading.value = false; loading.value = false;
@ -73,8 +78,8 @@ export function useStoreActions<T extends BoundT>(
async function deleteOne(id: string | number) { async function deleteOne(id: string | number) {
loading.value = true; loading.value = true;
const { data } = await api.deleteOne(id); const { response } = await api.deleteOne(id);
if (data && allRef?.value) { if (response && allRef?.value) {
refresh(); refresh();
} }
loading.value = false; loading.value = false;

View File

@ -1,7 +1,6 @@
export { useFraction } from "./use-fraction"; export { useFraction } from "./use-fraction";
export { useRecipe } from "./use-recipe"; export { useRecipe } from "./use-recipe";
export { useRecipes, recentRecipes, allRecipes, useLazyRecipes, useSorter } from "./use-recipes"; export { useRecipes, recentRecipes, allRecipes, useLazyRecipes, useSorter } from "./use-recipes";
export { useTags, useCategories, allCategories, allTags } from "./use-tags-categories";
export { parseIngredientText } from "./use-recipe-ingredients"; export { parseIngredientText } from "./use-recipe-ingredients";
export { useRecipeSearch } from "./use-recipe-search"; export { useRecipeSearch } from "./use-recipe-search";
export { useTools } from "./use-recipe-tools"; export { useTools } from "./use-recipe-tools";

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 { useFoodStore, useFoodData } from "./use-food-store";
export { useUnitStore, useUnitData } from "./use-unit-store"; export { useUnitStore, useUnitData } from "./use-unit-store";
export { useLabelStore, useLabelData } from "./use-label-store"; export { useLabelStore, useLabelData } from "./use-label-store";
export { useToolStore, useToolData } from "./use-tool-store";
export { useCategoryStore, useCategoryData } from "./use-category-store";
export { useTagStore, useTagData } from "./use-tag-store";

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-card-text v-if="cookbooks">
<v-text-field v-model="cookbooks[index].name" label="Cookbook Name"></v-text-field> <v-text-field v-model="cookbooks[index].name" label="Cookbook Name"></v-text-field>
<v-textarea v-model="cookbooks[index].description" auto-grow :rows="2" label="Description"></v-textarea> <v-textarea v-model="cookbooks[index].description" auto-grow :rows="2" label="Description"></v-textarea>
<RecipeOrganizerSelector <RecipeOrganizerSelector v-model="cookbooks[index].categories" selector-type="categories" />
v-model="cookbooks[index].categories" <RecipeOrganizerSelector v-model="cookbooks[index].tags" selector-type="tags" />
:items="allCategories || []" <RecipeOrganizerSelector v-model="cookbooks[index].tools" selector-type="tools" />
selector-type="category"
/>
<RecipeOrganizerSelector v-model="cookbooks[index].tags" :items="allTags || []" selector-type="tag" />
<RecipeOrganizerSelector v-model="cookbooks[index].tools" :items="tools || []" selector-type="tool" />
<v-switch v-model="cookbooks[index].public" hide-details single-line> <v-switch v-model="cookbooks[index].public" hide-details single-line>
<template #label> <template #label>
Public Cookbook Public Cookbook
@ -102,26 +97,15 @@ import { defineComponent } from "@nuxtjs/composition-api";
import draggable from "vuedraggable"; import draggable from "vuedraggable";
import { useCookbooks } from "@/composables/use-group-cookbooks"; import { useCookbooks } from "@/composables/use-group-cookbooks";
import RecipeOrganizerSelector from "~/components/Domain/Recipe/RecipeOrganizerSelector.vue"; import RecipeOrganizerSelector from "~/components/Domain/Recipe/RecipeOrganizerSelector.vue";
import { useCategories, useTags, useTools } from "~/composables/recipes";
export default defineComponent({ export default defineComponent({
components: { draggable, RecipeOrganizerSelector }, components: { draggable, RecipeOrganizerSelector },
setup() { setup() {
const { cookbooks, actions } = useCookbooks(); const { cookbooks, actions } = useCookbooks();
const { tools } = useTools();
const { allCategories, useAsyncGetAll: getAllCategories } = useCategories();
const { allTags, useAsyncGetAll: getAllTags } = useTags();
getAllCategories();
getAllTags();
return { return {
allCategories,
allTags,
cookbooks, cookbooks,
actions, actions,
tools,
}; };
}, },
head() { head() {

View File

@ -22,10 +22,10 @@
@submit="dialog.callback" @submit="dialog.callback"
> >
<v-card-text v-if="dialog.mode == MODES.tag"> <v-card-text v-if="dialog.mode == MODES.tag">
<RecipeCategoryTagSelector v-model="toSetTags" :tag-selector="true" /> <RecipeOrganizerSelector v-model="toSetTags" selector-type="tags" />
</v-card-text> </v-card-text>
<v-card-text v-else-if="dialog.mode == MODES.category"> <v-card-text v-else-if="dialog.mode == MODES.category">
<RecipeCategoryTagSelector v-model="toSetCategories" /> <RecipeOrganizerSelector v-model="toSetCategories" selector-type="categories" />
</v-card-text> </v-card-text>
<v-card-text v-else-if="dialog.mode == MODES.delete"> <v-card-text v-else-if="dialog.mode == MODES.delete">
<p class="h4">Are you sure you want to delete the following recipes? This action cannot be undone.</p> <p class="h4">Are you sure you want to delete the following recipes? This action cannot be undone.</p>
@ -149,7 +149,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, reactive, ref, useContext, onMounted } from "@nuxtjs/composition-api"; import { defineComponent, reactive, ref, useContext, onMounted } from "@nuxtjs/composition-api";
import RecipeDataTable from "~/components/Domain/Recipe/RecipeDataTable.vue"; import RecipeDataTable from "~/components/Domain/Recipe/RecipeDataTable.vue";
import RecipeCategoryTagSelector from "~/components/Domain/Recipe/RecipeCategoryTagSelector.vue"; import RecipeOrganizerSelector from "~/components/Domain/Recipe/RecipeOrganizerSelector.vue";
import { useUserApi } from "~/composables/api"; import { useUserApi } from "~/composables/api";
import { useRecipes, allRecipes } from "~/composables/recipes"; import { useRecipes, allRecipes } from "~/composables/recipes";
import { Recipe } from "~/types/api-types/recipe"; import { Recipe } from "~/types/api-types/recipe";
@ -165,7 +165,7 @@ const MODES = {
}; };
export default defineComponent({ export default defineComponent({
components: { RecipeDataTable, RecipeCategoryTagSelector, GroupExportData }, components: { RecipeDataTable, RecipeOrganizerSelector, GroupExportData },
scrollToTop: true, scrollToTop: true,
setup() { setup() {
const { getAllRecipes, refreshRecipes } = useRecipes(true, true); const { getAllRecipes, refreshRecipes } = useRecipes(true, true);

View File

@ -239,7 +239,7 @@
hide-details hide-details
class="pt-0 my-auto py-auto" class="pt-0 my-auto py-auto"
color="secondary" color="secondary"
@change="updateTool(recipe.tools[index])" @change="toolStore.actions.updateOne(recipe.tools[index])"
> >
</v-checkbox> </v-checkbox>
<v-list-item-content> <v-list-item-content>
@ -256,12 +256,12 @@
</v-card-title> </v-card-title>
<v-divider class="mx-2"></v-divider> <v-divider class="mx-2"></v-divider>
<v-card-text> <v-card-text>
<RecipeCategoryTagSelector <RecipeOrganizerSelector
v-if="form" v-if="form"
v-model="recipe.recipeCategory" v-model="recipe.recipeCategory"
:return-object="true" :return-object="true"
:show-add="true" :show-add="true"
:show-label="false" selector-type="categories"
/> />
<RecipeChips v-else :items="recipe.recipeCategory" /> <RecipeChips v-else :items="recipe.recipeCategory" />
</v-card-text> </v-card-text>
@ -274,13 +274,12 @@
</v-card-title> </v-card-title>
<v-divider class="mx-2"></v-divider> <v-divider class="mx-2"></v-divider>
<v-card-text> <v-card-text>
<RecipeCategoryTagSelector <RecipeOrganizerSelector
v-if="form" v-if="form"
v-model="recipe.tags" v-model="recipe.tags"
:return-object="true" :return-object="true"
:show-add="true" :show-add="true"
:tag-selector="true" selector-type="tags"
:show-label="false"
/> />
<RecipeChips v-else :items="recipe.tags" url-prefix="tags" /> <RecipeChips v-else :items="recipe.tags" url-prefix="tags" />
</v-card-text> </v-card-text>
@ -291,7 +290,7 @@
<v-card-title class="py-2"> Required Tools </v-card-title> <v-card-title class="py-2"> Required Tools </v-card-title>
<v-divider class="mx-2"></v-divider> <v-divider class="mx-2"></v-divider>
<v-card-text class="pt-0"> <v-card-text class="pt-0">
<RecipeTools v-model="recipe.tools" :edit="form" /> <RecipeOrganizerSelector v-model="recipe.tools" selector-type="tools" />
</v-card-text> </v-card-text>
</v-card> </v-card>
@ -344,12 +343,12 @@
</v-card-title> </v-card-title>
<v-divider class="mx-2"></v-divider> <v-divider class="mx-2"></v-divider>
<v-card-text> <v-card-text>
<RecipeCategoryTagSelector <RecipeOrganizerSelector
v-if="form" v-if="form"
v-model="recipe.recipeCategory" v-model="recipe.recipeCategory"
:return-object="true" :return-object="true"
:show-add="true" :show-add="true"
:show-label="false" selector-type="categories"
/> />
<RecipeChips v-else :items="recipe.recipeCategory" /> <RecipeChips v-else :items="recipe.recipeCategory" />
</v-card-text> </v-card-text>
@ -362,14 +361,14 @@
</v-card-title> </v-card-title>
<v-divider class="mx-2"></v-divider> <v-divider class="mx-2"></v-divider>
<v-card-text> <v-card-text>
<RecipeCategoryTagSelector <RecipeOrganizerSelector
v-if="form" v-if="form"
v-model="recipe.tags" v-model="recipe.tags"
:return-object="true" :return-object="true"
:show-add="true" :show-add="true"
:tag-selector="true" selector-type="tags"
:show-label="false"
/> />
<RecipeChips v-else :items="recipe.tags" url-prefix="tags" /> <RecipeChips v-else :items="recipe.tags" url-prefix="tags" />
</v-card-text> </v-card-text>
</v-card> </v-card>
@ -484,7 +483,7 @@ import VueMarkdown from "@adapttive/vue-markdown";
import draggable from "vuedraggable"; import draggable from "vuedraggable";
import { invoke, until, useWakeLock } from "@vueuse/core"; import { invoke, until, useWakeLock } from "@vueuse/core";
import { onUnmounted } from "vue-demi"; import { onUnmounted } from "vue-demi";
import RecipeCategoryTagSelector from "@/components/Domain/Recipe/RecipeCategoryTagSelector.vue"; import RecipeOrganizerSelector from "@/components/Domain/Recipe/RecipeOrganizerSelector.vue";
import RecipeDialogBulkAdd from "@/components/Domain/Recipe//RecipeDialogBulkAdd.vue"; import RecipeDialogBulkAdd from "@/components/Domain/Recipe//RecipeDialogBulkAdd.vue";
import { useUserApi, useStaticRoutes } from "~/composables/api"; import { useUserApi, useStaticRoutes } from "~/composables/api";
import { validators } from "~/composables/use-validators"; import { validators } from "~/composables/use-validators";
@ -503,9 +502,10 @@ import RecipeIngredientEditor from "~/components/Domain/Recipe/RecipeIngredientE
import RecipePrintView from "~/components/Domain/Recipe/RecipePrintView.vue"; import RecipePrintView from "~/components/Domain/Recipe/RecipePrintView.vue";
import RecipeTools from "~/components/Domain/Recipe/RecipeTools.vue"; import RecipeTools from "~/components/Domain/Recipe/RecipeTools.vue";
import RecipeComments from "~/components/Domain/Recipe/RecipeComments.vue"; import RecipeComments from "~/components/Domain/Recipe/RecipeComments.vue";
import { Recipe, RecipeTool } from "~/types/api-types/recipe"; import { Recipe } from "~/types/api-types/recipe";
import { uuid4, deepCopy } from "~/composables/use-utils"; import { uuid4, deepCopy } from "~/composables/use-utils";
import { useRouteQuery } from "~/composables/use-router"; import { useRouteQuery } from "~/composables/use-router";
import { useToolStore } from "~/composables/store";
export default defineComponent({ export default defineComponent({
components: { components: {
@ -516,7 +516,7 @@ export default defineComponent({
return import(/* webpackChunkName: "RecipeAssets" */ "~/components/Domain/Recipe/RecipeAssets.vue"); return import(/* webpackChunkName: "RecipeAssets" */ "~/components/Domain/Recipe/RecipeAssets.vue");
} }
}, },
RecipeCategoryTagSelector, RecipeOrganizerSelector,
RecipeChips, RecipeChips,
RecipeComments, RecipeComments,
RecipeDialogBulkAdd, RecipeDialogBulkAdd,
@ -758,18 +758,7 @@ export default defineComponent({
// =============================================================== // ===============================================================
// Recipe Tools // Recipe Tools
async function updateTool(tool: RecipeTool) { const toolStore = useToolStore();
if (tool.id === undefined) return;
const { response } = await api.tools.updateOne(tool.id, tool);
if (response?.status === 200) {
console.log("Update Successful");
}
}
// ===============================================================
// Recipe API Extras
const apiNewKey = ref(""); const apiNewKey = ref("");
@ -864,13 +853,13 @@ export default defineComponent({
deleteRecipe, deleteRecipe,
printRecipe, printRecipe,
closeEditor, closeEditor,
updateTool,
updateRecipe, updateRecipe,
uploadImage, uploadImage,
validators, validators,
recipeImage, recipeImage,
addIngredient, addIngredient,
removeApiExtra, removeApiExtra,
toolStore,
}; };
}, },
head: {}, head: {},

View File

@ -1,6 +1,6 @@
<template> <template>
<div> <div>
<div flat> <div>
<v-card-title class="headline"> Recipe Bulk Importer </v-card-title> <v-card-title class="headline"> Recipe Bulk Importer </v-card-title>
<v-card-text> <v-card-text>
The Bulk recipe importer allows you to import multiple recipes at once by queing the sites on the backend and The Bulk recipe importer allows you to import multiple recipes at once by queing the sites on the backend and
@ -38,8 +38,7 @@
<v-col cols="12" xs="12" sm="6"> <v-col cols="12" xs="12" sm="6">
<RecipeOrganizerSelector <RecipeOrganizerSelector
v-model="bulkUrls[idx].categories" v-model="bulkUrls[idx].categories"
:items="allCategories || []" selector-type="categories"
selector-type="category"
:input-attrs="{ :input-attrs="{
filled: true, filled: true,
singleLine: true, singleLine: true,
@ -54,8 +53,7 @@
<v-col cols="12" xs="12" sm="6"> <v-col cols="12" xs="12" sm="6">
<RecipeOrganizerSelector <RecipeOrganizerSelector
v-model="bulkUrls[idx].tags" v-model="bulkUrls[idx].tags"
:items="allTags || []" selector-type="tags"
selector-type="tag"
:input-attrs="{ :input-attrs="{
filled: true, filled: true,
singleLine: true, singleLine: true,
@ -109,7 +107,6 @@ import { whenever } from "@vueuse/shared";
import { useUserApi } from "~/composables/api"; import { useUserApi } from "~/composables/api";
import { alert } from "~/composables/use-toast"; import { alert } from "~/composables/use-toast";
import RecipeOrganizerSelector from "~/components/Domain/Recipe/RecipeOrganizerSelector.vue"; import RecipeOrganizerSelector from "~/components/Domain/Recipe/RecipeOrganizerSelector.vue";
import { useCategories, useTags } from "~/composables/recipes";
import { ReportSummary } from "~/types/api-types/reports"; import { ReportSummary } from "~/types/api-types/reports";
import RecipeDialogBulkAdd from "~/components/Domain/Recipe/RecipeDialogBulkAdd.vue"; import RecipeDialogBulkAdd from "~/components/Domain/Recipe/RecipeDialogBulkAdd.vue";
@ -152,12 +149,6 @@ export default defineComponent({
fetchReports(); fetchReports();
} }
const { allTags, useAsyncGetAll: getAllTags } = useTags();
const { allCategories, useAsyncGetAll: getAllCategories } = useCategories();
getAllTags();
getAllCategories();
// ========================================================= // =========================================================
// Reports // Reports
@ -189,8 +180,6 @@ export default defineComponent({
assignUrls, assignUrls,
reports, reports,
deleteReport, deleteReport,
allTags,
allCategories,
bulkCreate, bulkCreate,
bulkUrls, bulkUrls,
lockBulkImport, lockBulkImport,

View File

@ -1,44 +1,36 @@
<template> <template>
<v-container> <v-container>
<RecipeCategoryTagToolPage v-if="categories" :items="categories" item-type="categories" @delete="removeCat" /> <RecipeOrganizerPage
v-if="items"
:items="items"
:icon="$globals.icons.tags"
item-type="categories"
@delete="actions.deleteOne"
>
<template #title> {{ $tc("category.categories") }} </template>
</RecipeOrganizerPage>
</v-container> </v-container>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, useAsync } from "@nuxtjs/composition-api"; import { defineComponent } from "@nuxtjs/composition-api";
import RecipeCategoryTagToolPage from "~/components/Domain/Recipe/RecipeCategoryTagToolPage.vue"; import RecipeOrganizerPage from "~/components/Domain/Recipe/RecipeOrganizerPage.vue";
import { useUserApi } from "~/composables/api"; import { useCategoryStore } from "~/composables/store";
import { useAsyncKey } from "~/composables/use-utils";
export default defineComponent({ export default defineComponent({
components: { components: {
RecipeCategoryTagToolPage, RecipeOrganizerPage,
}, },
setup() { setup() {
const userApi = useUserApi(); const { items, actions } = useCategoryStore();
const categories = useAsync(async () => {
const { data } = await userApi.categories.getAll();
if (data) {
return data;
}
}, useAsyncKey());
function removeCat(id: string) {
if (categories.value) {
for (let i = 0; i < categories.value.length; i++) {
if (categories.value[i].id === id) {
categories.value.splice(i, 1);
break;
}
}
}
}
return { return {
categories, items,
removeCat, actions,
}; };
}, },
head: {
title: "Tags",
},
}); });
</script> </script>

View File

@ -1,44 +1,36 @@
<template> <template>
<v-container> <v-container>
<RecipeCategoryTagToolPage v-if="tools" :items="tools" item-type="tags" @delete="removeTag" /> <RecipeOrganizerPage
v-if="items"
:items="items"
:icon="$globals.icons.tags"
item-type="tags"
@delete="actions.deleteOne"
>
<template #title> Tags </template>
</RecipeOrganizerPage>
</v-container> </v-container>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, useAsync } from "@nuxtjs/composition-api"; import { defineComponent } from "@nuxtjs/composition-api";
import RecipeCategoryTagToolPage from "~/components/Domain/Recipe/RecipeCategoryTagToolPage.vue"; import RecipeOrganizerPage from "~/components/Domain/Recipe/RecipeOrganizerPage.vue";
import { useUserApi } from "~/composables/api"; import { useTagStore } from "~/composables/store";
import { useAsyncKey } from "~/composables/use-utils";
export default defineComponent({ export default defineComponent({
components: { components: {
RecipeCategoryTagToolPage, RecipeOrganizerPage,
}, },
setup() { setup() {
const userApi = useUserApi(); const { items, actions } = useTagStore();
const tools = useAsync(async () => {
const { data } = await userApi.tags.getAll();
if (data) {
return data;
}
}, useAsyncKey());
function removeTag(id: string) {
if (tools.value) {
for (let i = 0; i < tools.value.length; i++) {
if (tools.value[i].id === id) {
tools.value.splice(i, 1);
break;
}
}
}
}
return { return {
tools, items,
removeTag, actions,
}; };
}, },
head: {
title: "Tags",
},
}); });
</script> </script>

View File

@ -1,44 +1,38 @@
<template> <template>
<v-container> <v-container>
<RecipeCategoryTagToolPage v-if="tools" :items="tools" item-type="tools" @delete="removeTool" /> <RecipeOrganizerPage
v-if="tools"
:icon="$globals.icons.potSteam"
:items="tools"
item-type="tools"
@delete="actions.deleteOne"
>
<template #title> Tools </template>
</RecipeOrganizerPage>
</v-container> </v-container>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, useAsync } from "@nuxtjs/composition-api"; import { defineComponent, ref } from "@nuxtjs/composition-api";
import RecipeCategoryTagToolPage from "~/components/Domain/Recipe/RecipeCategoryTagToolPage.vue"; import RecipeOrganizerPage from "~/components/Domain/Recipe/RecipeOrganizerPage.vue";
import { useUserApi } from "~/composables/api"; import { useToolStore } from "~/composables/store";
import { useAsyncKey } from "~/composables/use-utils";
export default defineComponent({ export default defineComponent({
components: { components: {
RecipeCategoryTagToolPage, RecipeOrganizerPage,
}, },
setup() { setup() {
const userApi = useUserApi(); const toolStore = useToolStore();
const tools = useAsync(async () => { const dialog = ref(false);
const { data } = await userApi.tools.getAll();
if (data) {
return data;
}
}, useAsyncKey());
function removeTool(id: string) {
if (tools.value) {
for (let i = 0; i < tools.value.length; i++) {
if (tools.value[i].id === id) {
tools.value.splice(i, 1);
break;
}
}
}
}
return { return {
tools, dialog,
removeTool, tools: toolStore.items,
actions: toolStore.actions,
}; };
}, },
head: {
title: "Tools",
},
}); });
</script> </script>

View File

@ -35,23 +35,30 @@
<v-expand-transition> <v-expand-transition>
<v-row v-show="state" dense class="my-0 dense flex-row align-center justify-space-around"> <v-row v-show="state" dense class="my-0 dense flex-row align-center justify-space-around">
<v-col cols="12" class="d-flex flex-wrap flex-md-nowrap justify-center" style="gap: 0.8rem"> <v-col cols="12" class="d-flex flex-wrap flex-md-nowrap justify-center" style="gap: 0.8rem">
<RecipeCategoryTagSelector <RecipeOrganizerSelector
v-model="includeCategories" v-model="includeCategories"
hide-details :input-attrs="{
:solo="true" solo: true,
:dense="false" hideDetails: true,
dense: false,
}"
:show-add="false"
:return-object="false" :return-object="false"
selector-type="categories"
/> />
<RecipeSearchFilterSelector class="mb-1" @update="updateCatParams" /> <RecipeSearchFilterSelector class="mb-1" @update="updateCatParams" />
</v-col> </v-col>
<v-col cols="12" class="d-flex flex-wrap flex-md-nowrap justify-center" style="gap: 0.8rem"> <v-col cols="12" class="d-flex flex-wrap flex-md-nowrap justify-center" style="gap: 0.8rem">
<RecipeCategoryTagSelector <RecipeOrganizerSelector
v-model="includeTags" v-model="includeTags"
hide-details :input-attrs="{
:solo="true" solo: true,
:dense="false" hideDetails: true,
dense: false,
}"
:show-add="false"
:return-object="false" :return-object="false"
:tag-selector="true" selector-type="tags"
/> />
<RecipeSearchFilterSelector class="mb-1" @update="updateTagParams" /> <RecipeSearchFilterSelector class="mb-1" @update="updateTagParams" />
</v-col> </v-col>
@ -106,7 +113,7 @@
import Fuse from "fuse.js"; import Fuse from "fuse.js";
import { defineComponent, toRefs, computed, reactive } from "@nuxtjs/composition-api"; import { defineComponent, toRefs, computed, reactive } from "@nuxtjs/composition-api";
import RecipeSearchFilterSelector from "~/components/Domain/Recipe/RecipeSearchFilterSelector.vue"; import RecipeSearchFilterSelector from "~/components/Domain/Recipe/RecipeSearchFilterSelector.vue";
import RecipeCategoryTagSelector from "~/components/Domain/Recipe/RecipeCategoryTagSelector.vue"; import RecipeOrganizerSelector from "~/components/Domain/Recipe/RecipeOrganizerSelector.vue";
import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue"; import RecipeCardSection from "~/components/Domain/Recipe/RecipeCardSection.vue";
import { useRecipes, allRecipes } from "~/composables/recipes"; import { useRecipes, allRecipes } from "~/composables/recipes";
import { RecipeSummary } from "~/types/api-types/recipe"; import { RecipeSummary } from "~/types/api-types/recipe";
@ -121,7 +128,7 @@ interface GenericFilter {
export default defineComponent({ export default defineComponent({
components: { components: {
RecipeCategoryTagSelector, RecipeOrganizerSelector,
RecipeSearchFilterSelector, RecipeSearchFilterSelector,
RecipeCardSection, RecipeCardSection,
}, },

View File

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