diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e540f91..6dc135e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * [UIPQB-210](https://folio-org.atlassian.net/browse/UIPQB-210) Localize operators, boolean operators, and boolean values in the query builder and user-friendly query * [UIPQB-296](https://folio-org.atlassian.net/browse/UIPQB-296) Adjust color contrast of queryArea to comply with WCAG AA standard (4.5:1). * [UIPQB-277](https://folio-org.atlassian.net/browse/UIPQB-277) Define supported operators for marcDataType +* [UIPQB-287](https://folio-org.atlassian.net/browse/UIPQB-287) Support querying dynamic MARC fields ## [3.0.2](https://github.com/folio-org/ui-plugin-query-builder/tree/v3.0.2) (2026-06-03) * [UIPQB-279](https://folio-org.atlassian.net/browse/UIPQB-279) Add support for fields containing a valueSourceApi property without a source property diff --git a/src/QueryBuilder/QueryBuilder/QueryBuilderModal/MarcFieldControl/MarcFieldControl.js b/src/QueryBuilder/QueryBuilder/QueryBuilderModal/MarcFieldControl/MarcFieldControl.js new file mode 100644 index 00000000..31027ab6 --- /dev/null +++ b/src/QueryBuilder/QueryBuilder/QueryBuilderModal/MarcFieldControl/MarcFieldControl.js @@ -0,0 +1,140 @@ +import { useState } from 'react'; +import PropTypes from 'prop-types'; +import { FormattedMessage, useIntl } from 'react-intl'; +import { Select, TextField } from '@folio/stripes/components'; + +import { assembleMarcFieldName, parseMarcFieldName, isControlFieldTag, MARC_TARGETS } from '../../helpers/marcFields'; +import { getMarcIndicatorValueOptions } from '../../helpers/selectOptions'; + +// Field-cell control for a MARC condition. The user picks a tag and what to query ("search on": a subfield, an +// indicator, or the whole tag) — that's the target, whose value goes in the row's value box. Any remaining +// indicator can be pinned to a single fixed value (a filter). The control owns its draft state and emits the +// canonical field name (or '' while incomplete) plus the chosen target, so the row can attach the right operator +// set. +const toDraft = (fieldName) => { + const parsed = parseMarcFieldName(fieldName); + + if (parsed) { + return { + tag: parsed.tag, + target: parsed.target, + subfield: parsed.subfield ?? '', + ind1: parsed.ind1 ?? '', + ind2: parsed.ind2 ?? '', + }; + } + + return { tag: '', target: MARC_TARGETS.SUBFIELD, subfield: '', ind1: '', ind2: '' }; +}; + +export const MarcFieldControl = ({ sourcePrefix, value, onFieldChange, index }) => { + const intl = useIntl(); + const [draft, setDraft] = useState(() => toDraft(value)); + + const update = (patch) => { + const next = { ...draft, ...patch }; + + // Control fields (00X) have no indicators or subfields — only the whole tag can be queried. Force the target + // so the user can't build an invalid field like marc_008_a. + if (isControlFieldTag(next.tag)) { + next.target = MARC_TARGETS.TAG; + } + + setDraft(next); + onFieldChange( + assembleMarcFieldName({ + sourcePrefix, + tag: next.tag, + target: next.target, + subfield: next.subfield, + ind1: next.ind1, + ind2: next.ind2, + }) ?? '', + next.target, + ); + }; + + const wholeTagOption = { + value: MARC_TARGETS.TAG, + label: intl.formatMessage({ id: 'ui-plugin-query-builder.marc.target.wholeTag' }), + }; + // Control fields can only be queried as a whole; data fields (010+) offer subfield/indicator targets too. + const targetOptions = isControlFieldTag(draft.tag) + ? [wholeTagOption] + : [ + { value: MARC_TARGETS.SUBFIELD, label: intl.formatMessage({ id: 'ui-plugin-query-builder.marc.target.subfield' }) }, + { value: MARC_TARGETS.IND1, label: intl.formatMessage({ id: 'ui-plugin-query-builder.marc.target.ind1' }) }, + { value: MARC_TARGETS.IND2, label: intl.formatMessage({ id: 'ui-plugin-query-builder.marc.target.ind2' }) }, + wholeTagOption, + ]; + + // Indicator constraint inputs appear for any indicator that is neither the target nor irrelevant (whole tag). + const showInd1Filter = draft.target === MARC_TARGETS.SUBFIELD || draft.target === MARC_TARGETS.IND2; + const showInd2Filter = draft.target === MARC_TARGETS.SUBFIELD || draft.target === MARC_TARGETS.IND1; + + // A pinned indicator holds exactly one value; "Any" (value '') means no constraint on that indicator. + const indicatorConstraintOptions = [ + { value: '', label: intl.formatMessage({ id: 'ui-plugin-query-builder.marc.indicator.any' }) }, + ...getMarcIndicatorValueOptions(intl), + ]; + + return ( +
+ } + value={draft.tag} + onChange={(e) => update({ tag: e.target.value.trim() })} + maxLength={3} + data-testid={`marc-tag-${index}`} + /> + } + dataOptions={indicatorConstraintOptions} + value={draft.ind1} + onChange={(e) => update({ ind1: e.target.value })} + data-testid={`marc-ind1-${index}`} + /> + )} + {showInd2Filter && ( +