From 32bba8060a549181dd39eb9996db14cfa4d5be42 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Wed, 28 Feb 2024 17:17:17 -0500 Subject: [PATCH] - Progress Commit Signed-off-by: Dave Richer --- ...center-modal-filters-sorters-component.jsx | 77 +++- client/src/utils/graphQLmodifier.js | 374 ++++++++++-------- 2 files changed, 276 insertions(+), 175 deletions(-) diff --git a/client/src/components/report-center-modal/report-center-modal-filters-sorters-component.jsx b/client/src/components/report-center-modal/report-center-modal-filters-sorters-component.jsx index 1fc7a4700..7c7b3cc98 100644 --- a/client/src/components/report-center-modal/report-center-modal-filters-sorters-component.jsx +++ b/client/src/components/report-center-modal/report-center-modal-filters-sorters-component.jsx @@ -6,6 +6,7 @@ import {useTranslation} from "react-i18next"; import {getOrderOperatorsByType, getWhereOperatorsByType} from "../../utils/graphQLmodifier"; import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component"; import {generateInternalReflections} from "./report-center-modal-utils"; +import {FormDatePicker} from "../form-date-picker/form-date-picker.component.jsx"; export default function ReportCenterModalFiltersSortersComponent({form, bodyshop}) { @@ -71,8 +72,9 @@ function FiltersSection({filters, form, bodyshop}) { - + { () => { const name = form.getFieldValue(['filters', field.name, "field"]); @@ -82,7 +84,6 @@ function FiltersSection({filters, form, bodyshop}) { key={`${index}operator`} label={t('reportcenter.labels.advanced_filters_filter_operator')} name={[field.name, "operator"]} - dependencies={[]} rules={[ { required: true, @@ -92,7 +93,13 @@ function FiltersSection({filters, form, bodyshop}) { > trigger.parentNode} + onChange={(value) => { + form.setFieldValue(fieldPath, value); + }} + /> + ); + } return ( trigger.parentNode} + options={[ + { + label: "True", + value: true + }, + { + label: "False", + value: false + } + ]} + onChange={(value) => form.setFieldValue(fieldPath, value)} + /> + ); + } + return ( form.setFieldValue(fieldPath, e.target.value)}/> ); @@ -370,4 +429,4 @@ function RenderFilters({templateId, form, bodyshop}) { )} ); -} \ No newline at end of file +} diff --git a/client/src/utils/graphQLmodifier.js b/client/src/utils/graphQLmodifier.js index 77aa14036..96f31f94c 100644 --- a/client/src/utils/graphQLmodifier.js +++ b/client/src/utils/graphQLmodifier.js @@ -2,22 +2,42 @@ import {Kind, parse, print, visit} from "graphql"; import client from "./GraphQLClient"; 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 = [ - {value: "_eq", label: "equals"}, - {value: "_neq", label: "does not equal"}, - {value: "_like", label: "contains"}, - {value: "_nlike", label: "does not contain"}, - {value: "_ilike", label: "contains case-insensitive"}, - {value: "_nilike", label: "does not contain case-insensitive"} + {value: "_eq", label: "equals"}, + {value: "_neq", label: "does not equal"}, + {value: "_like", label: "contains"}, + {value: "_nlike", label: "does not contain"}, + {value: "_ilike", label: "contains 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 = [ - {value: "_eq", label: "equals"}, - {value: "_neq", label: "does not equal"}, - {value: "_gt", label: "greater than"}, - {value: "_lt", label: "less than"}, - {value: "_gte", label: "greater than or equal"}, - {value: "_lte", label: "less than or equal"} + {value: "_eq", label: "equals"}, + {value: "_neq", label: "does not equal"}, + {value: "_gt", label: "greater than"}, + {value: "_lt", label: "less than"}, + {value: "_gte", label: "greater 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 = [ {value: "asc", label: "ascending"}, {value: "desc", label: "descending"} @@ -31,7 +51,6 @@ export function getOrderOperatorsByType() { return ORDER_BY_OPERATORS; } - /** * Get the available operators for filtering * @param type @@ -45,8 +64,6 @@ export function getWhereOperatorsByType(type = 'string') { return operators[type]; } -/* eslint-disable no-loop-func */ - /** * Parse a GraphQL query into an AST * @param query @@ -94,8 +111,8 @@ export async function generateTemplate(templateQueryToExecute, templateObject, u const finalQuery = printQuery(ast); // commented out for future revision debugging - // console.log('Modified Query'); - // console.log(finalQuery); + console.log('Modified Query'); + console.log(finalQuery); let contextData = {}; if (templateQueryToExecute) { @@ -109,7 +126,6 @@ export async function generateTemplate(templateQueryToExecute, templateObject, u return {contextData, useShopSpecificTemplate}; } - /** * Apply sorters to the AST * @param ast @@ -174,157 +190,180 @@ export function applySorters(ast, sorters) { * Apply filters to the AST * @param ast * @param filters + * @returns {ASTNode} */ export function applyFilters(ast, filters) { - return visit(ast, { - OperationDefinition: { - enter(node) { - filters.forEach(filter => { - const fieldPath = filter.field.split('.'); - let topLevel = false; + return visit(ast, { + OperationDefinition: { + enter(node) { + filters.forEach(filter => { + const fieldPath = filter.field.split('.'); + let topLevel = false; - // Determine if the filter should be applied at the top level - if (fieldPath[0].startsWith('[') && fieldPath[0].endsWith(']')) { - fieldPath[0] = fieldPath[0].substring(1, fieldPath[0].length - 1); // Strip the brackets - topLevel = true; - } + // Determine if the filter should be applied at the top level + if (fieldPath[0].startsWith('[') && fieldPath[0].endsWith(']')) { + fieldPath[0] = fieldPath[0].substring(1, fieldPath[0].length - 1); // Strip the brackets + topLevel = true; + } - if (topLevel) { - // Construct the filter for a top-level application - const targetFieldName = fieldPath[fieldPath.length - 1]; - const filterValue = { - kind: getGraphQLKind(filter.value), - value: filter.value, - }; + // Construct the filter for a top-level application + const targetFieldName = fieldPath[fieldPath.length - 1]; - const nestedFilter = { - kind: Kind.OBJECT_FIELD, - name: {kind: Kind.NAME, value: targetFieldName}, - value: { - kind: Kind.OBJECT, - fields: [{ - kind: Kind.OBJECT_FIELD, - name: {kind: Kind.NAME, value: filter.operator}, - value: filterValue, - }], - }, - }; + let filterValue = createFilterValue(filter); + let filterField = createFilterField(targetFieldName, filter, filterValue); - // 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'); + if (topLevel) { + applyTopLevelFilter(node, fieldPath, filterField); + } else { + applyNestedFilter(node, fieldPath, filterField); + } + }); + } + } + }); +} - if (!whereArg) { - whereArg = { - kind: Kind.ARGUMENT, - name: {kind: Kind.NAME, value: 'where'}, - value: {kind: Kind.OBJECT, fields: []}, - }; - const topLevelSelection = node.selectionSet.selections.find(selection => - selection.name.value === fieldPath[0] - ); - if (topLevelSelection) { - topLevelSelection.arguments = topLevelSelection.arguments || []; - topLevelSelection.arguments.push(whereArg); - } - } +/** + * Create a filter value based on the filter + * @param filter + * @returns {{kind: (Kind|Kind.INT), value}|{kind: Kind.LIST, values: *}} + */ +function createFilterValue(filter) { + if (Array.isArray(filter.value)) { + // If it's an array, create a list value with the array items + return { + kind: Kind.LIST, + values: filter.value.map(item => ({ + 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 - let currentField = whereArg.value; - fieldPath.slice(1, -1).forEach((path, index) => { - let existingField = currentField.fields.find(f => f.name.value === path); - if (!existingField) { - existingField = { - kind: Kind.OBJECT_FIELD, - name: {kind: Kind.NAME, value: path}, - value: {kind: Kind.OBJECT, fields: []} - }; - currentField.fields.push(existingField); - } - currentField = existingField.value; - }); - currentField.fields.push(nestedFilter); - } else { // Directly under the top level - whereArg.value.fields.push(nestedFilter); - } - } else { - // Initialize a reference to the current selection to traverse down the AST - let currentSelection = node; - let whereArgFound = false; +/** + * Create a filter field based on the target field and filter + * @param targetFieldName + * @param filter + * @param filterValue + * @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}]}}} + */ +function createFilterField(targetFieldName, filter, filterValue) { + return { + kind: Kind.OBJECT_FIELD, + name: {kind: Kind.NAME, value: targetFieldName}, + value: { + kind: Kind.OBJECT, + fields: [{ + kind: Kind.OBJECT_FIELD, + name: {kind: Kind.NAME, value: filter.operator}, + value: filterValue, + }], + }, + }; +} - // 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; +/** + * Apply a top-level filter to the AST + * @param node + * @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 (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 (!whereArg) { + whereArg = { + kind: Kind.ARGUMENT, + name: {kind: Kind.NAME, value: 'where'}, + value: {kind: Kind.OBJECT, fields: []}, + }; + const topLevelSelection = node.selectionSet.selections.find(selection => + 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 - 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) { - whereArgFound = true; - } else { - // 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 - }); - } - - // 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); - } - } - - }); - } - } + // Correctly position the nested filter without an extra 'where' + if (fieldPath.length > 2) { // More than one level deep + let currentField = whereArg.value; + fieldPath.slice(1, -1).forEach((path, index) => { + let existingField = currentField.fields.find(f => f.name.value === path); + if (!existingField) { + existingField = { + kind: Kind.OBJECT_FIELD, + name: {kind: Kind.NAME, value: path}, + value: {kind: Kind.OBJECT, fields: []} + }; + currentField.fields.push(existingField); + } + currentField = existingField.value; }); + 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} */ function getGraphQLKind(value) { - if (typeof value === 'number') { - return value % 1 === 0 ? Kind.INT : Kind.FLOAT; - } else if (typeof value === 'boolean') { - return Kind.BOOLEAN; - } else if (typeof value === 'string') { - return Kind.STRING; - } - // Extend with more types as needed + if (Array.isArray(value)) { + return Kind.LIST; + } else if (typeof value === 'number') { + return value % 1 === 0 ? Kind.INT : Kind.FLOAT; + } else if (typeof value === 'boolean') { + return Kind.BOOLEAN; + } else if (typeof value === 'string') { + 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 */ \ No newline at end of file +/* eslint-enable no-loop-func */