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}}
- {{!-- Object type filter dropdown if we are in mobile --}}
{{t 'search.left-panel.header'}}
+ {{#if this.showSidePanelToggle}}
+
+
+
+
+
+
+ {{/if}}
{{#if this.activeFilters.length}}
{{#each this.activeFilters as |filter|}}
@@ -58,7 +76,7 @@ as |layout|>
local-class='active-filter-item'
>
- {{filter.property}} |
+ {{filter.property}}:
{{filter.value}}
{{/if}}
{{#each this.filterableProperties as |filterableProperty|}}
- {{#let filterableProperty.metadataRecord as |propertyRecord|}}
+ {{#let filterableProperty.indexCard as |propertyCard|}}
@@ -88,12 +106,45 @@ as |layout|>
{{t 'search.left-panel.no-filterable-properties'}}
{{/each}}
-
- Main
- {{!-- placeholder if no search term (maybe) --}}
- {{!-- object type filtering tabs if desktop--}}
- {{!-- search cards --}}
- {{!-- sort dropdown --}}
+
{{!-- paginator --}}
+ {{#unless this.showSidePanelToggle}}
+
+
+
+
+ {{sortOption.display}}
+
+
+
+ {{/unless}}
diff --git a/app/serializers/metadata-record-search.ts b/app/serializers/index-card-search.ts
similarity index 57%
rename from app/serializers/metadata-record-search.ts
rename to app/serializers/index-card-search.ts
index c868b4c2edc..5b86f0c5600 100644
--- a/app/serializers/metadata-record-search.ts
+++ b/app/serializers/index-card-search.ts
@@ -1,10 +1,10 @@
import ShareSerializer from './share-serializer';
-export default class MetadataRecordSearchSerializer extends ShareSerializer {
+export default class IndexCardSearchSerializer extends ShareSerializer {
}
declare module 'ember-data/types/registries/serializer' {
export default interface SerializerRegistry {
- 'metadata-record-search': MetadataRecordSearchSerializer;
+ 'index-card-search': IndexCardSearchSerializer;
} // eslint-disable-line semi
}
diff --git a/app/serializers/metadata-record.ts b/app/serializers/index-card.ts
similarity index 61%
rename from app/serializers/metadata-record.ts
rename to app/serializers/index-card.ts
index a91a8e27bdc..9b5b13575f5 100644
--- a/app/serializers/metadata-record.ts
+++ b/app/serializers/index-card.ts
@@ -1,10 +1,10 @@
import ShareSerializer from './share-serializer';
-export default class MetadataRecordSerializer extends ShareSerializer {
+export default class IndexCardSerializer extends ShareSerializer {
}
declare module 'ember-data/types/registries/serializer' {
export default interface SerializerRegistry {
- 'metadata-record': MetadataRecordSerializer;
+ 'index-card': IndexCardSerializer;
} // eslint-disable-line semi
}
diff --git a/app/serializers/metadata-value-search.ts b/app/serializers/index-property-search.ts
similarity index 62%
rename from app/serializers/metadata-value-search.ts
rename to app/serializers/index-property-search.ts
index 655a3f882cb..22707f3206d 100644
--- a/app/serializers/metadata-value-search.ts
+++ b/app/serializers/index-property-search.ts
@@ -1,10 +1,10 @@
import ShareSerializer from './share-serializer';
-export default class MetadataValueSearchSerializer extends ShareSerializer {
+export default class IndexPropertySearchSerializer extends ShareSerializer {
}
declare module 'ember-data/types/registries/serializer' {
export default interface SerializerRegistry {
- 'metadata-value-search': MetadataValueSearchSerializer;
+ 'index-property-search': IndexPropertySearchSerializer;
} // eslint-disable-line semi
}
diff --git a/app/serializers/metadata-property-search.ts b/app/serializers/index-value-search.ts
similarity index 56%
rename from app/serializers/metadata-property-search.ts
rename to app/serializers/index-value-search.ts
index 6751421b625..fca2709c0f7 100644
--- a/app/serializers/metadata-property-search.ts
+++ b/app/serializers/index-value-search.ts
@@ -1,10 +1,10 @@
import ShareSerializer from './share-serializer';
-export default class MetadataPropertySearchSerializer extends ShareSerializer {
+export default class IndexValueSearchSerializer extends ShareSerializer {
}
declare module 'ember-data/types/registries/serializer' {
export default interface SerializerRegistry {
- 'metadata-property-search': MetadataPropertySearchSerializer;
+ 'index-value-search': IndexValueSearchSerializer;
} // eslint-disable-line semi
}
diff --git a/mirage/config.ts b/mirage/config.ts
index ca11119b935..339b1e6323f 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, valueSearch } from './views/search';
+import { cardSearch, valueSearch } from './views/search';
import { createToken } from './views/token';
import { createEmails, updateEmails } from './views/update-email';
import {
@@ -62,9 +62,9 @@ export default function(this: Server) {
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.get('/index-card-searches', cardSearch);
+ this.get('/index-value-searches', valueSearch);
+ // this.get('/index-cards/:id', Detail);
this.urlPrefix = apiUrl;
this.namespace = '/v2';
diff --git a/mirage/views/search.ts b/mirage/views/search.ts
index e4a36bec5fe..d8db249102c 100644
--- a/mirage/views/search.ts
+++ b/mirage/views/search.ts
@@ -1,15 +1,15 @@
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
+export function cardSearch(_: Schema, __: Request) {
+ // TODO: replace with a real index-card-search and use request to populate attrs
return {
data: {
- type: 'metadata-record-search',
+ type: 'index-card-search',
id: 'zzzzzz',
attributes:{
- recordSearchText: 'hello',
- recordSearchFilter: [
+ cardSearchText: 'hello',
+ cardSearchFilter: [
{
propertyPath: 'resourceType',
filterType: 'eq',
@@ -50,7 +50,7 @@ export function recordSearch(_: Schema, __: Request) {
},
relatedPropertySearch: {
data: {
- type: 'metadata-property-search',
+ type: 'index-property-search',
id: 'tuv',
},
},
@@ -73,13 +73,13 @@ export function recordSearch(_: Schema, __: Request) {
],
},
relationships: {
- metadataRecord: {
+ indexCard: {
data: {
- type: 'metadata-record',
+ type: 'index-card',
id: 'abc',
},
links: {
- related: 'https://share.osf.io/api/v2/metadata-record/abc',
+ related: 'https://share.osf.io/api/v2/index-card/abc',
},
},
},
@@ -96,13 +96,13 @@ export function recordSearch(_: Schema, __: Request) {
],
},
relationships: {
- metadataRecord: {
+ indexCard: {
data: {
- type: 'metadata-record',
+ type: 'index-card',
id: 'def',
},
links: {
- related: 'https://share.osf.io/api/v2/metadata-record/def',
+ related: 'https://share.osf.io/api/v2/index-card/def',
},
},
},
@@ -119,19 +119,19 @@ export function recordSearch(_: Schema, __: Request) {
],
},
relationships: {
- metadataRecord: {
+ indexCard: {
data: {
- type: 'metadata-record',
- id: 'abc',
+ type: 'index-card',
+ id: 'ghi',
},
links: {
- related: 'https://share.osf.io/api/v2/metadata-record/abc',
+ related: 'https://share.osf.io/api/v2/index-card/abc',
},
},
},
},
{
- type: 'metadata-record',
+ type: 'index-card',
id: 'abc',
attributes: {
resourceType: [
@@ -212,25 +212,79 @@ export function recordSearch(_: Schema, __: Request) {
},
},
links: {
- self: 'https://share.osf.io/api/v2/metadata-record/abc',
+ self: 'https://share.osf.io/api/v2/index-card/abc',
resource: 'https://osf.example/abcfoo',
},
},
{
- type: 'metadata-record',
+ type: 'index-card',
id: 'def',
+ 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': 'Hi!',
+ '@language': 'en',
+ },
+ ],
+ },
+ },
+ links: {
+ self: 'https://share.osf.io/api/v2/index-card/ghi',
+ resource: 'https://osf.example/abcfoo',
+ },
},
{
- type: 'metadata-record',
+ type: 'index-card',
id: 'ghi',
+ 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': 'Ahoj! That\'s hello in Czech!',
+ '@language': 'en',
+ },
+ ],
+ description: [
+ {
+ '@value': 'Some description',
+ '@language': 'en',
+ },
+ ],
+ },
+ },
+ links: {
+ self: 'https://share.osf.io/api/v2/index-card/ghi',
+ resource: 'https://osf.example/abcfoo',
+ },
},
// Related properties search object
{
- type: 'metadata-property-search',
+ type: 'index-property-search',
id: 'tuv',
attributes: {
- recordSearchText: 'hello',
- recordSearchFilter: [
+ cardSearchText: 'hello',
+ cardSearchFilter: [
{
propertyPath: 'resourceType',
filterType: 'eq',
@@ -293,13 +347,13 @@ export function recordSearch(_: Schema, __: Request) {
recordResultCount: 345,
},
relationships: {
- metadataRecord: {
+ indexCard: {
data: {
- type: 'metadata-record',
+ type: 'index-card',
id: 'idForPropertyRecord1',
},
links: {
- related: 'https://share.osf.io/api/v2/metadata-record/idForPropertyRecord1',
+ related: 'https://share.osf.io/api/v2/index-card/idForPropertyRecord1',
},
},
},
@@ -317,13 +371,13 @@ export function recordSearch(_: Schema, __: Request) {
recordResultCount: 123,
},
relationships: {
- metadataRecord: {
+ indexCard: {
data: {
- type: 'metadata-record',
+ type: 'index-card',
id: 'idForPropertyRecord2',
},
links: {
- related: 'https://share.osf.io/api/v2/metadata-record/idForPropertyRecord2',
+ related: 'https://share.osf.io/api/v2/index-card/idForPropertyRecord2',
},
},
},
@@ -341,19 +395,19 @@ export function recordSearch(_: Schema, __: Request) {
recordResultCount: 33,
},
relationships: {
- metadataRecord: {
+ indexCard: {
data: {
- type: 'metadata-record',
+ type: 'index-card',
id: 'idForPropertyRecord3',
},
links: {
- related: 'https://share.osf.io/api/v2/metadata-record/idForPropertyRecord3',
+ related: 'https://share.osf.io/api/v2/index-card/idForPropertyRecord3',
},
},
},
},
{
- type: 'metadata-record',
+ type: 'index-card',
id: 'idForPropertyRecord1',
attributes: {
resourceType: [
@@ -380,12 +434,12 @@ export function recordSearch(_: Schema, __: Request) {
},
},
links: {
- self: 'https://share.osf.io/api/v2/metadata-record/idForPropertyRecord1',
+ self: 'https://share.osf.io/api/v2/index-card/idForPropertyRecord1',
resource: 'http://purl.org/dc/terms/license',
},
},
{
- type: 'metadata-record',
+ type: 'index-card',
id: 'idForPropertyRecord2',
attributes: {
resourceType: [
@@ -412,12 +466,12 @@ export function recordSearch(_: Schema, __: Request) {
},
},
links: {
- self: 'https://share.osf.io/api/v2/metadata-record/idForPropertyRecord2',
+ self: 'https://share.osf.io/api/v2/index-card/idForPropertyRecord2',
resource: 'http://purl.org/dc/terms/published',
},
},
{
- type: 'metadata-record',
+ type: 'index-card',
id: 'idForPropertyRecord3',
attributes: {
resourceType: [
@@ -444,7 +498,7 @@ export function recordSearch(_: Schema, __: Request) {
},
},
links: {
- self: 'https://share.osf.io/api/v2/metadata-record/idForPropertyRecord2',
+ self: 'https://share.osf.io/api/v2/index-card/idForPropertyRecord2',
resource: 'http://purl.org/dc/terms/funder',
},
},
@@ -457,7 +511,7 @@ export function valueSearch(_: Schema, __: Request) {
const property2Id = faker.random.uuid();
return {
data: {
- type: 'metadata-value-search',
+ type: 'index-value-search',
id: 'lmnop',
attributes: {
valueSearchText: 'Institute of Health',
@@ -468,8 +522,8 @@ export function valueSearch(_: Schema, __: Request) {
filterValues: ['datacite:Funder'],
},
],
- recordSearchText: 'influenza',
- recordSearchFilter: [
+ cardSearchText: 'influenza',
+ cardSearchFilter: [
{
propertyPath: 'resourceType',
filterType: 'eq',
@@ -490,7 +544,7 @@ export function valueSearch(_: Schema, __: Request) {
},
},
relatedPropertySearch: {
- data: {type: 'metadata-property-search', id: '12345'},
+ data: {type: 'index-property-search', id: '12345'},
},
},
},
@@ -505,9 +559,9 @@ export function valueSearch(_: Schema, __: Request) {
recordResultCount: 2134,
},
relationships: {
- metadataRecord: {
- data: {type: 'metadata-record', id: property1Id},
- links: {related: 'https://share.osf.example/metadata-record/abc'},
+ indexCard: {
+ data: {type: 'index-card', id: property1Id},
+ links: {related: 'https://share.osf.example/index-card/abc'},
},
},
},
@@ -521,14 +575,14 @@ export function valueSearch(_: Schema, __: Request) {
recordResultCount: 2,
},
relationships: {
- metadataRecord: {
- data: {type: 'metadata-record', id: property2Id},
- links: {related: 'https://share.osf.example/metadata-record/def'},
+ indexCard: {
+ data: {type: 'index-card', id: property2Id},
+ links: {related: 'https://share.osf.example/index-card/def'},
},
},
},
{
- type: 'metadata-record',
+ type: 'index-card',
id: property1Id,
attributes: {
resourceType: 'osf:Funder',
@@ -541,7 +595,7 @@ export function valueSearch(_: Schema, __: Request) {
},
},
{
- type: 'metadata-record',
+ type: 'index-card',
id: property2Id,
attributes: {
resourceType: 'osf:Funder',
diff --git a/tests/acceptance/search/search-filters-test.ts b/tests/acceptance/search/search-filters-test.ts
index eb8c160e7d7..ef31ca35de8 100644
--- a/tests/acceptance/search/search-filters-test.ts
+++ b/tests/acceptance/search/search-filters-test.ts
@@ -48,7 +48,7 @@ module(moduleName, hooks => {
.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 clickTrigger('[data-test-dialog]');
await untrackedClick('[data-option-index="0"]');
assert.dom('[data-test-see-more-dialog-apply-button]')
.isNotDisabled('Apply button enabled after selecting a filter');
diff --git a/tests/acceptance/search/search-query-params-test.ts b/tests/acceptance/search/search-query-params-test.ts
new file mode 100644
index 00000000000..9f850491248
--- /dev/null
+++ b/tests/acceptance/search/search-query-params-test.ts
@@ -0,0 +1,84 @@
+import { click as untrackedClick, visit } from '@ember/test-helpers';
+import { setupMirage } from 'ember-cli-mirage/test-support';
+import { setBreakpoint } from 'ember-responsive/test-support';
+import { clickTrigger } from 'ember-power-select/test-support/helpers';
+import { module, test } from 'qunit';
+
+import { click, currentURL, setupOSFApplicationTest } from 'ember-osf-web/tests/helpers';
+
+const moduleName = 'Acceptance | search | query-params';
+
+module(moduleName, hooks => {
+ setupOSFApplicationTest(hooks);
+ setupMirage(hooks);
+
+ test('default query-parameters', async assert => {
+ // Load search page
+ await visit('/search');
+ // assert object type nav is shown
+ assert.dom('[data-test-topbar-object-type-nav]').exists('Object type nav shown in desktop view');
+ assert.dom('[data-test-left-panel-object-type-dropdown]')
+ .doesNotExist('Left-panel object type dropdown not shown in desktop view');
+ // assert sort dropdown is shown
+ assert.dom('[data-test-topbar-sort-dropdown]').exists('Sort dropdown shown in desktop view');
+ assert.dom('[data-test-left-panel-sort-dropdown]')
+ .doesNotExist('Left-panel sort dropdown not shown in desktop view');
+ // assert default query-param values
+ assert.equal(currentURL(), '/search', 'Default query-params are empty');
+ assert.dom('[data-test-topbar-object-type-link="All"]').hasClass('active', 'All is the default object type');
+ assert.dom('[data-test-topbar-sort-dropdown]').containsText('Relevance', 'Relevance is the default sort');
+ // change object type
+ await click('[data-test-topbar-object-type-link="Projects"]');
+ assert.dom('[data-test-topbar-object-type-link="Projects"]').hasClass('active', 'Projects is selected');
+ assert.dom('[data-test-topbar-object-type-link="All"]').doesNotHaveClass('active', 'All is not selected');
+ assert.equal(currentURL(), '/search?resourceType=Projects', 'Query-params are updated');
+ // change sort
+ await clickTrigger('[data-test-topbar-sort-dropdown]');
+ await untrackedClick('[data-option-index="2"]'); // date-createst, oldest
+ assert.dom('[data-test-topbar-sort-dropdown]').containsText('Date created, oldest',
+ 'Date created, oldest first is selected');
+ assert.equal(currentURL(), '/search?resourceType=Projects&sort=date_created', 'Query-params are updated');
+ });
+
+ test('default query-parameters, mobile', async assert => {
+ setBreakpoint('mobile');
+ // Load search page
+ await visit('/search');
+ // assert object type nav is shown
+ await click('[data-test-toggle-side-panel]');
+ assert.dom('[data-test-topbar-object-type-nav]').doesNotExist('Object type nav not shown in mobile view');
+ assert.dom('[data-test-left-panel-object-type-dropdown]')
+ .exists('Left-panel object type dropdown shown in mobile view');
+ // assert sort dropdown is shown
+ assert.dom('[data-test-topbar-sort-dropdown]').doesNotExist('Sort dropdown not shown in mobile view');
+ assert.dom('[data-test-left-panel-sort-dropdown]')
+ .exists('Left-panel sort dropdown shown in mobile view');
+ // assert default query-param values
+ assert.equal(currentURL(), '/search', 'Default query-params are empty');
+ assert.dom('[data-test-left-panel-object-type-dropdown]').containsText('All', 'All is the default object type');
+ assert.dom('[data-test-left-panel-sort-dropdown]').containsText('Relevance', 'Relevance is the default sort');
+ // change object type
+ await clickTrigger('[data-test-left-panel-object-type-dropdown]');
+ await untrackedClick('[data-option-index="2"]'); // Registrations
+ assert.equal(currentURL(), '/search?resourceType=Registrations', 'Object type query-param updated');
+ // change sort
+ await clickTrigger('[data-test-left-panel-sort-dropdown]');
+ await untrackedClick('[data-option-index="4"]'); // date-modified, oldest
+ assert.equal(currentURL(), '/search?resourceType=Registrations&sort=date_modified', 'Query-params are updated');
+ });
+
+ test('query-parameters from url', async assert => {
+ await visit('/search?resourceType=Preprints&sort=-date_modified');
+ assert.dom('[data-test-topbar-object-type-link="Preprints"]')
+ .hasClass('active', 'Desktop: Active object type filter selected from url');
+ assert.dom('[data-test-topbar-sort-dropdown]')
+ .containsText('Date modified, newest', 'Desktop: Active sort selected from url');
+
+ setBreakpoint('mobile');
+ await click('[data-test-toggle-side-panel]');
+ assert.dom('[data-test-left-panel-object-type-dropdown]')
+ .containsText('Preprints', 'Mobile: Active object type filter selected from url');
+ assert.dom('[data-test-left-panel-sort-dropdown]')
+ .containsText('Date modified, newest', 'Mobile: Active sort selected from url');
+ });
+});
diff --git a/translations/en-us.yml b/translations/en-us.yml
index 5044295eb46..a03d677fb56 100644
--- a/translations/en-us.yml
+++ b/translations/en-us.yml
@@ -205,12 +205,27 @@ dashboard:
search:
page-title: 'Search'
- metadata-record:
+ index-card:
no-label: 'No label found'
no-title: 'No title found'
search-header: 'Search OSF'
textbox-placeholder: 'Search placeholder'
search-button-label: 'Search'
+ resource-type:
+ search-by: 'Search by resource type'
+ all: 'All'
+ projects: 'Projects'
+ registrations: 'Registrations'
+ preprints: 'Preprints'
+ files: 'Files'
+ users: 'Users'
+ sort:
+ sort-by: 'Sort by'
+ relevance: 'Relevance'
+ created-date-descending: 'Date created, newest'
+ created-date-ascending: 'Date created, oldest'
+ modified-date-descending: 'Date modified, newest'
+ modified-date-ascending: 'Date modified, oldest'
toggle-sidenav: 'Toggle search sidebar'
left-panel:
header: Refine
@@ -222,6 +237,9 @@ search:
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'
+helpers:
+ get-localized-property:
+ not-provided: 'Not provided'
new_project:
header: 'Create new project'
title_placeholder: 'Enter project title'
From dda39dfe3b4b1135b8e33293c6c086495b37b889 Mon Sep 17 00:00:00 2001
From: futa-ikeda <51409893+futa-ikeda@users.noreply.github.com>
Date: Tue, 13 Jun 2023 12:30:11 -0400
Subject: [PATCH 07/60] [ENG-4470] Reroute to new search (#1870)
- Ticket: [ENG-4470]
- Feature flag: n/a
## Purpose
- reroute users to the new search page instead of registries discover
- Use OsfLink for navbar link to search page to avoid a full app reload
## Summary of Changes
- Change search behavior on registries landing page (osf.io/registries)
- Rewrite how osf-navbar handles "Search" button to use OsfLink
---
.../components/osf-navbar/x-links/component.ts | 1 -
.../components/osf-navbar/x-links/template.hbs | 18 ++++++++++--------
lib/registries/addon/index/controller.ts | 7 +++++--
3 files changed, 15 insertions(+), 11 deletions(-)
diff --git a/lib/osf-components/addon/components/osf-navbar/x-links/component.ts b/lib/osf-components/addon/components/osf-navbar/x-links/component.ts
index 8dbb3e70094..b713e643914 100644
--- a/lib/osf-components/addon/components/osf-navbar/x-links/component.ts
+++ b/lib/osf-components/addon/components/osf-navbar/x-links/component.ts
@@ -15,7 +15,6 @@ const osfURL = config.OSF.url;
export default class XLinks extends Component {
@service session!: Session;
- searchURL = `${osfURL}search/`;
myProjectsURL = `${osfURL}myprojects/`;
myRegistrationsURL = `${osfURL}myprojects/#registrations`;
supportURL = `${config.support.faqPageUrl}`;
diff --git a/lib/osf-components/addon/components/osf-navbar/x-links/template.hbs b/lib/osf-components/addon/components/osf-navbar/x-links/template.hbs
index 5dc77d9e6cf..e43191e9d0e 100644
--- a/lib/osf-components/addon/components/osf-navbar/x-links/template.hbs
+++ b/lib/osf-components/addon/components/osf-navbar/x-links/template.hbs
@@ -15,13 +15,6 @@
text=(t 'navbar.my_registrations')
onClicked=this.onLinkClicked
)
- search=(component 'osf-navbar/x-links/hyper-link'
- this.searchURL
- analyticsLabel='Search'
- tagName='li'
- text=(t 'navbar.search')
- onClicked=this.onLinkClicked
- )
support=(component 'osf-navbar/x-links/hyper-link'
this.supportURL
analyticsLabel='Support'
@@ -42,7 +35,16 @@
{{yield links}}
{{else}}
-
+
+
+ {{t 'navbar.search'}}
+
+
{{/if}}
diff --git a/lib/registries/addon/index/controller.ts b/lib/registries/addon/index/controller.ts
index 9a13d76df75..b64229a8ebb 100644
--- a/lib/registries/addon/index/controller.ts
+++ b/lib/registries/addon/index/controller.ts
@@ -34,8 +34,11 @@ export default class Index extends Controller {
@action
onSearch(query: string) {
- this.router.transitionTo('registries.discover', {
- queryParams: { q: query },
+ this.router.transitionTo('search', {
+ queryParams: {
+ q: query,
+ resourceType: 'osf:Registration',
+ },
});
}
}
From 998642b4ff382438df44d7ac9719cec97159bb78 Mon Sep 17 00:00:00 2001
From: futa-ikeda <51409893+futa-ikeda@users.noreply.github.com>
Date: Wed, 21 Jun 2023 17:05:12 -0400
Subject: [PATCH 08/60] [No ticket] Update SHARE endpoints (#1879)
- Ticket: [No ticket]
- Feature flag: n/a
## Purpose
- Update SHAREAdapter to point to correct locations
## Summary of Changes
- Update SHAREAdapter parent class to point use config variable for share-url
- Update SHAREAdapter parent class to point use api/v3 endpoints
- Update search-related adapters to point to singularized endpoint names (e.g. api/v3/index-card-search**es** -> api/v3/index-card-search
- Update mirage endpoints to reflect these changes
---
app/adapters/index-card-search.ts | 3 +++
app/adapters/index-card.ts | 3 +++
app/adapters/index-property-search.ts | 3 +++
app/adapters/index-value-search.ts | 3 +++
app/adapters/share-adapter.ts | 5 +++--
mirage/config.ts | 15 ++++++++-------
6 files changed, 23 insertions(+), 9 deletions(-)
diff --git a/app/adapters/index-card-search.ts b/app/adapters/index-card-search.ts
index 8a5bb6ff082..c30f0c69128 100644
--- a/app/adapters/index-card-search.ts
+++ b/app/adapters/index-card-search.ts
@@ -1,5 +1,8 @@
import ShareAdapter from './share-adapter';
export default class IndexCardSearchAdapter extends ShareAdapter {
+ pathForType() {
+ return 'index-card-search';
+ }
}
declare module 'ember-data/types/registries/adapter' {
diff --git a/app/adapters/index-card.ts b/app/adapters/index-card.ts
index cd5e49cb14e..3bd54888714 100644
--- a/app/adapters/index-card.ts
+++ b/app/adapters/index-card.ts
@@ -1,6 +1,9 @@
import ShareAdapter from './share-adapter';
export default class IndexCardAdapter extends ShareAdapter {
+ pathForType() {
+ return 'index-card';
+ }
}
declare module 'ember-data/types/registries/adapter' {
diff --git a/app/adapters/index-property-search.ts b/app/adapters/index-property-search.ts
index 3bec7157b32..9a69df75bfc 100644
--- a/app/adapters/index-property-search.ts
+++ b/app/adapters/index-property-search.ts
@@ -1,6 +1,9 @@
import ShareAdapter from './share-adapter';
export default class IndexPropertySearchAdapter extends ShareAdapter {
+ pathForType() {
+ return 'index-property-search';
+ }
}
declare module 'ember-data/types/registries/adapter' {
diff --git a/app/adapters/index-value-search.ts b/app/adapters/index-value-search.ts
index 6545fd9d1ae..552cc0c5c29 100644
--- a/app/adapters/index-value-search.ts
+++ b/app/adapters/index-value-search.ts
@@ -1,6 +1,9 @@
import ShareAdapter from './share-adapter';
export default class IndexValueSearchAdapter extends ShareAdapter {
+ pathForType() {
+ return 'index-value-search';
+ }
}
declare module 'ember-data/types/registries/adapter' {
diff --git a/app/adapters/share-adapter.ts b/app/adapters/share-adapter.ts
index 63194a537b5..b8ce1c88dd6 100644
--- a/app/adapters/share-adapter.ts
+++ b/app/adapters/share-adapter.ts
@@ -1,6 +1,7 @@
import JSONAPIAdapter from '@ember-data/adapter/json-api';
+import config from 'ember-get-config';
export default class ShareAdapter extends JSONAPIAdapter {
- host = 'https://share.osf.io';
- namespace = 'api/v2';
+ host = config.OSF.shareBaseUrl.replace(/\/$/, ''); // Remove trailing slash to avoid // in URLs
+ namespace = 'api/v3';
}
diff --git a/mirage/config.ts b/mirage/config.ts
index 339b1e6323f..aadb879e9b3 100644
--- a/mirage/config.ts
+++ b/mirage/config.ts
@@ -49,22 +49,23 @@ import { updatePassword } from './views/user-password';
import * as userSettings from './views/user-setting';
import * as wb from './views/wb';
-const { OSF: { apiUrl } } = config;
+const { OSF: { apiUrl, shareBaseUrl } } = config;
export default function(this: Server) {
this.passthrough(); // pass through all requests on currrent domain
this.passthrough('https://api.crossref.org/*');
- // SHARE search
- this.urlPrefix = 'https://share.osf.io';
- this.namespace = '/api/v2/';
// SHARE-powered registration discover endpoint
+ this.urlPrefix = 'https://share.osf.io';
+ this.namespace = '/api/v2/';
this.post('/search/creativeworks/_search', shareSearch);
// SHARE-powered search endpoints
- this.get('/index-card-searches', cardSearch);
- this.get('/index-value-searches', valueSearch);
- // this.get('/index-cards/:id', Detail);
+ this.urlPrefix = shareBaseUrl;
+ this.namespace = '/api/v3/';
+ this.get('/index-card-search', cardSearch);
+ this.get('/index-value-search', valueSearch);
+ // this.get('/index-card/:id', Detail);
this.urlPrefix = apiUrl;
this.namespace = '/v2';
From 35694bb9352236e2ffe87c301a95896fb709a3be Mon Sep 17 00:00:00 2001
From: futa-ikeda <51409893+futa-ikeda@users.noreply.github.com>
Date: Tue, 27 Jun 2023 11:19:34 -0400
Subject: [PATCH 09/60] [ENG-4568] Componentize search page (#1886)
- Ticket: [ENG-4568]
- Feature flag: n/a
## Purpose
- Componentize search page for reuse in branded pages
## Summary of Changes
- Move logic and templating from search page route to `search-page` component
- No logic for branding and default query-params yet in this PR
---
app/search/controller.ts | 138 +--------------
app/search/route.ts | 7 -
app/search/template.hbs | 159 ++----------------
.../addon/components/search-page/component.ts | 159 ++++++++++++++++++
.../search-page}/filter-facet/component.ts | 2 +-
.../search-page}/filter-facet/styles.scss | 0
.../search-page}/filter-facet/template.hbs | 0
.../addon/components/search-page}/styles.scss | 0
.../addon/components/search-page/template.hbs | 148 ++++++++++++++++
.../app/components/search-page/component.js | 1 +
.../search-page/filter-facet/component.js | 1 +
.../search-page/filter-facet/template.js | 1 +
.../app/components/search-page/template.js | 1 +
13 files changed, 329 insertions(+), 288 deletions(-)
create mode 100644 lib/osf-components/addon/components/search-page/component.ts
rename {app/search/-components => lib/osf-components/addon/components/search-page}/filter-facet/component.ts (98%)
rename {app/search/-components => lib/osf-components/addon/components/search-page}/filter-facet/styles.scss (100%)
rename {app/search/-components => lib/osf-components/addon/components/search-page}/filter-facet/template.hbs (100%)
rename {app/search => lib/osf-components/addon/components/search-page}/styles.scss (100%)
create mode 100644 lib/osf-components/addon/components/search-page/template.hbs
create mode 100644 lib/osf-components/app/components/search-page/component.js
create mode 100644 lib/osf-components/app/components/search-page/filter-facet/component.js
create mode 100644 lib/osf-components/app/components/search-page/filter-facet/template.js
create mode 100644 lib/osf-components/app/components/search-page/template.js
diff --git a/app/search/controller.ts b/app/search/controller.ts
index 8a8de6e3ec2..831b947b217 100644
--- a/app/search/controller.ts
+++ b/app/search/controller.ts
@@ -1,155 +1,31 @@
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 { 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 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', 'resourceType'];
@tracked q?: string = '';
- @tracked seachBoxText?: string = '';
-
- @tracked page?: number = 1;
-
- // 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;
+ @tracked sort?: string = '-relevance';
+ @tracked resourceType?: string = 'All';
- 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 searchResults?: SearchResultModel[];
- @tracked propertySearch?: IndexPropertySearchModel;
+ queryParams = ['q', 'page', 'sort', 'resourceType'];
get showSidePanelToggle() {
return this.media.isMobile || this.media.isTablet;
}
- get filterableProperties() {
- if (!this.propertySearch) {
- return [];
- }
- return this.propertySearch.get('searchResultPage');
- }
-
@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
- 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 search() {
- try {
- const { q, 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();
- } catch (e) {
- this.toast.error(e);
- }
+ onSearch(queryOptions: Record) {
+ this.q = queryOptions.q;
+ this.sort = queryOptions.sort;
+ this.resourceType = queryOptions.resourceType;
}
}
diff --git a/app/search/route.ts b/app/search/route.ts
index 75b0900985d..0a451bd304d 100644
--- a/app/search/route.ts
+++ b/app/search/route.ts
@@ -1,7 +1,5 @@
import Route from '@ember/routing/route';
-import SearchController from './controller';
-
export default class Search extends Route {
buildRouteInfoMetadata() {
return {
@@ -10,9 +8,4 @@ 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/template.hbs b/app/search/template.hbs
index 3ed23334418..f07f893656b 100644
--- a/app/search/template.hbs
+++ b/app/search/template.hbs
@@ -1,150 +1,11 @@
-{{!-- template-lint-disable no-bare-strings --}}
{{page-title (t 'search.page-title')}}
-
-
-
-
-
-
-
- {{#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'
>
- {{#if @displayModerationButton}}
+ {{#if (eq this.theme.providerType 'collection')}}
+ {{#if @displayModerationButton}}
+ -
+
+ {{t 'app_components.branded_navbar.moderation'}}
+
+
+ {{/if}}
+ -
+
+ {{t 'app_components.branded_navbar.my_osf_projects'}}
+
+
+ -
+
+ {{t @addLinkKey}}
+
+
+ -
+
+ {{t 'navbar.search'}}
+
+
+ {{else if (eq this.theme.providerType 'preprint')}}
-
- {{t 'app_components.branded_navbar.moderation'}}
+ {{t 'navbar.my_preprints'}}
+
+
+ {{#if this.currentUser.user.canViewReviews}}
+ -
+
+ {{t 'navbar.reviews'}}
+
+
+ {{/if}}
+ -
+
+ {{t 'navbar.add_a_preprint' preprintWord=this.provider.preprintWord}}
{{/if}}
- -
-
- {{t 'app_components.branded_navbar.my_osf_projects'}}
-
-
- -
-
- {{t @addLinkKey}}
-
-
- -
-
- {{t 'navbar.search'}}
-
-
-
{{yield (hash
diff --git a/lib/osf-components/addon/components/search-page/component.ts b/lib/osf-components/addon/components/search-page/component.ts
index 8e8c7f4cb5b..67e99aec0a1 100644
--- a/lib/osf-components/addon/components/search-page/component.ts
+++ b/lib/osf-components/addon/components/search-page/component.ts
@@ -9,9 +9,11 @@ import Intl from 'ember-intl/services/intl';
import { A } from '@ember/array';
import Store from '@ember-data/store';
import { action } from '@ember/object';
+import Media from 'ember-responsive';
import IndexPropertySearchModel from 'ember-osf-web/models/index-property-search';
import SearchResultModel from 'ember-osf-web/models/search-result';
+import ProviderModel from 'ember-osf-web/models/provider';
interface ResourceTypeOption {
display: string;
@@ -38,6 +40,9 @@ interface SearchArgs {
toggleFilter: (filter: Filter) => void;
sort: string;
resourceType: string;
+ defaultQueryOptions: Record;
+ provider?: ProviderModel;
+ showResourceTypeFilter: boolean;
}
const searchDebounceTime = 100;
@@ -46,11 +51,13 @@ export default class SearchPage extends Component {
@service intl!: Intl;
@service toast!: Toastr;
@service store!: Store;
+ @service media!: Media;
@tracked searchText?: string;
@tracked searchResults?: SearchResultModel[];
@tracked propertySearch?: IndexPropertySearchModel;
@tracked page?: number = 1;
+ @tracked totalResultCount?: number;
constructor( owner: unknown, args: SearchArgs) {
super(owner, args);
@@ -60,6 +67,10 @@ export default class SearchPage extends Component {
taskFor(this.search).perform();
}
+ get showSidePanelToggle() {
+ return this.media.isMobile || this.media.isTablet;
+ }
+
get filterableProperties() {
if (!this.propertySearch) {
return [];
@@ -71,6 +82,16 @@ export default class SearchPage extends Component {
return this.resourceTypeOptions.find(option => option.value === this.resourceType);
}
+ get showResultCountMiddle() {
+ const hasResults = this.totalResultCount && this.totalResultCount > 0;
+ return hasResults && !this.args.showResourceTypeFilter && !this.showSidePanelToggle;
+ }
+
+ get showResultCountLeft() {
+ const hasResults = this.totalResultCount && this.totalResultCount > 0;
+ return hasResults && this.showSidePanelToggle;
+ }
+
get selectedSortOption() {
return this.sortOptions.find(option => option.value === this.sort);// || this.sortOptions[0];
}
@@ -104,11 +125,12 @@ export default class SearchPage extends Component {
try {
const q = this.searchText;
const { page, sort, activeFilters, resourceType } = this;
- const filterQueryObject = activeFilters.reduce((acc, filter) => {
+ let filterQueryObject = activeFilters.reduce((acc, filter) => {
acc[filter.property] = filter.value;
return acc;
}, {} as { [key: string]: string });
filterQueryObject['resourceType'] = resourceType;
+ filterQueryObject = { ...filterQueryObject, ...this.args.defaultQueryOptions };
const searchResult = await this.store.queryRecord('index-card-search', {
q,
page,
@@ -116,7 +138,8 @@ export default class SearchPage extends Component {
filter: filterQueryObject,
});
this.propertySearch = await searchResult.relatedPropertySearch;
- this.searchResults = searchResult.searchResultPage.toArray();
+ this.searchResults = searchResult.searchResultPage.toArray();
+ this.totalResultCount = searchResult.totalResultCount;
if (this.args.onSearch) {
this.args.onSearch({q, sort, resourceType});
}
diff --git a/lib/osf-components/addon/components/search-page/styles.scss b/lib/osf-components/addon/components/search-page/styles.scss
index 07b6ae675ce..8f12f2ef4e4 100644
--- a/lib/osf-components/addon/components/search-page/styles.scss
+++ b/lib/osf-components/addon/components/search-page/styles.scss
@@ -8,6 +8,37 @@
background-color: $osf-dark-blue-navbar;
}
+.provider-logo {
+ background: var(--hero-logo-img-url) top center no-repeat;
+ background-size: contain;
+ height: 70px;
+ width: 100%;
+ margin: 2em 0 1em;
+}
+
+.hero-overlay {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ height: 100%;
+ min-width: 100%;
+ min-height: 100%;
+ position: relative;
+ background-color: transparent; // override .heading-wrapper background-color
+
+ &::after {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: var(--hero-background-img-url);
+ background-size: cover;
+ z-index: -1;
+ content: '';
+ }
+}
+
.heading-label {
padding: 40px;
}
@@ -34,6 +65,7 @@
.topbar {
margin-left: 20px;
display: flex;
+ justify-content: space-between;
border-bottom: 1px solid $color-text-black;
}
@@ -77,6 +109,13 @@
width: 300px;
}
+.search-count {
+ margin-top: 10px;
+ margin-bottom: 10px;
+ font-size: 1.3em;
+ font-weight: bold;
+}
+
.active-filter-list {
padding-left: 0;
list-style: none;
diff --git a/lib/osf-components/addon/components/search-page/template.hbs b/lib/osf-components/addon/components/search-page/template.hbs
index 10cb80a31d7..683354ab083 100644
--- a/lib/osf-components/addon/components/search-page/template.hbs
+++ b/lib/osf-components/addon/components/search-page/template.hbs
@@ -1,13 +1,29 @@
-
-
+
+ {{#if @provider}}
+
+
+
+ {{@provider.description}}
+
+ {{else}}
+
+ {{/if}}
- {{#if @showSidePanelToggle}}
+ {{#if this.showSidePanelToggle}}
',
});
}
diff --git a/tests/acceptance/preprints/discover-test.ts b/tests/acceptance/preprints/discover-test.ts
index 64d81787f49..539143a02e4 100644
--- a/tests/acceptance/preprints/discover-test.ts
+++ b/tests/acceptance/preprints/discover-test.ts
@@ -31,6 +31,8 @@ module('Acceptance | preprints | discover', hooks => {
test('Desktop', async function(this: PreprintDiscoverTestContext, assert) {
await visit(`/preprints/${this.provider.id}/discover`);
assert.equal(currentRouteName(), 'preprints.discover', 'Current route is preprints discover');
+ const pageTitle = document.getElementsByTagName('title')[0].innerText;
+ assert.equal(pageTitle, 'Thesis Commons | Search', 'Page title is correct');
assert.dom('[data-test-search-provider-logo]').exists('Desktop: Preprint provider logo is shown');
assert.dom('[data-test-search-provider-description]').exists('Desktop: Preprint provider description is shown');
assert.dom('[data-test-search-header]').doesNotExist('Desktop: Non-branded search header is not shown');
diff --git a/tests/unit/models/preprint-provider-test.ts b/tests/unit/models/preprint-provider-test.ts
index 23264f7bd6a..bcd5b8d77ac 100644
--- a/tests/unit/models/preprint-provider-test.ts
+++ b/tests/unit/models/preprint-provider-test.ts
@@ -9,4 +9,18 @@ module('Unit | Model | preprint-provider', hooks => {
const model = run(() => this.owner.lookup('service:store').createRecord('preprint-provider'));
assert.ok(!!model);
});
+
+ test('it has the correct provider title', function(assert) {
+ const store = this.owner.lookup('service:store');
+ const thesisCommons = store.createRecord('preprint-provider', { id: 'thesiscommons', name: 'Thesis Commons' });
+ assert.equal(thesisCommons.get('providerTitle'), 'Thesis Commons');
+
+ const osf = store.createRecord('preprint-provider', { id: 'osf', preprintWord: 'preprint' });
+ assert.equal(osf.get('providerTitle'), 'OSF Preprints');
+
+ const workrxiv = store.createRecord('preprint-provider', {
+ id: 'workrxiv', preprintWord: 'paper', name: 'WorkrXiv',
+ });
+ assert.equal(workrxiv.get('providerTitle'), 'WorkrXiv Papers');
+ });
});
diff --git a/translations/en-us.yml b/translations/en-us.yml
index ab9d841c5d0..05fe5917e01 100644
--- a/translations/en-us.yml
+++ b/translations/en-us.yml
@@ -1077,6 +1077,11 @@ collections:
accept: accepted
reject: rejected
remove: 'removed'
+preprints:
+ osf-title: 'OSF Preprints'
+ provider-title: '{name} {pluralizedPreprintWord}'
+ discover:
+ title: 'Search'
registries:
header:
osf_registrations: 'OSF Registrations'
From d258e21190d085bb8a68ffb5fd479e9f15f404db Mon Sep 17 00:00:00 2001
From: futa-ikeda <51409893+futa-ikeda@users.noreply.github.com>
Date: Tue, 18 Jul 2023 10:53:36 -0400
Subject: [PATCH 15/60] Track theme service properties (#1908)
---
app/services/theme.ts | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/app/services/theme.ts b/app/services/theme.ts
index dbbdcc33a3d..6777ed08bfd 100644
--- a/app/services/theme.ts
+++ b/app/services/theme.ts
@@ -2,6 +2,7 @@ import Store from '@ember-data/store';
import { assert } from '@ember/debug';
import { computed } from '@ember/object';
import Service, { inject as service } from '@ember/service';
+import { tracked } from '@glimmer/tracking';
import config from 'ember-get-config';
import Provider from 'ember-osf-web/models/provider';
@@ -36,13 +37,13 @@ const settings: { [P in ProviderType]: Setting } = {
export default class Theme extends Service {
@service store!: Store;
- id = defaultProvider;
- defaultProvider = defaultProvider;
+ @tracked id = defaultProvider;
+ @tracked defaultProvider = defaultProvider;
- providerType?: ProviderType;
+ @tracked providerType?: ProviderType;
// If we're using a provider domain
- isDomain = window.isProviderDomain;
+ @tracked isDomain = window.isProviderDomain;
// If we're using a branded provider
@computed('id')
From 691326b47c5fbd89c5800923ad61eea6cf9dd717 Mon Sep 17 00:00:00 2001
From: Ashley Robinson <82047646+chth0n1x@users.noreply.github.com>
Date: Tue, 1 Aug 2023 13:43:08 -0400
Subject: [PATCH 16/60] [ENG-4575] Institution dashboard page re-write (#1902)
- Ticket: https://openscience.atlassian.net/browse/ENG-4575
- Git branch: feature/search-institutions-rewrite
## Purpose
The purpose of these changes is to implement the new search page component for the Institutions Discover page.
## Summary of Changes
-Used the new search-page component to re-implement the Institutions dashboard/discover page
-Added institution header with description and banner for desktop, logo and description for mobile
-Added logic for institution colors and default OSF colors when none provided
-Added an affiliated institution filter applied by default to SHARE queries
---
app/institutions/dashboard/controller.ts | 2 +-
app/institutions/discover/controller.ts | 23 ++++++++
app/institutions/discover/template.hbs | 12 +++-
.../addon/components/search-page/styles.scss | 55 +++++++++++++++++++
.../addon/components/search-page/template.hbs | 32 ++++++++++-
.../acceptance/institutions/discover-test.ts | 49 +++++++++++++++++
translations/en-us.yml | 2 +
7 files changed, 170 insertions(+), 5 deletions(-)
create mode 100644 tests/acceptance/institutions/discover-test.ts
diff --git a/app/institutions/dashboard/controller.ts b/app/institutions/dashboard/controller.ts
index 796197bcef3..785f77100c5 100644
--- a/app/institutions/dashboard/controller.ts
+++ b/app/institutions/dashboard/controller.ts
@@ -1,6 +1,6 @@
+import { alias } from '@ember/object/computed';
import Controller from '@ember/controller';
import { computed } from '@ember/object';
-import { alias } from '@ember/object/computed';
import { inject as service } from '@ember/service';
import { InstitutionsDashboardModel } from 'ember-osf-web/institutions/dashboard/route';
diff --git a/app/institutions/discover/controller.ts b/app/institutions/discover/controller.ts
index d672a9de675..ed364e432ce 100644
--- a/app/institutions/discover/controller.ts
+++ b/app/institutions/discover/controller.ts
@@ -1,7 +1,30 @@
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
import CurrentUser from 'ember-osf-web/services/current-user';
+import { tracked } from '@glimmer/tracking';
+import { action } from '@ember/object';
+import pathJoin from 'ember-osf-web/utils/path-join';
+import config from 'ember-get-config';
export default class InstitutionDiscoverController extends Controller {
@service currentUser!: CurrentUser;
+
+ @tracked q?: string = '';
+ @tracked sort?: string = '-relevance';
+ @tracked resourceType?: string = 'All';
+
+ queryParams = ['q', 'page', 'sort', 'resourceType'];
+
+ get defaultQueryOptions() {
+ return {
+ publisher: pathJoin(config.OSF.url, 'institutions', this.model.id),
+ };
+ }
+
+ @action
+ onSearch(queryOptions: Record) {
+ this.q = queryOptions.q;
+ this.sort = queryOptions.sort;
+ this.resourceType = queryOptions.resourceType;
+ }
}
diff --git a/app/institutions/discover/template.hbs b/app/institutions/discover/template.hbs
index 01e9d8456cd..7a30ee06cb5 100644
--- a/app/institutions/discover/template.hbs
+++ b/app/institutions/discover/template.hbs
@@ -1 +1,11 @@
-{{!-- placeholder route template --}}
\ No newline at end of file
+
diff --git a/lib/osf-components/addon/components/search-page/styles.scss b/lib/osf-components/addon/components/search-page/styles.scss
index 8f12f2ef4e4..98c7080ae8a 100644
--- a/lib/osf-components/addon/components/search-page/styles.scss
+++ b/lib/osf-components/addon/components/search-page/styles.scss
@@ -39,6 +39,61 @@
}
}
+.heading-wrapper-institutions {
+ background-color: $color-shadow-gray-dark;
+ color: $color-text-gray-cool;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ padding-bottom: 0.5rem;
+
+ label {
+ max-width: 835px;
+ min-width: 250px;
+ width: 50vw;
+ padding-bottom: 0;
+
+ img {
+ max-height: 65px;
+ }
+
+ p {
+ font-weight: 400;
+ text-align: center;
+ padding: 1rem 0;
+ margin: 0;
+ }
+ }
+}
+
+.heading-wrapper-institutions-mobile {
+ padding-bottom: 0.5rem;
+
+ label {
+ padding-bottom: 0.5rem;
+ margin-bottom: 0;
+ width: 95%;
+
+ img {
+ margin: 0.5rem auto;
+ max-height: inherit;
+ }
+
+ p {
+ margin: 0 auto;
+ }
+ }
+
+ span {
+ padding: 0 1rem 1rem;
+ }
+
+ > button {
+ width: 100%;
+ margin-top: 0.5rem;
+ }
+}
+
.heading-label {
padding: 40px;
}
diff --git a/lib/osf-components/addon/components/search-page/template.hbs b/lib/osf-components/addon/components/search-page/template.hbs
index 48e94b066e9..5111099e2c7 100644
--- a/lib/osf-components/addon/components/search-page/template.hbs
+++ b/lib/osf-components/addon/components/search-page/template.hbs
@@ -3,8 +3,9 @@
...attributes
as |layout|>
{{#if @provider}}
{{html-safe @provider.description}}
+ {{else if @institution}}
+
+
+
{{else}}
{{/if}}
{{#if this.activeFilters.length}}
-
+
{{#each this.activeFilters as |filter|}}
-
{{!-- paginator --}}
{{#unless this.showSidePanelToggle}}
-
+
{{#if @showResourceTypeFilter}}