Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
77652d6
feat: Add authorities search functionality and related configurations
SKarolFolio Jun 24, 2026
edf983f
feat: Implement authorities search functionality with result formatti…
SKarolFolio Jun 24, 2026
492030f
feat: Refactor authorities search configurations and introduce new se…
SKarolFolio Jun 25, 2026
a69f217
feat: Add AUTHORITY_RESOURCE_TYPE to BFLITE_URIS and update search ac…
SKarolFolio Jun 25, 2026
4177a2b
feat: Enhance authority resource type handling in profile selection a…
SKarolFolio Jun 25, 2026
94a5856
feat: Improve source handling in search controls by defaulting to seg…
SKarolFolio Jun 25, 2026
0ac60a1
feat: Introduce AuthoritiesSourceEnricher and authoritySourceService …
SKarolFolio Jun 25, 2026
f51c664
feat: Refactor authorities searchable indices for improved maintainab…
SKarolFolio Jun 26, 2026
a6399cd
feat: Enhance authority result formatting and add marigold authoritie…
SKarolFolio Jun 26, 2026
18b2e8b
feat: Refactor authority identifiers handling and update related conf…
SKarolFolio Jun 29, 2026
1222bbe
feat: Update AuthorityLabelFormatter to use Button component for titl…
SKarolFolio Jun 29, 2026
08133a7
feat: Add unit tests for LDAuthoritiesRequestBuilder, LDAuthoritiesRe…
SKarolFolio Jun 29, 2026
8231fdc
feat: Add unit tests for AuthoritiesPageResultList, AuthorityActionFo…
SKarolFolio Jun 29, 2026
62120c2
feat: Add unit tests for authoritySourceService, covering getSourceMa…
SKarolFolio Jun 29, 2026
c8a829c
feat: Update AuthorityLabelFormatter to include onTitleClick handler …
SKarolFolio Jun 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@
* Refactor imports and add import boundary rules. Refs [UILD-816].
* Fix regression issues that appeared after refactoring. Refs [UILD-816], [UILD-827].
* Add Authority edit page. Refs [UILD-826].
* Extend Search page with Authority search and create options. Refs [UILD-825].

[UILD-744]:https://folio-org.atlassian.net/browse/UILD-744
[UILD-816]:https://folio-org.atlassian.net/browse/UILD-816
[UILD-821]:https://folio-org.atlassian.net/browse/UILD-821
[UILD-827]:https://folio-org.atlassian.net/browse/UILD-827
[UILD-826]:https://folio-org.atlassian.net/browse/UILD-826
[UILD-825]:https://folio-org.atlassian.net/browse/UILD-825

## 2.0.4 (2026-06-03)
* Fix default profile type persistence across edit form and profile settings. Fixes [UILD-820].
Expand Down
1 change: 1 addition & 0 deletions src/common/constants/api.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const SEARCH_API_ENDPOINT = {
RESOURCES: '/search/linked-data/works',
AUTHORITIES_SEARCH: '/search/authorities',
AUTHORITIES_BROWSE: '/browse/authorities',
AUTHORITIES_LOCAL: '/search/linked-data/authorities',
HUBS_EXTERNAL: 'https://id.loc.gov/resources/hubs/suggest2/',
HUBS_LOCAL: '/search/linked-data/hubs',
};
Expand Down
1 change: 1 addition & 0 deletions src/common/constants/bibframe.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export const TYPE_URIS = {
WORK: BFLITE_URIS.WORK,
HUB: BFLITE_URIS.HUB,
AUTHORITY: BFLITE_URIS.AUTHORITY,
AUTHORITY_RESOURCE_TYPE: BFLITE_URIS.AUTHORITY_RESOURCE_TYPE,
};

export const CONSTRAINTS: Constraints = {
Expand Down
1 change: 1 addition & 0 deletions src/common/constants/bibframeMapping.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export const BFLITE_URIS = {
WORK: 'http://bibfra.me/vocab/lite/Work',
HUB: 'http://bibfra.me/vocab/lite/Hub',
AUTHORITY: '_authority',
AUTHORITY_RESOURCE_TYPE: 'http://bibfra.me/vocab/lite/Authority',
NAME: 'http://bibfra.me/vocab/lite/name',
LABEL: 'http://bibfra.me/vocab/lite/label',
LINK: 'http://bibfra.me/vocab/lite/link',
Expand Down
11 changes: 11 additions & 0 deletions src/common/constants/searchableIndex.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,17 @@ export enum SearchableIndex {
HubNameKeyword = 'hubNameKeyword',
Title = 'title',
Contributor = 'contributor',
// LD Authority / combined authority SearchBy options
Concept = 'concept',
Family = 'family',
Form = 'form',
Jurisdiction = 'jurisdiction',
Meeting = 'meeting',
Organization = 'organization',
Person = 'person',
Place = 'place',
Temporal = 'temporal',
Topic = 'topic',
}

export enum SearchableIndexQuerySelector {
Expand Down
54 changes: 29 additions & 25 deletions src/common/helpers/profileSelection.helper.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,36 @@
import { TYPE_URIS } from '@/common/constants/bibframe.constants';
import { profileWarningsByName } from '@/configs';
import { getProfileLabelId, getResourceTypeFromURL } from '@/configs/resourceTypes';

export const getLabelId = ({
labels: { workSet, instanceSet, workChange, instanceChange, defaultLabel },
profileSelectionType: { action, resourceTypeURL },
}: {
labels: {
workSet?: string;
instanceSet?: string;
workChange?: string;
instanceChange?: string;
defaultLabel: string;
};
profileSelectionType: ProfileSelectionType;
}) => {
const isWorkResourceType = resourceTypeURL === TYPE_URIS.WORK;
let labelId;
/**
* Generic, type-agnostic translation ids for the profile-selection modal.
* Each message uses a `{type}` placeholder filled with the resource-type name,
* so adding a new resource type requires no new label keys here.
*/
export const PROFILE_SELECTION_LABEL_IDS = {
titleSet: 'ld.newType',
titleChange: 'ld.changeTypeProfile',
select: 'ld.modal.chooseResourceProfile.typeProfile',
setAsDefault: 'ld.setDefaultTypeProfile',
submitSet: 'ld.create.base',
submitChange: 'ld.change',
} as const;

if (action === 'set') {
labelId = isWorkResourceType ? workSet : instanceSet;
} else if (action === 'change') {
labelId = isWorkResourceType ? workChange : instanceChange;
} else {
labelId = defaultLabel;
}
/**
* Returns the translation id for the human-readable resource-type name
* (e.g. 'ld.work', 'ld.authority') resolved from a resource-type URL. Used to
* fill the `{type}` placeholder in profile-selection labels.
*/
export const getResourceTypeLabelId = (resourceTypeURL?: ResourceTypeURL): string =>
getProfileLabelId(getResourceTypeFromURL(resourceTypeURL ?? null));

return labelId;
};
/**
* Resolves the modal title and submit-button translation ids for a given
* profile-selection action.
*/
export const getProfileSelectionMessageIds = ({ action }: ProfileSelectionType) => ({
titleId: action === 'set' ? PROFILE_SELECTION_LABEL_IDS.titleSet : PROFILE_SELECTION_LABEL_IDS.titleChange,
submitId: action === 'set' ? PROFILE_SELECTION_LABEL_IDS.submitSet : PROFILE_SELECTION_LABEL_IDS.submitChange,
});

export const getWarningByProfileNames = (
resourceTypeURL: ResourceTypeURL,
Expand Down
3 changes: 2 additions & 1 deletion src/configs/resourceTypes/resourceType.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export const RESOURCE_TYPE_REGISTRY: ResourceTypeRegistry = {
[ResourceType.authority]: {
type: ResourceType.authority,
uri: BFLITE_URIS.AUTHORITY,
resourceTypeUri: BFLITE_URIS.AUTHORITY_RESOURCE_TYPE,
profileBfid: PROFILE_BFIDS.AUTHORITY,
defaultProfileId: 8,
profileChildren: ['Profile:Authority'],
Expand All @@ -81,7 +82,7 @@ export const RESOURCE_TYPE_REGISTRY: ResourceTypeRegistry = {
hasPreview: false,
editPageLayout: 'single',
},
labelId: 'ld.hub',
labelId: 'ld.authority',
search: {
segment: 'authorities',
},
Expand Down
8 changes: 8 additions & 0 deletions src/configs/resourceTypes/resourceType.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ export interface ResourceTypeSearchConfig {
export interface ResourceTypeDefinition {
type: ResourceType;
uri: string;
/**
* Canonical resource-type URI used to identify the resource type for profile
* selection and profile fetching. Defaults to `uri` when omitted. This is kept
* separate from `uri` because some types (e.g. authority) use an internal token
* (`_authority`) for record processing/generation while the real BIBFRAME URI is
* used to define the resource type.
*/
resourceTypeUri?: string;
profileBfid: string;
defaultProfileId: number;
profileChildren: string[];
Expand Down
12 changes: 12 additions & 0 deletions src/configs/resourceTypes/utils/resourceType.accessors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,18 @@ describe('resourceType.accessors', () => {
expect(result).toBe(ResourceType.hub);
});

it('Returns authority type for the canonical authority resource-type URL', () => {
const result = getResourceTypeFromURL(BFLITE_URIS.AUTHORITY_RESOURCE_TYPE as ResourceTypeURL);

expect(result).toBe(ResourceType.authority);
});

it('Returns authority type for the internal authority record token', () => {
const result = getResourceTypeFromURL(BFLITE_URIS.AUTHORITY as ResourceTypeURL);

expect(result).toBe(ResourceType.authority);
});

it('Falls back to instance for null', () => {
const result = getResourceTypeFromURL(null);

Expand Down
7 changes: 5 additions & 2 deletions src/configs/resourceTypes/utils/resourceType.accessors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,15 @@ export const getResourceTypeConfig = (type: ResourceTypeInput): ResourceTypeDefi

/**
* Get the ResourceType from a type URL.
* Falls back to instance type if the type URL is not found in the registry.
* Matches against both the canonical `resourceTypeUri` (used for profile selection)
* and the record-processing `uri`. Falls back to instance type if not found.
*/
export const getResourceTypeFromURL = (typeURL: ResourceTypeURLInput): ResourceType => {
if (typeURL) {
for (const type in RESOURCE_TYPE_REGISTRY) {
if (RESOURCE_TYPE_REGISTRY[type as ResourceType].uri === typeURL) {
const config = RESOURCE_TYPE_REGISTRY[type as ResourceType];

if (config.uri === typeURL || config.resourceTypeUri === typeURL) {
return type as ResourceType;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@ describe('resourceType.mappers', () => {
expect(mapUriToResourceType(uri)).toBe(ResourceType.instance);
});

it('Maps the canonical authority URI to ResourceType.authority', () => {
expect(mapUriToResourceType('http://bibfra.me/vocab/lite/Authority')).toBe(ResourceType.authority);
});

it('Maps the internal authority record token to ResourceType.authority', () => {
expect(mapUriToResourceType('_authority')).toBe(ResourceType.authority);
});

it('Returns undefined for null', () => {
expect(mapUriToResourceType(null)).toBeUndefined();
});
Expand Down
4 changes: 4 additions & 0 deletions src/configs/resourceTypes/utils/resourceType.mappers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ const URI_TO_RESOURCE_TYPE_MAP: Record<string, ResourceType> = Object.values(RES
(acc, config) => {
acc[config.uri] = config.type;

if (config.resourceTypeUri) {
acc[config.resourceTypeUri] = config.type;
}

return acc;
},
{} as Record<string, ResourceType>,
Expand Down

This file was deleted.

5 changes: 0 additions & 5 deletions src/features/complexLookup/configs/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,2 @@
export { COMPLEX_LOOKUP_FILTERS_CONFIG } from './complexLookupFilters.config';
export {
COMPLEX_LOOKUP_SEARCHABLE_INDICES_MAP,
HUB_SEARCHABLE_INDICES_MAP,
HUB_LOCAL_SEARCHABLE_INDICES_MAP,
} from './complexLookupSeachableIndicesMap';
export { COMPLEX_LOOKUP_SEARCH_BY_CONFIG } from './complexLookupSearchBy.config';
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { FC, memo, useEffect, useState } from 'react';
import { FormattedMessage, useIntl } from 'react-intl';

import { getLabelId, isProfilePreferred } from '@/common/helpers/profileSelection.helper';
import {
PROFILE_SELECTION_LABEL_IDS,
getProfileSelectionMessageIds,
getResourceTypeLabelId,
isProfilePreferred,
} from '@/common/helpers/profileSelection.helper';
import { Modal } from '@/components/Modal';
import { Select, SelectValue } from '@/components/Select';

Expand Down Expand Up @@ -84,43 +89,13 @@ export const ModalChooseProfile: FC<ModalChooseProfileProps> = memo(
}) as SelectValue[];
};

const title = formatMessage({
id: getLabelId({
labels: {
workChange: 'ld.changeWorkProfile',
instanceChange: 'ld.changeInstanceProfile',
workSet: 'ld.newWork',
instanceSet: 'ld.newInstance',
defaultLabel: 'ld.newInstance',
},
profileSelectionType,
}),
});
const labelSelect = formatMessage({
id: getLabelId({
labels: {
workChange: 'ld.modal.chooseResourceProfile.workProfile',
instanceChange: 'ld.modal.chooseResourceProfile.instanceProfile',
workSet: 'ld.modal.chooseResourceProfile.workProfile',
instanceSet: 'ld.modal.chooseResourceProfile.instanceProfile',
defaultLabel: 'ld.resourceProfile',
},
profileSelectionType,
}),
});
const labelSetAsDefault = formatMessage({
id: getLabelId({
labels: {
workChange: 'ld.modal.chooseResourceProfile.setDefaultWorkProfile',
instanceChange: 'ld.modal.chooseResourceProfile.setDefaultInstanceProfile',
workSet: 'ld.modal.chooseResourceProfile.setDefaultWorkProfile',
instanceSet: 'ld.modal.chooseResourceProfile.setDefaultInstanceProfile',
defaultLabel: 'ld.modal.chooseResourceProfile.setAsDefault',
},
profileSelectionType,
}),
});
const labelSubmit = formatMessage({ id: profileSelectionType.action === 'set' ? 'ld.create.base' : 'ld.change' });
const typeLabel = formatMessage({ id: getResourceTypeLabelId(resourceTypeURL) });
const { titleId, submitId } = getProfileSelectionMessageIds(profileSelectionType);

const title = formatMessage({ id: titleId }, { type: typeLabel });
const labelSelect = formatMessage({ id: PROFILE_SELECTION_LABEL_IDS.select }, { type: typeLabel });
const labelSetAsDefault = formatMessage({ id: PROFILE_SELECTION_LABEL_IDS.setAsDefault }, { type: typeLabel });
const labelSubmit = formatMessage({ id: submitId });

return (
<Modal
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { SearchableIndex as SearchableIndexEnum } from '@/common/constants/searchableIndex.constants';
import { COMPLEX_LOOKUP_SEARCHABLE_INDICES_MAP } from '@/common/constants/searchableIndices.constants';

import { COMPLEX_LOOKUP_SEARCHABLE_INDICES_MAP } from '../constants';
import { AuthoritiesBrowseRequestBuilder } from '../strategies/requestBuilders';
import { AuthoritiesBrowseResponseTransformer } from '../strategies/responseTransformers';
import { AuthoritiesResultFormatter } from '../strategies/resultFormatters';
Expand Down
29 changes: 29 additions & 0 deletions src/features/search/core/config/authoritiesLD.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { SearchableIndex as SearchableIndexEnum } from '@/common/constants/searchableIndex.constants';

import { LD_AUTHORITIES_SEARCHABLE_INDICES_MAP } from '../constants';
import { LDAuthoritiesRequestBuilder } from '../strategies/requestBuilders';
import { LDAuthoritiesResponseTransformer } from '../strategies/responseTransformers';
import { LDAuthoritiesResultFormatter } from '../strategies/resultFormatters';
import type { SearchTypeConfig } from '../types';

export const authoritiesLDConfig: SearchTypeConfig = {
id: 'authorities:ld',

strategies: {
requestBuilder: new LDAuthoritiesRequestBuilder(LD_AUTHORITIES_SEARCHABLE_INDICES_MAP),
responseTransformer: new LDAuthoritiesResponseTransformer(),
resultFormatter: new LDAuthoritiesResultFormatter(),
},

capabilities: {
defaultLimit: 100,
maxLimit: 100,
},

defaults: {
searchBy: SearchableIndexEnum.Keyword,
query: '',
limit: 100,
offset: 0,
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { SearchableIndex as SearchableIndexEnum } from '@/common/constants/searchableIndex.constants';

import { COMPLEX_LOOKUP_SEARCHABLE_INDICES_MAP } from '../constants';
import { AuthoritiesSearchRequestBuilder } from '../strategies/requestBuilders';
import { AuthoritiesSearchResponseTransformer } from '../strategies/responseTransformers';
import { AuthoritiesSourceEnricher } from '../strategies/resultEnrichers';
import { MarcAuthoritiesPageResultFormatter } from '../strategies/resultFormatters';
import type { SearchTypeConfig } from '../types';

export const authoritiesLocChildrenConfig: SearchTypeConfig = {
id: 'authorities:locChildren',

strategies: {
requestBuilder: new AuthoritiesSearchRequestBuilder(COMPLEX_LOOKUP_SEARCHABLE_INDICES_MAP),
responseTransformer: new AuthoritiesSearchResponseTransformer(),
resultFormatter: new MarcAuthoritiesPageResultFormatter(),
resultEnricher: new AuthoritiesSourceEnricher(),
},

capabilities: {
defaultLimit: 100,
maxLimit: 100,
},

defaults: {
searchBy: SearchableIndexEnum.ChildrenSubjectHeading,
query: '',
limit: 100,
offset: 0,
},
};
31 changes: 31 additions & 0 deletions src/features/search/core/config/authoritiesMarc.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { SearchableIndex as SearchableIndexEnum } from '@/common/constants/searchableIndex.constants';

import { COMPLEX_LOOKUP_SEARCHABLE_INDICES_MAP } from '../constants';
import { AuthoritiesSearchRequestBuilder } from '../strategies/requestBuilders';
import { AuthoritiesSearchResponseTransformer } from '../strategies/responseTransformers';
import { AuthoritiesSourceEnricher } from '../strategies/resultEnrichers';
import { MarcAuthoritiesPageResultFormatter } from '../strategies/resultFormatters';
import type { SearchTypeConfig } from '../types';

export const authoritiesMarcConfig: SearchTypeConfig = {
id: 'authorities:marc',

strategies: {
requestBuilder: new AuthoritiesSearchRequestBuilder(COMPLEX_LOOKUP_SEARCHABLE_INDICES_MAP),
responseTransformer: new AuthoritiesSearchResponseTransformer(),
resultFormatter: new MarcAuthoritiesPageResultFormatter(),
resultEnricher: new AuthoritiesSourceEnricher(),
},

capabilities: {
defaultLimit: 100,
maxLimit: 100,
},

defaults: {
searchBy: SearchableIndexEnum.Keyword,
query: '',
limit: 100,
offset: 0,
},
};
Loading
Loading