mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-07-25 08:09:41 +02:00
feat: Query Filter Builder for Cookbooks and Meal Plans (#4346)
This commit is contained in:
parent
2a9a6fa5e6
commit
b8e62ab8dd
47 changed files with 2043 additions and 440 deletions
|
@ -51,6 +51,9 @@ export function useReadOnlyActions<T extends BoundT>(
|
|||
}
|
||||
|
||||
async function refresh(page = 1, perPage = -1, params = {} as Record<string, QueryValue>) {
|
||||
params.orderBy ??= "name";
|
||||
params.orderDirection ??= "asc";
|
||||
|
||||
loading.value = true;
|
||||
const { data } = await api.getAll(page, perPage, params);
|
||||
|
||||
|
@ -102,6 +105,9 @@ export function useStoreActions<T extends BoundT>(
|
|||
}
|
||||
|
||||
async function refresh(page = 1, perPage = -1, params = {} as Record<string, QueryValue>) {
|
||||
params.orderBy ??= "name";
|
||||
params.orderDirection ??= "asc";
|
||||
|
||||
loading.value = true;
|
||||
const { data } = await api.getAll(page, perPage, params);
|
||||
|
||||
|
|
|
@ -103,6 +103,8 @@ export const useCookbooks = function () {
|
|||
loading.value = true;
|
||||
const { data } = await api.cookbooks.createOne({
|
||||
name: i18n.t("cookbook.household-cookbook-name", [household.value?.name || "", String((cookbookStore?.value?.length ?? 0) + 1)]) as string,
|
||||
position: (cookbookStore?.value?.length ?? 0) + 1,
|
||||
queryFilterString: "",
|
||||
});
|
||||
if (data && cookbookStore?.value) {
|
||||
cookbookStore.value.push(data);
|
||||
|
|
318
frontend/composables/use-query-filter-builder.ts
Normal file
318
frontend/composables/use-query-filter-builder.ts
Normal file
|
@ -0,0 +1,318 @@
|
|||
import { computed, useContext } from "@nuxtjs/composition-api";
|
||||
import { Organizer, RecipeOrganizer } from "~/lib/api/types/non-generated";
|
||||
import { LogicalOperator, RelationalKeyword, RelationalOperator } from "~/lib/api/types/response";
|
||||
|
||||
export interface FieldLogicalOperator {
|
||||
label: string;
|
||||
value: LogicalOperator;
|
||||
}
|
||||
|
||||
export interface FieldRelationalOperator {
|
||||
label: string;
|
||||
value: RelationalKeyword | RelationalOperator;
|
||||
}
|
||||
|
||||
export interface OrganizerBase {
|
||||
id: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export type FieldType =
|
||||
| "string"
|
||||
| "number"
|
||||
| "boolean"
|
||||
| "date"
|
||||
| RecipeOrganizer;
|
||||
|
||||
export type FieldValue =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| Date
|
||||
| Organizer;
|
||||
|
||||
export interface SelectableItem {
|
||||
label: string;
|
||||
value: FieldValue;
|
||||
};
|
||||
|
||||
export interface FieldDefinition {
|
||||
name: string;
|
||||
label: string;
|
||||
type: FieldType;
|
||||
|
||||
// only for select/organizer fields
|
||||
fieldOptions?: SelectableItem[];
|
||||
}
|
||||
|
||||
export interface Field extends FieldDefinition {
|
||||
leftParenthesis?: string;
|
||||
logicalOperator?: FieldLogicalOperator;
|
||||
value: FieldValue;
|
||||
relationalOperatorValue: FieldRelationalOperator;
|
||||
relationalOperatorOptions: FieldRelationalOperator[];
|
||||
rightParenthesis?: string;
|
||||
|
||||
// only for select/organizer fields
|
||||
values: FieldValue[];
|
||||
organizers: OrganizerBase[];
|
||||
}
|
||||
|
||||
export function useQueryFilterBuilder() {
|
||||
const { i18n } = useContext();
|
||||
|
||||
const logOps = computed<Record<LogicalOperator, FieldLogicalOperator>>(() => {
|
||||
const AND = {
|
||||
label: i18n.tc("query-filter.logical-operators.and"),
|
||||
value: "AND",
|
||||
} as FieldLogicalOperator;
|
||||
|
||||
const OR = {
|
||||
label: i18n.tc("query-filter.logical-operators.or"),
|
||||
value: "OR",
|
||||
} as FieldLogicalOperator;
|
||||
|
||||
return {
|
||||
AND,
|
||||
OR,
|
||||
};
|
||||
});
|
||||
|
||||
const relOps = computed<Record<RelationalKeyword | RelationalOperator, FieldRelationalOperator>>(() => {
|
||||
const EQ = {
|
||||
label: i18n.tc("query-filter.relational-operators.equals"),
|
||||
value: "=",
|
||||
} as FieldRelationalOperator;
|
||||
|
||||
const NOT_EQ = {
|
||||
label: i18n.tc("query-filter.relational-operators.does-not-equal"),
|
||||
value: "<>",
|
||||
} as FieldRelationalOperator;
|
||||
|
||||
const GT = {
|
||||
label: i18n.tc("query-filter.relational-operators.is-greater-than"),
|
||||
value: ">",
|
||||
} as FieldRelationalOperator;
|
||||
|
||||
const GTE = {
|
||||
label: i18n.tc("query-filter.relational-operators.is-greater-than-or-equal-to"),
|
||||
value: ">=",
|
||||
} as FieldRelationalOperator;
|
||||
|
||||
const LT = {
|
||||
label: i18n.tc("query-filter.relational-operators.is-less-than"),
|
||||
value: "<",
|
||||
} as FieldRelationalOperator;
|
||||
|
||||
const LTE = {
|
||||
label: i18n.tc("query-filter.relational-operators.is-less-than-or-equal-to"),
|
||||
value: "<=",
|
||||
} as FieldRelationalOperator;
|
||||
|
||||
const IS = {
|
||||
label: i18n.tc("query-filter.relational-keywords.is"),
|
||||
value: "IS",
|
||||
} as FieldRelationalOperator;
|
||||
|
||||
const IS_NOT = {
|
||||
label: i18n.tc("query-filter.relational-keywords.is-not"),
|
||||
value: "IS NOT",
|
||||
} as FieldRelationalOperator;
|
||||
|
||||
const IN = {
|
||||
label: i18n.tc("query-filter.relational-keywords.is-one-of"),
|
||||
value: "IN",
|
||||
} as FieldRelationalOperator;
|
||||
|
||||
const NOT_IN = {
|
||||
label: i18n.tc("query-filter.relational-keywords.is-not-one-of"),
|
||||
value: "NOT IN",
|
||||
} as FieldRelationalOperator;
|
||||
|
||||
const CONTAINS_ALL = {
|
||||
label: i18n.tc("query-filter.relational-keywords.contains-all-of"),
|
||||
value: "CONTAINS ALL",
|
||||
} as FieldRelationalOperator;
|
||||
|
||||
const LIKE = {
|
||||
label: i18n.tc("query-filter.relational-keywords.is-like"),
|
||||
value: "LIKE",
|
||||
} as FieldRelationalOperator;
|
||||
|
||||
const NOT_LIKE = {
|
||||
label: i18n.tc("query-filter.relational-keywords.is-not-like"),
|
||||
value: "NOT LIKE",
|
||||
} as FieldRelationalOperator;
|
||||
|
||||
/* eslint-disable object-shorthand */
|
||||
return {
|
||||
"=": EQ,
|
||||
"<>": NOT_EQ,
|
||||
">": GT,
|
||||
">=": GTE,
|
||||
"<": LT,
|
||||
"<=": LTE,
|
||||
"IS": IS,
|
||||
"IS NOT": IS_NOT,
|
||||
"IN": IN,
|
||||
"NOT IN": NOT_IN,
|
||||
"CONTAINS ALL": CONTAINS_ALL,
|
||||
"LIKE": LIKE,
|
||||
"NOT LIKE": NOT_LIKE,
|
||||
};
|
||||
/* eslint-enable object-shorthand */
|
||||
});
|
||||
|
||||
function isOrganizerType(type: FieldType): type is Organizer {
|
||||
return (
|
||||
type === Organizer.Category ||
|
||||
type === Organizer.Tag ||
|
||||
type === Organizer.Tool ||
|
||||
type === Organizer.Food ||
|
||||
type === Organizer.Household
|
||||
);
|
||||
};
|
||||
|
||||
function getFieldFromFieldDef(field: Field | FieldDefinition, resetValue = false): Field {
|
||||
/* eslint-disable dot-notation */
|
||||
const updatedField = {logicalOperator: logOps.value.AND, ...field} as Field;
|
||||
let operatorOptions: FieldRelationalOperator[];
|
||||
if (updatedField.fieldOptions?.length || isOrganizerType(updatedField.type)) {
|
||||
operatorOptions = [
|
||||
relOps.value["IN"],
|
||||
relOps.value["NOT IN"],
|
||||
relOps.value["CONTAINS ALL"],
|
||||
];
|
||||
} else {
|
||||
switch (updatedField.type) {
|
||||
case "string":
|
||||
operatorOptions = [
|
||||
relOps.value["="],
|
||||
relOps.value["<>"],
|
||||
relOps.value["LIKE"],
|
||||
relOps.value["NOT LIKE"],
|
||||
];
|
||||
break;
|
||||
case "number":
|
||||
operatorOptions = [
|
||||
relOps.value["="],
|
||||
relOps.value["<>"],
|
||||
relOps.value[">"],
|
||||
relOps.value[">="],
|
||||
relOps.value["<"],
|
||||
relOps.value["<="],
|
||||
];
|
||||
break;
|
||||
case "boolean":
|
||||
operatorOptions = [relOps.value["="]];
|
||||
break;
|
||||
case "date":
|
||||
operatorOptions = [
|
||||
relOps.value["="],
|
||||
relOps.value["<>"],
|
||||
relOps.value[">"],
|
||||
relOps.value[">="],
|
||||
relOps.value["<"],
|
||||
relOps.value["<="],
|
||||
];
|
||||
break;
|
||||
default:
|
||||
operatorOptions = [relOps.value["="], relOps.value["<>"]];
|
||||
}
|
||||
}
|
||||
updatedField.relationalOperatorOptions = operatorOptions;
|
||||
if (!operatorOptions.includes(updatedField.relationalOperatorValue)) {
|
||||
updatedField.relationalOperatorValue = operatorOptions[0];
|
||||
}
|
||||
|
||||
if (resetValue) {
|
||||
updatedField.value = "";
|
||||
updatedField.values = [];
|
||||
updatedField.organizers = [];
|
||||
} else {
|
||||
updatedField.value = updatedField.value || "";
|
||||
updatedField.values = updatedField.values || [];
|
||||
updatedField.organizers = updatedField.organizers || [];
|
||||
}
|
||||
|
||||
return updatedField;
|
||||
/* eslint-enable dot-notation */
|
||||
};
|
||||
|
||||
function buildQueryFilterString(fields: Field[], useParenthesis: boolean): string {
|
||||
let isValid = true;
|
||||
let lParenCounter = 0;
|
||||
let rParenCounter = 0;
|
||||
|
||||
const parts: string[] = [];
|
||||
fields.forEach((field, index) => {
|
||||
if (index) {
|
||||
if (!field.logicalOperator) {
|
||||
field.logicalOperator = logOps.value.AND;
|
||||
}
|
||||
parts.push(field.logicalOperator.value);
|
||||
}
|
||||
|
||||
if (field.leftParenthesis && useParenthesis) {
|
||||
lParenCounter += field.leftParenthesis.length;
|
||||
parts.push(field.leftParenthesis);
|
||||
}
|
||||
|
||||
if (field.label) {
|
||||
parts.push(field.name);
|
||||
} else {
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (field.relationalOperatorValue) {
|
||||
parts.push(field.relationalOperatorValue.value);
|
||||
} else if (field.type !== "boolean") {
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (field.fieldOptions?.length || isOrganizerType(field.type)) {
|
||||
if (field.values?.length) {
|
||||
let val: string;
|
||||
if (field.type === "string" || field.type === "date" || isOrganizerType(field.type)) {
|
||||
val = field.values.map((value) => `"${value.toString()}"`).join(",");
|
||||
} else {
|
||||
val = field.values.join(",");
|
||||
}
|
||||
parts.push(`[${val}]`);
|
||||
} else {
|
||||
isValid = false;
|
||||
}
|
||||
} else if (field.value) {
|
||||
if (field.type === "string" || field.type === "date") {
|
||||
parts.push(`"${field.value.toString()}"`);
|
||||
} else {
|
||||
parts.push(field.value.toString());
|
||||
}
|
||||
} else if (field.type === "boolean") {
|
||||
parts.push("false");
|
||||
} else {
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (field.rightParenthesis && useParenthesis) {
|
||||
rParenCounter += field.rightParenthesis.length;
|
||||
parts.push(field.rightParenthesis);
|
||||
}
|
||||
});
|
||||
|
||||
if (lParenCounter !== rParenCounter) {
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
return isValid ? parts.join(" ") : "";
|
||||
}
|
||||
|
||||
return {
|
||||
logOps,
|
||||
relOps,
|
||||
buildQueryFilterString,
|
||||
getFieldFromFieldDef,
|
||||
isOrganizerType,
|
||||
};
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue