1
0
Fork 0
mirror of https://github.com/mealie-recipes/mealie.git synced 2025-07-25 08:09:41 +02:00

feat: Query Filter Builder for Cookbooks and Meal Plans (#4346)

This commit is contained in:
Michael Genson 2024-10-17 10:35:39 -05:00 committed by GitHub
parent 2a9a6fa5e6
commit b8e62ab8dd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
47 changed files with 2043 additions and 440 deletions

View file

@ -1,11 +1,13 @@
<template>
<div>
<v-card-text v-if="cookbook">
<v-card-text v-if="cookbook" class="px-1">
<v-text-field v-model="cookbook.name" :label="$t('cookbook.cookbook-name')"></v-text-field>
<v-textarea v-model="cookbook.description" auto-grow :rows="2" :label="$t('recipe.description')"></v-textarea>
<RecipeOrganizerSelector v-model="cookbook.categories" selector-type="categories" />
<RecipeOrganizerSelector v-model="cookbook.tags" selector-type="tags" />
<RecipeOrganizerSelector v-model="cookbook.tools" selector-type="tools" />
<QueryFilterBuilder
:field-defs="fieldDefs"
:initial-query-filter="cookbook.queryFilter"
@input="handleInput"
/>
<v-switch v-model="cookbook.public" hide-details single-line>
<template #label>
{{ $t('cookbook.public-cookbook') }}
@ -14,33 +16,19 @@
</HelpIcon>
</template>
</v-switch>
<div class="mt-4">
<h3 class="text-subtitle-1 d-flex align-center mb-0 pb-0">
{{ $t('cookbook.filter-options') }}
<HelpIcon right small class="ml-2">
{{ $t('cookbook.filter-options-description') }}
</HelpIcon>
</h3>
<v-switch v-model="cookbook.requireAllCategories" class="mt-0" hide-details single-line>
<template #label> {{ $t('cookbook.require-all-categories') }} </template>
</v-switch>
<v-switch v-model="cookbook.requireAllTags" hide-details single-line>
<template #label> {{ $t('cookbook.require-all-tags') }} </template>
</v-switch>
<v-switch v-model="cookbook.requireAllTools" hide-details single-line>
<template #label> {{ $t('cookbook.require-all-tools') }} </template>
</v-switch>
</div>
</v-card-text>
</div>
</template>
<script lang="ts">
import { defineComponent } from "@nuxtjs/composition-api";
import { defineComponent, useContext } from "@nuxtjs/composition-api";
import { ReadCookBook } from "~/lib/api/types/cookbook";
import RecipeOrganizerSelector from "~/components/Domain/Recipe/RecipeOrganizerSelector.vue";
import { Organizer } from "~/lib/api/types/non-generated";
import QueryFilterBuilder from "~/components/Domain/QueryFilterBuilder.vue";
import { FieldDefinition } from "~/composables/use-query-filter-builder";
export default defineComponent({
components: { RecipeOrganizerSelector },
components: { QueryFilterBuilder },
props: {
cookbook: {
type: Object as () => ReadCookBook,
@ -51,5 +39,50 @@ export default defineComponent({
required: true,
},
},
setup(props) {
const { i18n } = useContext();
function handleInput(value: string | undefined) {
props.cookbook.queryFilterString = value || "";
}
const fieldDefs: FieldDefinition[] = [
{
name: "recipe_category.id",
label: i18n.tc("category.categories"),
type: Organizer.Category,
},
{
name: "tags.id",
label: i18n.tc("tag.tags"),
type: Organizer.Tag,
},
{
name: "tools.id",
label: i18n.tc("tool.tools"),
type: Organizer.Tool,
},
{
name: "household_id",
label: i18n.tc("household.households"),
type: Organizer.Household,
},
{
name: "created_at",
label: i18n.tc("general.date-created"),
type: "date",
},
{
name: "updated_at",
label: i18n.tc("general.date-updated"),
type: "date",
},
];
return {
handleInput,
fieldDefs,
};
},
});
</script>

View file

@ -4,11 +4,13 @@
<BaseDialog
v-if="editTarget"
v-model="dialogStates.edit"
:width="650"
width="100%"
max-width="1100px"
:icon="$globals.icons.pages"
:title="$t('general.edit')"
:submit-icon="$globals.icons.save"
:submit-text="$tc('general.save')"
:submit-disabled="!editTarget.queryFilterString"
@submit="editCookbook"
>
<v-card-text>

View file

@ -6,12 +6,10 @@
</div>
<div class="mb-5">
<RecipeOrganizerSelector v-model="inputCategories" selector-type="categories" />
<RecipeOrganizerSelector v-model="inputTags" selector-type="tags" />
<GroupHouseholdSelector
v-model="inputHouseholds"
multiselect
:description="$tc('meal-plan.mealplan-households-description')"
<QueryFilterBuilder
:field-defs="fieldDefs"
:initial-query-filter="queryFilter"
@input="handleQueryFilterInput"
/>
</div>
@ -25,14 +23,14 @@
<script lang="ts">
import { defineComponent, computed, useContext } from "@nuxtjs/composition-api";
import GroupHouseholdSelector from "~/components/Domain/Household/GroupHouseholdSelector.vue";
import RecipeOrganizerSelector from "~/components/Domain/Recipe/RecipeOrganizerSelector.vue";
import { PlanCategory, PlanHousehold, PlanTag } from "~/lib/api/types/meal-plan";
import QueryFilterBuilder from "~/components/Domain/QueryFilterBuilder.vue";
import { FieldDefinition } from "~/composables/use-query-filter-builder";
import { Organizer } from "~/lib/api/types/non-generated";
import { QueryFilterJSON } from "~/lib/api/types/response";
export default defineComponent({
components: {
GroupHouseholdSelector,
RecipeOrganizerSelector,
QueryFilterBuilder,
},
props: {
day: {
@ -43,17 +41,13 @@ export default defineComponent({
type: String,
default: "unset",
},
categories: {
type: Array as () => PlanCategory[],
default: () => [],
queryFilterString: {
type: String,
default: "",
},
tags: {
type: Array as () => PlanTag[],
default: () => [],
},
households: {
type: Array as () => PlanHousehold[],
default: () => [],
queryFilter: {
type: Object as () => QueryFilterJSON,
default: null,
},
showHelp: {
type: Boolean,
@ -100,41 +94,65 @@ export default defineComponent({
},
});
const inputCategories = computed({
const inputQueryFilterString = computed({
get: () => {
return props.categories;
return props.queryFilterString;
},
set: (val) => {
context.emit("update:categories", val);
context.emit("update:query-filter-string", val);
},
});
const inputTags = computed({
get: () => {
return props.tags;
},
set: (val) => {
context.emit("update:tags", val);
},
});
function handleQueryFilterInput(value: string | undefined) {
inputQueryFilterString.value = value || "";
};
const inputHouseholds = computed({
get: () => {
return props.households;
const fieldDefs: FieldDefinition[] = [
{
name: "recipe_category.id",
label: i18n.tc("category.categories"),
type: Organizer.Category,
},
set: (val) => {
context.emit("update:households", val);
{
name: "tags.id",
label: i18n.tc("tag.tags"),
type: Organizer.Tag,
},
});
{
name: "tools.id",
label: i18n.tc("tool.tools"),
type: Organizer.Tool,
},
{
name: "household_id",
label: i18n.tc("household.households"),
type: Organizer.Household,
},
{
name: "last_made",
label: i18n.tc("general.last-made"),
type: "date",
},
{
name: "created_at",
label: i18n.tc("general.date-created"),
type: "date",
},
{
name: "updated_at",
label: i18n.tc("general.date-updated"),
type: "date",
},
];
return {
MEAL_TYPE_OPTIONS,
MEAL_DAY_OPTIONS,
inputDay,
inputEntryType,
inputCategories,
inputTags,
inputHouseholds,
inputQueryFilterString,
handleQueryFilterInput,
fieldDefs,
};
},
});

View file

@ -0,0 +1,622 @@
<template>
<v-card class="ma-0" style="overflow-x: auto;">
<v-card-text class="ma-0 pa-0">
<v-container fluid class="ma-0 pa-0">
<draggable
:value="fields"
handle=".handle"
v-bind="{
animation: 200,
group: 'recipe-instructions',
ghostClass: 'ghost',
}"
@start="drag = true"
@end="onDragEnd"
>
<v-row
v-for="(field, index) in fields"
:key="index"
class="d-flex flex-nowrap"
style="max-width: 100%;"
>
<v-col
:cols="attrs.fields.icon.cols"
:class="attrs.col.class"
:style="attrs.fields.icon.style"
>
<v-icon
class="handle"
style="width: 100%; height: 100%;"
>
{{ $globals.icons.arrowUpDown }}
</v-icon>
</v-col>
<v-col
:cols="attrs.fields.logicalOperator.cols"
:class="attrs.col.class"
:style="attrs.fields.logicalOperator.style"
>
<v-select
v-if="index"
v-model="field.logicalOperator"
:items="[logOps.AND, logOps.OR]"
item-text="label"
item-value="value"
@input="setLogicalOperatorValue(field, index, $event)"
>
<template #selection="{ item }">
<span :class="attrs.select.textClass" style="width: 100%;">
{{ item.label }}
</span>
</template>
</v-select>
</v-col>
<v-col
v-if="showAdvanced"
:cols="attrs.fields.leftParens.cols"
:class="attrs.col.class"
:style="attrs.fields.leftParens.style"
>
<v-select
v-model="field.leftParenthesis"
:items="['', '(', '((', '(((']"
@input="setLeftParenthesisValue(field, index, $event)"
>
<template #selection="{ item }">
<span :class="attrs.select.textClass" style="width: 100%;">
{{ item }}
</span>
</template>
</v-select>
</v-col>
<v-col
:cols="attrs.fields.fieldName.cols"
:class="attrs.col.class"
:style="attrs.fields.fieldName.style"
>
<v-select
v-model="field.label"
:items="fieldDefs"
item-text="label"
@change="setField(index, $event)"
>
<template #selection="{ item }">
<span :class="attrs.select.textClass" style="width: 100%;">
{{ item.label }}
</span>
</template>
</v-select>
</v-col>
<v-col
:cols="attrs.fields.relationalOperator.cols"
:class="attrs.col.class"
:style="attrs.fields.relationalOperator.style"
>
<v-select
v-if="field.type !== 'boolean'"
v-model="field.relationalOperatorValue"
:items="field.relationalOperatorOptions"
item-text="label"
item-value="value"
@input="setRelationalOperatorValue(field, index, $event)"
>
<template #selection="{ item }">
<span :class="attrs.select.textClass" style="width: 100%;">
{{ item.label }}
</span>
</template>
</v-select>
</v-col>
<v-col
:cols="attrs.fields.fieldValue.cols"
:class="attrs.col.class"
:style="attrs.fields.fieldValue.style"
>
<v-select
v-if="field.fieldOptions"
v-model="field.values"
:items="field.fieldOptions"
item-text="label"
item-value="value"
multiple
@input="setFieldValues(field, index, $event)"
/>
<v-text-field
v-else-if="field.type === 'string'"
v-model="field.value"
@input="setFieldValue(field, index, $event)"
/>
<v-text-field
v-else-if="field.type === 'number'"
v-model="field.value"
type="number"
@input="setFieldValue(field, index, $event)"
/>
<v-checkbox
v-else-if="field.type === 'boolean'"
v-model="field.value"
@change="setFieldValue(field, index, $event)"
/>
<v-menu
v-else-if="field.type === 'date'"
v-model="datePickers[index]"
:close-on-content-click="false"
transition="scale-transition"
offset-y
max-width="290px"
min-width="auto"
>
<template #activator="{ on, attrs: menuAttrs }">
<v-text-field
v-model="field.value"
persistent-hint
:prepend-icon="$globals.icons.calendar"
v-bind="menuAttrs"
readonly
v-on="on"
/>
</template>
<v-date-picker
v-model="field.value"
no-title
:first-day-of-week="firstDayOfWeek"
:local="$i18n.locale"
@input="setFieldValue(field, index, $event)"
/>
</v-menu>
<RecipeOrganizerSelector
v-else-if="field.type === Organizer.Category"
v-model="field.organizers"
:selector-type="Organizer.Category"
:show-add="false"
:show-label="false"
:show-icon="false"
@input="setOrganizerValues(field, index, $event)"
/>
<RecipeOrganizerSelector
v-else-if="field.type === Organizer.Tag"
v-model="field.organizers"
:selector-type="Organizer.Tag"
:show-add="false"
:show-label="false"
:show-icon="false"
@input="setOrganizerValues(field, index, $event)"
/>
<RecipeOrganizerSelector
v-else-if="field.type === Organizer.Tool"
v-model="field.organizers"
:selector-type="Organizer.Tool"
:show-add="false"
:show-label="false"
:show-icon="false"
@input="setOrganizerValues(field, index, $event)"
/>
<RecipeOrganizerSelector
v-else-if="field.type === Organizer.Food"
v-model="field.organizers"
:selector-type="Organizer.Food"
:show-add="false"
:show-label="false"
:show-icon="false"
@input="setOrganizerValues(field, index, $event)"
/>
<RecipeOrganizerSelector
v-else-if="field.type === Organizer.Household"
v-model="field.organizers"
:selector-type="Organizer.Household"
:show-add="false"
:show-label="false"
:show-icon="false"
@input="setOrganizerValues(field, index, $event)"
/>
</v-col>
<v-col
v-if="showAdvanced"
:cols="attrs.fields.rightParens.cols"
:class="attrs.col.class"
:style="attrs.fields.rightParens.style"
>
<v-select
v-model="field.rightParenthesis"
:items="['', ')', '))', ')))']"
@input="setRightParenthesisValue(field, index, $event)"
>
<template #selection="{ item }">
<span :class="attrs.select.textClass" style="width: 100%;">
{{ item }}
</span>
</template>
</v-select>
</v-col>
<v-col
:cols="attrs.fields.fieldActions.cols"
:class="attrs.col.class"
:style="attrs.fields.fieldActions.style"
>
<BaseButtonGroup
:buttons="[
{
icon: $globals.icons.delete,
text: $tc('general.delete'),
event: 'delete',
disabled: fields.length === 1,
}
]"
class="my-auto"
@delete="removeField(index)"
/>
</v-col>
</v-row>
</draggable>
</v-container>
</v-card-text>
<v-card-actions>
<v-container fluid class="d-flex justify-end pa-0">
<v-checkbox
v-model="showAdvanced"
hide-details
:label="$tc('general.show-advanced')"
class="my-auto mr-4"
/>
<BaseButton create :text="$tc('general.add-field')" @click="addField(fieldDefs[0])" />
</v-container>
</v-card-actions>
</v-card>
</template>
<script lang="ts">
import draggable from "vuedraggable";
import { computed, defineComponent, reactive, ref, toRefs, watch } from "@nuxtjs/composition-api";
import { useHouseholdSelf } from "~/composables/use-households";
import RecipeOrganizerSelector from "~/components/Domain/Recipe/RecipeOrganizerSelector.vue";
import { Organizer } from "~/lib/api/types/non-generated";
import { LogicalOperator, QueryFilterJSON, QueryFilterJSONPart, RelationalKeyword, RelationalOperator } from "~/lib/api/types/response";
import { useCategoryStore, useFoodStore, useHouseholdStore, useTagStore, useToolStore } from "~/composables/store";
import { Field, FieldDefinition, FieldValue, OrganizerBase, useQueryFilterBuilder } from "~/composables/use-query-filter-builder";
export default defineComponent({
components: {
draggable,
RecipeOrganizerSelector,
},
props: {
fieldDefs: {
type: Array as () => FieldDefinition[],
required: true,
},
initialQueryFilter: {
type: Object as () => QueryFilterJSON | null,
default: null,
}
},
setup(props, context) {
const { household } = useHouseholdSelf();
const { logOps, relOps, buildQueryFilterString, getFieldFromFieldDef, isOrganizerType } = useQueryFilterBuilder();
const firstDayOfWeek = computed(() => {
return household.value?.preferences?.firstDayOfWeek || 0;
});
const state = reactive({
showAdvanced: false,
qfValid: false,
datePickers: [] as boolean[],
drag: false,
});
const storeMap = {
[Organizer.Category]: useCategoryStore(),
[Organizer.Tag]: useTagStore(),
[Organizer.Tool]: useToolStore(),
[Organizer.Food]: useFoodStore(),
[Organizer.Household]: useHouseholdStore(),
};
function onDragEnd(event: any) {
state.drag = false;
const oldIndex: number = event.oldIndex;
const newIndex: number = event.newIndex;
state.datePickers[oldIndex] = false;
state.datePickers[newIndex] = false;
const field = fields.value.splice(oldIndex, 1)[0];
fields.value.splice(newIndex, 0, field);
}
const fields = ref<Field[]>([]);
function addField(field: FieldDefinition) {
fields.value.push(getFieldFromFieldDef(field));
state.datePickers.push(false);
};
function setField(index: number, fieldLabel: string) {
state.datePickers[index] = false;
const fieldDef = props.fieldDefs.find((fieldDef) => fieldDef.label === fieldLabel);
if (!fieldDef) {
return;
}
const resetValue = (fieldDef.type !== fields.value[index].type) || (fieldDef.fieldOptions !== fields.value[index].fieldOptions);
const updatedField = {...fields.value[index], ...fieldDef};
// we have to set this explicitly since it might be undefined
updatedField.fieldOptions = fieldDef.fieldOptions;
fields.value.splice(index, 1, getFieldFromFieldDef(updatedField, resetValue));
}
function setLeftParenthesisValue(field: Field, index: number, value: string) {
fields.value.splice(index, 1, {
...field,
leftParenthesis: value,
});
}
function setRightParenthesisValue(field: Field, index: number, value: string) {
fields.value.splice(index, 1, {
...field,
rightParenthesis: value,
});
}
function setLogicalOperatorValue(field: Field, index: number, value: LogicalOperator | undefined) {
if (!value) {
value = logOps.value.AND.value;
}
fields.value.splice(index, 1, {
...field,
logicalOperator: value ? logOps.value[value] : undefined,
});
}
function setRelationalOperatorValue(field: Field, index: number, value: RelationalKeyword | RelationalOperator) {
fields.value.splice(index, 1, {
...field,
relationalOperatorValue: relOps.value[value],
});
}
function setFieldValue(field: Field, index: number, value: FieldValue) {
state.datePickers[index] = false;
fields.value.splice(index, 1, {
...field,
value,
});
}
function setFieldValues(field: Field, index: number, values: FieldValue[]) {
fields.value.splice(index, 1, {
...field,
values,
});
}
function setOrganizerValues(field: Field, index: number, values: OrganizerBase[]) {
setFieldValues(field, index, values.map((value) => value.id.toString()));
}
function removeField(index: number) {
fields.value.splice(index, 1);
state.datePickers.splice(index, 1);
};
watch(
// Toggling showAdvanced changes the builder logic without changing the field values,
// so we need to manually trigger reactivity to re-run the builder.
() => state.showAdvanced,
() => {
if (fields.value?.length) {
fields.value = [...fields.value];
}
},
)
watch(
() => fields.value,
(newFields) => {
newFields.forEach((field, index) => {
const updatedField = getFieldFromFieldDef(field);
fields.value[index] = updatedField;
});
const qf = buildQueryFilterString(fields.value, state.showAdvanced);
if (qf) {
console.debug(`Set query filter: ${qf}`);
}
state.qfValid = !!qf;
context.emit("input", qf || undefined);
},
{
deep: true
},
);
async function hydrateOrganizers(field: Field, index: number) {
if (!field.values?.length || !isOrganizerType(field.type)) {
return;
}
field.organizers = [];
const { store, actions } = storeMap[field.type];
if (!store.value.length) {
await actions.refresh();
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
const organizers = field.values.map((value) => store.value.find((organizer) => organizer.id === value));
field.organizers = organizers.filter((organizer) => organizer !== undefined) as OrganizerBase[];
setOrganizerValues(field, index, field.organizers);
}
function initFieldsError(error = "") {
if (error) {
console.error(error);
}
fields.value = [];
if (props.fieldDefs.length) {
addField(props.fieldDefs[0]);
}
}
function initializeFields() {
if (!props.initialQueryFilter?.parts?.length) {
return initFieldsError();
};
const initFields: Field[] = [];
let error = false;
props.initialQueryFilter.parts.forEach((part: QueryFilterJSONPart, index: number) => {
const fieldDef = props.fieldDefs.find((fieldDef) => fieldDef.name === part.attributeName);
if (!fieldDef) {
error = true;
return initFieldsError(`Invalid query filter; unknown attribute name "${part.attributeName || ""}"`);
}
const field = getFieldFromFieldDef(fieldDef);
field.leftParenthesis = part.leftParenthesis || field.leftParenthesis;
field.rightParenthesis = part.rightParenthesis || field.rightParenthesis;
field.logicalOperator = part.logicalOperator ?
logOps.value[part.logicalOperator] : field.logicalOperator;
field.relationalOperatorValue = part.relationalOperator ?
relOps.value[part.relationalOperator] : field.relationalOperatorValue;
if (field.leftParenthesis || field.rightParenthesis) {
state.showAdvanced = true;
}
if (field.fieldOptions?.length || isOrganizerType(field.type)) {
if (typeof part.value === "string") {
field.values = part.value ? [part.value] : [];
} else {
field.values = part.value || [];
}
if (isOrganizerType(field.type)) {
hydrateOrganizers(field, index);
}
} else if (field.type === "boolean") {
const boolString = part.value || "false";
field.value = (
boolString[0].toLowerCase() === "t" ||
boolString[0].toLowerCase() === "y" ||
boolString[0] === "1"
);
} else if (field.type === "number") {
field.value = Number(part.value as string || "0");
if (isNaN(field.value)) {
error = true;
return initFieldsError(`Invalid query filter; invalid number value "${(part.value || "").toString()}"`);
}
} else if (field.type === "date") {
field.value = part.value as string || "";
const date = new Date(field.value);
if (isNaN(date.getTime())) {
error = true;
return initFieldsError(`Invalid query filter; invalid date value "${(part.value || "").toString()}"`);
}
} else {
field.value = part.value as string || "";
}
initFields.push(field);
});
if (initFields.length && !error) {
fields.value = initFields;
} else {
initFieldsError();
}
};
try {
initializeFields();
} catch (error) {
initFieldsError(`Error initializing fields: ${(error || "").toString()}`);
}
const attrs = computed(() => {
const baseColMaxWidth = 55;
const attrs = {
col: {
class: "d-flex justify-center align-end field-col pa-1",
},
select: {
textClass: "d-flex justify-center text-center",
},
fields: {
icon: {
cols: 1,
style: "width: fit-content;",
},
leftParens: {
cols: state.showAdvanced ? 1 : 0,
style: `min-width: ${state.showAdvanced ? baseColMaxWidth : 0}px;`,
},
logicalOperator: {
cols: 1,
style: `min-width: ${baseColMaxWidth}px;`,
},
fieldName: {
cols: state.showAdvanced ? 2 : 3,
style: `min-width: ${state.showAdvanced ? baseColMaxWidth * 2 : baseColMaxWidth * 3}px;`,
},
relationalOperator: {
cols: 2,
style: `min-width: ${baseColMaxWidth * 2}px;`,
},
fieldValue: {
cols: state.showAdvanced ? 3 : 4,
style: `min-width: ${state.showAdvanced ? baseColMaxWidth * 2 : baseColMaxWidth * 3}px;`,
},
rightParens: {
cols: state.showAdvanced ? 1 : 0,
style: `min-width: ${state.showAdvanced ? baseColMaxWidth : 0}px;`,
},
fieldActions: {
cols: 1,
style: `min-width: ${baseColMaxWidth}px;`,
},
},
}
return attrs;
})
return {
Organizer,
...toRefs(state),
logOps,
relOps,
attrs,
firstDayOfWeek,
onDragEnd,
// Fields
fields,
addField,
setField,
setLeftParenthesisValue,
setRightParenthesisValue,
setLogicalOperatorValue,
setRelationalOperatorValue,
setFieldValue,
setFieldValues,
setOrganizerValues,
removeField,
};
},
});
</script>
<style scoped>
* {
font-size: 1em;
}
</style>

View file

@ -143,7 +143,9 @@ export default defineComponent({
const typeMap = {
"categories": "category.category",
"tags": "tag.tag",
"tools": "tool.tool"
"tools": "tool.tool",
"foods": "shopping-list.food",
"households": "household.household",
};
return typeMap[props.itemType] || "";
});

View file

@ -8,13 +8,12 @@
deletable-chips
item-text="name"
multiple
:prepend-inner-icon="selectorType === Organizer.Tool ? $globals.icons.potSteam :
selectorType === Organizer.Category ? $globals.icons.categories :
$globals.icons.tags"
:prepend-inner-icon="icon"
return-object
v-bind="inputAttrs"
auto-select-first
:search-input.sync="searchInput"
class="pa-0"
@change="resetSearchInput"
>
<template #selection="data">
@ -46,11 +45,11 @@
<script lang="ts">
import { defineComponent, ref, useContext, computed, onMounted } from "@nuxtjs/composition-api";
import RecipeOrganizerDialog from "./RecipeOrganizerDialog.vue";
import { RecipeCategory, RecipeTag } from "~/lib/api/types/recipe";
import { IngredientFood, RecipeCategory, RecipeTag } from "~/lib/api/types/recipe";
import { RecipeTool } from "~/lib/api/types/admin";
import { useTagStore } from "~/composables/store/use-tag-store";
import { useCategoryStore, useToolStore } from "~/composables/store";
import { useCategoryStore, useFoodStore, useHouseholdStore, useTagStore, useToolStore } from "~/composables/store";
import { Organizer, RecipeOrganizer } from "~/lib/api/types/non-generated";
import { HouseholdSummary } from "~/lib/api/types/household";
export default defineComponent({
components: {
@ -58,7 +57,14 @@ export default defineComponent({
},
props: {
value: {
type: Array as () => (RecipeTag | RecipeCategory | RecipeTool | string)[] | undefined,
type: Array as () => (
| HouseholdSummary
| RecipeTag
| RecipeCategory
| RecipeTool
| IngredientFood
| string
)[] | undefined,
required: true,
},
/**
@ -80,6 +86,14 @@ export default defineComponent({
type: Boolean,
default: true,
},
showLabel: {
type: Boolean,
default: true,
},
showIcon: {
type: Boolean,
default: true,
},
},
setup(props, context) {
@ -96,9 +110,13 @@ export default defineComponent({
}
});
const { i18n } = useContext();
const { $globals, i18n } = useContext();
const label = computed(() => {
if (!props.showLabel) {
return "";
}
switch (props.selectorType) {
case Organizer.Tag:
return i18n.t("tag.tags");
@ -106,30 +124,57 @@ export default defineComponent({
return i18n.t("category.categories");
case Organizer.Tool:
return i18n.t("tool.tools");
case Organizer.Food:
return i18n.t("general.foods");
case Organizer.Household:
return i18n.t("household.households");
default:
return i18n.t("general.organizer");
}
});
const icon = computed(() => {
if (!props.showIcon) {
return "";
}
switch (props.selectorType) {
case Organizer.Tag:
return $globals.icons.tags;
case Organizer.Category:
return $globals.icons.categories;
case Organizer.Tool:
return $globals.icons.tools;
case Organizer.Food:
return $globals.icons.foods;
case Organizer.Household:
return $globals.icons.household;
default:
return $globals.icons.tags;
}
});
// ===========================================================================
// Store & Items Setup
const store = (() => {
switch (props.selectorType) {
case Organizer.Tag:
return useTagStore();
case Organizer.Tool:
return useToolStore();
default:
return useCategoryStore();
}
})();
const storeMap = {
[Organizer.Category]: useCategoryStore(),
[Organizer.Tag]: useTagStore(),
[Organizer.Tool]: useToolStore(),
[Organizer.Food]: useFoodStore(),
[Organizer.Household]: useHouseholdStore(),
};
const store = computed(() => {
const { store } = storeMap[props.selectorType];
return store.value;
})
const items = computed(() => {
if (!props.returnObject) {
return store.store.value.map((item) => item.name);
return store.value.map((item) => item.name);
}
return store.store.value;
return store.value;
});
function removeByIndex(index: number) {
@ -140,7 +185,7 @@ export default defineComponent({
selected.value = [...newSelected];
}
function appendCreated(item: RecipeTag | RecipeCategory | RecipeTool) {
function appendCreated(item: any) {
if (selected.value === undefined) {
return;
}
@ -162,6 +207,7 @@ export default defineComponent({
dialog,
storeItem: items,
label,
icon,
selected,
removeByIndex,
searchInput,
@ -170,3 +216,10 @@ export default defineComponent({
},
});
</script>
<style scoped>
.v-autocomplete {
/* This aligns the input with other standard input fields */
margin-top: 6px;
}
</style>