@internetarchive/collection-browser 1.6.0 → 1.7.1-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { getMockSuccessSingleResult, getMockSuccessMultipleResults, getMockSuccessSingleResultWithSort, getMockSuccessLoggedInResult, getMockSuccessNoPreviewResult, getMockSuccessLoggedInAndNoPreviewResult, getMockSuccessWithYearHistogramAggs, getMockSuccessMultiLineDescription, getMockSuccessFirstTitleResult, getMockSuccessFirstCreatorResult, getMockErrorResult, getMockMalformedResult, getMockSuccessWithCollectionTitles, getMockSuccessWithCollectionAggregations, getMockSuccessExtraQuotedHref, } from './mock-search-responses';
1
+ import { getMockSuccessSingleResult, getMockSuccessMultipleResults, getMockSuccessSingleResultWithSort, getMockSuccessLoggedInResult, getMockSuccessNoPreviewResult, getMockSuccessLoggedInAndNoPreviewResult, getMockSuccessWithYearHistogramAggs, getMockSuccessMultiLineDescription, getMockSuccessFirstTitleResult, getMockSuccessFirstCreatorResult, getMockErrorResult, getMockMalformedResult, getMockSuccessWithCollectionTitles, getMockSuccessWithCollectionAggregations, getMockSuccessExtraQuotedHref, getMockSuccessWithDefaultSort, getMockSuccessWithConciseDefaultSort, } from './mock-search-responses';
2
2
  const responses = {
3
3
  'single-result': getMockSuccessSingleResult,
4
4
  years: getMockSuccessWithYearHistogramAggs,
@@ -11,6 +11,8 @@ const responses = {
11
11
  'collection-titles': getMockSuccessWithCollectionTitles,
12
12
  'collection-aggregations': getMockSuccessWithCollectionAggregations,
13
13
  'extra-quoted-href': getMockSuccessExtraQuotedHref,
14
+ 'default-sort': getMockSuccessWithDefaultSort,
15
+ 'default-sort-concise': getMockSuccessWithConciseDefaultSort,
14
16
  error: getMockErrorResult,
15
17
  malformed: getMockMalformedResult,
16
18
  };
@@ -21,7 +23,7 @@ export class MockSearchService {
21
23
  this.resultsSpy = resultsSpy;
22
24
  }
23
25
  async search(params, searchType) {
24
- var _a;
26
+ var _a, _b;
25
27
  this.searchParams = params;
26
28
  this.searchType = searchType;
27
29
  if (this.asyncResponse) {
@@ -30,7 +32,8 @@ export class MockSearchService {
30
32
  setTimeout(res, this.asyncResponseDelay);
31
33
  });
32
34
  }
33
- const resultFn = (_a = responses[this.searchParams.query]) !== null && _a !== void 0 ? _a : getMockSuccessMultipleResults;
35
+ const responseKey = (_a = (this.searchParams.query || this.searchParams.pageTarget)) !== null && _a !== void 0 ? _a : '';
36
+ const resultFn = (_b = responses[responseKey]) !== null && _b !== void 0 ? _b : getMockSuccessMultipleResults;
34
37
  let result = resultFn();
35
38
  // with-sort query has special handling
36
39
  if (this.searchParams.query === 'with-sort') {
@@ -1 +1 @@
1
- {"version":3,"file":"mock-search-service.js","sourceRoot":"","sources":["../../../test/mocks/mock-search-service.ts"],"names":[],"mappings":"AAQA,OAAO,EACL,0BAA0B,EAC1B,6BAA6B,EAC7B,kCAAkC,EAClC,4BAA4B,EAC5B,6BAA6B,EAC7B,wCAAwC,EACxC,mCAAmC,EACnC,kCAAkC,EAClC,8BAA8B,EAC9B,gCAAgC,EAChC,kBAAkB,EAClB,sBAAsB,EACtB,kCAAkC,EAClC,wCAAwC,EACxC,6BAA6B,GAC9B,MAAM,yBAAyB,CAAC;AAEjC,MAAM,SAAS,GAGX;IACF,eAAe,EAAE,0BAA0B;IAC3C,KAAK,EAAE,mCAAmC;IAC1C,wBAAwB,EAAE,kCAAkC;IAC5D,QAAQ,EAAE,4BAA4B;IACtC,YAAY,EAAE,6BAA6B;IAC3C,qBAAqB,EAAE,wCAAwC;IAC/D,aAAa,EAAE,8BAA8B;IAC7C,eAAe,EAAE,gCAAgC;IACjD,mBAAmB,EAAE,kCAAkC;IACvD,yBAAyB,EAAE,wCAAwC;IACnE,mBAAmB,EAAE,6BAA6B;IAClD,KAAK,EAAE,kBAAkB;IACzB,SAAS,EAAE,sBAAsB;CAClC,CAAC;AAEF,MAAM,OAAO,iBAAiB;IAW5B,YAAY,EACV,aAAa,GAAG,KAAK,EACrB,kBAAkB,GAAG,CAAC,EACtB,UAAU,GAAG,GAAG,EAAE,GAAE,CAAC,GACtB,GAAG,EAAE;QACJ,IAAI,CAAC,aAAa,GAAG,aAAa,CAAC;QACnC,IAAI,CAAC,kBAAkB,GAAG,kBAAkB,CAAC;QAC7C,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;IAC/B,CAAC;IAED,KAAK,CAAC,MAAM,CACV,MAAoB,EACpB,UAAsB;;QAEtB,IAAI,CAAC,YAAY,GAAG,MAAM,CAAC;QAC3B,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAE7B,IAAI,IAAI,CAAC,aAAa,EAAE;YACtB,iCAAiC;YACjC,MAAM,IAAI,OAAO,CAAC,GAAG,CAAC,EAAE;gBACtB,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,kBAAkB,CAAC,CAAC;YAC3C,CAAC,CAAC,CAAC;SACJ;QAED,MAAM,QAAQ,GACZ,MAAA,SAAS,CAAC,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,mCAAI,6BAA6B,CAAC;QACtE,IAAI,MAAM,GAAG,QAAQ,EAAE,CAAC;QAExB,uCAAuC;QACvC,IAAI,IAAI,CAAC,YAAY,CAAC,KAAK,KAAK,WAAW,EAAE;YAC3C,MAAM,GAAG,kCAAkC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;SAC9D;QAED,uCAAuC;QACvC,IAAI,MAAM,CAAC,OAAO,EAAE;YACjB,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,gBAAwB,CAAC,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC;SACnE;QAED,OAAO,MAAM,CAAC;IAChB,CAAC;CACF","sourcesContent":["import type { Result } from '@internetarchive/result-type';\nimport type {\n SearchParams,\n SearchResponse,\n SearchServiceInterface,\n SearchServiceError,\n SearchType,\n} from '@internetarchive/search-service';\nimport {\n getMockSuccessSingleResult,\n getMockSuccessMultipleResults,\n getMockSuccessSingleResultWithSort,\n getMockSuccessLoggedInResult,\n getMockSuccessNoPreviewResult,\n getMockSuccessLoggedInAndNoPreviewResult,\n getMockSuccessWithYearHistogramAggs,\n getMockSuccessMultiLineDescription,\n getMockSuccessFirstTitleResult,\n getMockSuccessFirstCreatorResult,\n getMockErrorResult,\n getMockMalformedResult,\n getMockSuccessWithCollectionTitles,\n getMockSuccessWithCollectionAggregations,\n getMockSuccessExtraQuotedHref,\n} from './mock-search-responses';\n\nconst responses: Record<\n string,\n () => Result<SearchResponse, SearchServiceError>\n> = {\n 'single-result': getMockSuccessSingleResult,\n years: getMockSuccessWithYearHistogramAggs,\n 'multi-line-description': getMockSuccessMultiLineDescription,\n loggedin: getMockSuccessLoggedInResult,\n 'no-preview': getMockSuccessNoPreviewResult,\n 'loggedin-no-preview': getMockSuccessLoggedInAndNoPreviewResult,\n 'first-title': getMockSuccessFirstTitleResult,\n 'first-creator': getMockSuccessFirstCreatorResult,\n 'collection-titles': getMockSuccessWithCollectionTitles,\n 'collection-aggregations': getMockSuccessWithCollectionAggregations,\n 'extra-quoted-href': getMockSuccessExtraQuotedHref,\n error: getMockErrorResult,\n malformed: getMockMalformedResult,\n};\n\nexport class MockSearchService implements SearchServiceInterface {\n searchParams?: SearchParams;\n\n searchType?: SearchType;\n\n asyncResponse: boolean;\n\n asyncResponseDelay: number;\n\n resultsSpy: Function;\n\n constructor({\n asyncResponse = false,\n asyncResponseDelay = 0,\n resultsSpy = () => {},\n } = {}) {\n this.asyncResponse = asyncResponse;\n this.asyncResponseDelay = asyncResponseDelay;\n this.resultsSpy = resultsSpy;\n }\n\n async search(\n params: SearchParams,\n searchType: SearchType\n ): Promise<Result<SearchResponse, SearchServiceError>> {\n this.searchParams = params;\n this.searchType = searchType;\n\n if (this.asyncResponse) {\n // Add an artificial 1-tick delay\n await new Promise(res => {\n setTimeout(res, this.asyncResponseDelay);\n });\n }\n\n const resultFn: () => Result<SearchResponse, SearchServiceError> =\n responses[this.searchParams.query] ?? getMockSuccessMultipleResults;\n let result = resultFn();\n\n // with-sort query has special handling\n if (this.searchParams.query === 'with-sort') {\n result = getMockSuccessSingleResultWithSort(this.resultsSpy);\n }\n\n // Apply any uid param from the request\n if (result.success) {\n (result.success.request.clientParameters as any).uid = params.uid;\n }\n\n return result;\n }\n}\n"]}
1
+ {"version":3,"file":"mock-search-service.js","sourceRoot":"","sources":["../../../test/mocks/mock-search-service.ts"],"names":[],"mappings":"AAQA,OAAO,EACL,0BAA0B,EAC1B,6BAA6B,EAC7B,kCAAkC,EAClC,4BAA4B,EAC5B,6BAA6B,EAC7B,wCAAwC,EACxC,mCAAmC,EACnC,kCAAkC,EAClC,8BAA8B,EAC9B,gCAAgC,EAChC,kBAAkB,EAClB,sBAAsB,EACtB,kCAAkC,EAClC,wCAAwC,EACxC,6BAA6B,EAC7B,6BAA6B,EAC7B,oCAAoC,GACrC,MAAM,yBAAyB,CAAC;AAEjC,MAAM,SAAS,GAGX;IACF,eAAe,EAAE,0BAA0B;IAC3C,KAAK,EAAE,mCAAmC;IAC1C,wBAAwB,EAAE,kCAAkC;IAC5D,QAAQ,EAAE,4BAA4B;IACtC,YAAY,EAAE,6BAA6B;IAC3C,qBAAqB,EAAE,wCAAwC;IAC/D,aAAa,EAAE,8BAA8B;IAC7C,eAAe,EAAE,gCAAgC;IACjD,mBAAmB,EAAE,kCAAkC;IACvD,yBAAyB,EAAE,wCAAwC;IACnE,mBAAmB,EAAE,6BAA6B;IAClD,cAAc,EAAE,6BAA6B;IAC7C,sBAAsB,EAAE,oCAAoC;IAC5D,KAAK,EAAE,kBAAkB;IACzB,SAAS,EAAE,sBAAsB;CAClC,CAAC;AAEF,MAAM,OAAO,iBAAiB;IAW5B,YAAY,EACV,aAAa,GAAG,KAAK,EACrB,kBAAkB,GAAG,CAAC,EACtB,UAAU,GAAG,GAAG,EAAE,GAAE,CAAC,GACtB,GAAG,EAAE;QACJ,IAAI,CAAC,aAAa,GAAG,aAAa,CAAC;QACnC,IAAI,CAAC,kBAAkB,GAAG,kBAAkB,CAAC;QAC7C,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;IAC/B,CAAC;IAED,KAAK,CAAC,MAAM,CACV,MAAoB,EACpB,UAAsB;;QAEtB,IAAI,CAAC,YAAY,GAAG,MAAM,CAAC;QAC3B,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAE7B,IAAI,IAAI,CAAC,aAAa,EAAE;YACtB,iCAAiC;YACjC,MAAM,IAAI,OAAO,CAAC,GAAG,CAAC,EAAE;gBACtB,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,kBAAkB,CAAC,CAAC;YAC3C,CAAC,CAAC,CAAC;SACJ;QAED,MAAM,WAAW,GACf,MAAA,CAAC,IAAI,CAAC,YAAY,CAAC,KAAK,IAAI,IAAI,CAAC,YAAY,CAAC,UAAU,CAAC,mCAAI,EAAE,CAAC;QAClE,MAAM,QAAQ,GACZ,MAAA,SAAS,CAAC,WAAW,CAAC,mCAAI,6BAA6B,CAAC;QAC1D,IAAI,MAAM,GAAG,QAAQ,EAAE,CAAC;QAExB,uCAAuC;QACvC,IAAI,IAAI,CAAC,YAAY,CAAC,KAAK,KAAK,WAAW,EAAE;YAC3C,MAAM,GAAG,kCAAkC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;SAC9D;QAED,uCAAuC;QACvC,IAAI,MAAM,CAAC,OAAO,EAAE;YACjB,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,gBAAwB,CAAC,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC;SACnE;QAED,OAAO,MAAM,CAAC;IAChB,CAAC;CACF","sourcesContent":["import type { Result } from '@internetarchive/result-type';\nimport type {\n SearchParams,\n SearchResponse,\n SearchServiceInterface,\n SearchServiceError,\n SearchType,\n} from '@internetarchive/search-service';\nimport {\n getMockSuccessSingleResult,\n getMockSuccessMultipleResults,\n getMockSuccessSingleResultWithSort,\n getMockSuccessLoggedInResult,\n getMockSuccessNoPreviewResult,\n getMockSuccessLoggedInAndNoPreviewResult,\n getMockSuccessWithYearHistogramAggs,\n getMockSuccessMultiLineDescription,\n getMockSuccessFirstTitleResult,\n getMockSuccessFirstCreatorResult,\n getMockErrorResult,\n getMockMalformedResult,\n getMockSuccessWithCollectionTitles,\n getMockSuccessWithCollectionAggregations,\n getMockSuccessExtraQuotedHref,\n getMockSuccessWithDefaultSort,\n getMockSuccessWithConciseDefaultSort,\n} from './mock-search-responses';\n\nconst responses: Record<\n string,\n () => Result<SearchResponse, SearchServiceError>\n> = {\n 'single-result': getMockSuccessSingleResult,\n years: getMockSuccessWithYearHistogramAggs,\n 'multi-line-description': getMockSuccessMultiLineDescription,\n loggedin: getMockSuccessLoggedInResult,\n 'no-preview': getMockSuccessNoPreviewResult,\n 'loggedin-no-preview': getMockSuccessLoggedInAndNoPreviewResult,\n 'first-title': getMockSuccessFirstTitleResult,\n 'first-creator': getMockSuccessFirstCreatorResult,\n 'collection-titles': getMockSuccessWithCollectionTitles,\n 'collection-aggregations': getMockSuccessWithCollectionAggregations,\n 'extra-quoted-href': getMockSuccessExtraQuotedHref,\n 'default-sort': getMockSuccessWithDefaultSort,\n 'default-sort-concise': getMockSuccessWithConciseDefaultSort,\n error: getMockErrorResult,\n malformed: getMockMalformedResult,\n};\n\nexport class MockSearchService implements SearchServiceInterface {\n searchParams?: SearchParams;\n\n searchType?: SearchType;\n\n asyncResponse: boolean;\n\n asyncResponseDelay: number;\n\n resultsSpy: Function;\n\n constructor({\n asyncResponse = false,\n asyncResponseDelay = 0,\n resultsSpy = () => {},\n } = {}) {\n this.asyncResponse = asyncResponse;\n this.asyncResponseDelay = asyncResponseDelay;\n this.resultsSpy = resultsSpy;\n }\n\n async search(\n params: SearchParams,\n searchType: SearchType\n ): Promise<Result<SearchResponse, SearchServiceError>> {\n this.searchParams = params;\n this.searchType = searchType;\n\n if (this.asyncResponse) {\n // Add an artificial 1-tick delay\n await new Promise(res => {\n setTimeout(res, this.asyncResponseDelay);\n });\n }\n\n const responseKey =\n (this.searchParams.query || this.searchParams.pageTarget) ?? '';\n const resultFn: () => Result<SearchResponse, SearchServiceError> =\n responses[responseKey] ?? getMockSuccessMultipleResults;\n let result = resultFn();\n\n // with-sort query has special handling\n if (this.searchParams.query === 'with-sort') {\n result = getMockSuccessSingleResultWithSort(this.resultsSpy);\n }\n\n // Apply any uid param from the request\n if (result.success) {\n (result.success.request.clientParameters as any).uid = params.uid;\n }\n\n return result;\n }\n}\n"]}
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "The Internet Archive Collection Browser.",
4
4
  "license": "AGPL-3.0-only",
5
5
  "author": "Internet Archive",
6
- "version": "1.6.0",
6
+ "version": "1.7.1-alpha.0",
7
7
  "main": "dist/index.js",
8
8
  "module": "dist/index.js",
9
9
  "scripts": {
@@ -23,7 +23,7 @@
23
23
  "types": "dist/index.d.ts",
24
24
  "dependencies": {
25
25
  "@internetarchive/analytics-manager": "^0.1.2",
26
- "@internetarchive/collection-name-cache": "^0.2.12",
26
+ "@internetarchive/collection-name-cache": "^0.2.13-alpha.0",
27
27
  "@internetarchive/feature-feedback": "^0.1.4",
28
28
  "@internetarchive/field-parsers": "^0.1.3",
29
29
  "@internetarchive/histogram-date-range": "^1.1.0",
@@ -32,7 +32,7 @@
32
32
  "@internetarchive/infinite-scroller": "^0.1.4",
33
33
  "@internetarchive/local-cache": "^0.2.1",
34
34
  "@internetarchive/modal-manager": "^0.2.8",
35
- "@internetarchive/search-service": "^1.1.0",
35
+ "@internetarchive/search-service": "^1.1.1-alpha.0",
36
36
  "@internetarchive/shared-resize-observer": "^0.2.0",
37
37
  "@lit/localize": "^0.11.2",
38
38
  "dompurify": "^2.3.6",
@@ -57,6 +57,8 @@ import {
57
57
  PrefixFilterCounts,
58
58
  prefixFilterAggregationKeys,
59
59
  FacetEventDetails,
60
+ MetadataFieldToSortField,
61
+ MetadataSortField,
60
62
  } from './models';
61
63
  import {
62
64
  RestorationStateHandlerInterface,
@@ -100,7 +102,7 @@ export class CollectionBrowser
100
102
 
101
103
  @property({ type: Object }) sortParam: SortParam | null = null;
102
104
 
103
- @property({ type: String }) selectedSort: SortField = SortField.relevance;
105
+ @property({ type: String }) selectedSort: SortField = SortField.default;
104
106
 
105
107
  @property({ type: String }) selectedTitleFilter: string | null = null;
106
108
 
@@ -194,6 +196,11 @@ export class CollectionBrowser
194
196
 
195
197
  @state() private contentWidth?: number;
196
198
 
199
+ @state() private defaultSortField: Exclude<SortField, SortField.default> =
200
+ SortField.relevance;
201
+
202
+ @state() private defaultSortDirection: SortDirection | null = null;
203
+
197
204
  @state() private placeholderType: PlaceholderType = null;
198
205
 
199
206
  @state() private prefixFilterCountMap: Partial<
@@ -351,7 +358,7 @@ export class CollectionBrowser
351
358
  if (sort) {
352
359
  this.sortParam = null;
353
360
  this.sortDirection = null;
354
- this.selectedSort = SortField.relevance;
361
+ this.selectedSort = SortField.default;
355
362
  }
356
363
  }
357
364
 
@@ -553,8 +560,11 @@ export class CollectionBrowser
553
560
  private get sortFilterBarTemplate() {
554
561
  return html`
555
562
  <sort-filter-bar
563
+ .defaultSortField=${this.defaultSortField}
564
+ .defaultSortDirection=${this.defaultSortDirection}
556
565
  .selectedSort=${this.selectedSort}
557
566
  .sortDirection=${this.sortDirection}
567
+ .showRelevance=${this.isRelevanceSortAvailable}
558
568
  .displayMode=${this.displayMode}
559
569
  .selectedTitleFilter=${this.selectedTitleFilter}
560
570
  .selectedCreatorFilter=${this.selectedCreatorFilter}
@@ -598,7 +608,7 @@ export class CollectionBrowser
598
608
  }
599
609
 
600
610
  private selectedSortChanged(): void {
601
- if (this.selectedSort === 'relevance') {
611
+ if ([SortField.default, SortField.relevance].includes(this.selectedSort)) {
602
612
  this.sortParam = null;
603
613
  return;
604
614
  }
@@ -1195,6 +1205,11 @@ export class CollectionBrowser
1195
1205
  this.infiniteScroller.reload();
1196
1206
  }
1197
1207
 
1208
+ if (this.withinCollection && this.baseQuery?.trim()) {
1209
+ this.defaultSortField = SortField.relevance;
1210
+ this.defaultSortDirection = null;
1211
+ }
1212
+
1198
1213
  if (!this.initialQueryChangeHappened && this.initialPageNumber > 1) {
1199
1214
  this.scrollToPage(this.initialPageNumber);
1200
1215
  }
@@ -1238,7 +1253,7 @@ export class CollectionBrowser
1238
1253
  this.displayMode = restorationState.displayMode;
1239
1254
  if (restorationState.searchType != null)
1240
1255
  this.searchType = restorationState.searchType;
1241
- this.selectedSort = restorationState.selectedSort ?? SortField.relevance;
1256
+ this.selectedSort = restorationState.selectedSort ?? SortField.default;
1242
1257
  this.sortDirection = restorationState.sortDirection ?? null;
1243
1258
  this.selectedTitleFilter = restorationState.selectedTitleFilter ?? null;
1244
1259
  this.selectedCreatorFilter = restorationState.selectedCreatorFilter ?? null;
@@ -1612,6 +1627,14 @@ export class CollectionBrowser
1612
1627
  });
1613
1628
  }
1614
1629
 
1630
+ /**
1631
+ * Whether sorting by relevance makes sense for the current state.
1632
+ * Currently equivalent to having a non-empty query.
1633
+ */
1634
+ private get isRelevanceSortAvailable(): boolean {
1635
+ return !!this.baseQuery?.trim();
1636
+ }
1637
+
1615
1638
  /**
1616
1639
  * Whether a search may be performed in the current state of the component.
1617
1640
  * This is only true if the search service is defined, and either
@@ -1757,6 +1780,10 @@ export class CollectionBrowser
1757
1780
 
1758
1781
  if (this.withinCollection) {
1759
1782
  this.collectionInfo = success.response.collectionExtraInfo;
1783
+
1784
+ // For collections, we want the UI to respect the default sort option
1785
+ // which can be specified in metadata, or otherwise assumed to be week:desc
1786
+ this.applyDefaultCollectionSort(success.response.collectionExtraInfo);
1760
1787
  }
1761
1788
 
1762
1789
  const { results, collectionTitles } = success.response;
@@ -1804,6 +1831,39 @@ export class CollectionBrowser
1804
1831
  this.collectionNameCache?.preloadIdentifiers(collectionIdsArray);
1805
1832
  }
1806
1833
 
1834
+ /**
1835
+ * Applies any default sort option for the current collection, by checking
1836
+ * for one in the collection's metadata. If none is found, defaults to sorting
1837
+ * descending by weekly views.
1838
+ */
1839
+ private applyDefaultCollectionSort(collectionInfo?: CollectionExtraInfo) {
1840
+ if (this.baseQuery) {
1841
+ // If there's a query set, then we default to relevance sorting regardless of
1842
+ // the collection metadata-specified sort.
1843
+ this.defaultSortField = SortField.relevance;
1844
+ this.defaultSortDirection = null;
1845
+ return;
1846
+ }
1847
+
1848
+ const defaultSort: string =
1849
+ collectionInfo?.public_metadata?.['sort-by'] ?? '-week';
1850
+
1851
+ // Account for both -field and field:dir formats
1852
+ let [field, dir] = defaultSort.split(':');
1853
+ if (field.startsWith('-')) {
1854
+ field = field.slice(1);
1855
+ dir = 'desc';
1856
+ } else if (!['asc', 'desc'].includes(dir)) {
1857
+ dir = 'asc';
1858
+ }
1859
+
1860
+ const sortField = MetadataFieldToSortField[field as MetadataSortField];
1861
+ if (sortField && sortField !== SortField.default) {
1862
+ this.defaultSortField = sortField;
1863
+ this.defaultSortDirection = dir as SortDirection;
1864
+ }
1865
+ }
1866
+
1807
1867
  /**
1808
1868
  * This is useful for determining whether we need to reload the scroller.
1809
1869
  *
package/src/models.ts CHANGED
@@ -55,6 +55,7 @@ export type CollectionBrowserContext = 'collection' | 'search';
55
55
  * The sort fields shown in the sort filter bar
56
56
  */
57
57
  export enum SortField {
58
+ 'default' = 'default',
58
59
  'relevance' = 'relevance',
59
60
  'alltimeview' = 'alltimeview',
60
61
  'weeklyview' = 'weeklyview',
@@ -87,6 +88,7 @@ export type URLSortField = MetadataSortField | 'title' | 'creator';
87
88
  export const SortFieldDisplayName: {
88
89
  [key in SortField]: string;
89
90
  } = {
91
+ default: '', // Use the default sorting option for the current page context, if none has been selected
90
92
  relevance: 'Relevance',
91
93
  alltimeview: 'All-time views',
92
94
  weeklyview: 'Weekly views',
@@ -99,9 +101,10 @@ export const SortFieldDisplayName: {
99
101
  };
100
102
 
101
103
  export const DefaultSortDirection: {
102
- [key in SortField]: SortDirection;
104
+ [key in SortField]: SortDirection | null;
103
105
  } = {
104
- relevance: 'desc', // Can't actually change the sort direction for relevance
106
+ default: null,
107
+ relevance: null, // Sort direction is disabled entirely for relevance sort (user can't click the button)
105
108
  alltimeview: 'desc',
106
109
  weeklyview: 'desc',
107
110
  title: 'asc',
@@ -118,6 +121,7 @@ export const DefaultSortDirection: {
118
121
  export const SortFieldToMetadataField: {
119
122
  [key in SortField]: MetadataSortField | null;
120
123
  } = {
124
+ default: null,
121
125
  relevance: null,
122
126
  alltimeview: 'downloads',
123
127
  weeklyview: 'week',
@@ -42,11 +42,20 @@ export class SortFilterBar
42
42
  /** Which display mode the tiles are being rendered with (grid/list-detail/list-compact) */
43
43
  @property({ type: String }) displayMode?: CollectionDisplayMode;
44
44
 
45
+ /** The default sort direction to use if none is set */
46
+ @property({ type: String }) defaultSortDirection: SortDirection | null = null;
47
+
48
+ /** The default sort field to use if none is set */
49
+ @property({ type: String }) defaultSortField: Exclude<
50
+ SortField,
51
+ SortField.default
52
+ > = SortField.relevance;
53
+
45
54
  /** The current sort direction (asc/desc), or null if none is set */
46
55
  @property({ type: String }) sortDirection: SortDirection | null = null;
47
56
 
48
57
  /** The field currently being sorted on (e.g., 'title'). Defaults to relevance. */
49
- @property({ type: String }) selectedSort: SortField = SortField.relevance;
58
+ @property({ type: String }) selectedSort: SortField = SortField.default;
50
59
 
51
60
  /** The currently selected title letter filter, or null if none is set */
52
61
  @property({ type: String }) selectedTitleFilter: string | null = null;
@@ -144,7 +153,7 @@ export class SortFilterBar
144
153
  }
145
154
 
146
155
  if (changed.has('selectedSort') && this.sortDirection === null) {
147
- this.sortDirection = DefaultSortDirection[this.selectedSort];
156
+ this.sortDirection = DefaultSortDirection[this.finalizedSortField];
148
157
  }
149
158
 
150
159
  if (changed.has('selectedTitleFilter') && this.selectedTitleFilter) {
@@ -265,8 +274,8 @@ export class SortFilterBar
265
274
  return html`
266
275
  <button
267
276
  class="sort-direction-selector"
268
- ?disabled=${this.selectedSort === 'relevance'}
269
- @click=${this.toggleSortDirection}
277
+ ?disabled=${this.finalizedSortField === SortField.relevance}
278
+ @click=${this.handleSortDirectionClicked}
270
279
  >
271
280
  <span class="sr-only">${srLabel}</span>
272
281
  ${this.sortDirectionIcon}
@@ -277,14 +286,14 @@ export class SortFilterBar
277
286
  /** Template to render the sort direction button's icon in the correct current state */
278
287
  private get sortDirectionIcon(): TemplateResult {
279
288
  // For relevance sort, show a fully disabled icon
280
- if (this.selectedSort === 'relevance') {
289
+ if (this.finalizedSortField === SortField.relevance) {
281
290
  return html`<div class="sort-direction-icon">${sortDisabledIcon}</div>`;
282
291
  }
283
292
 
284
293
  // For all other sorts, show the ascending/descending direction
285
294
  return html`
286
295
  <div class="sort-direction-icon">
287
- ${this.sortDirection === 'asc' ? sortUpIcon : sortDownIcon}
296
+ ${this.finalizedSortDirection === 'asc' ? sortUpIcon : sortDownIcon}
288
297
  </div>
289
298
  `;
290
299
  }
@@ -297,25 +306,25 @@ export class SortFilterBar
297
306
  class=${this.mobileSelectorVisible ? 'hidden' : 'visible'}
298
307
  >
299
308
  <ul id="desktop-sort-selector">
300
- <li>
301
- ${this.showRelevance
302
- ? this.getSortDisplayOption(SortField.relevance, {
309
+ ${this.showRelevance
310
+ ? html`<li>
311
+ ${this.getSortDisplayOption(SortField.relevance, {
303
312
  onClick: () => {
304
313
  this.dropdownBackdropVisible = false;
305
- if (this.selectedSort !== SortField.relevance) {
314
+ if (this.finalizedSortField !== SortField.relevance) {
306
315
  this.clearAlphaBarFilters();
307
316
  this.setSelectedSort(SortField.relevance);
308
317
  }
309
318
  },
310
- })
311
- : nothing}
312
- </li>
319
+ })}
320
+ </li>`
321
+ : nothing}
313
322
  <li>${this.viewsDropdownTemplate}</li>
314
323
  <li>
315
324
  ${this.getSortDisplayOption(SortField.title, {
316
325
  onClick: () => {
317
326
  this.dropdownBackdropVisible = false;
318
- if (this.selectedSort !== SortField.title) {
327
+ if (this.finalizedSortField !== SortField.title) {
319
328
  this.alphaSelectorVisible = 'title';
320
329
  this.selectedCreatorFilter = null;
321
330
  this.setSelectedSort(SortField.title);
@@ -329,7 +338,7 @@ export class SortFilterBar
329
338
  ${this.getSortDisplayOption(SortField.creator, {
330
339
  onClick: () => {
331
340
  this.dropdownBackdropVisible = false;
332
- if (this.selectedSort !== SortField.creator) {
341
+ if (this.finalizedSortField !== SortField.creator) {
333
342
  this.alphaSelectorVisible = 'creator';
334
343
  this.selectedTitleFilter = null;
335
344
  this.setSelectedSort(SortField.creator);
@@ -345,19 +354,24 @@ export class SortFilterBar
345
354
 
346
355
  /** The template to render all the sort options in mobile view */
347
356
  private get mobileSortSelectorTemplate() {
357
+ const isDisplayableField = (field: string) =>
358
+ field !== SortField.default &&
359
+ (field !== SortField.relevance || this.showRelevance);
360
+
348
361
  return html`
349
362
  <div
350
363
  id="mobile-sort-container"
351
364
  class=${this.mobileSelectorVisible ? 'visible' : 'hidden'}
352
365
  >
353
366
  ${this.getSortDropdown({
354
- displayName: html`${SortFieldDisplayName[this.selectedSort] ?? ''}`,
367
+ displayName: html`${SortFieldDisplayName[this.finalizedSortField] ??
368
+ 'Relevance'}`,
355
369
  id: 'mobile-dropdown',
356
370
  selected: true,
357
- dropdownOptions: Object.keys(SortField).map(field =>
358
- this.getDropdownOption(field as SortField)
359
- ),
360
- selectedOption: this.selectedSort ?? SortField.relevance,
371
+ dropdownOptions: Object.keys(SortField)
372
+ .filter(field => isDisplayableField(field))
373
+ .map(field => this.getDropdownOption(field as SortField)),
374
+ selectedOption: this.finalizedSortField,
361
375
  onOptionSelected: this.mobileSortChanged,
362
376
  onDropdownClick: () => {
363
377
  this.dropdownBackdropVisible = this.mobileDropdown.open;
@@ -392,7 +406,8 @@ export class SortFilterBar
392
406
  onClick?: (e: Event) => void;
393
407
  }
394
408
  ): TemplateResult {
395
- const isSelected = options?.selected ?? this.selectedSort === sortField;
409
+ const isSelected =
410
+ options?.selected ?? this.finalizedSortField === sortField;
396
411
  const displayName = options?.displayName ?? SortFieldDisplayName[sortField];
397
412
  return html`
398
413
  <button
@@ -653,7 +668,25 @@ export class SortFilterBar
653
668
 
654
669
  /** Toggles the current sort direction between 'asc' and 'desc' */
655
670
  private toggleSortDirection() {
656
- this.setSortDirection(this.sortDirection === 'desc' ? 'asc' : 'desc');
671
+ this.setSortDirection(
672
+ this.finalizedSortDirection === 'desc' ? 'asc' : 'desc'
673
+ );
674
+ }
675
+
676
+ private handleSortDirectionClicked(): void {
677
+ if (
678
+ !this.sortDirection &&
679
+ this.defaultSortField &&
680
+ this.defaultSortDirection
681
+ ) {
682
+ // When the sort direction is merely defaulted (not set by the user), clicking
683
+ // the toggled button should "promote" the default sort to an explicitly-set one
684
+ // and then toggle it as usual.
685
+ this.selectedSort = this.defaultSortField;
686
+ this.sortDirection = this.defaultSortDirection;
687
+ }
688
+
689
+ this.toggleSortDirection();
657
690
  }
658
691
 
659
692
  private setSelectedSort(sort: SortField) {
@@ -663,6 +696,20 @@ export class SortFilterBar
663
696
  this.emitSortChangedEvent();
664
697
  }
665
698
 
699
+ /** The current sort field, or the default one if no explicit sort is set */
700
+ private get finalizedSortField(): SortField {
701
+ return this.selectedSort === SortField.default
702
+ ? this.defaultSortField
703
+ : this.selectedSort;
704
+ }
705
+
706
+ /** The current sort direction, or the default one if no explicit direction is set */
707
+ private get finalizedSortDirection(): SortDirection | null {
708
+ return this.sortDirection === null
709
+ ? this.defaultSortDirection
710
+ : this.sortDirection;
711
+ }
712
+
666
713
  /**
667
714
  * There are four date sort options.
668
715
  *
@@ -680,7 +727,7 @@ export class SortFilterBar
680
727
  SortField.datereviewed,
681
728
  SortField.dateadded,
682
729
  ];
683
- return dateSortFields.includes(this.selectedSort);
730
+ return dateSortFields.includes(this.finalizedSortField);
684
731
  }
685
732
 
686
733
  /**
@@ -698,7 +745,7 @@ export class SortFilterBar
698
745
  SortField.alltimeview,
699
746
  SortField.weeklyview,
700
747
  ];
701
- return viewSortFields.includes(this.selectedSort);
748
+ return viewSortFields.includes(this.finalizedSortField);
702
749
  }
703
750
 
704
751
  /**
@@ -712,7 +759,7 @@ export class SortFilterBar
712
759
  private get dateSortField(): string {
713
760
  const defaultSort = SortFieldDisplayName[SortField.date];
714
761
  const name = this.dateOptionSelected
715
- ? SortFieldDisplayName[this.selectedSort] ?? defaultSort
762
+ ? SortFieldDisplayName[this.finalizedSortField] ?? defaultSort
716
763
  : defaultSort;
717
764
  return name;
718
765
  }
@@ -728,7 +775,7 @@ export class SortFilterBar
728
775
  private get viewSortField(): string {
729
776
  const defaultSort = SortFieldDisplayName[SortField.weeklyview];
730
777
  const name = this.viewOptionSelected
731
- ? SortFieldDisplayName[this.selectedSort] ?? defaultSort
778
+ ? SortFieldDisplayName[this.finalizedSortField] ?? defaultSort
732
779
  : defaultSort;
733
780
  return name;
734
781
  }
@@ -197,7 +197,7 @@ export class TileList extends BaseTileComponent {
197
197
  ${this.labelTemplate(msg('By'))}
198
198
  ${join(
199
199
  map(this.model.creators, id => this.searchLink('creator', id)),
200
- html`, `
200
+ ', '
201
201
  )}
202
202
  </div>
203
203
  `;
@@ -262,7 +262,7 @@ export class TileList extends BaseTileComponent {
262
262
  ${this.labelTemplate(msg('Topics'))}
263
263
  ${join(
264
264
  map(this.model.subjects, id => this.searchLink('subject', id)),
265
- html`, `
265
+ ', '
266
266
  )}
267
267
  </div>
268
268
  `;
@@ -275,7 +275,7 @@ export class TileList extends BaseTileComponent {
275
275
  return html`
276
276
  <div id="collections" class="metadata">
277
277
  ${this.labelTemplate(msg('Collections'))}
278
- ${join(this.collectionLinks, html`, `)}
278
+ ${join(this.collectionLinks, ', ')}
279
279
  </div>
280
280
  `;
281
281
  }
@@ -544,6 +544,10 @@ export class TileList extends BaseTileComponent {
544
544
  overflow-wrap: anywhere;
545
545
  }
546
546
 
547
+ #collections > a {
548
+ display: inline-block;
549
+ }
550
+
547
551
  #icon {
548
552
  padding-top: 5px;
549
553
  }
@@ -20,6 +20,7 @@ import { analyticsCategories } from '../src/utils/analytics-events';
20
20
  import type { TileDispatcher } from '../src/tiles/tile-dispatcher';
21
21
  import type { CollectionFacets } from '../src/collection-facets';
22
22
  import type { EmptyPlaceholder } from '../src/empty-placeholder';
23
+ import type { SortFilterBar } from '../src/sort-filter-bar/sort-filter-bar';
23
24
 
24
25
  /**
25
26
  * Wait for the next tick of the event loop.
@@ -93,7 +94,7 @@ describe('Collection Browser', () => {
93
94
  el.clearFilters({ sort: true }); // Sort is reset too due to the option
94
95
 
95
96
  expect(el.selectedFacets).to.deep.equal(getDefaultSelectedFacets());
96
- expect(el.selectedSort).to.equal('relevance');
97
+ expect(el.selectedSort).to.equal(SortField.default);
97
98
  expect(el.sortDirection).to.be.null;
98
99
  expect(el.sortParam).to.be.null;
99
100
  expect(el.selectedCreatorFilter).to.be.null;
@@ -763,7 +764,7 @@ describe('Collection Browser', () => {
763
764
  </collection-browser>`
764
765
  );
765
766
 
766
- expect(el.selectedSort).to.equal(SortField.relevance);
767
+ expect(el.selectedSort).to.equal(SortField.default);
767
768
 
768
769
  el.baseQuery = 'foo';
769
770
  await el.updateComplete;
@@ -988,6 +989,82 @@ describe('Collection Browser', () => {
988
989
  ).to.equal('/details/foo?q=%22quoted+query%22');
989
990
  });
990
991
 
992
+ it('sets default sort from collection metadata', async () => {
993
+ const searchService = new MockSearchService();
994
+ const el = await fixture<CollectionBrowser>(
995
+ html`<collection-browser
996
+ .searchService=${searchService}
997
+ .baseNavigationUrl=${''}
998
+ ></collection-browser>`
999
+ );
1000
+
1001
+ el.withinCollection = 'default-sort';
1002
+ await el.updateComplete;
1003
+ await el.initialSearchComplete;
1004
+ await el.updateComplete;
1005
+ await aTimeout(50);
1006
+
1007
+ const sortBar = el.shadowRoot?.querySelector(
1008
+ 'sort-filter-bar'
1009
+ ) as SortFilterBar;
1010
+ expect(sortBar).to.exist;
1011
+ expect(sortBar.defaultSortField).to.equal(SortField.title);
1012
+ expect(sortBar.defaultSortDirection).to.equal('asc');
1013
+ expect(sortBar.selectedSort).to.equal(SortField.default);
1014
+ expect(sortBar.sortDirection).to.be.null;
1015
+ });
1016
+
1017
+ it('sets default sort from collection metadata in "-field" format', async () => {
1018
+ const searchService = new MockSearchService();
1019
+ const el = await fixture<CollectionBrowser>(
1020
+ html`<collection-browser
1021
+ .searchService=${searchService}
1022
+ .baseNavigationUrl=${''}
1023
+ ></collection-browser>`
1024
+ );
1025
+
1026
+ el.withinCollection = 'default-sort-concise';
1027
+ await el.updateComplete;
1028
+ await el.initialSearchComplete;
1029
+ await el.updateComplete;
1030
+ await aTimeout(50);
1031
+
1032
+ const sortBar = el.shadowRoot?.querySelector(
1033
+ 'sort-filter-bar'
1034
+ ) as SortFilterBar;
1035
+ expect(sortBar).to.exist;
1036
+ expect(sortBar.defaultSortField).to.equal(SortField.dateadded);
1037
+ expect(sortBar.defaultSortDirection).to.equal('desc');
1038
+ expect(sortBar.selectedSort).to.equal(SortField.default);
1039
+ expect(sortBar.sortDirection).to.be.null;
1040
+ });
1041
+
1042
+ it('uses relevance sort as default when a query is set', async () => {
1043
+ const searchService = new MockSearchService();
1044
+ const el = await fixture<CollectionBrowser>(
1045
+ html`<collection-browser
1046
+ .searchService=${searchService}
1047
+ .baseNavigationUrl=${''}
1048
+ ></collection-browser>`
1049
+ );
1050
+
1051
+ el.withinCollection = 'default-sort';
1052
+ el.baseQuery = 'default-sort';
1053
+ await el.updateComplete;
1054
+ await el.initialSearchComplete;
1055
+ await el.updateComplete;
1056
+ await aTimeout(50);
1057
+
1058
+ const sortBar = el.shadowRoot?.querySelector(
1059
+ 'sort-filter-bar'
1060
+ ) as SortFilterBar;
1061
+ expect(sortBar).to.exist;
1062
+ expect(sortBar.defaultSortField).to.equal(SortField.relevance);
1063
+ expect(sortBar.defaultSortDirection).to.be.null;
1064
+ expect(sortBar.selectedSort).to.equal(SortField.default);
1065
+ expect(sortBar.sortDirection).to.be.null;
1066
+ });
1067
+
991
1068
  it('scrolls to page', async () => {
992
1069
  const searchService = new MockSearchService();
993
1070
  const el = await fixture<CollectionBrowser>(