mirror of
https://github.com/hay-kot/mealie.git
synced 2025-08-24 07:15:24 -07:00
fix RqueryFilterBuilder Hydration issues and move to script setup
This commit is contained in:
parent
96320d9f36
commit
c20448b219
2 changed files with 448 additions and 519 deletions
|
@ -163,14 +163,14 @@
|
|||
max-width="290px"
|
||||
min-width="auto"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<template #activator="{ props: activatorProps }">
|
||||
<v-text-field
|
||||
v-model="field.value"
|
||||
persistent-hint
|
||||
:prepend-icon="$globals.icons.calendar"
|
||||
variant="underlined"
|
||||
color="primary"
|
||||
v-bind="props"
|
||||
v-bind="activatorProps"
|
||||
readonly
|
||||
/>
|
||||
</template>
|
||||
|
@ -184,53 +184,48 @@
|
|||
</v-menu>
|
||||
<RecipeOrganizerSelector
|
||||
v-else-if="field.type === Organizer.Category"
|
||||
:model-value="field.organizers"
|
||||
v-model="field.organizers"
|
||||
:selector-type="Organizer.Category"
|
||||
:show-add="false"
|
||||
:show-label="false"
|
||||
:show-icon="false"
|
||||
variant="underlined"
|
||||
@update:model-value="setOrganizerValues(field, index, $event)"
|
||||
/>
|
||||
<RecipeOrganizerSelector
|
||||
v-else-if="field.type === Organizer.Tag"
|
||||
:model-value="field.organizers"
|
||||
v-model="field.organizers"
|
||||
:selector-type="Organizer.Tag"
|
||||
:show-add="false"
|
||||
:show-label="false"
|
||||
:show-icon="false"
|
||||
variant="underlined"
|
||||
@update:model-value="setOrganizerValues(field, index, $event)"
|
||||
/>
|
||||
<RecipeOrganizerSelector
|
||||
v-else-if="field.type === Organizer.Tool"
|
||||
:model-value="field.organizers"
|
||||
v-model="field.organizers"
|
||||
:selector-type="Organizer.Tool"
|
||||
:show-add="false"
|
||||
:show-label="false"
|
||||
:show-icon="false"
|
||||
variant="underlined"
|
||||
@update:model-value="setOrganizerValues(field, index, $event)"
|
||||
/>
|
||||
<RecipeOrganizerSelector
|
||||
v-else-if="field.type === Organizer.Food"
|
||||
:model-value="field.organizers"
|
||||
v-model="field.organizers"
|
||||
:selector-type="Organizer.Food"
|
||||
:show-add="false"
|
||||
:show-label="false"
|
||||
:show-icon="false"
|
||||
variant="underlined"
|
||||
@update:model-value="setOrganizerValues(field, index, $event)"
|
||||
/>
|
||||
<RecipeOrganizerSelector
|
||||
v-else-if="field.type === Organizer.Household"
|
||||
:model-value="field.organizers"
|
||||
v-model="field.organizers"
|
||||
:selector-type="Organizer.Household"
|
||||
:show-add="false"
|
||||
:show-label="false"
|
||||
:show-icon="false"
|
||||
variant="underlined"
|
||||
@update:model-value="setOrganizerValues(field, index, $event)"
|
||||
/>
|
||||
</v-col>
|
||||
<!-- right parenthesis -->
|
||||
|
@ -297,7 +292,7 @@
|
|||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import { VueDraggable } from "vue-draggable-plus";
|
||||
import { useDebounceFn } from "@vueuse/core";
|
||||
import { useHouseholdSelf } from "~/composables/use-households";
|
||||
|
@ -307,12 +302,7 @@ import type { LogicalOperator, QueryFilterJSON, QueryFilterJSONPart, RelationalK
|
|||
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";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
components: {
|
||||
VueDraggable,
|
||||
RecipeOrganizerSelector,
|
||||
},
|
||||
props: {
|
||||
const props = defineProps({
|
||||
fieldDefs: {
|
||||
type: Array as () => FieldDefinition[],
|
||||
required: true,
|
||||
|
@ -321,57 +311,62 @@ export default defineNuxtComponent({
|
|||
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<{
|
||||
(event: "input", value: string | undefined): void;
|
||||
(event: "inputJSON", value: QueryFilterJSON | undefined): void;
|
||||
}>();
|
||||
|
||||
const { household } = useHouseholdSelf();
|
||||
const { logOps, relOps, buildQueryFilterString, getFieldFromFieldDef, isOrganizerType } = useQueryFilterBuilder();
|
||||
|
||||
const firstDayOfWeek = computed(() => {
|
||||
return household.value?.preferences?.firstDayOfWeek || 0;
|
||||
});
|
||||
});
|
||||
|
||||
const state = reactive({
|
||||
const state = reactive({
|
||||
showAdvanced: false,
|
||||
qfValid: false,
|
||||
datePickers: [] as boolean[],
|
||||
drag: false,
|
||||
});
|
||||
});
|
||||
const { showAdvanced, datePickers, drag } = toRefs(state);
|
||||
|
||||
const storeMap = {
|
||||
const storeMap = {
|
||||
[Organizer.Category]: useCategoryStore(),
|
||||
[Organizer.Tag]: useTagStore(),
|
||||
[Organizer.Tool]: useToolStore(),
|
||||
[Organizer.Food]: useFoodStore(),
|
||||
[Organizer.Household]: useHouseholdStore(),
|
||||
};
|
||||
};
|
||||
|
||||
function onDragEnd(event: any) {
|
||||
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[]>([]);
|
||||
// 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() {
|
||||
const uid = ref(1); // init uid to pass to fields
|
||||
function useUid() {
|
||||
return uid.value++;
|
||||
}
|
||||
function addField(field: FieldDefinition) {
|
||||
}
|
||||
function addField(field: FieldDefinition) {
|
||||
fields.value.push({
|
||||
...getFieldFromFieldDef(field),
|
||||
id: useUid(),
|
||||
});
|
||||
state.datePickers.push(false);
|
||||
};
|
||||
}
|
||||
|
||||
function setField(index: number, fieldLabel: string) {
|
||||
function setField(index: number, fieldLabel: string) {
|
||||
state.datePickers[index] = false;
|
||||
const fieldDef = props.fieldDefs.find(fieldDef => fieldDef.label === fieldLabel);
|
||||
if (!fieldDef) {
|
||||
|
@ -388,48 +383,48 @@ export default defineNuxtComponent({
|
|||
...getFieldFromFieldDef(updatedField, resetValue),
|
||||
id: fields.value[index].id, // keep the id
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function setLeftParenthesisValue(field: FieldWithId, index: number, value: string) {
|
||||
function setLeftParenthesisValue(field: FieldWithId, index: number, value: string) {
|
||||
fields.value[index].leftParenthesis = value;
|
||||
}
|
||||
}
|
||||
|
||||
function setRightParenthesisValue(field: FieldWithId, index: number, value: string) {
|
||||
function setRightParenthesisValue(field: FieldWithId, index: number, value: string) {
|
||||
fields.value[index].rightParenthesis = value;
|
||||
}
|
||||
}
|
||||
|
||||
function setLogicalOperatorValue(field: FieldWithId, index: number, value: LogicalOperator | undefined) {
|
||||
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) {
|
||||
function setRelationalOperatorValue(field: FieldWithId, index: number, value: RelationalKeyword | RelationalOperator) {
|
||||
fields.value[index].relationalOperatorValue = relOps.value[value];
|
||||
}
|
||||
}
|
||||
|
||||
function setFieldValue(field: FieldWithId, index: number, value: FieldValue) {
|
||||
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[]) {
|
||||
function setFieldValues(field: FieldWithId, index: number, values: FieldValue[]) {
|
||||
fields.value[index].values = values;
|
||||
}
|
||||
}
|
||||
|
||||
function setOrganizerValues(field: FieldWithId, index: number, values: OrganizerBase[]) {
|
||||
function setOrganizerValues(field: FieldWithId, index: number, values: OrganizerBase[]) {
|
||||
setFieldValues(field, index, values.map(value => value.id.toString()));
|
||||
fields.value[index].organizers = values;
|
||||
}
|
||||
}
|
||||
|
||||
function removeField(index: number) {
|
||||
function removeField(index: number) {
|
||||
fields.value.splice(index, 1);
|
||||
state.datePickers.splice(index, 1);
|
||||
};
|
||||
}
|
||||
|
||||
const fieldsUpdater = useDebounceFn((/* newFields: typeof fields.value */) => {
|
||||
const fieldsUpdater = useDebounceFn((/* newFields: typeof fields.value */) => {
|
||||
/* newFields.forEach((field, index) => {
|
||||
const updatedField = getFieldFromFieldDef(field);
|
||||
fields.value[index] = updatedField; // recursive!!!
|
||||
|
@ -441,18 +436,18 @@ export default defineNuxtComponent({
|
|||
}
|
||||
state.qfValid = !!qf;
|
||||
|
||||
context.emit("input", qf || undefined);
|
||||
context.emit("inputJSON", qf ? buildQueryFilterJSON() : undefined);
|
||||
}, 500);
|
||||
emit("input", qf || undefined);
|
||||
emit("inputJSON", qf ? buildQueryFilterJSON() : undefined);
|
||||
}, 500);
|
||||
|
||||
watch(fields, fieldsUpdater, { deep: true });
|
||||
watch(fields, fieldsUpdater, { deep: true });
|
||||
|
||||
async function hydrateOrganizers(field: FieldWithId, index: number) {
|
||||
async function hydrateOrganizers(field: FieldWithId, index: number) {
|
||||
if (!field.values?.length || !isOrganizerType(field.type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
field.organizers = [];
|
||||
const newField = { ...field, organizers: [] as OrganizerBase[] };
|
||||
|
||||
const { store, actions } = storeMap[field.type];
|
||||
if (!store.value.length) {
|
||||
|
@ -467,11 +462,11 @@ export default defineNuxtComponent({
|
|||
}
|
||||
return organizer;
|
||||
});
|
||||
field.organizers = organizers.filter(organizer => organizer !== undefined) as OrganizerBase[];
|
||||
setOrganizerValues(field, index, field.organizers);
|
||||
}
|
||||
newField.organizers = organizers.filter(organizer => organizer !== undefined) as OrganizerBase[];
|
||||
setOrganizerValues(newField, index, newField.organizers);
|
||||
}
|
||||
|
||||
function initFieldsError(error = "") {
|
||||
function initFieldsError(error = "") {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
@ -480,12 +475,12 @@ export default defineNuxtComponent({
|
|||
if (props.fieldDefs.length) {
|
||||
addField(props.fieldDefs[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function initializeFields() {
|
||||
function initializeFields() {
|
||||
if (!props.initialQueryFilter?.parts?.length) {
|
||||
return initFieldsError();
|
||||
};
|
||||
}
|
||||
|
||||
const initFields: FieldWithId[] = [];
|
||||
let error = false;
|
||||
|
@ -561,16 +556,16 @@ export default defineNuxtComponent({
|
|||
else {
|
||||
initFieldsError();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
try {
|
||||
initializeFields();
|
||||
}
|
||||
catch (error) {
|
||||
}
|
||||
catch (error) {
|
||||
initFieldsError(`Error initializing fields: ${(error || "").toString()}`);
|
||||
}
|
||||
}
|
||||
|
||||
function buildQueryFilterJSON(): QueryFilterJSON {
|
||||
function buildQueryFilterJSON(): QueryFilterJSON {
|
||||
const parts = fields.value.map((field) => {
|
||||
const part: QueryFilterJSONPart = {
|
||||
attributeName: field.name,
|
||||
|
@ -596,9 +591,9 @@ export default defineNuxtComponent({
|
|||
const qfJSON = { parts } as QueryFilterJSON;
|
||||
console.debug(`Built query filter JSON: ${JSON.stringify(qfJSON)}`);
|
||||
return qfJSON;
|
||||
}
|
||||
}
|
||||
|
||||
const config = computed(() => {
|
||||
const config = computed(() => {
|
||||
const baseColMaxWidth = 55;
|
||||
return {
|
||||
col: {
|
||||
|
@ -642,30 +637,6 @@ export default defineNuxtComponent({
|
|||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
Organizer,
|
||||
...toRefs(state),
|
||||
logOps,
|
||||
relOps,
|
||||
config,
|
||||
firstDayOfWeek,
|
||||
onDragEnd,
|
||||
// Fields
|
||||
fields,
|
||||
addField,
|
||||
setField,
|
||||
setLeftParenthesisValue,
|
||||
setRightParenthesisValue,
|
||||
setLogicalOperatorValue,
|
||||
setRelationalOperatorValue,
|
||||
setFieldValue,
|
||||
setFieldValues,
|
||||
setOrganizerValues,
|
||||
removeField,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
v-model="selected"
|
||||
v-bind="inputAttrs"
|
||||
v-model:search="searchInput"
|
||||
:items="storeItem"
|
||||
:items="items"
|
||||
:label="label"
|
||||
chips
|
||||
closable-chips
|
||||
|
@ -46,78 +46,51 @@
|
|||
</v-autocomplete>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script setup lang="ts">
|
||||
import type { IngredientFood, RecipeCategory, RecipeTag } from "~/lib/api/types/recipe";
|
||||
import type { RecipeTool } from "~/lib/api/types/admin";
|
||||
import { Organizer, type RecipeOrganizer } from "~/lib/api/types/non-generated";
|
||||
import type { HouseholdSummary } from "~/lib/api/types/household";
|
||||
import { useCategoryStore, useFoodStore, useHouseholdStore, useTagStore, useToolStore } from "~/composables/store";
|
||||
|
||||
export default defineNuxtComponent({
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Array as () => (
|
||||
interface Props {
|
||||
selectorType: RecipeOrganizer;
|
||||
inputAttrs?: Record<string, any>;
|
||||
returnObject?: boolean;
|
||||
showAdd?: boolean;
|
||||
showLabel?: boolean;
|
||||
showIcon?: boolean;
|
||||
variant?: "filled" | "underlined" | "outlined" | "plain" | "solo" | "solo-inverted" | "solo-filled";
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
inputAttrs: () => ({}),
|
||||
returnObject: true,
|
||||
showAdd: true,
|
||||
showLabel: true,
|
||||
showIcon: true,
|
||||
variant: "outlined",
|
||||
});
|
||||
|
||||
const selected = defineModel<(
|
||||
| HouseholdSummary
|
||||
| RecipeTag
|
||||
| RecipeCategory
|
||||
| RecipeTool
|
||||
| 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"],
|
||||
)[] | undefined>({ required: true });
|
||||
|
||||
setup(props, context) {
|
||||
const selected = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => {
|
||||
context.emit("update:modelValue", val);
|
||||
},
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
onMounted(() => {
|
||||
if (selected.value === undefined) {
|
||||
selected.value = [];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const i18n = useI18n();
|
||||
const { $globals } = useNuxtApp();
|
||||
const i18n = useI18n();
|
||||
const { $globals } = useNuxtApp();
|
||||
|
||||
const label = computed(() => {
|
||||
const label = computed(() => {
|
||||
if (!props.showLabel) {
|
||||
return "";
|
||||
}
|
||||
|
@ -136,9 +109,9 @@ export default defineNuxtComponent({
|
|||
default:
|
||||
return i18n.t("general.organizer");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const icon = computed(() => {
|
||||
const icon = computed(() => {
|
||||
if (!props.showIcon) {
|
||||
return "";
|
||||
}
|
||||
|
@ -157,69 +130,54 @@ export default defineNuxtComponent({
|
|||
default:
|
||||
return $globals.icons.tags;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ===========================================================================
|
||||
// Store & Items Setup
|
||||
// ===========================================================================
|
||||
// Store & Items Setup
|
||||
|
||||
const storeMap = {
|
||||
const storeMap = {
|
||||
[Organizer.Category]: useCategoryStore(),
|
||||
[Organizer.Tag]: useTagStore(),
|
||||
[Organizer.Tool]: useToolStore(),
|
||||
[Organizer.Food]: useFoodStore(),
|
||||
[Organizer.Household]: useHouseholdStore(),
|
||||
};
|
||||
};
|
||||
|
||||
const store = computed(() => {
|
||||
const store = computed(() => {
|
||||
const { store } = storeMap[props.selectorType];
|
||||
return store.value;
|
||||
});
|
||||
});
|
||||
|
||||
const items = computed(() => {
|
||||
const items = computed(() => {
|
||||
if (!props.returnObject) {
|
||||
return store.value.map(item => item.name);
|
||||
}
|
||||
return store.value;
|
||||
});
|
||||
});
|
||||
|
||||
function removeByIndex(index: number) {
|
||||
function removeByIndex(index: number) {
|
||||
if (selected.value === undefined) {
|
||||
return;
|
||||
}
|
||||
const newSelected = selected.value.filter((_, i) => i !== index);
|
||||
selected.value = [...newSelected];
|
||||
}
|
||||
}
|
||||
|
||||
function appendCreated(item: any) {
|
||||
function appendCreated(item: any) {
|
||||
if (selected.value === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
selected.value = [...selected.value, item];
|
||||
}
|
||||
}
|
||||
|
||||
const dialog = ref(false);
|
||||
const dialog = ref(false);
|
||||
|
||||
const searchInput = ref("");
|
||||
const searchInput = ref("");
|
||||
|
||||
function resetSearchInput() {
|
||||
function resetSearchInput() {
|
||||
searchInput.value = "";
|
||||
}
|
||||
|
||||
return {
|
||||
Organizer,
|
||||
appendCreated,
|
||||
dialog,
|
||||
storeItem: items,
|
||||
label,
|
||||
icon,
|
||||
selected,
|
||||
removeByIndex,
|
||||
searchInput,
|
||||
resetSearchInput,
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue