diff --git a/specifyweb/backend/stored_queries/urls.py b/specifyweb/backend/stored_queries/urls.py index ff5b0aee3fb..ca4f0844a53 100644 --- a/specifyweb/backend/stored_queries/urls.py +++ b/specifyweb/backend/stored_queries/urls.py @@ -10,5 +10,6 @@ path('make_recordset/', views.make_recordset), path('merge_recordsets/', views.merge_recordsets), path('return_loan_preps/', views.return_loan_preps), - path('batch_edit/', views.batch_edit) + path('batch_edit/', views.batch_edit), + path('query//ids/', views.query_ids), ] diff --git a/specifyweb/backend/stored_queries/views.py b/specifyweb/backend/stored_queries/views.py index 8ead7e580cf..36ade9521e6 100644 --- a/specifyweb/backend/stored_queries/views.py +++ b/specifyweb/backend/stored_queries/views.py @@ -104,9 +104,44 @@ def query(request, id): limit=limit, offset=offset ) + return HttpResponse(toJson(data), content_type='application/json') +@require_GET +@login_maybe_required +@never_cache +def query_ids(request, id): + """Executes the query with id and returns only the record IDs of the results as JSON.""" + check_permission_targets(request.specify_collection.id, request.specify_user.id, [QueryBuilderPt.execute]) + offset = int(request.GET.get('offset', 0)) + + with models.session_context() as session: + sp_query = session.query(models.SpQuery).get(int(id)) + distinct = sp_query.selectDistinct + tableid = sp_query.contextTableId + + if sp_query is None: + return HttpResponseBadRequest(f"SpQuery with id {id} does not exist.") + + field_specs = [QueryField.from_spqueryfield(field, value_from_request(field, request.GET)) + for field in sorted(sp_query.fields, key=lambda field: field.position)] + + data = execute( + session=session, + collection=request.specify_collection, + user=request.specify_user, + tableid=tableid, + distinct=distinct, + series=False, + count_only=False, + field_specs=field_specs, + limit=None, + offset=offset + ) + + ids = [row[0] for row in data.get("results", [])] + return HttpResponse(toJson({ "ids": ids }), content_type='application/json') @require_POST @login_maybe_required diff --git a/specifyweb/frontend/.DS_Store b/specifyweb/frontend/.DS_Store new file mode 100644 index 00000000000..a62bf900d0a Binary files /dev/null and b/specifyweb/frontend/.DS_Store differ diff --git a/specifyweb/frontend/js_src/lib/components/QueryBuilder/Results.tsx b/specifyweb/frontend/js_src/lib/components/QueryBuilder/Results.tsx index 5e46eb601d8..3b211cddfb5 100644 --- a/specifyweb/frontend/js_src/lib/components/QueryBuilder/Results.tsx +++ b/specifyweb/frontend/js_src/lib/components/QueryBuilder/Results.tsx @@ -5,10 +5,12 @@ import { useAsyncState } from '../../hooks/useAsyncState'; import { useInfiniteScroll } from '../../hooks/useInfiniteScroll'; import { commonText } from '../../localization/common'; import { interactionsText } from '../../localization/interactions'; +import {ajax} from "../../utils/ajax"; import { f } from '../../utils/functools'; import { type GetSet, type RA } from '../../utils/types'; import { Container, H3 } from '../Atoms'; import { Button } from '../Atoms/Button'; +import { LoadingContext } from '../Core/Contexts'; import type { SpecifyResource } from '../DataModel/legacyTypes'; import { schema } from '../DataModel/schema'; import type { SpecifyTable } from '../DataModel/specifyTable'; @@ -188,6 +190,8 @@ export function QueryResults(props: QueryResultsProps): JSX.Element { typeof loadedResults?.[0]?.[0] === 'string' && loadedResults !== undefined; const metaColumns = (showLineNumber ? 1 : 0) + 2; + const loading = React.useContext(LoadingContext); + return (
@@ -210,7 +214,45 @@ export function QueryResults(props: QueryResultsProps): JSX.Element { > {interactionsText.deselectAll()} + + + )} + {/* Buttons for select All and invert selection*/} + {(totalCount ?? 0) > 0 && (totalCount ?? 0) < 10_000_000 && queryResource?.get("id") && ( + { + loading( + fetchAllIDs(queryResource, loading) + .then((allIDs) => { + setSelectedRows(new Set(allIDs)); + handleSelected?.(allIDs); + }) + .catch((error) => { + console.error('Error fetching all IDs:', error); + }) + ); + }} + > + {interactionsText.selectAll()} + )} + {(totalCount ?? 0) > 0 && queryResource?.get("id") && ( + { + if (!loadedResults) return; + const allIDs = new Set( + loadedResults.map((result) => result[queryIdField] as number) + ); + const invertedSelection = new Set( Array.from(allIDs).filter(id => !(selectedRows.has(id)))); + setSelectedRows(invertedSelection); + handleSelected?.(Array.from(invertedSelection)); + + }} + > + {interactionsText.invertSelection()} + + )} +
{displayedFields.length > 0 && visibleFieldSpecs.length > 0 && @@ -468,3 +510,40 @@ export function canMerge(table: SpecifyTable): boolean { canMerge; return canMergeOtherTables || canMergePaleoContext || canMergeCollectingEvent; } + +async function fetchAllIDs( +queryResource: SpecifyResource | undefined, + loading: (promise: Promise) => void +): Promise> { + + const queryId = queryResource!.get('id'); + + if (!queryResource) { + throw new Error('Query resource is undefined'); + } + + return new Promise>((resolve, reject) => { + loading( + (async () => { + try { + console.log('Fetching all IDs for query'); + const startTime = performance.now(); + const {data} = await ajax<{readonly ids: RA}>( + `/stored_query/query/${queryId}/ids/`, + { + headers: { Accept: 'application/json' }, + errorMode: "visible", + } + ); + + const elapsed = ((performance.now() - startTime) / 1000).toFixed(2); + console.log(`Fetched ${data.ids.length} IDs in ${elapsed} seconds`); + + resolve(data.ids); + } catch (error) { + reject(error); + } + })() + ); + }); +} diff --git a/specifyweb/frontend/js_src/lib/localization/interactions.ts b/specifyweb/frontend/js_src/lib/localization/interactions.ts index 8d67412b9ad..65c27ceab97 100644 --- a/specifyweb/frontend/js_src/lib/localization/interactions.ts +++ b/specifyweb/frontend/js_src/lib/localization/interactions.ts @@ -421,4 +421,7 @@ export const interactionsText = createDictionary({ 'ru-ru': 'Нет в наличии', 'uk-ua': 'Не доступно', }, + invertSelection: { + "en-us": "Invert Selection", + } } as const);