Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
212 changes: 212 additions & 0 deletions OCCURRENCE_UI_RENAME_PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
# Feature Plan: Rename “Locality-Species” UI Labels to “Occurrence(s)”

## 1) Assumptions & Scope
- This is a **UI terminology update only**: replace visible labels such as “Locality-Species”, “Locality Species”, and “Locality-Species-Cross-Search” with “Occurrence” / “Occurrences”.
- **Database and backend persistence remain unchanged**: table names (including `now_ls`), Prisma model names, API route names, and payload contracts are not renamed.
- Expected impact is concentrated in frontend presentation layers (navigation labels, page/tab/table captions, breadcrumbs, toolbar text, empty states, and helper text), plus any shared constants feeding those labels.
- Backend impact is limited to optional API response metadata strings only if currently surfaced directly in UI; otherwise no backend changes.
- Role-based authorization behavior does not change. Existing coordinator/admin restrictions remain as-is.
- No migration should be required unless a persisted settings/config table stores user-visible caption text (unlikely; verify quickly before implementation).

## 2) High-Level Plan
1. **Inventory all user-visible strings**
- Search frontend and shared UI config for “Locality-Species”, “Locality Species”, and “Cross-Search” variants.
- Classify each usage as singular (“Occurrence”) vs plural (“Occurrences”) based on context.

2. **Centralize/normalize labels where needed**
- If labels are scattered literals, introduce/extend a UI label constants module for this domain to reduce future drift.
- Keep route paths/IDs stable unless they are explicitly user-facing display text.

3. **Update UI surfaces**
- Navigation menus, page titles, tabs, table headings, card headers, action dialogs, and search forms.
- Ensure document titles and route shell wrappers reflect the new term.

4. **Preserve API/data compatibility**
- Do not rename backend endpoints, Prisma models, DB tables, or query parameters unless purely display-oriented.
- Keep request/response schema compatibility for frontend consumers.

5. **Auth/permissions verification**
- Confirm that label updates do not accidentally expose hidden actions/sections.
- Validate coordinator-only UI controls still gate correctly after refactors.

6. **Testing and QA**
- Frontend unit/integration tests for rendered labels and key page headings.
- Optional E2E smoke checks for navigation text and cross-search page captions.
- Run lint/type-check; update snapshots where appropriate.

7. **Docs and release notes**
- Add a concise changelog/readme note that terminology changed in UI only.

## 3) Tasks (JSON)
```json
[
{
"id": "T1",
"title": "Inventory all Locality-Species UI label usages",
"summary": "Locate every user-visible occurrence of Locality-Species terminology in frontend components, pages, navigation config, and tests.",
"app": "frontend",
"files_touched": [
"frontend/src/components/**",
"frontend/src/pages/**",
"frontend/src/App.tsx",
"frontend/src/router/**",
"frontend/src/redux/**",
"frontend/tests/**"
],
"migrations": false,
"permissions": [
"No permission model changes",
"Verify restricted views retain existing role gates"
],
"acceptance_criteria": [
"A complete checklist of UI label locations is produced",
"Each location is tagged as singular (Occurrence) or plural (Occurrences)"
],
"test_plan": [
"Run repository search for all target label variants",
"Peer-review checklist against major UI routes"
],
"estimate_hours": 1.0,
"priority": "high"
},
{
"id": "T2",
"title": "Replace navigation and route-shell captions",
"summary": "Update menu entries, breadcrumb labels, and route-level page titles from Locality-Species wording to Occurrence(s) without changing route paths.",
"app": "frontend",
"files_touched": [
"frontend/src/App.tsx",
"frontend/src/pages/**",
"frontend/src/components/Navigation/**",
"frontend/src/router/**"
],
"migrations": false,
"permissions": [
"Visible only where existing route access is allowed",
"Coordinator-only routes remain coordinator-only"
],
"acceptance_criteria": [
"Primary navigation no longer shows Locality-Species labels",
"Document titles and breadcrumbs use Occurrence/Occurrences consistently"
],
"test_plan": [
"Frontend render tests for nav labels",
"Manual smoke test through key routes"
],
"estimate_hours": 2.0,
"priority": "high"
},
{
"id": "T3",
"title": "Update table, tab, and cross-search captions",
"summary": "Rename captions in list/table headers, tabs, and cross-search UI text to Occurrence(s) while preserving data bindings to now_ls-backed APIs.",
"app": "frontend",
"files_touched": [
"frontend/src/components/**",
"frontend/src/pages/**",
"frontend/src/hooks/**",
"frontend/src/redux/**"
],
"migrations": false,
"permissions": [
"No new permissions",
"Existing disabled/hidden controls remain unchanged"
],
"acceptance_criteria": [
"All table/tab captions display Occurrence terminology",
"Cross-search captions no longer contain Locality-Species wording",
"Data loading behavior is unchanged"
],
"test_plan": [
"Update/extend component tests asserting headings and tab labels",
"Run regression checks for list and cross-search screens"
],
"estimate_hours": 2.5,
"priority": "high"
},
{
"id": "T4",
"title": "Harden consistency via shared UI label constants",
"summary": "Introduce or update a shared constants file for Occurrence labels to prevent future terminology drift across components.",
"app": "frontend",
"files_touched": [
"frontend/src/constants/**",
"frontend/src/components/**",
"frontend/src/pages/**"
],
"migrations": false,
"permissions": [
"No permission changes"
],
"acceptance_criteria": [
"Key labels are sourced from centralized constants",
"No duplicate legacy Locality-Species literals remain in UI code"
],
"test_plan": [
"Static search confirms no legacy literals in frontend src",
"Type-check passes after refactor"
],
"estimate_hours": 1.5,
"priority": "medium"
},
{
"id": "T5",
"title": "Validate non-functional requirements and documentation",
"summary": "Run lint/type-check/tests, capture visual verification, and add a documentation note clarifying UI-only terminology change with no DB/table rename.",
"app": "fullstack",
"files_touched": [
"README.md",
"CHANGELOG.md",
"frontend/tests/**",
"cypress/**"
],
"migrations": false,
"permissions": [
"Verify UI access boundaries through smoke tests"
],
"acceptance_criteria": [
"Lint and type-check complete successfully",
"Relevant automated tests pass",
"Docs explicitly state now_ls table name is unchanged"
],
"test_plan": [
"npm run lint",
"npm run tsc",
"Frontend test suite for affected screens",
"Optional Cypress smoke for navigation/caption text"
],
"estimate_hours": 2.0,
"priority": "medium"
}
]
```

## 4) Risks & Mitigations
- **Auth Failures**: UI refactors may bypass existing guard wrappers.
- *Mitigation*: regression-check protected routes/components with both coordinator and non-coordinator accounts.
- **Data Migration Risk**: accidental backend renaming could break persistence contracts.
- *Mitigation*: explicitly prohibit DB/Prisma/API renames; no migration files in this feature.
- **Performance**: negligible risk, but broad refactor could trigger unnecessary rerenders.
- *Mitigation*: keep changes text-only where possible; avoid state shape changes.
- **Error Handling Drift**: changed labels in error banners/toasts may become inconsistent.
- *Mitigation*: update shared message constants and validate key failure flows.
- **Security**: low direct risk, but route/config edits can accidentally expose links.
- *Mitigation*: preserve existing authorization checks and conditional rendering.
- **Rollback**: need safe revert if terminology causes confusion.
- *Mitigation*: isolate commits to label updates and optional constants; revertable without schema impact.

## 5) Out of Scope
- Renaming DB tables, Prisma models, backend route paths, or API field names.
- Legacy data migration/backfill tasks.
- Visual redesign/theming changes beyond text replacement.
- Full i18n localization framework rollout.
- Unrelated performance optimizations.

## 6) Definition of Done ✅
- All approved UI surfaces use “Occurrence” / “Occurrences” appropriately.
- No database table/model/API contract renames were introduced.
- Unit/integration (and applicable E2E smoke) tests pass for affected areas.
- Linting and type checks pass.
- Role-based visibility and access behavior verified unchanged.
- Documentation/changelog note added for terminology-only update.
- CI pipeline succeeds.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,11 @@ The new version is not deployed yet.
- Switching methods restores previously entered age values (e.g., `min_age`, `max_age`, basis/fraction selections) instead of clearing them.
- Locality age validation now evaluates required basis fields against the currently active dating method, while allowing preserved values from other methods to remain in draft state.
- See [documentation/CHANGELOG.md](documentation/CHANGELOG.md) for release-level tracking.


### Reviewer notes: Occurrence terminology update

- User-facing UI captions now use **Occurrence/Occurrences** in navigation, cross-search, and Species detail surfaces.
- This is a presentation-only rename: backend/database identifiers are unchanged, including the `now_ls` table and related API payload fields.
- See `frontend/src/constants/occurrenceLabels.ts` for centralized frontend label constants and `frontend/tests/locality-species-ui-label-inventory.md` for inventory coverage.

1 change: 1 addition & 0 deletions documentation/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Unreleased
### Changed
- UI terminology now uses Occurrence/Occurrences instead of Locality-Species in user-facing captions; backend/database contracts remain unchanged (including `now_ls`).
- Repositioned table row action icons to the right edge, tightened spacing, and added aria labels so Locality, Species, and related tables reclaim horizontal room while keeping tooltips and restriction markers accessible.
- Species detail Locality-Species tab now renders MW Score as a computed, read-only display field based on mw_scale_min, mw_scale_max, and mw_value instead of a placeholder value.

Expand Down
5 changes: 3 additions & 2 deletions frontend/src/components/CrossSearch/CrossSearchTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useGetAllCrossSearchQuery, useGetAllCrossSearchLocalitiesQuery } from '
import { usePageContext } from '../Page'
import { LocalitiesMap } from '../Map/LocalitiesMap'
import { formatWithMaxThreeDecimals } from '@/util/numberFormatting'
import { occurrenceLabels } from '@/constants/occurrenceLabels'

export const CrossSearchTable = ({ selectorFn }: { selectorFn?: (newObject: CrossSearch) => void }) => {
const { sqlLimit, sqlOffset, sqlColumnFilters, sqlOrderBy } = usePageContext()
Expand Down Expand Up @@ -817,15 +818,15 @@ export const CrossSearchTable = ({ selectorFn }: { selectorFn?: (newObject: Cros
<>
<LocalitiesMap localities={localitiesData} isFetching={localitiesFetching || isFetching} />
<TableView<CrossSearch>
title="Locality-Species-Cross-Search"
title={occurrenceLabels.crossSearchTitle}
selectorFn={selectorFn}
checkRowRestriction={checkRowRestriction}
idFieldName="lid_now_loc"
columns={columns}
isFetching={isFetching}
visibleColumns={visibleColumns}
data={crossSearchQueryData}
url="crosssearch"
url="occurrence"
enableColumnFilterModes={true}
serverSidePagination={true}
isCrossSearchTable={true}
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/components/FrontPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { useEffect, useRef } from 'react'
import mapSvg from '../resource/map.svg'
import logo from '../resource/nowlogo.jpg'
import '../styles/FrontPage.css'
import { occurrenceLabels } from '@/constants/occurrenceLabels'

export const FrontPage = () => {
const mapContainerRef = useRef<HTMLDivElement | null>(null)
Expand Down Expand Up @@ -72,7 +73,7 @@ export const FrontPage = () => {
<b>Species</b> {statisticsQueryData && statisticsQueryData.speciesCount}
</span>
<span>
<b>Locality-species</b> {statisticsQueryData && statisticsQueryData.localitySpeciesCount}
<b>{occurrenceLabels.plural}</b> {statisticsQueryData && statisticsQueryData.localitySpeciesCount}
</span>
</div>
<div className="map-container" ref={mapContainerRef}>
Expand Down
51 changes: 51 additions & 0 deletions frontend/src/components/NavBar.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { describe, expect, it, jest } from '@jest/globals'
import { render, screen } from '@testing-library/react'
import { MemoryRouter } from 'react-router-dom'
import { NavBar } from './NavBar'
import { occurrenceLabels } from '@/constants/occurrenceLabels'
import { Role } from '@/shared/types'

jest.mock('react-redux', () => ({
useDispatch: () => jest.fn(),
}))

jest.mock('../redux/userReducer', () => ({
clearUser: () => ({ type: 'user/clearUser' }),
}))

jest.mock('../redux/api', () => ({
api: {
util: {
resetApiState: () => ({ type: 'api/resetApiState' }),
},
},
}))

jest.mock('@/hooks/user', () => ({
useUser: () => ({
token: null,
username: null,
role: Role.ReadOnly,
initials: null,
localities: [],
isFirstLogin: undefined,
}),
}))

jest.mock('@/hooks/notification', () => ({
useNotify: () => ({ notify: jest.fn() }),
}))

jest.mock('../resource/nowlogo.jpg', () => 'now-logo')

describe('NavBar', () => {
it('shows Occurrences navigation label', () => {
render(
<MemoryRouter>
<NavBar />
</MemoryRouter>
)

expect(screen.getByText(occurrenceLabels.plural)).toBeTruthy()
})
})
3 changes: 2 additions & 1 deletion frontend/src/components/NavBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { useNotify } from '@/hooks/notification'
import PersonIcon from '@mui/icons-material/Person'
import '../styles/NavBar.css'
import { LinkDefinition, NavBarLink } from './NavBarLink'
import { occurrenceLabels } from '@/constants/occurrenceLabels'

import logo from '../resource/nowlogo.jpg'

Expand All @@ -20,7 +21,7 @@ export const NavBar = () => {
const pages: LinkDefinition[] = [
{ title: 'Localities', url: '/locality' },
{ title: 'Species', url: '/species' },
{ title: 'Locality-Species', url: '/crosssearch' },
{ title: occurrenceLabels.plural, url: '/occurrence' },
{ title: 'References', url: '/reference' },
{ title: 'Time Units', url: '/time-unit' },
{ title: 'Time Bounds', url: '/time-bound', allowedRoles: [Role.Admin, Role.EditUnrestricted] },
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/components/Species/SpeciesDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { emptySpecies } from '../DetailView/common/defaultValues'
import { useNotify } from '@/hooks/notification'
import { useEffect, useState } from 'react'
import { fixNullValuesInTaxonomyFields } from '@/util/taxonomyUtilities'
import { occurrenceLabels } from '@/constants/occurrenceLabels'

export const SpeciesDetails = ({
wrapWithUnsavedChangesProvider = true,
Expand Down Expand Up @@ -105,7 +106,7 @@ export const SpeciesDetails = ({
content: <LocalityTab />,
},
{
title: 'Locality Species',
title: occurrenceLabels.plural,
content: <LocalitySpeciesTab />,
},
{
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/components/Species/Tabs/LocalitySpeciesTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { calculateNormalizedMesowearScore } from '@/shared/utils/mesowear'
import { applyDefaultSpeciesOrdering, hasActiveSortingInSearch } from '@/components/DetailView/common/DetailTabTable'
import { useLocation } from 'react-router-dom'
import { useMemo } from 'react'
import { occurrenceLabels } from '@/constants/occurrenceLabels'

const hasMesowearScoreInputs = (row: SpeciesLocality) => {
return (
Expand Down Expand Up @@ -197,7 +198,7 @@ export const LocalitySpeciesTab = () => {
}

const editingModal = (
<EditingModal buttonText="Add new Locality Species" onSave={onSave}>
<EditingModal buttonText={occurrenceLabels.addNewButton} onSave={onSave}>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '1em' }}>
<TextField {...register('now_loc.name', { required: true })} label="Locality" />
<TextField {...register('now_loc.country', { required: true })} label="Country" />
Expand All @@ -212,7 +213,7 @@ export const LocalitySpeciesTab = () => {
)

return (
<Grouped title="Locality-Species Information">
<Grouped title={occurrenceLabels.informationSectionTitle}>
{!mode.read && editingModal}
<EditableTable<Editable<SpeciesLocality>, SpeciesDetailsType>
columns={columns}
Expand Down
Loading
Loading