Skip to content

Configurable taxa lists#1094

Open
annavik wants to merge 30 commits intomainfrom
feat/taxa-lists
Open

Configurable taxa lists#1094
annavik wants to merge 30 commits intomainfrom
feat/taxa-lists

Conversation

@annavik
Copy link
Member

@annavik annavik commented Jan 20, 2026

Background

Will be a help for the upcoming class masking feature, but also something we have been wanting in general. This PR is still in draft mode.

Related to #997.

Missing BE stuff

  • Make it possible to add and remove taxa from taxa lists using the API ✅
  • Make it possible to access direct taxa only (exclude children) when using the API ✅
  • Getting error 500 when creating new taxa list after the BE updates in this branch ✅

Missing FE stuff

  • Make sure breadcrumbs are rendered correctly ✅
  • Show taxon details from taxa list detail view without changing route ✅
  • Update logic after API updates ✅

Notes

  • When we have decided on naming, setup new tab "Collections" and add "Taxa lists" as a sub view to this tab (can happen in follow up PR)
  • Expand taxa list detail view with more columns, or is it nice to keep it simple?
  • Fetching taxa is very slow in production, can we make it quicker by not consider related occurrences BE side? Similar to what we did for captures? Or is child taxa the problem?
  • If we can return more information about taxa lists in the taxa response, it would open up for more display options and quick actions in the generic taxa view

Screenshots

List view:
Screenshot 2026-01-20 at 15 03 48

Create view:
Screenshot 2026-01-20 at 15 03 56

Edit view:
Screenshot 2026-01-20 at 15 04 11

Delete view:
Screenshot 2026-01-20 at 15 04 21

Detail view:
Screenshot 2026-01-20 at 15 04 31

Summary by CodeRabbit

  • New Features

    • Taxa lists management pages: create, view details, and manage taxa.
    • Add/remove individual taxa to/from a taxa list via new UI controls and routes.
  • UI / Navigation

    • New sidebar item and routes for taxa lists and taxa within a list.
    • New dialogs, popovers, table columns, and localized labels for taxa-list flows.
  • API / Backend

    • Taxa list endpoints now expose taxa counts, project info, and nested taxa management.
  • Tests

    • New API tests covering taxa-list taxon add/remove and validation.

annavik and others added 13 commits January 14, 2026 12:07
* Initial plan

* Add sorting, timestamps and taxa_count to TaxaList API

Co-Authored-By: Claude <noreply@anthropic.com>

Co-authored-by: mihow <158175+mihow@users.noreply.github.com>

* Optimize taxa_count with query annotation

Co-Authored-By: Claude <noreply@anthropic.com>

Co-authored-by: mihow <158175+mihow@users.noreply.github.com>

* Format lists with trailing commas per black style

Co-Authored-By: Claude <noreply@anthropic.com>

Co-authored-by: mihow <158175+mihow@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: mihow <158175+mihow@users.noreply.github.com>
Co-authored-by: Anna Viklund <annamariaviklund@gmail.com>
@netlify
Copy link

netlify bot commented Jan 20, 2026

Deploy Preview for antenna-preview ready!

Name Link
🔨 Latest commit 17cf37e
🔍 Latest deploy log https://app.netlify.com/projects/antenna-preview/deploys/69860aeac4512c000813c7c4
😎 Deploy Preview https://deploy-preview-1094--antenna-preview.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
1 paths audited
Performance: 63 (🔴 down 3 from production)
Accessibility: 80 (no change from production)
Best Practices: 100 (no change from production)
SEO: 92 (no change from production)
PWA: 80 (no change from production)
View the detailed breakdown and full score reports

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify
Copy link

netlify bot commented Jan 20, 2026

Deploy Preview for antenna-ssec ready!

Name Link
🔨 Latest commit 17cf37e
🔍 Latest deploy log https://app.netlify.com/projects/antenna-ssec/deploys/69860aea77ab480008d37d94
😎 Deploy Preview https://deploy-preview-1094--antenna-ssec.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 20, 2026

📝 Walkthrough

Walkthrough

Adds full taxa-list management: backend serializers, viewsets, nested routes and tests; frontend pages, hooks, components, routes, and translations; plus UI tweaks and utility updates to support listing, adding, and removing taxa within taxa lists.

Changes

Cohort / File(s) Summary
Backend — Serializers & Models
ami/main/api/serializers.py, ami/ml/serializers.py
TaxaListSerializer switched to DefaultSerializer; added taxa_count and projects SerializerMethodFields; new TaxaListTaxonInputSerializer and TaxaListTaxonSerializer. ProcessingServiceSerializer replaces write-only project with computed projects.
Backend — Viewsets
ami/main/api/views.py, ami/ml/views.py
TaxaListViewSetDefaultViewSet, annotated annotated_taxa_count, perform_create associates active project. New TaxaListTaxonViewSet for nested /taxa/lists/{id}/taxa (list, create, delete_by_taxon). ProcessingServiceViewSet adds require_project and perform_create.
Backend — Routing
config/api_router.py
Added nested taxa_lists_router registration exposing TaxaListTaxonViewSet at /taxa/lists/{taxalist_id}/taxa/.
Backend — Tests
ami/main/tests.py
Added TaxaListTaxonAPITestCase and TaxaListTaxonValidationTestCase covering add/list/delete, duplicates, validation, and M2M behavior.
Frontend — Pages & Components
ui/src/pages/taxa-lists/*, ui/src/pages/taxa-list-details/*, ui/src/pages/taxa-list-details/add-taxa-list-taxon/*, ui/src/pages/taxa-list-details/remove-taxa-list-taxon/*
New TaxaLists and TaxaListDetails pages; table column configs; Add/Remove taxa components (popover, dialog, forms); species dialog integration and routing-aware behavior.
Frontend — Hooks & Data Services
ui/src/data-services/hooks/taxa-lists/*, ui/src/data-services/hooks/entities/*
New hooks: useTaxaLists (exposes total), useTaxaListDetails, useAddTaxaListTaxon, useRemoveTaxaListTaxon. useDeleteEntity signature changed to require { collection, projectId, onSuccess? } and appends project_id to delete URLs.
Frontend — Models & Utils
ui/src/data-services/models/taxa-list.ts, ui/src/data-services/models/entity.ts, ui/src/data-services/hooks/entities/utils.ts
ServerTaxaList adds taxa_count; TaxaList exposes taxaCount() (removes taxaUrl getter). Permission checks made null-safe. convertToServerFieldValues returns project_id instead of project.
Frontend — Routing, Breadcrumbs & UI polish
ui/src/app.tsx, ui/src/pages/project/sidebar/*, ui/src/components/breadcrumbs/breadcrumbs.tsx, ui/src/utils/constants.ts, ui/src/utils/language.ts
Added routes and APP_ROUTES builders for taxa lists and taxon details under lists. Sidebar and breadcrumbs adjusted to surface taxa-lists. New STRING keys and translations added. Minor dialog autofocus and button i18n tweaks.
Frontend — Misc
ui/src/pages/species/*, ui/src/pages/occurrences/*, ui/src/components/taxon-search/add-taxon.tsx
Small UI behavior/localization changes: localized labels, preserved search params on navigation, and dialog autofocus suppression.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant UI as Client (UI)
participant API as Server (Django API)
participant Auth as Auth layer
participant DB as Database
UI->>Auth: include auth headers
UI->>API: POST /taxa/lists/{taxaListId}/taxa/?project_id=... { taxon_id }
API->>Auth: validate token
Auth-->>API: auth ok
API->>DB: fetch TaxaList by id
DB-->>API: taxa list record
API->>DB: fetch Taxon by taxon_id (validate exists)
DB-->>API: taxon record / not found
alt taxon exists & not duplicate
API->>DB: create M2M association (taxa list ↔ taxon)
DB-->>API: association created
API-->>UI: 201 Created with taxon data
else duplicate or invalid
API-->>UI: 400 Bad Request / 404

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • mihow

Poem

🐰 I hopped the code from root to leaf,

Nested routes for taxon relief.
Lists and popovers, add and delete—
M2M dances, tests complete.
Crunching counts with a carrot-sized beat. 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Description check ❓ Inconclusive The PR description follows the template structure with Background/Related Issues, separated missing BE and FE items, notes on future work, and screenshots. However, the 'Summary' section (tl;dr) and several template sections are missing or incomplete. Add a brief 'Summary' section at the top, and flesh out 'Detailed Description', 'How to Test', and 'Deployment Notes' sections with relevant details and test instructions.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Configurable taxa lists' accurately summarizes the main feature being added—a taxa lists feature with CRUD and member management functionality.
Docstring Coverage ✅ Passed Docstring coverage is 88.24% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/taxa-lists

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Co-Authored-By: Claude <noreply@anthropic.com>
@mihow
Copy link
Collaborator

mihow commented Jan 22, 2026

Exciting! I will look into the missing BE parts. But would love to merge some version of this sooner and continue to improve

@annavik
Copy link
Member Author

annavik commented Jan 23, 2026

Exciting! I will look into the missing BE parts. But would love to merge some version of this sooner and continue to improve

Yes, let's try get something out! I'm fine leaving everything under "Notes" for later. I have updated the PR description with current status. We can now add and remove taxa lists members from UI! 🎉

I think these two backend issues would be nice to fix before we try get this out, at least the first one (listed under "Missing BE stuff")...

Screenshot 2026-01-23 at 12 58 39 Screenshot 2026-01-23 at 12 59 12

@annavik
Copy link
Member Author

annavik commented Jan 23, 2026

Some observations when using the new endpoints to add and remove taxa from lists:

  • The endpoint to add taxon to a list returns 200 even if the taxon is already added
  • The endpoint to remove taxon from a list returns 200 even if the taxon is not a member if the list
  • The endpoint to remove taxon from a list uses method POST, maybe should beDELETE?
  • Maybe these endpoints should follow same structure as the project member endpoints? I'm thinking we have a similar situation of "related objects"...

Just some thoughts, I'm fine leaving this as is for now! Thank you so much for the help @mohamedelabbas1996, it worked well to hook up with FE 🙏

@annavik
Copy link
Member Author

annavik commented Jan 23, 2026

Question: does it make sense to check "can update" permission for the taxa list, before rendering controls for "Add taxon" and "Remove taxon"? This is what I do know! :)

annavik and others added 5 commits January 23, 2026 15:38
… model)

Refactors taxa list endpoints to use nested routes and proper HTTP methods
while keeping the simple ManyToManyField (no through model).

**Backend changes:**
- Created TaxaListTaxonViewSet with nested routes under /taxa/lists/{id}/taxa/
- Simple serializers for input validation and output
- Removed deprecated add_taxon/remove_taxon actions from TaxaListViewSet
- Uses Django M2M .add() and .remove() methods directly

**Frontend changes:**
- Updated useAddTaxaListTaxon to POST to /taxa/ endpoint
- Updated useRemoveTaxaListTaxon to use DELETE method

**API changes:**
- POST /taxa/lists/{id}/taxa/ - Add taxon (returns 201, 400 for duplicates)
- DELETE /taxa/lists/{id}/taxa/by-taxon/{taxon_id}/ - Remove taxon (returns 204, 404 for non-existent)
- GET /taxa/lists/{id}/taxa/ - List taxa in list
- Removed POST /taxa/lists/{id}/add_taxon/ (deprecated)
- Removed POST /taxa/lists/{id}/remove_taxon/ (deprecated)

**Benefits:**
- Same API as project membership (consistency)
- No migration needed (keeps existing simple M2M)
- Proper HTTP semantics (POST=201, DELETE=204)
- RESTful nested resource design

**Tests:**
- Added comprehensive test suite (13 tests, all passing)
- Tests for CRUD operations, validation, and error cases

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@mihow
Copy link
Collaborator

mihow commented Jan 30, 2026

@annavik @mohamedelabbas1996 I updated the endpoints to use the same pattern as the project-user membership API, but no through-model to keep it a little simpler. I have in a separate PR here until I test it. Feel free to take a look before then. #1104

@annavik
Copy link
Member Author

annavik commented Jan 30, 2026

@annavik @mohamedelabbas1996 I updated the endpoints to use the same pattern as the project-user membership API, but no through-model to keep it a little simpler. I have in a separate PR here until I test it. Feel free to take a look before then. #1104

Great, thank you!! I had a look and commented.

mihow added 5 commits February 3, 2026 16:48
Fixes 500 error when creating taxa lists via the API. The error was caused
by attempting to directly assign many-to-many fields during model instantiation,
which Django does not allow.

Also addresses a security issue discovered during the fix: users were able to
assign taxa lists and processing services to arbitrary projects.

Changes:
- Make projects field read-only on TaxaListSerializer and ProcessingServiceSerializer
- Add perform_create() methods to handle m2m project assignment after instance creation
- Update UI to send project_id instead of project for proper backend detection
- Remove client-side project field from taxa list creation

Resources are now automatically assigned to the active project by the server,
preventing the m2m assignment error and ensuring proper project scoping.
Refactor taxa list endpoints to follow the project-user membership API
...(description ? { description } : {}),
...(name ? { name } : {}),
project: projectId,
project_id: projectId,
Copy link
Collaborator

Choose a reason for hiding this comment

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

@annavik this may be bigger change, but the backend is already expecting project_id everywhere now for create & update requests.

@mihow mihow marked this pull request as ready for review February 4, 2026 02:23
Copilot AI review requested due to automatic review settings February 4, 2026 02:23
@mihow
Copy link
Collaborator

mihow commented Feb 4, 2026

@annavik I merged the API restructuring (#1104) to this branch! I left some comments there about the specific changes, but I figure it's time to bring the conversation back to this branch :)

Maybe one final test & look-over?

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces configurable, project-scoped taxa lists and wiring for class-masking–related workflows, plus some refactors around entity creation and processing services.

Changes:

  • Add full frontend support for taxa lists: new list/detail pages, navigation entry, table columns, and in-place taxon add/remove actions that integrate with existing species views and breadcrumbs.
  • Extend the backend taxa API with project-aware TaxaListViewSet, nested /taxa/lists/{id}/taxa/ endpoints (add/list/remove taxa in a list), and improved taxa_list_id filtering with optional descendant inclusion.
  • Adjust generic entity creation (project field mapping) and processing service handling to be more project-centric, while also adding small UX tweaks (breadcrumb handling, dialog autofocus, string constants).

Reviewed changes

Copilot reviewed 33 out of 33 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
ui/src/utils/language.ts Adds string IDs and English labels for taxa lists/taxa entities, nav item, add/remove actions, and a remove-taxon confirmation message used across the new UI.
ui/src/utils/getAppRoute.ts Extends the filter key union to cover include_unobserved, taxa_list_id, and other new query params used when linking to taxa and occurrences.
ui/src/utils/constants.ts Introduces APP_ROUTES helpers for taxa lists list/details and taxa-list taxon details, used for routing and breadcrumb links.
ui/src/pages/taxa-lists/taxa-lists.tsx New taxa lists index page with sorting, pagination, permissions-aware “New taxa list” dialog, and results header.
ui/src/pages/taxa-lists/taxa-list-columns.tsx Defines taxa list table columns (name, description, taxa count, created/updated timestamps, actions) and links into taxa view with include_unobserved and taxa_list_id filters.
ui/src/pages/taxa-list-details/taxa-list-details.tsx New taxa list detail page: loads list metadata and species via filters, wires sorting/pagination, breadcrumb integration, add-taxon popover, and a species details dialog overlay.
ui/src/pages/taxa-list-details/taxa-list-details-columns.tsx Table column config for taxa within a list (cover image, taxon details, rank, remove button) including deep linking to a taxon dialog in the context of the list.
ui/src/pages/taxa-list-details/remove-taxa-list-taxon/remove-taxa-list-taxon-dialog.tsx Confirmation dialog + mutation hook wiring to remove a taxon from a taxa list via the new nested DELETE endpoint, with error handling and success state.
ui/src/pages/taxa-list-details/add-taxa-list-taxon/add-taxa-list-taxon.tsx Popover content for selecting and adding a taxon into a taxa list, with loading/success indicators and basic error parsing.
ui/src/pages/taxa-list-details/add-taxa-list-taxon/add-taxa-list-taxon-popover.tsx Wraps AddTaxaListTaxon in a button-triggered popover with localized “Add taxon” label.
ui/src/pages/species/species.tsx Reuses taxa lists to adjust the species page title based on active taxa_list_id filter and adds autofocus prevention in the species detail dialog.
ui/src/pages/species/species-columns.tsx Localizes the cover-image header and updates links to use getAppRoute with keepSearchParams so taxa filters persist into taxon details.
ui/src/pages/project/sidebar/useSidebarSections.tsx Adds a “Taxa lists” item under the Collections section, with a matchPath that covers taxa list detail routes for correct sidebar highlighting.
ui/src/pages/project/sidebar/sidebar.tsx Switches sidebar to drive the main breadcrumb (instead of detail) using the active sidebar item.
ui/src/pages/project/entities/new-entity-dialog.tsx Simplifies entity creation payload assembly (builds a fieldValues object with projectId) while keeping the flexible details form / dialog behavior.
ui/src/pages/occurrences/occurrences.tsx Adds autofocus prevention to the occurrence detail dialog to avoid tooltip focus stealing, aligning it with the species dialogs.
ui/src/data-services/models/taxa-list.ts Refines the TaxaList model to match new API shape: adds taxa_count support and exposes a taxaCount getter.
ui/src/data-services/models/entity.ts Makes createdAt optional and uses safe permission checks against optional user_permissions in the base Entity model.
ui/src/data-services/hooks/taxa-lists/useTaxaLists.ts Fetches paginated taxa lists with total count and maps raw server entities into TaxaList instances, returning total and userPermissions.
ui/src/data-services/hooks/taxa-lists/useTaxaListDetails.ts New hook to fetch a single taxa list by ID (project-scoped) and convert the server payload into a TaxaList instance.
ui/src/data-services/hooks/taxa-lists/useRemoveTaxaListTaxon.ts React Query mutation hook for removing a taxon from a taxa list via the new nested DELETE endpoint, with cache invalidation for taxa lists and species.
ui/src/data-services/hooks/taxa-lists/useAddTaxaListTaxon.ts React Query mutation hook for adding a taxon to a taxa list via the new nested POST endpoint, again invalidating taxa lists and species queries.
ui/src/data-services/hooks/entities/utils.ts Adjusts generic entity field-to-payload mapping to send project_id instead of project to the backend, and spreads any customFields.
ui/src/components/taxon-search/add-taxon.tsx Localizes the “Add taxon” trigger label using shared ENTITY_ADD/ENTITY_TYPE_TAXON strings.
ui/src/components/breadcrumbs/breadcrumbs.tsx Changes breadcrumb behavior so non-project nav items set the main breadcrumb, which is later overridden by sidebar and detail breadcrumbs.
ui/src/app.tsx Wires new taxa lists routes into the project router, including an optional taxon sub-route that reuses the taxa list details page and dialog.
config/api_router.py Registers the new TaxaListViewSet and a nested TaxaListTaxonViewSet under /taxa/lists/{taxalist_id}/taxa/, adding the nested router URLs to urlpatterns.
ami/ml/views.py Makes ProcessingServiceViewSet project-scoped (require_project=True) and ensures new processing services are auto-associated with the active project in perform_create.
ami/ml/serializers.py Refactors ProcessingServiceSerializer to expose projects as a derived list of IDs (read-only) and drops the writable project field; the view now manages project association.
ami/main/tests/test_taxa_list_taxa_api.py New test suite that exercises the nested taxa-list–taxa endpoints: add, list, delete, validation errors, and M2M integrity.
ami/main/tests/__init__.py Adds a marker __init__ for the ami.main.tests package.
ami/main/api/views.py Refactors TaxaListViewSet to reuse DefaultViewSet, annotate taxa counts, and scope to the active project, and adds TaxaListTaxonViewSet plus an updated TaxonTaxaListFilter with include_descendants support.
ami/main/api/serializers.py Updates TaxaListSerializer to use DefaultSerializer, include taxa count, projects list, and timestamps, and adds serializers for taxa-list taxa add/list operations.
Comments suppressed due to low confidence (1)

ui/src/pages/project/entities/new-entity-dialog.tsx:12

  • NewEntityDialog imports API_ROUTES but never uses it in this file. This import can be removed to avoid unused-import warnings and keep the component lean.
import classNames from 'classnames'
import { API_ROUTES } from 'data-services/constants'
import { useCreateEntity } from 'data-services/hooks/entities/useCreateEntity'
import * as Dialog from 'design-system/components/dialog/dialog'
import { PlusIcon } from 'lucide-react'
import { Button } from 'nova-ui-kit'
import { useState } from 'react'
import { useParams } from 'react-router-dom'
import { STRING, translate } from 'utils/language'
import { customFormMap } from './details-form/constants'
import { EntityDetailsForm } from './details-form/entity-details-form'
import styles from './styles.module.scss'

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 189 to 193
MESSAGE_PERMISSIONS_MISSING,
MESSAGE_PROCESS_NOW_TOOLTIP,
MESSAGE_REMOVE_MEMBER_CONFIRM,
MESSAGE_MESSAGE_REMOVE_TAXA_LIST_TAXON_CONFIRM,
MESSAGE_RESET_INSTRUCTIONS_SENT,
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

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

The new enum key MESSAGE_MESSAGE_REMOVE_TAXA_LIST_TAXON_CONFIRM has a duplicated MESSAGE_ prefix, whereas other confirmation messages follow the simpler MESSAGE_<ACTION>_CONFIRM pattern (e.g. MESSAGE_REMOVE_MEMBER_CONFIRM). Renaming this key to MESSAGE_REMOVE_TAXA_LIST_TAXON_CONFIRM (and updating its usages) would keep the naming consistent and easier to read.

Copilot uses AI. Check for mistakes.
Comment on lines +17 to +24
const { data, isLoading, isFetching, error } = useAuthorizedQuery<TaxaList>({
queryKey: [API_ROUTES.TAXA_LISTS, id],
url: `${API_URL}/${API_ROUTES.TAXA_LISTS}/${id}/?project_id=${projectId}`,
})

const taxaList = useMemo(
() => (data ? convertServerRecord(data) : undefined),
[data]
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

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

useTaxaListDetails calls useAuthorizedQuery with a generic type of TaxaList, then immediately wraps the response with convertServerRecord which expects a ServerTaxaList (the raw API shape). For type safety and clarity, the query should be typed to ServerTaxaList and only the mapped TaxaList instance should be exposed from the hook.

Copilot uses AI. Check for mistakes.
Comment on lines +1268 to +1273
By default, queries for taxa that are directly in the TaxaList.
If include_descendants=true, also includes descendants (children or deeper) recursively.

Query parameters:
- taxa_list_id: ID of the taxa list to filter by
- include_descendants: Set to 'true' to include descendants (default: false)
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

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

TaxonTaxaListFilter's docstring says include_descendants defaults to false, but the implementation sets include_descendants_default = True and uses that as the fallback when the query param is missing. To avoid confusing API consumers (and future maintainers), either the default should be switched to False or the documentation updated to match the actual behaviour.

Suggested change
By default, queries for taxa that are directly in the TaxaList.
If include_descendants=true, also includes descendants (children or deeper) recursively.
Query parameters:
- taxa_list_id: ID of the taxa list to filter by
- include_descendants: Set to 'true' to include descendants (default: false)
By default, queries for taxa that are directly in the TaxaList and their descendants.
If include_descendants=false, limits results to only taxa directly in the TaxaList.
Query parameters:
- taxa_list_id: ID of the taxa list to filter by
- include_descendants: Set to 'false' to exclude descendants (default: true)

Copilot uses AI. Check for mistakes.
taxaListId: id as string,
})}
error={error}
isLoading={!id && isLoading}
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

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

In TaxaListDetails, the table isLoading flag is gated on !id && isLoading, but id here is the taxa list ID, not the taxon detail ID. This prevents the table from ever showing its loading state for the taxa list species (because id is always set on this route) and does not hide the loader when a taxon detail dialog (taxonId) is open; this condition should likely mirror the pattern used in species.tsx/occurrences.tsx and check !taxonId instead.

Suggested change
isLoading={!id && isLoading}
isLoading={!taxonId && isLoading}

Copilot uses AI. Check for mistakes.
setSort={setSort}
sort={sort}
/>
<AddTaxaListTaxonPopover taxaListId={id as string} />
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

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

TaxaListDetails always renders the AddTaxaListTaxonPopover in the header regardless of taxaList?.canUpdate, while the actions column in taxa-list-details-columns and the taxa lists table correctly gate mutation controls on canUpdate. To keep permissions consistent in the UI and avoid exposing add actions to users who cannot update the list, the header button should be shown only when taxaList?.canUpdate is true.

Suggested change
<AddTaxaListTaxonPopover taxaListId={id as string} />
{taxaList?.canUpdate ? (
<AddTaxaListTaxonPopover taxaListId={id as string} />
) : null}

Copilot uses AI. Check for mistakes.
Comment on lines +1662 to +1690
class TaxaListTaxonViewSet(viewsets.GenericViewSet, ProjectMixin):
"""
Nested ViewSet for managing taxa in a taxa list.
Accessed via /taxa/lists/{taxa_list_id}/taxa/
"""

serializer_class = TaxaListTaxonSerializer
permission_classes = [] # Allow public access for now
require_project = True

def get_taxa_list(self):
"""Get the parent taxa list from URL parameters."""
taxa_list_id = self.kwargs.get("taxalist_pk")
try:
return TaxaList.objects.get(pk=taxa_list_id)
except TaxaList.DoesNotExist:
raise api_exceptions.NotFound("Taxa list not found.")

def get_queryset(self):
"""Return taxa in the specified taxa list."""
taxa_list = self.get_taxa_list()
return taxa_list.taxa.all()

def list(self, request, taxalist_pk=None):
"""List all taxa in the taxa list."""
queryset = self.get_queryset()
serializer = self.get_serializer(queryset, many=True)
return Response({"count": queryset.count(), "results": serializer.data})

Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

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

TaxaListTaxonViewSet declares permission_classes = [] while still exposing create and delete_by_taxon endpoints, and it never checks that the active project or requesting user is allowed to modify the targeted taxa list. Because ProjectMixin.get_active_project() only fetches the project and does not perform any permission checks, this effectively lets any caller with a taxalist_pk and project_id add or remove taxa from any list, which is a security regression compared to other viewsets that use ObjectPermission/IsActiveStaffOrReadOnly; this viewset should reuse appropriate permission classes and/or enforce that the taxa list belongs to the active project and is mutable by the current user.

Copilot uses AI. Check for mistakes.
Comment on lines +141 to +190
class ProcessingServiceViewSet(DefaultViewSet, ProjectMixin):
"""
API endpoint that allows processing services to be viewed or edited.
"""

queryset = ProcessingService.objects.all()
serializer_class = ProcessingServiceSerializer
filterset_fields = ["projects"]
ordering_fields = ["id", "created_at", "updated_at"]
require_project = True

def get_queryset(self) -> QuerySet:
qs: QuerySet = super().get_queryset()
project = self.get_active_project()
if project:
qs = qs.filter(projects=project)
return qs

@extend_schema(parameters=[project_id_doc_param])
def list(self, request, *args, **kwargs):
return super().list(request, *args, **kwargs)

def create(self, request, *args, **kwargs):
data = request.data.copy()
data["slug"] = slugify(data["name"])
serializer = self.get_serializer(data=data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
# immediately get status after creating a processing service
instance: ProcessingService | None = serializer.instance
assert instance is not None
status_response = instance.get_status()
return Response(
{"instance": serializer.data, "status": status_response.dict()}, status=status.HTTP_201_CREATED
)

def perform_create(self, serializer):
"""
Create a ProcessingService and automatically assign it to the active project.

Users cannot manually assign processing services to projects for security reasons.
A processing service is always created in the context of the active project.

@TODO Do we need a permission check here to ensure the user can add processing services to the project?
"""
instance = serializer.save()
project = self.get_active_project()
if project:
instance.projects.add(project)

Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

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

ProcessingServiceViewSet now sets require_project = True and uses get_active_project() inside perform_create, but the frontend NewEntityDialog posts to the ml/processing_services collection without a project_id query parameter. As a result, create requests from the UI will hit this view without any active project and will raise a 404 from get_active_project, so either the API should accept the project via request body again or the client needs to be updated to pass project_id (or use a nested project route) when creating processing services.

Copilot uses AI. Check for mistakes.

get createdAt(): string {
get createdAt(): string | undefined {
if (!this._data.updated_at) {
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

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

Entity.createdAt now returns undefined whenever updated_at is falsy (line 21 checks !this._data.updated_at), which means newly created entities with no updated_at will never show a created date even though created_at is present. This looks like a typo and the guard should be based on the created_at field, not updated_at, to avoid hiding valid data in tables like the generic Entities list.

Suggested change
if (!this._data.updated_at) {
if (!this._data.created_at) {

Copilot uses AI. Check for mistakes.
...(description ? { description } : {}),
...(name ? { name } : {}),
project: projectId,
project_id: projectId,
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

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

Changing convertToServerFieldValues to send project_id instead of project breaks compatibility with several existing serializers (e.g. DeviceSerializer, SiteSerializer, StorageSourceSerializer, SourceImageCollectionSerializer) which still declare a writable project field and no project_id. Generic entity create/update flows (Sites, Devices, Storage, generic Entities list, and also the new taxa lists) will now POST/PUT an unknown project_id field while omitting the required project field, causing 400 validation errors; the helper should either keep using project or be made collection-aware so it matches each serializer's expected field name.

Suggested change
project_id: projectId,
project: projectId,

Copilot uses AI. Check for mistakes.
<PageHeader
title={translate(STRING.NAV_ITEM_TAXA_LISTS)}
subTitle={translate(STRING.RESULTS, {
total: taxaLists?.length ?? 0,
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

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

TaxaLists uses taxaLists?.length ?? 0 when building the RESULTS subtitle, even though the hook now exposes a total count from the paginated API. This means the header count will show only the number of items on the current page instead of the total number of taxa lists, which is inconsistent with other list pages and can be misleading when there are multiple pages; using total here would align it with the rest of the UI.

Suggested change
total: taxaLists?.length ?? 0,
total: total ?? taxaLists?.length ?? 0,

Copilot uses AI. Check for mistakes.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
ami/ml/views.py (1)

150-190: ⚠️ Potential issue | 🟠 Major

Prevent orphan ProcessingService records by validating project before database persistence.

With require_project = True, get_active_project() raises Http404 when the project is missing or invalid. However, serializer.save() is called first, persisting the ProcessingService record to the database. If get_active_project() then raises an exception, an orphan record remains in the database without project assignment.

Proposed fix
-        instance = serializer.save()
-        project = self.get_active_project()
+        project = self.get_active_project()
+        instance = serializer.save()
         if project:
             instance.projects.add(project)
🤖 Fix all issues with AI agents
In `@ami/main/api/views.py`:
- Around line 1691-1713: The create method may raise Taxon.DoesNotExist between
validation and Taxon.objects.get; to fix, catch that race by replacing or
wrapping the Taxon.objects.get(pk=taxon_id) call: use Django's
get_object_or_404(Taxon, pk=taxon_id) or try/except around Taxon.objects.get to
return a 404 Response when not found, then proceed to taxa_list.taxa.add(taxon)
and return self.get_serializer(taxon).data; update references in this method
(create, TaxaListTaxonInputSerializer, Taxon.objects.get, taxa_list.taxa.add,
get_serializer) accordingly.
- Around line 1662-1670: The class TaxaListTaxonViewSet currently sets
permission_classes = [], allowing unrestricted access; remove this override so
it inherits the default permissions (from DefaultViewSet/TaxaListViewSet) or
explicitly set permission_classes to the same tuple used by TaxaListViewSet
(e.g., [IsAuthenticated, <project-specific update permission class>); also
ensure the create() and destroy() handlers in TaxaListTaxonViewSet perform the
"can update" check (reuse the same permission check or call the parent/utility
that enforces the taxa-list "can_update" permission) so add explicit permission
checks if they aren’t already present.

In `@ui/src/data-services/hooks/taxa-lists/useTaxaListDetails.ts`:
- Around line 17-20: Change the query generic to match the server payload and
include projectId in the cache key: update the useAuthorizedQuery generic from
<TaxaList> to <ServerTaxaList> (the hook receives ServerTaxaList JSON which is
then converted via convertServerRecord) and add projectId to the queryKey array
passed to useAuthorizedQuery (alongside API_ROUTES.TAXA_LISTS and id) to prevent
cache collisions across projects.

In `@ui/src/data-services/models/entity.ts`:
- Around line 21-28: The createdAt getter incorrectly checks
this._data.updated_at before formatting; change the condition to check
this._data.created_at instead so that createdAt returns undefined only when
created_at is missing, and continue to call getFormatedDateTimeString with new
Date(this._data.created_at) inside the createdAt getter to format the existing
timestamp.

In `@ui/src/pages/species/species.tsx`:
- Around line 197-204: The onOpenAutoFocus handler on Dialog.Content currently
calls e.preventDefault(), which prevents focus from moving into the modal;
update that handler so after calling e.preventDefault() you manually transfer
focus into the dialog (e.g., call focus() on the Dialog.Content element or its
dialogRef/current DOM node, or focus its first focusable child) so keyboard
focus lands inside the modal; locate the onOpenAutoFocus prop on Dialog.Content
and implement the manual focus transfer (using e.currentTarget.focus() or
dialogRef.current.focus()/focusFirstDescendant) immediately after
preventDefault().

In `@ui/src/pages/taxa-list-details/add-taxa-list-taxon/add-taxa-list-taxon.tsx`:
- Around line 44-55: The Add button currently only checks for taxon presence, so
rapid clicks can fire duplicate addTaxaListTaxon calls; update the button to
also disable while the mutation is in flight (e.g., add disabled={!taxon ||
isAdding}), derive isAdding from the mutation's loading flag returned by the
hook that provides addTaxaListTaxon (use the isLoading/isMutating boolean from
the useMutation or API hook), and add a defensive guard at the top of the
onClick handler (if (isAdding) return) before awaiting addTaxaListTaxon({
taxaListId, taxonId: taxon.id }) to ensure a single inflight request; keep the
existing setTimeout(setTaxon...) behavior unchanged.

In
`@ui/src/pages/taxa-list-details/remove-taxa-list-taxon/remove-taxa-list-taxon-dialog.tsx`:
- Around line 56-60: The confirm Button currently only uses isSuccess to
disable, so rapid clicks can trigger duplicate requests; update the Button's
disabled prop to also check the mutation's in-flight state (e.g., isLoading or
isPending) returned by the removeTaxaListTaxon hook. Concretely, modify the
disabled expression to something like disabled={isSuccess || isLoading} (or
isPending) so the Button is disabled while removeTaxaListTaxon({ taxaListId,
taxonId }) is executing; ensure you reference the mutation status variable you
already get from the hook alongside isSuccess.

In `@ui/src/pages/taxa-list-details/taxa-list-details.tsx`:
- Around line 60-69: The AddTaxaListTaxonPopover is always rendered even when
the list is read-only; update the JSX to only render AddTaxaListTaxonPopover
when taxaList?.canUpdate is truthy (same gating used for remove), e.g.
conditionally render AddTaxaListTaxonPopover using the taxaList?.canUpdate flag
so the "Add taxon" action is hidden for read-only lists; ensure you reference
the existing props/ids (AddTaxaListTaxonPopover taxaListId={id as string}) and
leave SortControl unchanged.
- Around line 71-79: The Table's isLoading prop uses an inverted guard (!id &&
isLoading) so the loading state never shows when an id exists; update the
isLoading prop on the Table (in taxa-list-details.tsx where Table is rendered)
to a correct expression such as isLoading={isLoading || !id} so the table shows
loading when data is loading or when the id is not yet available (reference the
Table component and the id and isLoading variables).
🧹 Nitpick comments (6)
ui/src/pages/project/entities/new-entity-dialog.tsx (1)

2-2: Remove unused import.

API_ROUTES is imported but never used in this file.

♻️ Proposed fix
 import classNames from 'classnames'
-import { API_ROUTES } from 'data-services/constants'
 import { useCreateEntity } from 'data-services/hooks/entities/useCreateEntity'
ui/src/components/breadcrumbs/breadcrumbs.tsx (1)

24-32: Logic change approved; consider adding setMainBreadcrumb to dependency array.

The inverted condition correctly prevents setting mainBreadcrumb for the 'project' item. Note that setMainBreadcrumb is missing from the dependency array. While context setters are typically stable, adding it ensures exhaustive deps compliance.

♻️ Proposed fix
-  }, [navItems, activeNavItem])
+  }, [navItems, activeNavItem, setMainBreadcrumb])
ui/src/data-services/hooks/taxa-lists/useAddTaxaListTaxon.ts (1)

35-35: Minor inconsistency: reset is returned here but not in useRemoveTaxaListTaxon.

This hook returns reset in its return object, but the sibling hook useRemoveTaxaListTaxon does not. Consider aligning the return signatures for consistency.

ui/src/pages/project/sidebar/useSidebarSections.tsx (1)

147-150: Remove dead code.

This expression computes a value but doesn't assign or return it. It appears to be leftover debug code or an incomplete refactor.

🧹 Proposed fix
-  sidebarSections
-    .map(({ items }) => items)
-    .flat()
-    .find((item) => !!matchPath(item.path, location.pathname))
-
   return { sidebarSections, activeItem }
ami/ml/serializers.py (1)

133-158: Consider prefetching projects to avoid N+1 on list endpoints.

SerializerMethodField accesses obj.projects per instance; without prefetching in the viewset, list responses can trigger extra queries. Ensure the queryset prefetches this relation (or annotate IDs) to keep list performance stable.

ami/main/api/views.py (1)

1672-1678: Use raise ... from for proper exception chaining.

When re-raising as a different exception type, use from to preserve the original traceback for debugging.

♻️ Proposed fix
     def get_taxa_list(self):
         """Get the parent taxa list from URL parameters."""
         taxa_list_id = self.kwargs.get("taxalist_pk")
         try:
             return TaxaList.objects.get(pk=taxa_list_id)
         except TaxaList.DoesNotExist:
-            raise api_exceptions.NotFound("Taxa list not found.")
+            raise api_exceptions.NotFound("Taxa list not found.") from None

Comment on lines +1662 to +1670
class TaxaListTaxonViewSet(viewsets.GenericViewSet, ProjectMixin):
"""
Nested ViewSet for managing taxa in a taxa list.
Accessed via /taxa/lists/{taxa_list_id}/taxa/
"""

serializer_class = TaxaListTaxonSerializer
permission_classes = [] # Allow public access for now
require_project = True
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Security concern: Empty permission_classes allows unrestricted access.

permission_classes = [] on line 1669 allows unauthenticated access to add/remove taxa from taxa lists. The comment says "Allow public access for now" but this seems unintentional given:

  1. The PR discussion mentions checking "can update" permission
  2. The parent TaxaListViewSet inherits default permissions from DefaultViewSet

Consider applying appropriate permissions:

🔒 Proposed fix to add permission controls
 class TaxaListTaxonViewSet(viewsets.GenericViewSet, ProjectMixin):
     """
     Nested ViewSet for managing taxa in a taxa list.
     Accessed via /taxa/lists/{taxa_list_id}/taxa/
     """

     serializer_class = TaxaListTaxonSerializer
-    permission_classes = []  # Allow public access for now
+    permission_classes = [ObjectPermission]
     require_project = True
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
class TaxaListTaxonViewSet(viewsets.GenericViewSet, ProjectMixin):
"""
Nested ViewSet for managing taxa in a taxa list.
Accessed via /taxa/lists/{taxa_list_id}/taxa/
"""
serializer_class = TaxaListTaxonSerializer
permission_classes = [] # Allow public access for now
require_project = True
class TaxaListTaxonViewSet(viewsets.GenericViewSet, ProjectMixin):
"""
Nested ViewSet for managing taxa in a taxa list.
Accessed via /taxa/lists/{taxa_list_id}/taxa/
"""
serializer_class = TaxaListTaxonSerializer
permission_classes = [ObjectPermission]
require_project = True
🧰 Tools
🪛 Ruff (0.14.14)

[warning] 1669-1669: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

🤖 Prompt for AI Agents
In `@ami/main/api/views.py` around lines 1662 - 1670, The class
TaxaListTaxonViewSet currently sets permission_classes = [], allowing
unrestricted access; remove this override so it inherits the default permissions
(from DefaultViewSet/TaxaListViewSet) or explicitly set permission_classes to
the same tuple used by TaxaListViewSet (e.g., [IsAuthenticated,
<project-specific update permission class>); also ensure the create() and
destroy() handlers in TaxaListTaxonViewSet perform the "can update" check (reuse
the same permission check or call the parent/utility that enforces the taxa-list
"can_update" permission) so add explicit permission checks if they aren’t
already present.

Comment on lines +1691 to +1713
def create(self, request, taxalist_pk=None):
"""Add a taxon to the taxa list."""
taxa_list = self.get_taxa_list()

# Validate input
input_serializer = TaxaListTaxonInputSerializer(data=request.data)
input_serializer.is_valid(raise_exception=True)
taxon_id = input_serializer.validated_data["taxon_id"]

# Check if already exists
if taxa_list.taxa.filter(pk=taxon_id).exists():
return Response(
{"non_field_errors": ["Taxon is already in this taxa list."]},
status=status.HTTP_400_BAD_REQUEST,
)

# Add taxon
taxon = Taxon.objects.get(pk=taxon_id)
taxa_list.taxa.add(taxon)

# Return the added taxon
serializer = self.get_serializer(taxon)
return Response(serializer.data, status=status.HTTP_201_CREATED)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential race condition between validation and Taxon.objects.get().

The TaxaListTaxonInputSerializer validates that the taxon exists, but there's a window between validation (line 1697) and the Taxon.objects.get() call (line 1708) where the taxon could be deleted. While rare, this would cause an unhandled DoesNotExist exception.

Consider using get_object_or_404 or handling the exception:

🛡️ Proposed defensive fix
         # Add taxon
-        taxon = Taxon.objects.get(pk=taxon_id)
+        try:
+            taxon = Taxon.objects.get(pk=taxon_id)
+        except Taxon.DoesNotExist:
+            return Response(
+                {"taxon_id": ["Taxon does not exist."]},
+                status=status.HTTP_400_BAD_REQUEST,
+            )
         taxa_list.taxa.add(taxon)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def create(self, request, taxalist_pk=None):
"""Add a taxon to the taxa list."""
taxa_list = self.get_taxa_list()
# Validate input
input_serializer = TaxaListTaxonInputSerializer(data=request.data)
input_serializer.is_valid(raise_exception=True)
taxon_id = input_serializer.validated_data["taxon_id"]
# Check if already exists
if taxa_list.taxa.filter(pk=taxon_id).exists():
return Response(
{"non_field_errors": ["Taxon is already in this taxa list."]},
status=status.HTTP_400_BAD_REQUEST,
)
# Add taxon
taxon = Taxon.objects.get(pk=taxon_id)
taxa_list.taxa.add(taxon)
# Return the added taxon
serializer = self.get_serializer(taxon)
return Response(serializer.data, status=status.HTTP_201_CREATED)
def create(self, request, taxalist_pk=None):
"""Add a taxon to the taxa list."""
taxa_list = self.get_taxa_list()
# Validate input
input_serializer = TaxaListTaxonInputSerializer(data=request.data)
input_serializer.is_valid(raise_exception=True)
taxon_id = input_serializer.validated_data["taxon_id"]
# Check if already exists
if taxa_list.taxa.filter(pk=taxon_id).exists():
return Response(
{"non_field_errors": ["Taxon is already in this taxa list."]},
status=status.HTTP_400_BAD_REQUEST,
)
# Add taxon
try:
taxon = Taxon.objects.get(pk=taxon_id)
except Taxon.DoesNotExist:
return Response(
{"taxon_id": ["Taxon does not exist."]},
status=status.HTTP_400_BAD_REQUEST,
)
taxa_list.taxa.add(taxon)
# Return the added taxon
serializer = self.get_serializer(taxon)
return Response(serializer.data, status=status.HTTP_201_CREATED)
🧰 Tools
🪛 Ruff (0.14.14)

[warning] 1691-1691: Unused method argument: taxalist_pk

(ARG002)

🤖 Prompt for AI Agents
In `@ami/main/api/views.py` around lines 1691 - 1713, The create method may raise
Taxon.DoesNotExist between validation and Taxon.objects.get; to fix, catch that
race by replacing or wrapping the Taxon.objects.get(pk=taxon_id) call: use
Django's get_object_or_404(Taxon, pk=taxon_id) or try/except around
Taxon.objects.get to return a 404 Response when not found, then proceed to
taxa_list.taxa.add(taxon) and return self.get_serializer(taxon).data; update
references in this method (create, TaxaListTaxonInputSerializer,
Taxon.objects.get, taxa_list.taxa.add, get_serializer) accordingly.

Comment on lines +17 to +20
const { data, isLoading, isFetching, error } = useAuthorizedQuery<TaxaList>({
queryKey: [API_ROUTES.TAXA_LISTS, id],
url: `${API_URL}/${API_ROUTES.TAXA_LISTS}/${id}/?project_id=${projectId}`,
})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, find and read the target file
cat -n ui/src/data-services/hooks/taxa-lists/useTaxaListDetails.ts

Repository: RolnickLab/antenna

Length of output: 1151


🏁 Script executed:

# Search for TaxaList and ServerTaxaList type definitions
rg "type\s+ServerTaxaList|interface\s+ServerTaxaList|type\s+TaxaList|interface\s+TaxaList" -A 5

Repository: RolnickLab/antenna

Length of output: 520


🏁 Script executed:

# Check how useAuthorizedQuery is used in other hooks for comparison
fd -e ts -e tsx . ui/src/data-services/hooks | head -20

Repository: RolnickLab/antenna

Length of output: 1137


🏁 Script executed:

# Look for instances of cache key patterns with projectId
rg "queryKey.*projectId" -B 2 -A 2

Repository: RolnickLab/antenna

Length of output: 1937


Correct the generic type and include projectId in the cache key.

The generic type should match what Axios actually returns from the server. The hook receives ServerTaxaList JSON and converts it via convertServerRecord (line 6), so the query generic must be <ServerTaxaList>, not <TaxaList>. Additionally, the projectId is part of the API request but missing from the cache key, creating the potential for cache collisions across projects—other hooks in this codebase (e.g., useCaptureDetails) include variable parameters in the key to avoid this issue.

Proposed fix
-  const { data, isLoading, isFetching, error } = useAuthorizedQuery<TaxaList>({
-    queryKey: [API_ROUTES.TAXA_LISTS, id],
+  const { data, isLoading, isFetching, error } =
+    useAuthorizedQuery<ServerTaxaList>({
+      queryKey: [API_ROUTES.TAXA_LISTS, projectId, id],
     url: `${API_URL}/${API_ROUTES.TAXA_LISTS}/${id}/?project_id=${projectId}`,
-  })
+    })
🤖 Prompt for AI Agents
In `@ui/src/data-services/hooks/taxa-lists/useTaxaListDetails.ts` around lines 17
- 20, Change the query generic to match the server payload and include projectId
in the cache key: update the useAuthorizedQuery generic from <TaxaList> to
<ServerTaxaList> (the hook receives ServerTaxaList JSON which is then converted
via convertServerRecord) and add projectId to the queryKey array passed to
useAuthorizedQuery (alongside API_ROUTES.TAXA_LISTS and id) to prevent cache
collisions across projects.

Comment on lines +21 to 28
get createdAt(): string | undefined {
if (!this._data.updated_at) {
return undefined
}

return getFormatedDateTimeString({
date: new Date(this._data.created_at),
})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Bug: createdAt checks updated_at instead of created_at.

The getter returns undefined when updated_at is missing, but this should logically check created_at instead. An entity could have a created_at timestamp without an updated_at (if never modified), and this code would incorrectly return undefined for createdAt.

🐛 Proposed fix
  get createdAt(): string | undefined {
-   if (!this._data.updated_at) {
+   if (!this._data.created_at) {
      return undefined
    }

    return getFormatedDateTimeString({
      date: new Date(this._data.created_at),
    })
  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
get createdAt(): string | undefined {
if (!this._data.updated_at) {
return undefined
}
return getFormatedDateTimeString({
date: new Date(this._data.created_at),
})
get createdAt(): string | undefined {
if (!this._data.created_at) {
return undefined
}
return getFormatedDateTimeString({
date: new Date(this._data.created_at),
})
}
🤖 Prompt for AI Agents
In `@ui/src/data-services/models/entity.ts` around lines 21 - 28, The createdAt
getter incorrectly checks this._data.updated_at before formatting; change the
condition to check this._data.created_at instead so that createdAt returns
undefined only when created_at is missing, and continue to call
getFormatedDateTimeString with new Date(this._data.created_at) inside the
createdAt getter to format the existing timestamp.

Comment on lines 197 to +204
<Dialog.Content
ariaCloselabel={translate(STRING.CLOSE)}
isLoading={isLoading}
error={error}
isLoading={isLoading}
onOpenAutoFocus={(e) => {
/* Prevent tooltip auto focus */
e.preventDefault()
}}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the file and the specific lines mentioned
cat -n ui/src/pages/species/species.tsx | sed -n '190,210p'

Repository: RolnickLab/antenna

Length of output: 784


🏁 Script executed:

# Let's get more context around this section - understand the component structure
head -n 220 ui/src/pages/species/species.tsx | tail -n 40

Repository: RolnickLab/antenna

Length of output: 942


🏁 Script executed:

# Search for Dialog.Content usage in the codebase to understand the API
fd -t tsx -t ts | xargs grep -l "Dialog.Content" | head -5

Repository: RolnickLab/antenna

Length of output: 232


🏁 Script executed:

# Find if design-system package is available locally
fd -t d "design-system" | head -3

Repository: RolnickLab/antenna

Length of output: 83


🏁 Script executed:

# Examine design-system Dialog component
find ui/src/design-system -type f -name "*dialog*" -o -name "*Dialog*" | head -10

Repository: RolnickLab/antenna

Length of output: 169


🏁 Script executed:

# Search for Dialog.Content usage in codebase to see patterns
rg "onOpenAutoFocus" --type tsx -B 2 -A 5 | head -50

Repository: RolnickLab/antenna

Length of output: 89


🏁 Script executed:

# Check design-system structure
ls -la ui/src/design-system/ | head -20

Repository: RolnickLab/antenna

Length of output: 344


🏁 Script executed:

# Search for Dialog implementation - likely in components
find ui/src/design-system -type f \( -name "*.tsx" -o -name "*.ts" \) | xargs grep -l "Dialog" | head -5

Repository: RolnickLab/antenna

Length of output: 111


🏁 Script executed:

# Examine the Dialog component implementation
cat -n ui/src/design-system/components/dialog/dialog.tsx

Repository: RolnickLab/antenna

Length of output: 2794


🏁 Script executed:

# Also check if there are other Dialog.Content usage patterns with onOpenAutoFocus
rg "onOpenAutoFocus" -B 3 -A 3

Repository: RolnickLab/antenna

Length of output: 3112


🏁 Script executed:

# Check if SpeciesDetails has any focus management
rg "useEffect.*focus|autoFocus|setFocus" ui/src/pages/species/ -A 2 -B 2

Repository: RolnickLab/antenna

Length of output: 44


🏁 Script executed:

# Check Dialog.Root in the species.tsx context - is it modal?
cat -n ui/src/pages/species/species.tsx | sed -n '180,220p'

Repository: RolnickLab/antenna

Length of output: 1142


Move focus into the dialog after preventing auto‑focus.

Calling preventDefault() stops the default focus transfer, leaving keyboard focus on the background page. This breaks modal dialog a11y. Add a manual focus call to move focus into the dialog.

Note: Dialog.Content already has tabIndex={-1} in the design-system, so no need to add it. Just ensure focus is transferred manually:

💡 Suggested fix
         onOpenAutoFocus={(e) => {
           /* Prevent tooltip auto focus */
           e.preventDefault()
+          ;(e.currentTarget as HTMLElement).focus()
         }}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Dialog.Content
ariaCloselabel={translate(STRING.CLOSE)}
isLoading={isLoading}
error={error}
isLoading={isLoading}
onOpenAutoFocus={(e) => {
/* Prevent tooltip auto focus */
e.preventDefault()
}}
<Dialog.Content
ariaCloselabel={translate(STRING.CLOSE)}
error={error}
isLoading={isLoading}
onOpenAutoFocus={(e) => {
/* Prevent tooltip auto focus */
e.preventDefault()
;(e.currentTarget as HTMLElement).focus()
}}
🤖 Prompt for AI Agents
In `@ui/src/pages/species/species.tsx` around lines 197 - 204, The onOpenAutoFocus
handler on Dialog.Content currently calls e.preventDefault(), which prevents
focus from moving into the modal; update that handler so after calling
e.preventDefault() you manually transfer focus into the dialog (e.g., call
focus() on the Dialog.Content element or its dialogRef/current DOM node, or
focus its first focusable child) so keyboard focus lands inside the modal;
locate the onOpenAutoFocus prop on Dialog.Content and implement the manual focus
transfer (using e.currentTarget.focus() or
dialogRef.current.focus()/focusFirstDescendant) immediately after
preventDefault().

Comment on lines +44 to +55
<Button
disabled={!taxon}
onClick={async () => {
if (taxon) {
await addTaxaListTaxon({ taxaListId, taxonId: taxon.id })
setTimeout(() => setTaxon(undefined), SUCCESS_TIMEOUT)
}
}}
size="small"
variant="success"
>
<span>{translate(STRING.ADD)}</span>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Disable the Add action while the mutation is in flight.

Right now the button is enabled during loading, so rapid clicks can enqueue duplicate add requests.

🛠️ Proposed fix
-            disabled={!taxon}
+            disabled={!taxon || isLoading}
🤖 Prompt for AI Agents
In `@ui/src/pages/taxa-list-details/add-taxa-list-taxon/add-taxa-list-taxon.tsx`
around lines 44 - 55, The Add button currently only checks for taxon presence,
so rapid clicks can fire duplicate addTaxaListTaxon calls; update the button to
also disable while the mutation is in flight (e.g., add disabled={!taxon ||
isAdding}), derive isAdding from the mutation's loading flag returned by the
hook that provides addTaxaListTaxon (use the isLoading/isMutating boolean from
the useMutation or API hook), and add a defensive guard at the top of the
onClick handler (if (isAdding) return) before awaiting addTaxaListTaxon({
taxaListId, taxonId: taxon.id }) to ensure a single inflight request; keep the
existing setTimeout(setTaxon...) behavior unchanged.

Comment on lines +56 to +60
<Button
disabled={isSuccess}
onClick={() => removeTaxaListTaxon({ taxaListId, taxonId })}
size="small"
variant="destructive"
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Disable confirm while the request is in flight.

Prevents double‑submits if the user clicks multiple times during deletion.

Proposed fix
-            <Button
-              disabled={isSuccess}
+            <Button
+              disabled={isLoading || isSuccess}
               onClick={() => removeTaxaListTaxon({ taxaListId, taxonId })}
               size="small"
               variant="destructive"
             >
🤖 Prompt for AI Agents
In
`@ui/src/pages/taxa-list-details/remove-taxa-list-taxon/remove-taxa-list-taxon-dialog.tsx`
around lines 56 - 60, The confirm Button currently only uses isSuccess to
disable, so rapid clicks can trigger duplicate requests; update the Button's
disabled prop to also check the mutation's in-flight state (e.g., isLoading or
isPending) returned by the removeTaxaListTaxon hook. Concretely, modify the
disabled expression to something like disabled={isSuccess || isLoading} (or
isPending) so the Button is disabled while removeTaxaListTaxon({ taxaListId,
taxonId }) is executing; ensure you reference the mutation status variable you
already get from the hook alongside isSuccess.

Comment on lines +60 to +69
<SortControl
columns={columns({
canUpdate: taxaList?.canUpdate,
projectId: projectId as string,
taxaListId: id as string,
})}
setSort={setSort}
sort={sort}
/>
<AddTaxaListTaxonPopover taxaListId={id as string} />
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Hide “Add taxon” when the list is read‑only.

This aligns with the remove action gating and avoids offering an action that will fail.

Proposed fix
-        <AddTaxaListTaxonPopover taxaListId={id as string} />
+        {taxaList?.canUpdate ? (
+          <AddTaxaListTaxonPopover taxaListId={id as string} />
+        ) : null}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<SortControl
columns={columns({
canUpdate: taxaList?.canUpdate,
projectId: projectId as string,
taxaListId: id as string,
})}
setSort={setSort}
sort={sort}
/>
<AddTaxaListTaxonPopover taxaListId={id as string} />
<SortControl
columns={columns({
canUpdate: taxaList?.canUpdate,
projectId: projectId as string,
taxaListId: id as string,
})}
setSort={setSort}
sort={sort}
/>
{taxaList?.canUpdate ? (
<AddTaxaListTaxonPopover taxaListId={id as string} />
) : null}
🤖 Prompt for AI Agents
In `@ui/src/pages/taxa-list-details/taxa-list-details.tsx` around lines 60 - 69,
The AddTaxaListTaxonPopover is always rendered even when the list is read-only;
update the JSX to only render AddTaxaListTaxonPopover when taxaList?.canUpdate
is truthy (same gating used for remove), e.g. conditionally render
AddTaxaListTaxonPopover using the taxaList?.canUpdate flag so the "Add taxon"
action is hidden for read-only lists; ensure you reference the existing
props/ids (AddTaxaListTaxonPopover taxaListId={id as string}) and leave
SortControl unchanged.

Comment on lines +71 to +79
<Table
columns={columns({
canUpdate: taxaList?.canUpdate,
projectId: projectId as string,
taxaListId: id as string,
})}
error={error}
isLoading={!id && isLoading}
items={species}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Loading state never shows because of the inverted guard.

!id && isLoading is false for normal routes, so the table won’t display a loading state.

Proposed fix
-        isLoading={!id && isLoading}
+        isLoading={isLoading}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Table
columns={columns({
canUpdate: taxaList?.canUpdate,
projectId: projectId as string,
taxaListId: id as string,
})}
error={error}
isLoading={!id && isLoading}
items={species}
<Table
columns={columns({
canUpdate: taxaList?.canUpdate,
projectId: projectId as string,
taxaListId: id as string,
})}
error={error}
isLoading={isLoading}
items={species}
🤖 Prompt for AI Agents
In `@ui/src/pages/taxa-list-details/taxa-list-details.tsx` around lines 71 - 79,
The Table's isLoading prop uses an inverted guard (!id && isLoading) so the
loading state never shows when an id exists; update the isLoading prop on the
Table (in taxa-list-details.tsx where Table is rendered) to a correct expression
such as isLoading={isLoading || !id} so the table shows loading when data is
loading or when the id is not yet available (reference the Table component and
the id and isLoading variables).

@mihow
Copy link
Collaborator

mihow commented Feb 4, 2026

We are going to need an import option after this too :)

@mihow
Copy link
Collaborator

mihow commented Feb 5, 2026

Hm, I guess we are not ready for the project => project_id change. I thought we did most of that last year.

Here are some fixes in response to the other feedback: #1119

@annavik
Copy link
Member Author

annavik commented Feb 6, 2026

We are going to need an import option after this too :)

Yes!

@annavik
Copy link
Member Author

annavik commented Feb 6, 2026

Hm, I guess we are not ready for the project => project_id change. I thought we did most of that last year.

Here are some fixes in response to the other feedback: #1119

I

@annavik I merged the API restructuring (#1104) to this branch! I left some comments there about the specific changes, but I figure it's time to bring the conversation back to this branch :)

Maybe one final test & look-over?

Thanks for the updates! I tested everything in this branch, I found one small thing needed related to project_id and pushed a fix. After that, works like a charm 👌

For the PR to address feedback, see comments: #1119

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants