Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
121 changes: 121 additions & 0 deletions docs/ALTERNATE_TERM_NAMES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# Alternate Term Names

This feature allows consuming applications to provide alternate display names for facet terms in the UI filtering system.

## Overview

The alternate term names system provides a way to display user-friendly names for facet terms while maintaining the original term values for filtering and data operations.

### Use Cases
- **Synonyms**: Display "Human" instead of "Homo sapiens"
- **UI Localization**: Show translated terms in different languages
- **User-friendly phrasing**: Replace technical terms with more accessible language

## Setup

### 1. Add the Provider

Wrap your application with the `AlternateTermNamesProvider` at a high level (typically alongside other providers):

```tsx
import { AlternateTermNamesProvider } from "@databiosphere/findable-ui/lib/providers/alternateTermNames/alternateTermNames";

function App() {
return (
<AlternateTermNamesProvider>
{/* Your app components */}
</AlternateTermNamesProvider>
);
}
```

### 2. Create the Alternate Names File

Create a file at `/public/fe-api/alternateTermNames.json` in your app with the following structure:

```json
{
"facetName": {
"originalTermName": "Alternate Display Name"
}
}
```

**Example:**

```json
{
"species": {
"Homo sapiens": "Human",
"Mus musculus": "Mouse",
"Rattus norvegicus": "Rat"
},
"tissue": {
"cerebral cortex": "Brain Cortex",
"myocardium": "Heart Muscle"
}
}
```

## Behavior

### File Loading
- The file is loaded **once** when the app initializes
- The file is **optional** - if it doesn't exist, the system continues without errors
- If the file is missing or fails to load, facet terms display their original names

### Term Display
- When an alternate name is defined, it will be displayed in facet selection UI
- The original term name is still used for filtering and data operations
- If no alternate name is found for a term, the original name is displayed

### Fallback Behavior
The system is robust to various error conditions:
- **File not found**: No errors, terms display original names
- **Empty file**: Terms display original names
- **Missing facet**: Terms for that facet display original names
- **Missing term**: That specific term displays its original name
- **JSON parse error**: No errors, terms display original names

## Data Flow

1. **Term Creation**: Terms are created in `bindFacetTerms` with both `name` (original) and `alternateName` (if available)
2. **Term Display**: UI components use `term.alternateName ?? term.name` to display the user-friendly name
3. **Term Operations**: Filtering and data operations continue to use `term.name` (the original value)

## Important Notes

- **Facet UI Only**: Alternate names are used only in facet selection UI, not in result tables or data rows
- **Case Sensitive**: Term name matching is case-sensitive
- **Performance**: The file is loaded once and cached in memory for the session
- **No Retries**: There are no automatic retries if the file fails to load

## Testing

When testing alternate term names:

```typescript
import { AlternateTermNamesProvider } from "@databiosphere/findable-ui/lib/providers/alternateTermNames/alternateTermNames";
import { useAlternateTermNames } from "@databiosphere/findable-ui/lib/providers/alternateTermNames/useAlternateTermNames";

// In your test
const wrapper = ({ children }) => (
<AlternateTermNamesProvider>{children}</AlternateTermNamesProvider>
);

// Mock the fetch call
global.fetch = jest.fn().mockResolvedValue({
ok: true,
json: async () => ({
species: { "Homo sapiens": "Human" }
})
});
```

## Migrating Existing Applications

1. Add the `AlternateTermNamesProvider` to your app's provider hierarchy
2. Create the `/public/fe-api/alternateTermNames.json` file (optional)
3. No code changes are required in components that display facet terms

The system is backward compatible - applications that don't provide alternate names will continue to work exactly as before.
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export const ExportFileSummaryForm = ({
</TableHead>
<TableBody>
{fileSummaryFacet.terms.map(
({ count, name, selected, size = 0 }) => (
({ alternateName, count, name, selected, size = 0 }) => (
<TableRow key={name}>
<TableCell>
<FormControlLabel
Expand All @@ -100,7 +100,7 @@ export const ExportFileSummaryForm = ({
/>
}
key={name}
label={name}
label={alternateName ?? name}
/>
</TableCell>
<TableCell>{formatCountSize(count)}</TableCell>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export const ExportSpeciesForm = ({
</FormHelperText>
)}
<FormGroup>
{speciesFacet.terms.map(({ name, selected }) => (
{speciesFacet.terms.map(({ alternateName, name, selected }) => (
<FormControlLabel
control={
<Checkbox
Expand All @@ -55,7 +55,7 @@ export const ExportSpeciesForm = ({
/>
}
key={name}
label={name}
label={alternateName ?? name}
/>
))}
</FormGroup>
Expand Down
1 change: 1 addition & 0 deletions src/hooks/useFileManifest/common/entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export type SelectedSearchTermsBySearchKey = Map<
* Model of an individual facet value. For example, the term "Homo Sapiens" contained in the facet "Species".
*/
export interface Term {
alternateName?: string;
count: number;
name: string;
selected: boolean;
Expand Down
36 changes: 28 additions & 8 deletions src/hooks/useFileManifest/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
} from "../../../apis/azul/common/entities";
import { COLLATOR_CASE_INSENSITIVE } from "../../../common/constants";
import { Filters } from "../../../common/entities";
import { AlternateTermNamesMap } from "../../../providers/alternateTermNames/alternateTermNames";
import { getAlternateName } from "../../../providers/alternateTermNames/utils";
import {
EntitySearchResults,
FileFacet,
Expand All @@ -19,16 +21,22 @@ import {
* Parse the entity API response and build up entity search results.
* @param entityResponse - Response model return from the given entity API.
* @param filters - Selected filters.
* @param alternateTermNames - Optional map of alternate term names.
* @returns entity search results.
*/
export function bindEntitySearchResultsResponse(
entityResponse: AzulEntitiesResponse | undefined,
filters: Filters
filters: Filters,
alternateTermNames?: AlternateTermNamesMap | null
): EntitySearchResults {
// Grab the search terms by search key
const searchTermsBySearchKey = getSelectedSearchTermsBySearchKey(filters);
// Build up term facets
const facets = bindFacets(searchTermsBySearchKey, entityResponse?.termFacets);
const facets = bindFacets(
searchTermsBySearchKey,
entityResponse?.termFacets,
alternateTermNames
);
return {
facets,
};
Expand All @@ -41,11 +49,13 @@ export function bindEntitySearchResultsResponse(
* TODO age range facet to be added, if required.
* @param searchTermsBySearchKey - Selected search terms by search key.
* @param responseFacetsByName - Facets returned from the API response.
* @param alternateTermNames - Optional map of alternate term names.
* @returns file facets.
*/
function bindFacets(
searchTermsBySearchKey: SelectedSearchTermsBySearchKey,
responseFacetsByName: AzulTermFacets | undefined
responseFacetsByName: AzulTermFacets | undefined,
alternateTermNames?: AlternateTermNamesMap | null
): FileFacet[] {
if (!responseFacetsByName) {
return [];
Expand All @@ -54,7 +64,8 @@ function bindFacets(
return buildFileFacet(
facetName,
searchTermsBySearchKey,
responseFacetsByName[facetName]
responseFacetsByName[facetName],
alternateTermNames
);
});
}
Expand All @@ -65,20 +76,26 @@ function bindFacets(
* @param facetName - Facet name.
* @param responseTerms - Response terms.
* @param searchTermNames - Selected search term names.
* @param alternateTermNames - Optional map of alternate term names.
* @returns response terms with selected state.
*/
function bindFacetTerms(
facetName: string,
responseTerms: AzulTerm[],
searchTermNames: unknown[]
searchTermNames: unknown[],
alternateTermNames?: AlternateTermNamesMap | null
): Term[] {
return responseTerms.reduce((accum: Term[], responseTerm) => {
// Default term name to "Unspecified" if term name is null.
const name = bindTermName(responseTerm);
// Determine if term is currently selected as a search term.
const selected = searchTermNames.indexOf(responseTerm.term) >= 0;
// Look up alternate name if available.
const alternateName = alternateTermNames
? getAlternateName(alternateTermNames, facetName, name)
: undefined;
// Create new term - default name to "Unspecified" if no value is returned.
accum.push({ count: responseTerm.count, name, selected });
accum.push({ alternateName, count: responseTerm.count, name, selected });
return accum;
}, []);
}
Expand All @@ -97,12 +114,14 @@ function bindTermName(termResponse: AzulTerm): string {
* @param facetName - Facet name.
* @param searchTermsBySearchKey - Selected search terms by search key.
* @param responseFacet - Response facet of the given facet name.
* @param alternateTermNames - Optional map of alternate term names.
* @returns something.
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The return description "something" is not descriptive. It should be updated to accurately describe what the function returns, such as "file facet with populated terms and selected state" or similar.

Suggested change
* @returns something.
* @returns File facet with populated terms and selected state.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit ded05c8. Updated the JSDoc return description to "File facet with populated terms and selected state."

*/
function buildFileFacet(
facetName: string,
searchTermsBySearchKey: SelectedSearchTermsBySearchKey,
responseFacet: AzulTermFacet
responseFacet: AzulTermFacet,
alternateTermNames?: AlternateTermNamesMap | null
): FileFacet {
// Determine the set of currently selected search term names for this facet.
const searchTermNames = listFacetSearchTermNames(
Expand All @@ -113,7 +132,8 @@ function buildFileFacet(
const responseTerms = bindFacetTerms(
facetName,
responseFacet.terms,
searchTermNames
searchTermNames,
alternateTermNames
);
// Create facet from newly built terms and newly calculated total.
return getFileFacet(facetName, responseFacet.total || 0, responseTerms);
Expand Down
6 changes: 4 additions & 2 deletions src/hooks/useFileManifest/useFetchFilesFacets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
} from "../../apis/azul/common/entities";
import { Filters } from "../../common/entities";
import { fetchEntitiesFromURL } from "../../entity/common/service";
import { useAlternateTermNames } from "../../providers/alternateTermNames/useAlternateTermNames";
import { fetchQueryParams, SearchParams } from "../../utils/fetchQueryParams";
import { useToken } from "../authentication/token/useToken";
import { useAsync } from "../useAsync";
Expand All @@ -27,6 +28,7 @@ export const useFetchFilesFacets = (
isEnabled: boolean
): FetchFilesFacets => {
const { token } = useToken();
const { alternateTermNames } = useAlternateTermNames();
// Build request params.
const requestParams = fetchQueryParams(filters, catalog, searchParams);
// Build request URL.
Expand All @@ -36,8 +38,8 @@ export const useFetchFilesFacets = (
useAsync<AzulEntitiesResponse>();
// Bind facets.
const { facets } = useMemo(
() => bindEntitySearchResultsResponse(data, filters),
[data, filters]
() => bindEntitySearchResultsResponse(data, filters, alternateTermNames),
[alternateTermNames, data, filters]
);

// Fetch facets from files endpoint.
Expand Down
68 changes: 68 additions & 0 deletions src/providers/alternateTermNames/alternateTermNames.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import React, { createContext, ReactNode, useEffect, useState } from "react";

/**
* Alternate term names map structure: { facetName: { termName: alternateName } }
*/
export type AlternateTermNamesMap = Record<string, Record<string, string>>;

/**
* Context props for alternate term names.
*/
export type AlternateTermNamesContextProps = {
alternateTermNames: AlternateTermNamesMap | null;
};

/**
* Provider props for alternate term names.
*/
export interface AlternateTermNamesProps {
children: ReactNode | ReactNode[];
}

/**
* Context for alternate term names.
*/
export const AlternateTermNamesContext =
createContext<AlternateTermNamesContextProps>({
alternateTermNames: null,
});

/**
* Provider for loading alternate term names from /fe-api/alternateTermNames.json.
* Loads the file once on mount and caches it in memory for the session.
* If the file is missing or fails to load, falls back to an empty map.
* @param props - Provider props.
* @param props.children - Child components.
* @returns JSX element.
*/
export function AlternateTermNamesProvider({
children,
}: AlternateTermNamesProps): JSX.Element {
const [alternateTermNames, setAlternateTermNames] =
useState<AlternateTermNamesMap | null>(null);

useEffect(() => {
// Load alternate term names once on mount
fetch("/fe-api/alternateTermNames.json")
.then((response) => {
if (response.ok) {
return response.json();
}
// If file doesn't exist or isn't ok, return empty object
return {};
})
.then((data: AlternateTermNamesMap) => {
setAlternateTermNames(data);
})
.catch(() => {
// On any error, use empty map (file missing, parse error, etc.)
setAlternateTermNames({});
});
}, []);

return (
<AlternateTermNamesContext.Provider value={{ alternateTermNames }}>
{children}
</AlternateTermNamesContext.Provider>
);
}
13 changes: 13 additions & 0 deletions src/providers/alternateTermNames/useAlternateTermNames.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { useContext } from "react";
import {
AlternateTermNamesContext,
AlternateTermNamesContextProps,
} from "./alternateTermNames";

/**
* Hook to access alternate term names from context.
* @returns Context props containing the alternate term names map.
*/
export function useAlternateTermNames(): AlternateTermNamesContextProps {
return useContext(AlternateTermNamesContext);
}
Loading
Loading