- Progress Commit

Signed-off-by: Dave Richer <dave@imexsystems.ca>
This commit is contained in:
Dave Richer
2024-02-28 17:17:17 -05:00
parent 9daf992582
commit 32bba8060a
2 changed files with 276 additions and 175 deletions

View File

@@ -6,6 +6,7 @@ import {useTranslation} from "react-i18next";
import {getOrderOperatorsByType, getWhereOperatorsByType} from "../../utils/graphQLmodifier"; import {getOrderOperatorsByType, getWhereOperatorsByType} from "../../utils/graphQLmodifier";
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component"; import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component";
import {generateInternalReflections} from "./report-center-modal-utils"; import {generateInternalReflections} from "./report-center-modal-utils";
import {FormDatePicker} from "../form-date-picker/form-date-picker.component.jsx";
export default function ReportCenterModalFiltersSortersComponent({form, bodyshop}) { export default function ReportCenterModalFiltersSortersComponent({form, bodyshop}) {
@@ -71,8 +72,9 @@ function FiltersSection({filters, form, bodyshop}) {
</Form.Item> </Form.Item>
</Col> </Col>
<Col span={6}> <Col span={6}>
<Form.Item <Form.Item
dependencies={[['filters', field.name, "field"]]}> dependencies={[['filters', field.name, "field"]]}
>
{ {
() => { () => {
const name = form.getFieldValue(['filters', field.name, "field"]); const name = form.getFieldValue(['filters', field.name, "field"]);
@@ -82,7 +84,6 @@ function FiltersSection({filters, form, bodyshop}) {
key={`${index}operator`} key={`${index}operator`}
label={t('reportcenter.labels.advanced_filters_filter_operator')} label={t('reportcenter.labels.advanced_filters_filter_operator')}
name={[field.name, "operator"]} name={[field.name, "operator"]}
dependencies={[]}
rules={[ rules={[
{ {
required: true, required: true,
@@ -92,7 +93,13 @@ function FiltersSection({filters, form, bodyshop}) {
> >
<Select <Select
getPopupContainer={trigger => trigger.parentNode} getPopupContainer={trigger => trigger.parentNode}
options={getWhereOperatorsByType(type)}/> options={getWhereOperatorsByType(type)}
onChange={() => {
// Clear related Fields
form.setFieldValue(['filters', field.name, 'value'], null);
}}
/>
</Form.Item> </Form.Item>
} }
} }
@@ -100,12 +107,19 @@ function FiltersSection({filters, form, bodyshop}) {
</Col> </Col>
<Col span={6}> <Col span={6}>
<Form.Item <Form.Item
dependencies={[['filters', field.name, "field"]]}> dependencies={[
['filters', field.name, "field"],
['filters', field.name, "operator"]
]}
>
{ {
() => { () => {
const name = form.getFieldValue(['filters', field.name, "field"]); // Because it looks cleaner than inlining.
const type = filters.find(f => f.name === name)?.type; const name = form.getFieldValue(['filters', field.name, "field"]);
const reflector = filters.find(f => f.name === name)?.reflector; const type = filters.find(f => f.name === name)?.type;
const reflector = filters.find(f => f.name === name)?.reflector;
const operator = form.getFieldValue(['filters', field.name, "operator"]);
const operatorType = getWhereOperatorsByType(type).find((o) => o.value === operator)?.type;
return <Form.Item return <Form.Item
key={`${index}value`} key={`${index}value`}
@@ -138,7 +152,23 @@ function FiltersSection({filters, form, bodyshop}) {
const reflections = reflector ? generateReflections(reflector) : []; const reflections = reflector ? generateReflections(reflector) : [];
const fieldPath = [[field.name, "value"]]; const fieldPath = [[field.name, "value"]];
// We have reflections so we will use a select box
if (reflections.length > 0) { if (reflections.length > 0) {
// We have reflections and the operator type is array, so we will use a select box with multiple options
console.log(`operatorType: ${operatorType}`)
console.log(`operator: ${operator}`)
if (operatorType === "array") {
return (
<Select
mode="multiple"
options={reflections}
getPopupContainer={trigger => trigger.parentNode}
onChange={(value) => {
form.setFieldValue(fieldPath, value);
}}
/>
);
}
return ( return (
<Select <Select
options={reflections} options={reflections}
@@ -150,6 +180,7 @@ function FiltersSection({filters, form, bodyshop}) {
); );
} }
// We have a type of number, so we will use a number input
if (type === "number") { if (type === "number") {
return ( return (
<InputNumber <InputNumber
@@ -157,7 +188,35 @@ function FiltersSection({filters, form, bodyshop}) {
); );
} }
// We have a type of date, so we will use a date picker
if (type === "date") {
return ( return (
<FormDatePicker
onChange={(date) => form.setFieldValue(fieldPath, date)}
/>
);
}
// we have a type of boolean, so we will use a select box with a true or false option.
if (type === "boolean" || type === "bool") {
return (
<Select
getPopupContainer={trigger => trigger.parentNode}
options={[
{
label: "True",
value: true
},
{
label: "False",
value: false
}
]}
onChange={(value) => form.setFieldValue(fieldPath, value)}
/>
);
}
return (
<Input <Input
onChange={(e) => form.setFieldValue(fieldPath, e.target.value)}/> onChange={(e) => form.setFieldValue(fieldPath, e.target.value)}/>
); );
@@ -370,4 +429,4 @@ function RenderFilters({templateId, form, bodyshop}) {
)} )}
</div> </div>
); );
} }

View File

@@ -2,22 +2,42 @@ import {Kind, parse, print, visit} from "graphql";
import client from "./GraphQLClient"; import client from "./GraphQLClient";
import {gql} from "@apollo/client"; import {gql} from "@apollo/client";
/* eslint-disable no-loop-func */
/**
* The available operators for filtering (string)
* @type {[{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},null,null,null]}
*/
const STRING_OPERATORS = [ const STRING_OPERATORS = [
{value: "_eq", label: "equals"}, {value: "_eq", label: "equals"},
{value: "_neq", label: "does not equal"}, {value: "_neq", label: "does not equal"},
{value: "_like", label: "contains"}, {value: "_like", label: "contains"},
{value: "_nlike", label: "does not contain"}, {value: "_nlike", label: "does not contain"},
{value: "_ilike", label: "contains case-insensitive"}, {value: "_ilike", label: "contains case-insensitive"},
{value: "_nilike", label: "does not contain case-insensitive"} {value: "_nilike", label: "does not contain case-insensitive"},
{value: "_in", label: "in", type: "array"},
{value: "_nin", label: "not in", type: "array"}
]; ];
/**
* The available operators for filtering (numbers)
* @type {[{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},{label: string, value: string},null,null,null]}
*/
const NUMBER_OPERATORS = [ const NUMBER_OPERATORS = [
{value: "_eq", label: "equals"}, {value: "_eq", label: "equals"},
{value: "_neq", label: "does not equal"}, {value: "_neq", label: "does not equal"},
{value: "_gt", label: "greater than"}, {value: "_gt", label: "greater than"},
{value: "_lt", label: "less than"}, {value: "_lt", label: "less than"},
{value: "_gte", label: "greater than or equal"}, {value: "_gte", label: "greater than or equal"},
{value: "_lte", label: "less than or equal"} {value: "_lte", label: "less than or equal"},
{value: "_in", label: "in", type: "array"},
{value: "_nin", label: "not in", type: "array"}
]; ];
/**
* The available operators for sorting
* @type {[{label: string, value: string},{label: string, value: string}]}
*/
const ORDER_BY_OPERATORS = [ const ORDER_BY_OPERATORS = [
{value: "asc", label: "ascending"}, {value: "asc", label: "ascending"},
{value: "desc", label: "descending"} {value: "desc", label: "descending"}
@@ -31,7 +51,6 @@ export function getOrderOperatorsByType() {
return ORDER_BY_OPERATORS; return ORDER_BY_OPERATORS;
} }
/** /**
* Get the available operators for filtering * Get the available operators for filtering
* @param type * @param type
@@ -45,8 +64,6 @@ export function getWhereOperatorsByType(type = 'string') {
return operators[type]; return operators[type];
} }
/* eslint-disable no-loop-func */
/** /**
* Parse a GraphQL query into an AST * Parse a GraphQL query into an AST
* @param query * @param query
@@ -94,8 +111,8 @@ export async function generateTemplate(templateQueryToExecute, templateObject, u
const finalQuery = printQuery(ast); const finalQuery = printQuery(ast);
// commented out for future revision debugging // commented out for future revision debugging
// console.log('Modified Query'); console.log('Modified Query');
// console.log(finalQuery); console.log(finalQuery);
let contextData = {}; let contextData = {};
if (templateQueryToExecute) { if (templateQueryToExecute) {
@@ -109,7 +126,6 @@ export async function generateTemplate(templateQueryToExecute, templateObject, u
return {contextData, useShopSpecificTemplate}; return {contextData, useShopSpecificTemplate};
} }
/** /**
* Apply sorters to the AST * Apply sorters to the AST
* @param ast * @param ast
@@ -174,157 +190,180 @@ export function applySorters(ast, sorters) {
* Apply filters to the AST * Apply filters to the AST
* @param ast * @param ast
* @param filters * @param filters
* @returns {ASTNode}
*/ */
export function applyFilters(ast, filters) { export function applyFilters(ast, filters) {
return visit(ast, { return visit(ast, {
OperationDefinition: { OperationDefinition: {
enter(node) { enter(node) {
filters.forEach(filter => { filters.forEach(filter => {
const fieldPath = filter.field.split('.'); const fieldPath = filter.field.split('.');
let topLevel = false; let topLevel = false;
// Determine if the filter should be applied at the top level // Determine if the filter should be applied at the top level
if (fieldPath[0].startsWith('[') && fieldPath[0].endsWith(']')) { if (fieldPath[0].startsWith('[') && fieldPath[0].endsWith(']')) {
fieldPath[0] = fieldPath[0].substring(1, fieldPath[0].length - 1); // Strip the brackets fieldPath[0] = fieldPath[0].substring(1, fieldPath[0].length - 1); // Strip the brackets
topLevel = true; topLevel = true;
} }
if (topLevel) { // Construct the filter for a top-level application
// Construct the filter for a top-level application const targetFieldName = fieldPath[fieldPath.length - 1];
const targetFieldName = fieldPath[fieldPath.length - 1];
const filterValue = {
kind: getGraphQLKind(filter.value),
value: filter.value,
};
const nestedFilter = { let filterValue = createFilterValue(filter);
kind: Kind.OBJECT_FIELD, let filterField = createFilterField(targetFieldName, filter, filterValue);
name: {kind: Kind.NAME, value: targetFieldName},
value: {
kind: Kind.OBJECT,
fields: [{
kind: Kind.OBJECT_FIELD,
name: {kind: Kind.NAME, value: filter.operator},
value: filterValue,
}],
},
};
// Find or create the where argument for the top-level field if (topLevel) {
let whereArg = node.selectionSet.selections applyTopLevelFilter(node, fieldPath, filterField);
.find(selection => selection.name.value === fieldPath[0]) } else {
?.arguments.find(arg => arg.name.value === 'where'); applyNestedFilter(node, fieldPath, filterField);
}
});
}
}
});
}
if (!whereArg) { /**
whereArg = { * Create a filter value based on the filter
kind: Kind.ARGUMENT, * @param filter
name: {kind: Kind.NAME, value: 'where'}, * @returns {{kind: (Kind|Kind.INT), value}|{kind: Kind.LIST, values: *}}
value: {kind: Kind.OBJECT, fields: []}, */
}; function createFilterValue(filter) {
const topLevelSelection = node.selectionSet.selections.find(selection => if (Array.isArray(filter.value)) {
selection.name.value === fieldPath[0] // If it's an array, create a list value with the array items
); return {
if (topLevelSelection) { kind: Kind.LIST,
topLevelSelection.arguments = topLevelSelection.arguments || []; values: filter.value.map(item => ({
topLevelSelection.arguments.push(whereArg); kind: getGraphQLKind(item),
} value: item,
} })),
};
} else {
// If it's not an array, use the existing logic
return {
kind: getGraphQLKind(filter.value),
value: filter.value,
};
}
}
// Correctly position the nested filter without an extra 'where' /**
if (fieldPath.length > 2) { // More than one level deep * Create a filter field based on the target field and filter
let currentField = whereArg.value; * @param targetFieldName
fieldPath.slice(1, -1).forEach((path, index) => { * @param filter
let existingField = currentField.fields.find(f => f.name.value === path); * @param filterValue
if (!existingField) { * @returns {{kind: Kind.OBJECT_FIELD, name: {kind: Kind.NAME, value}, value: {kind: Kind.OBJECT, fields: [{kind: Kind.OBJECT_FIELD, name: {kind: Kind.NAME, value}, value}]}}}
existingField = { */
kind: Kind.OBJECT_FIELD, function createFilterField(targetFieldName, filter, filterValue) {
name: {kind: Kind.NAME, value: path}, return {
value: {kind: Kind.OBJECT, fields: []} kind: Kind.OBJECT_FIELD,
}; name: {kind: Kind.NAME, value: targetFieldName},
currentField.fields.push(existingField); value: {
} kind: Kind.OBJECT,
currentField = existingField.value; fields: [{
}); kind: Kind.OBJECT_FIELD,
currentField.fields.push(nestedFilter); name: {kind: Kind.NAME, value: filter.operator},
} else { // Directly under the top level value: filterValue,
whereArg.value.fields.push(nestedFilter); }],
} },
} else { };
// Initialize a reference to the current selection to traverse down the AST }
let currentSelection = node;
let whereArgFound = false;
// Iterate over the fieldPath, except for the last entry, to navigate the structure /**
for (let i = 0; i < fieldPath.length - 1; i++) { * Apply a top-level filter to the AST
const fieldName = fieldPath[i]; * @param node
let fieldFound = false; * @param fieldPath
* @param filterField
*/
function applyTopLevelFilter(node, fieldPath, filterField) {
// Find or create the where argument for the top-level field
let whereArg = node.selectionSet.selections
.find(selection => selection.name.value === fieldPath[0])
?.arguments.find(arg => arg.name.value === 'where');
// Check if the current selection has a selectionSet and selections if (!whereArg) {
if (currentSelection.selectionSet && currentSelection.selectionSet.selections) { whereArg = {
// Look for the field in the current selection's selections kind: Kind.ARGUMENT,
const selection = currentSelection.selectionSet.selections.find(sel => sel.name.value === fieldName); name: {kind: Kind.NAME, value: 'where'},
if (selection) { value: {kind: Kind.OBJECT, fields: []},
// Move down the AST to the found selection };
currentSelection = selection; const topLevelSelection = node.selectionSet.selections.find(selection =>
fieldFound = true; selection.name.value === fieldPath[0]
} );
} if (topLevelSelection) {
topLevelSelection.arguments = topLevelSelection.arguments || [];
topLevelSelection.arguments.push(whereArg);
}
}
// If the field was not found in the current path, it's an issue // Correctly position the nested filter without an extra 'where'
if (!fieldFound) { if (fieldPath.length > 2) { // More than one level deep
console.error(`Field ${fieldName} not found in the current selection.`); let currentField = whereArg.value;
return; // Exit the loop and function due to error fieldPath.slice(1, -1).forEach((path, index) => {
} let existingField = currentField.fields.find(f => f.name.value === path);
} if (!existingField) {
existingField = {
// At this point, currentSelection should be the parent field where the filter needs to be applied kind: Kind.OBJECT_FIELD,
// Check if the 'where' argument already exists in the current selection name: {kind: Kind.NAME, value: path},
const whereArg = currentSelection.arguments.find(arg => arg.name.value === 'where'); value: {kind: Kind.OBJECT, fields: []}
if (whereArg) { };
whereArgFound = true; currentField.fields.push(existingField);
} else { }
// If not found, create a new 'where' argument for the current selection currentField = existingField.value;
currentSelection.arguments.push({
kind: Kind.ARGUMENT,
name: {kind: Kind.NAME, value: 'where'},
value: {kind: Kind.OBJECT, fields: []} // Empty fields array to be populated with the filter
});
}
// Assuming the last entry in fieldPath is the field to apply the filter on
const targetField = fieldPath[fieldPath.length - 1];
const filterValue = {
kind: getGraphQLKind(filter.value),
value: filter.value,
};
// Construct the filter field object
const filterField = {
kind: Kind.OBJECT_FIELD,
name: {kind: Kind.NAME, value: targetField},
value: {
kind: Kind.OBJECT,
fields: [{
kind: Kind.OBJECT_FIELD,
name: {kind: Kind.NAME, value: filter.operator},
value: filterValue,
}],
},
};
// Add the filter field to the 'where' clause of the current selection
if (whereArgFound) {
whereArg.value.fields.push(filterField);
} else {
// If the whereArg was newly created, find it again (since we didn't store its reference) and add the filter
currentSelection.arguments.find(arg => arg.name.value === 'where').value.fields.push(filterField);
}
}
});
}
}
}); });
currentField.fields.push(filterField);
} else { // Directly under the top level
whereArg.value.fields.push(filterField);
}
}
/**
* Apply a nested filter to the AST
* @param node
* @param fieldPath
* @param filterField
*/
function applyNestedFilter(node, fieldPath, filterField) {
// Initialize a reference to the current selection to traverse down the AST
let currentSelection = node;
// Iterate over the fieldPath, except for the last entry, to navigate the structure
for (let i = 0; i < fieldPath.length - 1; i++) {
const fieldName = fieldPath[i];
let fieldFound = false;
// Check if the current selection has a selectionSet and selections
if (currentSelection.selectionSet && currentSelection.selectionSet.selections) {
// Look for the field in the current selection's selections
const selection = currentSelection.selectionSet.selections.find(sel => sel.name.value === fieldName);
if (selection) {
// Move down the AST to the found selection
currentSelection = selection;
fieldFound = true;
}
}
// If the field was not found in the current path, it's an issue
if (!fieldFound) {
console.error(`Field ${fieldName} not found in the current selection.`);
return; // Exit the loop and function due to error
}
}
// At this point, currentSelection should be the parent field where the filter needs to be applied
// Check if the 'where' argument already exists in the current selection
const whereArg = currentSelection.arguments.find(arg => arg.name.value === 'where');
if (!whereArg) {
// If not found, create a new 'where' argument for the current selection
currentSelection.arguments.push({
kind: Kind.ARGUMENT,
name: {kind: Kind.NAME, value: 'where'},
value: {kind: Kind.OBJECT, fields: []} // Empty fields array to be populated with the filter
});
}
// Add the filter field to the 'where' clause of the current selection
currentSelection.arguments.find(arg => arg.name.value === 'where').value.fields.push(filterField);
} }
/** /**
@@ -333,14 +372,17 @@ export function applyFilters(ast, filters) {
* @returns {Kind|Kind.INT} * @returns {Kind|Kind.INT}
*/ */
function getGraphQLKind(value) { function getGraphQLKind(value) {
if (typeof value === 'number') { if (Array.isArray(value)) {
return value % 1 === 0 ? Kind.INT : Kind.FLOAT; return Kind.LIST;
} else if (typeof value === 'boolean') { } else if (typeof value === 'number') {
return Kind.BOOLEAN; return value % 1 === 0 ? Kind.INT : Kind.FLOAT;
} else if (typeof value === 'string') { } else if (typeof value === 'boolean') {
return Kind.STRING; return Kind.BOOLEAN;
} } else if (typeof value === 'string') {
// Extend with more types as needed return Kind.STRING;
} else if (value instanceof Date) {
return Kind.STRING; // GraphQL does not have a Date type, so we return it as a string
}
} }
/** /**
@@ -370,4 +412,4 @@ export function wrapFiltersInAnd(ast, filterFields) {
}); });
} }
/* eslint-enable no-loop-func */ /* eslint-enable no-loop-func */