From 950734779e633083ba2766fc41ea1f8bdf893fa3 Mon Sep 17 00:00:00 2001 From: futa-ikeda <51409893+futa-ikeda@users.noreply.github.com> Date: Fri, 24 Mar 2023 09:18:38 -0400 Subject: [PATCH 01/60] Add new route for search (#1830) --- app/router.ts | 1 + app/search/route.ts | 4 ++++ app/search/template.hbs | 2 ++ 3 files changed, 7 insertions(+) create mode 100644 app/search/route.ts create mode 100644 app/search/template.hbs diff --git a/app/router.ts b/app/router.ts index 06be6e4c76a..b0d9f5c8833 100644 --- a/app/router.ts +++ b/app/router.ts @@ -20,6 +20,7 @@ Router.map(function() { this.route('home', { path: '/' }); this.route('dashboard'); this.route('goodbye'); + this.route('search'); this.route('institutions', function() { this.route('dashboard', { path: '/:institution_id/dashboard' }); }); diff --git a/app/search/route.ts b/app/search/route.ts new file mode 100644 index 00000000000..dd27660b802 --- /dev/null +++ b/app/search/route.ts @@ -0,0 +1,4 @@ +import Route from '@ember/routing/route'; + +export default class Search extends Route { +} diff --git a/app/search/template.hbs b/app/search/template.hbs new file mode 100644 index 00000000000..0082a7f0f25 --- /dev/null +++ b/app/search/template.hbs @@ -0,0 +1,2 @@ +{{!-- template-lint-disable no-bare-strings --}} +New search placeholder From 2aa4c9cd16e538649956a96c2fcc6f4ab24364c3 Mon Sep 17 00:00:00 2001 From: futa-ikeda <51409893+futa-ikeda@users.noreply.github.com> Date: Wed, 19 Apr 2023 11:05:22 -0400 Subject: [PATCH 02/60] [ENG-4450] Add new share-search models (#1835) - Ticket: [ENG-4450] - Feature flag: n/a ## Purpose - Add new models needed for SHARE-powered search page ## Summary of Changes - Add new models - `metadata-record-search` - `metadata-property-search` - `metadata-value-search` - `metadata-record` - `search-match` - New `ShareAdapter` and `ShareSerializer` to be used by these new models - New mirage endpoint for metadata-record-search (other endpoints coming later) --- app/adapters/metadata-property-search.ts | 10 + app/adapters/metadata-record-search.ts | 9 + app/adapters/metadata-record.ts | 10 + app/adapters/metadata-value-search.ts | 10 + app/adapters/search-result.ts | 10 + app/adapters/share-adapter.ts | 6 + app/models/metadata-property-search.ts | 21 ++ app/models/metadata-record-search.ts | 28 ++ app/models/metadata-record.ts | 21 ++ app/models/metadata-value-search.ts | 25 ++ app/models/search-result.ts | 29 ++ app/serializers/metadata-property-search.ts | 10 + app/serializers/metadata-record-search.ts | 10 + app/serializers/metadata-record.ts | 10 + app/serializers/metadata-value-search.ts | 10 + app/serializers/search-result.ts | 10 + app/serializers/share-serializer.ts | 12 + mirage/config.ts | 7 + mirage/views/search.ts | 364 ++++++++++++++++++++ 19 files changed, 612 insertions(+) create mode 100644 app/adapters/metadata-property-search.ts create mode 100644 app/adapters/metadata-record-search.ts create mode 100644 app/adapters/metadata-record.ts create mode 100644 app/adapters/metadata-value-search.ts create mode 100644 app/adapters/search-result.ts create mode 100644 app/adapters/share-adapter.ts create mode 100644 app/models/metadata-property-search.ts create mode 100644 app/models/metadata-record-search.ts create mode 100644 app/models/metadata-record.ts create mode 100644 app/models/metadata-value-search.ts create mode 100644 app/models/search-result.ts create mode 100644 app/serializers/metadata-property-search.ts create mode 100644 app/serializers/metadata-record-search.ts create mode 100644 app/serializers/metadata-record.ts create mode 100644 app/serializers/metadata-value-search.ts create mode 100644 app/serializers/search-result.ts create mode 100644 app/serializers/share-serializer.ts create mode 100644 mirage/views/search.ts diff --git a/app/adapters/metadata-property-search.ts b/app/adapters/metadata-property-search.ts new file mode 100644 index 00000000000..a1b399e109d --- /dev/null +++ b/app/adapters/metadata-property-search.ts @@ -0,0 +1,10 @@ +import ShareAdapter from './share-adapter'; + +export default class MetadataPropertySearchAdapter extends ShareAdapter { +} + +declare module 'ember-data/types/registries/adapter' { + export default interface AdapterRegistry { + 'metadata-property-search': MetadataPropertySearchAdapter; + } // eslint-disable-line semi +} diff --git a/app/adapters/metadata-record-search.ts b/app/adapters/metadata-record-search.ts new file mode 100644 index 00000000000..6049cfaaf99 --- /dev/null +++ b/app/adapters/metadata-record-search.ts @@ -0,0 +1,9 @@ +import ShareAdapter from './share-adapter'; +export default class MetadataRecordSearchAdapter extends ShareAdapter { +} + +declare module 'ember-data/types/registries/adapter' { + export default interface AdapterRegistry { + 'metadata-record-search': MetadataRecordSearchAdapter; + } // eslint-disable-line semi +} diff --git a/app/adapters/metadata-record.ts b/app/adapters/metadata-record.ts new file mode 100644 index 00000000000..dfbf851870e --- /dev/null +++ b/app/adapters/metadata-record.ts @@ -0,0 +1,10 @@ +import ShareAdapter from './share-adapter'; + +export default class MetadataRecordAdapter extends ShareAdapter { +} + +declare module 'ember-data/types/registries/adapter' { + export default interface AdapterRegistry { + 'metadata-record': MetadataRecordAdapter; + } // eslint-disable-line semi +} diff --git a/app/adapters/metadata-value-search.ts b/app/adapters/metadata-value-search.ts new file mode 100644 index 00000000000..e1e021292df --- /dev/null +++ b/app/adapters/metadata-value-search.ts @@ -0,0 +1,10 @@ +import ShareAdapter from './share-adapter'; + +export default class MetadataValueSearchAdapter extends ShareAdapter { +} + +declare module 'ember-data/types/registries/adapter' { + export default interface AdapterRegistry { + 'metadata-value-search': MetadataValueSearchAdapter; + } // eslint-disable-line semi +} diff --git a/app/adapters/search-result.ts b/app/adapters/search-result.ts new file mode 100644 index 00000000000..1fa4045fe5b --- /dev/null +++ b/app/adapters/search-result.ts @@ -0,0 +1,10 @@ +import ShareAdapter from './share-adapter'; + +export default class SearchResultAdapter extends ShareAdapter { +} + +declare module 'ember-data/types/registries/adapter' { + export default interface AdapterRegistry { + 'search-result': SearchResultAdapter; + } // eslint-disable-line semi +} diff --git a/app/adapters/share-adapter.ts b/app/adapters/share-adapter.ts new file mode 100644 index 00000000000..63194a537b5 --- /dev/null +++ b/app/adapters/share-adapter.ts @@ -0,0 +1,6 @@ +import JSONAPIAdapter from '@ember-data/adapter/json-api'; + +export default class ShareAdapter extends JSONAPIAdapter { + host = 'https://share.osf.io'; + namespace = 'api/v2'; +} diff --git a/app/models/metadata-property-search.ts b/app/models/metadata-property-search.ts new file mode 100644 index 00000000000..744ced694ba --- /dev/null +++ b/app/models/metadata-property-search.ts @@ -0,0 +1,21 @@ +import Model, { AsyncHasMany, attr, hasMany } from '@ember-data/model'; + +import { SearchFilter } from './metadata-record-search'; +import SearchResultModel from './search-result'; + +export default class MetadataPropertySearchModel extends Model { + @attr('string') propertySearchText!: string; + @attr('array') propertySearchFilter!: SearchFilter[]; + @attr('string') recordSearchText!: string; + @attr('array') recordSearchFilter!: SearchFilter[]; + @attr('number') totalResultCount!: number; + + @hasMany('search-result', { inverse: null }) + searchResultPage!: AsyncHasMany & SearchResultModel[]; +} + +declare module 'ember-data/types/registries/model' { + export default interface ModelRegistry { + 'metadata-property-search': MetadataPropertySearchModel; + } // eslint-disable-line semi +} diff --git a/app/models/metadata-record-search.ts b/app/models/metadata-record-search.ts new file mode 100644 index 00000000000..199301e0461 --- /dev/null +++ b/app/models/metadata-record-search.ts @@ -0,0 +1,28 @@ +import Model, { AsyncBelongsTo, AsyncHasMany, attr, belongsTo, hasMany } from '@ember-data/model'; + +import MetadataPropertySearchModel from './metadata-property-search'; +import SearchResultModel from './search-result'; + +export interface SearchFilter { + propertyPath: string; + filterValue: string[]; + filterType?: string; +} + +export default class MetadataRecordSearchModel extends Model { + @attr('string') recordSearchText!: string; + @attr('array') recordSearchFilters!: SearchFilter[]; + @attr('number') totalResultCount!: number; + + @hasMany('search-result', { inverse: null }) + searchResultPage!: AsyncHasMany & SearchResultModel[]; + + @belongsTo('metadata-property-search', { inverse: null }) + relatedPropertySearch!: AsyncBelongsTo & MetadataPropertySearchModel; +} + +declare module 'ember-data/types/registries/model' { + export default interface ModelRegistry { + 'metadata-record-search': MetadataRecordSearchModel; + } // eslint-disable-line semi +} diff --git a/app/models/metadata-record.ts b/app/models/metadata-record.ts new file mode 100644 index 00000000000..e6480363a39 --- /dev/null +++ b/app/models/metadata-record.ts @@ -0,0 +1,21 @@ +import Model, { AsyncHasMany, attr, hasMany } from '@ember-data/model'; + +export interface LanguageText { + '@language': string; + '@value': string; +} + +export default class MetadataRecordModel extends Model { + @attr('array') resourceType!: string[]; + @attr('array') resourceIdentifier!: string[]; + @attr('object') resourceMetadata!: any; + + @hasMany('metadata-record', { inverse: null }) + relatedRecordSet!: AsyncHasMany & MetadataRecordModel[]; +} + +declare module 'ember-data/types/registries/model' { + export default interface ModelRegistry { + 'metadata-record': MetadataRecordModel; + } // eslint-disable-line semi +} diff --git a/app/models/metadata-value-search.ts b/app/models/metadata-value-search.ts new file mode 100644 index 00000000000..cc97695694a --- /dev/null +++ b/app/models/metadata-value-search.ts @@ -0,0 +1,25 @@ +import Model, { AsyncHasMany, AsyncBelongsTo, attr, belongsTo, hasMany } from '@ember-data/model'; + +import MetadataPropertySearchModel from './metadata-property-search'; +import { SearchFilter } from './metadata-record-search'; +import SearchResultModel from './search-result'; + +export default class MetadataValueSearchModel extends Model { + @attr('string') valueSearchText!: string; + @attr('array') valueSearchFilter!: SearchFilter[]; + @attr('string') recordSearchText!: string; + @attr('array') recordSearchFilter!: SearchFilter[]; + @attr('number') totalResultCount!: number; + + @hasMany('search-result', { inverse: null }) + searchResultPage!: AsyncHasMany & SearchResultModel[]; + + @belongsTo('metadata-property-search', { inverse: null }) + relatedPropertySearch!: AsyncBelongsTo & MetadataPropertySearchModel; +} + +declare module 'ember-data/types/registries/model' { + export default interface ModelRegistry { + 'metadata-value-search': MetadataValueSearchModel; + } // eslint-disable-line semi +} diff --git a/app/models/search-result.ts b/app/models/search-result.ts new file mode 100644 index 00000000000..af9ddc859c5 --- /dev/null +++ b/app/models/search-result.ts @@ -0,0 +1,29 @@ +import Model, { AsyncBelongsTo, attr, belongsTo } from '@ember-data/model'; + +import MetadataRecordModel from './metadata-record'; + +export interface IriMatchEvidence { + '@type': 'IriMatchEvidence'; + matchingIri: string; + propertyPath: string[]; +} + +export interface TextMatchEvidence { + '@type': 'TextMatchEvidence'; + matchingHighlight: string; + propertyPath: string[]; +} + +export default class SearchResultModel extends Model { + @attr('array') matchEvidence!: Array; + @attr('number') recordResultCount!: number; + + @belongsTo('metadata-record', { inverse: null }) + metadataRecord!: AsyncBelongsTo | MetadataRecordModel; +} + +declare module 'ember-data/types/registries/model' { + export default interface ModelRegistry { + 'search-result': SearchResultModel; + } // eslint-disable-line semi +} diff --git a/app/serializers/metadata-property-search.ts b/app/serializers/metadata-property-search.ts new file mode 100644 index 00000000000..6751421b625 --- /dev/null +++ b/app/serializers/metadata-property-search.ts @@ -0,0 +1,10 @@ +import ShareSerializer from './share-serializer'; + +export default class MetadataPropertySearchSerializer extends ShareSerializer { +} + +declare module 'ember-data/types/registries/serializer' { + export default interface SerializerRegistry { + 'metadata-property-search': MetadataPropertySearchSerializer; + } // eslint-disable-line semi +} diff --git a/app/serializers/metadata-record-search.ts b/app/serializers/metadata-record-search.ts new file mode 100644 index 00000000000..c868b4c2edc --- /dev/null +++ b/app/serializers/metadata-record-search.ts @@ -0,0 +1,10 @@ +import ShareSerializer from './share-serializer'; + +export default class MetadataRecordSearchSerializer extends ShareSerializer { +} + +declare module 'ember-data/types/registries/serializer' { + export default interface SerializerRegistry { + 'metadata-record-search': MetadataRecordSearchSerializer; + } // eslint-disable-line semi +} diff --git a/app/serializers/metadata-record.ts b/app/serializers/metadata-record.ts new file mode 100644 index 00000000000..a91a8e27bdc --- /dev/null +++ b/app/serializers/metadata-record.ts @@ -0,0 +1,10 @@ +import ShareSerializer from './share-serializer'; + +export default class MetadataRecordSerializer extends ShareSerializer { +} + +declare module 'ember-data/types/registries/serializer' { + export default interface SerializerRegistry { + 'metadata-record': MetadataRecordSerializer; + } // eslint-disable-line semi +} diff --git a/app/serializers/metadata-value-search.ts b/app/serializers/metadata-value-search.ts new file mode 100644 index 00000000000..655a3f882cb --- /dev/null +++ b/app/serializers/metadata-value-search.ts @@ -0,0 +1,10 @@ +import ShareSerializer from './share-serializer'; + +export default class MetadataValueSearchSerializer extends ShareSerializer { +} + +declare module 'ember-data/types/registries/serializer' { + export default interface SerializerRegistry { + 'metadata-value-search': MetadataValueSearchSerializer; + } // eslint-disable-line semi +} diff --git a/app/serializers/search-result.ts b/app/serializers/search-result.ts new file mode 100644 index 00000000000..20208cd602e --- /dev/null +++ b/app/serializers/search-result.ts @@ -0,0 +1,10 @@ +import ShareSerializer from './share-serializer'; + +export default class SearchResultSerializer extends ShareSerializer { +} + +declare module 'ember-data/types/registries/serializer' { + export default interface SerializerRegistry { + 'search-result': SearchResultSerializer; + } // eslint-disable-line semi +} diff --git a/app/serializers/share-serializer.ts b/app/serializers/share-serializer.ts new file mode 100644 index 00000000000..8db52140260 --- /dev/null +++ b/app/serializers/share-serializer.ts @@ -0,0 +1,12 @@ +import JSONAPISerializer from '@ember-data/serializer/json-api'; +import { camelize } from '@ember/string'; + +export default class ShareSerializer extends JSONAPISerializer { + keyForAttribute(key: string) { + return camelize(key); + } + + keyForRelationship(key: string) { + return camelize(key); + } +} diff --git a/mirage/config.ts b/mirage/config.ts index 4fee24ca92b..d1cfa53b3b1 100644 --- a/mirage/config.ts +++ b/mirage/config.ts @@ -37,6 +37,7 @@ import { createNewSchemaResponse } from './views/schema-response'; import { createSchemaResponseAction } from './views/schema-response-action'; import { rootDetail } from './views/root'; import { shareSearch } from './views/share-search'; +import { recordSearch } from './views/search'; import { createToken } from './views/token'; import { createEmails, updateEmails } from './views/update-email'; import { @@ -57,8 +58,14 @@ export default function(this: Server) { this.urlPrefix = 'https://share.osf.io'; this.namespace = '/api/v2/'; + // SHARE-powered registration discover endpoint this.post('/search/creativeworks/_search', shareSearch); + // SHARE-powered search endpoints + this.get('/metadata-record-searches', recordSearch); + // this.get('/metadata-value-searches', valueSearch); + // this.get('/metadata-records/:id', metadataRecordDetail); + this.urlPrefix = apiUrl; this.namespace = '/v2'; diff --git a/mirage/views/search.ts b/mirage/views/search.ts new file mode 100644 index 00000000000..3bc43082c90 --- /dev/null +++ b/mirage/views/search.ts @@ -0,0 +1,364 @@ +import { Request, Schema } from 'ember-cli-mirage'; + +export function recordSearch(_: Schema, __: Request) { + // TODO: replace with a real metadata-record-search and use request to populate attrs + return { + data: { + type: 'metadata-record-search', + id: 'zzzzzz', + attributes:{ + recordSearchText: 'hello', + recordSearchFilter: [ + { + propertyPath: 'resourceType', + filterType: 'eq', + filterValues: [ + 'osf:Registration', + ], + }, + { + propertyPath: 'subject', + filterType: 'eq', + filterValues: [ + 'https://subjects.org/subjectId', + ], + }, + ], + totalResultCount: 3, + }, + relationships: { + searchResultPage: { + data: [ + { + type: 'search-result', + id: 'abc', + }, + { + type: 'search-result', + id: 'def', + }, + { + type: 'search-result', + id: 'ghi', + }, + ], + links: { + next: '...', + last: '...', + }, + }, + relatedPropertySearch: { + data: { + type: 'metadata-property-search', + id: 'tuv', + }, + }, + }, + }, + included: [ + { + type: 'search-result', + id: 'abc', + attributes: { + matchEvidence: [ + { + propertyPath: 'description', + matchingHighlight: '... say hello!', + }, + { + propertyPath: 'title', + matchingHighlight: '... shout hello!', + }, + ], + }, + relationships: { + metadataRecord: { + data: { + type: 'metadata-record', + id: 'abc', + }, + links: { + related: 'https://share.osf.io/api/v2/metadata-record/abc', + }, + }, + }, + }, + { + type: 'search-result', + id: 'def', + attributes: { + matchEvidence: [ + { + propertyPath: 'description', + matchingHighlight: '... computer said hello world!', + }, + ], + }, + relationships: { + metadataRecord: { + data: { + type: 'metadata-record', + id: 'def', + }, + links: { + related: 'https://share.osf.io/api/v2/metadata-record/def', + }, + }, + }, + }, + { + type: 'search-result', + id: 'ghi', + attributes: { + matchEvidence: [ + { + propertyPath: 'title', + matchingHighlight: '... you said hello!', + }, + ], + }, + relationships: { + metadataRecord: { + data: { + type: 'metadata-record', + id: 'abc', + }, + links: { + related: 'https://share.osf.io/api/v2/metadata-record/abc', + }, + }, + }, + }, + { + type: 'metadata-record', + id: 'abc', + attributes: { + resourceType: [ + 'osf:Registration', + 'dcterms:Dataset', + ], + resourceIdentifier: [ + 'https://osf.example/abcfoo', + 'https://doi.org/10.0000/osf.example/abcfoo', + ], + resourceMetadata: { + '@id': 'https://osf.example/abcfoo', + '@type': 'osf:Registration', + title: [ + { + '@value': 'I shout hello!', + '@language': 'en', + }, + ], + description: [ + { + '@value': 'I say hello!', + '@language': 'en', + }, + ], + isPartOf: [ + { + '@id': 'https://osf.example/xyzfoo', + '@type': 'osf:Registration', + title: [ + { + '@value': 'a parent!', + '@language': 'en', + }, + ], + }, + ], + hasPart: [ + { + '@id': 'https://osf.example/deffoo', + '@type': 'osf:Registration', + title: [ + { + '@value': 'a child!', + '@language': 'en', + }, + ], + }, + { + '@id': 'https://osf.example/ghifoo', + '@type': 'osf:Registration', + title: [ + { + '@value': 'another child!', + '@language': 'en', + }, + ], + }, + ], + subject: [ + { + '@id': 'https://subjects.org/subjectId', + '@type': 'dcterms:Subject', + label: [ + { + '@value': 'wibbleplop', + '@language': 'wi-bl', + }, + ], + }, + ], + creator: { + '@id': 'https://osf.example/person', + '@type': 'dcterms:Agent', + specificType: 'foaf:Person', + name: 'person person, prsn', + }, + // ... + }, + }, + links: { + self: 'https://share.osf.io/api/v2/metadata-record/abc', + resource: 'https://osf.example/abcfoo', + }, + }, + { + type: 'metadata-record', + id: 'def', + // ... + }, + { + type: 'metadata-record', + id: 'ghi', + // ... + }, + // Related properties search object + { + type: 'metadata-property-search', + id: 'tuv', + attributes: { + recordSearchText: 'hello', + recordSearchFilter: [ + { + propertyPath: 'resourceType', + filterType: 'eq', + filterValues: [ + 'osf:Registration', + ], + }, + { + propertyPath: 'subject', + filterType: 'eq', + filterValues: [ + 'https://subjects.org/subjectId', + ], + }, + ], + propertySearchText: '', + propertySearchFilter: [ + { + propertyPath: 'resourceType', + filterType: 'eq', + filterValues: [ + 'rdf:Property', + ], + }, + ], + }, + relationships: { + searchResultPage: { + data: [ + { + type: 'search-result', + id: 'propertyMatch1', + }, + { + type: 'search-result', + id: 'propertyMatch2', + }, + { + type: 'search-result', + id: 'propertyMatch3', + }, + ], + links: { + next: '...', + last: '...', + }, + }, + }, + }, + { + type: 'search-result', + id: 'propertyMatch1', + attributes: { + matchEvidence: [ + { + propertyPath: 'resourceType', + matchingIri: 'rdf:Property', + }, + ], + recordResultCount: 345, + }, + relationships: { + metadataRecord: { + data: { + type: 'metadata-record', + id: 'idForPropertyRecord1', + }, + links: { + related: 'https://share.osf.io/api/v2/metadata-record/idForPropertyRecord1', + }, + }, + }, + }, + { + type: 'search-result', + id: 'propertyMatch2', + // ... + }, + { + type: 'search-result', + id: 'propertyMatch3', + // ... + }, + { + type: 'metadata-record', + id: 'idForPropertyRecord1', + attributes: { + resourceType: [ + 'rdf:Property', + ], + resourceIdentifier: [ + 'http://purl.org/dc/terms/license', + ], + resourceMetadata: { + '@id': 'http://purl.org/dc/terms/license', + '@type': 'rdf:Property', + label: [ + { + '@value': 'License', + '@language': 'en', + }, + ], + description: [ + { + '@value': 'Some description about license in this case', + '@language': 'en', + }, + ], + // ... + }, + }, + links: { + self: 'https://share.osf.io/api/v2/metadata-record/idForPropertyRecord1', + resource: 'http://purl.org/dc/terms/license', + }, + }, + { + type: 'metadata-record', + id: 'idForPropertyRecord2', + // ... + }, + { + type: 'metadata-record', + id: 'idForPropertyRecord3', + // ... + }, + ], + }; +} From 8ac0470b8efe204c3a20390e5ad7b1dceda67974 Mon Sep 17 00:00:00 2001 From: futa-ikeda <51409893+futa-ikeda@users.noreply.github.com> Date: Mon, 24 Apr 2023 10:23:48 -0400 Subject: [PATCH 03/60] Add basic search page layout (#1850) --- app/search/controller.ts | 46 ++++++++++++++++++++++++++++++++++++ app/search/styles.scss | 34 +++++++++++++++++++++++++++ app/search/template.hbs | 51 +++++++++++++++++++++++++++++++++++++++- translations/en-us.yml | 6 +++++ 4 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 app/search/controller.ts create mode 100644 app/search/styles.scss diff --git a/app/search/controller.ts b/app/search/controller.ts new file mode 100644 index 00000000000..8f2bb7d7a1f --- /dev/null +++ b/app/search/controller.ts @@ -0,0 +1,46 @@ +import Store from '@ember-data/store'; +import Controller from '@ember/controller'; +import { action } from '@ember/object'; +import { inject as service } from '@ember/service'; +import { waitFor } from '@ember/test-waiters'; +import { task } from 'ember-concurrency'; +import { taskFor } from 'ember-concurrency-ts'; +import { tracked } from '@glimmer/tracking'; +import Media from 'ember-responsive'; + +import MetadataPropertySearchModel from 'ember-osf-web/models/metadata-property-search'; + +export default class SearchController extends Controller { + @service store!: Store; + @service media!: Media; + + queryParams = ['q', 'page', 'sort']; + @tracked q?: string; + @tracked page?: number; + @tracked sort?: string; + + @tracked propertySearch?: MetadataPropertySearchModel; + + get showSidenavToggle() { + return this.media.isMobile || this.media.isTablet; + } + + @action + onKeyPress(event: KeyboardEvent) { + if (event.key === 'Enter') { + taskFor(this.doSearch).perform(); + } + } + + @task + @waitFor + async doSearch() { + const { q, page, sort } = this; + const searchResult = await this.store.queryRecord('metadata-record-search', { + q, + page, + sort, + }); + this.propertySearch = searchResult.relatedPropertySearch; + } +} diff --git a/app/search/styles.scss b/app/search/styles.scss new file mode 100644 index 00000000000..d08096b4ca6 --- /dev/null +++ b/app/search/styles.scss @@ -0,0 +1,34 @@ +.search-page { + background-color: $color-bg-gray; +} + +.heading-wrapper { + text-align: center; + color: $color-text-white; + background-color: $osf-dark-blue-navbar; +} + +.heading-label { + font-size: 1.5em; + font-weight: 400; + padding: 40px; +} + +.search-input { + color: $color-text-black; + padding: 9px 5px; + font-size: 1.5em; + max-width: 700px; + min-width: 250px; + width: 50vw; +} + +.search-button { + position: relative; + right: 3px; + bottom: 4px; +} + +.sidenav-toggle { + float: left; +} diff --git a/app/search/template.hbs b/app/search/template.hbs index 0082a7f0f25..654a5d1df95 100644 --- a/app/search/template.hbs +++ b/app/search/template.hbs @@ -1,2 +1,51 @@ {{!-- template-lint-disable no-bare-strings --}} -New search placeholder + + +
+ + + +
+ + {{#if this.showSidenavToggle}} + + {{/if}} +
+ + {{!-- current filters --}} + {{!-- filter facets --}} + Left + + + Main + {{!-- placeholder if no search term (maybe) --}} + {{!-- object type filtering tabs --}} + {{!-- search cards --}} + {{!-- sort dropdown --}} + {{!-- paginator --}} + +
diff --git a/translations/en-us.yml b/translations/en-us.yml index f7c3ad22170..3a003adea1f 100644 --- a/translations/en-us.yml +++ b/translations/en-us.yml @@ -201,6 +201,12 @@ dashboard: title: 'Browse the latest research' description: 'Check out the latest preprints hosted on OSF covering a variety of research areas.' button: 'View preprints' + +search: + search_header: 'Search OSF' + textbox_placeholder: 'Search placeholder' + search_button_label: 'Search' + toggle_sidenav: 'Toggle search sidebar' new_project: header: 'Create new project' title_placeholder: 'Enter project title' From f0166c6c031f667e08a6d3863e33a6b92c8c91c5 Mon Sep 17 00:00:00 2001 From: futa-ikeda <51409893+futa-ikeda@users.noreply.github.com> Date: Mon, 15 May 2023 11:03:09 -0400 Subject: [PATCH 04/60] [ENG-4465] Left panel facets manager (#1858) - Ticket: [ENG-4465] [ENG-4466] - Feature flag: n/a ## Purpose - Add logic to search page controller to handle active filters and list of filterable properties - Add a component to handle fetching values in a filterable properties in the search page ## Summary of Changes - Add a `filter-facet` component - takes care of fetching filterable property values - `See more` modal --- app/models/metadata-record.ts | 36 +++ .../-components/filter-facet/component.ts | 83 +++++++ .../-components/filter-facet/styles.scss | 40 ++++ .../-components/filter-facet/template.hbs | 105 +++++++++ app/search/controller.ts | 86 +++++-- app/search/route.ts | 6 + app/search/styles.scss | 22 +- app/search/template.hbs | 82 +++++-- mirage/config.ts | 4 +- mirage/views/search.ts | 210 +++++++++++++++++- .../acceptance/search/search-filters-test.ts | 60 +++++ translations/en-us.yml | 23 +- 12 files changed, 706 insertions(+), 51 deletions(-) create mode 100644 app/search/-components/filter-facet/component.ts create mode 100644 app/search/-components/filter-facet/styles.scss create mode 100644 app/search/-components/filter-facet/template.hbs create mode 100644 tests/acceptance/search/search-filters-test.ts diff --git a/app/models/metadata-record.ts b/app/models/metadata-record.ts index e6480363a39..0094bd8ea7d 100644 --- a/app/models/metadata-record.ts +++ b/app/models/metadata-record.ts @@ -1,4 +1,6 @@ +import { inject as service } from '@ember/service'; import Model, { AsyncHasMany, attr, hasMany } from '@ember-data/model'; +import IntlService from 'ember-intl/services/intl'; export interface LanguageText { '@language': string; @@ -6,12 +8,46 @@ export interface LanguageText { } export default class MetadataRecordModel extends Model { + @service intl!: IntlService; + @attr('array') resourceType!: string[]; @attr('array') resourceIdentifier!: string[]; @attr('object') resourceMetadata!: any; @hasMany('metadata-record', { inverse: null }) relatedRecordSet!: AsyncHasMany & MetadataRecordModel[]; + + get label(): string { + const { resourceMetadata } = this; + const preferredLanguage = this.intl.locale; + const labels = resourceMetadata?.label; + if (labels) { + const languageAppropriateLabel = labels.filter((label: any) => label['@language'] === preferredLanguage); + // give the locale appropriate label if it exists, otherwise give the first label + if (languageAppropriateLabel.length > 0) { + return labels.filter((label: any) => label['@language'] === preferredLanguage)[0]['@value']; + } else if (labels.length > 0) { + return labels[0]['@value']; + } + } + return this.intl.t('search.metadata-record.no-label'); + } + + get title(): string { + const { resourceMetadata } = this; + const preferredLanguage = this.intl.locale; + const titles = resourceMetadata?.title; + if (titles) { + const languageAppropriateTitle = titles.filter((title: any) => title['@language'] === preferredLanguage); + // give the locale appropriate title if it exists, otherwise give the first title + if (languageAppropriateTitle.length > 0) { + return titles.filter((title: any) => title['@language'] === preferredLanguage)[0]['@value']; + } else if (titles.length > 0) { + return titles[0]['@value']; + } + } + return this.intl.t('search.metadata-record.no-title'); + } } declare module 'ember-data/types/registries/model' { diff --git a/app/search/-components/filter-facet/component.ts b/app/search/-components/filter-facet/component.ts new file mode 100644 index 00000000000..008553507b8 --- /dev/null +++ b/app/search/-components/filter-facet/component.ts @@ -0,0 +1,83 @@ +import Store from '@ember-data/store'; +import { action } from '@ember/object'; +import { inject as service } from '@ember/service'; +import { waitFor } from '@ember/test-waiters'; +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { task } from 'ember-concurrency'; +import { taskFor } from 'ember-concurrency-ts'; +import IntlService from 'ember-intl/services/intl'; + +import MetadataRecordModel from 'ember-osf-web/models/metadata-record'; +import SearchResultModel from 'ember-osf-web/models/search-result'; + +import { Filter } from '../../controller'; + +interface FilterFacetArgs { + recordSearchText: string; + recordSearchFilters: Filter[]; + propertyRecord: MetadataRecordModel; + propertySearch: SearchResultModel; + toggleFilter: (filter: Filter) => void; +} + +export default class FilterFacet extends Component { + @service store!: Store; + @service intl!: IntlService; + @service toast!: Toastr; + + @tracked page = 1; + @tracked sort = '-relevance'; + @tracked collapsed = true; + @tracked filterableValues: SearchResultModel[] = []; + @tracked seeMoreModalShown = false; + @tracked selectedProperty: SearchResultModel | null = null; + + get showSeeMoreButton() { + // TODO: make this actually check if there are more + return true; + } + + @action + toggleFacet() { + if (this.filterableValues.length === 0 && !taskFor(this.fetchFacetValues).lastComplete) { + taskFor(this.fetchFacetValues).perform(); + } + this.collapsed = !this.collapsed; + } + + @action + updateSelectedProperty(property: SearchResultModel) { + this.selectedProperty = property; + } + + @task + @waitFor + async applySelectedProperty() { + if (this.selectedProperty) { + const { toggleFilter, propertyRecord } = this.args; + const record = await this.selectedProperty.metadataRecord; + const filter = { + property: propertyRecord.get('label'), + value: record.title, + }; + toggleFilter(filter); + this.selectedProperty = null; + } + } + + @task + @waitFor + async fetchFacetValues() { + const { recordSearchText, recordSearchFilters } = this.args; + const { page, sort } = this; + const valueSearch = await this.store.queryRecord('metadata-value-search', { + recordSearchText, + recordSearchFilters, + page, + sort, + }); + const results = valueSearch.get('searchResultPage').toArray(); + this.filterableValues = results; + } +} diff --git a/app/search/-components/filter-facet/styles.scss b/app/search/-components/filter-facet/styles.scss new file mode 100644 index 00000000000..dcc89e22e45 --- /dev/null +++ b/app/search/-components/filter-facet/styles.scss @@ -0,0 +1,40 @@ +.facet-wrapper { + border-top: 1px solid $color-border-gray; + padding: 0.5rem 0; +} + +.facet-expand-button { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + + &:active { + box-shadow: none; + } +} + +.facet-list { + padding: 0; + margin: 0; + list-style: none; + + &.collapsed { + display: none; + } +} + +.facet-value { + margin: 10px 0; + display: flex; + justify-content: space-between; + + .facet-link, + .facet-count { + margin: 0 5px; + } +} + +.see-more-dialog { + min-width: 50vw; +} diff --git a/app/search/-components/filter-facet/template.hbs b/app/search/-components/filter-facet/template.hbs new file mode 100644 index 00000000000..260f5ec9e6b --- /dev/null +++ b/app/search/-components/filter-facet/template.hbs @@ -0,0 +1,105 @@ +
+ {{#let (unique-id @propertyRecord.label) as |facetElementId|}} + + {{#if this.fetchFacetValues.isRunning}} + + {{else if this.fetchFacetValues.isError}} + {{t 'search.filter-facet.facet-load-failed'}} + {{else}} +
    + {{#each this.filterableValues as |value|}} +
  • + + + {{value.recordResultCount}} + +
  • + {{/each}} + + {{#if this.showSeeMoreButton}} +
  • + +
  • + {{/if}} +
+ {{/if}} + + + {{@propertyRecord.label}} + + + {{t 'search.filter-facet.see-more-modal-text'}} + + {{property.metadataRecord.title}} + + + + + + + + {{/let}} +
diff --git a/app/search/controller.ts b/app/search/controller.ts index 8f2bb7d7a1f..14bb1d7fcef 100644 --- a/app/search/controller.ts +++ b/app/search/controller.ts @@ -1,46 +1,96 @@ import Store from '@ember-data/store'; +import { A } from '@ember/array'; import Controller from '@ember/controller'; import { action } from '@ember/object'; import { inject as service } from '@ember/service'; import { waitFor } from '@ember/test-waiters'; -import { task } from 'ember-concurrency'; -import { taskFor } from 'ember-concurrency-ts'; import { tracked } from '@glimmer/tracking'; +import { task, timeout } from 'ember-concurrency'; +import { taskFor } from 'ember-concurrency-ts'; import Media from 'ember-responsive'; import MetadataPropertySearchModel from 'ember-osf-web/models/metadata-property-search'; +export interface Filter { + property: string; + value: string; +} + +const searchDebounceTime = 100; export default class SearchController extends Controller { @service store!: Store; @service media!: Media; + @service toast!: Toastr; queryParams = ['q', 'page', 'sort']; - @tracked q?: string; - @tracked page?: number; - @tracked sort?: string; + @tracked q?: string = ''; + @tracked seachBoxText?: string = ''; + + @tracked page?: number = 1; + @tracked sort?: string = '-relevance'; + @tracked activeFilters = A([]); @tracked propertySearch?: MetadataPropertySearchModel; - get showSidenavToggle() { + get showSidePanelToggle() { return this.media.isMobile || this.media.isTablet; } + get filterableProperties() { + if (!this.propertySearch) { + return []; + } + return this.propertySearch.get('searchResultPage'); + } + @action - onKeyPress(event: KeyboardEvent) { - if (event.key === 'Enter') { - taskFor(this.doSearch).perform(); + toggleFilter(filter: Filter) { + const filterIndex = this.activeFilters.findIndex( + f => f.property === filter.property && f.value === filter.value, + ); + if (filterIndex > -1) { + this.activeFilters.removeAt(filterIndex); + } else { + this.activeFilters.pushObject(filter); } + taskFor(this.search).perform(); } - @task + @action + ingestQueryParams() { + const { q } = this; + if (q) { + this.seachBoxText = q; + } + } + + @task({ restartable: true }) + @waitFor + async doDebounceSearch() { + await timeout(searchDebounceTime); + this.q = this.seachBoxText; + taskFor(this.search).perform(); + } + + @task({ restartable: true, on: 'init' }) @waitFor - async doSearch() { - const { q, page, sort } = this; - const searchResult = await this.store.queryRecord('metadata-record-search', { - q, - page, - sort, - }); - this.propertySearch = searchResult.relatedPropertySearch; + async search() { + try { + const { q, page, sort, activeFilters } = this; + const filterQueryObject = activeFilters.reduce((acc, filter) => { + acc[filter.property] = filter.value; + return acc; + }, {} as { [key: string]: string }); + const searchResult = await this.store.queryRecord('metadata-record-search', { + q, + page, + sort, + filter: filterQueryObject, + }); + + this.propertySearch = searchResult.relatedPropertySearch; + } catch (e) { + this.toast.error(e); + } } } diff --git a/app/search/route.ts b/app/search/route.ts index dd27660b802..c1b6cd97ca8 100644 --- a/app/search/route.ts +++ b/app/search/route.ts @@ -1,4 +1,10 @@ import Route from '@ember/routing/route'; +import SearchController from './controller'; + export default class Search extends Route { + setupController(controller: SearchController, _model: any, _transition: any) { + super.setupController(controller, _model, _transition); + controller.ingestQueryParams(); + } } diff --git a/app/search/styles.scss b/app/search/styles.scss index d08096b4ca6..b0b1b98f406 100644 --- a/app/search/styles.scss +++ b/app/search/styles.scss @@ -9,11 +9,13 @@ } .heading-label { - font-size: 1.5em; - font-weight: 400; padding: 40px; } +.search-input-wrapper { + white-space: nowrap; +} + .search-input { color: $color-text-black; padding: 9px 5px; @@ -32,3 +34,19 @@ .sidenav-toggle { float: left; } + +.left-panel { + padding-left: 12px; + width: 300px; +} + +.active-filter-list { + padding-left: 0; + list-style: none; +} + +.active-filter-item { + display: flex; + justify-content: space-between; +} + diff --git a/app/search/template.hbs b/app/search/template.hbs index 654a5d1df95..9d5dfbe6081 100644 --- a/app/search/template.hbs +++ b/app/search/template.hbs @@ -1,33 +1,42 @@ {{!-- template-lint-disable no-bare-strings --}} +{{page-title (t 'search.page-title')}} -
- + + - +
- {{#if this.showSidenavToggle}} + {{#if this.showSidePanelToggle}} {{/if}}
- - {{!-- current filters --}} - {{!-- filter facets --}} - Left + + {{!-- Object type filter dropdown if we are in mobile --}} +

{{t 'search.left-panel.header'}}

+ {{#if this.activeFilters.length}} +
    + {{#each this.activeFilters as |filter|}} +
  • + + {{filter.property}} | + {{filter.value}} + + +
  • + {{/each}} +
+ {{/if}} + {{#each this.filterableProperties as |filterableProperty|}} + {{#let filterableProperty.metadataRecord as |propertyRecord|}} + + {{/let}} + {{else}} + {{t 'search.left-panel.no-filterable-properties'}} + {{/each}}
Main {{!-- placeholder if no search term (maybe) --}} - {{!-- object type filtering tabs --}} + {{!-- object type filtering tabs if desktop--}} {{!-- search cards --}} {{!-- sort dropdown --}} {{!-- paginator --}} diff --git a/mirage/config.ts b/mirage/config.ts index d1cfa53b3b1..ca11119b935 100644 --- a/mirage/config.ts +++ b/mirage/config.ts @@ -37,7 +37,7 @@ import { createNewSchemaResponse } from './views/schema-response'; import { createSchemaResponseAction } from './views/schema-response-action'; import { rootDetail } from './views/root'; import { shareSearch } from './views/share-search'; -import { recordSearch } from './views/search'; +import { recordSearch, valueSearch } from './views/search'; import { createToken } from './views/token'; import { createEmails, updateEmails } from './views/update-email'; import { @@ -63,7 +63,7 @@ export default function(this: Server) { // SHARE-powered search endpoints this.get('/metadata-record-searches', recordSearch); - // this.get('/metadata-value-searches', valueSearch); + this.get('/metadata-value-searches', valueSearch); // this.get('/metadata-records/:id', metadataRecordDetail); this.urlPrefix = apiUrl; diff --git a/mirage/views/search.ts b/mirage/views/search.ts index 3bc43082c90..e4a36bec5fe 100644 --- a/mirage/views/search.ts +++ b/mirage/views/search.ts @@ -1,4 +1,5 @@ import { Request, Schema } from 'ember-cli-mirage'; +import faker from 'faker'; export function recordSearch(_: Schema, __: Request) { // TODO: replace with a real metadata-record-search and use request to populate attrs @@ -208,7 +209,6 @@ export function recordSearch(_: Schema, __: Request) { specificType: 'foaf:Person', name: 'person person, prsn', }, - // ... }, }, links: { @@ -219,12 +219,10 @@ export function recordSearch(_: Schema, __: Request) { { type: 'metadata-record', id: 'def', - // ... }, { type: 'metadata-record', id: 'ghi', - // ... }, // Related properties search object { @@ -309,12 +307,50 @@ export function recordSearch(_: Schema, __: Request) { { type: 'search-result', id: 'propertyMatch2', - // ... + attributes: { + matchEvidence: [ + { + propertyPath: 'resourceType', + matchingIri: 'rdf:Property', + }, + ], + recordResultCount: 123, + }, + relationships: { + metadataRecord: { + data: { + type: 'metadata-record', + id: 'idForPropertyRecord2', + }, + links: { + related: 'https://share.osf.io/api/v2/metadata-record/idForPropertyRecord2', + }, + }, + }, }, { type: 'search-result', id: 'propertyMatch3', - // ... + attributes: { + matchEvidence: [ + { + propertyPath: 'resourceType', + matchingIri: 'rdf:Property', + }, + ], + recordResultCount: 33, + }, + relationships: { + metadataRecord: { + data: { + type: 'metadata-record', + id: 'idForPropertyRecord3', + }, + links: { + related: 'https://share.osf.io/api/v2/metadata-record/idForPropertyRecord3', + }, + }, + }, }, { type: 'metadata-record', @@ -341,7 +377,6 @@ export function recordSearch(_: Schema, __: Request) { '@language': 'en', }, ], - // ... }, }, links: { @@ -352,12 +387,171 @@ export function recordSearch(_: Schema, __: Request) { { type: 'metadata-record', id: 'idForPropertyRecord2', - // ... + attributes: { + resourceType: [ + 'rdf:Property', + ], + resourceIdentifier: [ + 'http://purl.org/dc/terms/published', + ], + resourceMetadata: { + '@id': 'http://purl.org/dc/terms/published', + '@type': 'rdf:Property', + label: [ + { + '@value': 'Date Published', + '@language': 'en', + }, + ], + description: [ + { + '@value': 'Some description about published date in this case', + '@language': 'en', + }, + ], + }, + }, + links: { + self: 'https://share.osf.io/api/v2/metadata-record/idForPropertyRecord2', + resource: 'http://purl.org/dc/terms/published', + }, }, { type: 'metadata-record', id: 'idForPropertyRecord3', - // ... + attributes: { + resourceType: [ + 'rdf:Property', + ], + resourceIdentifier: [ + 'http://purl.org/dc/terms/funder', + ], + resourceMetadata: { + '@id': 'http://purl.org/dc/terms/funder', + '@type': 'rdf:Property', + label: [ + { + '@value': 'Funder', + '@language': 'en', + }, + ], + description: [ + { + '@value': 'Some description about funder in this case', + '@language': 'en', + }, + ], + }, + }, + links: { + self: 'https://share.osf.io/api/v2/metadata-record/idForPropertyRecord2', + resource: 'http://purl.org/dc/terms/funder', + }, + }, + ], + }; +} + +export function valueSearch(_: Schema, __: Request) { + const property1Id = faker.random.uuid(); + const property2Id = faker.random.uuid(); + return { + data: { + type: 'metadata-value-search', + id: 'lmnop', + attributes: { + valueSearchText: 'Institute of Health', + valueSearchFilter: [ + { + propertyPath: 'resourceType', + filterType: 'eq', + filterValues: ['datacite:Funder'], + }, + ], + recordSearchText: 'influenza', + recordSearchFilter: [ + { + propertyPath: 'resourceType', + filterType: 'eq', + filterValues: ['datacite:Dataset'], + }, + ], + totalResultCount: 2, + }, + relationships: { + searchResultPage: { + data: [ + {type: 'search-result', id: property1Id}, + {type: 'search-result', id: property2Id}, + ], + links: { + next: '...', + last: '...', + }, + }, + relatedPropertySearch: { + data: {type: 'metadata-property-search', id: '12345'}, + }, + }, + }, + included: [ + { + type: 'search-result', + id: property1Id, + attributes: { + matchEvidence: [ + {propertyPath: 'title', matchingHighlight: 'National Institute of Health'}, + ], + recordResultCount: 2134, + }, + relationships: { + metadataRecord: { + data: {type: 'metadata-record', id: property1Id}, + links: {related: 'https://share.osf.example/metadata-record/abc'}, + }, + }, + }, + { + type: 'search-result', + id: property2Id, + attributes: { + matchEvidence: [ + {propertyPath: 'title', matchingHighlight: 'Virginia Institute of Health'}, + ], + recordResultCount: 2, + }, + relationships: { + metadataRecord: { + data: {type: 'metadata-record', id: property2Id}, + links: {related: 'https://share.osf.example/metadata-record/def'}, + }, + }, + }, + { + type: 'metadata-record', + id: property1Id, + attributes: { + resourceType: 'osf:Funder', + resourceIdentifier: 'http://dx.doi.org/10.10000/505000005050', + resourceMetadata: { + '@id': 'http://dx.doi.org/10.10000/505000005050', + '@type': 'datacite:Funder', + title: [{'@value': faker.lorem.words(3), '@language':'en'}], + }, + }, + }, + { + type: 'metadata-record', + id: property2Id, + attributes: { + resourceType: 'osf:Funder', + resourceIdentifier: 'https://doi.org/10.10000/100000001', + resourceMetadata: { + '@id': 'http://dx.doi.org/10.10000/100000001', + '@type': 'datacite:Funder', + title: [{'@value':faker.lorem.word(), '@language':'en'}], + }, + }, }, ], }; diff --git a/tests/acceptance/search/search-filters-test.ts b/tests/acceptance/search/search-filters-test.ts new file mode 100644 index 00000000000..eb8c160e7d7 --- /dev/null +++ b/tests/acceptance/search/search-filters-test.ts @@ -0,0 +1,60 @@ +import { click as untrackedClick, visit } from '@ember/test-helpers'; +import { setupMirage } from 'ember-cli-mirage/test-support'; +import { click, setupOSFApplicationTest } from 'ember-osf-web/tests/helpers'; +import { clickTrigger } from 'ember-power-select/test-support/helpers'; +import { module, test } from 'qunit'; + + +const moduleName = 'Acceptance | search | filters'; + +module(moduleName, hooks => { + setupOSFApplicationTest(hooks); + setupMirage(hooks); + + test('add and remove search filters', async assert => { + // Load search page + await visit('/search'); + // assert there are search filters after initial search + assert.dom('[data-test-filter-facet]').exists({ count: 3 }, 'Filterable properties shown after initial search'); + // assert that mobile only side panel toggle is not shown + assert.dom('[data-test-toggle-side-panel]').doesNotExist('Side panel toggle not shown in desktop view'); + // assert there are no active filters + assert.dom('[data-test-active-filter]').doesNotExist('No active filters shown initially'); + // expand a filterable property + await click('[data-test-filter-facet-toggle="License"]'); + // ensure there are filter options + assert.dom('[data-test-filter-facet-value]') + .exists({ count: 2 }, 'Filter options shown after expanding a filterable property'); + // click on a filter option + await click('[data-test-filter-facet-value] button'); + // assert there is one active filter + assert.dom('[data-test-active-filter]') + .exists({ count: 1 }, 'Active filter shown after clicking a filter option'); + // remove the active filter + await click('[data-test-remove-active-filter]'); + }); + + test('add a search filter using the see-more modal', async assert => { + // Load search page + await visit('/search'); + // assert there are no search filters + assert.dom('[data-test-filter-facet]').exists({ count: 3 }, 'Filterable properties shown after initial search'); + // click the first filterable property + await click('[data-test-filter-facet-toggle="License"]'); + // open the see-more modal + await click('[data-test-see-more-filterable-values]'); + assert.dom('[data-test-see-more-dialog-heading]').containsText('License', 'See more modal shown'); + assert.dom('[data-test-property-value-select]') + .containsText('Search for a filter to apply', 'Placeholder message shown in select'); + assert.dom('[data-test-see-more-dialog-apply-button]').isDisabled('Apply button disabled initially'); + // select a filter value + await clickTrigger(); + await untrackedClick('[data-option-index="0"]'); + assert.dom('[data-test-see-more-dialog-apply-button]') + .isNotDisabled('Apply button enabled after selecting a filter'); + // apply the filter + await click('[data-test-see-more-dialog-apply-button]'); + assert.dom('[data-test-see-more-dialog-heading]').doesNotExist('See more modal closed after applying filter'); + assert.dom('[data-test-active-filter]').exists({ count: 1 }, 'Active filter shown after applying filter'); + }); +}); diff --git a/translations/en-us.yml b/translations/en-us.yml index 3a003adea1f..5044295eb46 100644 --- a/translations/en-us.yml +++ b/translations/en-us.yml @@ -37,6 +37,7 @@ general: cancel: Cancel add: Add ok: OK + apply: Apply revisions: Revisions md5: MD5 date: Date @@ -203,10 +204,24 @@ dashboard: button: 'View preprints' search: - search_header: 'Search OSF' - textbox_placeholder: 'Search placeholder' - search_button_label: 'Search' - toggle_sidenav: 'Toggle search sidebar' + page-title: 'Search' + metadata-record: + no-label: 'No label found' + no-title: 'No title found' + search-header: 'Search OSF' + textbox-placeholder: 'Search placeholder' + search-button-label: 'Search' + toggle-sidenav: 'Toggle search sidebar' + left-panel: + header: Refine + no-filterable-properties: 'No properties' + active-filters: + remove-filter: 'Remove filter {property} {value}' + filter-facet: + facet-load-failed: 'Failed to load facet values' + see-more: 'See more' + see-more-modal-text: 'Please select a filter to apply to your search.' + see-more-modal-placeholder: 'Search for a filter to apply' new_project: header: 'Create new project' title_placeholder: 'Enter project title' From b503bddfe77358daf5fda6574ea32ce7b9359c46 Mon Sep 17 00:00:00 2001 From: futa-ikeda <51409893+futa-ikeda@users.noreply.github.com> Date: Tue, 16 May 2023 12:12:35 -0400 Subject: [PATCH 05/60] Add route analytics metadata (#1865) --- app/search/route.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/search/route.ts b/app/search/route.ts index c1b6cd97ca8..75b0900985d 100644 --- a/app/search/route.ts +++ b/app/search/route.ts @@ -3,6 +3,14 @@ import Route from '@ember/routing/route'; import SearchController from './controller'; export default class Search extends Route { + buildRouteInfoMetadata() { + return { + osfMetrics: { + isSearch: true, + }, + }; + } + setupController(controller: SearchController, _model: any, _transition: any) { super.setupController(controller, _model, _transition); controller.ingestQueryParams(); From 2631945388968fa389d76872fdf5b5ac2bcbe563 Mon Sep 17 00:00:00 2001 From: futa-ikeda <51409893+futa-ikeda@users.noreply.github.com> Date: Tue, 23 May 2023 14:17:53 -0400 Subject: [PATCH 06/60] [ENG-4469] Add object filter and sort dropdown to search (#1864) - Ticket: [ENG-4469] - Feature flag: n/a ## Purpose - Add object type filter and sort dropdown to search page ## Summary of Changes - Add tabs to filter by object type (All, Projects, Registrations, Preprints, Files, Users) - Add dropdown to sort results by Relevance, Date modified/created ascending and descending - Change model names to reflect more library-analogy based names - Change how metadata properties are fetched from SHARE models --- ...-record-search.ts => index-card-search.ts} | 4 +- .../{metadata-record.ts => index-card.ts} | 4 +- ...lue-search.ts => index-property-search.ts} | 4 +- ...operty-search.ts => index-value-search.ts} | 4 +- app/helpers/get-localized-property.ts | 40 ++++ ...-record-search.ts => index-card-search.ts} | 14 +- app/models/index-card.ts | 25 +++ ...rty-search.ts => index-property-search.ts} | 10 +- ...-value-search.ts => index-value-search.ts} | 16 +- app/models/metadata-record.ts | 57 ----- app/models/search-result.ts | 6 +- .../-components/filter-facet/component.ts | 28 +-- .../-components/filter-facet/template.hbs | 198 +++++++++--------- app/search/controller.ts | 73 ++++++- app/search/styles.scss | 38 +++- app/search/template.hbs | 81 +++++-- ...-record-search.ts => index-card-search.ts} | 4 +- .../{metadata-record.ts => index-card.ts} | 4 +- ...lue-search.ts => index-property-search.ts} | 4 +- ...operty-search.ts => index-value-search.ts} | 4 +- mirage/config.ts | 8 +- mirage/views/search.ts | 154 +++++++++----- .../acceptance/search/search-filters-test.ts | 2 +- .../search/search-query-params-test.ts | 84 ++++++++ translations/en-us.yml | 20 +- 25 files changed, 602 insertions(+), 284 deletions(-) rename app/adapters/{metadata-record-search.ts => index-card-search.ts} (57%) rename app/adapters/{metadata-record.ts => index-card.ts} (61%) rename app/adapters/{metadata-value-search.ts => index-property-search.ts} (60%) rename app/adapters/{metadata-property-search.ts => index-value-search.ts} (56%) create mode 100644 app/helpers/get-localized-property.ts rename app/models/{metadata-record-search.ts => index-card-search.ts} (55%) create mode 100644 app/models/index-card.ts rename app/models/{metadata-property-search.ts => index-property-search.ts} (65%) rename app/models/{metadata-value-search.ts => index-value-search.ts} (52%) delete mode 100644 app/models/metadata-record.ts rename app/serializers/{metadata-record-search.ts => index-card-search.ts} (57%) rename app/serializers/{metadata-record.ts => index-card.ts} (61%) rename app/serializers/{metadata-value-search.ts => index-property-search.ts} (62%) rename app/serializers/{metadata-property-search.ts => index-value-search.ts} (56%) create mode 100644 tests/acceptance/search/search-query-params-test.ts diff --git a/app/adapters/metadata-record-search.ts b/app/adapters/index-card-search.ts similarity index 57% rename from app/adapters/metadata-record-search.ts rename to app/adapters/index-card-search.ts index 6049cfaaf99..8a5bb6ff082 100644 --- a/app/adapters/metadata-record-search.ts +++ b/app/adapters/index-card-search.ts @@ -1,9 +1,9 @@ import ShareAdapter from './share-adapter'; -export default class MetadataRecordSearchAdapter extends ShareAdapter { +export default class IndexCardSearchAdapter extends ShareAdapter { } declare module 'ember-data/types/registries/adapter' { export default interface AdapterRegistry { - 'metadata-record-search': MetadataRecordSearchAdapter; + 'index-card-search': IndexCardSearchAdapter; } // eslint-disable-line semi } diff --git a/app/adapters/metadata-record.ts b/app/adapters/index-card.ts similarity index 61% rename from app/adapters/metadata-record.ts rename to app/adapters/index-card.ts index dfbf851870e..cd5e49cb14e 100644 --- a/app/adapters/metadata-record.ts +++ b/app/adapters/index-card.ts @@ -1,10 +1,10 @@ import ShareAdapter from './share-adapter'; -export default class MetadataRecordAdapter extends ShareAdapter { +export default class IndexCardAdapter extends ShareAdapter { } declare module 'ember-data/types/registries/adapter' { export default interface AdapterRegistry { - 'metadata-record': MetadataRecordAdapter; + 'index-card': IndexCardAdapter; } // eslint-disable-line semi } diff --git a/app/adapters/metadata-value-search.ts b/app/adapters/index-property-search.ts similarity index 60% rename from app/adapters/metadata-value-search.ts rename to app/adapters/index-property-search.ts index e1e021292df..3bec7157b32 100644 --- a/app/adapters/metadata-value-search.ts +++ b/app/adapters/index-property-search.ts @@ -1,10 +1,10 @@ import ShareAdapter from './share-adapter'; -export default class MetadataValueSearchAdapter extends ShareAdapter { +export default class IndexPropertySearchAdapter extends ShareAdapter { } declare module 'ember-data/types/registries/adapter' { export default interface AdapterRegistry { - 'metadata-value-search': MetadataValueSearchAdapter; + 'index-property-search': IndexPropertySearchAdapter; } // eslint-disable-line semi } diff --git a/app/adapters/metadata-property-search.ts b/app/adapters/index-value-search.ts similarity index 56% rename from app/adapters/metadata-property-search.ts rename to app/adapters/index-value-search.ts index a1b399e109d..6545fd9d1ae 100644 --- a/app/adapters/metadata-property-search.ts +++ b/app/adapters/index-value-search.ts @@ -1,10 +1,10 @@ import ShareAdapter from './share-adapter'; -export default class MetadataPropertySearchAdapter extends ShareAdapter { +export default class IndexValueSearchAdapter extends ShareAdapter { } declare module 'ember-data/types/registries/adapter' { export default interface AdapterRegistry { - 'metadata-property-search': MetadataPropertySearchAdapter; + 'index-value-search': IndexValueSearchAdapter; } // eslint-disable-line semi } diff --git a/app/helpers/get-localized-property.ts b/app/helpers/get-localized-property.ts new file mode 100644 index 00000000000..848a9f4607a --- /dev/null +++ b/app/helpers/get-localized-property.ts @@ -0,0 +1,40 @@ +import Helper from '@ember/component/helper'; +import { inject as service } from '@ember/service'; +import IntlService from 'ember-intl/services/intl'; + +import { LanguageText } from 'ember-osf-web/models/index-card'; + +/** + * This helper is used to get a locale-appropriate string for a property from a metadata hash. + * It is used to fetch metadata fields from a index-card's resourceMetadata attribute, but can be used for any + * hash that contains an array of LangaugeText objects. + * If the property is not found, the first value in the array is returned, or if the property is found, + * but there is no locale-appropriate value, the first value in the array is returned. + * + * @example + * ```handlebars + * {{get-localized-property indexCard.resourceMetadata 'title'}} + * ``` + * where `indexCard` is an index-card model instance. + * @class get-localized-property + * @param {Object} metadataHash The metadata hash to search for the property + * @param {String} propertyName The name of the property to search for + * @return {String} The locale-appropriate string or the first value in the array or 'Not provided' message + */ +export default class GetLocalizedPropertyHelper extends Helper { + @service intl!: IntlService; + + compute([metadataHash, propertyName]: [Record, string]): string { + const locale = this.intl.locale; + const valueOptions = metadataHash?.[propertyName]; + if (!metadataHash || !valueOptions || valueOptions.length === 0) { + return this.intl.t('helpers.get-localized-property.not-provided'); + } + + const index = valueOptions.findIndex((valueOption: LanguageText) => valueOption['@language'] === locale); + if (index === -1) { + return valueOptions[0]['@value']; + } + return valueOptions[index]['@value']; + } +} diff --git a/app/models/metadata-record-search.ts b/app/models/index-card-search.ts similarity index 55% rename from app/models/metadata-record-search.ts rename to app/models/index-card-search.ts index 199301e0461..65bfa3b9e61 100644 --- a/app/models/metadata-record-search.ts +++ b/app/models/index-card-search.ts @@ -1,6 +1,6 @@ import Model, { AsyncBelongsTo, AsyncHasMany, attr, belongsTo, hasMany } from '@ember-data/model'; -import MetadataPropertySearchModel from './metadata-property-search'; +import IndexPropertySearchModel from './index-property-search'; import SearchResultModel from './search-result'; export interface SearchFilter { @@ -9,20 +9,20 @@ export interface SearchFilter { filterType?: string; } -export default class MetadataRecordSearchModel extends Model { - @attr('string') recordSearchText!: string; - @attr('array') recordSearchFilters!: SearchFilter[]; +export default class IndexCardSearchModel extends Model { + @attr('string') cardSearchText!: string; + @attr('array') cardSearchFilters!: SearchFilter[]; @attr('number') totalResultCount!: number; @hasMany('search-result', { inverse: null }) searchResultPage!: AsyncHasMany & SearchResultModel[]; - @belongsTo('metadata-property-search', { inverse: null }) - relatedPropertySearch!: AsyncBelongsTo & MetadataPropertySearchModel; + @belongsTo('index-property-search', { inverse: null }) + relatedPropertySearch!: AsyncBelongsTo & IndexPropertySearchModel; } declare module 'ember-data/types/registries/model' { export default interface ModelRegistry { - 'metadata-record-search': MetadataRecordSearchModel; + 'index-card-search': IndexCardSearchModel; } // eslint-disable-line semi } diff --git a/app/models/index-card.ts b/app/models/index-card.ts new file mode 100644 index 00000000000..8881f63dc89 --- /dev/null +++ b/app/models/index-card.ts @@ -0,0 +1,25 @@ +import { inject as service } from '@ember/service'; +import Model, { AsyncHasMany, attr, hasMany } from '@ember-data/model'; +import IntlService from 'ember-intl/services/intl'; + +export interface LanguageText { + '@language': string; + '@value': string; +} + +export default class IndexCardModel extends Model { + @service intl!: IntlService; + + @attr('array') resourceType!: string[]; + @attr('array') resourceIdentifier!: string[]; + @attr('object') resourceMetadata!: any; + + @hasMany('index-card', { inverse: null }) + relatedRecordSet!: AsyncHasMany & IndexCardModel[]; +} + +declare module 'ember-data/types/registries/model' { + export default interface ModelRegistry { + 'index-card': IndexCardModel; + } // eslint-disable-line semi +} diff --git a/app/models/metadata-property-search.ts b/app/models/index-property-search.ts similarity index 65% rename from app/models/metadata-property-search.ts rename to app/models/index-property-search.ts index 744ced694ba..c87fe60feb6 100644 --- a/app/models/metadata-property-search.ts +++ b/app/models/index-property-search.ts @@ -1,13 +1,13 @@ import Model, { AsyncHasMany, attr, hasMany } from '@ember-data/model'; -import { SearchFilter } from './metadata-record-search'; +import { SearchFilter } from './index-card-search'; import SearchResultModel from './search-result'; -export default class MetadataPropertySearchModel extends Model { +export default class IndexPropertySearchModel extends Model { @attr('string') propertySearchText!: string; @attr('array') propertySearchFilter!: SearchFilter[]; - @attr('string') recordSearchText!: string; - @attr('array') recordSearchFilter!: SearchFilter[]; + @attr('string') cardSearchText!: string; + @attr('array') cardSearchFilter!: SearchFilter[]; @attr('number') totalResultCount!: number; @hasMany('search-result', { inverse: null }) @@ -16,6 +16,6 @@ export default class MetadataPropertySearchModel extends Model { declare module 'ember-data/types/registries/model' { export default interface ModelRegistry { - 'metadata-property-search': MetadataPropertySearchModel; + 'index-property-search': IndexPropertySearchModel; } // eslint-disable-line semi } diff --git a/app/models/metadata-value-search.ts b/app/models/index-value-search.ts similarity index 52% rename from app/models/metadata-value-search.ts rename to app/models/index-value-search.ts index cc97695694a..27f5c41dde7 100644 --- a/app/models/metadata-value-search.ts +++ b/app/models/index-value-search.ts @@ -1,25 +1,25 @@ import Model, { AsyncHasMany, AsyncBelongsTo, attr, belongsTo, hasMany } from '@ember-data/model'; -import MetadataPropertySearchModel from './metadata-property-search'; -import { SearchFilter } from './metadata-record-search'; +import IndexPropertySearchModel from './index-property-search'; +import { SearchFilter } from './index-card-search'; import SearchResultModel from './search-result'; -export default class MetadataValueSearchModel extends Model { +export default class IndexValueSearchModel extends Model { @attr('string') valueSearchText!: string; @attr('array') valueSearchFilter!: SearchFilter[]; - @attr('string') recordSearchText!: string; - @attr('array') recordSearchFilter!: SearchFilter[]; + @attr('string') cardSearchText!: string; + @attr('array') cardSearchFilter!: SearchFilter[]; @attr('number') totalResultCount!: number; @hasMany('search-result', { inverse: null }) searchResultPage!: AsyncHasMany & SearchResultModel[]; - @belongsTo('metadata-property-search', { inverse: null }) - relatedPropertySearch!: AsyncBelongsTo & MetadataPropertySearchModel; + @belongsTo('index-property-search', { inverse: null }) + relatedPropertySearch!: AsyncBelongsTo & IndexPropertySearchModel; } declare module 'ember-data/types/registries/model' { export default interface ModelRegistry { - 'metadata-value-search': MetadataValueSearchModel; + 'index-value-search': IndexValueSearchModel; } // eslint-disable-line semi } diff --git a/app/models/metadata-record.ts b/app/models/metadata-record.ts deleted file mode 100644 index 0094bd8ea7d..00000000000 --- a/app/models/metadata-record.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { inject as service } from '@ember/service'; -import Model, { AsyncHasMany, attr, hasMany } from '@ember-data/model'; -import IntlService from 'ember-intl/services/intl'; - -export interface LanguageText { - '@language': string; - '@value': string; -} - -export default class MetadataRecordModel extends Model { - @service intl!: IntlService; - - @attr('array') resourceType!: string[]; - @attr('array') resourceIdentifier!: string[]; - @attr('object') resourceMetadata!: any; - - @hasMany('metadata-record', { inverse: null }) - relatedRecordSet!: AsyncHasMany & MetadataRecordModel[]; - - get label(): string { - const { resourceMetadata } = this; - const preferredLanguage = this.intl.locale; - const labels = resourceMetadata?.label; - if (labels) { - const languageAppropriateLabel = labels.filter((label: any) => label['@language'] === preferredLanguage); - // give the locale appropriate label if it exists, otherwise give the first label - if (languageAppropriateLabel.length > 0) { - return labels.filter((label: any) => label['@language'] === preferredLanguage)[0]['@value']; - } else if (labels.length > 0) { - return labels[0]['@value']; - } - } - return this.intl.t('search.metadata-record.no-label'); - } - - get title(): string { - const { resourceMetadata } = this; - const preferredLanguage = this.intl.locale; - const titles = resourceMetadata?.title; - if (titles) { - const languageAppropriateTitle = titles.filter((title: any) => title['@language'] === preferredLanguage); - // give the locale appropriate title if it exists, otherwise give the first title - if (languageAppropriateTitle.length > 0) { - return titles.filter((title: any) => title['@language'] === preferredLanguage)[0]['@value']; - } else if (titles.length > 0) { - return titles[0]['@value']; - } - } - return this.intl.t('search.metadata-record.no-title'); - } -} - -declare module 'ember-data/types/registries/model' { - export default interface ModelRegistry { - 'metadata-record': MetadataRecordModel; - } // eslint-disable-line semi -} diff --git a/app/models/search-result.ts b/app/models/search-result.ts index af9ddc859c5..e97e9001dcb 100644 --- a/app/models/search-result.ts +++ b/app/models/search-result.ts @@ -1,6 +1,6 @@ import Model, { AsyncBelongsTo, attr, belongsTo } from '@ember-data/model'; -import MetadataRecordModel from './metadata-record'; +import IndexCardModel from './index-card'; export interface IriMatchEvidence { '@type': 'IriMatchEvidence'; @@ -18,8 +18,8 @@ export default class SearchResultModel extends Model { @attr('array') matchEvidence!: Array; @attr('number') recordResultCount!: number; - @belongsTo('metadata-record', { inverse: null }) - metadataRecord!: AsyncBelongsTo | MetadataRecordModel; + @belongsTo('index-card', { inverse: null }) + indexCard!: AsyncBelongsTo | IndexCardModel; } declare module 'ember-data/types/registries/model' { diff --git a/app/search/-components/filter-facet/component.ts b/app/search/-components/filter-facet/component.ts index 008553507b8..ce903a676df 100644 --- a/app/search/-components/filter-facet/component.ts +++ b/app/search/-components/filter-facet/component.ts @@ -1,4 +1,5 @@ import Store from '@ember-data/store'; +import { getOwner } from '@ember/application'; import { action } from '@ember/object'; import { inject as service } from '@ember/service'; import { waitFor } from '@ember/test-waiters'; @@ -7,16 +8,17 @@ import { tracked } from '@glimmer/tracking'; import { task } from 'ember-concurrency'; import { taskFor } from 'ember-concurrency-ts'; import IntlService from 'ember-intl/services/intl'; +import GetLocalizedPropertyHelper from 'ember-osf-web/helpers/get-localized-property'; -import MetadataRecordModel from 'ember-osf-web/models/metadata-record'; +import IndexCardModel from 'ember-osf-web/models/index-card'; import SearchResultModel from 'ember-osf-web/models/search-result'; import { Filter } from '../../controller'; interface FilterFacetArgs { - recordSearchText: string; - recordSearchFilters: Filter[]; - propertyRecord: MetadataRecordModel; + cardSearchText: string; + cardSearchFilters: Filter[]; + propertyCard: IndexCardModel; propertySearch: SearchResultModel; toggleFilter: (filter: Filter) => void; } @@ -33,6 +35,8 @@ export default class FilterFacet extends Component { @tracked seeMoreModalShown = false; @tracked selectedProperty: SearchResultModel | null = null; + getLocalizedString = new GetLocalizedPropertyHelper(getOwner(this)); + get showSeeMoreButton() { // TODO: make this actually check if there are more return true; @@ -55,11 +59,11 @@ export default class FilterFacet extends Component { @waitFor async applySelectedProperty() { if (this.selectedProperty) { - const { toggleFilter, propertyRecord } = this.args; - const record = await this.selectedProperty.metadataRecord; + const { toggleFilter, propertyCard } = this.args; + const card = await this.selectedProperty.indexCard; const filter = { - property: propertyRecord.get('label'), - value: record.title, + property: this.getLocalizedString.compute([propertyCard.get('resourceMetadata'), 'label']), + value: this.getLocalizedString.compute([card.resourceMetadata, 'title']), }; toggleFilter(filter); this.selectedProperty = null; @@ -69,11 +73,11 @@ export default class FilterFacet extends Component { @task @waitFor async fetchFacetValues() { - const { recordSearchText, recordSearchFilters } = this.args; + const { cardSearchText, cardSearchFilters } = this.args; const { page, sort } = this; - const valueSearch = await this.store.queryRecord('metadata-value-search', { - recordSearchText, - recordSearchFilters, + const valueSearch = await this.store.queryRecord('index-value-search', { + cardSearchText, + cardSearchFilters, page, sort, }); diff --git a/app/search/-components/filter-facet/template.hbs b/app/search/-components/filter-facet/template.hbs index 260f5ec9e6b..1c1862232c1 100644 --- a/app/search/-components/filter-facet/template.hbs +++ b/app/search/-components/filter-facet/template.hbs @@ -2,104 +2,108 @@ data-test-filter-facet local-class='facet-wrapper' > - {{#let (unique-id @propertyRecord.label) as |facetElementId|}} - - {{#if this.fetchFacetValues.isRunning}} - - {{else if this.fetchFacetValues.isError}} - {{t 'search.filter-facet.facet-load-failed'}} - {{else}} -
    - {{#each this.filterableValues as |value|}} -
  • - - - {{value.recordResultCount}} - -
  • - {{/each}} - - {{#if this.showSeeMoreButton}} -
  • - -
  • - {{/if}} -
- {{/if}} - - - {{@propertyRecord.label}} - - - {{t 'search.filter-facet.see-more-modal-text'}} - - {{property.metadataRecord.title}} - - - - + {{#if this.fetchFacetValues.isRunning}} + + {{else if this.fetchFacetValues.isError}} + {{t 'search.filter-facet.facet-load-failed'}} + {{else}} +
    - {{t 'general.cancel'}} - - - - + {{#each this.filterableValues as |value|}} + {{#let (get-localized-property value.indexCard.resourceMetadata 'title') as |valueTitle|}} +
  • + + + {{value.recordResultCount}} + +
  • + {{/let}} + {{/each}} + + {{#if this.showSeeMoreButton}} +
  • + +
  • + {{/if}} +
+ {{/if}} + + + {{propertyLabel}} + + + {{t 'search.filter-facet.see-more-modal-text'}} + + {{get-localized-property property.indexCard.resourceMetadata 'title'}} + + + + + + + + {{/let}} {{/let}} diff --git a/app/search/controller.ts b/app/search/controller.ts index 14bb1d7fcef..8a8de6e3ec2 100644 --- a/app/search/controller.ts +++ b/app/search/controller.ts @@ -7,30 +7,87 @@ import { waitFor } from '@ember/test-waiters'; import { tracked } from '@glimmer/tracking'; import { task, timeout } from 'ember-concurrency'; import { taskFor } from 'ember-concurrency-ts'; +import Intl from 'ember-intl/services/intl'; import Media from 'ember-responsive'; -import MetadataPropertySearchModel from 'ember-osf-web/models/metadata-property-search'; +import IndexPropertySearchModel from 'ember-osf-web/models/index-property-search'; +import SearchResultModel from 'ember-osf-web/models/search-result'; export interface Filter { property: string; value: string; } +interface ResourceTypeOption { + display: string; + value: string; +} + +interface SortOption { + display: string; + value: string; +} + const searchDebounceTime = 100; export default class SearchController extends Controller { + @service intl!: Intl; @service store!: Store; @service media!: Media; @service toast!: Toastr; - queryParams = ['q', 'page', 'sort']; + queryParams = ['q', 'page', 'sort', 'resourceType']; @tracked q?: string = ''; @tracked seachBoxText?: string = ''; @tracked page?: number = 1; - @tracked sort?: string = '-relevance'; + + // Resource type + resourceTypeOptions: ResourceTypeOption[] = [ + { display: this.intl.t('search.resource-type.all'), value: 'All' }, + { display: this.intl.t('search.resource-type.projects'), value: 'Projects' }, + { display: this.intl.t('search.resource-type.registrations'), value: 'Registrations' }, + { display: this.intl.t('search.resource-type.preprints'), value: 'Preprints' }, + { display: this.intl.t('search.resource-type.files'), value: 'Files' }, + { display: this.intl.t('search.resource-type.users'), value: 'Users' }, + ]; + + @tracked resourceType = this.resourceTypeOptions[0].value; + + get selectedResourceTypeOption() { + return this.resourceTypeOptions.find(option => option.value === this.resourceType); + } + + @action + updateResourceType(resourceTypeOption: ResourceTypeOption) { + this.resourceType = resourceTypeOption.value; + taskFor(this.search).perform(); + } + + // Sort + sortOptions: SortOption[] = [ + { display: this.intl.t('search.sort.relevance'), value: '-relevance' }, + { display: this.intl.t('search.sort.created-date-descending'), value: '-date_created' }, + { display: this.intl.t('search.sort.created-date-ascending'), value: 'date_created' }, + { display: this.intl.t('search.sort.modified-date-descending'), value: '-date_modified' }, + { display: this.intl.t('search.sort.modified-date-ascending'), value: 'date_modified' }, + ]; + + @tracked sort: string = this.sortOptions[0].value; + + get selectedSortOption() { + return this.sortOptions.find(option => option.value === this.sort);// || this.sortOptions[0]; + } + + @action + updateSort(sortOption: SortOption) { + this.sort = sortOption.value; + taskFor(this.search).perform(); + } + @tracked activeFilters = A([]); - @tracked propertySearch?: MetadataPropertySearchModel; + @tracked searchResults?: SearchResultModel[]; + @tracked propertySearch?: IndexPropertySearchModel; get showSidePanelToggle() { return this.media.isMobile || this.media.isTablet; @@ -76,19 +133,21 @@ export default class SearchController extends Controller { @waitFor async search() { try { - const { q, page, sort, activeFilters } = this; + const { q, page, sort, activeFilters, resourceType } = this; const filterQueryObject = activeFilters.reduce((acc, filter) => { acc[filter.property] = filter.value; return acc; }, {} as { [key: string]: string }); - const searchResult = await this.store.queryRecord('metadata-record-search', { + filterQueryObject['resourceType'] = resourceType; + const searchResult = await this.store.queryRecord('index-card-search', { q, page, sort, filter: filterQueryObject, }); - this.propertySearch = searchResult.relatedPropertySearch; + this.propertySearch = await searchResult.relatedPropertySearch; + this.searchResults = searchResult.searchResultPage.toArray(); } catch (e) { this.toast.error(e); } diff --git a/app/search/styles.scss b/app/search/styles.scss index b0b1b98f406..07b6ae675ce 100644 --- a/app/search/styles.scss +++ b/app/search/styles.scss @@ -31,6 +31,43 @@ bottom: 4px; } +.topbar { + margin-left: 20px; + display: flex; + border-bottom: 1px solid $color-text-black; +} + +.object-type-nav { + ul { + padding-left: 0; + margin-bottom: 0; + list-style: none; + } + + li { + display: inline-flex; + margin-right: 10px; + } +} + +.object-type-filter-link { + padding: 10px 15px; + font-size: 1.3em; + + &:global(.active), + &:hover { + text-decoration: none; + background-color: $color-bg-gray; + color: $color-text-black; + border-bottom: 2px solid $color-text-black; + } +} + +.sort-dropdown { + width: 170px; + margin-top: 10px; +} + .sidenav-toggle { float: left; } @@ -49,4 +86,3 @@ display: flex; justify-content: space-between; } - diff --git a/app/search/template.hbs b/app/search/template.hbs index 9d5dfbe6081..3ed23334418 100644 --- a/app/search/template.hbs +++ b/app/search/template.hbs @@ -1,6 +1,6 @@ {{!-- template-lint-disable no-bare-strings --}} {{page-title (t 'search.page-title')}} - @@ -31,7 +31,6 @@ as |layout|> - {{#if this.showSidePanelToggle}} - - {{#if this.showSidePanelToggle}} - - {{/if}} - - -

{{t 'search.left-panel.header'}}

- {{#if this.showSidePanelToggle}} -
- -
-
- -
- {{/if}} - {{#if this.activeFilters.length}} -
    - {{#each this.activeFilters as |filter|}} -
  • - - {{filter.property}}: - {{filter.value}} - - -
  • - {{/each}} -
- {{/if}} - {{#each this.filterableProperties as |filterableProperty|}} - {{#let filterableProperty.indexCard as |propertyCard|}} - - {{/let}} - {{else}} - {{t 'search.left-panel.no-filterable-properties'}} - {{/each}} -
- - {{!-- paginator --}} - {{#unless this.showSidePanelToggle}} -
- -
- - {{sortOption.display}} - -
-
- {{/unless}} -
-
+ + diff --git a/lib/osf-components/addon/components/search-page/component.ts b/lib/osf-components/addon/components/search-page/component.ts new file mode 100644 index 00000000000..8e8c7f4cb5b --- /dev/null +++ b/lib/osf-components/addon/components/search-page/component.ts @@ -0,0 +1,159 @@ +import { tracked } from '@glimmer/tracking'; +import Component from '@glimmer/component'; +import { inject as service } from '@ember/service'; +import IndexCardModel from 'ember-osf-web/models/index-card'; +import { waitFor } from '@ember/test-waiters'; +import { taskFor } from 'ember-concurrency-ts'; +import { task, timeout } from 'ember-concurrency'; +import Intl from 'ember-intl/services/intl'; +import { A } from '@ember/array'; +import Store from '@ember-data/store'; +import { action } from '@ember/object'; + +import IndexPropertySearchModel from 'ember-osf-web/models/index-property-search'; +import SearchResultModel from 'ember-osf-web/models/search-result'; + +interface ResourceTypeOption { + display: string; + value: string; +} + +interface SortOption { + display: string; + value: string; +} + +export interface Filter { + property: string; + value: string; +} + +interface SearchArgs { + onSearch?: (obj: object) => void; + query?: string; + cardSearchText: string; + cardSearchFilters: Filter[]; + propertyCard: IndexCardModel; + propertySearch: SearchResultModel; + toggleFilter: (filter: Filter) => void; + sort: string; + resourceType: string; +} + +const searchDebounceTime = 100; + +export default class SearchPage extends Component { + @service intl!: Intl; + @service toast!: Toastr; + @service store!: Store; + + @tracked searchText?: string; + @tracked searchResults?: SearchResultModel[]; + @tracked propertySearch?: IndexPropertySearchModel; + @tracked page?: number = 1; + + constructor( owner: unknown, args: SearchArgs) { + super(owner, args); + this.searchText = this.args.query; + this.sort = this.args.sort; + this.resourceType = this.args.resourceType; + taskFor(this.search).perform(); + } + + get filterableProperties() { + if (!this.propertySearch) { + return []; + } + return this.propertySearch.get('searchResultPage'); + } + + get selectedResourceTypeOption() { + return this.resourceTypeOptions.find(option => option.value === this.resourceType); + } + + get selectedSortOption() { + return this.sortOptions.find(option => option.value === this.sort);// || this.sortOptions[0]; + } + + // Resource type + resourceTypeOptions: ResourceTypeOption[] = [ + { display: this.intl.t('search.resource-type.all'), value: 'All' }, + { display: this.intl.t('search.resource-type.projects'), value: 'Projects' }, + { display: this.intl.t('search.resource-type.registrations'), value: 'Registrations' }, + { display: this.intl.t('search.resource-type.preprints'), value: 'Preprints' }, + { display: this.intl.t('search.resource-type.files'), value: 'Files' }, + { display: this.intl.t('search.resource-type.users'), value: 'Users' }, + ]; + + // Sort + sortOptions: SortOption[] = [ + { display: this.intl.t('search.sort.relevance'), value: '-relevance' }, + { display: this.intl.t('search.sort.created-date-descending'), value: '-date_created' }, + { display: this.intl.t('search.sort.created-date-ascending'), value: 'date_created' }, + { display: this.intl.t('search.sort.modified-date-descending'), value: '-date_modified' }, + { display: this.intl.t('search.sort.modified-date-ascending'), value: 'date_modified' }, + ]; + + @tracked resourceType: string; + @tracked sort: string; + @tracked activeFilters = A([]); + + @task({ restartable: true }) + @waitFor + async search() { + try { + const q = this.searchText; + const { page, sort, activeFilters, resourceType } = this; + const filterQueryObject = activeFilters.reduce((acc, filter) => { + acc[filter.property] = filter.value; + return acc; + }, {} as { [key: string]: string }); + filterQueryObject['resourceType'] = resourceType; + const searchResult = await this.store.queryRecord('index-card-search', { + q, + page, + sort, + filter: filterQueryObject, + }); + this.propertySearch = await searchResult.relatedPropertySearch; + this.searchResults = searchResult.searchResultPage.toArray(); + if (this.args.onSearch) { + this.args.onSearch({q, sort, resourceType}); + } + } catch (e) { + this.toast.error(e); + } + } + + @task({ restartable: true }) + @waitFor + async doDebounceSearch() { + await timeout(searchDebounceTime); + taskFor(this.search).perform(); + } + + @action + toggleFilter(filter: Filter) { + const filterIndex = this.activeFilters.findIndex( + f => f.property === filter.property && f.value === filter.value, + ); + if (filterIndex > -1) { + this.activeFilters.removeAt(filterIndex); + } else { + this.activeFilters.pushObject(filter); + } + taskFor(this.search).perform(); + } + + @action + updateSort(sortOption: SortOption) { + this.sort = sortOption.value; + taskFor(this.search).perform(); + } + + @action + updateResourceType(resourceTypeOption: ResourceTypeOption) { + this.resourceType = resourceTypeOption.value; + taskFor(this.search).perform(); + } +} diff --git a/app/search/-components/filter-facet/component.ts b/lib/osf-components/addon/components/search-page/filter-facet/component.ts similarity index 98% rename from app/search/-components/filter-facet/component.ts rename to lib/osf-components/addon/components/search-page/filter-facet/component.ts index ce903a676df..b819c66e3bb 100644 --- a/app/search/-components/filter-facet/component.ts +++ b/lib/osf-components/addon/components/search-page/filter-facet/component.ts @@ -13,7 +13,7 @@ import GetLocalizedPropertyHelper from 'ember-osf-web/helpers/get-localized-prop import IndexCardModel from 'ember-osf-web/models/index-card'; import SearchResultModel from 'ember-osf-web/models/search-result'; -import { Filter } from '../../controller'; +import { Filter } from '../component'; interface FilterFacetArgs { cardSearchText: string; diff --git a/app/search/-components/filter-facet/styles.scss b/lib/osf-components/addon/components/search-page/filter-facet/styles.scss similarity index 100% rename from app/search/-components/filter-facet/styles.scss rename to lib/osf-components/addon/components/search-page/filter-facet/styles.scss diff --git a/app/search/-components/filter-facet/template.hbs b/lib/osf-components/addon/components/search-page/filter-facet/template.hbs similarity index 100% rename from app/search/-components/filter-facet/template.hbs rename to lib/osf-components/addon/components/search-page/filter-facet/template.hbs diff --git a/app/search/styles.scss b/lib/osf-components/addon/components/search-page/styles.scss similarity index 100% rename from app/search/styles.scss rename to lib/osf-components/addon/components/search-page/styles.scss diff --git a/lib/osf-components/addon/components/search-page/template.hbs b/lib/osf-components/addon/components/search-page/template.hbs new file mode 100644 index 00000000000..10cb80a31d7 --- /dev/null +++ b/lib/osf-components/addon/components/search-page/template.hbs @@ -0,0 +1,148 @@ + + + + + + + + {{#if @showSidePanelToggle}} + + {{/if}} + + +

{{t 'search.left-panel.header'}}

+ {{#if @showSidePanelToggle}} +
+ +
+
+ +
+ {{/if}} + {{#if this.activeFilters.length}} +
    + {{#each this.activeFilters as |filter|}} +
  • + + {{filter.property}}: + {{filter.value}} + + +
  • + {{/each}} +
+ {{/if}} + {{#each this.filterableProperties as |filterableProperty|}} + {{#let filterableProperty.indexCard as |propertyCard|}} + + {{/let}} + {{else}} + {{t 'search.left-panel.no-filterable-properties'}} + {{/each}} +
+ + {{!-- paginator --}} + {{#unless @showSidePanelToggle}} +
+ +
+ + {{sortOption.display}} + +
+
+ {{/unless}} +
+
diff --git a/lib/osf-components/app/components/search-page/component.js b/lib/osf-components/app/components/search-page/component.js new file mode 100644 index 00000000000..2e83fa9360e --- /dev/null +++ b/lib/osf-components/app/components/search-page/component.js @@ -0,0 +1 @@ +export { default } from 'osf-components/components/search-page/component'; diff --git a/lib/osf-components/app/components/search-page/filter-facet/component.js b/lib/osf-components/app/components/search-page/filter-facet/component.js new file mode 100644 index 00000000000..2aea79fdf23 --- /dev/null +++ b/lib/osf-components/app/components/search-page/filter-facet/component.js @@ -0,0 +1 @@ +export { default } from 'osf-components/components/search-page/filter-facet/component'; diff --git a/lib/osf-components/app/components/search-page/filter-facet/template.js b/lib/osf-components/app/components/search-page/filter-facet/template.js new file mode 100644 index 00000000000..43c36976ad4 --- /dev/null +++ b/lib/osf-components/app/components/search-page/filter-facet/template.js @@ -0,0 +1 @@ +export { default } from 'osf-components/components/search-page/filter-facet/template'; diff --git a/lib/osf-components/app/components/search-page/template.js b/lib/osf-components/app/components/search-page/template.js new file mode 100644 index 00000000000..ad34c5c2daf --- /dev/null +++ b/lib/osf-components/app/components/search-page/template.js @@ -0,0 +1 @@ +export { default } from 'osf-components/components/search-page/template'; From 0a68c2a56582a6ccc063aaa05baec2e9811e6689 Mon Sep 17 00:00:00 2001 From: Yuhuai Liu Date: Wed, 28 Jun 2023 10:40:58 -0400 Subject: [PATCH 10/60] [ENG-4570] Add institution search placeholder route (#1888) * add institution search placeholder route * modify route path * modify route path * change route name and path --- app/institutions/discover/controller.ts | 7 +++++++ app/institutions/discover/route.ts | 8 ++++++++ app/institutions/discover/template.hbs | 1 + app/router.ts | 1 + 4 files changed, 17 insertions(+) create mode 100644 app/institutions/discover/controller.ts create mode 100644 app/institutions/discover/route.ts create mode 100644 app/institutions/discover/template.hbs diff --git a/app/institutions/discover/controller.ts b/app/institutions/discover/controller.ts new file mode 100644 index 00000000000..d672a9de675 --- /dev/null +++ b/app/institutions/discover/controller.ts @@ -0,0 +1,7 @@ +import Controller from '@ember/controller'; +import { inject as service } from '@ember/service'; +import CurrentUser from 'ember-osf-web/services/current-user'; + +export default class InstitutionDiscoverController extends Controller { + @service currentUser!: CurrentUser; +} diff --git a/app/institutions/discover/route.ts b/app/institutions/discover/route.ts new file mode 100644 index 00000000000..de06d3125f4 --- /dev/null +++ b/app/institutions/discover/route.ts @@ -0,0 +1,8 @@ +import Route from '@ember/routing/route'; +import RouterService from '@ember/routing/router-service'; +import { inject as service } from '@ember/service'; + + +export default class InstitutionDiscoverRoute extends Route { + @service router!: RouterService; +} diff --git a/app/institutions/discover/template.hbs b/app/institutions/discover/template.hbs new file mode 100644 index 00000000000..01e9d8456cd --- /dev/null +++ b/app/institutions/discover/template.hbs @@ -0,0 +1 @@ +{{!-- placeholder route template --}} \ No newline at end of file diff --git a/app/router.ts b/app/router.ts index b0d9f5c8833..2e42a9a3759 100644 --- a/app/router.ts +++ b/app/router.ts @@ -22,6 +22,7 @@ Router.map(function() { this.route('goodbye'); this.route('search'); this.route('institutions', function() { + this.route('discover', { path: '/:institution_id' }); this.route('dashboard', { path: '/:institution_id/dashboard' }); }); this.route('register'); From 5917ee30d0faf7d995f371e564757f0bcbbb7950 Mon Sep 17 00:00:00 2001 From: Yuhuai Liu Date: Wed, 28 Jun 2023 10:41:06 -0400 Subject: [PATCH 11/60] add route for preprint discover page (#1890) --- app/preprints/discover/controller.ts | 8 ++++++++ app/preprints/discover/route.ts | 11 +++++++++++ app/preprints/discover/template.hbs | 1 + app/router.ts | 3 +++ 4 files changed, 23 insertions(+) create mode 100644 app/preprints/discover/controller.ts create mode 100644 app/preprints/discover/route.ts create mode 100644 app/preprints/discover/template.hbs diff --git a/app/preprints/discover/controller.ts b/app/preprints/discover/controller.ts new file mode 100644 index 00000000000..dc06ccb66de --- /dev/null +++ b/app/preprints/discover/controller.ts @@ -0,0 +1,8 @@ + +import Store from '@ember-data/store'; +import Controller from '@ember/controller'; +import { inject as service } from '@ember/service'; + +export default class PreprintDiscoverController extends Controller { + @service store!: Store; +} diff --git a/app/preprints/discover/route.ts b/app/preprints/discover/route.ts new file mode 100644 index 00000000000..26e3c22de4c --- /dev/null +++ b/app/preprints/discover/route.ts @@ -0,0 +1,11 @@ +import Store from '@ember-data/store'; +import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; + +export default class PreprintDiscoverRoute extends Route { + @service store!: Store; + + async model(args: any) { + return await this.store.findRecord('preprint-provider', args.provider_id); + } +} diff --git a/app/preprints/discover/template.hbs b/app/preprints/discover/template.hbs new file mode 100644 index 00000000000..00f89bf0da2 --- /dev/null +++ b/app/preprints/discover/template.hbs @@ -0,0 +1 @@ +{{!-- placeholder route for preprint discover --}} \ No newline at end of file diff --git a/app/router.ts b/app/router.ts index 2e42a9a3759..70f4176fe25 100644 --- a/app/router.ts +++ b/app/router.ts @@ -25,6 +25,9 @@ Router.map(function() { this.route('discover', { path: '/:institution_id' }); this.route('dashboard', { path: '/:institution_id/dashboard' }); }); + this.route('preprints', function() { + this.route('discover', { path: '/:provider_id/discover' }); + }); this.route('register'); this.route('settings', function() { this.route('profile', function() { From 3e7e77980f3aae18211ed0146b82abe217f51de3 Mon Sep 17 00:00:00 2001 From: futa-ikeda <51409893+futa-ikeda@users.noreply.github.com> Date: Mon, 10 Jul 2023 23:07:34 -0400 Subject: [PATCH 12/60] [ENG-4574] Preprint discover rewrite (#1896) * add brand relationship to preprint provider model (#1887) * Remove unused services from search controller * Use search-page component on preprint discover page * Modifiy branded-navbar for preprints * Error handling and theme resetting * Branded preprint discover part 1 * Branded preprint discover part 2 * Test prerpint discover page * Group CR feedback re: search-page component arguments * Fix test --------- Co-authored-by: Yuhuai Liu --- app/models/preprint-provider.ts | 10 +- app/preprints/discover/controller.ts | 26 ++++ app/preprints/discover/route.ts | 28 ++++- app/preprints/discover/template.hbs | 12 +- app/preprints/template.hbs | 7 ++ app/search/controller.ts | 13 -- app/search/template.hbs | 2 +- .../components/branded-navbar/component.ts | 15 +++ .../components/branded-navbar/template.hbs | 98 +++++++++------ .../addon/components/osf-layout/template.hbs | 1 + .../addon/components/search-page/component.ts | 27 ++++- .../addon/components/search-page/styles.scss | 39 ++++++ .../addon/components/search-page/template.hbs | 114 ++++++++++++------ mirage/scenarios/default.ts | 4 + mirage/scenarios/preprints.ts | 24 ++++ mirage/serializers/preprint-provider.ts | 73 +++++++++++ package.json | 1 + tests/acceptance/preprints/discover-test.ts | 58 +++++++++ translations/en-us.yml | 4 +- 19 files changed, 459 insertions(+), 97 deletions(-) create mode 100644 app/preprints/template.hbs create mode 100644 mirage/scenarios/preprints.ts create mode 100644 mirage/serializers/preprint-provider.ts create mode 100644 tests/acceptance/preprints/discover-test.ts diff --git a/app/models/preprint-provider.ts b/app/models/preprint-provider.ts index 2ea03a9a80a..f3a1937d784 100644 --- a/app/models/preprint-provider.ts +++ b/app/models/preprint-provider.ts @@ -1,13 +1,14 @@ -import { attr, hasMany, AsyncHasMany } from '@ember-data/model'; +import { attr, hasMany, AsyncHasMany, belongsTo, AsyncBelongsTo } from '@ember-data/model'; import { computed } from '@ember/object'; import { alias } from '@ember/object/computed'; import { inject as service } from '@ember/service'; import Intl from 'ember-intl/services/intl'; +import BrandModel from 'ember-osf-web/models/brand'; import { RelatedLinkMeta } from 'osf-api'; import PreprintModel from './preprint'; -import ProviderModel from './provider'; +import ProviderModel, { ReviewPermissions } from './provider'; export type PreprintWord = 'default' | 'work' | 'paper' | 'preprint' | 'thesis'; export type PreprintWordGrammar = 'plural' | 'pluralCapitalized' | 'singular' | 'singularCapitalized'; @@ -21,10 +22,13 @@ export default class PreprintProviderModel extends ProviderModel { @attr('string') preprintWord!: PreprintWord; // Reviews settings - @attr('array') permissions!: string[]; + @attr('array') permissions!: ReviewPermissions[]; @attr('boolean', { allowNull: true }) reviewsCommentsPrivate!: boolean | null; // Relationships + @belongsTo('brand') + brand!: AsyncBelongsTo & BrandModel; + @hasMany('preprint', { inverse: 'provider' }) preprints!: AsyncHasMany; diff --git a/app/preprints/discover/controller.ts b/app/preprints/discover/controller.ts index dc06ccb66de..27473082596 100644 --- a/app/preprints/discover/controller.ts +++ b/app/preprints/discover/controller.ts @@ -1,8 +1,34 @@ import Store from '@ember-data/store'; import Controller from '@ember/controller'; +import { action } from '@ember/object'; import { inject as service } from '@ember/service'; +import { tracked } from '@glimmer/tracking'; +import config from 'ember-get-config'; + +import Theme from 'ember-osf-web/services/theme'; +import pathJoin from 'ember-osf-web/utils/path-join'; export default class PreprintDiscoverController extends Controller { @service store!: Store; + @service theme!: Theme; + + @tracked q?: string = ''; + @tracked sort?: string = '-relevance'; + + queryParams = ['q', 'page', 'sort']; + + get defaultQueryOptions() { + return { + resourceType: 'osf:Preprints', + // TODO: get this from the API? + publisher: pathJoin(config.OSF.url, 'preprints', this.theme.id), + }; + } + + @action + onSearch(queryOptions: Record) { + this.q = queryOptions.q; + this.sort = queryOptions.sort; + } } diff --git a/app/preprints/discover/route.ts b/app/preprints/discover/route.ts index 26e3c22de4c..529014833ca 100644 --- a/app/preprints/discover/route.ts +++ b/app/preprints/discover/route.ts @@ -1,11 +1,37 @@ import Store from '@ember-data/store'; import Route from '@ember/routing/route'; +import RouterService from '@ember/routing/router-service'; import { inject as service } from '@ember/service'; +import Theme from 'ember-osf-web/services/theme'; + export default class PreprintDiscoverRoute extends Route { @service store!: Store; + @service theme!: Theme; + @service router!: RouterService; + + buildRouteInfoMetadata() { + return { + osfMetrics: { + isSearch: true, + providerId: this.theme.id, + }, + }; + } async model(args: any) { - return await this.store.findRecord('preprint-provider', args.provider_id); + try { + const provider = await this.store.findRecord('preprint-provider', args.provider_id); + this.theme.providerType = 'preprint'; + this.theme.id = args.provider_id; + return provider; + } catch (e) { + this.router.transitionTo('not-found', `preprints/${args.provider_id}/discover`); + return null; + } + } + + deactivate() { + this.theme.reset(); } } diff --git a/app/preprints/discover/template.hbs b/app/preprints/discover/template.hbs index 00f89bf0da2..b7d71eda91b 100644 --- a/app/preprints/discover/template.hbs +++ b/app/preprints/discover/template.hbs @@ -1 +1,11 @@ -{{!-- placeholder route for preprint discover --}} \ No newline at end of file + diff --git a/app/preprints/template.hbs b/app/preprints/template.hbs new file mode 100644 index 00000000000..3eecfc9f29d --- /dev/null +++ b/app/preprints/template.hbs @@ -0,0 +1,7 @@ + + +{{outlet}} diff --git a/app/search/controller.ts b/app/search/controller.ts index 831b947b217..0a08e091808 100644 --- a/app/search/controller.ts +++ b/app/search/controller.ts @@ -1,27 +1,14 @@ -import Store from '@ember-data/store'; import Controller from '@ember/controller'; import { action } from '@ember/object'; -import { inject as service } from '@ember/service'; import { tracked } from '@glimmer/tracking'; -import Intl from 'ember-intl/services/intl'; -import Media from 'ember-responsive'; export default class SearchController extends Controller { - @service intl!: Intl; - @service store!: Store; - @service media!: Media; - @service toast!: Toastr; - @tracked q?: string = ''; @tracked sort?: string = '-relevance'; @tracked resourceType?: string = 'All'; queryParams = ['q', 'page', 'sort', 'resourceType']; - get showSidePanelToggle() { - return this.media.isMobile || this.media.isTablet; - } - @action onSearch(queryOptions: Record) { this.q = queryOptions.q; diff --git a/app/search/template.hbs b/app/search/template.hbs index f07f893656b..74ccec30553 100644 --- a/app/search/template.hbs +++ b/app/search/template.hbs @@ -4,8 +4,8 @@ @route='search' @query={{this.q}} @queryParams={{this.queryParams}} - @showSidePanelToggle={{this.showSidePanelToggle}} @onSearch={{action this.onSearch}} + @showResourceTypeFilter={{true}} @sort={{this.sort}} @resourceType={{this.resourceType}} /> diff --git a/lib/app-components/addon/components/branded-navbar/component.ts b/lib/app-components/addon/components/branded-navbar/component.ts index b4695b5b57e..ec223783e87 100644 --- a/lib/app-components/addon/components/branded-navbar/component.ts +++ b/lib/app-components/addon/components/branded-navbar/component.ts @@ -3,6 +3,7 @@ import Component from '@ember/component'; import { action, computed } from '@ember/object'; import { alias } from '@ember/object/computed'; import { inject as service } from '@ember/service'; +import config from 'ember-get-config'; import Intl from 'ember-intl/services/intl'; import Media from 'ember-responsive'; import Session from 'ember-simple-auth/services/session'; @@ -10,13 +11,17 @@ import { tracked } from 'tracked-built-ins'; import { serviceLinks } from 'ember-osf-web/const/service-links'; import { layout, requiredAction } from 'ember-osf-web/decorators/component'; +import ProviderModel from 'ember-osf-web/models/provider'; import Analytics from 'ember-osf-web/services/analytics'; +import CurrentUserService from 'ember-osf-web/services/current-user'; import Theme from 'ember-osf-web/services/theme'; import styles from './styles'; import template from './template'; type ObjectType = 'collection' | 'preprint' | 'registration'; +const osfURL = config.OSF.url; + @layout(template, styles) @tagName('') export default class BrandedNavbar extends Component { @@ -25,6 +30,7 @@ export default class BrandedNavbar extends Component { @service media!: Media; @service session!: Session; @service theme!: Theme; + @service currentUser!: CurrentUserService; brandRoute!: string; objectType!: ObjectType; @@ -35,6 +41,15 @@ export default class BrandedNavbar extends Component { myProjectsUrl = serviceLinks.myProjects; + get reviewUrl() { + return `${osfURL}reviews`; + } + + get submitPreprintUrl() { + return this.theme.isProvider ? `${osfURL}preprints/${this.theme.id}/submit/` : `${osfURL}preprints/submit/`; + } + + @alias('theme.provider') provider!: ProviderModel; @alias('theme.provider.id') providerId!: string; @computed('intl.locale', 'theme.provider', 'translateKey') diff --git a/lib/app-components/addon/components/branded-navbar/template.hbs b/lib/app-components/addon/components/branded-navbar/template.hbs index 5cb05b5edb8..47023f0647b 100644 --- a/lib/app-components/addon/components/branded-navbar/template.hbs +++ b/lib/app-components/addon/components/branded-navbar/template.hbs @@ -34,47 +34,75 @@ local-class='secondary-navigation' >