Skip to content

Commit d254be7

Browse files
Merge pull request #703 from reactome/spec016/t013-results-v3
Spec 016 T013: the search answers in the contract
2 parents 9ecdf69 + 627d7cf commit d254be7

14 files changed

Lines changed: 187 additions & 77 deletions

File tree

‎projects/contract/search.ts‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,3 +257,49 @@ export interface PathwayOfEntry {
257257
name?: Highlighted;
258258
hasEHLD?: boolean;
259259
}
260+
261+
// -- The search answers -------------------------------------------------------
262+
//
263+
// Each takes its entry type as an argument: the service writes them with the
264+
// entries it builds, a reader reads them with the view it needs.
265+
266+
/** One group of results: a type's, or "Results" for a search not grouped. */
267+
export interface ResultGroup<Entry> {
268+
/** Left out when the group has none (an ungrouped search that found none). */
269+
entries?: Entry[];
270+
/** Left out for a group Solr names with nothing. */
271+
typeName?: string;
272+
/** How many Solr found. */
273+
entriesCount: number;
274+
/** How many are written here. */
275+
rowCount: number;
276+
}
277+
278+
/** A term the search found nothing for, and whether it names a resource; an empty term or resource is left out. */
279+
export interface TargetResult {
280+
term?: string;
281+
resource?: string | number | boolean;
282+
target: boolean;
283+
}
284+
285+
/** `/search/query`: the groups, the rows written, the groups and matches Solr counted. */
286+
export interface GroupedResult<Entry> {
287+
results: ResultGroup<Entry>[];
288+
rowCount: number;
289+
numberOfGroups: number;
290+
numberOfMatches: number;
291+
/** For a search that found nothing: the terms, and whether each is a target. */
292+
targetResults?: TargetResult[];
293+
}
294+
295+
/** `/search/diagram/{id}` and `/search/fireworks`: the entries, their type facets, Solr's count. */
296+
export interface DiagramResult<Entry> {
297+
/** Left out when there are none. */
298+
entries?: Entry[];
299+
/** Left out when there are none. */
300+
facets?: FacetCount[];
301+
found: number;
302+
}
303+
304+
/** `/search/icon/query`: the icon library's page, one ungrouped group of icons. */
305+
export type IconResult = ResultGroup<IconEntry>;
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1-
<a [href]="`${CONTENT_DETAIL}/${icon().id}`" target="_blank" [matTooltip]="`Open origin of icon ${icon().name}`">
1+
<a
2+
[href]="`${CONTENT_DETAIL}/${icon().id}`"
3+
target="_blank"
4+
[matTooltip]="`Open origin of icon ${icon().iconName ?? icon().id}`"
5+
>
26
<div [innerHTML]="iconSVG()! | safe: 'html' : true"></div>
37
</a>
Lines changed: 12 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import type { IconEntry, PathwayOfEntry, SearchEntryView } from '../../../../contract/search.ts';
1+
import type {
2+
DiagramResult,
3+
FacetCount,
4+
GroupedResult,
5+
IconEntry,
6+
PathwayOfEntry,
7+
SearchEntryView,
8+
} from '../../../../contract/search.ts';
29

310
export namespace Search {
411
export const EMPTY_RESULTS = { found: 0, facets: [], entries: [] } as Search.Result;
@@ -21,17 +28,9 @@ export namespace Search {
2128
export type Entry = SearchEntryView;
2229
export type PathwayOf = PathwayOfEntry;
2330

24-
export interface Facet {
25-
name: string;
26-
count: number;
27-
}
28-
29-
// firework and diagram
30-
export interface Result {
31-
entries: Entry[];
32-
facets: Facet[];
33-
found: number;
34-
}
31+
// A diagram's or the species overview's search: the contract's (spec 016).
32+
export type Facet = FacetCount;
33+
export type Result = DiagramResult<Entry>;
3534

3635
// Add a sub-namespace for Icon-specific results
3736
export namespace Icon {
@@ -40,18 +39,6 @@ export namespace Search {
4039

4140
// A query for icons answers whatever its groups hold: a query matching
4241
// no icon answers proteins, compounds and the like, any kind.
43-
export interface EntryResult {
44-
entries: Search.Entry[];
45-
typeName: string;
46-
entriesCount: number;
47-
rowCount: number;
48-
}
49-
50-
export interface Result {
51-
results: EntryResult[];
52-
rowCount: number;
53-
numberOfGroups: number;
54-
numberOfMatches: number;
55-
}
42+
export type Result = GroupedResult<Search.Entry>;
5643
}
5744
}

‎projects/pathway-browser/src/app/services/icon.service.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ export class IconService {
268268
// No result group at all is no icon, not an error.
269269
map((response) => {
270270
const [group] = response.results;
271-
const [entry] = group?.typeName === 'Icon' ? group.entries : [];
271+
const [entry] = group?.typeName === 'Icon' ? (group.entries ?? []) : [];
272272
return entry && isIcon(entry) ? entry : null;
273273
}),
274274
switchMap((entry) => {

‎projects/pathway-browser/src/app/viewport/search/search.component.ts‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -396,7 +396,13 @@ export class SearchComponent {
396396
// TODO find a way to avoid the break in scroll when loading a new page
397397
export class SearchDataSource extends DataSource<Search.Entry | undefined> {
398398
private readonly _result = signal<Search.Result | undefined>(undefined);
399-
readonly facets = computed(() => this._result()?.facets || []);
399+
// A count Solr gives under no name is written without one: nothing to show
400+
// or filter by, as on the site's search page.
401+
readonly facets = computed(() =>
402+
(this._result()?.facets || []).filter(
403+
(facet): facet is Search.Facet & { name: string } => !!facet.name
404+
)
405+
);
400406
readonly found = computed(() => this._result()?.found || 0);
401407
readonly isLoading = signal<boolean>(false);
402408
/** Pages that could not be loaded, until asked for again. */
@@ -484,7 +490,7 @@ export class SearchDataSource extends DataSource<Search.Entry | undefined> {
484490
if (!current()) return;
485491
this._result.set(result);
486492
if (this.cachedEntries.length !== result.found) this.cachedEntries.length = result.found;
487-
this.cachedEntries.splice(page * this.pageSize, this.pageSize, ...result.entries);
493+
this.cachedEntries.splice(page * this.pageSize, this.pageSize, ...(result.entries ?? []));
488494
this.dataStream.next(this.cachedEntries);
489495
markFailed(false);
490496
this.isLoading.set(false);

‎projects/website-angular/src/app/content/detail/interactor-detail/interactor-detail.component.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ export class InteractorDetailComponent implements OnInit {
122122
// The first hit that is a reference: when nothing matches as an
123123
// interactor the search answers unfiltered, and an Icon can lead.
124124
const entry = search?.results
125-
.flatMap((group) => group.entries)
125+
.flatMap((group) => group.entries ?? [])
126126
.find((hit) => hit.databaseName && hit.referenceIdentifier);
127127
if (entry) {
128128
// The search is highlighted: an identifier or name that matched

‎projects/website-angular/src/app/search/search.component.ts‎

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import {
55
SearchFilters,
66
Facet,
77
NamedFacetCount,
8-
ResultGroup,
8+
ShownGroup,
9+
ShownResult,
910
} from '../../model/search.model';
1011
import {
1112
Component,
@@ -108,7 +109,7 @@ export class SearchComponent implements OnInit, OnDestroy {
108109
this.cdr.markForCheck();
109110
}
110111
suggestedTerms: string[] = [];
111-
results: SearchResult | null = null;
112+
results: ShownResult | null = null;
112113
facets: FacetResponse | null = null;
113114
loading = false;
114115
error = '';
@@ -310,7 +311,15 @@ export class SearchComponent implements OnInit, OnDestroy {
310311
this.hasNoResults = false;
311312
} else {
312313
// Successful API response - check if we have results
313-
const res = results as SearchResult;
314+
const answer = results as SearchResult;
315+
const res: ShownResult = {
316+
...answer,
317+
results: answer.results.map((group) => ({
318+
...group,
319+
entries: group.entries ?? [],
320+
typeName: group.typeName ?? '',
321+
})),
322+
};
314323
// Solr's true total across the whole result set; per-group
315324
// pagination chops each `group.entries` down to ~30, so we
316325
// must NOT recompute this from entries.length later.
@@ -391,7 +400,7 @@ export class SearchComponent implements OnInit, OnDestroy {
391400
typeName: 'Pages',
392401
entries: pageEntries,
393402
entriesCount: pageEntries.length,
394-
} as ResultGroup,
403+
} as ShownGroup,
395404
];
396405
}
397406
// Keep Solr's biology total; add Pages count (when shown); zero
@@ -445,6 +454,7 @@ export class SearchComponent implements OnInit, OnDestroy {
445454
return of({
446455
results: [],
447456
rowCount: 0,
457+
numberOfGroups: 0,
448458
numberOfMatches: 0,
449459
});
450460
}
@@ -479,7 +489,7 @@ export class SearchComponent implements OnInit, OnDestroy {
479489
for (const result of results) {
480490
if (!result?.results.length) continue;
481491
const group = result.results.find((g) => g.typeName === 'Protein');
482-
if (group) allProteins.push(...group.entries);
492+
if (group) allProteins.push(...(group.entries ?? []));
483493
}
484494

485495
// Group by referenceIdentifier, else the entry's own id (its stId, or
@@ -700,18 +710,18 @@ export class SearchComponent implements OnInit, OnDestroy {
700710
}
701711

702712
// Per-group pagination
703-
getGroupPage(group: ResultGroup): number {
713+
getGroupPage(group: ShownGroup): number {
704714
return this.groupPages[group.typeName] || 0;
705715
}
706716

707-
getGroupTotalPages(group: ResultGroup): number {
717+
getGroupTotalPages(group: ShownGroup): number {
708718
if (group.typeName === 'Protein') {
709719
return Math.ceil(this.uniqueProteins.length / this.groupPageSize) || 1;
710720
}
711721
return Math.ceil(group.entriesCount / this.groupPageSize);
712722
}
713723

714-
getGroupPageEntries(group: ResultGroup): SearchEntry[] {
724+
getGroupPageEntries(group: ShownGroup): SearchEntry[] {
715725
if (group.typeName === 'Protein') {
716726
// While still loading all proteins, show nothing (loading message is displayed)
717727
if (this.proteinLoading) return [];
@@ -726,7 +736,7 @@ export class SearchComponent implements OnInit, OnDestroy {
726736
return this.groupPageEntries[group.typeName] || group.entries.slice(0, this.groupPageSize);
727737
}
728738

729-
goToGroupPage(group: ResultGroup, page: number): void {
739+
goToGroupPage(group: ShownGroup, page: number): void {
730740
const total = this.getGroupTotalPages(group);
731741
if (page < 0 || page >= total) return;
732742

@@ -760,7 +770,7 @@ export class SearchComponent implements OnInit, OnDestroy {
760770
});
761771
}
762772

763-
getGroupPageNumbers(group: ResultGroup): (number | '...')[] {
773+
getGroupPageNumbers(group: ShownGroup): (number | '...')[] {
764774
return this.buildPageNumbers(this.getGroupPage(group), this.getGroupTotalPages(group));
765775
}
766776

‎projects/website-angular/src/model/icon.model.ts‎

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,5 @@ export type { IconCategory, IconFacetResponse } from '../../../contract/search.t
22

33
// One icon: the contract's (spec 016).
44
export type { IconEntry } from '../../../contract/search.ts';
5-
import type { IconEntry } from '../../../contract/search.ts';
65

7-
export interface IconResult {
8-
/** Left out when nothing matches. */
9-
entries?: IconEntry[];
10-
entriesCount: number;
11-
}
6+
export type { IconResult } from '../../../contract/search.ts';

‎projects/website-angular/src/model/search.model.ts‎

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,19 @@
66
export type { SearchEntryView as SearchEntry } from '../../../contract/search.ts';
77
import type { SearchEntryView } from '../../../contract/search.ts';
88

9-
export interface ResultGroup {
10-
typeName: string;
11-
entries: SearchEntryView[];
12-
entriesCount: number;
13-
rowCount: number;
14-
}
9+
// A search's answer, as the contract has it, with the view's entries.
10+
import type { GroupedResult, ResultGroup as Group } from '../../../contract/search.ts';
11+
type ResultGroup = Group<SearchEntryView>;
12+
export type SearchResult = GroupedResult<SearchEntryView>;
1513

16-
export interface SearchResult {
17-
results: ResultGroup[];
18-
rowCount: number;
19-
numberOfMatches: number;
20-
}
14+
/**
15+
* A group as the search page keeps it: its entries and name settled when the
16+
* answer arrives, an absent list as none and an absent name as empty text.
17+
* The answer leaves both out at times (an ungrouped page past the end, a
18+
* group Solr names with nothing); the page reads them everywhere.
19+
*/
20+
export type ShownGroup = ResultGroup & Required<Pick<ResultGroup, 'entries' | 'typeName'>>;
21+
export type ShownResult = Omit<SearchResult, 'results'> & { results: ShownGroup[] };
2122

2223
export type { Facet, FacetResponse, NamedFacetCount } from '../../../contract/search.ts';
2324

‎specs/016-api-contract/tasks.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ description: 'Tasks for spec 016: one API contract for the content service and t
6060
- [x] T010 [US2] Make the site's `SearchEntry` and `IconEntry` re-export the contract's, and narrow by kind where `search.component.ts`, `interactor-detail.component.ts`, `icon-lib.component.ts` and `icon-detail.component.ts` read kind-specific fields.
6161
- Escape or render `Highlighted` fields as each reader does today, with no change visible on the page.
6262
- Check the search page and an icon page in a browser.
63-
- Done in #691. The site's `SearchEntry` is the contract's `SearchEntryView`: any kind, what every kind carries required, the rest optional (FR-003's view, since the search page shows every kind side by side and a template cannot read a field one kind lacks); `IconEntry` is the contract's kind. Two readers changed: the protein forms' key drops a fallback `id` already covers (`id` is the stId, or the dbId, never empty), and an icon's image is found by `id`, the same value as `stId` but never highlighted. From #690's review, the icon library too (#691): its search is highlighted, and asking beta's service for `R-ICO-012449` there answers `stId` as `<span class="highlighting" >R-ICO-012449</span>`, `id` plain, so its image, link, fetch and shown id were built from markup; they take `id` now, as does the pathway browser's icon load. From #691's review (#697): the search page printed a highlighted reference identifier as text, `PTEN (<span class="highlighting" >P60484</span>)` for a search of P60484, seen in the browser against beta's services; it is rendered as its neighbours are (`[innerHTML]`) and reads `PTEN (P60484)`. An icon's alt text and the pathway browser's icon link take `id`, and the interactor page strips a highlighted name as it strips the identifier. Nothing else visible changes; the search, search-error and icon-library end-to-end specs pass (46 and 78 tests with the specs below). The single icon page reads only type changes; no recording opens one (the `gap` T014 closes).
63+
- Done in #691. The site's `SearchEntry` is the contract's `SearchEntryView`: any kind, what every kind carries required, the rest optional (FR-003's view, since the search page shows every kind side by side and a template cannot read a field one kind lacks); `IconEntry` is the contract's kind. Two readers changed: the protein forms' key drops a fallback `id` already covers (`id` is the stId, or the dbId, never empty), and an icon's image is found by `id`, the same value as `stId` but never highlighted. From #690's review, the icon library too (#691): its search is highlighted, and asking beta's service for `R-ICO-012449` there answers `stId` as `<span class="highlighting" >R-ICO-012449</span>`, `id` plain, so its image, link, fetch and shown id were built from markup; they take `id` now, as does the pathway browser's icon load. From #691's review (#697): the search page printed a highlighted reference identifier as text, `PTEN (<span class="highlighting" >P60484</span>)` for a search of P60484, seen in the browser against beta's services; it is rendered as its neighbours are (`[innerHTML]`) and reads `PTEN (P60484)`. An icon's alt text and the pathway browser's icon link take `id`, and the interactor page strips a highlighted name as it strips the identifier. Nothing else visible changes; the search, search-error and icon-library end-to-end specs passed for #691 (46 and 78 tests with the specs below). The single icon page reads only type changes; no recording opens one (the `gap` T014 closes).
6464
- [x] T011 [US2] Make the pathway browser's `Search.Entry` and `Search.Icon.Entry` re-export the contract's, keeping the value `EMPTY_RESULTS` in the namespace. Adjust `viewport/search/search.component.ts`, `services/icon.service.ts` and `details/tabs/description-tab/icon/icon.component.ts`, and check the diagram search in a browser.
6565
- Done in #691. `Search.Entry` is the view, `Search.Icon.Entry` the icon kind, and an icon query's groups hold any kind (three of the five recorded answers are proteins, compounds or drugs), so `fetchIcon` takes an entry only when it is an icon, not deleted, besides its group's name. Its unit spec's icon is now one as the search writes it, and a deleted icon in an icon group gives none (failing without the check). The flag button takes `id`. The in-diagram search, flagging, pathway-browser, entity popup, structures and pathway-data specs pass; their recordings answer the icon query with icons (R-ICO-013990, R-ICO-012536) and with other kinds.
6666
- [x] T012 [US2] Recount `tools/api-types/known-problems.json`. Record what remains on the search routes, each item with its reason, in this task's PR and here.
@@ -72,7 +72,8 @@ description: 'Tasks for spec 016: one API contract for the content service and t
7272

7373
**Independent test**: every search route's known problems are at zero or explained.
7474

75-
- [ ] T013 [US3] Write `ResultGroup<E>`, `GroupedResult` (with `numberOfGroups`), the diagram and species-overview result types and `IconResult` (with `rowCount` and `typeName`) in `projects/contract/search.ts`, and point their writers in `search-endpoints.ts` and `search-diagram-endpoints.ts`, and their readers, at them.
75+
- [x] T013 [US3] Write `ResultGroup<E>`, `GroupedResult` (with `numberOfGroups`), the diagram and species-overview result types and `IconResult` (with `rowCount` and `typeName`) in `projects/contract/search.ts`, and point their writers in `search-endpoints.ts` and `search-diagram-endpoints.ts`, and their readers, at them.
76+
- Done in #703. The contract's `ResultGroup<Entry>`, `GroupedResult<Entry>`, `DiagramResult<Entry>`, `TargetResult` and `IconResult`, each taking its entry type as a named argument: the service writes them with its `WrittenEntry`, a reader reads them with the view. A group's `entries` is optional (an ungrouped page past the end has none) and so is its `typeName` (a group Solr names with nothing); the search page settles both once when the answer arrives (`ShownGroup`), and the pathway browser's search shows only named type facets, as the site's does. A target's empty term or resource is left out, so both are optional. A grouping without `matches` is skipped, as SolrJ's `QueryResponse.extractGroupedInfo` skips it (read from its bytecode, from #700's review): the first grouping that has its matches is read, and none is no result, Java's 500 (a spec of two groupings, the first without matches, fails on the first-grouping reading). The search routes' known problems are 0, the whole 12,164. From #697's review, the pathway browser's icon tooltip takes `iconName`, never highlighted.
7677
- [ ] T014 [US3] Record an answer for `/search/icon/{id}` (already in `tools/api-types/calls.ts` as `IconEntry`, with a `gap`) through the e2e suite and remove the `gap` (record rewrites three HARs: check the diff). Type its writer.
7778
- [ ] T015 [US3] Tick spec 013 T019 in `specs/013-server-types/tasks.md` with T002–T014's PR numbers.
7879

0 commit comments

Comments
 (0)