Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
0cfc311
added dummy buttons
Iwantexpresso Jan 27, 2026
d43f15c
Lint code with ESLint and Prettier
Iwantexpresso Jan 27, 2026
09884a4
added fetch all ids function ad functionality to select all button
Iwantexpresso Jan 27, 2026
1cada53
implemented invert selection button and interactions for Invert text
Iwantexpresso Jan 29, 2026
09e3729
Merge branch 'issue-7485' of https://github.com/specify/specify7 into…
Iwantexpresso Jan 29, 2026
ceb428f
removed unused import and edited total count for future use
Iwantexpresso Feb 2, 2026
9ca2fca
Lint code with ESLint and Prettier
Iwantexpresso Feb 2, 2026
67e8f5d
made new api endpoint to retreive resource Ids only of the currently …
Iwantexpresso Feb 3, 2026
74ce965
Merge branch 'issue-7485' of https://github.com/specify/specify7 into…
Iwantexpresso Feb 3, 2026
38ed1f2
removed unused import, added comment to endpoint
Iwantexpresso Feb 3, 2026
0c9f1f5
removed dulplicate imports
Iwantexpresso Feb 3, 2026
369601b
fixed js error run
Iwantexpresso Feb 3, 2026
037a3d5
Lint code with ESLint and Prettier
Iwantexpresso Feb 3, 2026
b4e8935
edited invert select to use fetch all IDs as well, reduced max amount…
Iwantexpresso Feb 9, 2026
e26691c
Merge branch 'issue-7485' of https://github.com/specify/specify7 into…
Iwantexpresso Feb 9, 2026
1f37753
removed unused error import
Iwantexpresso Feb 9, 2026
9c4fd45
Merge branch 'main' into issue-7485
Iwantexpresso Feb 9, 2026
feb347e
Lint code with ESLint and Prettier
Iwantexpresso Feb 9, 2026
154ce1f
potential fix for record sets select all, but adds high latency
Iwantexpresso Feb 12, 2026
353ced0
clarified function name, added basic id preloading
Iwantexpresso Feb 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion specifyweb/backend/stored_queries/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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/<int:id>/ids/', views.query_ids),
]
35 changes: 35 additions & 0 deletions specifyweb/backend/stored_queries/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <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
Expand Down
Binary file added specifyweb/frontend/.DS_Store
Binary file not shown.
114 changes: 107 additions & 7 deletions specifyweb/frontend/js_src/lib/components/QueryBuilder/Results.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -94,6 +96,12 @@ export function QueryResults(props: QueryResultsProps): JSX.Element {
displayedFields,
} = props;

React.useEffect(() => {
if (queryResource?.get('id') && (props.totalCount ?? 0)>0) {
fetchAllRecordIDs(queryResource, () => {}).catch(() => {});
}
}, [queryResource?.get('id'), props.totalCount]);

const {
results: [results, setResults],
onFetchMore: handleFetchMore,
Expand Down Expand Up @@ -188,6 +196,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 (
<Container.Base className="w-full !bg-[color:var(--form-background)]">
<div className="flex items-center items-stretch gap-2">
Expand All @@ -210,7 +220,51 @@ export function QueryResults(props: QueryResultsProps): JSX.Element {
>
{interactionsText.deselectAll()}
</Button.Small>


)}
{/* Buttons for select All and invert selection*/}
{(totalCount ?? 0) > 0 && (totalCount ?? 0) < 10_000_000 && queryResource?.get("id") && (
<Button.Small
onClick={(): void => {
loading(
fetchAllRecordIDs(queryResource, loading)
.then((allIDs) => {
setSelectedRows(new Set(allIDs));
handleSelected?.(allIDs);
})
.catch((error) => {
console.error('Error fetching all IDs:', error);
})
);
}}
>
{interactionsText.selectAll()}
</Button.Small>
)}
{(totalCount ?? 0) > 0 && (totalCount ?? 0) < 3_000_000 && queryResource?.get("id") && (
<Button.Small
onClick={(): void => {
loading(
fetchAllRecordIDs(queryResource,loading)
.then((allIDs) => {
if (!loadedResults) return;
const invertedSelection = new Set( Array.from(allIDs).filter(id => !(selectedRows.has(id))));
setSelectedRows(invertedSelection);
handleSelected?.(Array.from(invertedSelection));

})
.catch((error) => {
console.error("Error fetchign all IDs", error);
})
);

}}
>
{interactionsText.invertSelection()}
</Button.Small>
)}

<div className="-ml-2 flex-1" />
{displayedFields.length > 0 &&
visibleFieldSpecs.length > 0 &&
Expand Down Expand Up @@ -245,13 +299,15 @@ export function QueryResults(props: QueryResultsProps): JSX.Element {
? undefined
: queryResource?.get('name')
}
recordIds={(): RA<number> =>
loadedResults
.filter((result) =>
selectedRows.has(result[queryIdField] as number)
)
.map((result) => result[queryIdField] as number)
}
recordIds={(): RA<number> => {
const queryId = queryResource?.get('id');
if (queryId && storedRecordIDs.has(queryId)) {
const allIDs = storedRecordIDs.get(queryId)!;
return allIDs.filter((id) => selectedRows.has(id));
}
return Array.from(selectedRows);
}}

saveComponent={recordSetFromQueryLoading}
/>
) : (
Expand Down Expand Up @@ -468,3 +524,47 @@ export function canMerge(table: SpecifyTable): boolean {
canMerge;
return canMergeOtherTables || canMergePaleoContext || canMergeCollectingEvent;
}

const storedRecordIDs = new Map<number, RA<number>>();

async function fetchAllRecordIDs(
queryResource: SpecifyResource<SpQuery> | undefined,
loading: (promise: Promise<unknown>) => void
): Promise<RA<number>> {

const queryId = queryResource!.get('id');

if (!queryResource) {
throw new Error('Query resource is undefined');
}

if (storedRecordIDs.has(queryId)) {
return storedRecordIDs.get(queryId)!;
}

return new Promise<RA<number>>((resolve, reject) => {
loading(
(async () => {
try {
console.log('Fetching all IDs for query');
const startTime = performance.now();
const {data} = await ajax<{readonly ids: RA<number>}>(
`/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`);

storedRecordIDs.set(queryId, data.ids);
resolve(data.ids);
} catch (error) {
reject(error);
}
})()
);
});
}
3 changes: 3 additions & 0 deletions specifyweb/frontend/js_src/lib/localization/interactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -421,4 +421,7 @@ export const interactionsText = createDictionary({
'ru-ru': 'Нет в наличии',
'uk-ua': 'Не доступно',
},
invertSelection: {
'en-us': 'Invert Selection',
},
} as const);