mirror of
https://github.com/hay-kot/mealie.git
synced 2025-08-22 06:23:34 -07:00
Merge branch 'mealie-next' into fix/admin-users-couldnt-delete-other-household-recipes
This commit is contained in:
commit
4f11a6910e
24 changed files with 662 additions and 731 deletions
|
@ -44,78 +44,54 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import type { ReadCookBook } from "~/lib/api/types/cookbook";
|
|
||||||
import { Organizer } from "~/lib/api/types/non-generated";
|
import { Organizer } from "~/lib/api/types/non-generated";
|
||||||
import QueryFilterBuilder from "~/components/Domain/QueryFilterBuilder.vue";
|
import QueryFilterBuilder from "~/components/Domain/QueryFilterBuilder.vue";
|
||||||
import type { FieldDefinition } from "~/composables/use-query-filter-builder";
|
import type { FieldDefinition } from "~/composables/use-query-filter-builder";
|
||||||
|
import type { ReadCookBook } from "~/lib/api/types/cookbook";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
const modelValue = defineModel<ReadCookBook>({ required: true });
|
||||||
components: { QueryFilterBuilder },
|
const i18n = useI18n();
|
||||||
props: {
|
const cookbook = toRef(modelValue);
|
||||||
modelValue: {
|
function handleInput(value: string | undefined) {
|
||||||
type: Object as () => ReadCookBook,
|
cookbook.value.queryFilterString = value || "";
|
||||||
required: true,
|
}
|
||||||
},
|
|
||||||
actions: {
|
const fieldDefs: FieldDefinition[] = [
|
||||||
type: Object as () => any,
|
{
|
||||||
required: true,
|
name: "recipe_category.id",
|
||||||
},
|
label: i18n.t("category.categories"),
|
||||||
|
type: Organizer.Category,
|
||||||
},
|
},
|
||||||
emits: ["update:modelValue"],
|
{
|
||||||
setup(props, { emit }) {
|
name: "tags.id",
|
||||||
const i18n = useI18n();
|
label: i18n.t("tag.tags"),
|
||||||
|
type: Organizer.Tag,
|
||||||
const cookbook = toRef(() => props.modelValue);
|
|
||||||
|
|
||||||
function handleInput(value: string | undefined) {
|
|
||||||
cookbook.value.queryFilterString = value || "";
|
|
||||||
emit("update:modelValue", cookbook.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fieldDefs: FieldDefinition[] = [
|
|
||||||
{
|
|
||||||
name: "recipe_category.id",
|
|
||||||
label: i18n.t("category.categories"),
|
|
||||||
type: Organizer.Category,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "tags.id",
|
|
||||||
label: i18n.t("tag.tags"),
|
|
||||||
type: Organizer.Tag,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "recipe_ingredient.food.id",
|
|
||||||
label: i18n.t("recipe.ingredients"),
|
|
||||||
type: Organizer.Food,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "tools.id",
|
|
||||||
label: i18n.t("tool.tools"),
|
|
||||||
type: Organizer.Tool,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "household_id",
|
|
||||||
label: i18n.t("household.households"),
|
|
||||||
type: Organizer.Household,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "created_at",
|
|
||||||
label: i18n.t("general.date-created"),
|
|
||||||
type: "date",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "updated_at",
|
|
||||||
label: i18n.t("general.date-updated"),
|
|
||||||
type: "date",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return {
|
|
||||||
cookbook,
|
|
||||||
handleInput,
|
|
||||||
fieldDefs,
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
});
|
{
|
||||||
|
name: "recipe_ingredient.food.id",
|
||||||
|
label: i18n.t("recipe.ingredients"),
|
||||||
|
type: Organizer.Food,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tools.id",
|
||||||
|
label: i18n.t("tool.tools"),
|
||||||
|
type: Organizer.Tool,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "household_id",
|
||||||
|
label: i18n.t("household.households"),
|
||||||
|
type: Organizer.Household,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "created_at",
|
||||||
|
label: i18n.t("general.date-created"),
|
||||||
|
type: "date",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "updated_at",
|
||||||
|
label: i18n.t("general.date-updated"),
|
||||||
|
type: "date",
|
||||||
|
},
|
||||||
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -17,7 +17,6 @@
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<CookbookEditor
|
<CookbookEditor
|
||||||
v-model="editTarget"
|
v-model="editTarget"
|
||||||
:actions="actions"
|
|
||||||
/>
|
/>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
|
@ -65,7 +64,7 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { useLazyRecipes } from "~/composables/recipes";
|
import { useLazyRecipes } from "~/composables/recipes";
|
||||||
import RecipeCardSection from "@/components/Domain/Recipe/RecipeCardSection.vue";
|
import RecipeCardSection from "@/components/Domain/Recipe/RecipeCardSection.vue";
|
||||||
import { useCookbookStore } from "~/composables/store/use-cookbook-store";
|
import { useCookbookStore } from "~/composables/store/use-cookbook-store";
|
||||||
|
@ -74,81 +73,58 @@ import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||||
import type { RecipeCookBook } from "~/lib/api/types/cookbook";
|
import type { RecipeCookBook } from "~/lib/api/types/cookbook";
|
||||||
import CookbookEditor from "~/components/Domain/Cookbook/CookbookEditor.vue";
|
import CookbookEditor from "~/components/Domain/Cookbook/CookbookEditor.vue";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
const $auth = useMealieAuth();
|
||||||
components: { RecipeCardSection, CookbookEditor },
|
const { isOwnGroup } = useLoggedInState();
|
||||||
setup() {
|
|
||||||
const $auth = useMealieAuth();
|
|
||||||
const { isOwnGroup } = useLoggedInState();
|
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
|
const groupSlug = computed(() => route.params.groupSlug as string || $auth.user.value?.groupSlug || "");
|
||||||
|
|
||||||
const { recipes, appendRecipes, assignSorted, removeRecipe, replaceRecipes } = useLazyRecipes(isOwnGroup.value ? null : groupSlug.value);
|
const { recipes, appendRecipes, assignSorted, removeRecipe, replaceRecipes } = useLazyRecipes(isOwnGroup.value ? null : groupSlug.value);
|
||||||
const slug = route.params.slug as string;
|
const slug = route.params.slug as string;
|
||||||
const { getOne } = useCookbook(isOwnGroup.value ? null : groupSlug.value);
|
const { getOne } = useCookbook(isOwnGroup.value ? null : groupSlug.value);
|
||||||
const { actions } = useCookbookStore();
|
const { actions } = useCookbookStore();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const tab = ref(null);
|
const book = getOne(slug);
|
||||||
const book = getOne(slug);
|
|
||||||
|
|
||||||
const isOwnHousehold = computed(() => {
|
const isOwnHousehold = computed(() => {
|
||||||
if (!($auth.user.value && book.value?.householdId)) {
|
if (!($auth.user.value && book.value?.householdId)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $auth.user.value.householdId === book.value.householdId;
|
return $auth.user.value.householdId === book.value.householdId;
|
||||||
});
|
});
|
||||||
const canEdit = computed(() => isOwnGroup.value && isOwnHousehold.value);
|
const canEdit = computed(() => isOwnGroup.value && isOwnHousehold.value);
|
||||||
|
|
||||||
const dialogStates = reactive({
|
const dialogStates = reactive({
|
||||||
edit: false,
|
edit: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const editTarget = ref<RecipeCookBook | null>(null);
|
const editTarget = ref<RecipeCookBook | null>(null);
|
||||||
function handleEditCookbook() {
|
function handleEditCookbook() {
|
||||||
dialogStates.edit = true;
|
dialogStates.edit = true;
|
||||||
editTarget.value = book.value;
|
editTarget.value = book.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function editCookbook() {
|
async function editCookbook() {
|
||||||
if (!editTarget.value) {
|
if (!editTarget.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const response = await actions.updateOne(editTarget.value);
|
const response = await actions.updateOne(editTarget.value);
|
||||||
|
|
||||||
if (response?.slug && book.value?.slug !== response?.slug) {
|
if (response?.slug && book.value?.slug !== response?.slug) {
|
||||||
// if name changed, redirect to new slug
|
// if name changed, redirect to new slug
|
||||||
router.push(`/g/${route.params.groupSlug}/cookbooks/${response?.slug}`);
|
router.push(`/g/${route.params.groupSlug}/cookbooks/${response?.slug}`);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// otherwise reload the page, since the recipe criteria changed
|
// otherwise reload the page, since the recipe criteria changed
|
||||||
router.go(0);
|
router.go(0);
|
||||||
}
|
}
|
||||||
dialogStates.edit = false;
|
dialogStates.edit = false;
|
||||||
editTarget.value = null;
|
editTarget.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
useSeoMeta({
|
useSeoMeta({
|
||||||
title: book?.value?.name || "Cookbook",
|
title: book?.value?.name || "Cookbook",
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
book,
|
|
||||||
slug,
|
|
||||||
tab,
|
|
||||||
appendRecipes,
|
|
||||||
assignSorted,
|
|
||||||
recipes,
|
|
||||||
removeRecipe,
|
|
||||||
replaceRecipes,
|
|
||||||
canEdit,
|
|
||||||
dialogStates,
|
|
||||||
editTarget,
|
|
||||||
handleEditCookbook,
|
|
||||||
editCookbook,
|
|
||||||
actions,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -163,14 +163,14 @@
|
||||||
max-width="290px"
|
max-width="290px"
|
||||||
min-width="auto"
|
min-width="auto"
|
||||||
>
|
>
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props: activatorProps }">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="field.value"
|
v-model="field.value"
|
||||||
persistent-hint
|
persistent-hint
|
||||||
:prepend-icon="$globals.icons.calendar"
|
:prepend-icon="$globals.icons.calendar"
|
||||||
variant="underlined"
|
variant="underlined"
|
||||||
color="primary"
|
color="primary"
|
||||||
v-bind="props"
|
v-bind="activatorProps"
|
||||||
readonly
|
readonly
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
@ -184,53 +184,48 @@
|
||||||
</v-menu>
|
</v-menu>
|
||||||
<RecipeOrganizerSelector
|
<RecipeOrganizerSelector
|
||||||
v-else-if="field.type === Organizer.Category"
|
v-else-if="field.type === Organizer.Category"
|
||||||
:model-value="field.organizers"
|
v-model="field.organizers"
|
||||||
:selector-type="Organizer.Category"
|
:selector-type="Organizer.Category"
|
||||||
:show-add="false"
|
:show-add="false"
|
||||||
:show-label="false"
|
:show-label="false"
|
||||||
:show-icon="false"
|
:show-icon="false"
|
||||||
variant="underlined"
|
variant="underlined"
|
||||||
@update:model-value="setOrganizerValues(field, index, $event)"
|
|
||||||
/>
|
/>
|
||||||
<RecipeOrganizerSelector
|
<RecipeOrganizerSelector
|
||||||
v-else-if="field.type === Organizer.Tag"
|
v-else-if="field.type === Organizer.Tag"
|
||||||
:model-value="field.organizers"
|
v-model="field.organizers"
|
||||||
:selector-type="Organizer.Tag"
|
:selector-type="Organizer.Tag"
|
||||||
:show-add="false"
|
:show-add="false"
|
||||||
:show-label="false"
|
:show-label="false"
|
||||||
:show-icon="false"
|
:show-icon="false"
|
||||||
variant="underlined"
|
variant="underlined"
|
||||||
@update:model-value="setOrganizerValues(field, index, $event)"
|
|
||||||
/>
|
/>
|
||||||
<RecipeOrganizerSelector
|
<RecipeOrganizerSelector
|
||||||
v-else-if="field.type === Organizer.Tool"
|
v-else-if="field.type === Organizer.Tool"
|
||||||
:model-value="field.organizers"
|
v-model="field.organizers"
|
||||||
:selector-type="Organizer.Tool"
|
:selector-type="Organizer.Tool"
|
||||||
:show-add="false"
|
:show-add="false"
|
||||||
:show-label="false"
|
:show-label="false"
|
||||||
:show-icon="false"
|
:show-icon="false"
|
||||||
variant="underlined"
|
variant="underlined"
|
||||||
@update:model-value="setOrganizerValues(field, index, $event)"
|
|
||||||
/>
|
/>
|
||||||
<RecipeOrganizerSelector
|
<RecipeOrganizerSelector
|
||||||
v-else-if="field.type === Organizer.Food"
|
v-else-if="field.type === Organizer.Food"
|
||||||
:model-value="field.organizers"
|
v-model="field.organizers"
|
||||||
:selector-type="Organizer.Food"
|
:selector-type="Organizer.Food"
|
||||||
:show-add="false"
|
:show-add="false"
|
||||||
:show-label="false"
|
:show-label="false"
|
||||||
:show-icon="false"
|
:show-icon="false"
|
||||||
variant="underlined"
|
variant="underlined"
|
||||||
@update:model-value="setOrganizerValues(field, index, $event)"
|
|
||||||
/>
|
/>
|
||||||
<RecipeOrganizerSelector
|
<RecipeOrganizerSelector
|
||||||
v-else-if="field.type === Organizer.Household"
|
v-else-if="field.type === Organizer.Household"
|
||||||
:model-value="field.organizers"
|
v-model="field.organizers"
|
||||||
:selector-type="Organizer.Household"
|
:selector-type="Organizer.Household"
|
||||||
:show-add="false"
|
:show-add="false"
|
||||||
:show-label="false"
|
:show-label="false"
|
||||||
:show-icon="false"
|
:show-icon="false"
|
||||||
variant="underlined"
|
variant="underlined"
|
||||||
@update:model-value="setOrganizerValues(field, index, $event)"
|
|
||||||
/>
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
<!-- right parenthesis -->
|
<!-- right parenthesis -->
|
||||||
|
@ -297,7 +292,7 @@
|
||||||
</v-card>
|
</v-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { VueDraggable } from "vue-draggable-plus";
|
import { VueDraggable } from "vue-draggable-plus";
|
||||||
import { useDebounceFn } from "@vueuse/core";
|
import { useDebounceFn } from "@vueuse/core";
|
||||||
import { useHouseholdSelf } from "~/composables/use-households";
|
import { useHouseholdSelf } from "~/composables/use-households";
|
||||||
|
@ -307,365 +302,338 @@ import type { LogicalOperator, QueryFilterJSON, QueryFilterJSONPart, RelationalK
|
||||||
import { useCategoryStore, useFoodStore, useHouseholdStore, useTagStore, useToolStore } from "~/composables/store";
|
import { useCategoryStore, useFoodStore, useHouseholdStore, useTagStore, useToolStore } from "~/composables/store";
|
||||||
import { type Field, type FieldDefinition, type FieldValue, type OrganizerBase, useQueryFilterBuilder } from "~/composables/use-query-filter-builder";
|
import { type Field, type FieldDefinition, type FieldValue, type OrganizerBase, useQueryFilterBuilder } from "~/composables/use-query-filter-builder";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
const props = defineProps({
|
||||||
components: {
|
fieldDefs: {
|
||||||
VueDraggable,
|
type: Array as () => FieldDefinition[],
|
||||||
RecipeOrganizerSelector,
|
required: true,
|
||||||
},
|
},
|
||||||
props: {
|
initialQueryFilter: {
|
||||||
fieldDefs: {
|
type: Object as () => QueryFilterJSON | null,
|
||||||
type: Array as () => FieldDefinition[],
|
default: null,
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
initialQueryFilter: {
|
|
||||||
type: Object as () => QueryFilterJSON | null,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
emits: ["input", "inputJSON"],
|
});
|
||||||
setup(props, context) {
|
|
||||||
const { household } = useHouseholdSelf();
|
|
||||||
const { logOps, relOps, buildQueryFilterString, getFieldFromFieldDef, isOrganizerType } = useQueryFilterBuilder();
|
|
||||||
|
|
||||||
const firstDayOfWeek = computed(() => {
|
const emit = defineEmits<{
|
||||||
return household.value?.preferences?.firstDayOfWeek || 0;
|
(event: "input", value: string | undefined): void;
|
||||||
});
|
(event: "inputJSON", value: QueryFilterJSON | undefined): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
const state = reactive({
|
const { household } = useHouseholdSelf();
|
||||||
showAdvanced: false,
|
const { logOps, relOps, buildQueryFilterString, getFieldFromFieldDef, isOrganizerType } = useQueryFilterBuilder();
|
||||||
qfValid: false,
|
|
||||||
datePickers: [] as boolean[],
|
|
||||||
drag: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const storeMap = {
|
const firstDayOfWeek = computed(() => {
|
||||||
[Organizer.Category]: useCategoryStore(),
|
return household.value?.preferences?.firstDayOfWeek || 0;
|
||||||
[Organizer.Tag]: useTagStore(),
|
});
|
||||||
[Organizer.Tool]: useToolStore(),
|
|
||||||
[Organizer.Food]: useFoodStore(),
|
|
||||||
[Organizer.Household]: useHouseholdStore(),
|
|
||||||
};
|
|
||||||
|
|
||||||
function onDragEnd(event: any) {
|
const state = reactive({
|
||||||
state.drag = false;
|
showAdvanced: false,
|
||||||
|
qfValid: false,
|
||||||
|
datePickers: [] as boolean[],
|
||||||
|
drag: false,
|
||||||
|
});
|
||||||
|
const { showAdvanced, datePickers, drag } = toRefs(state);
|
||||||
|
|
||||||
const oldIndex: number = event.oldIndex;
|
const storeMap = {
|
||||||
const newIndex: number = event.newIndex;
|
[Organizer.Category]: useCategoryStore(),
|
||||||
state.datePickers[oldIndex] = false;
|
[Organizer.Tag]: useTagStore(),
|
||||||
state.datePickers[newIndex] = false;
|
[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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// add id to fields to prevent reactivity issues
|
||||||
|
type FieldWithId = Field & { id: number };
|
||||||
|
const fields = ref<FieldWithId[]>([]);
|
||||||
|
|
||||||
|
const uid = ref(1); // init uid to pass to fields
|
||||||
|
function useUid() {
|
||||||
|
return uid.value++;
|
||||||
|
}
|
||||||
|
function addField(field: FieldDefinition) {
|
||||||
|
fields.value.push({
|
||||||
|
...getFieldFromFieldDef(field),
|
||||||
|
id: useUid(),
|
||||||
|
});
|
||||||
|
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[index] = {
|
||||||
|
...getFieldFromFieldDef(updatedField, resetValue),
|
||||||
|
id: fields.value[index].id, // keep the id
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLeftParenthesisValue(field: FieldWithId, index: number, value: string) {
|
||||||
|
fields.value[index].leftParenthesis = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setRightParenthesisValue(field: FieldWithId, index: number, value: string) {
|
||||||
|
fields.value[index].rightParenthesis = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLogicalOperatorValue(field: FieldWithId, index: number, value: LogicalOperator | undefined) {
|
||||||
|
if (!value) {
|
||||||
|
value = logOps.value.AND.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
fields.value[index].logicalOperator = value ? logOps.value[value] : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setRelationalOperatorValue(field: FieldWithId, index: number, value: RelationalKeyword | RelationalOperator) {
|
||||||
|
fields.value[index].relationalOperatorValue = relOps.value[value];
|
||||||
|
}
|
||||||
|
|
||||||
|
function setFieldValue(field: FieldWithId, index: number, value: FieldValue) {
|
||||||
|
state.datePickers[index] = false;
|
||||||
|
fields.value[index].value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setFieldValues(field: FieldWithId, index: number, values: FieldValue[]) {
|
||||||
|
fields.value[index].values = values;
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeField(index: number) {
|
||||||
|
fields.value.splice(index, 1);
|
||||||
|
state.datePickers.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldsUpdater = useDebounceFn((/* newFields: typeof fields.value */) => {
|
||||||
|
/* newFields.forEach((field, index) => {
|
||||||
|
const updatedField = getFieldFromFieldDef(field);
|
||||||
|
fields.value[index] = updatedField; // recursive!!!
|
||||||
|
}); */
|
||||||
|
|
||||||
|
const qf = buildQueryFilterString(fields.value, state.showAdvanced);
|
||||||
|
if (qf) {
|
||||||
|
console.debug(`Set query filter: ${qf}`);
|
||||||
|
}
|
||||||
|
state.qfValid = !!qf;
|
||||||
|
|
||||||
|
emit("input", qf || undefined);
|
||||||
|
emit("inputJSON", qf ? buildQueryFilterJSON() : undefined);
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
watch(fields, fieldsUpdater, { deep: true });
|
||||||
|
|
||||||
|
async function hydrateOrganizers(field: FieldWithId, _index: number) {
|
||||||
|
if (!field.values?.length || !isOrganizerType(field.type)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { store, actions } = storeMap[field.type];
|
||||||
|
if (!store.value.length) {
|
||||||
|
await actions.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
const organizers = field.values.map((value) => {
|
||||||
|
const organizer = store.value.find(item => item?.id?.toString() === value);
|
||||||
|
if (!organizer) {
|
||||||
|
console.error(`Could not find organizer with id ${value}`);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return organizer;
|
||||||
|
});
|
||||||
|
|
||||||
|
field.organizers = organizers.filter(organizer => organizer !== undefined) as OrganizerBase[];
|
||||||
|
return field;
|
||||||
|
}
|
||||||
|
|
||||||
|
function initFieldsError(error = "") {
|
||||||
|
if (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
fields.value = [];
|
||||||
|
if (props.fieldDefs.length) {
|
||||||
|
addField(props.fieldDefs[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initializeFields() {
|
||||||
|
if (!props.initialQueryFilter?.parts?.length) {
|
||||||
|
return initFieldsError();
|
||||||
|
}
|
||||||
|
|
||||||
|
const initFields: FieldWithId[] = [];
|
||||||
|
let error = false;
|
||||||
|
|
||||||
|
for (const [index, part] of props.initialQueryFilter.parts.entries()) {
|
||||||
|
const fieldDef = props.fieldDefs.find(fieldDef => fieldDef.name === part.attributeName);
|
||||||
|
if (!fieldDef) {
|
||||||
|
error = true;
|
||||||
|
return initFieldsError(`Invalid query filter; unknown attribute name "${part.attributeName || ""}"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// add id to fields to prevent reactivity issues
|
const field: FieldWithId = {
|
||||||
type FieldWithId = Field & { id: number };
|
...getFieldFromFieldDef(fieldDef),
|
||||||
const fields = ref<FieldWithId[]>([]);
|
id: useUid(),
|
||||||
|
};
|
||||||
|
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;
|
||||||
|
|
||||||
const uid = ref(1); // init uid to pass to fields
|
if (field.leftParenthesis || field.rightParenthesis) {
|
||||||
function useUid() {
|
state.showAdvanced = true;
|
||||||
return uid.value++;
|
}
|
||||||
}
|
|
||||||
function addField(field: FieldDefinition) {
|
|
||||||
fields.value.push({
|
|
||||||
...getFieldFromFieldDef(field),
|
|
||||||
id: useUid(),
|
|
||||||
});
|
|
||||||
state.datePickers.push(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
function setField(index: number, fieldLabel: string) {
|
if (field.fieldOptions?.length || isOrganizerType(field.type)) {
|
||||||
state.datePickers[index] = false;
|
if (typeof part.value === "string") {
|
||||||
const fieldDef = props.fieldDefs.find(fieldDef => fieldDef.label === fieldLabel);
|
field.values = part.value ? [part.value] : [];
|
||||||
if (!fieldDef) {
|
}
|
||||||
return;
|
else {
|
||||||
}
|
field.values = part.value || [];
|
||||||
|
}
|
||||||
|
|
||||||
const resetValue = (fieldDef.type !== fields.value[index].type) || (fieldDef.fieldOptions !== fields.value[index].fieldOptions);
|
if (isOrganizerType(field.type)) {
|
||||||
const updatedField = { ...fields.value[index], ...fieldDef };
|
await 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 || "";
|
||||||
|
}
|
||||||
|
|
||||||
// we have to set this explicitly since it might be undefined
|
initFields.push(field);
|
||||||
updatedField.fieldOptions = fieldDef.fieldOptions;
|
}
|
||||||
|
|
||||||
fields.value[index] = {
|
if (initFields.length && !error) {
|
||||||
...getFieldFromFieldDef(updatedField, resetValue),
|
fields.value = initFields;
|
||||||
id: fields.value[index].id, // keep the id
|
}
|
||||||
};
|
else {
|
||||||
}
|
initFieldsError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function setLeftParenthesisValue(field: FieldWithId, index: number, value: string) {
|
onMounted(async () => {
|
||||||
fields.value[index].leftParenthesis = value;
|
try {
|
||||||
}
|
await initializeFields();
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
initFieldsError(`Error initializing fields: ${(error || "").toString()}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function setRightParenthesisValue(field: FieldWithId, index: number, value: string) {
|
function buildQueryFilterJSON(): QueryFilterJSON {
|
||||||
fields.value[index].rightParenthesis = value;
|
const parts = fields.value.map((field) => {
|
||||||
}
|
const part: QueryFilterJSONPart = {
|
||||||
|
attributeName: field.name,
|
||||||
|
leftParenthesis: field.leftParenthesis,
|
||||||
|
rightParenthesis: field.rightParenthesis,
|
||||||
|
logicalOperator: field.logicalOperator?.value,
|
||||||
|
relationalOperator: field.relationalOperatorValue?.value,
|
||||||
|
};
|
||||||
|
|
||||||
function setLogicalOperatorValue(field: FieldWithId, index: number, value: LogicalOperator | undefined) {
|
if (field.fieldOptions?.length || isOrganizerType(field.type)) {
|
||||||
if (!value) {
|
part.value = field.values.map(value => value.toString());
|
||||||
value = logOps.value.AND.value;
|
}
|
||||||
}
|
else if (field.type === "boolean") {
|
||||||
|
part.value = field.value ? "true" : "false";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
part.value = (field.value || "").toString();
|
||||||
|
}
|
||||||
|
|
||||||
fields.value[index].logicalOperator = value ? logOps.value[value] : undefined;
|
return part;
|
||||||
}
|
});
|
||||||
|
|
||||||
function setRelationalOperatorValue(field: FieldWithId, index: number, value: RelationalKeyword | RelationalOperator) {
|
const qfJSON = { parts } as QueryFilterJSON;
|
||||||
fields.value[index].relationalOperatorValue = relOps.value[value];
|
console.debug(`Built query filter JSON: ${JSON.stringify(qfJSON)}`);
|
||||||
}
|
return qfJSON;
|
||||||
|
}
|
||||||
|
|
||||||
function setFieldValue(field: FieldWithId, index: number, value: FieldValue) {
|
const config = computed(() => {
|
||||||
state.datePickers[index] = false;
|
const baseColMaxWidth = 55;
|
||||||
fields.value[index].value = value;
|
return {
|
||||||
}
|
col: {
|
||||||
|
class: "d-flex justify-center align-end field-col pa-1",
|
||||||
function setFieldValues(field: FieldWithId, index: number, values: FieldValue[]) {
|
},
|
||||||
fields.value[index].values = values;
|
select: {
|
||||||
}
|
textClass: "d-flex justify-center text-center",
|
||||||
|
},
|
||||||
function setOrganizerValues(field: FieldWithId, index: number, values: OrganizerBase[]) {
|
items: {
|
||||||
setFieldValues(field, index, values.map(value => value.id.toString()));
|
icon: {
|
||||||
fields.value[index].organizers = values;
|
cols: 1,
|
||||||
}
|
style: "width: fit-content;",
|
||||||
|
},
|
||||||
function removeField(index: number) {
|
leftParens: {
|
||||||
fields.value.splice(index, 1);
|
cols: state.showAdvanced ? 1 : 0,
|
||||||
state.datePickers.splice(index, 1);
|
style: `min-width: ${state.showAdvanced ? baseColMaxWidth : 0}px;`,
|
||||||
};
|
},
|
||||||
|
logicalOperator: {
|
||||||
const fieldsUpdater = useDebounceFn((/* newFields: typeof fields.value */) => {
|
cols: 1,
|
||||||
/* newFields.forEach((field, index) => {
|
style: `min-width: ${baseColMaxWidth}px;`,
|
||||||
const updatedField = getFieldFromFieldDef(field);
|
},
|
||||||
fields.value[index] = updatedField; // recursive!!!
|
fieldName: {
|
||||||
}); */
|
cols: state.showAdvanced ? 2 : 3,
|
||||||
|
style: `min-width: ${state.showAdvanced ? baseColMaxWidth * 2 : baseColMaxWidth * 3}px;`,
|
||||||
const qf = buildQueryFilterString(fields.value, state.showAdvanced);
|
},
|
||||||
if (qf) {
|
relationalOperator: {
|
||||||
console.debug(`Set query filter: ${qf}`);
|
cols: 2,
|
||||||
}
|
style: `min-width: ${baseColMaxWidth * 2}px;`,
|
||||||
state.qfValid = !!qf;
|
},
|
||||||
|
fieldValue: {
|
||||||
context.emit("input", qf || undefined);
|
cols: state.showAdvanced ? 3 : 4,
|
||||||
context.emit("inputJSON", qf ? buildQueryFilterJSON() : undefined);
|
style: `min-width: ${state.showAdvanced ? baseColMaxWidth * 2 : baseColMaxWidth * 3}px;`,
|
||||||
}, 500);
|
},
|
||||||
|
rightParens: {
|
||||||
watch(fields, fieldsUpdater, { deep: true });
|
cols: state.showAdvanced ? 1 : 0,
|
||||||
|
style: `min-width: ${state.showAdvanced ? baseColMaxWidth : 0}px;`,
|
||||||
async function hydrateOrganizers(field: FieldWithId, index: number) {
|
},
|
||||||
if (!field.values?.length || !isOrganizerType(field.type)) {
|
fieldActions: {
|
||||||
return;
|
cols: 1,
|
||||||
}
|
style: `min-width: ${baseColMaxWidth}px;`,
|
||||||
|
},
|
||||||
field.organizers = [];
|
},
|
||||||
|
};
|
||||||
const { store, actions } = storeMap[field.type];
|
|
||||||
if (!store.value.length) {
|
|
||||||
await actions.refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
const organizers = field.values.map((value) => {
|
|
||||||
const organizer = store.value.find(item => item?.id?.toString() === value);
|
|
||||||
if (!organizer) {
|
|
||||||
console.error(`Could not find organizer with id ${value}`);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return organizer;
|
|
||||||
});
|
|
||||||
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: FieldWithId[] = [];
|
|
||||||
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: FieldWithId = {
|
|
||||||
...getFieldFromFieldDef(fieldDef),
|
|
||||||
id: useUid(),
|
|
||||||
};
|
|
||||||
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()}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildQueryFilterJSON(): QueryFilterJSON {
|
|
||||||
const parts = fields.value.map((field) => {
|
|
||||||
const part: QueryFilterJSONPart = {
|
|
||||||
attributeName: field.name,
|
|
||||||
leftParenthesis: field.leftParenthesis,
|
|
||||||
rightParenthesis: field.rightParenthesis,
|
|
||||||
logicalOperator: field.logicalOperator?.value,
|
|
||||||
relationalOperator: field.relationalOperatorValue?.value,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (field.fieldOptions?.length || isOrganizerType(field.type)) {
|
|
||||||
part.value = field.values.map(value => value.toString());
|
|
||||||
}
|
|
||||||
else if (field.type === "boolean") {
|
|
||||||
part.value = field.value ? "true" : "false";
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
part.value = (field.value || "").toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
return part;
|
|
||||||
});
|
|
||||||
|
|
||||||
const qfJSON = { parts } as QueryFilterJSON;
|
|
||||||
console.debug(`Built query filter JSON: ${JSON.stringify(qfJSON)}`);
|
|
||||||
return qfJSON;
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = computed(() => {
|
|
||||||
const baseColMaxWidth = 55;
|
|
||||||
return {
|
|
||||||
col: {
|
|
||||||
class: "d-flex justify-center align-end field-col pa-1",
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
textClass: "d-flex justify-center text-center",
|
|
||||||
},
|
|
||||||
items: {
|
|
||||||
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 {
|
|
||||||
Organizer,
|
|
||||||
...toRefs(state),
|
|
||||||
logOps,
|
|
||||||
relOps,
|
|
||||||
config,
|
|
||||||
firstDayOfWeek,
|
|
||||||
onDragEnd,
|
|
||||||
// Fields
|
|
||||||
fields,
|
|
||||||
addField,
|
|
||||||
setField,
|
|
||||||
setLeftParenthesisValue,
|
|
||||||
setRightParenthesisValue,
|
|
||||||
setLogicalOperatorValue,
|
|
||||||
setRelationalOperatorValue,
|
|
||||||
setFieldValue,
|
|
||||||
setFieldValues,
|
|
||||||
setOrganizerValues,
|
|
||||||
removeField,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
<RecipeFavoriteBadge v-if="loggedIn" color="info" button-style :recipe-id="recipe.id!" show-always />
|
<RecipeFavoriteBadge v-if="loggedIn" color="info" button-style :recipe-id="recipe.id!" show-always />
|
||||||
<RecipeTimelineBadge v-if="loggedIn" class="ml-1" color="info" button-style :slug="recipe.slug" :recipe-name="recipe.name!" />
|
<RecipeTimelineBadge v-if="loggedIn" class="ml-1" color="info" button-style :slug="recipe.slug" :recipe-name="recipe.name!" />
|
||||||
<div v-if="loggedIn">
|
<div v-if="loggedIn">
|
||||||
<v-tooltip v-if="canEdit" bottom color="info">
|
<v-tooltip v-if="canEdit" location="bottom" color="info">
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props }">
|
||||||
<v-btn
|
<v-btn
|
||||||
icon
|
icon
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
>
|
>
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
<div class="ma-auto">
|
<div class="ma-auto">
|
||||||
<v-tooltip bottom>
|
<v-tooltip location="bottom">
|
||||||
<template #activator="{ props: tooltipProps }">
|
<template #activator="{ props: tooltipProps }">
|
||||||
<v-icon v-bind="tooltipProps">
|
<v-icon v-bind="tooltipProps">
|
||||||
{{ getIconDefinition(item.icon).icon }}
|
{{ getIconDefinition(item.icon).icon }}
|
||||||
|
|
|
@ -63,6 +63,22 @@
|
||||||
clearable
|
clearable
|
||||||
@keyup.enter="handleUnitEnter"
|
@keyup.enter="handleUnitEnter"
|
||||||
>
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<v-tooltip v-if="unitError" location="bottom">
|
||||||
|
<template #activator="{ props: unitTooltipProps }">
|
||||||
|
<v-icon
|
||||||
|
v-bind="unitTooltipProps"
|
||||||
|
class="ml-2 mr-n3 opacity-100"
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
{{ $globals.icons.alert }}
|
||||||
|
</v-icon>
|
||||||
|
</template>
|
||||||
|
<span v-if="unitErrorTooltip">
|
||||||
|
{{ unitErrorTooltip }}
|
||||||
|
</span>
|
||||||
|
</v-tooltip>
|
||||||
|
</template>
|
||||||
<template #no-data>
|
<template #no-data>
|
||||||
<div class="caption text-center pb-2">
|
<div class="caption text-center pb-2">
|
||||||
{{ $t("recipe.press-enter-to-create") }}
|
{{ $t("recipe.press-enter-to-create") }}
|
||||||
|
@ -104,6 +120,22 @@
|
||||||
clearable
|
clearable
|
||||||
@keyup.enter="handleFoodEnter"
|
@keyup.enter="handleFoodEnter"
|
||||||
>
|
>
|
||||||
|
<template #prepend>
|
||||||
|
<v-tooltip v-if="foodError" location="bottom">
|
||||||
|
<template #activator="{ props: foodTooltipProps }">
|
||||||
|
<v-icon
|
||||||
|
v-bind="foodTooltipProps"
|
||||||
|
class="ml-2 mr-n3 opacity-100"
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
{{ $globals.icons.alert }}
|
||||||
|
</v-icon>
|
||||||
|
</template>
|
||||||
|
<span v-if="foodErrorTooltip">
|
||||||
|
{{ foodErrorTooltip }}
|
||||||
|
</span>
|
||||||
|
</v-tooltip>
|
||||||
|
</template>
|
||||||
<template #no-data>
|
<template #no-data>
|
||||||
<div class="caption text-center pb-2">
|
<div class="caption text-center pb-2">
|
||||||
{{ $t("recipe.press-enter-to-create") }}
|
{{ $t("recipe.press-enter-to-create") }}
|
||||||
|
@ -153,7 +185,6 @@
|
||||||
@toggle-original="toggleOriginalText"
|
@toggle-original="toggleOriginalText"
|
||||||
@insert-above="$emit('insert-above')"
|
@insert-above="$emit('insert-above')"
|
||||||
@insert-below="$emit('insert-below')"
|
@insert-below="$emit('insert-below')"
|
||||||
@insert-ingredient="$emit('insert-ingredient')"
|
|
||||||
@delete="$emit('delete')"
|
@delete="$emit('delete')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -184,22 +215,33 @@ import type { RecipeIngredient } from "~/lib/api/types/recipe";
|
||||||
// defineModel replaces modelValue prop
|
// defineModel replaces modelValue prop
|
||||||
const model = defineModel<RecipeIngredient>({ required: true });
|
const model = defineModel<RecipeIngredient>({ required: true });
|
||||||
|
|
||||||
const props = defineProps({
|
defineProps({
|
||||||
disableAmount: {
|
disableAmount: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
allowInsertIngredient: {
|
unitError: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
unitErrorTooltip: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
foodError: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
foodErrorTooltip: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
defineEmits([
|
defineEmits([
|
||||||
"clickIngredientField",
|
"clickIngredientField",
|
||||||
"insert-above",
|
"insert-above",
|
||||||
"insert-below",
|
"insert-below",
|
||||||
"insert-ingredient",
|
|
||||||
"delete",
|
"delete",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -228,13 +270,6 @@ const contextMenuOptions = computed(() => {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (props.allowInsertIngredient) {
|
|
||||||
options.push({
|
|
||||||
text: i18n.t("recipe.insert-ingredient"),
|
|
||||||
event: "insert-ingredient",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (model.value.originalText) {
|
if (model.value.originalText) {
|
||||||
options.push({
|
options.push({
|
||||||
text: i18n.t("recipe.see-original-text"),
|
text: i18n.t("recipe.see-original-text"),
|
||||||
|
|
|
@ -86,7 +86,7 @@
|
||||||
<div>
|
<div>
|
||||||
<div v-if="lastMadeReady" class="d-flex justify-center flex-wrap">
|
<div v-if="lastMadeReady" class="d-flex justify-center flex-wrap">
|
||||||
<v-row no-gutters class="d-flex flex-wrap align-center" style="font-size: larger">
|
<v-row no-gutters class="d-flex flex-wrap align-center" style="font-size: larger">
|
||||||
<v-tooltip bottom>
|
<v-tooltip location="bottom">
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props }">
|
||||||
<v-btn
|
<v-btn
|
||||||
rounded
|
rounded
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
v-model="selected"
|
v-model="selected"
|
||||||
v-bind="inputAttrs"
|
v-bind="inputAttrs"
|
||||||
v-model:search="searchInput"
|
v-model:search="searchInput"
|
||||||
:items="storeItem"
|
:items="items"
|
||||||
:label="label"
|
:label="label"
|
||||||
chips
|
chips
|
||||||
closable-chips
|
closable-chips
|
||||||
|
@ -46,180 +46,138 @@
|
||||||
</v-autocomplete>
|
</v-autocomplete>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import type { IngredientFood, RecipeCategory, RecipeTag } from "~/lib/api/types/recipe";
|
import type { IngredientFood, RecipeCategory, RecipeTag } from "~/lib/api/types/recipe";
|
||||||
import type { RecipeTool } from "~/lib/api/types/admin";
|
import type { RecipeTool } from "~/lib/api/types/admin";
|
||||||
import { Organizer, type RecipeOrganizer } from "~/lib/api/types/non-generated";
|
import { Organizer, type RecipeOrganizer } from "~/lib/api/types/non-generated";
|
||||||
import type { HouseholdSummary } from "~/lib/api/types/household";
|
import type { HouseholdSummary } from "~/lib/api/types/household";
|
||||||
import { useCategoryStore, useFoodStore, useHouseholdStore, useTagStore, useToolStore } from "~/composables/store";
|
import { useCategoryStore, useFoodStore, useHouseholdStore, useTagStore, useToolStore } from "~/composables/store";
|
||||||
|
|
||||||
export default defineNuxtComponent({
|
interface Props {
|
||||||
props: {
|
selectorType: RecipeOrganizer;
|
||||||
modelValue: {
|
inputAttrs?: Record<string, any>;
|
||||||
type: Array as () => (
|
returnObject?: boolean;
|
||||||
| HouseholdSummary
|
showAdd?: boolean;
|
||||||
| RecipeTag
|
showLabel?: boolean;
|
||||||
| RecipeCategory
|
showIcon?: boolean;
|
||||||
| RecipeTool
|
variant?: "filled" | "underlined" | "outlined" | "plain" | "solo" | "solo-inverted" | "solo-filled";
|
||||||
| IngredientFood
|
}
|
||||||
| string
|
|
||||||
)[] | undefined,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* The type of organizer to use.
|
|
||||||
*/
|
|
||||||
selectorType: {
|
|
||||||
type: String as () => RecipeOrganizer,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
inputAttrs: {
|
|
||||||
type: Object as () => Record<string, any>,
|
|
||||||
default: () => ({}),
|
|
||||||
},
|
|
||||||
returnObject: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
showAdd: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
showLabel: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
showIcon: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
variant: {
|
|
||||||
type: String as () => "filled" | "underlined" | "outlined" | "plain" | "solo" | "solo-inverted" | "solo-filled",
|
|
||||||
default: "outlined",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emits: ["update:modelValue"],
|
|
||||||
|
|
||||||
setup(props, context) {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
const selected = computed({
|
inputAttrs: () => ({}),
|
||||||
get: () => props.modelValue,
|
returnObject: true,
|
||||||
set: (val) => {
|
showAdd: true,
|
||||||
context.emit("update:modelValue", val);
|
showLabel: true,
|
||||||
},
|
showIcon: true,
|
||||||
});
|
variant: "outlined",
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
if (selected.value === undefined) {
|
|
||||||
selected.value = [];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const i18n = useI18n();
|
|
||||||
const { $globals } = useNuxtApp();
|
|
||||||
|
|
||||||
const label = computed(() => {
|
|
||||||
if (!props.showLabel) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (props.selectorType) {
|
|
||||||
case Organizer.Tag:
|
|
||||||
return i18n.t("tag.tags");
|
|
||||||
case Organizer.Category:
|
|
||||||
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 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.value.map(item => item.name);
|
|
||||||
}
|
|
||||||
return store.value;
|
|
||||||
});
|
|
||||||
|
|
||||||
function removeByIndex(index: number) {
|
|
||||||
if (selected.value === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const newSelected = selected.value.filter((_, i) => i !== index);
|
|
||||||
selected.value = [...newSelected];
|
|
||||||
}
|
|
||||||
|
|
||||||
function appendCreated(item: any) {
|
|
||||||
if (selected.value === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
selected.value = [...selected.value, item];
|
|
||||||
}
|
|
||||||
|
|
||||||
const dialog = ref(false);
|
|
||||||
|
|
||||||
const searchInput = ref("");
|
|
||||||
|
|
||||||
function resetSearchInput() {
|
|
||||||
searchInput.value = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
Organizer,
|
|
||||||
appendCreated,
|
|
||||||
dialog,
|
|
||||||
storeItem: items,
|
|
||||||
label,
|
|
||||||
icon,
|
|
||||||
selected,
|
|
||||||
removeByIndex,
|
|
||||||
searchInput,
|
|
||||||
resetSearchInput,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const selected = defineModel<(
|
||||||
|
| HouseholdSummary
|
||||||
|
| RecipeTag
|
||||||
|
| RecipeCategory
|
||||||
|
| RecipeTool
|
||||||
|
| IngredientFood
|
||||||
|
| string
|
||||||
|
)[] | undefined>({ required: true });
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (selected.value === undefined) {
|
||||||
|
selected.value = [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const i18n = useI18n();
|
||||||
|
const { $globals } = useNuxtApp();
|
||||||
|
|
||||||
|
const label = computed(() => {
|
||||||
|
if (!props.showLabel) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (props.selectorType) {
|
||||||
|
case Organizer.Tag:
|
||||||
|
return i18n.t("tag.tags");
|
||||||
|
case Organizer.Category:
|
||||||
|
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 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.value.map(item => item.name);
|
||||||
|
}
|
||||||
|
return store.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
function removeByIndex(index: number) {
|
||||||
|
if (selected.value === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newSelected = selected.value.filter((_, i) => i !== index);
|
||||||
|
selected.value = [...newSelected];
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendCreated(item: any) {
|
||||||
|
if (selected.value === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
selected.value = [...selected.value, item];
|
||||||
|
}
|
||||||
|
|
||||||
|
const dialog = ref(false);
|
||||||
|
|
||||||
|
const searchInput = ref("");
|
||||||
|
|
||||||
|
function resetSearchInput() {
|
||||||
|
searchInput.value = "";
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
@ -35,7 +35,7 @@
|
||||||
>
|
>
|
||||||
<RecipeYield
|
<RecipeYield
|
||||||
:yield-quantity="recipe.recipeYieldQuantity"
|
:yield-quantity="recipe.recipeYieldQuantity"
|
||||||
:yield="recipe.recipeYield"
|
:yield-text="recipe.recipeYield"
|
||||||
:scale="recipeScale"
|
:scale="recipeScale"
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -42,7 +42,7 @@
|
||||||
/>
|
/>
|
||||||
<div class="d-flex flex-wrap justify-center justify-sm-end mt-3">
|
<div class="d-flex flex-wrap justify-center justify-sm-end mt-3">
|
||||||
<v-tooltip
|
<v-tooltip
|
||||||
top
|
location="top"
|
||||||
color="accent"
|
color="accent"
|
||||||
>
|
>
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props }">
|
||||||
|
|
|
@ -222,7 +222,7 @@ export default defineNuxtComponent({
|
||||||
|
|
||||||
const servingsDisplay = computed(() => {
|
const servingsDisplay = computed(() => {
|
||||||
const { scaledAmountDisplay } = useScaledAmount(props.recipe.recipeYieldQuantity, props.scale);
|
const { scaledAmountDisplay } = useScaledAmount(props.recipe.recipeYieldQuantity, props.scale);
|
||||||
return scaledAmountDisplay
|
return scaledAmountDisplay || props.recipe.recipeYield
|
||||||
? i18n.t("recipe.yields-amount-with-text", {
|
? i18n.t("recipe.yields-amount-with-text", {
|
||||||
amount: scaledAmountDisplay,
|
amount: scaledAmountDisplay,
|
||||||
text: props.recipe.recipeYield,
|
text: props.recipe.recipeYield,
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
<v-tooltip
|
<v-tooltip
|
||||||
v-if="canEditScale"
|
v-if="canEditScale"
|
||||||
size="small"
|
size="small"
|
||||||
top
|
location="top"
|
||||||
color="secondary-darken-1"
|
color="secondary-darken-1"
|
||||||
>
|
>
|
||||||
<template #activator="{ props: tooltipProps }">
|
<template #activator="{ props: tooltipProps }">
|
||||||
|
@ -74,7 +74,7 @@
|
||||||
@update:model-value="recalculateScale(parseFloat($event) || 0)"
|
@update:model-value="recalculateScale(parseFloat($event) || 0)"
|
||||||
/>
|
/>
|
||||||
<v-tooltip
|
<v-tooltip
|
||||||
end
|
location="end"
|
||||||
color="secondary-darken-1"
|
color="secondary-darken-1"
|
||||||
>
|
>
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props }">
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<v-tooltip
|
<v-tooltip
|
||||||
bottom
|
location="bottom"
|
||||||
nudge-right="50"
|
nudge-right="50"
|
||||||
:color="buttonStyle ? 'info' : 'secondary'"
|
:color="buttonStyle ? 'info' : 'secondary'"
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="scaledAmount"
|
v-if="yieldDisplay"
|
||||||
class="d-flex align-center"
|
class="d-flex align-center"
|
||||||
>
|
>
|
||||||
<v-row
|
<v-row
|
||||||
|
@ -18,7 +18,7 @@
|
||||||
<p class="my-0 opacity-80">
|
<p class="my-0 opacity-80">
|
||||||
<span class="font-weight-bold">{{ $t("recipe.yield") }}</span><br>
|
<span class="font-weight-bold">{{ $t("recipe.yield") }}</span><br>
|
||||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||||
<span v-html="scaledAmount" /> {{ text }}
|
<span v-html="yieldDisplay" />
|
||||||
</p>
|
</p>
|
||||||
</v-row>
|
</v-row>
|
||||||
</div>
|
</div>
|
||||||
|
@ -34,7 +34,7 @@ export default defineNuxtComponent({
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 0,
|
default: 0,
|
||||||
},
|
},
|
||||||
yield: {
|
yieldText: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "",
|
default: "",
|
||||||
},
|
},
|
||||||
|
@ -55,15 +55,24 @@ export default defineNuxtComponent({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const scaledAmount = computed(() => {
|
const yieldDisplay = computed<string>(() => {
|
||||||
|
const components: string[] = [];
|
||||||
|
|
||||||
const { scaledAmountDisplay } = useScaledAmount(props.yieldQuantity, props.scale);
|
const { scaledAmountDisplay } = useScaledAmount(props.yieldQuantity, props.scale);
|
||||||
return scaledAmountDisplay;
|
if (scaledAmountDisplay) {
|
||||||
|
components.push(scaledAmountDisplay);
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = props.yieldText;
|
||||||
|
if (text) {
|
||||||
|
components.push(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sanitizeHTML(components.join(" "));
|
||||||
});
|
});
|
||||||
const text = sanitizeHTML(props.yield);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
scaledAmount,
|
yieldDisplay,
|
||||||
text,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -59,7 +59,7 @@
|
||||||
open-delay="200"
|
open-delay="200"
|
||||||
transition="slide-x-reverse-transition"
|
transition="slide-x-reverse-transition"
|
||||||
density="compact"
|
density="compact"
|
||||||
right
|
location="end"
|
||||||
content-class="text-caption"
|
content-class="text-caption"
|
||||||
>
|
>
|
||||||
<template #activator="{ props: tooltipProps }">
|
<template #activator="{ props: tooltipProps }">
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<v-tooltip
|
<v-tooltip
|
||||||
v-if="userId"
|
v-if="userId"
|
||||||
:disabled="!user || !tooltip"
|
:disabled="!user || !tooltip"
|
||||||
right
|
location="end"
|
||||||
>
|
>
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props }">
|
||||||
<v-avatar
|
<v-avatar
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<v-tooltip
|
<v-tooltip
|
||||||
ref="copyToolTip"
|
ref="copyToolTip"
|
||||||
v-model="show"
|
v-model="show"
|
||||||
top
|
location="top"
|
||||||
:open-on-hover="false"
|
:open-on-hover="false"
|
||||||
:open-on-click="true"
|
:open-on-click="true"
|
||||||
close-delay="500"
|
close-delay="500"
|
||||||
|
|
|
@ -48,7 +48,7 @@
|
||||||
open-delay="200"
|
open-delay="200"
|
||||||
transition="slide-y-reverse-transition"
|
transition="slide-y-reverse-transition"
|
||||||
density="compact"
|
density="compact"
|
||||||
bottom
|
location="bottom"
|
||||||
content-class="text-caption"
|
content-class="text-caption"
|
||||||
>
|
>
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props }">
|
||||||
|
|
|
@ -663,6 +663,8 @@
|
||||||
"no-unit": "No unit",
|
"no-unit": "No unit",
|
||||||
"missing-unit": "Create missing unit: {unit}",
|
"missing-unit": "Create missing unit: {unit}",
|
||||||
"missing-food": "Create missing food: {food}",
|
"missing-food": "Create missing food: {food}",
|
||||||
|
"this-unit-could-not-be-parsed-automatically": "This unit could not be parsed automatically",
|
||||||
|
"this-food-could-not-be-parsed-automatically": "This food could not be parsed automatically",
|
||||||
"no-food": "No Food"
|
"no-food": "No Food"
|
||||||
},
|
},
|
||||||
"reset-servings-count": "Reset Servings Count",
|
"reset-servings-count": "Reset Servings Count",
|
||||||
|
|
|
@ -60,7 +60,7 @@
|
||||||
</template>
|
</template>
|
||||||
<template #[`item.actions`]="{ item }">
|
<template #[`item.actions`]="{ item }">
|
||||||
<v-tooltip
|
<v-tooltip
|
||||||
bottom
|
location="bottom"
|
||||||
:disabled="!(item && (item.households!.length > 0 || item.users!.length > 0))"
|
:disabled="!(item && (item.households!.length > 0 || item.users!.length > 0))"
|
||||||
>
|
>
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props }">
|
||||||
|
|
|
@ -82,7 +82,7 @@
|
||||||
</template>
|
</template>
|
||||||
<template #[`item.actions`]="{ item }">
|
<template #[`item.actions`]="{ item }">
|
||||||
<v-tooltip
|
<v-tooltip
|
||||||
bottom
|
location="bottom"
|
||||||
:disabled="!(item && item.users!.length > 0)"
|
:disabled="!(item && item.users!.length > 0)"
|
||||||
>
|
>
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props }">
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
@cancel="deleteCreateTarget()"
|
@cancel="deleteCreateTarget()"
|
||||||
>
|
>
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<CookbookEditor :key="createTargetKey" v-model="createTarget" :actions="actions" />
|
<CookbookEditor :key="createTargetKey" v-model="createTarget" />
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
|
|
||||||
|
@ -105,9 +105,7 @@
|
||||||
<v-expansion-panel-text>
|
<v-expansion-panel-text>
|
||||||
<CookbookEditor
|
<CookbookEditor
|
||||||
v-model="myCookbooks[index]"
|
v-model="myCookbooks[index]"
|
||||||
:actions="actions"
|
|
||||||
:collapsable="false"
|
:collapsable="false"
|
||||||
@delete="deleteEventHandler"
|
|
||||||
/>
|
/>
|
||||||
<v-card-actions>
|
<v-card-actions>
|
||||||
<v-spacer />
|
<v-spacer />
|
||||||
|
|
|
@ -103,7 +103,12 @@
|
||||||
<RecipeIngredientEditor
|
<RecipeIngredientEditor
|
||||||
v-model="parsedIng[index].ingredient"
|
v-model="parsedIng[index].ingredient"
|
||||||
allow-insert-ingredient
|
allow-insert-ingredient
|
||||||
@insert-ingredient="insertIngredient(index)"
|
:unit-error="errors[index].unitError && errors[index].unitErrorMessage !== ''"
|
||||||
|
:unit-error-tooltip="$t('recipe.parser.this-unit-could-not-be-parsed-automatically')"
|
||||||
|
:food-error="errors[index].foodError && errors[index].foodErrorMessage !== ''"
|
||||||
|
:food-error-tooltip="$t('recipe.parser.this-food-could-not-be-parsed-automatically')"
|
||||||
|
@insert-above="insertIngredient(index)"
|
||||||
|
@insert-below="insertIngredient(index + 1)"
|
||||||
@delete="deleteIngredient(index)"
|
@delete="deleteIngredient(index)"
|
||||||
/>
|
/>
|
||||||
{{ ing.input }}
|
{{ ing.input }}
|
||||||
|
@ -113,7 +118,7 @@
|
||||||
v-if="errors[index].unitError && errors[index].unitErrorMessage !== ''"
|
v-if="errors[index].unitError && errors[index].unitErrorMessage !== ''"
|
||||||
color="warning"
|
color="warning"
|
||||||
size="small"
|
size="small"
|
||||||
@click="createUnit(ing.ingredient.unit!, index)"
|
@click="createUnit(errors[index].unitName, index)"
|
||||||
>
|
>
|
||||||
{{ errors[index].unitErrorMessage }}
|
{{ errors[index].unitErrorMessage }}
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
|
@ -121,7 +126,7 @@
|
||||||
v-if="errors[index].foodError && errors[index].foodErrorMessage !== ''"
|
v-if="errors[index].foodError && errors[index].foodErrorMessage !== ''"
|
||||||
color="warning"
|
color="warning"
|
||||||
size="small"
|
size="small"
|
||||||
@click="createFood(ing.ingredient.food!, index)"
|
@click="createFood(errors[index].foodName, index)"
|
||||||
>
|
>
|
||||||
{{ errors[index].foodErrorMessage }}
|
{{ errors[index].foodErrorMessage }}
|
||||||
</BaseButton>
|
</BaseButton>
|
||||||
|
@ -157,8 +162,10 @@ import type { Parser } from "~/lib/api/user/recipes/recipe";
|
||||||
|
|
||||||
interface Error {
|
interface Error {
|
||||||
ingredientIndex: number;
|
ingredientIndex: number;
|
||||||
|
unitName: string;
|
||||||
unitError: boolean;
|
unitError: boolean;
|
||||||
unitErrorMessage: string;
|
unitErrorMessage: string;
|
||||||
|
foodName: string;
|
||||||
foodError: boolean;
|
foodError: boolean;
|
||||||
foodErrorMessage: string;
|
foodErrorMessage: string;
|
||||||
}
|
}
|
||||||
|
@ -224,30 +231,33 @@ export default defineNuxtComponent({
|
||||||
const unitError = !checkForUnit(ing.ingredient.unit!);
|
const unitError = !checkForUnit(ing.ingredient.unit!);
|
||||||
const foodError = !checkForFood(ing.ingredient.food!);
|
const foodError = !checkForFood(ing.ingredient.food!);
|
||||||
|
|
||||||
|
const unit = ing.ingredient.unit?.name || i18n.t("recipe.parser.no-unit");
|
||||||
|
const food = ing.ingredient.food?.name || i18n.t("recipe.parser.no-food");
|
||||||
|
|
||||||
let unitErrorMessage = "";
|
let unitErrorMessage = "";
|
||||||
let foodErrorMessage = "";
|
let foodErrorMessage = "";
|
||||||
|
|
||||||
if (unitError || foodError) {
|
if (unitError) {
|
||||||
if (unitError) {
|
if (ing?.ingredient?.unit?.name) {
|
||||||
if (ing?.ingredient?.unit?.name) {
|
ing.ingredient.unit = undefined;
|
||||||
const unit = ing.ingredient.unit.name || i18n.t("recipe.parser.no-unit");
|
unitErrorMessage = i18n.t("recipe.parser.missing-unit", { unit }).toString();
|
||||||
unitErrorMessage = i18n.t("recipe.parser.missing-unit", { unit }).toString();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (foodError) {
|
if (foodError) {
|
||||||
if (ing?.ingredient?.food?.name) {
|
if (ing?.ingredient?.food?.name) {
|
||||||
const food = ing.ingredient.food.name || i18n.t("recipe.parser.no-food");
|
ing.ingredient.food = undefined;
|
||||||
foodErrorMessage = i18n.t("recipe.parser.missing-food", { food }).toString();
|
foodErrorMessage = i18n.t("recipe.parser.missing-food", { food }).toString();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
panels.value.push(index);
|
panels.value.push(index);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ingredientIndex: index,
|
ingredientIndex: index,
|
||||||
|
unitName: unit,
|
||||||
unitError,
|
unitError,
|
||||||
unitErrorMessage,
|
unitErrorMessage,
|
||||||
|
foodName: food,
|
||||||
foodError,
|
foodError,
|
||||||
foodErrorMessage,
|
foodErrorMessage,
|
||||||
} as Error;
|
} as Error;
|
||||||
|
@ -320,24 +330,24 @@ export default defineNuxtComponent({
|
||||||
return !!food?.id;
|
return !!food?.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createFood(food: CreateIngredientFood | undefined, index: number) {
|
async function createFood(foodName: string, index: number) {
|
||||||
if (!food) {
|
if (!foodName) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
foodData.data.name = food.name;
|
foodData.data.name = foodName;
|
||||||
parsedIng.value[index].ingredient.food = await foodStore.actions.createOne(foodData.data) || undefined;
|
parsedIng.value[index].ingredient.food = await foodStore.actions.createOne(foodData.data) || undefined;
|
||||||
errors.value[index].foodError = false;
|
errors.value[index].foodError = false;
|
||||||
|
|
||||||
foodData.reset();
|
foodData.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createUnit(unit: CreateIngredientUnit | undefined, index: number) {
|
async function createUnit(unitName: string | undefined, index: number) {
|
||||||
if (!unit) {
|
if (!unitName) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
unitData.data.name = unit.name;
|
unitData.data.name = unitName;
|
||||||
parsedIng.value[index].ingredient.unit = await unitStore.actions.createOne(unitData.data) || undefined;
|
parsedIng.value[index].ingredient.unit = await unitStore.actions.createOne(unitData.data) || undefined;
|
||||||
errors.value[index].unitError = false;
|
errors.value[index].unitError = false;
|
||||||
|
|
||||||
|
|
37
poetry.lock
generated
37
poetry.lock
generated
|
@ -3269,30 +3269,29 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruff"
|
name = "ruff"
|
||||||
version = "0.12.5"
|
version = "0.12.6"
|
||||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
groups = ["dev"]
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "ruff-0.12.5-py3-none-linux_armv6l.whl", hash = "sha256:1de2c887e9dec6cb31fcb9948299de5b2db38144e66403b9660c9548a67abd92"},
|
{file = "ruff-0.12.6-py3-none-linux_armv6l.whl", hash = "sha256:59b48d8581989e0527b64c3297e672357c03b78d58cf1b228037a49915316277"},
|
||||||
{file = "ruff-0.12.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d1ab65e7d8152f519e7dea4de892317c9da7a108da1c56b6a3c1d5e7cf4c5e9a"},
|
{file = "ruff-0.12.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:412518260394e8a6647a0c610062cac48ff230d39b9df57faae93aa77123e90c"},
|
||||||
{file = "ruff-0.12.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:962775ed5b27c7aa3fdc0d8f4d4433deae7659ef99ea20f783d666e77338b8cf"},
|
{file = "ruff-0.12.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b56a3f51a27d0db8141d5b4b095c2849b24f639539a05d201f72f8d83f829a78"},
|
||||||
{file = "ruff-0.12.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73b4cae449597e7195a49eb1cdca89fd9fbb16140c7579899e87f4c85bf82f73"},
|
{file = "ruff-0.12.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ef9e292957bd6a868ce4e5f57931d0583814a363add2adedae3a1c9854b7ad9"},
|
||||||
{file = "ruff-0.12.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b13489c3dc50de5e2d40110c0cce371e00186b880842e245186ca862bf9a1ac"},
|
{file = "ruff-0.12.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0c3fd9955d3009c33e60bb596ea7bc66832de34d621883061114bb3b6114d358"},
|
||||||
{file = "ruff-0.12.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1504fea81461cf4841778b3ef0a078757602a3b3ea4b008feb1308cb3f23e08"},
|
{file = "ruff-0.12.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e7456efef8dd6957843de60a245152e34a842210d8b13381d5f3e7540d17935"},
|
||||||
{file = "ruff-0.12.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c7da4129016ae26c32dfcbd5b671fe652b5ab7fc40095d80dcff78175e7eddd4"},
|
{file = "ruff-0.12.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c99e62bae20c7e1a8d4de84f96754e9732d0831614ed165415ed2c4f4aa83864"},
|
||||||
{file = "ruff-0.12.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ca972c80f7ebcfd8af75a0f18b17c42d9f1ef203d163669150453f50ca98ab7b"},
|
{file = "ruff-0.12.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d47ff2b300da87df8437e1b35291349faaceb666d8349edef733b6562d29264f"},
|
||||||
{file = "ruff-0.12.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8dbbf9f25dfb501f4237ae7501d6364b76a01341c6f1b2cd6764fe449124bb2a"},
|
{file = "ruff-0.12.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8883ab5e9506574a6a2abacb5da34d416fdd8434151b35421ba3f79ca9a14a11"},
|
||||||
{file = "ruff-0.12.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c47dea6ae39421851685141ba9734767f960113d51e83fd7bb9958d5be8763a"},
|
{file = "ruff-0.12.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3cfbd192c312669fb22cd4bf8c700e8b4b1dced7ce034e581459c0e375486fa"},
|
||||||
{file = "ruff-0.12.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c5076aa0e61e30f848846f0265c873c249d4b558105b221be1828f9f79903dc5"},
|
{file = "ruff-0.12.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c1d87f2b1abf330281b3972d6bf34d366ee84b3077df66a89169e2d81b291891"},
|
||||||
{file = "ruff-0.12.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a5a4c7830dadd3d8c39b1cc85386e2c1e62344f20766be6f173c22fb5f72f293"},
|
{file = "ruff-0.12.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:3f32aaa9b5ed69de80693abeecf9961cd97851cadf7850081461261d0e6551b6"},
|
||||||
{file = "ruff-0.12.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:46699f73c2b5b137b9dc0fc1a190b43e35b008b398c6066ea1350cce6326adcb"},
|
{file = "ruff-0.12.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:de5185f19289a800c16d6ec8a9ba0b8b911b4640a4927b487f48fb51634ce315"},
|
||||||
{file = "ruff-0.12.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5a655a0a0d396f0f072faafc18ebd59adde8ca85fb848dc1b0d9f024b9c4d3bb"},
|
{file = "ruff-0.12.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:80f9d56205f6f6c4a1039c79d9acc0a9c104915f4fc0fc0385170decc72f6e4c"},
|
||||||
{file = "ruff-0.12.5-py3-none-win32.whl", hash = "sha256:dfeb2627c459b0b78ca2bbdc38dd11cc9a0a88bf91db982058b26ce41714ffa9"},
|
{file = "ruff-0.12.6-py3-none-win32.whl", hash = "sha256:b553271d6ed5611fcbe5f6752852eef695f2a77c0405b3a16fd507e5a057f5b0"},
|
||||||
{file = "ruff-0.12.5-py3-none-win_amd64.whl", hash = "sha256:ae0d90cf5f49466c954991b9d8b953bd093c32c27608e409ae3564c63c5306a5"},
|
{file = "ruff-0.12.6-py3-none-win_amd64.whl", hash = "sha256:48b73d4acef6768bfe9912e8f623ec87677bcfb6dc748ac406ebff06a84a6d70"},
|
||||||
{file = "ruff-0.12.5-py3-none-win_arm64.whl", hash = "sha256:48cdbfc633de2c5c37d9f090ba3b352d1576b0015bfc3bc98eaf230275b7e805"},
|
{file = "ruff-0.12.6-py3-none-win_arm64.whl", hash = "sha256:cd2c9c898a11f1441778d1cf9e358244cf5f4f2f11e93ff03c1a6c6759f4b15d"},
|
||||||
{file = "ruff-0.12.5.tar.gz", hash = "sha256:b209db6102b66f13625940b7f8c7d0f18e20039bb7f6101fbdac935c9612057e"},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue