From 1508bb8f3b100f920b7ed416d1fa744376f17207 Mon Sep 17 00:00:00 2001 From: Uladzislau_Kutarkin Date: Mon, 16 Dec 2024 12:18:07 +0400 Subject: [PATCH 1/4] UIPQB-141: Modal dialog focus inconsistencies across screenreaders --- CHANGELOG.md | 1 + .../QueryBuilderModal/QueryBuilderModal.js | 2 +- .../RepeatableFields/RepeatableFields.js | 11 +++++++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45e76d64..767d4c55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## IN PROGRESS * [UIPQB-168](https://folio-org.atlassian.net/browse/UIPQB-168) Allow editing queries containing no fields. +* [UIPQB-141](https://folio-org.atlassian.net/browse/UIPQB-141) Modal dialog focus inconsistencies across screenreaders. ## [1.2.6](https://github.com/folio-org/ui-plugin-query-builder/tree/v1.2.6) (2024-12-11) diff --git a/src/QueryBuilder/QueryBuilder/QueryBuilderModal/QueryBuilderModal.js b/src/QueryBuilder/QueryBuilder/QueryBuilderModal/QueryBuilderModal.js index d875f8ae..cd608a07 100644 --- a/src/QueryBuilder/QueryBuilder/QueryBuilderModal/QueryBuilderModal.js +++ b/src/QueryBuilder/QueryBuilder/QueryBuilderModal/QueryBuilderModal.js @@ -222,7 +222,7 @@ export const QueryBuilderModal = ({ label={} size="large" contentClass={css.modalClass} - enforceFocus={false} + enforceFocus={true} > diff --git a/src/QueryBuilder/QueryBuilder/QueryBuilderModal/RepeatableFields/RepeatableFields.js b/src/QueryBuilder/QueryBuilder/QueryBuilderModal/RepeatableFields/RepeatableFields.js index 4e9fbdac..add54c17 100644 --- a/src/QueryBuilder/QueryBuilder/QueryBuilderModal/RepeatableFields/RepeatableFields.js +++ b/src/QueryBuilder/QueryBuilder/QueryBuilderModal/RepeatableFields/RepeatableFields.js @@ -6,6 +6,7 @@ import { Selection, Col, Row, + getFirstFocusable } from '@folio/stripes/components'; import PropTypes from 'prop-types'; @@ -42,6 +43,16 @@ export const RepeatableFields = ({ source, setSource, getParamsSource, columns } const filteredFields = source.filter((_, i) => i !== index); setSource(filteredFields); + + const previousRowSelector = `[class^=repeatableFieldItem-]:nth-child(${index})`; + const previousRowElement = document.querySelector(previousRowSelector); + + if (previousRowElement) { + const firstFocusableElement = getFirstFocusable(previousRowElement); + if (firstFocusableElement) { + firstFocusableElement.focus(); + } + } }; const handleChange = (value, index, fieldName) => { From e3f6cb21eb114d256568e161de5197fd46ee39c1 Mon Sep 17 00:00:00 2001 From: Uladzislau_Kutarkin Date: Thu, 19 Dec 2024 02:16:51 +0400 Subject: [PATCH 2/4] UIPQB-162: Errors when query includes a modified custom field --- CHANGELOG.md | 1 + .../RepeatableFields/RepeatableFields.js | 35 +++++++- .../QueryBuilder/helpers/query.js | 17 ++++ .../QueryBuilder/helpers/query.test.js | 90 ++++++++++++++++++- translations/ui-plugin-query-builder/en.json | 2 + 5 files changed, 140 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 767d4c55..8367e68a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * [UIPQB-168](https://folio-org.atlassian.net/browse/UIPQB-168) Allow editing queries containing no fields. * [UIPQB-141](https://folio-org.atlassian.net/browse/UIPQB-141) Modal dialog focus inconsistencies across screenreaders. +* [UIPQB-162](https://folio-org.atlassian.net/browse/UIPQB-162) Errors when query includes a modified custom field. ## [1.2.6](https://github.com/folio-org/ui-plugin-query-builder/tree/v1.2.6) (2024-12-11) diff --git a/src/QueryBuilder/QueryBuilder/QueryBuilderModal/RepeatableFields/RepeatableFields.js b/src/QueryBuilder/QueryBuilder/QueryBuilderModal/RepeatableFields/RepeatableFields.js index add54c17..097ed98f 100644 --- a/src/QueryBuilder/QueryBuilder/QueryBuilderModal/RepeatableFields/RepeatableFields.js +++ b/src/QueryBuilder/QueryBuilder/QueryBuilderModal/RepeatableFields/RepeatableFields.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { memo, useEffect, useRef } from 'react'; import { IconButton, RepeatableField, @@ -8,9 +8,11 @@ import { Row, getFirstFocusable } from '@folio/stripes/components'; +import { useShowCallout } from "@folio/stripes-acq-components"; +import {MESSAGE_TYPES} from "@folio/lists/src/hooks/useMessages/useMessages"; import PropTypes from 'prop-types'; -import { useIntl } from 'react-intl'; +import {FormattedMessage, useIntl} from 'react-intl'; import { QueryBuilderTitle } from '../../QueryBuilderTitle'; import css from '../QueryBuilderModal.css'; import { COLUMN_KEYS } from '../../../../constants/columnKeys'; @@ -23,9 +25,12 @@ import { } from '../../helpers/selectOptions'; import { BOOLEAN_OPERATORS } from '../../../../constants/operators'; import { DataTypeInput } from '../DataTypeInput'; +import {findMissingValues} from "../../helpers/query"; -export const RepeatableFields = ({ source, setSource, getParamsSource, columns }) => { +export const RepeatableFields = memo(({ source, setSource, getParamsSource, columns }) => { const intl = useIntl(); + const callout = useShowCallout(); + const calloutCalledRef = useRef(false); const fieldOptions = getFieldOptions(columns); @@ -84,6 +89,7 @@ export const RepeatableFields = ({ source, setSource, getParamsSource, columns } }, }; } + if (isOperator) { return { [COLUMN_KEYS.VALUE]: { @@ -113,6 +119,27 @@ export const RepeatableFields = ({ source, setSource, getParamsSource, columns } })); }; + useEffect(() => { + if (calloutCalledRef.current) return; + + const deletedFields = findMissingValues(fieldOptions, source) + + if(deletedFields.length >= 1) { + calloutCalledRef.current = true; + + callout({ + type: MESSAGE_TYPES.WARNING, + message: ( + + ), + timeout: 0, + }) + } + }, []); + return ( <> @@ -201,7 +228,7 @@ export const RepeatableFields = ({ source, setSource, getParamsSource, columns } /> ); -}; +}); RepeatableFields.propTypes = { source: PropTypes.arrayOf(PropTypes.object), diff --git a/src/QueryBuilder/QueryBuilder/helpers/query.js b/src/QueryBuilder/QueryBuilder/helpers/query.js index d29cc453..eb15c828 100644 --- a/src/QueryBuilder/QueryBuilder/helpers/query.js +++ b/src/QueryBuilder/QueryBuilder/helpers/query.js @@ -279,3 +279,20 @@ export const mongoQueryToSource = async ({ return [singleItem]; }; + +export const findMissingValues = ( + mainArray, + secondaryArray +) => { + const mainValues = new Set(mainArray?.map((item) => item.value)); + + const missingValues = []; + for (const secondaryItem of secondaryArray) { + const currentValue = secondaryItem.field.current; + if (currentValue && !mainValues.has(currentValue)) { + missingValues.push(currentValue); + } + } + + return missingValues; +} diff --git a/src/QueryBuilder/QueryBuilder/helpers/query.test.js b/src/QueryBuilder/QueryBuilder/helpers/query.test.js index 14510629..eba8db7c 100644 --- a/src/QueryBuilder/QueryBuilder/helpers/query.test.js +++ b/src/QueryBuilder/QueryBuilder/helpers/query.test.js @@ -1,4 +1,4 @@ -import { getTransformedValue, isQueryValid, mongoQueryToSource, sourceToMongoQuery } from './query'; +import {findMissingValues, getTransformedValue, isQueryValid, mongoQueryToSource, sourceToMongoQuery} from './query'; import { booleanOptions } from './selectOptions'; import { OPERATORS } from '../../../constants/operators'; import { fieldOptions } from '../../../../test/jest/data/entityType'; @@ -372,3 +372,91 @@ describe('getTransformedValue', () => { expect(actual).toEqual(expected); }); }); + +describe('findMissingValues', () => { + it('should return missing values from secondaryArray that are not in mainArray', () => { + const mainArray = [ + { value: 'value1' }, + { value: 'value2' }, + { value: 'value3' }, + ]; + + const secondaryArray = [ + { field: { current: 'value2' } }, + { field: { current: 'value4' } }, + { field: { current: 'value5' } }, + ]; + + const result = findMissingValues(mainArray, secondaryArray); + + expect(result).toEqual(['value4', 'value5']); + }); + + it('should return an empty array when all values are present in mainArray', () => { + const mainArray = [ + { value: 'value1' }, + { value: 'value2' }, + ]; + + const secondaryArray = [ + { field: { current: 'value1' } }, + { field: { current: 'value2' } }, + ]; + + const result = findMissingValues(mainArray, secondaryArray); + + expect(result).toEqual([]); + }); + + it('should handle cases where mainArray is empty', () => { + const mainArray = []; + + const secondaryArray = [ + { field: { current: 'value1' } }, + { field: { current: 'value2' } }, + ]; + + const result = findMissingValues(mainArray, secondaryArray); + + expect(result).toEqual(['value1', 'value2']); + }); + + it('should handle cases where secondaryArray is empty', () => { + const mainArray = [ + { value: 'value1' }, + { value: 'value2' }, + ]; + + const secondaryArray = []; + + const result = findMissingValues(mainArray, secondaryArray); + + expect(result).toEqual([]); + }); + + it('should handle cases where both arrays are empty', () => { + const mainArray = []; + const secondaryArray = []; + + const result = findMissingValues(mainArray, secondaryArray); + + expect(result).toEqual([]); + }); + + it('should ignore null or undefined values in secondaryArray', () => { + const mainArray = [ + { value: 'value1' }, + { value: 'value2' }, + ]; + + const secondaryArray = [ + { field: { current: 'value3' } }, + { field: { current: null } }, + { field: { current: undefined } }, + ]; + + const result = findMissingValues(mainArray, secondaryArray); + + expect(result).toEqual(['value3']); + }); +}); diff --git a/translations/ui-plugin-query-builder/en.json b/translations/ui-plugin-query-builder/en.json index e175289c..595c4a87 100644 --- a/translations/ui-plugin-query-builder/en.json +++ b/translations/ui-plugin-query-builder/en.json @@ -35,5 +35,7 @@ "error.sww": "Something went wrong", "error.occurredMessage": "An error occurred.", + "warning.deletedField": "{value} in your query is unavailable. Please revise your query. ", + "ariaLabel.columnFilter": "Column filter input" } From 23488fd7482f59a9271e45ef188ab2de9db62fe2 Mon Sep 17 00:00:00 2001 From: Uladzislau_Kutarkin Date: Thu, 19 Dec 2024 02:22:52 +0400 Subject: [PATCH 3/4] UIPQB-161: fix lint --- .../RepeatableFields/RepeatableFields.js | 25 ++++++++++--------- .../QueryBuilder/helpers/query.js | 8 +++--- .../QueryBuilder/helpers/query.test.js | 2 +- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/QueryBuilder/QueryBuilder/QueryBuilderModal/RepeatableFields/RepeatableFields.js b/src/QueryBuilder/QueryBuilder/QueryBuilderModal/RepeatableFields/RepeatableFields.js index 98c15887..ce9834d4 100644 --- a/src/QueryBuilder/QueryBuilder/QueryBuilderModal/RepeatableFields/RepeatableFields.js +++ b/src/QueryBuilder/QueryBuilder/QueryBuilderModal/RepeatableFields/RepeatableFields.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { memo, useEffect, useRef } from 'react'; import { IconButton, RepeatableField, @@ -8,10 +8,10 @@ import { Row, getFirstFocusable, } from '@folio/stripes/components'; -import { useShowCallout } from "@folio/stripes-acq-components"; +import { useShowCallout } from '@folio/stripes-acq-components'; import PropTypes from 'prop-types'; -import {FormattedMessage, useIntl} from 'react-intl'; +import { FormattedMessage, useIntl } from 'react-intl'; import { QueryBuilderTitle } from '../../QueryBuilderTitle'; import css from '../QueryBuilderModal.css'; import { COLUMN_KEYS } from '../../../../constants/columnKeys'; @@ -24,7 +24,7 @@ import { } from '../../helpers/selectOptions'; import { BOOLEAN_OPERATORS } from '../../../../constants/operators'; import { DataTypeInput } from '../DataTypeInput'; -import {findMissingValues} from "../../helpers/query"; +import { findMissingValues } from '../../helpers/query'; export const RepeatableFields = memo(({ source, setSource, getParamsSource, columns }) => { const intl = useIntl(); @@ -53,6 +53,7 @@ export const RepeatableFields = memo(({ source, setSource, getParamsSource, colu if (previousRowElement) { const firstFocusableElement = getFirstFocusable(previousRowElement); + if (firstFocusableElement) { firstFocusableElement.focus(); } @@ -121,21 +122,21 @@ export const RepeatableFields = memo(({ source, setSource, getParamsSource, colu useEffect(() => { if (calloutCalledRef.current) return; - const deletedFields = findMissingValues(fieldOptions, source) + const deletedFields = findMissingValues(fieldOptions, source); - if(deletedFields.length >= 1) { + if (deletedFields.length >= 1) { calloutCalledRef.current = true; callout({ type: 'warning', message: ( - + ), timeout: 0, - }) + }); } }, []); @@ -227,7 +228,7 @@ export const RepeatableFields = memo(({ source, setSource, getParamsSource, colu /> ); -}; +}); RepeatableFields.propTypes = { source: PropTypes.arrayOf(PropTypes.object), diff --git a/src/QueryBuilder/QueryBuilder/helpers/query.js b/src/QueryBuilder/QueryBuilder/helpers/query.js index eb15c828..21dba0f7 100644 --- a/src/QueryBuilder/QueryBuilder/helpers/query.js +++ b/src/QueryBuilder/QueryBuilder/helpers/query.js @@ -281,18 +281,20 @@ export const mongoQueryToSource = async ({ }; export const findMissingValues = ( - mainArray, - secondaryArray + mainArray, + secondaryArray, ) => { const mainValues = new Set(mainArray?.map((item) => item.value)); const missingValues = []; + for (const secondaryItem of secondaryArray) { const currentValue = secondaryItem.field.current; + if (currentValue && !mainValues.has(currentValue)) { missingValues.push(currentValue); } } return missingValues; -} +}; diff --git a/src/QueryBuilder/QueryBuilder/helpers/query.test.js b/src/QueryBuilder/QueryBuilder/helpers/query.test.js index eba8db7c..0e8a0ef7 100644 --- a/src/QueryBuilder/QueryBuilder/helpers/query.test.js +++ b/src/QueryBuilder/QueryBuilder/helpers/query.test.js @@ -1,4 +1,4 @@ -import {findMissingValues, getTransformedValue, isQueryValid, mongoQueryToSource, sourceToMongoQuery} from './query'; +import { findMissingValues, getTransformedValue, isQueryValid, mongoQueryToSource, sourceToMongoQuery } from './query'; import { booleanOptions } from './selectOptions'; import { OPERATORS } from '../../../constants/operators'; import { fieldOptions } from '../../../../test/jest/data/entityType'; From bc1b1e0c52692dcc9255570d28ba1095feda87f6 Mon Sep 17 00:00:00 2001 From: Uladzislau_Kutarkin Date: Fri, 20 Dec 2024 14:11:01 +0400 Subject: [PATCH 4/4] UIPQB-162: fix comments --- .../QueryBuilderModal/RepeatableFields/RepeatableFields.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/QueryBuilder/QueryBuilder/QueryBuilderModal/RepeatableFields/RepeatableFields.js b/src/QueryBuilder/QueryBuilder/QueryBuilderModal/RepeatableFields/RepeatableFields.js index ce9834d4..21d42476 100644 --- a/src/QueryBuilder/QueryBuilder/QueryBuilderModal/RepeatableFields/RepeatableFields.js +++ b/src/QueryBuilder/QueryBuilder/QueryBuilderModal/RepeatableFields/RepeatableFields.js @@ -132,7 +132,7 @@ export const RepeatableFields = memo(({ source, setSource, getParamsSource, colu message: ( ), timeout: 0,