Conversation
* 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>
✅ Deploy Preview for antenna-preview ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for antenna-ssec ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
📝 WalkthroughWalkthroughAdds 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
Sequence Diagram(s)mermaid Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
Co-Authored-By: Claude <noreply@anthropic.com>
|
Exciting! I will look into the missing BE parts. But would love to merge some version of this sooner and continue to improve |
|
Some observations when using the new endpoints to add and remove taxa from lists:
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 🙏 |
|
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! :) |
… 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>
|
@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. |
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, |
There was a problem hiding this comment.
@annavik this may be bigger change, but the backend is already expecting project_id everywhere now for create & update requests.
There was a problem hiding this comment.
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 improvedtaxa_list_idfiltering 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
NewEntityDialogimportsAPI_ROUTESbut 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.
| MESSAGE_PERMISSIONS_MISSING, | ||
| MESSAGE_PROCESS_NOW_TOOLTIP, | ||
| MESSAGE_REMOVE_MEMBER_CONFIRM, | ||
| MESSAGE_MESSAGE_REMOVE_TAXA_LIST_TAXON_CONFIRM, | ||
| MESSAGE_RESET_INSTRUCTIONS_SENT, |
There was a problem hiding this comment.
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.
| 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] |
There was a problem hiding this comment.
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.
| 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) |
There was a problem hiding this comment.
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.
| 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) |
| taxaListId: id as string, | ||
| })} | ||
| error={error} | ||
| isLoading={!id && isLoading} |
There was a problem hiding this comment.
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.
| isLoading={!id && isLoading} | |
| isLoading={!taxonId && isLoading} |
| setSort={setSort} | ||
| sort={sort} | ||
| /> | ||
| <AddTaxaListTaxonPopover taxaListId={id as string} /> |
There was a problem hiding this comment.
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.
| <AddTaxaListTaxonPopover taxaListId={id as string} /> | |
| {taxaList?.canUpdate ? ( | |
| <AddTaxaListTaxonPopover taxaListId={id as string} /> | |
| ) : null} |
| 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}) | ||
|
|
There was a problem hiding this comment.
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.
| 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) | ||
|
|
There was a problem hiding this comment.
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.
|
|
||
| get createdAt(): string { | ||
| get createdAt(): string | undefined { | ||
| if (!this._data.updated_at) { |
There was a problem hiding this comment.
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.
| if (!this._data.updated_at) { | |
| if (!this._data.created_at) { |
| ...(description ? { description } : {}), | ||
| ...(name ? { name } : {}), | ||
| project: projectId, | ||
| project_id: projectId, |
There was a problem hiding this comment.
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.
| project_id: projectId, | |
| project: projectId, |
| <PageHeader | ||
| title={translate(STRING.NAV_ITEM_TAXA_LISTS)} | ||
| subTitle={translate(STRING.RESULTS, { | ||
| total: taxaLists?.length ?? 0, |
There was a problem hiding this comment.
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.
| total: taxaLists?.length ?? 0, | |
| total: total ?? taxaLists?.length ?? 0, |
There was a problem hiding this comment.
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 | 🟠 MajorPrevent orphan ProcessingService records by validating project before database persistence.
With
require_project = True,get_active_project()raisesHttp404when the project is missing or invalid. However,serializer.save()is called first, persisting the ProcessingService record to the database. Ifget_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_ROUTESis 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 addingsetMainBreadcrumbto dependency array.The inverted condition correctly prevents setting
mainBreadcrumbfor the 'project' item. Note thatsetMainBreadcrumbis 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:resetis returned here but not inuseRemoveTaxaListTaxon.This hook returns
resetin its return object, but the sibling hookuseRemoveTaxaListTaxondoes 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 prefetchingprojectsto avoid N+1 on list endpoints.
SerializerMethodFieldaccessesobj.projectsper 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: Useraise ... fromfor proper exception chaining.When re-raising as a different exception type, use
fromto 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
| 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 |
There was a problem hiding this comment.
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:
- The PR discussion mentions checking "can update" permission
- The parent
TaxaListViewSetinherits default permissions fromDefaultViewSet
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.
| 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.
| 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) |
There was a problem hiding this comment.
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.
| 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.
| const { data, isLoading, isFetching, error } = useAuthorizedQuery<TaxaList>({ | ||
| queryKey: [API_ROUTES.TAXA_LISTS, id], | ||
| url: `${API_URL}/${API_ROUTES.TAXA_LISTS}/${id}/?project_id=${projectId}`, | ||
| }) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, find and read the target file
cat -n ui/src/data-services/hooks/taxa-lists/useTaxaListDetails.tsRepository: 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 5Repository: 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 -20Repository: RolnickLab/antenna
Length of output: 1137
🏁 Script executed:
# Look for instances of cache key patterns with projectId
rg "queryKey.*projectId" -B 2 -A 2Repository: 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.
| get createdAt(): string | undefined { | ||
| if (!this._data.updated_at) { | ||
| return undefined | ||
| } | ||
|
|
||
| return getFormatedDateTimeString({ | ||
| date: new Date(this._data.created_at), | ||
| }) |
There was a problem hiding this comment.
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.
| 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.
| <Dialog.Content | ||
| ariaCloselabel={translate(STRING.CLOSE)} | ||
| isLoading={isLoading} | ||
| error={error} | ||
| isLoading={isLoading} | ||
| onOpenAutoFocus={(e) => { | ||
| /* Prevent tooltip auto focus */ | ||
| e.preventDefault() | ||
| }} |
There was a problem hiding this comment.
🧩 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 40Repository: 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 -5Repository: RolnickLab/antenna
Length of output: 232
🏁 Script executed:
# Find if design-system package is available locally
fd -t d "design-system" | head -3Repository: 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 -10Repository: 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 -50Repository: RolnickLab/antenna
Length of output: 89
🏁 Script executed:
# Check design-system structure
ls -la ui/src/design-system/ | head -20Repository: 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 -5Repository: RolnickLab/antenna
Length of output: 111
🏁 Script executed:
# Examine the Dialog component implementation
cat -n ui/src/design-system/components/dialog/dialog.tsxRepository: 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 3Repository: 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 2Repository: 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.
| <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().
| <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> |
There was a problem hiding this comment.
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.
| <Button | ||
| disabled={isSuccess} | ||
| onClick={() => removeTaxaListTaxon({ taxaListId, taxonId })} | ||
| size="small" | ||
| variant="destructive" |
There was a problem hiding this comment.
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.
| <SortControl | ||
| columns={columns({ | ||
| canUpdate: taxaList?.canUpdate, | ||
| projectId: projectId as string, | ||
| taxaListId: id as string, | ||
| })} | ||
| setSort={setSort} | ||
| sort={sort} | ||
| /> | ||
| <AddTaxaListTaxonPopover taxaListId={id as string} /> |
There was a problem hiding this comment.
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.
| <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.
| <Table | ||
| columns={columns({ | ||
| canUpdate: taxaList?.canUpdate, | ||
| projectId: projectId as string, | ||
| taxaListId: id as string, | ||
| })} | ||
| error={error} | ||
| isLoading={!id && isLoading} | ||
| items={species} |
There was a problem hiding this comment.
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.
| <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).
|
We are going to need an import option after this too :) |
|
Hm, I guess we are not ready for the Here are some fixes in response to the other feedback: #1119 |
Yes! |
I
Thanks for the updates! I tested everything in this branch, I found one small thing needed related to For the PR to address feedback, see comments: #1119 |



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
Missing FE stuff
Notes
Screenshots
List view:

Create view:

Edit view:

Delete view:

Detail view:

Summary by CodeRabbit
New Features
UI / Navigation
API / Backend
Tests