@internetarchive/collection-browser 1.7.1-alpha.0 → 1.9.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.
Files changed (30) hide show
  1. package/dist/src/collection-browser.js +18 -11
  2. package/dist/src/collection-browser.js.map +1 -1
  3. package/dist/src/collection-facets/more-facets-content.d.ts +1 -0
  4. package/dist/src/collection-facets/more-facets-content.js +9 -2
  5. package/dist/src/collection-facets/more-facets-content.js.map +1 -1
  6. package/dist/src/collection-facets.d.ts +1 -0
  7. package/dist/src/collection-facets.js +4 -0
  8. package/dist/src/collection-facets.js.map +1 -1
  9. package/dist/src/models.d.ts +48 -35
  10. package/dist/src/models.js +139 -78
  11. package/dist/src/models.js.map +1 -1
  12. package/dist/src/restoration-state-handler.d.ts +9 -2
  13. package/dist/src/restoration-state-handler.js +61 -32
  14. package/dist/src/restoration-state-handler.js.map +1 -1
  15. package/dist/src/sort-filter-bar/sort-filter-bar.d.ts +2 -0
  16. package/dist/src/sort-filter-bar/sort-filter-bar.js +30 -25
  17. package/dist/src/sort-filter-bar/sort-filter-bar.js.map +1 -1
  18. package/dist/test/collection-facets/more-facets-content.test.js +25 -0
  19. package/dist/test/collection-facets/more-facets-content.test.js.map +1 -1
  20. package/dist/test/restoration-state-handler.test.js +53 -1
  21. package/dist/test/restoration-state-handler.test.js.map +1 -1
  22. package/package.json +3 -3
  23. package/src/collection-browser.ts +16 -8
  24. package/src/collection-facets/more-facets-content.ts +9 -2
  25. package/src/collection-facets.ts +3 -0
  26. package/src/models.ts +193 -109
  27. package/src/restoration-state-handler.ts +66 -40
  28. package/src/sort-filter-bar/sort-filter-bar.ts +34 -27
  29. package/test/collection-facets/more-facets-content.test.ts +35 -0
  30. package/test/restoration-state-handler.test.ts +68 -1
@@ -1,11 +1,6 @@
1
- import {
2
- SearchType,
3
- SortDirection,
4
- SortParam,
5
- } from '@internetarchive/search-service';
1
+ import { SearchType, SortDirection } from '@internetarchive/search-service';
6
2
  import { getCookie, setCookie } from 'typescript-cookie';
7
3
  import {
8
- MetadataSortField,
9
4
  FacetOption,
10
5
  CollectionBrowserContext,
11
6
  CollectionDisplayMode,
@@ -13,17 +8,15 @@ import {
13
8
  SortField,
14
9
  FacetBucket,
15
10
  FacetState,
16
- URLFieldToSortField,
17
- URLSortField,
18
11
  getDefaultSelectedFacets,
19
- MetadataFieldToURLField,
12
+ sortOptionFromAPIString,
13
+ SORT_OPTIONS,
20
14
  } from './models';
21
15
  import { arrayEquals } from './utils/array-equals';
22
16
 
23
17
  export interface RestorationState {
24
18
  displayMode?: CollectionDisplayMode;
25
19
  searchType?: SearchType;
26
- sortParam?: SortParam;
27
20
  selectedSort?: SortField;
28
21
  sortDirection?: SortDirection;
29
22
  selectedFacets: SelectedFacets;
@@ -120,11 +113,29 @@ export class RestorationStateHandler
120
113
  }
121
114
  }
122
115
 
123
- if (state.sortParam) {
124
- const prefix = state.sortParam.direction === 'desc' ? '-' : '';
125
- const readableSortField =
126
- MetadataFieldToURLField[state.sortParam.field as MetadataSortField];
127
- newParams.set('sort', `${prefix}${readableSortField}`);
116
+ if (state.selectedSort) {
117
+ const sortOption = SORT_OPTIONS[state.selectedSort];
118
+ let prefix = this.sortDirectionPrefix(state.sortDirection);
119
+
120
+ if (sortOption.field === SortField.unrecognized) {
121
+ // For unrecognized sorts, use the existing param, possibly updating its direction
122
+ const oldSortParam = oldParams.get('sort') ?? '';
123
+ const { field, direction } =
124
+ this.getSortFieldAndDirection(oldSortParam);
125
+
126
+ // Use the state-specified direction if available, or extract one from the param if not
127
+ if (!state.sortDirection) prefix = this.sortDirectionPrefix(direction);
128
+
129
+ if (field) {
130
+ newParams.set('sort', `${prefix}${field}`);
131
+ } else {
132
+ newParams.set('sort', oldSortParam);
133
+ }
134
+ } else if (sortOption.shownInURL) {
135
+ // Otherwise, use the canonical API form of the sort option
136
+ const canonicalApiSort = sortOption.urlNames[0];
137
+ newParams.set('sort', `${prefix}${canonicalApiSort}`);
138
+ }
128
139
  }
129
140
 
130
141
  if (state.selectedFacets) {
@@ -197,7 +208,7 @@ export class RestorationStateHandler
197
208
  query: state.baseQuery,
198
209
  searchType: state.searchType,
199
210
  page: state.currentPage,
200
- sort: state.sortParam,
211
+ sort: { field: state.selectedSort, direction: state.sortDirection },
201
212
  minDate: state.minSelectedDate,
202
213
  maxDate: state.maxSelectedDate,
203
214
  facets: state.selectedFacets,
@@ -261,29 +272,13 @@ export class RestorationStateHandler
261
272
  }
262
273
 
263
274
  if (sortQuery) {
264
- // check for two different sort formats: `date desc` and `-date`
265
- const hasSpace = sortQuery.indexOf(' ') > -1;
266
- if (hasSpace) {
267
- const [field, direction] = sortQuery.split(' ');
268
- const metadataField = URLFieldToSortField[field as URLSortField];
269
-
270
- if (metadataField) {
271
- restorationState.selectedSort = metadataField;
272
- }
273
- if (direction === 'desc' || direction === 'asc') {
274
- restorationState.sortDirection = direction as SortDirection;
275
- }
276
- } else {
277
- const direction = sortQuery.startsWith('-') ? 'desc' : 'asc';
278
- const field = sortQuery.startsWith('-')
279
- ? sortQuery.slice(1)
280
- : sortQuery;
281
-
282
- const metadataField = URLFieldToSortField[field as URLSortField];
283
- if (metadataField) {
284
- restorationState.selectedSort = metadataField;
285
- restorationState.sortDirection = direction as SortDirection;
286
- }
275
+ const { field, direction } = this.getSortFieldAndDirection(sortQuery);
276
+
277
+ const sortOption = sortOptionFromAPIString(field);
278
+ restorationState.selectedSort = sortOption.field;
279
+
280
+ if (['asc', 'desc'].includes(direction)) {
281
+ restorationState.sortDirection = direction as SortDirection;
287
282
  }
288
283
  }
289
284
 
@@ -296,6 +291,13 @@ export class RestorationStateHandler
296
291
  // which we want to normalize to 'creator', 'language', etc. if redirected here.
297
292
  field = field.replace(/Sorter$/, '');
298
293
 
294
+ // Legacy search also allowed a form of negative faceting like `and[]=-collection:foo`
295
+ // which we want to normalize to a not[] param instead
296
+ if (field.startsWith('-')) {
297
+ facetNots.push(and.slice(1));
298
+ return;
299
+ }
300
+
299
301
  switch (field) {
300
302
  case 'year': {
301
303
  const [minDate, maxDate] = value.split(' TO ');
@@ -354,7 +356,31 @@ export class RestorationStateHandler
354
356
  return restorationState;
355
357
  }
356
358
 
357
- // remove optional opening and closing quotes from a string
359
+ /**
360
+ * Converts a URL sort param into a field/direction pair, if possible.
361
+ * Either or both may be undefined if the param is not in a recognized format.
362
+ */
363
+ private getSortFieldAndDirection(sortParam: string) {
364
+ // check for two different sort formats: `date desc` and `-date`
365
+ const hasSpace = sortParam.indexOf(' ') > -1;
366
+ let field;
367
+ let direction;
368
+ if (hasSpace) {
369
+ [field, direction] = sortParam.split(' ');
370
+ } else {
371
+ field = sortParam.startsWith('-') ? sortParam.slice(1) : sortParam;
372
+ direction = sortParam.startsWith('-') ? 'desc' : 'asc';
373
+ }
374
+
375
+ return { field, direction };
376
+ }
377
+
378
+ /** Returns the `-` prefix for `desc` sort, or the empty string otherwise. */
379
+ private sortDirectionPrefix(sortDirection?: string) {
380
+ return sortDirection === 'desc' ? '-' : '';
381
+ }
382
+
383
+ /** Remove optional opening and closing quotes from a string */
358
384
  private stripQuotes(value: string): string {
359
385
  if (value.startsWith('"') && value.endsWith('"')) {
360
386
  return value.substring(1, value.length - 1);
@@ -16,11 +16,10 @@ import type { IaDropdown, optionInterface } from '@internetarchive/ia-dropdown';
16
16
  import type { SortDirection } from '@internetarchive/search-service';
17
17
  import {
18
18
  CollectionDisplayMode,
19
- DefaultSortDirection,
20
19
  PrefixFilterCounts,
21
20
  PrefixFilterType,
21
+ SORT_OPTIONS,
22
22
  SortField,
23
- SortFieldDisplayName,
24
23
  } from '../models';
25
24
  import './alpha-bar';
26
25
 
@@ -153,7 +152,8 @@ export class SortFilterBar
153
152
  }
154
153
 
155
154
  if (changed.has('selectedSort') && this.sortDirection === null) {
156
- this.sortDirection = DefaultSortDirection[this.finalizedSortField];
155
+ const sortOption = SORT_OPTIONS[this.finalizedSortField];
156
+ this.sortDirection = sortOption.defaultSortDirection;
157
157
  }
158
158
 
159
159
  if (changed.has('selectedTitleFilter') && this.selectedTitleFilter) {
@@ -274,7 +274,7 @@ export class SortFilterBar
274
274
  return html`
275
275
  <button
276
276
  class="sort-direction-selector"
277
- ?disabled=${this.finalizedSortField === SortField.relevance}
277
+ ?disabled=${!this.canChangeSortDirection}
278
278
  @click=${this.handleSortDirectionClicked}
279
279
  >
280
280
  <span class="sr-only">${srLabel}</span>
@@ -285,8 +285,8 @@ export class SortFilterBar
285
285
 
286
286
  /** Template to render the sort direction button's icon in the correct current state */
287
287
  private get sortDirectionIcon(): TemplateResult {
288
- // For relevance sort, show a fully disabled icon
289
- if (this.finalizedSortField === SortField.relevance) {
288
+ // Show a fully disabled icon for sort options without direction support
289
+ if (!this.canChangeSortDirection) {
290
290
  return html`<div class="sort-direction-icon">${sortDisabledIcon}</div>`;
291
291
  }
292
292
 
@@ -354,9 +354,9 @@ export class SortFilterBar
354
354
 
355
355
  /** The template to render all the sort options in mobile view */
356
356
  private get mobileSortSelectorTemplate() {
357
- const isDisplayableField = (field: string) =>
358
- field !== SortField.default &&
359
- (field !== SortField.relevance || this.showRelevance);
357
+ const displayedOptions = Object.values(SORT_OPTIONS)
358
+ .filter(opt => opt.shownInSortBar)
359
+ .filter(opt => this.showRelevance || opt.field !== SortField.relevance);
360
360
 
361
361
  return html`
362
362
  <div
@@ -364,13 +364,13 @@ export class SortFilterBar
364
364
  class=${this.mobileSelectorVisible ? 'visible' : 'hidden'}
365
365
  >
366
366
  ${this.getSortDropdown({
367
- displayName: html`${SortFieldDisplayName[this.finalizedSortField] ??
368
- 'Relevance'}`,
367
+ displayName: html`${SORT_OPTIONS[this.finalizedSortField]
368
+ .displayName}`,
369
369
  id: 'mobile-dropdown',
370
370
  selected: true,
371
- dropdownOptions: Object.keys(SortField)
372
- .filter(field => isDisplayableField(field))
373
- .map(field => this.getDropdownOption(field as SortField)),
371
+ dropdownOptions: displayedOptions.map(opt =>
372
+ this.getDropdownOption(opt.field)
373
+ ),
374
374
  selectedOption: this.finalizedSortField,
375
375
  onOptionSelected: this.mobileSortChanged,
376
376
  onDropdownClick: () => {
@@ -408,7 +408,8 @@ export class SortFilterBar
408
408
  ): TemplateResult {
409
409
  const isSelected =
410
410
  options?.selected ?? this.finalizedSortField === sortField;
411
- const displayName = options?.displayName ?? SortFieldDisplayName[sortField];
411
+ const displayName =
412
+ options?.displayName ?? SORT_OPTIONS[sortField].displayName;
412
413
  return html`
413
414
  <button
414
415
  class=${isSelected ? 'selected' : nothing}
@@ -488,7 +489,7 @@ export class SortFilterBar
488
489
  },
489
490
  label: html`
490
491
  <span class="dropdown-option-label">
491
- ${SortFieldDisplayName[sortField]}
492
+ ${SORT_OPTIONS[sortField].displayName}
492
493
  </span>
493
494
  `,
494
495
  };
@@ -692,7 +693,8 @@ export class SortFilterBar
692
693
  private setSelectedSort(sort: SortField) {
693
694
  this.selectedSort = sort;
694
695
  // Apply this field's default sort direction
695
- this.sortDirection = DefaultSortDirection[this.selectedSort];
696
+ const sortOption = SORT_OPTIONS[sort];
697
+ this.sortDirection = sortOption.defaultSortDirection;
696
698
  this.emitSortChangedEvent();
697
699
  }
698
700
 
@@ -710,6 +712,11 @@ export class SortFilterBar
710
712
  : this.sortDirection;
711
713
  }
712
714
 
715
+ /** Whether the sort direction button should be enabled for the current sort */
716
+ private get canChangeSortDirection(): boolean {
717
+ return SORT_OPTIONS[this.finalizedSortField].canSetDirection;
718
+ }
719
+
713
720
  /**
714
721
  * There are four date sort options.
715
722
  *
@@ -757,11 +764,11 @@ export class SortFilterBar
757
764
  * @memberof SortFilterBar
758
765
  */
759
766
  private get dateSortField(): string {
760
- const defaultSort = SortFieldDisplayName[SortField.date];
761
- const name = this.dateOptionSelected
762
- ? SortFieldDisplayName[this.finalizedSortField] ?? defaultSort
763
- : defaultSort;
764
- return name;
767
+ const defaultDateSort = SORT_OPTIONS[SortField.date];
768
+ const currentDateSort = this.dateOptionSelected
769
+ ? SORT_OPTIONS[this.finalizedSortField] ?? defaultDateSort
770
+ : defaultDateSort;
771
+ return currentDateSort.displayName;
765
772
  }
766
773
 
767
774
  /**
@@ -773,11 +780,11 @@ export class SortFilterBar
773
780
  * @memberof SortFilterBar
774
781
  */
775
782
  private get viewSortField(): string {
776
- const defaultSort = SortFieldDisplayName[SortField.weeklyview];
777
- const name = this.viewOptionSelected
778
- ? SortFieldDisplayName[this.finalizedSortField] ?? defaultSort
779
- : defaultSort;
780
- return name;
783
+ const defaultViewSort = SORT_OPTIONS[SortField.weeklyview];
784
+ const currentViewSort = this.viewOptionSelected
785
+ ? SORT_OPTIONS[this.finalizedSortField] ?? defaultViewSort
786
+ : defaultViewSort;
787
+ return currentViewSort.displayName;
781
788
  }
782
789
 
783
790
  private get titleSelectorBar() {
@@ -86,6 +86,41 @@ describe('More facets content', () => {
86
86
  expect(searchService.searchParams?.query).to.equal('title:hello');
87
87
  });
88
88
 
89
+ it('queries for more facets using search service within a collection (no query)', async () => {
90
+ const searchService = new MockSearchService();
91
+
92
+ const el = await fixture<MoreFacetsContent>(
93
+ html`<more-facets-content
94
+ .searchService=${searchService}
95
+ .withinCollection=${'foobar'}
96
+ ></more-facets-content>`
97
+ );
98
+
99
+ el.facetKey = 'subject';
100
+ await el.updateComplete;
101
+
102
+ expect(searchService.searchParams?.query).to.be.empty;
103
+ expect(searchService.searchParams?.pageTarget).to.equal('foobar');
104
+ });
105
+
106
+ it('queries for more facets using search service within a collection (with query)', async () => {
107
+ const searchService = new MockSearchService();
108
+
109
+ const el = await fixture<MoreFacetsContent>(
110
+ html`<more-facets-content
111
+ .searchService=${searchService}
112
+ .withinCollection=${'foobar'}
113
+ ></more-facets-content>`
114
+ );
115
+
116
+ el.facetKey = 'subject';
117
+ el.query = 'title:hello';
118
+ await el.updateComplete;
119
+
120
+ expect(searchService.searchParams?.query).to.equal('title:hello');
121
+ expect(searchService.searchParams?.pageTarget).to.equal('foobar');
122
+ });
123
+
89
124
  it('filter raw selectedFacets object', async () => {
90
125
  const searchService = new MockSearchService();
91
126
 
@@ -1,6 +1,6 @@
1
1
  import { SearchType } from '@internetarchive/search-service';
2
2
  import { expect } from '@open-wc/testing';
3
- import { getDefaultSelectedFacets } from '../src/models';
3
+ import { SortField, getDefaultSelectedFacets } from '../src/models';
4
4
  import { RestorationStateHandler } from '../src/restoration-state-handler';
5
5
 
6
6
  describe('Restoration state handler', () => {
@@ -245,6 +245,73 @@ describe('Restoration state handler', () => {
245
245
  expect(restorationState.sortDirection).to.equal('asc');
246
246
  });
247
247
 
248
+ it('should restore sort from URL (space format)', async () => {
249
+ const handler = new RestorationStateHandler({ context: 'search' });
250
+
251
+ const url = new URL(window.location.href);
252
+ url.search = '?sort=foo+desc';
253
+ window.history.replaceState({ path: url.href }, '', url.href);
254
+
255
+ const restorationState = handler.getRestorationState();
256
+ expect(restorationState.selectedSort).to.equal('unrecognized');
257
+ expect(restorationState.sortDirection).to.equal('desc');
258
+ });
259
+
260
+ it('should restore unrecognized sort from URL (prefix format)', async () => {
261
+ const handler = new RestorationStateHandler({ context: 'search' });
262
+
263
+ const url = new URL(window.location.href);
264
+ url.search = '?sort=-foo';
265
+ window.history.replaceState({ path: url.href }, '', url.href);
266
+
267
+ const restorationState = handler.getRestorationState();
268
+ expect(restorationState.selectedSort).to.equal('unrecognized');
269
+ expect(restorationState.sortDirection).to.equal('desc');
270
+ });
271
+
272
+ it('should save direction to URL even for unrecognized sort fields', async () => {
273
+ const url = new URL(window.location.href);
274
+ url.search = '?sort=foo';
275
+ window.history.replaceState({ path: url.href }, '', url.href);
276
+
277
+ const handler = new RestorationStateHandler({ context: 'search' });
278
+ handler.persistState({
279
+ selectedSort: SortField.unrecognized,
280
+ sortDirection: 'desc',
281
+ selectedFacets: getDefaultSelectedFacets(),
282
+ });
283
+
284
+ expect(window.location.search).to.equal('?sort=-foo');
285
+ });
286
+
287
+ it('should keep existing direction for unrecognized sort fields when unspecified in state', async () => {
288
+ const url = new URL(window.location.href);
289
+ url.search = '?sort=foo+desc';
290
+ window.history.replaceState({ path: url.href }, '', url.href);
291
+
292
+ const handler = new RestorationStateHandler({ context: 'search' });
293
+ handler.persistState({
294
+ selectedSort: SortField.unrecognized,
295
+ selectedFacets: getDefaultSelectedFacets(),
296
+ });
297
+
298
+ expect(window.location.search).to.equal('?sort=-foo');
299
+ });
300
+
301
+ it('should just ignore unrecognized sort fields w/ unknown formats', async () => {
302
+ const url = new URL(window.location.href);
303
+ url.search = '?sort=+foo';
304
+ window.history.replaceState({ path: url.href }, '', url.href);
305
+
306
+ const handler = new RestorationStateHandler({ context: 'search' });
307
+ handler.persistState({
308
+ selectedSort: SortField.unrecognized,
309
+ selectedFacets: getDefaultSelectedFacets(),
310
+ });
311
+
312
+ expect(window.location.search).to.equal('?sort=+foo');
313
+ });
314
+
248
315
  it('should not save current page state to the URL for page 1', async () => {
249
316
  const url = new URL(window.location.href);
250
317
  url.search = '';