@internetarchive/collection-browser 1.11.1-alpha.2 → 1.12.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.
@@ -10,6 +10,7 @@ import {
10
10
  import { customElement, property, state } from 'lit/decorators.js';
11
11
  import { map } from 'lit/directives/map.js';
12
12
  import { ref } from 'lit/directives/ref.js';
13
+ import { msg } from '@lit/localize';
13
14
  import type {
14
15
  Aggregation,
15
16
  AggregationSortType,
@@ -88,9 +89,13 @@ export class CollectionFacets extends LitElement {
88
89
 
89
90
  @property({ type: String }) query?: string;
90
91
 
92
+ @property({ type: String }) withinCollection?: string;
93
+
94
+ @property({ type: Array }) parentCollections: string[] = [];
95
+
91
96
  @property({ type: Object }) filterMap?: FilterMap;
92
97
 
93
- @property({ type: String }) withinCollection?: string;
98
+ @property({ type: String }) baseNavigationUrl?: string;
94
99
 
95
100
  @property({ type: String }) collectionPagePath: string = '/details/';
96
101
 
@@ -143,6 +148,7 @@ export class CollectionFacets extends LitElement {
143
148
  </section>
144
149
  `
145
150
  : nothing}
151
+ ${this.collectionPartOfTemplate}
146
152
  ${this.mergedFacets.map(facetGroup =>
147
153
  this.getFacetGroupTemplate(facetGroup)
148
154
  )}
@@ -150,6 +156,51 @@ export class CollectionFacets extends LitElement {
150
156
  `;
151
157
  }
152
158
 
159
+ private get collectionPartOfTemplate(): TemplateResult | typeof nothing {
160
+ // We only display the "Part Of" section on collection pages
161
+ if (!this.withinCollection || this.parentCollections.length === 0)
162
+ return nothing;
163
+
164
+ const headingId = 'partof-heading';
165
+ return html`
166
+ <section
167
+ class="facet-group partof-collections"
168
+ aria-labelledby=${headingId}
169
+ >
170
+ <div class="facet-group-header">
171
+ <h3 id=${headingId}>${msg('Part Of')}</h3>
172
+ </div>
173
+ <ul>
174
+ ${map(this.parentCollections, collxn => {
175
+ const collectionURL = `${this.baseNavigationUrl}${this.collectionPagePath}${collxn}`;
176
+
177
+ return html` <li>
178
+ <a
179
+ href=${collectionURL}
180
+ data-id=${collxn}
181
+ @click=${this.partOfCollectionClicked}
182
+ >
183
+ <async-collection-name
184
+ .collectionNameCache=${this.collectionNameCache}
185
+ .identifier=${collxn}
186
+ placeholder=${collxn}
187
+ ></async-collection-name>
188
+ </a>
189
+ </li>`;
190
+ })}
191
+ </ul>
192
+ </section>
193
+ `;
194
+ }
195
+
196
+ private partOfCollectionClicked(e: Event): void {
197
+ this.analyticsHandler?.sendEvent({
198
+ category: analyticsCategories.default,
199
+ action: analyticsActions.partOfCollectionClicked,
200
+ label: (e.target as HTMLElement).dataset.id,
201
+ });
202
+ }
203
+
153
204
  /**
154
205
  * Opens a modal dialog containing an enlarged version of the date picker.
155
206
  */
@@ -652,6 +703,14 @@ export class CollectionFacets extends LitElement {
652
703
  return [
653
704
  srOnlyStyle,
654
705
  css`
706
+ a:link {
707
+ text-decoration: none;
708
+ color: var(--ia-theme-link-color, #4b64ff);
709
+ }
710
+ a:link:hover {
711
+ text-decoration: underline;
712
+ }
713
+
655
714
  #container.loading {
656
715
  opacity: 0.5;
657
716
  }
@@ -711,6 +770,12 @@ export class CollectionFacets extends LitElement {
711
770
  max-height: 2000px;
712
771
  }
713
772
 
773
+ .partof-collections ul {
774
+ list-style-type: none;
775
+ padding: 0;
776
+ font-size: 1.2rem;
777
+ }
778
+
714
779
  h3 {
715
780
  font-size: 1.4rem;
716
781
  font-weight: 200
@@ -18,7 +18,6 @@ export type PlaceholderType =
18
18
  | 'empty-collection'
19
19
  | 'no-results'
20
20
  | 'query-error'
21
- | 'collection-error'
22
21
  | null;
23
22
  @customElement('empty-placeholder')
24
23
  export class EmptyPlaceholder extends LitElement {
@@ -48,11 +47,6 @@ export class EmptyPlaceholder extends LitElement {
48
47
  Tips for constructing search queries.
49
48
  </a> `);
50
49
 
51
- private static readonly MESSAGE_COLLECTION_ERROR = msg(html` The search engine
52
- encountered an error fetching details for this collection. If the problem
53
- persists, please let us know at
54
- <a href="mailto:info@archive.org">info@archive.org</a>.`);
55
-
56
50
  private static readonly QUERY_ERROR_DETAILS_MESSAGE = msg('Error details:');
57
51
 
58
52
  @property({ type: String }) placeholderType: PlaceholderType = null;
@@ -79,7 +73,6 @@ export class EmptyPlaceholder extends LitElement {
79
73
  ['empty-collection', () => this.emptyCollectionTemplate],
80
74
  ['no-results', () => this.noResultsTemplate],
81
75
  ['query-error', () => this.queryErrorTemplate],
82
- ['collection-error', () => this.collectionErrorTemplate],
83
76
  ])}
84
77
  </div>
85
78
  `;
@@ -120,16 +113,6 @@ export class EmptyPlaceholder extends LitElement {
120
113
  `;
121
114
  }
122
115
 
123
- private get collectionErrorTemplate(): TemplateResult {
124
- return html`
125
- <h2 class="title">${EmptyPlaceholder.MESSAGE_COLLECTION_ERROR}</h2>
126
- <div>${nullResultIcon}</div>
127
- <p class="error-details">
128
- ${EmptyPlaceholder.QUERY_ERROR_DETAILS_MESSAGE} ${this.detailMessage}
129
- </p>
130
- `;
131
- }
132
-
133
116
  static get styles(): CSSResultGroup {
134
117
  return css`
135
118
  :host {
@@ -14,6 +14,7 @@ export enum analyticsActions {
14
14
  facetDeselected = 'facetDeselected',
15
15
  facetNegativeSelected = 'facetNegativeSelected',
16
16
  facetNegativeDeselected = 'facetNegativeDeselected',
17
+ partOfCollectionClicked = 'partOfCollectionClicked',
17
18
  histogramChanged = 'histogramChanged',
18
19
  histogramChangedFromModal = 'histogramChangedFromModal',
19
20
  histogramExpanded = 'histogramExpanded',
@@ -1153,6 +1153,23 @@ describe('Collection Browser', () => {
1153
1153
  expect(mobileFacets).to.exist;
1154
1154
  });
1155
1155
 
1156
+ it('sets parent collections to prop when searching a collection', async () => {
1157
+ const searchService = new MockSearchService();
1158
+ const el = await fixture<CollectionBrowser>(
1159
+ html`<collection-browser
1160
+ .searchService=${searchService}
1161
+ .withinCollection=${'fake'}
1162
+ ></collection-browser>`
1163
+ );
1164
+
1165
+ el.baseQuery = 'parent-collections';
1166
+ await el.updateComplete;
1167
+ await el.initialSearchComplete;
1168
+ await aTimeout(0);
1169
+
1170
+ expect(el.parentCollections).to.deep.equal(['foo', 'bar']);
1171
+ });
1172
+
1156
1173
  it('refreshes when certain properties change - with some analytics event sampling', async () => {
1157
1174
  const mockAnalyticsHandler = new MockAnalyticsHandler();
1158
1175
  const searchService = new MockSearchService();
@@ -16,6 +16,7 @@ import {
16
16
  getDefaultSelectedFacets,
17
17
  } from '../src/models';
18
18
  import { MockAnalyticsHandler } from './mocks/mock-analytics-handler';
19
+ import { MockCollectionNameCache } from './mocks/mock-collection-name-cache';
19
20
 
20
21
  describe('Collection Facets', () => {
21
22
  it('has loader', async () => {
@@ -814,6 +815,41 @@ describe('Collection Facets', () => {
814
815
  expect(mockAnalyticsHandler.callLabel).to.equal('subject');
815
816
  });
816
817
 
818
+ it('includes Part Of section for collections', async () => {
819
+ const mockCollectionNameCache = new MockCollectionNameCache();
820
+ const el = await fixture<CollectionFacets>(
821
+ html`<collection-facets
822
+ .baseNavigationUrl=${''}
823
+ .withinCollection=${'foo'}
824
+ .parentCollections=${['bar', 'baz']}
825
+ .collectionNameCache=${mockCollectionNameCache}
826
+ ></collection-facets>`
827
+ );
828
+
829
+ const partOfSection = el.shadowRoot?.querySelector('.partof-collections');
830
+ expect(partOfSection).to.exist;
831
+
832
+ const partOfLinks = partOfSection?.querySelectorAll('a[href]');
833
+ expect(partOfLinks?.length).to.equal(2);
834
+
835
+ expect(partOfLinks?.[0]?.textContent?.trim()).to.equal('bar-name');
836
+ expect(partOfLinks?.[0]?.getAttribute('href')).to.equal('/details/bar');
837
+ expect(partOfLinks?.[1]?.textContent?.trim()).to.equal('baz-name');
838
+ expect(partOfLinks?.[1]?.getAttribute('href')).to.equal('/details/baz');
839
+ });
840
+
841
+ it('does not include Part Of section outside of collections', async () => {
842
+ // No withinCollection prop
843
+ const el = await fixture<CollectionFacets>(
844
+ html`<collection-facets
845
+ .parentCollections=${['bar', 'baz']}
846
+ ></collection-facets>`
847
+ );
848
+
849
+ const partOfSection = el.shadowRoot?.querySelector('.partof-collections');
850
+ expect(partOfSection).not.to.exist;
851
+ });
852
+
817
853
  it('fires analytics on expanding date picker', async () => {
818
854
  const mockAnalyticsHandler = new MockAnalyticsHandler();
819
855
 
@@ -846,4 +882,34 @@ describe('Collection Facets', () => {
846
882
  expect(mockAnalyticsHandler.callAction).to.equal('histogramExpanded');
847
883
  expect(mockAnalyticsHandler.callLabel).to.equal(window.location.href);
848
884
  });
885
+
886
+ it('fires analytics on clicking Part Of collection link', async () => {
887
+ const mockCollectionNameCache = new MockCollectionNameCache();
888
+ const mockAnalyticsHandler = new MockAnalyticsHandler();
889
+
890
+ const el = await fixture<CollectionFacets>(
891
+ html`<collection-facets
892
+ .baseNavigationUrl=${''}
893
+ .withinCollection=${'foo'}
894
+ .parentCollections=${['bar']}
895
+ .collectionNameCache=${mockCollectionNameCache}
896
+ .analyticsHandler=${mockAnalyticsHandler}
897
+ ></collection-facets>`
898
+ );
899
+
900
+ const partOfLinks = el.shadowRoot?.querySelectorAll(
901
+ '.partof-collections a[href]'
902
+ );
903
+ expect(partOfLinks?.length).to.equal(1);
904
+
905
+ // Click the expand button to open the modal
906
+ const link = partOfLinks?.[0] as HTMLAnchorElement;
907
+ link?.addEventListener('click', e => e.preventDefault());
908
+ link?.click();
909
+ await el.updateComplete;
910
+
911
+ expect(mockAnalyticsHandler.callCategory).to.equal('collection-browser');
912
+ expect(mockAnalyticsHandler.callAction).to.equal('partOfCollectionClicked');
913
+ expect(mockAnalyticsHandler.callLabel).to.equal('bar');
914
+ });
849
915
  });
@@ -728,6 +728,52 @@ export const getMockSuccessWithDefaultFavSort: () => Result<
728
728
  },
729
729
  });
730
730
 
731
+ export const getMockSuccessWithParentCollections: () => Result<
732
+ SearchResponse,
733
+ SearchServiceError
734
+ > = () => ({
735
+ success: {
736
+ request: {
737
+ kind: 'hits',
738
+ clientParameters: {
739
+ user_query: 'parent-collections',
740
+ sort: [],
741
+ },
742
+ backendRequests: {
743
+ primary: {
744
+ kind: 'hits',
745
+ finalized_parameters: {
746
+ user_query: 'parent-collections',
747
+ sort: [],
748
+ },
749
+ },
750
+ },
751
+ },
752
+ rawResponse: {},
753
+ response: {
754
+ totalResults: 1,
755
+ returnedCount: 1,
756
+ results: [
757
+ new ItemHit({
758
+ fields: {
759
+ identifier: 'foo',
760
+ title: 'Foo',
761
+ },
762
+ }),
763
+ ],
764
+ collectionExtraInfo: {
765
+ public_metadata: {
766
+ collection: ['foo', 'bar'],
767
+ },
768
+ },
769
+ },
770
+ responseHeader: {
771
+ succeeded: true,
772
+ query_time: 0,
773
+ },
774
+ },
775
+ });
776
+
731
777
  export const getMockErrorResult: () => Result<
732
778
  SearchResponse,
733
779
  SearchServiceError
@@ -25,6 +25,7 @@ import {
25
25
  getMockSuccessWithDefaultSort,
26
26
  getMockSuccessWithConciseDefaultSort,
27
27
  getMockSuccessWithDefaultFavSort,
28
+ getMockSuccessWithParentCollections,
28
29
  } from './mock-search-responses';
29
30
 
30
31
  const responses: Record<
@@ -45,6 +46,7 @@ const responses: Record<
45
46
  'default-sort': getMockSuccessWithDefaultSort,
46
47
  'default-sort-concise': getMockSuccessWithConciseDefaultSort,
47
48
  'fav-sort': getMockSuccessWithDefaultFavSort,
49
+ 'parent-collections': getMockSuccessWithParentCollections,
48
50
  error: getMockErrorResult,
49
51
  malformed: getMockMalformedResult,
50
52
  };