diff --git a/app/adapters/index-card-search.ts b/app/adapters/index-card-search.ts new file mode 100644 index 00000000000..c30f0c69128 --- /dev/null +++ b/app/adapters/index-card-search.ts @@ -0,0 +1,12 @@ +import ShareAdapter from './share-adapter'; +export default class IndexCardSearchAdapter extends ShareAdapter { + pathForType() { + return 'index-card-search'; + } +} + +declare module 'ember-data/types/registries/adapter' { + export default interface AdapterRegistry { + 'index-card-search': IndexCardSearchAdapter; + } // eslint-disable-line semi +} diff --git a/app/adapters/index-card.ts b/app/adapters/index-card.ts new file mode 100644 index 00000000000..3bd54888714 --- /dev/null +++ b/app/adapters/index-card.ts @@ -0,0 +1,13 @@ +import ShareAdapter from './share-adapter'; + +export default class IndexCardAdapter extends ShareAdapter { + pathForType() { + return 'index-card'; + } +} + +declare module 'ember-data/types/registries/adapter' { + export default interface AdapterRegistry { + 'index-card': IndexCardAdapter; + } // eslint-disable-line semi +} diff --git a/app/adapters/index-property-search.ts b/app/adapters/index-property-search.ts new file mode 100644 index 00000000000..9a69df75bfc --- /dev/null +++ b/app/adapters/index-property-search.ts @@ -0,0 +1,13 @@ +import ShareAdapter from './share-adapter'; + +export default class IndexPropertySearchAdapter extends ShareAdapter { + pathForType() { + return 'index-property-search'; + } +} + +declare module 'ember-data/types/registries/adapter' { + export default interface AdapterRegistry { + 'index-property-search': IndexPropertySearchAdapter; + } // eslint-disable-line semi +} diff --git a/app/adapters/index-value-search.ts b/app/adapters/index-value-search.ts new file mode 100644 index 00000000000..552cc0c5c29 --- /dev/null +++ b/app/adapters/index-value-search.ts @@ -0,0 +1,13 @@ +import ShareAdapter from './share-adapter'; + +export default class IndexValueSearchAdapter extends ShareAdapter { + pathForType() { + return 'index-value-search'; + } +} + +declare module 'ember-data/types/registries/adapter' { + export default interface AdapterRegistry { + 'index-value-search': IndexValueSearchAdapter; + } // 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..b8ce1c88dd6 --- /dev/null +++ b/app/adapters/share-adapter.ts @@ -0,0 +1,7 @@ +import JSONAPIAdapter from '@ember-data/adapter/json-api'; +import config from 'ember-get-config'; + +export default class ShareAdapter extends JSONAPIAdapter { + host = config.OSF.shareBaseUrl.replace(/\/$/, ''); // Remove trailing slash to avoid // in URLs + namespace = 'api/v3'; +} 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/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 new file mode 100644 index 00000000000..66502e85cbb --- /dev/null +++ b/app/institutions/discover/controller.ts @@ -0,0 +1,31 @@ +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'; +import { OnSearchParams, ResourceTypeFilterValue } from 'osf-components/components/search-page/component'; + +export default class InstitutionDiscoverController extends Controller { + @service currentUser!: CurrentUser; + + @tracked cardSearchText?: string = ''; + @tracked sort?: string = '-relevance'; + @tracked resourceType?: ResourceTypeFilterValue | null = null; + + queryParams = ['cardSearchText', 'page', 'sort', 'resourceType']; + + get defaultQueryOptions() { + return { + publisher: pathJoin(config.OSF.url, 'institutions', this.model.id), + }; + } + + @action + onSearch(queryOptions: OnSearchParams) { + this.cardSearchText = queryOptions.cardSearchText; + this.sort = queryOptions.sort; + this.resourceType = queryOptions.resourceType; + } +} 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..7a30ee06cb5 --- /dev/null +++ b/app/institutions/discover/template.hbs @@ -0,0 +1,11 @@ + diff --git a/app/models/index-card-search.ts b/app/models/index-card-search.ts new file mode 100644 index 00000000000..65bfa3b9e61 --- /dev/null +++ b/app/models/index-card-search.ts @@ -0,0 +1,28 @@ +import Model, { AsyncBelongsTo, AsyncHasMany, attr, belongsTo, hasMany } from '@ember-data/model'; + +import IndexPropertySearchModel from './index-property-search'; +import SearchResultModel from './search-result'; + +export interface SearchFilter { + propertyPath: string; + filterValue: string[]; + filterType?: string; +} + +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('index-property-search', { inverse: null }) + relatedPropertySearch!: AsyncBelongsTo & IndexPropertySearchModel; +} + +declare module 'ember-data/types/registries/model' { + export default interface ModelRegistry { + '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..175c3276548 --- /dev/null +++ b/app/models/index-card.ts @@ -0,0 +1,26 @@ +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[]; + // TODO: can we add a type for resourceMetadata? + @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/index-property-search.ts b/app/models/index-property-search.ts new file mode 100644 index 00000000000..c87fe60feb6 --- /dev/null +++ b/app/models/index-property-search.ts @@ -0,0 +1,21 @@ +import Model, { AsyncHasMany, attr, hasMany } from '@ember-data/model'; + +import { SearchFilter } from './index-card-search'; +import SearchResultModel from './search-result'; + +export default class IndexPropertySearchModel extends Model { + @attr('string') propertySearchText!: string; + @attr('array') propertySearchFilter!: SearchFilter[]; + @attr('string') cardSearchText!: string; + @attr('array') cardSearchFilter!: SearchFilter[]; + @attr('number') totalResultCount!: number; + + @hasMany('search-result', { inverse: null }) + searchResultPage!: AsyncHasMany & SearchResultModel[]; +} + +declare module 'ember-data/types/registries/model' { + export default interface ModelRegistry { + 'index-property-search': IndexPropertySearchModel; + } // eslint-disable-line semi +} diff --git a/app/models/index-value-search.ts b/app/models/index-value-search.ts new file mode 100644 index 00000000000..27f5c41dde7 --- /dev/null +++ b/app/models/index-value-search.ts @@ -0,0 +1,25 @@ +import Model, { AsyncHasMany, AsyncBelongsTo, attr, belongsTo, hasMany } from '@ember-data/model'; + +import IndexPropertySearchModel from './index-property-search'; +import { SearchFilter } from './index-card-search'; +import SearchResultModel from './search-result'; + +export default class IndexValueSearchModel extends Model { + @attr('string') valueSearchText!: string; + @attr('array') valueSearchFilter!: SearchFilter[]; + @attr('string') cardSearchText!: string; + @attr('array') cardSearchFilter!: SearchFilter[]; + @attr('number') totalResultCount!: number; + + @hasMany('search-result', { inverse: null }) + searchResultPage!: AsyncHasMany & SearchResultModel[]; + + @belongsTo('index-property-search', { inverse: null }) + relatedPropertySearch!: AsyncBelongsTo & IndexPropertySearchModel; +} + +declare module 'ember-data/types/registries/model' { + export default interface ModelRegistry { + 'index-value-search': IndexValueSearchModel; + } // eslint-disable-line semi +} diff --git a/app/models/preprint-provider.ts b/app/models/preprint-provider.ts index 7661f66766f..2f5e7702783 100644 --- a/app/models/preprint-provider.ts +++ b/app/models/preprint-provider.ts @@ -2,27 +2,30 @@ import { attr, hasMany, AsyncHasMany, belongsTo, AsyncBelongsTo } from '@ember-d import { 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 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'; +const { defaultProvider } = config; export default class PreprintProviderModel extends ProviderModel { @service intl!: Intl; + @attr('fixstring') email_support!: string | null; @attr('array') subjectsAcceptable!: string[]; @attr('array') additionalProviders!: string[]; @attr('string') shareSource!: string; @attr('string') preprintWord!: PreprintWord; // Reviews settings - @attr('array') permissions!: string[]; + @attr('array') permissions!: ReviewPermissions[]; @attr('boolean', { allowNull: true }) reviewsCommentsPrivate!: boolean | null; // Relationships @@ -49,6 +52,32 @@ export default class PreprintProviderModel extends ProviderModel { singularCapitalized: this.intl.t(`${documentType}.singularCapitalized`), }; } + + @computed('documentType.plural') + get searchPlaceholder(): string { + return this.intl.t('preprints.header.search_placeholder', + { placeholder: this.documentType.plural}); + } + + @computed('id') + get preprintWordInTitle() { + return this.id === 'thesiscommons'; + } + + // Is either OSF Preprints if provider is the default provider, + // name+preprintWord.pluralCapitalized(e.g.AfricArXiv Preprints or MarXiv Papers), or "Thesis Commons" + @computed('documentType.pluralCapitalized', 'id', 'name', 'preprintWordInTitle') + get providerTitle() { + if (this.id !== defaultProvider) { + if (this.preprintWordInTitle) { + return this.name; + } + return this.intl.t('preprints.provider-title', + { name: this.name, pluralizedPreprintWord: this.documentType.pluralCapitalized }); + } else { + return this.intl.t('preprints.header.osf_registrations'); + } + } } declare module 'ember-data/types/registries/model' { diff --git a/app/models/search-result.ts b/app/models/search-result.ts new file mode 100644 index 00000000000..4b7b9287c50 --- /dev/null +++ b/app/models/search-result.ts @@ -0,0 +1,228 @@ +import Model, { attr, belongsTo } from '@ember-data/model'; +import { inject as service } from '@ember/service'; +import { htmlSafe } from '@ember/template'; +import IntlService from 'ember-intl/services/intl'; + +import IndexCardModel from './index-card'; + +const textMatchEvidenceType = 'https://share.osf.io/vocab/2023/trove/TextMatchEvidence'; + +export interface IriMatchEvidence { + '@type': [string]; + matchingIri: string; + osfmapPropertyPath: string[]; +} + +export interface TextMatchEvidence { + '@type': [string]; + matchingHighlight: string; + osfmapPropertyPath: string[]; +} + +export default class SearchResultModel extends Model { + @service intl!: IntlService; + + @attr('array') matchEvidence!: Array; + @attr('number') recordResultCount!: number; + + @belongsTo('index-card', { inverse: null }) + indexCard!: IndexCardModel; + + get resourceMetadata() { + return this.indexCard.get('resourceMetadata'); + } + + // TODO: double check how matchEvidence works + get context() { + if (this.matchEvidence) { + const matchEvidenceString = this.matchEvidence.reduce( + (acc, current) => acc.concat( + `${current.osfmapPropertyPath[0]}: ${current['@type'][0] === textMatchEvidenceType + ? (current as TextMatchEvidence).matchingHighlight + : (current as IriMatchEvidence).matchingIri}; `, + ), + '', + ); + return htmlSafe(matchEvidenceString); + } + return null; + } + + get displayTitle() { + if (this.resourceType === 'user') { + return this.resourceMetadata['name'][0]['@value']; + } else if (this.resourceType === 'file') { + return this.resourceMetadata['fileName'][0]['@value']; + } + return this.resourceMetadata['title']?.[0]['@value']; + } + + get fileTitle() { + if (this.resourceType === 'file') { + return this.resourceMetadata.title?.[0]['@value']; + } + return null; + } + + get description() { + return this.resourceMetadata.description?.[0]?.['@value']; + } + + get absoluteUrl() { + return this.resourceMetadata['@id']; + } + + // returns list of contributors for osf objects + // returns list of affiliated institutions for osf users + get affiliatedEntities() { + if (this.resourceType === 'user') { + // return something + } else { + return this.resourceMetadata.creator?.map( (item:any) => item.name[0]['@value']); + } + } + + get dateFields() { + switch (this.resourceType) { + case 'user': + return []; + case 'registration': + case 'registration_component': + return [ + { + label: this.intl.t('osf-components.search-result-card.date_registered'), + date: this.resourceMetadata.dateCreated?.[0]['@value'], + }, + { + label: this.intl.t('osf-components.search-result-card.date_modified'), + date: this.resourceMetadata.dateModified?.[0]['@value'], + }, + ]; + default: + return [ + { + label: this.intl.t('osf-components.search-result-card.date_created'), + date: this.resourceMetadata.dateCreated?.[0]['@value'], + }, + { + label: this.intl.t('osf-components.search-result-card.date_modified'), + date: this.resourceMetadata.dateModified?.[0]['@value'], + }, + ]; + } + } + + get isPartOf() { + const isPartOf = this.resourceMetadata.isPartOf; + if (isPartOf) { + return { + title: this.resourceMetadata.isPartOf?.[0]?.title?.[0]?.['@value'], + absoluteUrl: this.resourceMetadata.isPartOf?.[0]?.['@id'], + }; + } + return null; + } + + get isPartOfCollection() { + const isPartOfCollection = this.resourceMetadata.isPartOfCollection; + if (isPartOfCollection) { + return { + title: this.resourceMetadata.isPartOfCollection?.[0]?.title?.[0]?.['@value'], + absoluteUrl: this.resourceMetadata.isPartOfCollection?.[0]?.['@id'], + }; + } + return null; + } + + get language() { + return this.resourceMetadata.language; + } + + get funders() { + if (this.resourceMetadata.funder) { + return this.resourceMetadata.funder.map( (item: any) => ({ + name: item.name[0]['@value'], + identifier: item.identifier?.[0]['@value'], + })); + } + return null; + } + + get provider() { + if (this.resourceMetadata.publisher) { + return { + name: this.resourceMetadata.publisher[0].name[0]['@value'], + identifier: this.resourceMetadata.publisher[0]['@id'], + }; + } + return null; + } + + get doi() { + return this.indexCard.get('resourceIdentifier').filter(id => id.includes('https://doi.org')); + } + + get license() { + if (this.resourceMetadata.rights) { + return { + name: this.resourceMetadata.rights?.[0].name[0]['@value'], + identifier: this.resourceMetadata.rights?.[0]['@id'], + }; + } + return null; + } + + get resourceType() { + const types = this.resourceMetadata.resourceType.map( (item: any) => item['@id']); + if (types.includes('Project')) { + return 'project'; + } else if (types.includes('Registration')) { + return 'registration'; + } else if (types.includes('Preprint')) { + return 'preprint'; + } else if (types.includes('ProjectComponent')) { + return 'project_component'; + } else if (types.includes('RegistrationComponent')) { + return 'registration_component'; + } else if (types.includes('Person')) { + return 'user'; + } else if(types.includes('File')) { + return 'file'; + } + return 'unknown'; + } + + get resourceNature() { + return this.resourceMetadata.resourceNature?.[0]['@value']; + } + + get hasDataResource() { + return this.resourceMetadata.hasDataResource; + } + + get hasAnalyticCodeResource() { + return this.resourceMetadata.hasAnalyticCodeResource; + } + + get hasMaterialsResource() { + return this.resourceMetadata.hasMaterialsResource; + } + + get hasPapersResource() { + return this.resourceMetadata.hasPapersResource; + } + + get hasSupplementalResource() { + return this.resourceMetadata.hasSupplementalResource; + } + + get registrationTemplate() { + return this.resourceMetadata['https://osf.io/vocab/2022/registration_type']?.[0]?.['@value']; + } +} + +declare module 'ember-data/types/registries/model' { + export default interface ModelRegistry { + 'search-result': SearchResultModel; + } // eslint-disable-line semi +} diff --git a/app/preprints/-components/branded-footer/component.ts b/app/preprints/-components/branded-footer/component.ts new file mode 100644 index 00000000000..e2119aa03eb --- /dev/null +++ b/app/preprints/-components/branded-footer/component.ts @@ -0,0 +1,13 @@ +import Component from '@glimmer/component'; + +interface InputArgs { + footerLinks: string; +} + +export default class BrandedFooter extends Component { + footerLinks = this.args.footerLinks; + + get hasFooters(): boolean { + return this.footerLinks !== ''; + } +} diff --git a/app/preprints/-components/branded-footer/styles.scss b/app/preprints/-components/branded-footer/styles.scss new file mode 100644 index 00000000000..95d12664ffb --- /dev/null +++ b/app/preprints/-components/branded-footer/styles.scss @@ -0,0 +1,18 @@ +// stylelint-disable max-nesting-depth, selector-max-compound-selectors + +.branded-footer-links { + width: 100%; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: flex-start; + margin: 2.5em 0; + + .social > a { + margin: 0 4px; + } + + .social > a:hover { + text-decoration: none; + } +} diff --git a/app/preprints/-components/branded-footer/template.hbs b/app/preprints/-components/branded-footer/template.hbs new file mode 100644 index 00000000000..ffee3b5b5a9 --- /dev/null +++ b/app/preprints/-components/branded-footer/template.hbs @@ -0,0 +1,5 @@ +{{#if this.hasFooters}} +
+ {{html-safe this.footerLinks}} +
+{{/if}} \ No newline at end of file diff --git a/app/preprints/-components/taxonomy-top-list/component.ts b/app/preprints/-components/taxonomy-top-list/component.ts new file mode 100644 index 00000000000..7928328c241 --- /dev/null +++ b/app/preprints/-components/taxonomy-top-list/component.ts @@ -0,0 +1,71 @@ +import Component from '@glimmer/component'; +import SubjectModel from 'ember-osf-web/models/subject'; +import Media from 'ember-responsive'; +import { inject as service } from '@ember/service'; + +interface InputArgs { + list: SubjectModel[]; + provider: string; +} + +interface PairModel { + queryParam: string; + text: string; +} + +export default class TaxonomyTopList extends Component { + @service media!: Media; + + provider = this.args.provider; + + get isMobile(): boolean { + return this.media.isMobile; + } + + get sortedList() { + if (!this.args.list) { + return; + } + const sortedList = this.args.list.sortBy('text'); + const pairedList = [] as PairModel[][]; + + if (this.isMobile) { + for (let i = 0; i < sortedList.get('length'); i += 1) { + const pair: PairModel[] = []; + const subject= sortedList.objectAt(i) as SubjectModel; + pair.pushObject({ + queryParam: subject?.taxonomyName, + text: subject?.text, + } as PairModel); + pairedList.pushObject(pair); + } + } else { + for (let i = 0; i < sortedList.get('length'); i += 2) { + const pair: PairModel[] = []; + // path in pair needs to be a list because that's what the + // subject param in the discover controller is expecting + const subjectOdd = sortedList.objectAt(i) as SubjectModel; + pair.pushObject({ + queryParam: subjectOdd?.taxonomyName, + text: subjectOdd?.text, + } as PairModel); + + if (sortedList.objectAt(i + 1)) { + const subjectEven = sortedList.objectAt(i + 1) as SubjectModel; + pair.pushObject({ + queryParam: subjectEven?.taxonomyName, + text: subjectEven?.text, + }); + } + pairedList.pushObject(pair); + } + + } + + if (pairedList.length > 0 && typeof this.args.provider !== 'string') { + throw new Error('A provider string must be provided with a valid list'); + } + + return pairedList; + } +} diff --git a/app/preprints/-components/taxonomy-top-list/styles.scss b/app/preprints/-components/taxonomy-top-list/styles.scss new file mode 100644 index 00000000000..4991ca1c5bf --- /dev/null +++ b/app/preprints/-components/taxonomy-top-list/styles.scss @@ -0,0 +1,57 @@ +// stylelint-disable max-nesting-depth, selector-max-compound-selectors + +.subject-container { + width: 100%; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 10px; + + .subject-item { + width: calc(50% - 20px); + + .btn { + display: inline-block; + margin-bottom: 0; + font-weight: 400; + text-align: center; + white-space: nowrap; + touch-action: manipulation; + cursor: pointer; + background-image: none; + border: 1px solid transparent; + border-radius: 2px; + padding: 6px 12px; + font-size: 14px; + line-height: 1.42857; + user-select: none; + vertical-align: middle; + color: $color-text-white; + } + + .subject-button { + width: 100%; + font-size: 17px; + overflow: hidden; + text-overflow: ellipsis; + background-color: var(--primary-color); + color: var(--secondary-color); + border: 1px solid var(--secondary-color); + + &:hover { + color: var(--primary-color); + background-color: var(--secondary-color); + border: 1px solid var(--primary-color); + } + } + } + + &.mobile { + flex-direction: column; + + .subject-item { + width: 100%; + } + } +} diff --git a/app/preprints/-components/taxonomy-top-list/template.hbs b/app/preprints/-components/taxonomy-top-list/template.hbs new file mode 100644 index 00000000000..d49088def37 --- /dev/null +++ b/app/preprints/-components/taxonomy-top-list/template.hbs @@ -0,0 +1,16 @@ +{{#each this.sortedList as |pair|}} +
+ {{#each pair as |subject|}} +
+ + {{subject.text}} + +
+ {{/each}} +
+{{/each}} diff --git a/app/preprints/controller.ts b/app/preprints/controller.ts new file mode 100644 index 00000000000..77d77211532 --- /dev/null +++ b/app/preprints/controller.ts @@ -0,0 +1,9 @@ + +import Controller from '@ember/controller'; +import { inject as service } from '@ember/service'; + +import Theme from 'ember-osf-web/services/theme'; + +export default class PreprintController extends Controller { + @service theme!: Theme; +} diff --git a/app/preprints/discover/controller.ts b/app/preprints/discover/controller.ts new file mode 100644 index 00000000000..20563261c84 --- /dev/null +++ b/app/preprints/discover/controller.ts @@ -0,0 +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'; +import { OnSearchParams } from 'osf-components/components/search-page/component'; + +export default class PreprintDiscoverController extends Controller { + @service store!: Store; + @service theme!: Theme; + + @tracked cardSearchText?: string = ''; + @tracked sort?: string = '-relevance'; + + queryParams = ['cardSearchText', 'page', 'sort']; + + get defaultQueryOptions() { + return { + // TODO: get this from the API? + publisher: pathJoin(config.OSF.url, 'preprints', this.theme.id), + }; + } + + @action + onSearch(queryOptions: OnSearchParams) { + this.cardSearchText = queryOptions.cardSearchText; + this.sort = queryOptions.sort; + } +} diff --git a/app/preprints/discover/route.ts b/app/preprints/discover/route.ts new file mode 100644 index 00000000000..529014833ca --- /dev/null +++ b/app/preprints/discover/route.ts @@ -0,0 +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) { + 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 new file mode 100644 index 00000000000..6a0bc4bc5cc --- /dev/null +++ b/app/preprints/discover/template.hbs @@ -0,0 +1,14 @@ +{{page-title (t 'preprints.discover.title')}} +
+ +
diff --git a/app/preprints/index/controller.ts b/app/preprints/index/controller.ts new file mode 100644 index 00000000000..8f6b536033a --- /dev/null +++ b/app/preprints/index/controller.ts @@ -0,0 +1,42 @@ +import Store from '@ember-data/store'; +import Controller from '@ember/controller'; +import { action } from '@ember/object'; +import RouterService from '@ember/routing/router-service'; +import { inject as service } from '@ember/service'; +import Theme from 'ember-osf-web/services/theme'; +import Media from 'ember-responsive'; +import Intl from 'ember-intl/services/intl'; + +export default class Preprints extends Controller { + @service store!: Store; + @service theme!: Theme; + @service router!: RouterService; + @service media!: Media; + @service intl!: Intl; + + get isMobile(): boolean { + return this.media.isMobile; + } + + get isOsf(): boolean { + return this.theme?.provider?.id === 'osf'; + } + + @action + onSearch(query: string) { + let route = 'search'; + + if (this.theme.isSubRoute) { + route = 'provider.discover'; + } + + this.router.transitionTo(route, { queryParams: { q: query } }); + } + + get supportEmail(): string { + const { isProvider, provider } = this.theme; + + // eslint-disable-next-line max-len + return `mailto:${isProvider && provider && provider.emailSupport ? provider.emailSupport : this.intl.t('contact.email')}`; + } +} diff --git a/app/preprints/index/route.ts b/app/preprints/index/route.ts new file mode 100644 index 00000000000..75cc66f5114 --- /dev/null +++ b/app/preprints/index/route.ts @@ -0,0 +1,49 @@ +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'; +import captureException from 'ember-osf-web/utils/capture-exception'; + +/** + * Loads all disciplines and preprint providers to the index page + * @class Index Route Handler + */ +export default class Preprints extends Route { + @service store!: Store; + @service theme!: Theme; + @service router!: RouterService; + + async model(params: { provider_id : string }) { + try { + const provider_id = params.provider_id ? params.provider_id : 'osf'; + + const provider = await this.store.findRecord('preprint-provider', provider_id, {include: 'brand'}); + this.theme.set('providerType', 'preprint'); + this.theme.set('id', provider_id); + + const taxonomies = await this.theme.provider?.queryHasMany('highlightedSubjects', { + page: { + size: 20, + }, + }); + + const brandedProviders = this.theme.id === 'osf' ? await this.store + .findAll('preprint-provider', { reload: true }) + .then(result => result + .filter(item => item.id !== 'osf')) : []; + + return { + provider, + taxonomies, + brandedProviders, + brand: provider.brand.content, + }; + + } catch (error) { + captureException(error); + this.router.transitionTo('not-found', 'preprints'); + return null; + } + } +} diff --git a/app/preprints/index/styles.scss b/app/preprints/index/styles.scss new file mode 100644 index 00000000000..e90e5bd0d33 --- /dev/null +++ b/app/preprints/index/styles.scss @@ -0,0 +1,213 @@ +// stylelint-disable max-nesting-depth, selector-max-compound-selectors + +.preprints-page-container { + width: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + .preprint-header { + border-bottom: 1px solid var(--primary-color); + width: 100%; + background-color: var(--secondary-color); + color: var(--primary-color) !important; + + a:not(.btn) { + color: var(--primary-color); + text-decoration: underline; + } + + .description { + color: var(--primary-color); + } + + .submit-container { + width: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + color: var(--primary-color); + + .or-container { + font-size: 21px; + margin-bottom: 20px; + font-weight: 300; + line-height: 1.4; + color: $color-text-white; + + width: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + } + + .example-container { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + margin-top: 25px; + } + } + } + + /* Subject Panel */ + .preprint-subjects-container { + background-color: $color-bg-gray-lighter; + border-bottom: 1px solid #6d8a98; + padding: 15px 0; + width: 100%; + flex-direction: column; + display: flex; + justify-content: center; + align-items: center; + + .preprint-subjects-inner-container { + background-color: $color-bg-gray-lighter; + padding: 0 30px; + width: calc(100% - 60px); + flex-direction: column; + display: flex; + justify-content: flex-start; + align-items: flex-start; + + .subject-list-container { + padding: 25px; + width: 100%; + flex-direction: column; + display: flex; + justify-content: flex-start; + align-items: flex-start; + } + } + } + + /* Preprint providers and become provider */ + .preprint-tool-container { + background: var(--hero-background-img-url); + color: $color-text-white; + text-shadow: 0 0 5px #506069; + background-size: cover; + padding: 15px 0; + min-width: 100%; + min-height: 100%; + width: 100%; + flex-direction: column; + display: flex; + justify-content: center; + align-items: center; + + .preprint-tool-inner-container { + padding-right: 30px; + padding-left: 30px; + width: calc(100% - 60px); + flex-direction: column; + display: flex; + justify-content: flex-start; + align-items: flex-start; + + .subtitle { + margin-bottom: 25px; + } + + .preprint-contact-container, + .preprint-logos-container { + padding: 25px; + width: 100%; + flex-direction: row; + flex-wrap: wrap; + display: flex; + justify-content: center; + align-items: flex-start; + + .provider-logo { + width: 150px; + height: 75px; + background-size: contain; + background-repeat: no-repeat; + background-position: center; + margin: 15px; + } + } + + .preprint-contact-container { + flex-direction: column; + align-items: center; + + .lead { + font-size: 21px; + line-height: 1.4; + font-weight: 300; + padding-bottom: 10px; + text-align: center; + + .links { + color: $color-text-white; + text-decoration: underline; + } + } + + .contact { + margin-top: 20px; + } + } + } + } + + .osf-preprint-advisory-container { + background-image: none; + } + + &.mobile { + .preprint-subjects-inner-container { + padding: 0; + background-color: $color-bg-gray-lighter; + padding: 0, 10px; + width: calc(100% - 20px); + + .subject-list-container { + padding: 25px 0; + width: 100%; + flex-direction: column; + } + } + + .preprint-tool-inner-container { + padding: 15px 10px; + width: calc(100% - 20px); + } + } + + .btn { + display: inline-block; + margin-bottom: 0; + font-weight: 400; + text-align: center; + white-space: nowrap; + touch-action: manipulation; + cursor: pointer; + background-image: none; + border: 1px solid transparent; + border-radius: 2px; + padding: 6px 12px; + font-size: 14px; + line-height: 1.42857; + user-select: none; + vertical-align: middle; + color: $color-text-white; + } + + .btn-success { + background-color: #357935; + border-color: #2d672d; + } + + .btn-lg { + padding: 10px 16px; + font-size: 18px; + line-height: 1.33333; + } +} diff --git a/app/preprints/index/template.hbs b/app/preprints/index/template.hbs new file mode 100644 index 00000000000..57c90a2a5a3 --- /dev/null +++ b/app/preprints/index/template.hbs @@ -0,0 +1,173 @@ +
+ {{!HEADER}} +
+ + {{#branded-header.lead}} +
+ {{html-safe this.theme.provider.description}} +
+
+ + {{t 'preprints.header.powered_by'}} + +
+ {{/branded-header.lead}} + {{#branded-header.row}} + {{#if this.theme.provider.allowSubmissions}} +
+
{{t 'preprints.header.or'}}
+ + {{t 'preprints.header.submit_label' documentType=this.theme.provider.content.documentType}} + +
+ + {{t 'preprints.header.example'}} + +
+
+ {{/if}} + {{/branded-header.row}} +
+
+ {{!END HEADER}} + + {{!SUBJECTS}} +
+
+

{{#if this.theme.provider.additionalProviders}} + {{t 'preprints.subjects.heading.provider'}} + {{else}} + {{if this.theme.provider.hasHighlightedSubjects (t 'preprints.subjects.heading.hasHighlightedSubjects') (t 'preprints.subjects.heading.noHighlightedSubjects')}} + {{/if}} +

+ {{#if this.theme.provider.hasHighlightedSubjects}} + + {{t 'preprints.subjects.links.seeAllSubjects'}} + + {{/if}} +
+ {{#if this.theme.provider.additionalProviders}} + {{additional-provider-list additionalProviders=this.theme.provider.additionalProviders}} + {{else}} + + {{/if}} +
+
+
+ + {{#unless this.theme.isProvider}} + {{!SERVICES}} +
+
+

{{t 'preprints.services.top.heading' documentType=this.theme.provider.documentType.singularCapitalized}}

+

+ {{t 'preprints.services.top.paragraph' documentType=this.theme.provider.documentType.singular}} +

+
+ {{#each this.model.brandedProviders as |provider| }} + {{#if (not-eq provider.id 'livedata') }} + + {{/if}} + {{/each}} +
+
+
+ {{t 'preprints.services.bottom.p1' documentType=this.theme.provider.documentType.singular}} +
+
+ {{t 'preprints.services.bottom.div.line1'}} + + + {{t 'preprints.services.bottom.div.linkText1'}} + + + {{t 'preprints.services.bottom.div.line2'}} + + + {{t 'preprints.services.bottom.div.linkText2'}} + + + {{t 'preprints.services.bottom.div.line3'}} +
+
+ + {{t 'preprints.services.bottom.contact'}} + +
+
+
+
+ {{/unless}} + + + {{!ADVISORY GROUP}} + {{#if this.theme.provider.advisoryBoard.length}} +
+
+ {{html-safe this.theme.provider.advisoryBoard}} +
+
+ {{/if}} + +
+ +
+
+{{!END INDEX}} diff --git a/app/preprints/route.ts b/app/preprints/route.ts new file mode 100644 index 00000000000..d72fd19a9e2 --- /dev/null +++ b/app/preprints/route.ts @@ -0,0 +1,8 @@ +import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; + +import Theme from 'ember-osf-web/services/theme'; + +export default class Preprints extends Route { + @service theme!: Theme; +} diff --git a/app/preprints/template.hbs b/app/preprints/template.hbs new file mode 100644 index 00000000000..5c74b906e29 --- /dev/null +++ b/app/preprints/template.hbs @@ -0,0 +1,12 @@ +{{page-title this.theme.provider.providerTitle replace=true}} +{{#if this.theme.isProvider}} +
+ + +
+{{/if}} +{{outlet}} diff --git a/app/router.ts b/app/router.ts index 06be6e4c76a..38099a6c7fa 100644 --- a/app/router.ts +++ b/app/router.ts @@ -20,9 +20,18 @@ Router.map(function() { this.route('home', { path: '/' }); this.route('dashboard'); 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('preprints', function() { + this.route('index', { path: '/' }); + this.route('index', { path: '/:provider_id' }); + this.route('discover', { path: '/:provider_id/discover' }); + }); + this.route('register'); this.route('settings', function() { this.route('profile', function() { diff --git a/app/search/controller.ts b/app/search/controller.ts new file mode 100644 index 00000000000..175d12a00eb --- /dev/null +++ b/app/search/controller.ts @@ -0,0 +1,19 @@ +import Controller from '@ember/controller'; +import { action } from '@ember/object'; +import { tracked } from '@glimmer/tracking'; +import { OnSearchParams, ResourceTypeFilterValue } from 'osf-components/components/search-page/component'; + +export default class SearchController extends Controller { + @tracked cardSearchText?: string = ''; + @tracked sort?: string = '-relevance'; + @tracked resourceType?: ResourceTypeFilterValue | null = null; + + queryParams = ['cardSearchText', 'page', 'sort', 'resourceType']; + + @action + onSearch(queryOptions: OnSearchParams) { + this.cardSearchText = queryOptions.cardSearchText; + this.sort = queryOptions.sort; + this.resourceType = queryOptions.resourceType; + } +} diff --git a/lib/registries/addon/discover/route.ts b/app/search/route.ts similarity index 65% rename from lib/registries/addon/discover/route.ts rename to app/search/route.ts index f0755af90c5..0a451bd304d 100644 --- a/lib/registries/addon/discover/route.ts +++ b/app/search/route.ts @@ -1,11 +1,10 @@ import Route from '@ember/routing/route'; -export default class RegistriesDiscoverRoute extends Route { +export default class Search extends Route { buildRouteInfoMetadata() { return { osfMetrics: { isSearch: true, - providerId: 'osf', }, }; } diff --git a/app/search/template.hbs b/app/search/template.hbs new file mode 100644 index 00000000000..74ccec30553 --- /dev/null +++ b/app/search/template.hbs @@ -0,0 +1,11 @@ +{{page-title (t 'search.page-title')}} + + diff --git a/app/serializers/index-card-search.ts b/app/serializers/index-card-search.ts new file mode 100644 index 00000000000..5b86f0c5600 --- /dev/null +++ b/app/serializers/index-card-search.ts @@ -0,0 +1,10 @@ +import ShareSerializer from './share-serializer'; + +export default class IndexCardSearchSerializer extends ShareSerializer { +} + +declare module 'ember-data/types/registries/serializer' { + export default interface SerializerRegistry { + 'index-card-search': IndexCardSearchSerializer; + } // eslint-disable-line semi +} diff --git a/app/serializers/index-card.ts b/app/serializers/index-card.ts new file mode 100644 index 00000000000..9b5b13575f5 --- /dev/null +++ b/app/serializers/index-card.ts @@ -0,0 +1,10 @@ +import ShareSerializer from './share-serializer'; + +export default class IndexCardSerializer extends ShareSerializer { +} + +declare module 'ember-data/types/registries/serializer' { + export default interface SerializerRegistry { + 'index-card': IndexCardSerializer; + } // eslint-disable-line semi +} diff --git a/app/serializers/index-property-search.ts b/app/serializers/index-property-search.ts new file mode 100644 index 00000000000..22707f3206d --- /dev/null +++ b/app/serializers/index-property-search.ts @@ -0,0 +1,10 @@ +import ShareSerializer from './share-serializer'; + +export default class IndexPropertySearchSerializer extends ShareSerializer { +} + +declare module 'ember-data/types/registries/serializer' { + export default interface SerializerRegistry { + 'index-property-search': IndexPropertySearchSerializer; + } // eslint-disable-line semi +} diff --git a/app/serializers/index-value-search.ts b/app/serializers/index-value-search.ts new file mode 100644 index 00000000000..fca2709c0f7 --- /dev/null +++ b/app/serializers/index-value-search.ts @@ -0,0 +1,10 @@ +import ShareSerializer from './share-serializer'; + +export default class IndexValueSearchSerializer extends ShareSerializer { +} + +declare module 'ember-data/types/registries/serializer' { + export default interface SerializerRegistry { + 'index-value-search': IndexValueSearchSerializer; + } // 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/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') diff --git a/app/styles/_preprint.scss b/app/styles/_preprint.scss new file mode 100644 index 00000000000..bb1767500fc --- /dev/null +++ b/app/styles/_preprint.scss @@ -0,0 +1,67 @@ +// stylelint-disable max-nesting-depth, selector-max-compound-selectors + +.preprint-advisory-container { + background: var(--hero-background-img-url); + background-color: $color-bg-gray-lighter; + border-bottom: 1px solid #6d8a98; + padding: 15px 0; + width: 100%; + flex-direction: column; + display: flex; + justify-content: center; + align-items: center; + + .preprint-advisory-inner-container { + padding: 0 30px; + width: calc(100% - 60px); + flex-direction: column; + display: flex; + justify-content: flex-start; + align-items: flex-start; + + .preprint-advisory-header { + width: 100%; + } + + .preprint-advisory-list { + width: 100%; + flex-direction: row; + display: flex; + justify-content: flex-start; + align-items: flex-start; + margin-top: 25px; + + .preprint-advisory-list-column { + width: 50%; + + ul { + list-style: none; + + li { + padding-top: 5px; + font-size: 16px; + } + } + } + } + } + + &.mobile { + .preprint-advisory-inner-container { + padding: 0 10px; + width: calc(100% - 20px); + + .preprint-advisory-list { + flex-direction: column; + + .preprint-advisory-list-column { + width: 100%; + + ul { + padding: 0; + } + } + } + } + } +} diff --git a/app/styles/headers.scss b/app/styles/headers.scss index ce654bede9b..5663ca4d799 100644 --- a/app/styles/headers.scss +++ b/app/styles/headers.scss @@ -19,6 +19,9 @@ // Branding @import 'branding'; +// Preprints +@import 'preprint'; + .theme-dropdown .dropdown-menu { position: static; display: block; diff --git a/lib/app-components/addon/components/branded-navbar/component.ts b/lib/app-components/addon/components/branded-navbar/component.ts index b4695b5b57e..286b82d4d43 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,20 @@ 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 BrandModel from 'ember-osf-web/models/brand'; +import PreprintProviderModel from 'ember-osf-web/models/preprint-provider'; +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 +33,7 @@ export default class BrandedNavbar extends Component { @service media!: Media; @service session!: Session; @service theme!: Theme; + @service currentUser!: CurrentUserService; brandRoute!: string; objectType!: ObjectType; @@ -35,20 +44,41 @@ 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; + @alias('theme.provider.brand.primaryColor') brandPrimaryColor!: BrandModel; - @computed('intl.locale', 'theme.provider', 'translateKey') - get brandTitle(): string { - const { name } = this.theme.provider!; + get useWhiteBackground(): boolean { + const { provider } = this.theme; + if (provider) { + return this.theme.providerType === 'preprint' && ['biohackrxiv', 'nutrixiv'].includes(provider.id); + } + return false; + } - return this.intl.t(this.translateKey, { name }); + @computed('intl.locale', 'theme.{providerType,provider.providerTitle}', 'translateKey') + get brandTitle(): string { + if (this.theme.providerType === 'collection') { + const { name } = this.theme.provider!; + return this.intl.t(this.translateKey, { name }); + } else { // preprint + return (this.theme.provider as PreprintProviderModel).providerTitle; + } } @requiredAction loginAction!: () => void; @action toggleSecondaryNavigation() { - this.toggleProperty('showNavLinks'); + this.showNavLinks = !this.showNavLinks; } get isMobileOrTablet() { diff --git a/lib/app-components/addon/components/branded-navbar/styles.scss b/lib/app-components/addon/components/branded-navbar/styles.scss index 8653354bd74..53ae3242693 100644 --- a/lib/app-components/addon/components/branded-navbar/styles.scss +++ b/lib/app-components/addon/components/branded-navbar/styles.scss @@ -63,6 +63,9 @@ background-position-y: center; background-size: contain; background-repeat: no-repeat; + // Preprints uses the brand's navbar logo + // Collections uses the theme-styles component and will overwrite this + background-image: var(--navbar-logo-img-url); } .NavBarBuffer { @@ -103,3 +106,38 @@ right: 0; } } + +// preprint-branded navbar uses brand relationship and not custom CSS +// Extra specificity needed to override the default styles set in app/styles/_components.scss +// branded-navbar dark/light text only used for preprint providers, as collection providers currently use custom CSS +.preprint-branded-navbar.preprint-branded-navbar { + background-color: var(--primary-color); + background-image: none; + + .secondary-navigation { + border-color: transparent; + } + + &.light-text { + a, + :global(.secondary-nav-dropdown) { + color: $color-text-white !important; + } + } + + &.dark-text { + a, + :global(.secondary-nav-dropdown) { + color: $color-text-black !important; + } + } +} + +.white-background-branded-navbar.white-background-branded-navbar.white-background-branded-navbar { + background-color: #fff; + + a, + :global(.secondary-nav-dropdown) { + color: $color-text-black !important; + } +} diff --git a/lib/app-components/addon/components/branded-navbar/template.hbs b/lib/app-components/addon/components/branded-navbar/template.hbs index c9db3e9233a..fcf8a480c0e 100644 --- a/lib/app-components/addon/components/branded-navbar/template.hbs +++ b/lib/app-components/addon/components/branded-navbar/template.hbs @@ -1,6 +1,9 @@
diff --git a/lib/registries/addon/components/registries-registration-type-facet/component.ts b/lib/registries/addon/components/registries-registration-type-facet/component.ts deleted file mode 100644 index a4c55fd366e..00000000000 --- a/lib/registries/addon/components/registries-registration-type-facet/component.ts +++ /dev/null @@ -1,117 +0,0 @@ -import Store from '@ember-data/store'; -import EmberArray, { A } from '@ember/array'; -import Component from '@ember/component'; -import { action, computed } from '@ember/object'; -import { inject as service } from '@ember/service'; -import { waitFor } from '@ember/test-waiters'; -import { task } from 'ember-concurrency'; -import Features from 'ember-feature-flags/services/features'; -import Intl from 'ember-intl/services/intl'; -import Toast from 'ember-toastr/services/toast'; - -import { layout, requiredAction } from 'ember-osf-web/decorators/component'; -import RegistrationProviderModel from 'ember-osf-web/models/registration-provider'; -import Analytics from 'ember-osf-web/services/analytics'; -import captureException, { getApiErrorMessage } from 'ember-osf-web/utils/capture-exception'; - -import registriesConfig from 'registries/config/environment'; -import { SearchOptions } from 'registries/services/search'; -import { ShareTermsFilter } from 'registries/services/share-search'; -import template from './template'; - -@layout(template) -export default class RegistriesRegistrationTypeFacet extends Component { - @service intl!: Intl; - @service toast!: Toast; - @service store!: Store; - @service analytics!: Analytics; - @service features!: Features; - - searchOptions!: SearchOptions; - provider?: RegistrationProviderModel; - @requiredAction onSearchOptionsUpdated!: (options: SearchOptions) => void; - - registrationTypes: EmberArray = A([]); - - @task({ on: 'didReceiveAttrs' }) - @waitFor - async fetchRegistrationTypes() { - const { defaultProviderId } = registriesConfig; - - try { - if (!this.provider){ - this.provider = await this.store.findRecord('registration-provider', defaultProviderId); - } - const metaschemas = await this.provider.queryHasMany('schemas', { - 'page[size]': 100, - }); - const metaschemaNames = metaschemas.mapBy('name'); - if (this.provider.id === defaultProviderId) { - metaschemaNames.push( - // Manually add 'Election Research Preacceptance Competition' to the list of possible - // facets. Metaschema was removed from the API as a possible registration type - // but should still be searchable - 'Election Research Preacceptance Competition', - ); - } - this.set('registrationTypes', A(metaschemaNames.sort())); - } catch (e) { - const errorMessage = this.intl.t('registries.facets.registration_type.registration_schema_error'); - captureException(e, { errorMessage }); - this.toast.error(getApiErrorMessage(e), errorMessage); - throw e; - } - } - - get title() { - return this.intl.t('registries.facets.registration_type.title'); - } - - @computed('searchOptions.filters') - get onlyOSF() { - const osfSelected = this.searchOptions.filters.find( - item => item instanceof ShareTermsFilter - && item.key === 'sources' - && item.value === 'OSF Registries', - ); - return this.searchOptions.filters.filter(filter => filter.key === 'sources').size === 1 && osfSelected; - } - - @computed('registrationTypes', 'searchOptions.filters') - get types() { - return this.registrationTypes.map(name => { - const filter = new ShareTermsFilter('registration_type', name, name); - - return { - name, - filter, - checked: this.searchOptions.filters.contains(filter), - }; - }); - } - - @action - typeChecked(filter: ShareTermsFilter, checked: boolean) { - if (!this.onlyOSF) { - return undefined; - } - - if (this.provider) { - this.analytics.track( - 'filter', - checked - ? 'remove' - : 'add', - `Discover - type ${filter.display} ${this.provider.name}`, - ); - } else { - this.analytics.track('filter', checked ? 'remove' : 'add', `Discover - type ${filter.display}`); - } - - if (checked) { - return this.onSearchOptionsUpdated(this.searchOptions.removeFilters(filter)); - } - - return this.onSearchOptionsUpdated(this.searchOptions.addFilters(filter)); - } -} diff --git a/lib/registries/addon/components/registries-registration-type-facet/styles.scss b/lib/registries/addon/components/registries-registration-type-facet/styles.scss deleted file mode 100644 index 863a0af9bde..00000000000 --- a/lib/registries/addon/components/registries-registration-type-facet/styles.scss +++ /dev/null @@ -1,54 +0,0 @@ -.RegistrationType { - opacity: 1; - animation: enable 0.5s ease-in-out; -} - -@keyframes disable { - 0% { - opacity: 1; - } - - 100% { - opacity: 0.5; - } -} - -@keyframes enable { - 0% { - opacity: 0.5; - } - - 100% { - opacity: 1; - } -} - -.RegistrationType__Warning { - font-weight: 700; - color: #000; - padding-left: 0; - display: block; -} - -.RegistrationType__List { - list-style: none; - padding: 0; -} - -.RegistrationType__Item { - label { - font-weight: 400; - - &:hover { - color: var(--primary-color); - } - } -} - -.RegistrationType__Filter { - background-color: #d7e6e9; -} - -.m-t-sm { - margin-top: 10px; -} diff --git a/lib/registries/addon/components/registries-registration-type-facet/template.hbs b/lib/registries/addon/components/registries-registration-type-facet/template.hbs deleted file mode 100644 index ac7e5de9f6f..00000000000 --- a/lib/registries/addon/components/registries-registration-type-facet/template.hbs +++ /dev/null @@ -1,25 +0,0 @@ -{{#if this.onlyOSF}} - {{#registries-search-facet-container title=this.title}} -
- - {{t 'registries.facets.registration_type.only_available_with_osf'}} - - -
    - {{#each this.types as |type index|}} -
  • - -
  • - {{/each}} -
-
- {{/registries-search-facet-container}} -{{/if}} diff --git a/lib/registries/addon/components/registries-search-facet-container/component.ts b/lib/registries/addon/components/registries-search-facet-container/component.ts deleted file mode 100644 index 38b3ccea31c..00000000000 --- a/lib/registries/addon/components/registries-search-facet-container/component.ts +++ /dev/null @@ -1,11 +0,0 @@ -import Component from '@ember/component'; -import { localClassNames } from 'ember-css-modules'; - -import { layout } from 'ember-osf-web/decorators/component'; -import template from './template'; - -@layout(template) -@localClassNames('SearchFacet') -export default class SearchFacetContainer extends Component { - title!: string; -} diff --git a/lib/registries/addon/components/registries-search-facet-container/styles.scss b/lib/registries/addon/components/registries-search-facet-container/styles.scss deleted file mode 100644 index 479fa378dcb..00000000000 --- a/lib/registries/addon/components/registries-search-facet-container/styles.scss +++ /dev/null @@ -1,14 +0,0 @@ -.SearchFacet { - border-width: 0; - box-shadow: none; -} -// stylelint-disable selector-no-qualifying-type -h3.SearchFacet__Heading { - font-size: 15px; - font-weight: 700; - margin-top: 10px; -} - -.SearchFacet__Body { - padding: 10px 0; -} diff --git a/lib/registries/addon/components/registries-search-facet-container/template.hbs b/lib/registries/addon/components/registries-search-facet-container/template.hbs deleted file mode 100644 index fcb847f2bab..00000000000 --- a/lib/registries/addon/components/registries-search-facet-container/template.hbs +++ /dev/null @@ -1,4 +0,0 @@ -

{{@title}}

-
- {{yield}} -
diff --git a/lib/registries/addon/components/registries-search-result/component.ts b/lib/registries/addon/components/registries-search-result/component.ts deleted file mode 100644 index eabf489d525..00000000000 --- a/lib/registries/addon/components/registries-search-result/component.ts +++ /dev/null @@ -1,46 +0,0 @@ -import Component from '@ember/component'; -import { computed } from '@ember/object'; -import { localClassNames } from 'ember-css-modules'; -import { inject as service } from '@ember/service'; -import Media from 'ember-responsive'; - -import { layout } from 'ember-osf-web/decorators/component'; -import { ShareRegistration } from 'registries/services/share-search'; - -import template from './template'; - -const OSF_GUID_REGEX = /^https?:\/\/.*osf\.io\/([^/]+)/; - -@layout(template) -@localClassNames('RegistriesSearchResult') -export default class RegistriesSearchResult extends Component { - @service media!: Media; - // Required - result!: ShareRegistration; - - // For use later, when the registration overview page is implemented - // @computed('result') - // get osfID() { - // const res = OSF_GUID_REGEX.exec(this.result.mainLink || ''); - - // if (res) { - // return res[1]; - // } - - // return false; - // } - - @computed('result.contributors') - get contributors() { - return this.result.contributors.filter( - contrib => contrib.bibliographic, - ).map(contrib => ({ - name: contrib.name, - link: contrib.identifiers.filter(ident => OSF_GUID_REGEX.test(ident))[0], - })); - } - - get isMobile() { - return this.media.isMobile; - } -} diff --git a/lib/registries/addon/components/registries-search-result/styles.scss b/lib/registries/addon/components/registries-search-result/styles.scss deleted file mode 100644 index 4382a0b490d..00000000000 --- a/lib/registries/addon/components/registries-search-result/styles.scss +++ /dev/null @@ -1,107 +0,0 @@ -// stylelint-disable max-nesting-depth, selector-max-compound-selectors - -.search-container { - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: flex-start; - width: 100%; - - &.mobile { - .search-result-container { - width: calc(100% - 50px); - } - - .badges-container { - width: 50px; - min-width: 50px; - padding-left: 0; - } - } - - .search-result-container { - display: flex; - flex-direction: column; - justify-content: flex-start; - align-items: flex-start; - width: 75%; - - .title { - width: 100%; - line-height: inherit; - margin-top: 10px; - - :global(.label-default) { - background-color: $color-bg-gray-darker; - } - } - - .updated { - width: 100%; - font-style: italic; - } - - .sources { - width: 100%; - font-weight: 700; - margin-top: 10px; - } - - .contributors { - width: 100%; - display: flex; - flex-direction: row; - flex-wrap: wrap; - list-style: none; - padding-left: 0; - - & > li { - display: inline-flex; - padding-right: 5px; - - &::after { - content: ','; - } - } - - & > li:last-child::after { - content: ''; - } - } - - .description { - max-height: 120px; - height: fit-content; - overflow: hidden; - transition: max-height 0.5s cubic-bezier(0, 1, 0, 1); - margin-top: 10px; - width: 100%; - padding-right: 10px; - } - } - - .badges-container { - min-width: 150px; - width: 25%; - border-left-width: thin; - border-left-color: $color-bg-gray-light; - border-left-style: solid; - padding-left: 10px; - margin-top: 12px; - } - -} - -.label { - display: inline; - padding: 0.2em 0.6em 0.3em; - font-size: 75%; - font-weight: 700; - line-height: 1; - color: $color-text-white; - background-color: $color-bg-gray-darker; - text-align: center; - white-space: nowrap; - vertical-align: baseline; - border-radius: 0.25em; -} diff --git a/lib/registries/addon/components/registries-search-result/template.hbs b/lib/registries/addon/components/registries-search-result/template.hbs deleted file mode 100644 index 63ecf039f8a..00000000000 --- a/lib/registries/addon/components/registries-search-result/template.hbs +++ /dev/null @@ -1,83 +0,0 @@ -
-
-

- {{#if @result.relatedResourceTypes}} - {{!-- This means it's an OSF resource, which means it'll have a guid --}} - - {{math @result.title}} - - {{else}} - - {{math @result.title}} - - {{/if}} - {{#if @result.withdrawn}} - {{t 'registries.discover.search_result.withdrawn'}} - {{/if}} -

- - {{#if this.contributors}} -
    - {{#each this.contributors as |contrib|}} -
  • - {{#if contrib.link}} - - {{contrib.name}} - - {{else}} - {{contrib.name}} - {{/if}} -
  • - {{/each}} -
- {{/if}} - - {{#if @result.dateUpdated}} -
- {{t 'registries.discover.search_result.last_edited' date=(moment-format (utc @result.dateUpdated) 'MMMM D, YYYY UTC')}} -
- {{/if}} - -
- {{#each @result.sources as |source index|}} - {{if index '| '}}{{source}} - {{/each}} - - {{#if @result.registrationType}} - | {{@result.registrationType}} - {{/if}} -
- -

- {{math @result.description}} -

-
- {{#if @result.relatedResourceTypes}} -
- -
- {{/if}} -
diff --git a/lib/registries/addon/components/registries-subjects-facet/component.ts b/lib/registries/addon/components/registries-subjects-facet/component.ts deleted file mode 100644 index a9b15516201..00000000000 --- a/lib/registries/addon/components/registries-subjects-facet/component.ts +++ /dev/null @@ -1,184 +0,0 @@ -import { action } from '@ember/object'; -import { inject as service } from '@ember/service'; -import Component from '@glimmer/component'; - -import ProviderModel from 'ember-osf-web/models/provider'; -import SubjectModel from 'ember-osf-web/models/subject'; -import Analytics from 'ember-osf-web/services/analytics'; -import { SubjectManager } from 'osf-components/components/subjects/manager/component'; -import { SearchOptions } from 'registries/services/search'; -import { ShareTermsFilter } from 'registries/services/share-search'; - -interface Args { - provider: ProviderModel; - searchOptions: SearchOptions; - onSearchOptionsUpdated(options: SearchOptions): void; -} - -// TODO: memoize some of these functions? could get expensive with lots of subjects expanded - -// WARNING: assumes subject.parent (and subject.parent.parent...) is already loaded -function getSubjectTerm(subject: SubjectModel): string { - // subjects are indexed with their full hierarchy, including taxonomy name - // 'taxonomy|parent subject text|this subject text' - // e.g. 'bepress|Law|Bird Law' - const parentSubject = subject.belongsTo('parent').value() as SubjectModel | null; - - return parentSubject - ? `${getSubjectTerm(parentSubject)}|${subject.text}` - : `${subject.taxonomyName}|${subject.text}`; -} - -function newSubjectFilter(subject: SubjectModel): ShareTermsFilter { - return new ShareTermsFilter('subjects', getSubjectTerm(subject), subject.text); -} - -/* want to get filters for all ancestors, e.g. - * given `bepress|foo|bar|baz` - * get [`bepress|foo`, `bepress|foo|bar`] - */ -function getAncestry(subjectTerm: string): string[] { - const parentTerms: string[] = []; - const [taxonomyName, ...subjectAncestry] = subjectTerm.split('|'); - for (let i = 1; i < subjectAncestry.length; i++) { - const ancestorLineage = subjectAncestry.slice(0, i).join('|'); - parentTerms.push(`${taxonomyName}|${ancestorLineage}`); - } - return parentTerms; -} - -function getAncestryFilters(subjectTerm: string): ShareTermsFilter[] { - const parentFilters: ShareTermsFilter[] = []; - const [taxonomyName, ...subjectAncestry] = subjectTerm.split('|'); - for (let i = 1; i < subjectAncestry.length; i++) { - const ancestorText = subjectAncestry[i]; - const ancestorLineage = subjectAncestry.slice(0, i).join('|'); - parentFilters.push(new ShareTermsFilter( - 'subjects', - `${taxonomyName}|${ancestorLineage}`, - ancestorText, - )); - } - return parentFilters; -} - -export default class RegistriesSubjectsFacet extends Component { - @service analytics!: Analytics; - - provider?: ProviderModel; - - get selectedSubjectFilters() { - const { searchOptions: { filters } } = this.args; - return filters.filter(f => f.key === 'subjects').toArray(); - } - - get selectedSubjectTerms(): Set { - return new Set( - this.selectedSubjectFilters.map(f => f.value as string), - ); - } - - get parentTermsWithSelectedChild(): Set { - const { selectedSubjectTerms } = this; - const parentTerms = new Set(); - - selectedSubjectTerms.forEach( - subjectTerm => getAncestry(subjectTerm).forEach( - ancestorTerm => parentTerms.add(ancestorTerm), - ), - ); - - return parentTerms; - } - - get subjectsManager(): SubjectManager { - const { - args: { provider }, - selectSubject, - unselectSubject, - selectedSubjectTerms, - parentTermsWithSelectedChild, - } = this; - - return { - provider, - selectSubject, - unselectSubject, - - subjectIsSelected(subject: SubjectModel): boolean { - // display a subject as selected if any of its children are selected - return selectedSubjectTerms.has(getSubjectTerm(subject)) - || parentTermsWithSelectedChild.has(getSubjectTerm(subject)); - }, - - subjectHasSelectedChildren(subject: SubjectModel) { - return parentTermsWithSelectedChild.has(getSubjectTerm(subject)); - }, - - subjectIsSaved: () => false, // TODO: should this return true? - - // NOTE: everything below is not needed by Subjects::Browse, so they're - // just here to fit the interface that assumes we're saving subjects - // on a model instance - savedSubjects: [], - selectedSubjects: [], - isSaving: false, - hasChanged: false, - discardChanges: () => undefined, - saveChanges: () => Promise.resolve(), - }; - } - - @action - selectSubject(subject: SubjectModel): void { - const { - searchOptions, - onSearchOptionsUpdated, - } = this.args; - - if (this.provider) { - this.analytics.track( - 'filter', - 'add', - `Discover - subject ${subject.text} ${this.provider.name}`, - ); - } else { - this.analytics.track('filter', 'add', `Discover - subject ${subject.taxonomyName}`); - } - - const filterToAdd = newSubjectFilter(subject); - const subjectTerm = getSubjectTerm(subject); - const parentFilters = getAncestryFilters(subjectTerm); - - onSearchOptionsUpdated(searchOptions.addFilters(filterToAdd, ...parentFilters)); - } - - @action - unselectSubject(subject: SubjectModel): void { - const { - args: { - searchOptions, - onSearchOptionsUpdated, - }, - selectedSubjectFilters, - } = this; - - if (this.provider) { - this.analytics.track( - 'filter', - 'remove', - `Discover - subject ${subject.text} ${this.provider.name}`, - ); - } else { - this.analytics.track('filter', 'remove', `Discover - subject ${subject.taxonomyName}`); - } - - const subjectTerm = getSubjectTerm(subject); - - const filtersToRemove = selectedSubjectFilters.filter( - f => (f.value as string).startsWith(subjectTerm), - ); - - onSearchOptionsUpdated(searchOptions.removeFilters(...filtersToRemove)); - } -} diff --git a/lib/registries/addon/components/registries-subjects-facet/template.hbs b/lib/registries/addon/components/registries-subjects-facet/template.hbs deleted file mode 100644 index 9ae740ef8cb..00000000000 --- a/lib/registries/addon/components/registries-subjects-facet/template.hbs +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/lib/registries/addon/discover/controller.ts b/lib/registries/addon/discover/controller.ts deleted file mode 100644 index 91c601de77a..00000000000 --- a/lib/registries/addon/discover/controller.ts +++ /dev/null @@ -1,397 +0,0 @@ -import Store from '@ember-data/store'; -import EmberArray, { A } from '@ember/array'; -import Controller from '@ember/controller'; -import { action, computed } from '@ember/object'; -import { inject as service } from '@ember/service'; -import { waitFor } from '@ember/test-waiters'; -import { restartableTask, task, timeout } from 'ember-concurrency'; -import { taskFor } from 'ember-concurrency-ts'; -import Intl from 'ember-intl/services/intl'; -import QueryParams from 'ember-parachute'; -import { is, OrderedSet } from 'immutable'; -import Media from 'ember-responsive'; - -import config from 'ember-get-config'; -import ProviderModel from 'ember-osf-web/models/provider'; -import Analytics from 'ember-osf-web/services/analytics'; -import discoverStyles from 'registries/components/registries-discover-search/styles'; -import { SearchFilter, SearchOptions, SearchOrder, SearchResults } from 'registries/services/search'; -import ShareSearch, { - ShareRegistration, - ShareTermsAggregation, - ShareTermsFilter, -} from 'registries/services/share-search'; - -// Helper for Immutable.is as it doesn't like Native Arrays -function isEqual(obj1: any, obj2: any) { - if (Array.isArray(obj1) && Array.isArray(obj2)) { - if (obj1.length !== obj2.length) { - return false; - } - - for (let i = 0; i < obj1.length; i++) { - if (!(isEqual(obj1[i], obj2[i]))) { - return false; - } - } - - return true; - } - - return is(obj1, obj2); -} - -interface DiscoverQueryParams { - page: number; - query: string; - size: number; - sort: SearchOrder; - registrationTypes: ShareTermsFilter[]; - sourceNames: string[]; - subjects: ShareTermsFilter[]; -} - -const sortOptions = [ - new SearchOrder({ - ascending: true, - display: 'registries.discover.order.relevance', - key: undefined, - }), - new SearchOrder({ - ascending: true, - display: 'registries.discover.order.modified_ascending', - key: 'date', - }), - new SearchOrder({ - ascending: false, - display: 'registries.discover.order.modified_descending', - key: 'date', - }), -]; - -const queryParams = { - sourceNames: { - as: 'provider', - defaultValue: [] as string[], - serialize(value: string[]) { - return value.join('|'); - }, - deserialize(value: string) { - return value.split('|'); - }, - }, - registrationTypes: { - as: 'type', - refresh: true, - defaultValue: [] as ShareTermsFilter[], - serialize(value: ShareTermsFilter[]) { - return value.map(filter => filter.value).join('|'); - }, - deserialize(value: string) { - // Handle empty strings - if (value.trim().length < 1) { - return []; - } - return value.split('|').map( - registrationType => new ShareTermsFilter('registration_type', registrationType, registrationType), - ); - }, - }, - query: { - as: 'q', - defaultValue: '', - replace: true, - }, - size: { - defaultValue: 10, - serialize(value: number) { - return value.toString(); - }, - deserialize(value: string) { - return parseInt(value, 10) || this.defaultValue; - }, - }, - sort: { - defaultValue: sortOptions[0], - serialize(value: SearchOrder) { - if (value.key === 'date_modified') { - return ''; - } - - return `${value.ascending ? '' : '-'}${value.key || ''}`; - }, - deserialize(value: string) { - return sortOptions.find( - option => !!option.key - && value.endsWith(option.key) - && option.ascending === !value.startsWith('-'), - ) || sortOptions[0]; - }, - }, - page: { - defaultValue: 1, - serialize(value: number) { - return value.toString(); - }, - deserialize(value: string) { - return parseInt(value, 10) || this.defaultValue; - }, - }, - subjects: { - defaultValue: [] as ShareTermsFilter[], - serialize(value: ShareTermsFilter[]) { - return value.map(filter => filter.value).join(',,'); - }, - deserialize(value: string) { - return value.split(',,').map( - subjectTerm => { - const subjectPieces = subjectTerm.split('|'); - const display = subjectPieces[subjectPieces.length - 1]; - return new ShareTermsFilter('subjects', subjectTerm, display); - }, - ); - }, - }, -}; - -export const discoverQueryParams = new QueryParams(queryParams); - -export default class Discover extends Controller.extend(discoverQueryParams.Mixin) { - @service media!: Media; - @service intl!: Intl; - @service analytics!: Analytics; - @service store!: Store; - @service shareSearch!: ShareSearch; - - sortOptions = sortOptions; - - results: EmberArray = A([]); - searchable!: number; - totalResults = 0; - searchOptions!: SearchOptions; - - filterableSources: Array<{ - count: number, - filter: SearchFilter, - }> = []; - - get providerModel(): ProviderModel | undefined { - return undefined; - } - - // used to filter the counts/aggregations and all search results - get additionalFilters(): ShareTermsFilter[] { - return []; - } - - @computed('sourceNames.[]', 'shareSearch.allRegistries.[]') - get sourceFilters() { - return this.sourceNames.map( - name => this.shareSearch.allRegistries.find(r => r.name === name), - ).filter(Boolean).map( - source => new ShareTermsFilter('sources', source!.name, source!.display || source!.name), - ); - } - - @computed('searchOptions.size', 'totalResults') - get maxPage() { - const max = Math.ceil(this.totalResults / this.searchOptions.size); - if (max > (10000 / this.searchOptions.size)) { - return Math.ceil(10000 / this.searchOptions.size); - } - return max; - } - - @task - @waitFor - async getCountsAndAggs() { - const results = await this.shareSearch.registrations(new SearchOptions({ - size: 0, - modifiers: OrderedSet([ - new ShareTermsAggregation('sources', 'sources'), - ]), - filters: OrderedSet([ - ...this.additionalFilters, - ]), - })); - - const osfProviders = await this.store.query('registration-provider', { - 'page[size]': 100, - }); - - // Setting osfProviders on the share-search service - const urlRegex = config.OSF.url.replace(/^https?/, '^https?'); - const filteredProviders = osfProviders.filter(provider => provider.shareSource).map(provider => ({ - name: provider.shareSource!, // `name` should match what SHARE calls it - display: provider.name, - https: true, - urlRegex, - })); - this.shareSearch.set('osfProviders', filteredProviders); - - const filterableSources: Array<{count: number, filter: SearchFilter}> = []; - /* eslint-disable camelcase */ - const buckets = results.aggregations.sources.buckets as Array<{key: string, doc_count: number}>; - - // NOTE: config.externalRegistries is iterated over here to match its order. - for (const source of this.shareSearch.allRegistries) { - const bucket = buckets.find(x => x.key === source.name); - if (!bucket) { - continue; - } - - filterableSources.push({ - count: bucket.doc_count, - filter: new ShareTermsFilter( - 'sources', - bucket.key, - source.display || source.name, - ), - }); - } - /* eslint-enable camelcase */ - - this.set('searchable', results.total); - this.set('filterableSources', filterableSources); - taskFor(this.doSearch).perform(); - } - - @restartableTask - @waitFor - async doSearch() { - // TODO-mob don't hard-code 'OSF' - - // Unless OSF is the only source, registration_type filters must be cleared - if (!(this.sourceNames.length === 1 && this.sourceNames[0]! === 'OSF Registries')) { - this.set('registrationTypes', A([])); - } - - // If query has changed but page has not changed reset page to 1. - // The page check stops other tests from breaking - if (this.searchOptions && this.searchOptions.query !== this.query && this.searchOptions.page === this.page) { - this.set('page', 1); - } - - let options = new SearchOptions({ - query: this.query, - size: this.size, - page: this.page, - order: this.sort, - filters: OrderedSet([ - ...this.sourceFilters, - ...this.registrationTypes, - ...this.additionalFilters, - ]), - }); - - // If there is no query, no filters, and no sort, default to -date_modified rather - // than relevance. - if (!options.order.key && (!options.query || options.query === '') && options.filters.size === 0) { - options = options.set('order', new SearchOrder({ - display: 'registries.discover.order.relevance', - ascending: false, - key: 'date_modified', - })); - } - - this.set('searchOptions', options); - - await timeout(250); - - const results: SearchResults = await this.shareSearch.registrations(options); - - this.set('results', A(results.results)); - this.set('totalResults', results.total); - } - - setup() { - taskFor(this.getCountsAndAggs).perform(); - } - - queryParamsDidChange() { - taskFor(this.doSearch).perform(); - } - - @action - onSearchOptionsUpdated(options: SearchOptions) { - const sources: ShareTermsFilter[] = []; - const registrationTypes: ShareTermsFilter[] = []; - const subjects: ShareTermsFilter[] = []; - - for (const filter of options.filters.values()) { - if (filter.key === 'sources') { - sources.push(filter as ShareTermsFilter); - } - - if (filter.key === 'registration_type') { - registrationTypes.push(filter as ShareTermsFilter); - } - - if (filter.key === 'subjects') { - subjects.push(filter as ShareTermsFilter); - } - } - - const changes = {} as Discover; - - if (!isEqual(this.sourceFilters, sources)) { - changes.page = 1; - changes.sourceNames = sources.map(filter => filter.value.toString()); - } - - if (!isEqual(this.registrationTypes, registrationTypes)) { - changes.page = 1; - changes.registrationTypes = registrationTypes; - } - - if (!isEqual(this.subjects, subjects)) { - changes.page = 1; - changes.subjects = subjects; - } - - // If any filters are changed page is reset to 1 - this.setProperties(changes); - } - - @action - changePage(page: number) { - this.set('page', page); - - // Get the application owner by using - // passed down services as rootElement - // isn't defined on engines' owners - const element = document.querySelector(`.${discoverStyles.Discover__Body}`) as HTMLElement; - if (!element) { - return; - } - element.scrollIntoView(); - } - - @action - onSearch(value: string) { - // Set page to 1 here to ensure page is always reset when updating a query - this.setProperties({ page: 1, query: value }); - // If query or page don't actually change ember won't fire related events - // So always kick off a doSearch task to allow forcing a "re-search" - taskFor(this.doSearch).perform(); - } - - @action - setOrder(value: SearchOrder) { - if (this.providerModel) { - this.analytics.track( - 'dropdown', - 'select', - `Discover - Sort By: ${this.intl.t(value.display)} ${this.providerModel.name}`, - ); - } else { - this.analytics.track('dropdown', 'select', `Discover - Sort By: ${this.intl.t(value.display)}`); - } - // Set page to 1 here to ensure page is always reset when changing the order/sorting of a search - this.setProperties({ page: 1, sort: value }); - } - - get isMobile() { - return this.media.isMobile; - } -} diff --git a/lib/registries/addon/discover/styles.scss b/lib/registries/addon/discover/styles.scss deleted file mode 100644 index 04033d43ce1..00000000000 --- a/lib/registries/addon/discover/styles.scss +++ /dev/null @@ -1,97 +0,0 @@ -// stylelint-disable max-nesting-depth, selector-max-compound-selectors - -.ResultsHeader { - padding: 0 0 15px; - - h2 { - margin: 0; - } -} - -.SortDropDown__List { - background-color: $color-bg-gray-light; - left: auto; -} - -.SortDropDown__Option { - text-align: left; - background-color: transparent; - cursor: pointer; - color: #000; - text-decoration: none; - width: 100%; -} - -.RegistriesSearchResult { - h4 { - font-size: 24px; - font-weight: 400; - } - - svg { - color: var(--primary-color); - } -} - -.search-container { - width: 100%; - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: flex-start; - - &.mobile { - flex-direction: column; - - .search-options-container { - width: 100%; - } - - .search-header-container { - width: 100%; - } - } - - .search-options-container { - width: 30%; - } - - .search-header-container { - width: 70%; - padding-left: 15px; - - .no-results { - width: 100%; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - } - - .Pagination { - width: 100%; - display: flex; - flex-direction: row; - justify-content: flex-end; - align-items: center; - padding-right: 12px; - } - - .loading-container, - .error-container { - width: 100%; - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - padding-top: 15px; - padding-bottom: 15px; - } - } -} - -.lead { - margin-bottom: 20px; - font-weight: 300; - line-height: 1.4; -} diff --git a/lib/registries/addon/discover/template.hbs b/lib/registries/addon/discover/template.hbs index 08744e3e9a5..d26027f5d4c 100644 --- a/lib/registries/addon/discover/template.hbs +++ b/lib/registries/addon/discover/template.hbs @@ -6,7 +6,7 @@ data-analytics-scope='Registries Discover page' > - {{registries-header + {{branded-header providerModel=this.providerModel showHelp=true value=(mut this.query) @@ -14,15 +14,15 @@ searchable=this.searchable }} - {{#registries-discover-search - results=this.results - isLoading=this.doSearch.isIdle - searchOptions=this.searchOptions - additionalFilters=this.additionalFilters - provider=this.providerModel +
{{#if this.searchOptions}} @@ -104,5 +104,5 @@
- {{/registries-discover-search}} +
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', + }, }); } } diff --git a/lib/registries/addon/index/template.hbs b/lib/registries/addon/index/template.hbs index c1a85e4a596..e571e88bf31 100644 --- a/lib/registries/addon/index/template.hbs +++ b/lib/registries/addon/index/template.hbs @@ -1,11 +1,16 @@ - {{#registries-header onSearch=(action 'onSearch') as |header|}} - {{#header.lead}} + + {{#branded-header.lead}} {{t 'registries.index.lead' htmlSafe=true}} - {{/header.lead}} - {{#header.row}} + {{/branded-header.lead}} + {{#branded-header.row}} - {{/header.row}} - {{/registries-header}} + {{/branded-header.row}} + {{#if this.getRecentRegistrations.isRunning}} diff --git a/lib/registries/addon/routes.ts b/lib/registries/addon/routes.ts index e26f07fd5b7..544dd5f8141 100644 --- a/lib/registries/addon/routes.ts +++ b/lib/registries/addon/routes.ts @@ -2,7 +2,6 @@ import buildRoutes from 'ember-engines/routes'; export default buildRoutes(function() { this.route('index', { path: '/registries' }); - this.route('discover', { path: '/registries/discover' }); this.route('branded', { path: '/registries/:providerId' }, function() { this.route('discover'); this.route('new'); diff --git a/mirage/config.ts b/mirage/config.ts index 4fee24ca92b..e2eae351843 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 { cardSearch, valueSearch } from './views/search'; import { createToken } from './views/token'; import { createEmails, updateEmails } from './views/update-email'; import { @@ -48,17 +49,24 @@ 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 + + // 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.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'; @@ -286,6 +294,13 @@ export default function(this: Server) { }); osfResource(this, 'preprint-provider', { path: '/providers/preprints' }); + osfNestedResource(this, 'preprint-provider', 'highlightedSubjects', { + only: ['index'], + path: '/providers/preprints/:parentID/subjects/highlighted/', + relatedModelName: 'subject', + }); + + osfResource(this, 'registration-provider', { path: '/providers/registrations' }); osfNestedResource(this, 'registration-provider', 'moderators', { only: ['index', 'show', 'update', 'delete'], diff --git a/mirage/fixtures/preprint-providers.ts b/mirage/fixtures/preprint-providers.ts index 738d808ff68..f362043c8fe 100644 --- a/mirage/fixtures/preprint-providers.ts +++ b/mirage/fixtures/preprint-providers.ts @@ -1,10 +1,12 @@ import PreprintProvider from 'ember-osf-web/models/preprint-provider'; +import { placekitten } from 'ember-osf-web/mirage/utils'; import { randomGravatar } from '../utils'; -function randomAssets() { +function randomAssets(i: number) { return { square_color_no_transparent: randomGravatar(100), + wide_white: placekitten(150, 75, i), }; } @@ -13,42 +15,67 @@ const preprintProviders: Array> = [ id: 'osf', name: 'Open Science Framework', preprintWord: 'preprint', - assets: randomAssets(), + assets: randomAssets(1), + footerLinks: 'fake footer links', }, { id: 'thesiscommons', name: 'Thesis Commons', preprintWord: 'thesis', - assets: randomAssets(), + assets: randomAssets(2), + footerLinks: 'fake footer links', }, { id: 'preprintrxiv', name: 'PreprintrXiv', preprintWord: 'preprint', - assets: randomAssets(), + assets: randomAssets(3), + footerLinks: 'fake footer links', }, { id: 'paperxiv', name: 'PaperXiv', preprintWord: 'paper', - assets: randomAssets(), + assets: randomAssets(4), + footerLinks: 'fake footer links', }, { id: 'thesisrxiv', name: 'ThesisrXiv', preprintWord: 'thesis', - assets: randomAssets(), + assets: randomAssets(5), + footerLinks: 'fake footer links', }, { id: 'workrxiv', name: 'WorkrXiv', preprintWord: 'work', - assets: randomAssets(), + assets: randomAssets(6), + footerLinks: 'fake footer links', }, { id: 'docrxiv', name: 'DocrXiv', preprintWord: 'default', + assets: randomAssets(7), + footerLinks: 'fake footer links', + }, + { + id: 'agrixiv', + name: 'AgriXiv', + preprintWord: 'preprint', + assets: randomAssets(), + }, + { + id: 'biohackrxiv', + name: 'BioHackrXiv', + preprintWord: 'preprint', + assets: randomAssets(), + }, + { + id: 'nutrixiv', + name: 'NutriXiv', + preprintWord: 'preprint', assets: randomAssets(), }, ]; diff --git a/mirage/scenarios/default.ts b/mirage/scenarios/default.ts index 3556cb32f26..be9188c86fa 100644 --- a/mirage/scenarios/default.ts +++ b/mirage/scenarios/default.ts @@ -8,6 +8,7 @@ import { import { dashboardScenario } from './dashboard'; import { forksScenario } from './forks'; import { meetingsScenario } from './meetings'; +import { preprintsScenario } from './preprints'; import { manyProjectRegistrationsScenario, registrationScenario } from './registrations'; import { settingsScenario } from './settings'; @@ -56,4 +57,7 @@ export default function(server: Server) { if (mirageScenarios.includes('manyProjectRegistrations')) { manyProjectRegistrationsScenario(server, currentUser); } + if (mirageScenarios.includes('preprints')) { + preprintsScenario(server, currentUser); + } } diff --git a/mirage/scenarios/preprints.ts b/mirage/scenarios/preprints.ts new file mode 100644 index 00000000000..3c0aec202d1 --- /dev/null +++ b/mirage/scenarios/preprints.ts @@ -0,0 +1,111 @@ +import { ModelInstance, Server } from 'ember-cli-mirage'; + +import PreprintProvider from 'ember-osf-web/models/preprint-provider'; +import User from 'ember-osf-web/models/user'; + +export function preprintsScenario( + server: Server, + currentUser: ModelInstance, +) { + buildOSF(server, currentUser); + buildThesisCommons(server, currentUser); +} + +function buildOSF( + server: Server, + currentUser: ModelInstance, +) { + const osf = server.schema.preprintProviders.find('osf') as ModelInstance; + const brand = server.create('brand', { + primaryColor: '#286090', + secondaryColor: '#fff', + heroLogoImage: 'images/default-brand/osf-preprints-white.png', + heroBackgroundImage: 'images/default-brand/bg-dark.jpg', + }); + const currentUserModerator = server.create('moderator', + { id: currentUser.id, user: currentUser, provider: osf }, 'asAdmin'); + + const preprints = server.createList('preprint', 4, { + provider: osf, + }); + + const subjects = server.createList('subject', 7); + + osf.update({ + allowSubmissions: true, + highlightedSubjects: subjects, + // eslint-disable-next-line max-len + advisory_board: '
\n

Advisory Group

\n

Our advisory group includes leaders in preprints and scholarly communication\n

\n
\n
    \n
  • Devin Berg : engrXiv, University of Wisconsin-Stout
  • \n
  • Pete Binfield : PeerJ PrePrints
  • \n
  • Benjamin Brown : PsyArXiv, Georgia Gwinnett College
  • \n
  • Philip Cohen : SocArXiv, University of Maryland
  • \n
  • Kathleen Fitzpatrick : Modern Language Association
  • \n
\n
\n
\n
    \n
  • John Inglis : bioRxiv, Cold Spring Harbor Laboratory Press
  • \n
  • Rebecca Kennison : K | N Consultants
  • \n
  • Kristen Ratan : CoKo Foundation
  • \n
  • Oya Rieger : Ithaka S+R
  • \n
  • Judy Ruttenberg : SHARE, Association of Research Libraries
  • \n
\n
\n
', + footer_links: '', + brand, + moderators: [currentUserModerator], + preprints, + description: 'This is the description for osf', + }); +} + +function buildThesisCommons( + server: Server, + currentUser: ModelInstance, +) { + const thesisCommons = server.schema.preprintProviders.find('thesiscommons') as ModelInstance; + const brand = server.create('brand', { + primaryColor: '#821e1e', + secondaryColor: '#94918e', + heroBackgroundImage: 'https://singlecolorimage.com/get/94918e/1000x1000', + }); + const currentUserModerator = server.create('moderator', + { id: currentUser.id, user: currentUser, provider: thesisCommons }, 'asAdmin'); + + const preprints = server.createList('preprint', 3, { + provider: thesisCommons, + }); + + const subjects = server.createList('subject', 2); + + thesisCommons.update({ + highlightedSubjects: subjects, + brand, + moderators: [currentUserModerator], + preprints, + description: '

This is the description for Thesis Commons and it has an inline-style!

', + // eslint-disable-next-line max-len + advisory_board: '

Steering Committee

\n
\n
    \n
  • Obasegun Ayodele, Vilsquare.org, Nigeria
  • \n
  • Fayza Mahmoud, Alexandria University, Egypt
  • \n
  • Johanssen Obanda, Jabulani Youths for Transformation (JAY4T), Kenya
  • \n
  • Umar Ahmad, University Putra Malaysia (UPM) and the Malaysia Genome Institute (MGI)
  • \n
  • Michael Cary, West Virginia University, USA
  • \n
  • Nada Fath, Mohamed V University & Hassan II Institute of Agronomy and Veterinary Medicine, Rabat, Morocco
  • \n
  • Greg Simpson, Cranfield University, England & South Africa
  • \n
\n
\n
    \n
  • Hisham Arafat, EMEA Applications Consulting, Egypt
  • \n
  • Justin Sègbédji Ahinon, AfricArXiv co-founder, IGDORE, Bénin
  • \n
  • Mahmoud M Ibrahim, Uniklinik RWTH Aachen, Germany & Egypt
  • \n
  • Luke Okelo, Technical University of Kenya
  • \n
  • Ryszard Auksztulewicz, MPI of Empirical Aesthetics, Germany & City University of Hong Kong
  • \n
  • Osman Aldirdiri, University of Khartoum, Sudan
  • \n
  • Jo Havemann, AfricArXiv co-founder, Access 2 Perspectives‘, IGDORE, Germany & Kenya
  • \n
', + // eslint-disable-next-line max-len + footer_links: '

AfricArXiv: About | Submission Guidelines | Support | Contact |

', + email_support: 'overwritten-email@osf.io', + }); + + const agrixiv = server.schema.preprintProviders.find('agrixiv') as ModelInstance; + const agrixivBrand = server.create('brand', { + primaryColor: '#85BF9B', + secondaryColor: '#E7F7E1', + heroBackgroundImage: 'https://singlecolorimage.com/get/E7F7E1/1000x1000', + }); + agrixiv.update({ + brand: agrixivBrand, + description: '

This is the description for agrixiv!

', + }); + + const nutrixiv = server.schema.preprintProviders.find('nutrixiv') as ModelInstance; + const nutrixivBrand = server.create('brand', { + primaryColor: '#000000', + secondaryColor: '#888888', + heroBackgroundImage: 'https://singlecolorimage.com/get/4a4a4a/1000x1000', + }); + nutrixiv.update({ + brand: nutrixivBrand, + description: '

This is the description for nutrixiv!

', + }); + + const biohackrxiv = server.schema.preprintProviders.find('biohackrxiv') as ModelInstance; + const biohackrxivBrand = server.create('brand', { + primaryColor: '#000000', + secondaryColor: '#ccc', + heroBackgroundImage: 'https://singlecolorimage.com/get/ffffff/1000x1000', + }); + biohackrxiv.update({ + brand: biohackrxivBrand, + description: '

This is the description for biohackrxiv!

', + }); +} diff --git a/mirage/scenarios/registrations.ts b/mirage/scenarios/registrations.ts index 8517335a35b..9fdd40e580b 100644 --- a/mirage/scenarios/registrations.ts +++ b/mirage/scenarios/registrations.ts @@ -49,6 +49,7 @@ export function registrationScenario( server.create('registration-provider', { id: defaultProvider, + brandedDiscoveryPage: false, shareSource: 'OSF Registries', name: 'OSF Registries', }, 'withAllSchemas'); diff --git a/mirage/serializers/preprint-provider.ts b/mirage/serializers/preprint-provider.ts new file mode 100644 index 00000000000..d2db241437f --- /dev/null +++ b/mirage/serializers/preprint-provider.ts @@ -0,0 +1,77 @@ +import { ModelInstance } from 'ember-cli-mirage'; +import config from 'ember-get-config'; +import PreprintProvider from 'ember-osf-web/models/preprint-provider'; +import ApplicationSerializer, { SerializedRelationships } from './application'; + +const { OSF: { apiUrl } } = config; + +export default class PreprintProviderSerializer extends ApplicationSerializer { + buildNormalLinks(model: ModelInstance) { + return { + self: `${apiUrl}/v2/providers/preprints/${model.id}/`, + }; + } + + buildRelationships(model: ModelInstance) { + const has_highlighted_subjects = model.highlightedSubjects.length > 0; + + const relationships: SerializedRelationships = { + subjects: { + links: { + related: { + href: `${apiUrl}/v2/providers/preprints/${model.id}/subjects/`, + meta: this.buildRelatedLinkMeta(model, 'subjects'), + }, + }, + }, + highlightedSubjects: { + links: { + related: { + href: `${apiUrl}/v2/providers/preprints/${model.id}/subjects/highlighted/`, + meta: { + has_highlighted_subjects, + }, + }, + }, + }, + licensesAcceptable: { + links: { + related: { + href: `${apiUrl}/v2/providers/preprints/${model.id}/licenses/`, + meta: {}, + }, + }, + }, + moderators: { + links: { + related: { + href: `${apiUrl}/v2/providers/preprints/${model.id}/moderators/`, + meta: this.buildRelatedLinkMeta(model, 'moderators'), + }, + }, + }, + preprints: { + links: { + related: { + href: `${apiUrl}/v2/providers/preprints/${model.id}/preprints/`, + meta: {}, + }, + }, + }, + // TODO: subscriptions when we move ember-osf-reviews¥ + }; + + if (model.brand) { + relationships.brand = { + links: { + related: { + href: `${apiUrl}/v2/brands/${model.brand.id}/`, + meta: {}, + }, + }, + }; + } + + return relationships; + } +} diff --git a/mirage/views/search.ts b/mirage/views/search.ts new file mode 100644 index 00000000000..edab0c7ed3a --- /dev/null +++ b/mirage/views/search.ts @@ -0,0 +1,627 @@ +import { Request, Schema } from 'ember-cli-mirage'; +import faker from 'faker'; + +export function cardSearch(_: Schema, __: Request) { + // TODO: replace with a real index-card-search and use request to populate attrs + return { + data: { + type: 'index-card-search', + id: 'zzzzzz', + attributes:{ + cardSearchText: 'hello', + cardSearchFilter: [ + { + osfmapPropertyPath: 'resourceType', + filterType: 'eq', + filterValues: [ + 'osf:Registration', + ], + }, + { + osfmapPropertyPath: '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: 'index-property-search', + id: 'tuv', + }, + }, + }, + }, + included: [ + { + type: 'search-result', + id: 'abc', + attributes: { + matchEvidence: [ + { + '@type': ['https://share.osf.io/vocab/2023/trove/TextMatchEvidence'], + osfmapPropertyPath: 'description', + matchingHighlight: '... say hello!', + }, + { + '@type': ['https://share.osf.io/vocab/2023/trove/TextMatchEvidence'], + osfmapPropertyPath: 'title', + matchingHighlight: '... shout hello!', + }, + ], + }, + relationships: { + indexCard: { + data: { + type: 'index-card', + id: 'abc', + }, + links: { + related: 'https://share.osf.io/api/v2/index-card/abc', + }, + }, + }, + }, + { + type: 'search-result', + id: 'def', + attributes: { + matchEvidence: [ + { + '@type': ['https://share.osf.io/vocab/2023/trove/TextMatchEvidence'], + osfmapPropertyPath: 'description', + matchingHighlight: '... computer said hello world!', + }, + ], + }, + relationships: { + indexCard: { + data: { + type: 'index-card', + id: 'def', + }, + links: { + related: 'https://share.osf.io/api/v2/index-card/def', + }, + }, + }, + }, + { + type: 'search-result', + id: 'ghi', + attributes: { + matchEvidence: [ + { + '@type': ['https://share.osf.io/vocab/2023/trove/TextMatchEvidence'], + osfmapPropertyPath: 'title', + matchingHighlight: '... you said hello!', + }, + ], + }, + relationships: { + indexCard: { + data: { + type: 'index-card', + id: 'ghi', + }, + links: { + related: 'https://share.osf.io/api/v2/index-card/abc', + }, + }, + }, + }, + { + type: 'index-card', + id: 'abc', + attributes: { + resourceIdentifier: [ + 'https://osf.example/abcfoo', + 'https://doi.org/10.0000/osf.example/abcfoo', + ], + resourceMetadata: { + resourceType: [ + 'osf:Registration', + 'dcterms:Dataset', + ], + '@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/index-card/abc', + resource: 'https://osf.example/abcfoo', + }, + }, + { + type: 'index-card', + id: 'def', + attributes: { + resourceIdentifier: [ + 'https://osf.example/abcfoo', + 'https://doi.org/10.0000/osf.example/abcfoo', + ], + resourceMetadata: { + resourceType: [ + 'osf:Registration', + 'dcterms:Dataset', + ], + '@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: 'index-card', + id: 'ghi', + attributes: { + resourceIdentifier: [ + 'https://osf.example/abcfoo', + 'https://doi.org/10.0000/osf.example/abcfoo', + ], + resourceMetadata: { + resourceType: [ + 'osf:Registration', + 'dcterms:Dataset', + ], + '@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: 'index-property-search', + id: 'tuv', + attributes: { + cardSearchText: 'hello', + cardSearchFilter: [ + { + 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: [ + { + '@type': ['https://share.osf.io/vocab/2023/trove/IriMatchEvidence'], + osfmapPropertyPath: 'resourceType', + matchingIri: 'rdf:Property', + }, + ], + recordResultCount: 345, + }, + relationships: { + indexCard: { + data: { + type: 'index-card', + id: 'idForPropertyRecord1', + }, + links: { + related: 'https://share.osf.io/api/v2/index-card/idForPropertyRecord1', + }, + }, + }, + }, + { + type: 'search-result', + id: 'propertyMatch2', + attributes: { + matchEvidence: [ + { + '@type': ['https://share.osf.io/vocab/2023/trove/IriMatchEvidence'], + osfmapPropertyPath: 'resourceType', + matchingIri: 'rdf:Property', + }, + ], + recordResultCount: 123, + }, + relationships: { + indexCard: { + data: { + type: 'index-card', + id: 'idForPropertyRecord2', + }, + links: { + related: 'https://share.osf.io/api/v2/index-card/idForPropertyRecord2', + }, + }, + }, + }, + { + type: 'search-result', + id: 'propertyMatch3', + attributes: { + matchEvidence: [ + { + '@type': ['https://share.osf.io/vocab/2023/trove/IriMatchEvidence'], + osfmapPropertyPath: 'resourceType', + matchingIri: 'rdf:Property', + }, + ], + recordResultCount: 33, + }, + relationships: { + indexCard: { + data: { + type: 'index-card', + id: 'idForPropertyRecord3', + }, + links: { + related: 'https://share.osf.io/api/v2/index-card/idForPropertyRecord3', + }, + }, + }, + }, + { + type: 'index-card', + 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/index-card/idForPropertyRecord1', + resource: 'http://purl.org/dc/terms/license', + }, + }, + { + type: 'index-card', + 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/index-card/idForPropertyRecord2', + resource: 'http://purl.org/dc/terms/published', + }, + }, + { + type: 'index-card', + 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/index-card/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: 'index-value-search', + id: 'lmnop', + attributes: { + valueSearchText: 'Institute of Health', + valueSearchFilter: [ + { + osfmapPropertyPath: 'resourceType', + filterType: 'eq', + filterValues: ['datacite:Funder'], + }, + ], + cardSearchText: 'influenza', + cardSearchFilter: [ + { + osfmapPropertyPath: '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: 'index-property-search', id: '12345'}, + }, + }, + }, + included: [ + { + type: 'search-result', + id: property1Id, + attributes: { + matchEvidence: [ + { + '@type': ['https://share.osf.io/vocab/2023/trove/TextMatchEvidence'], + osfmapPropertyPath: 'title', + matchingHighlight: 'National Institute of Health', + }, + ], + recordResultCount: 2134, + }, + relationships: { + indexCard: { + data: {type: 'index-card', id: property1Id}, + links: {related: 'https://share.osf.example/index-card/abc'}, + }, + }, + }, + { + type: 'search-result', + id: property2Id, + attributes: { + matchEvidence: [ + { + '@type': ['https://share.osf.io/vocab/2023/trove/TextMatchEvidence'], + osfmapPropertyPath: 'title', + matchingHighlight: 'Virginia Institute of Health', + }, + ], + recordResultCount: 2, + }, + relationships: { + indexCard: { + data: {type: 'index-card', id: property2Id}, + links: {related: 'https://share.osf.example/index-card/def'}, + }, + }, + }, + { + type: 'index-card', + 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: 'index-card', + 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/package.json b/package.json index 9a8543aea78..f3f41887317 100644 --- a/package.json +++ b/package.json @@ -267,6 +267,7 @@ "lib/analytics-page", "lib/assets-prefix-middleware", "lib/collections", + "lib/app-components", "lib/osf-components", "lib/registries" ] diff --git a/public/assets/images/default-brand/osf-preprints-white.png b/public/assets/images/default-brand/osf-preprints-white.png new file mode 100644 index 00000000000..ff363f18a63 Binary files /dev/null and b/public/assets/images/default-brand/osf-preprints-white.png differ diff --git a/tests/acceptance/institutions/discover-test.ts b/tests/acceptance/institutions/discover-test.ts new file mode 100644 index 00000000000..f450415bdce --- /dev/null +++ b/tests/acceptance/institutions/discover-test.ts @@ -0,0 +1,49 @@ +import { currentURL, visit } from '@ember/test-helpers'; +import { setupMirage } from 'ember-cli-mirage/test-support'; +import { percySnapshot } from 'ember-percy'; +import { setBreakpoint } from 'ember-responsive/test-support'; +import { module, test } from 'qunit'; +import { click, setupOSFApplicationTest} from 'ember-osf-web/tests/helpers'; + +module('Acceptance | institutions | discover', hooks => { + setupOSFApplicationTest(hooks); + setupMirage(hooks); + + test('Desktop: Default colors', async assert => { + server.create('institution', { + id: 'has-users', + }, 'withMetrics'); + await visit('/institutions/has-users'); + // verify institutions route + assert.equal(currentURL(), '/institutions/has-users', 'Current route is institutions discover'); + assert.dom('[data-test-heading-wrapper]').exists('Institutions heading wrapper shown'); + // verify banner and description + assert.dom('[data-test-institution-banner]').exists('Institution banner shown'); + assert.dom('[data-test-institution-description]').exists('Institution description shown'); + // verify topbar and sort dropdown + assert.dom('[data-test-topbar-wrapper]').exists('Topbar not shown on mobile'); + assert.dom('[data-test-topbar-sort-dropdown]').exists('Sort dropdown shown on desktop'); + await percySnapshot(assert); + }); + + test('Mobile: Default colors', async assert => { + setBreakpoint('mobile'); + server.create('institution', { + id: 'has-users', + }, 'withMetrics'); + // verify institutions route + await visit('/institutions/has-users'); + assert.equal(currentURL(), '/institutions/has-users', 'Current route is institutions discover'); + // verify logo and description + assert.dom('[data-test-institution-logo]').exists('Institution header logo shown'); + assert.dom('[data-test-institution-description]').exists('Institution description is shown'); + // verify mobile menu display + assert.dom('[data-test-topbar-wrapper]').doesNotExist('Topbar not shown on mobile'); + assert.dom('[data-test-toggle-side-panel]').exists('Institution header logo shown'); + await click('[data-test-toggle-side-panel]'); + // verify resource type and sort by dropdown + assert.dom('[data-test-left-panel-object-type-dropdown]').exists('Mobile resource type dropdown is shown'); + assert.dom('[data-test-left-panel-sort-dropdown]').exists('Mobile sort by dropdown is shown'); + await percySnapshot(assert); + }); +}); diff --git a/tests/acceptance/preprints/discover-test.ts b/tests/acceptance/preprints/discover-test.ts new file mode 100644 index 00000000000..539143a02e4 --- /dev/null +++ b/tests/acceptance/preprints/discover-test.ts @@ -0,0 +1,60 @@ +import { click, currentRouteName } from '@ember/test-helpers'; +import { ModelInstance } from 'ember-cli-mirage'; +import { setupMirage } from 'ember-cli-mirage/test-support'; +import { TestContext } from 'ember-test-helpers'; +import { percySnapshot } from 'ember-percy'; +import { setBreakpoint } from 'ember-responsive/test-support'; +import { module, test } from 'qunit'; + +import { setupOSFApplicationTest, visit } from 'ember-osf-web/tests/helpers'; +import PreprintProviderModel from 'ember-osf-web/models/preprint-provider'; + +interface PreprintDiscoverTestContext extends TestContext { + provider: ModelInstance; +} + +module('Acceptance | preprints | discover', hooks => { + setupOSFApplicationTest(hooks); + setupMirage(hooks); + + hooks.beforeEach(async function(this: PreprintDiscoverTestContext) { + server.loadFixtures('preprint-providers'); + const provider = server.schema.preprintProviders.find('thesiscommons') as ModelInstance; + const brand = server.create('brand'); + provider.update({ + brand, + description: 'This is the description for Thesis Commons', + }); + this.provider = provider; + }); + + 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'); + assert.dom('[data-test-topbar-object-type-nav]').doesNotExist('Desktop: Object type nav is not shown'); + assert.dom('[data-test-middle-search-count]').exists('Desktop: Result count is shown in middle panel'); + await percySnapshot(assert); + }); + + test('mobile', async function(this: PreprintDiscoverTestContext, assert) { + setBreakpoint('mobile'); + await visit(`/preprints/${this.provider.id}/discover`); + assert.equal(currentRouteName(), 'preprints.discover', 'Current route is preprints discover'); + assert.dom('[data-test-search-provider-logo]').exists('Mobile: Preprint provider logo is shown'); + assert.dom('[data-test-search-provider-description]').exists('Mobile: Preprint provider description is shown'); + assert.dom('[data-test-search-header]').doesNotExist('Mobile: Non-branded search header is not shown'); + assert.dom('[data-test-topbar-object-type-nav]').doesNotExist('Mobile: Object type nav is not shown'); + assert.dom('[data-test-middle-search-count]').doesNotExist('Mobile: Result count is not shown in middle panel'); + assert.dom('[data-test-toggle-side-panel]').exists('Mobile: Toggle side panel button is shown'); + await click('[data-test-toggle-side-panel]'); + assert.dom('[data-test-left-search-count]').exists('Mobile: Result count is shown in side panel'); + assert.dom('[data-test-left-panel-object-type-dropdown]') + .doesNotExist('Mobile: Object type dropdown is not shown'); + await percySnapshot(assert); + }); +}); diff --git a/tests/acceptance/preprints/taxonomy-top-list-test.ts b/tests/acceptance/preprints/taxonomy-top-list-test.ts new file mode 100644 index 00000000000..81b3c6e64bd --- /dev/null +++ b/tests/acceptance/preprints/taxonomy-top-list-test.ts @@ -0,0 +1,214 @@ +import Service from '@ember/service'; +import { render } from '@ember/test-helpers'; +import Ember from 'ember'; +import { hbs } from 'ember-cli-htmlbars'; +import { EnginesTestContext } from 'ember-engines/test-support'; +import SubjectModel from 'ember-osf-web/models/subject'; +import { setupRenderingTest } from 'ember-qunit'; +import { module, test } from 'qunit'; + +module('Acceptance | preprints | taxonomy-top-list', hooks => { + setupRenderingTest(hooks); + + test('it creates no rows when the list is undefined', async function(this: EnginesTestContext,assert) { + // Given no list is provided + // When the component is rendered + await render(hbs` + + + `); + + // Then the subject container is verified + assert.dom('[data-test-subject-container]').doesNotExist('no subjects are displayed'); + }); + + test('it creates no rows when the list is empty', async function(this: EnginesTestContext,assert) { + // Given the taxonomy list is created with two subjects + this.set('list', [ ]); + + // When the component is rendered + await render(hbs` + + + `); + + // Then the subject container is verified + assert.dom('[data-test-subject-container]').doesNotExist('no subjects are displayed'); + }); + + test('it throws an error with a list and no provider', async function(this: EnginesTestContext,assert) { + Ember.onerror = (error: Error) => { + assert.equal(error.message, + 'A provider string must be provided with a valid list'); + }; + + // Given the taxonomy list is created with two subjects + this.set('list', [ + { + text: 'elephant', + taxonomyName: 'forgetful', + } as SubjectModel, + ]); + + // When the component is rendered + await render(hbs` + + + `); + }); + + test('it creates a row of two subjects in desktop mode', async function(this: EnginesTestContext,assert) { + // Given the taxonomy list is created with two subjects + this.set('list', [ + { + text: 'elephant', + taxonomyName: 'forgetful', + } as SubjectModel, + { + text: 'deer', + taxonomyName: 'The sneaky', + } as SubjectModel, + ]); + + // When the component is rendered + await render(hbs` + + + `); + + // Then the subject container is verified + const subjectContainers = this.element.querySelectorAll('[data-test-subject-container]'); + assert.equal(subjectContainers.length, 1, 'The subject container has 1 container'); + + // And the link containers are verified + const linkContainers = subjectContainers[0].querySelectorAll('[data-test-taxonomy-container]'); + assert.equal(linkContainers.length, 2, 'The link container is grouped in two'); + + // And the first link is verified to be sorted correctly + const links = linkContainers[0].querySelectorAll('[data-test-taxonomy-link]'); + assert.dom(links[0]).hasText('deer', 'The first link is deer'); + // And the href is corrected + // eslint-disable-next-line max-len + assert.dom(links[0]).hasAttribute('href', 'preprints/outdoorLife/discover?subject=The sneaky', 'The href is blank'); + }); + + test('it creates two rows of two and one subject in desktop mode', async function(this: EnginesTestContext,assert) { + // Given the taxonomy list is created with two subjects + this.set('list', [ + { + text: 'elephant', + taxonomyName: 'forgetful', + } as SubjectModel, + { + text: 'bamboon', + taxonomyName: 'The great red bum', + } as SubjectModel, + { + text: 'deer', + taxonomyName: 'The sneaky', + } as SubjectModel, + ]); + + // When the component is rendered + await render(hbs` + + + `); + + + // Then the subject container is verified + const subjectContainers = this.element.querySelectorAll('[data-test-subject-container]'); + assert.equal(subjectContainers.length, 2, 'The subject container has 1 container'); + + // And the link containers are verified + let linkContainers = subjectContainers[0].querySelectorAll('[data-test-taxonomy-container]'); + assert.equal(linkContainers.length, 2, 'The link container is grouped in two'); + + // And the first link is verified to be sorted correctly + let links = linkContainers[1].querySelectorAll('[data-test-taxonomy-link]'); + assert.dom(links[0]).hasText('deer', 'The first link is deer'); + // And the href is corrected + // eslint-disable-next-line max-len + assert.dom(links[0]).hasAttribute('href', 'preprints/outdoorLife/discover?subject=The sneaky', 'The href is blank'); + + linkContainers = subjectContainers[1].querySelectorAll('[data-test-taxonomy-container]'); + assert.equal(linkContainers.length, 1, 'The link container is grouped in one'); + + // And the third link is verified to be sorted correctly + links = linkContainers[0].querySelectorAll('[data-test-taxonomy-link]'); + assert.dom(links[0]).hasText('elephant', 'The first link is elephant'); + // And the href is corrected + // eslint-disable-next-line max-len + assert.dom(links[0]).hasAttribute('href', 'preprints/outdoorLife/discover?subject=forgetful', 'The href is blank'); + }); + + test('it creates a 2 rows and one subjects in mobile mode', async function(this: EnginesTestContext,assert) { + + const mediaServiceStub = Service.extend({ + isMobile: true, + isDesktop: false, + }); + + this.owner.register('service:media', mediaServiceStub); + + // Given the taxonomy list is created with two subjects + this.set('list', [ + { + text: 'elephant', + taxonomyName: 'forgetful', + } as SubjectModel, + { + text: 'deer', + taxonomyName: 'The sneaky', + } as SubjectModel, + ]); + + // When the component is rendered + await render(hbs` + + + `); + + // Then the subject container is verified + const subjectContainers = this.element.querySelectorAll('[data-test-subject-container]'); + assert.equal(subjectContainers.length, 2, 'The subject container has 1 container'); + + // And the link containers are verified + let linkContainers = subjectContainers[0].querySelectorAll('[data-test-taxonomy-container]'); + assert.equal(linkContainers.length, 1, 'The link container is grouped in one'); + + // And the first link is verified to be sorted correctly + let links = linkContainers[0].querySelectorAll('[data-test-taxonomy-link]'); + assert.dom(links[0]).hasText('deer', 'The first link is deer'); + // And the href is corrected + // eslint-disable-next-line max-len + assert.dom(links[0]).hasAttribute('href', 'preprints/outdoorLife/discover?subject=The sneaky', 'The href is blank'); + + // And the link containers are verified + linkContainers = subjectContainers[1].querySelectorAll('[data-test-taxonomy-container]'); + assert.equal(linkContainers.length, 1, 'The link container is grouped in one'); + + // And the first link is verified to be sorted correctly + links = linkContainers[0].querySelectorAll('[data-test-taxonomy-link]'); + assert.dom(links[0]).hasText('elephant', 'The first link is elephant'); + // And the href is corrected + // eslint-disable-next-line max-len + assert.dom(links[0]).hasAttribute('href', 'preprints/outdoorLife/discover?subject=forgetful', 'The href is blank'); + }); +}); diff --git a/tests/acceptance/search/search-filters-test.ts b/tests/acceptance/search/search-filters-test.ts new file mode 100644 index 00000000000..ef31ca35de8 --- /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('[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'); + // 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/tests/acceptance/search/search-help-test.ts b/tests/acceptance/search/search-help-test.ts new file mode 100644 index 00000000000..efe94dc68f0 --- /dev/null +++ b/tests/acceptance/search/search-help-test.ts @@ -0,0 +1,80 @@ +import { click, currentURL, visit } from '@ember/test-helpers'; +import { setupMirage } from 'ember-cli-mirage/test-support'; +import { module, test } from 'qunit'; +import { setupOSFApplicationTest } from 'ember-osf-web/tests/helpers'; +import { EnginesIntlTestContext } from 'ember-engines/test-support'; + +module('Integration | Component | Search help', hooks => { + setupOSFApplicationTest(hooks); + setupMirage(hooks); + + test('ember popover renders', async function(this: EnginesIntlTestContext, assert) { + await visit('/search'); + assert.equal(currentURL(), '/search'); + + // start help tutorial + assert.dom('[data-test-start-help]').exists(); + await click('[data-test-start-help]'); + + // verify first popover displays + assert.dom('[data-test-search-help-1]').exists(); + // verify skip button present + assert.dom('[data-test-help-skip-1]').exists(); + assert.dom('[data-test-help-skip-1]').hasText('Skip'); + // verify next button works + assert.dom('[data-test-help-next-1]').exists(); + assert.dom('[data-test-help-next-1]').hasText('Next'); + // verify first popover content + assert.dom('[data-test-help-heading-1]').exists(); + assert.dom('[data-test-help-heading-1]').hasText('Improved OSF Search'); + assert.dom('[data-test-help-body-1]').exists(); + assert.dom('[data-test-help-body-1]').hasText(`Enter any term in the search box + and filter by specific object types.`); + assert.dom('[data-test-help-enumeration-1]').exists(); + assert.dom('[data-test-help-enumeration-1]').hasText('1 of 3'); + + // verify second popover displays + await click('[data-test-help-next-1]'); + assert.dom('[data-test-search-help-2]').exists(); + // verify second popover content + assert.dom('[data-test-help-heading-2]').exists(); + assert.dom('[data-test-help-heading-2]').hasText('OSF Smart Facets'); + assert.dom('[data-test-help-body-2]').exists(); + assert.dom('[data-test-help-body-2]').hasText(`Narrow the source, discipline, and more. + For example, find content supported by a specific funder or view only datasets.`); + assert.dom('[data-test-help-enumeration-2]').exists(); + assert.dom('[data-test-help-enumeration-2]').hasText('2 of 3'); + + // verify third popover displays + await click('[data-test-help-next-2]'); + assert.dom('[data-test-search-help-3]').exists(); + // verify third popover content + assert.dom('[data-test-help-heading-3]').exists(); + assert.dom('[data-test-help-heading-3]').hasText('Add Metadata'); + assert.dom('[data-test-help-body-3]').exists(); + assert.dom('[data-test-help-body-3]').hasText(`Remember to add metadata and resources + to your own work on OSF to make it more discoverable!`); + assert.dom('[data-test-help-enumeration-3]').exists(); + assert.dom('[data-test-help-enumeration-3]').hasText('3 of 3'); + + // verify popover closes + assert.dom('[data-test-help-done]').exists(); + assert.dom('[data-test-help-done]').hasText('Done'); + await click('[data-test-help-done]'); + assert.dom('[data-test-search-help-1]').isNotVisible(); + }); + + test('help tutorial can be skipped', async assert => { + await visit('/search'); + assert.equal(currentURL(), '/search'); + + // verify help tutorial starts + assert.dom('[data-test-start-help]').exists(); + await click('[data-test-start-help]'); + + // verify skip button works + assert.dom('[data-test-help-skip-1]').exists(); + await click('[data-test-help-skip-1]'); + assert.dom('[data-test-search-help-1]').isNotVisible(); + }); +}); 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..3cdfea69648 --- /dev/null +++ b/tests/acceptance/search/search-query-params-test.ts @@ -0,0 +1,96 @@ +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=Project%2CProjectComponent', '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=Project%2CProjectComponent&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=Registration%2CRegistrationComponent', + '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=Registration%2CRegistrationComponent&sort=date_modified', + 'Query-params are updated', + ); + }); + + test('query-parameters from url', async assert => { + await visit('/search?resourceType=Preprint&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/tests/engines/registries/acceptance/branded/discover-test.ts b/tests/engines/registries/acceptance/branded/discover-test.ts index eb96a69feb9..018f11304ca 100644 --- a/tests/engines/registries/acceptance/branded/discover-test.ts +++ b/tests/engines/registries/acceptance/branded/discover-test.ts @@ -27,30 +27,10 @@ module('Registries | Acceptance | branded.discover', hooks => { server.createList('registration', 3, { provider: this.brandedProvider }); }); - test('branded discover with no external providers', async function(this: ThisTestContext, assert) { + test('branded discover page renders', async function(this: ThisTestContext, assert) { await visit(`/registries/${this.brandedProvider.id}/discover`); await percySnapshot('branded discover page'); assert.equal(currentRouteName(), 'registries.branded.discover', 'On the branded discover page'); - - assert.dom('[data-test-active-filter]').doesNotExist('The given provider is not shown as an active filter'); - assert.dom('[data-test-source-filter-id]').exists({ count: 1 }, 'Only one provider is available'); - assert.dom('[data-test-source-filter-id]').isChecked('Provider facet checkbox is checked'); - assert.dom('[data-test-source-filter-id]').isDisabled('Provider facet checkbox is disabled'); - assert.dom('[data-test-link-other-registries]').exists('Link to other registries is shown'); - assert.dom('[data-test-provider-description]').containsText('Find out more', 'Provider description exists'); - assert.dom('[data-test-provider-description] a').exists('There is a link in the provider description'); - assert.ok(document.querySelector('link[rel="icon"][href="fakelink"]')); - }); - - test('branded discover with external providers', async function(this: ThisTestContext, assert) { - const externalProvider = server.create('external-provider', { shareSource: 'ClinicalTrials.gov' }); - server.createList('external-registration', 3, { provider: externalProvider }); - - await visit(`/registries/${this.brandedProvider.id}/discover`); - assert.dom('[data-test-source-filter-id]').exists({ count: 1 }, 'Only brand provider is shown'); - assert.dom(`[data-test-source-filter-id="${externalProvider.shareSource}"]`) - .doesNotExist('External provider is not shown'); - assert.ok(document.querySelector('link[rel="icon"][href="fakelink"]')); }); test('redirects branded.index to branded.discover', async function(this: ThisTestContext, assert) { @@ -58,7 +38,6 @@ module('Registries | Acceptance | branded.discover', hooks => { assert.equal(currentRouteName(), 'registries.branded.discover', 'successfully redirects index to discover'); - assert.dom(`[data-test-source-filter-id="${this.brandedProvider.shareSource}"]`).exists({ count: 1 }); }); test('redirects', async assert => { @@ -78,12 +57,12 @@ module('Registries | Acceptance | branded.discover', hooks => { await visit(`/registries/${osfProvider.id}/discover`); assert.equal(currentRouteName(), - 'registries.discover', - '/registries/osf/discover redirects to registries/discover'); + 'search', + '/registries/osf/discover redirects to /search'); await visit(`/registries/${osfProvider.id}`); assert.equal(currentRouteName(), - 'registries.discover', - '/registries/osf redirects to registries/discover'); + 'search', + '/registries/osf redirects to /search'); }); }); diff --git a/tests/engines/registries/acceptance/discover-page-test.ts b/tests/engines/registries/acceptance/discover-page-test.ts deleted file mode 100644 index 0f51b65892f..00000000000 --- a/tests/engines/registries/acceptance/discover-page-test.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { click, fillIn } from '@ember/test-helpers'; -import { setupMirage } from 'ember-cli-mirage/test-support'; -import { t } from 'ember-intl/test-support'; -import { percySnapshot } from 'ember-percy'; -import { module, test } from 'qunit'; - -import { visit } from 'ember-osf-web/tests/helpers'; -import { setupEngineApplicationTest } from 'ember-osf-web/tests/helpers/engines'; - -module('Registries | Acceptance | aggregate discover', hooks => { - setupEngineApplicationTest(hooks, 'registries'); - setupMirage(hooks); - - hooks.beforeEach(() => { - const osfProvider = server.create('registration-provider', { id: 'osf' }); - const anotherProvider = server.create('registration-provider', { id: 'another' }); - const externalProvider = server.create('external-provider', { shareSource: 'ClinicalTrials.gov' }); - server.createList('external-registration', 3, { provider: externalProvider }); - server.createList('registration', 3, { provider: osfProvider }); - server.createList('registration', 3, { provider: anotherProvider }); - }); - - test('page renders with all functionality', async assert => { - await visit('/registries/discover'); - await click('[data-test-sort-dropdown]'); - await percySnapshot('happy path'); - - const osfProvider = server.schema.registrationProviders.find('osf'); - const registrationIds = server.schema.registrations.all().models.map(item => item.id); - for (const id of registrationIds) { - assert.dom(`[data-test-result-title-id="${id}"]`).exists(); - } - assert.dom('[data-test-sort-dropdown]').exists('Sort dropdown exists'); - assert.dom('[data-test-active-filter]').doesNotExist('No filters are applied by default'); - assert.dom('[data-test-source-filter-id]').exists({ count: 3 }, 'Three sources exist'); - assert.dom('[data-test-page-number]').doesNotExist('No pagination for less than 10 registrations'); - - const searchableReg = server.schema.registrations.first(); - await fillIn('[data-test-search-box]', searchableReg.title); - assert.dom(`[data-test-result-title-id='${searchableReg.id}']`).exists('Search shows appropriate result'); - - await fillIn('[data-test-search-box]', ''); - - await click(`[data-test-source-filter-id="${osfProvider.shareSource}"]`); - assert.dom('[data-test-result-title-id]').exists({ count: 3 }, 'Provider filter works'); - - await fillIn('[data-test-search-box]', 'kjafnsdflkjhsdfnasdkndfa random string'); - assert.dom('[data-test-no-results-placeholder]').hasText(t('registries.discover.no_results')); - assert.dom('[data-test-result-title-id]').doesNotExist('No results rendered'); - }); - - test('paginator works', async assert => { - server.createList('registration', 2, { provider: server.schema.registrationProviders.first() }); - - await visit('/registries/discover/'); - - // Count is 4 including previous and next buttons - assert.dom('[data-test-page-number]').exists({ count: 4 }, 'Exactly two pages of results'); - assert.dom('[data-test-page-number="1"]').exists(); - assert.dom('[data-test-page-number="2"]').exists(); - assert.dom('[data-test-results-count]').hasText(t('registries.discover.registration_count', { count: 11 })); - - assert.dom('[data-test-result-title-id]').exists({ count: 10 }, 'First page has correct number of results'); - - await click('[data-test-page-number="2"]'); - assert.dom('[data-test-result-title-id]').exists({ count: 1 }, 'Second page has correct number of results'); - }); - - test('initial state from query params', async assert => { - const anotherProvider = server.schema.registrationProviders.find('another'); - const searchableReg = anotherProvider.registrations.models[0]; - - await visit(`/registries/discover?provider=${anotherProvider.shareSource}&q=${searchableReg.title}`); - - await percySnapshot('with initial query params'); - - assert.dom('[data-test-search-box]').hasValue(searchableReg.title, 'Search box has initial value'); - - assert.dom(`[data-test-source-filter-id='${anotherProvider.shareSource}']`).isChecked(); - assert.dom( - `[data-test-source-filter-id]:not([data-test-source-filter-id='${anotherProvider.shareSource}'])`, - ).isNotChecked(); - - assert.dom('[data-test-result-title-id]').exists({ count: 1 }, 'Initial search uses initial params'); - }); -}); diff --git a/tests/engines/registries/acceptance/landing-page-test.ts b/tests/engines/registries/acceptance/landing-page-test.ts index 1231065a1c5..f7f755250b6 100644 --- a/tests/engines/registries/acceptance/landing-page-test.ts +++ b/tests/engines/registries/acceptance/landing-page-test.ts @@ -28,10 +28,4 @@ module('Registries | Acceptance | landing page', hooks => { assert.dom('[data-test-search-box]').exists(); await percySnapshot(assert); }); - - test('visiting /registries/discover', async assert => { - await visit('/registries/discover/'); - assert.dom('[data-test-results]').exists(); - await percySnapshot(assert); - }); }); diff --git a/tests/engines/registries/integration/discover/discover-test.ts b/tests/engines/registries/integration/discover/discover-test.ts deleted file mode 100644 index 84279fc1532..00000000000 --- a/tests/engines/registries/integration/discover/discover-test.ts +++ /dev/null @@ -1,380 +0,0 @@ -import { click, fillIn, getRootElement, triggerEvent } from '@ember/test-helpers'; -import { setupMirage } from 'ember-cli-mirage/test-support'; -import { TestContext } from 'ember-test-helpers'; -import { OrderedSet, ValueObject } from 'immutable'; -import { module, test } from 'qunit'; -import sinon from 'sinon'; - -import { setupOSFApplicationTest, visit } from 'ember-osf-web/tests/helpers'; -import { loadEngine } from 'ember-osf-web/tests/helpers/engines'; -import param from 'ember-osf-web/utils/param'; - -import { SearchOptions, SearchOrder, SearchResults } from 'registries/services/search'; -import ShareSearch, { - ShareRegistration, - ShareTermsAggregation, - ShareTermsFilter, -} from 'registries/services/share-search'; - -const equals = (expected: ValueObject) => sinon.match((x: any) => expected.equals(x)); - -const emptyResults: SearchResults = { - total: 0, - results: [], - aggregations: { - sources: { - buckets: [], - }, - }, -}; - -// Default page ordering -const order = new SearchOrder({ - ascending: true, - display: 'registries.discover.order.relevance', - key: undefined, -}); - -const QueryParamTestCases: Array<{ - name: string, - params: { [key: string]: any }, - expected: { [key: string]: any }, - }> = [{ - name: 'Order by date_modified if no additional options are specified', - params: {}, - expected: { - query: '', - order: new SearchOrder({ - display: 'registries.discover.order.relevance', - ascending: false, - key: 'date_modified', - }), - }, - }, { - name: 'Basic query parameters', - params: { q: 'What', page: 10 }, - expected: { order, query: 'What', page: 10 }, - }, { - name: 'Providers Filters', - params: { q: 'Foo', provider: 'OSF Registries' }, - expected: { - order, - query: 'Foo', - filters: OrderedSet([ - new ShareTermsFilter('sources', 'OSF Registries', 'OSF Registries'), - ]), - }, - }, { - name: 'Multiple Providers Filters With Validation', - params: { q: 'Foo', provider: 'OSF Registries|ClinicalTrials.gov|Bar' }, - expected: { - order, - query: 'Foo', - filters: OrderedSet([ - new ShareTermsFilter('sources', 'OSF Registries', 'OSF Registries'), - new ShareTermsFilter('sources', 'ClinicalTrials.gov', 'ClinicalTrials.gov'), - ]), - }, - }, { - name: 'Sort', - params: { sort: 'date' }, - expected: { - query: '', - order: new SearchOrder({ - ascending: true, - display: 'registries.discover.order.modified_ascending', - key: 'date', - }), - }, - }, { - name: 'Sort decending', - params: { sort: '-date' }, - expected: { - query: '', - order: new SearchOrder({ - ascending: false, - display: 'registries.discover.order.modified_descending', - key: 'date', - }), - }, - }, { - name: 'Sort validation', - params: { q: 'Not Empty', sort: '-date_moodified' }, - expected: { order, query: 'Not Empty' }, - }, { - name: 'Registration Types without OSF', - params: { q: 'What', page: 10, type: 'Foo|BAR' }, - expected: { order, query: 'What', page: 10 }, - }, { - // NOTE: Not currently validated :( - name: 'Registration Types', - params: { q: 'What', page: 10, provider: 'OSF Registries', type: 'Foo|BAR' }, - expected: { - order, - query: 'What', - page: 10, - filters: OrderedSet([ - new ShareTermsFilter('sources', 'OSF Registries', 'OSF Registries'), - new ShareTermsFilter('registration_type', 'Foo', 'Foo'), - new ShareTermsFilter('registration_type', 'BAR', 'BAR'), - ]), - }, - }]; - -module('Registries | Integration | discover', hooks => { - setupOSFApplicationTest(hooks); - setupMirage(hooks); - - hooks.beforeEach(async function(this: TestContext) { - server.create('registration-schema', { name: 'Open Ended' }); - server.create('registration-schema', { name: 'Close Fronted' }); - server.create('registration-provider', { - id: 'osf', - shareSource: 'OSF Registries', - name: 'OSF Registries', - }); - server.create('registration-provider', { - id: 'someother', - shareSource: 'someother', - name: 'Some Other', - }); - server.create('registration-provider', { - id: 'clinicaltrials', - shareSource: 'ClinicalTrials.gov', - name: 'ClinicalTrials.gov', - }); - - const engine = await loadEngine('registries', 'registries'); - - const shareSearch = ShareSearch.create(); - - engine.register('service:share-search', shareSearch, { instantiate: false }); - this.owner.register('service:share-search', shareSearch, { instantiate: false }); - }); - - test('query parameters', async function(this: TestContext, assert: Assert) { - const stub = sinon.stub(this.owner.lookup('service:share-search'), 'registrations').returns(emptyResults); - - // Initial load so we don't have to deal with the aggregations loading - await visit('/registries/discover'); - assert.ok(stub.calledTwice, 'stub called twice upon discover page load'); - // Aggregations on load - assert.ok(stub.calledWith(new SearchOptions({ - size: 0, - modifiers: OrderedSet([ - new ShareTermsAggregation('sources', 'sources'), - ]), - })), 'stub called with the expected arguments'); - - for (const url of ['/--registries/registries/discover', '/registries/discover']) { - for (const testCase of QueryParamTestCases) { - stub.reset(); - stub.returns(emptyResults); - const params = param(testCase.params); - - await visit(`${url}?${params}`); - - assert.ok(true, testCase.name); - assert.ok(stub.calledOnce, 'stub called once'); - assert.ok( - stub.calledWith(new SearchOptions(testCase.expected)), - `stub called with the expected arguments for: url: ${url}, params: ${params}`, - ); - } - } - }); - - test('page resets on filtering', async function(this: TestContext, assert) { - assert.expect(3); - const stub = sinon.stub(this.owner.lookup('service:share-search'), 'registrations').returns({ - total: 0, - results: [], - aggregations: { - sources: { - buckets: [ - { key: 'OSF Registries', doc_count: 10 }, - { key: 'someother', doc_count: 10 }, - { key: 'clinicaltrials', doc_count: 10 }, - ], - }, - }, - }); - - await visit('/registries/discover?page=10'); - - assert.ok(stub.calledWith(new SearchOptions({ - query: '', - page: 10, - order: new SearchOrder({ - display: 'registries.discover.order.relevance', - ascending: false, - key: 'date_modified', - }), - })), '/registries/discover?page=10: stub called with expected arguments'); - - await click('[data-test-source-filter-id="OSF Registries"]'); - - assert.ok(stub.calledWith(new SearchOptions({ - query: '', - page: 1, - order, - filters: OrderedSet([ - new ShareTermsFilter('sources', 'OSF Registries', 'OSF Registries'), - ]), - })), 'stub called with expected arguments'); - }); - - test('page resets on sorting', async function(this: TestContext, assert) { - assert.expect(3); - const stub = sinon.stub(this.owner.lookup('service:share-search'), 'registrations').returns({ - total: 0, - results: [], - aggregations: { - sources: { - buckets: [{ key: 'OSF Registries', doc_count: 10 }], - }, - }, - }); - - await visit('/registries/discover?page=10'); - - assert.ok(stub.calledWith(new SearchOptions({ - query: '', - page: 10, - order: new SearchOrder({ - display: 'registries.discover.order.relevance', - ascending: false, - key: 'date_modified', - }), - })), 'stub for service:share-search.registrations called with expected arguments'); - - await click('[data-test-sort-dropdown]'); - await click('[data-test-sort-option-id="1"]'); - - assert.ok(stub.calledWith(new SearchOptions({ - query: '', - page: 1, - order: new SearchOrder({ - ascending: true, - display: 'registries.discover.order.modified_ascending', - key: 'date', - }), - })), 'stub for service:share-search.registrations called with expected arguments'); - }); - - test('page resets on typing query', async function(this: TestContext, assert) { - assert.expect(3); - const stub = sinon.stub(this.owner.lookup('service:share-search'), 'registrations').returns({ - total: 0, - results: [], - aggregations: { - sources: { - buckets: [{ key: 'OSF Registries', doc_count: 10 }], - }, - }, - }); - - await visit('/registries/discover?page=10'); - - assert.ok(stub.calledWith(equals(new SearchOptions({ - query: '', - page: 10, - order: new SearchOrder({ - display: 'registries.discover.order.relevance', - ascending: false, - key: 'date_modified', - }), - })))); - - await fillIn('[data-test-search-box]', 'Test Query'); - - assert.ok(stub.calledWith(equals(new SearchOptions({ - query: 'Test Query', - page: 1, - order: new SearchOrder({ - display: 'registries.discover.order.relevance', - ascending: true, - key: undefined, - }), - })))); - }); - - test('page resets on clicking search', async function(this: TestContext, assert) { - assert.expect(3); - sinon.stub(this.owner.lookup('service:analytics'), 'click'); - const stub = sinon.stub(this.owner.lookup('service:share-search'), 'registrations').returns({ - total: 0, - results: [], - aggregations: { - sources: { - buckets: [{ key: 'OSF Registries', doc_count: 10 }], - }, - }, - }); - - await visit('/registries/discover?page=10&q=Testing'); - - assert.ok(stub.calledWith(equals(new SearchOptions({ - query: 'Testing', - page: 10, - order: new SearchOrder({ - display: 'registries.discover.order.relevance', - ascending: true, - key: undefined, - }), - })))); - - await triggerEvent('[data-test-search-form]', 'submit'); - - assert.ok(stub.calledWith(equals(new SearchOptions({ - query: 'Testing', - page: 1, - order: new SearchOrder({ - display: 'registries.discover.order.relevance', - ascending: true, - key: undefined, - }), - })))); - }); - - test('scroll top on pagination', async function(this: TestContext, assert: Assert) { - assert.expect(3); - const results = { - total: 21, - results: Array(21).fill({ - title: 'place holder', - description: 'place holder', - contributors: [], - mainLink: 'fakeLink', - }), - aggregations: { - sources: { - buckets: [], - }, - }, - }; - - const stub = sinon.stub(this.owner.lookup('service:share-search'), 'registrations').returns(results); - - await visit('/registries/discover'); - - stub.reset(); - stub.returns(results); - - const resultsEl = getRootElement().querySelector('[data-test-results]')! as HTMLElement; - - const initialTopPosition = resultsEl.getBoundingClientRect().top; - await click('[data-test-page-number="2"]'); - const currentTopPosition = resultsEl.getBoundingClientRect().top; - assert.ok(currentTopPosition < initialTopPosition, 'we have scrolled'); - assert.ok(stub.calledWith(new SearchOptions({ - query: '', - page: 2, - order: new SearchOrder({ - display: 'registries.discover.order.relevance', - ascending: false, - key: 'date_modified', - }), - })), 'stub called with expected arguments'); - }); -}); diff --git a/tests/engines/registries/integration/index/index-test.ts b/tests/engines/registries/integration/index/index-test.ts deleted file mode 100644 index a60e27b525d..00000000000 --- a/tests/engines/registries/integration/index/index-test.ts +++ /dev/null @@ -1,153 +0,0 @@ -import EngineInstance from '@ember/engine/instance'; -import { click, fillIn, triggerKeyEvent } from '@ember/test-helpers'; -import { setupMirage } from 'ember-cli-mirage/test-support'; -import { TestContext } from 'ember-test-helpers'; -import { module, skip } from 'qunit'; -import sinon from 'sinon'; - -import { stubRegistriesShareSearch } from 'ember-osf-web/tests/engines/registries/helpers'; -import { visit } from 'ember-osf-web/tests/helpers'; -import { setupEngineApplicationTest } from 'ember-osf-web/tests/helpers/engines'; - -import HeaderStyles from 'registries/components/registries-header/styles'; -import ServiceListStyles from 'registries/components/registries-services-list/styles'; - -module('Registries | Integration | index', hooks => { - setupEngineApplicationTest(hooks, 'registries'); - setupMirage(hooks); - - hooks.beforeEach(function(this: TestContext) { - stubRegistriesShareSearch(this); - - const engine = this.owner.lookup('-engine-instance:registries-registries') as EngineInstance; - const analytics = engine.lookup('service:analytics'); - analytics.set('click', sinon.stub(analytics, 'click')); - analytics.actions.click = analytics.click; - }); - - skip('analytics', async function(this: TestContext, assert: Assert) { - const testCases: Array<{ - name: string, - action: () => Promise, - // category, label, extra - expected: Array, - }> = [ - { - name: 'Search Button (Clicked)', - action: async () => click('button'), - expected: ['link', 'Index - Search', ''], - }, - { - name: 'Search Button (Clicked, With query)', - action: async () => { - await fillIn('[data-test-search-box]', 'My Query'); - await click('[data-test-search-button]'); - }, - expected: ['link', 'Index - Search', 'My Query'], - }, - { - name: 'Search Button (Submitted)', - action: async () => triggerKeyEvent('[data-test-search-box]', 'keydown', 13), - expected: ['link', 'Index - Search', ''], - }, - { - name: 'Search Button (Submitted, With query)', - action: async () => { - await fillIn('[data-test-search-box]', 'My Query'); - await triggerKeyEvent('[data-test-search-box]', 'keydown', 13); - }, - expected: ['link', 'Index - Search', 'My Query'], - }, - { - name: 'See Example', - action: async () => click( - `.${HeaderStyles.RegistriesHeader} a`, - ), - expected: [ - 'link', - 'Index - See Example', - 'https://osf.io/jsznk/register/565fb3678c5e4a66b5582f67', - sinon.match.any, - ], - }, - { - name: 'Browse Recent (id = 1)', - action: async () => click('[data-test-recent-registration-id="1"]'), - expected: [ - 'link', - 'Index - Browse Recent: Can Potatoes Cause Cancer?', - 'https://example.com/cancer-potatoes', - ], - }, - { - name: 'Browse Recent (id = 2)', - action: async () => click('[data-test-recent-registration-id="2"]'), - expected: [ - 'link', - 'Index - Browse Recent: Can Potatoes Cure Cancer?', - 'https://example.com/super-potatoes', - ], - }, - { - name: 'GitHub Repo', - action: async () => click( - `a.${ServiceListStyles.ServicesList__ExternalLink}:first-child`, - ), - expected: [ - 'link', - 'Index - GitHub Repo', - 'https://github.com/CenterForOpenScience/ember-osf-web', - sinon.match.any, - ], - }, - { - name: 'Requirements and Roadmap', - action: async () => click( - `a.${ServiceListStyles.ServicesList__ExternalLink}:last-child`, - ), - expected: [ - 'link', - 'Index - public roadmap', - 'https://cos.io/our-products/product-roadmap/', - sinon.match.any, - ], - }, - { - name: 'Contact Us', - action: async () => click('a.btn.btn-info.btn-lg'), - expected: [ - 'link', - 'Index - Contact', - sinon.match.any, - ], - }, - ]; - - // 1 extra assertion from setupOSFApplicationTest - assert.expect((testCases.length * 3) + 1); - const engine = this.owner.lookup('-engine-instance:registries-registries') as EngineInstance; - const stub = engine.lookup('service:analytics').click; - - for (const testCase of testCases) { - stub.reset(); - stub.callsFake((...args: any[]) => { - const event = args[args.length - 1] as MouseEvent; - // Prevent redirects from being followed - if (!event.preventDefault) { - return true; - } - - event.preventDefault(); - return false; - }); - - await visit('/registries'); - - await testCase.action(); - - assert.ok(true, testCase.name); - sinon.assert.calledOnce(stub); - sinon.assert.calledWith(stub, ...testCase.expected); - } - }); -}); diff --git a/tests/integration/components/branded-header/component-test.ts b/tests/integration/components/branded-header/component-test.ts new file mode 100644 index 00000000000..56fa054ebb4 --- /dev/null +++ b/tests/integration/components/branded-header/component-test.ts @@ -0,0 +1,53 @@ +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; +import { setupMirage } from 'ember-cli-mirage/test-support'; +import { setupRenderingTest } from 'ember-qunit'; +import { TestContext } from 'ember-test-helpers'; +import { module, test } from 'qunit'; +import sinon from 'sinon'; + +module('Integration | Component | branded-header', hooks => { + setupRenderingTest(hooks); + setupMirage(hooks); + + hooks.beforeEach(function(this: TestContext) { + // this.store = this.owner.lookup('service:store'); + }); + + test('default display', async function(assert) { + // Given the input variablies are set + this.set('onSearch', sinon.fake()); + this.set('searchPlaceholder', 'preprints.header.search_placeholder'); + + // When the component is rendered + await render(hbs` + `); + + // Then the header container is verified + const headerContainer = this.element.querySelector('[data-test-header-container]'); + assert.dom(headerContainer).exists(); + + // Then the header container is verified + assert.dom(this.element.querySelector('[data-test-brand-logo]')).hasAttribute('aria-label', 'OSF Preprints'); + + // eslint-disable-next-line max-len + assert.dom(this.element.querySelector('[data-test-perform-search-button]')).hasAttribute('aria-label', 'Perform search'); + assert.dom(this.element.querySelector('[data-test-perform-search-button]')).hasAttribute('type', 'button'); + + // eslint-disable-next-line max-len + assert.dom(this.element.querySelector('[data-test-search-icon]')).hasAttribute('data-icon', 'search'); + + // eslint-disable-next-line max-len + assert.dom(this.element.querySelector('[data-test-search-box]')).hasAttribute('placeholder', 'preprints.header.search_placeholder'); + + // eslint-disable-next-line max-len + assert.dom(this.element.querySelector('[data-test-search-help-button]')).hasAttribute('aria-label', 'Search help'); + assert.dom(this.element.querySelector('[data-test-search-help-button]')).hasAttribute('type', 'button'); + }); +}); diff --git a/tests/integration/components/open-badges-list/component-test.ts b/tests/integration/components/open-badges-list/component-test.ts index 6d6847cf8ad..971bf322cab 100644 --- a/tests/integration/components/open-badges-list/component-test.ts +++ b/tests/integration/components/open-badges-list/component-test.ts @@ -37,6 +37,7 @@ module('Integration | Component | open-badges-list', hooks => { @hasPapers={{true}} @hasSupplements={{true}} @registration='guid1' + @verticalLayout={{true}} />`); assert.dom('[data-test-badge-list-title]') .hasText(t('osf-components.open-badges-list.title'), 'Title shows in desktop'); @@ -52,6 +53,7 @@ module('Integration | Component | open-badges-list', hooks => { @hasPapers={{false}} @hasSupplements={{false}} @registration='guid1' + @verticalLayout={{true}} />`); assert.dom('[data-test-badge-list-title]') .doesNotExist('Title does not show in mobile'); @@ -65,6 +67,7 @@ module('Integration | Component | open-badges-list', hooks => { @hasAnalyticCode={{true}} @hasMaterials={{true}} @registration='guid1' + @verticalLayout={{true}} />`); assert.dom('[data-test-badge-image="data"]').hasAttribute( 'src', @@ -113,6 +116,7 @@ module('Integration | Component | open-badges-list | open-badge-card', hooks => @resourceType='data' @isMobile={{false}} @registration='guid1' + @verticalLayout={{true}} />`); assert.dom('[data-test-badge-image="data"]').hasAttribute( 'src', @@ -138,6 +142,7 @@ module('Integration | Component | open-badges-list | open-badge-card', hooks => @resourceType='materials' @isMobile={{true}} @registration='guid1' + @verticalLayout={{true}} />`); assert.dom('[data-test-badge-image="materials"]').hasAttribute( 'src', @@ -168,6 +173,7 @@ module('Integration | Component | open-badges-list | open-badge-card', hooks => @resourceType='materials' @isMobile={{true}} @registration='guid1' + @verticalLayout={{true}} />`); assert.dom('[data-test-badge-image="materials"]').hasAttribute( 'src', diff --git a/tests/integration/components/subscriptions/component-test.ts b/tests/integration/components/subscriptions/component-test.ts index 4f18d04cdf1..8f95e1fc12f 100644 --- a/tests/integration/components/subscriptions/component-test.ts +++ b/tests/integration/components/subscriptions/component-test.ts @@ -2,6 +2,7 @@ import { click, render } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { setupIntl, TestContext } from 'ember-intl/test-support'; +import RegistrationProviderModel from 'ember-osf-web/models/registration-provider'; import { SubscriptionFrequency } from 'ember-osf-web/models/subscription'; import { clickTrigger } from 'ember-power-select/test-support/helpers'; import { setupRenderingTest } from 'ember-qunit'; diff --git a/tests/integration/helpers/sufficient-contrast-test.ts b/tests/integration/helpers/sufficient-contrast-test.ts new file mode 100644 index 00000000000..8f66b86f7c3 --- /dev/null +++ b/tests/integration/helpers/sufficient-contrast-test.ts @@ -0,0 +1,66 @@ +/* eslint-disable max-len */ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { render } from '@ember/test-helpers'; +import hbs from 'htmlbars-inline-precompile'; + +module('Integration | Helper | sufficient-contrast', function(hooks) { + setupRenderingTest(hooks); + + test('it calculates normal text for AA', async function(assert) { + // 21:1 ratio + await render(hbs`{{if (sufficient-contrast '#000' '#fff') 'good contrast' 'poor contrast'}}`); + assert.equal(this.element.textContent!.trim(), 'good contrast', '21:1 passes AA using three digit hex colors'); + // 4.6:1 ratio + await render(hbs`{{if (sufficient-contrast '#757575' '#fff') 'good contrast' 'poor contrast'}}`); + assert.equal(this.element.textContent!.trim(), 'good contrast', '4.6:1 passes AA'); + // 4.47:1 ratio + await render(hbs`{{if (sufficient-contrast '#fff' '#777') 'good contrast' 'poor contrast'}}`); + assert.equal(this.element.textContent!.trim(), 'poor contrast', '4.47:1 fails AA'); + }); + + test('it calculates large text AA', async function(assert) { + // 3.26:1 ratio + await render( + hbs`{{if (sufficient-contrast '#0090FF' '#fff' largeText=true) 'good contrast' 'poor contrast'}}`, + ); + assert.equal(this.element.textContent!.trim(), 'good contrast', '3.26:1 passes AA Large Text'); + // 2.81:1 ratio + await render( + hbs`{{if (sufficient-contrast '#fff' '#00A0FF' largeText=true) 'good contrast' 'poor contrast'}}`, + ); + assert.equal(this.element.textContent!.trim(), 'poor contrast', '2.81:1 fails AA Large Text'); + }); + + test('it calculates normal text AAA', async function(assert) { + // 7.2:1 ratio + await render( + hbs`{{if (sufficient-contrast '#50AA50' '#000' useAAA=true) 'good contrast' 'poor contrast'}}`, + ); + assert.equal(this.element.textContent!.trim(), 'good contrast', '7.2:1 passes AAA'); + // 6.48:1 ratio + await render( + hbs`{{if (sufficient-contrast '#000' '#50A050' useAAA=true) 'good contrast' 'poor contrast'}}`, + ); + assert.equal(this.element.textContent!.trim(), 'poor contrast', '6.48:1 fails AAA'); + }); + + test('it calculates large text AAA', async function(assert) { + // 6.48:1 ratio + await render( + hbs`{{if (sufficient-contrast '#000' '#50AA50' largeText=true useAAA=true) 'good contrast' 'poor contrast'}}`, + ); + assert.equal(this.element.textContent!.trim(), 'good contrast', '6.48:1 passes AAA Large Text'); + // 4.49:1 ratio + await render( + hbs`{{if (sufficient-contrast '#333' '#00A0FF' largeText=true useAAA=true) 'good contrast' 'poor contrast'}}`, + ); + assert.equal(this.element.textContent!.trim(), 'poor contrast', '4.49:1 fails AAA Large Text'); + // 4.53:1 ratio + await render( + hbs`{{if (sufficient-contrast '#00A0FF' '#303333' largeText=true useAAA=true) 'good contrast' 'poor contrast'}}`, + ); + assert.equal(this.element.textContent!.trim(), 'good contrast', '4.53:1 passes AAA Large Text'); + }); + +}); 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 de875b8848b..01b34388327 100644 --- a/translations/en-us.yml +++ b/translations/en-us.yml @@ -24,6 +24,8 @@ documentType: pluralCapitalized: Theses singular: thesis singularCapitalized: Thesis +contact: + email: support@osf.io general: OSF: OSF share: Share @@ -37,6 +39,7 @@ general: cancel: Cancel add: Add ok: OK + apply: Apply revisions: Revisions md5: MD5 date: Date @@ -208,6 +211,64 @@ dashboard: registries: 'OSF Registries' preprints: 'OSF Preprints' institutions: 'OSF Institutions' + +search: + page-title: 'Search' + index-card: + no-label: 'No label found' + no-title: 'No title found' + search-help: + header-1: 'Improved OSF Search' + header-2: 'OSF Smart Facets' + header-3: 'Add Metadata' + body-1: 'Enter any term in the search box and filter by specific object types.' + body-2: 'Narrow the source, discipline, and more. For example, find content supported by a specific funder or view only datasets.' + body-3: 'Remember to add metadata and resources to your own work on OSF to make it more discoverable!' + index-1: '1 of 3' + index-2: '2 of 3' + index-3: '3 of 3' + footer: + skip: 'Skip' + next: 'Next' + done: 'Done' + search-header: 'Search OSF' + textbox-placeholder: 'Search placeholder' + search-button-label: 'Search' + search-help-label: 'Help tutorial' + total-results: '{count} results' + no-results: 'No results found' + 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 + 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' + institutions: + institution-logo: 'Logo for ' +helpers: + get-localized-property: + not-provided: 'Not provided' + new_project: header: 'Create new project' title_placeholder: 'Enter project title' @@ -260,7 +321,7 @@ move_to_project: go_to_project: 'Go to project' navbar: add: Add - # add_a_preprint: 'Add a {preprintWords.preprint}' + add_a_preprint: 'Add a {preprintWord}' add_registration: 'Add New' moderation: 'Moderation' browse: Browse @@ -268,6 +329,7 @@ navbar: donate: Donate go_home: 'Go home' my_projects: 'My Projects' + my_preprints: 'My Preprints' my_registrations: 'My Registrations' reviews: 'My Reviewing' search: Search @@ -735,6 +797,8 @@ app_components: brand_color_inputs: primary_label: 'Brand Primary Color' primary_set: 'Set Primary Color' + secondary_label: 'Brand Secondary Color' + secondary_set: 'Set Secondary Color' error_page: email_message: 'If this should not have occurred and the issue persists, please report it to' go_to: 'Go to {brand}' @@ -1043,6 +1107,48 @@ collections: accept: accepted reject: rejected remove: 'removed' +preprints: + provider-title: '{name} {pluralizedPreprintWord}' + discover: + title: 'Search' + title: 'Preprints' + header: + osf_registrations: 'OSF Preprints' + registrations: 'Preprints' + search_placeholder: 'Search {placeholder} ...' + search_button: 'Perform search' + search_label: 'Search' + search_help: 'Search help' + powered_by: 'Powered by OSF Preprints' + or: 'or' + submit_label: 'Submit a preprint' + example: 'See an example' + subjects: + heading: + provider: 'Browse by provider' + hasHighlightedSubjects: 'Browse by featured subjects' + noHighlightedSubjects: 'Browse by subjects' + links: + seeAllSubjects: 'See all subjects available' + services: + top: + # heading: '{documentType.singularCapitalized} Services' + heading: '{documentType} Services' + # paragraph: 'Leading {documentType.singular} service providers use this open source infrastructure to support their communities.' + paragraph: 'Leading {documentType} service providers use this open source infrastructure to support their communities.' + bottom: + contact: 'Contact us' + # p1: 'Create your own branded {documentType.singular} servers backed by the OSF.' + p1: 'Create your own branded {documentType} servers backed by the OSF.' + div: + line1: 'Check out the' + linkText1: 'open source code' + line2: 'and our' + linkText2: 'public roadmap' + line3: '. Input welcome!' + advisory: + heading: 'Advisory Group' + paragraph: 'Our advisory group includes leaders in preprints and scholarly communication' registries: header: osf_registrations: 'OSF Registrations' @@ -1783,6 +1889,35 @@ routes: institution: Institution email: Email osf-components: + search-result-card: + resource_nature: Resource nature + title: Title + collection: Collection + context: Context + funder: Funder + registration_template: Registration template + doi: DOI + description: Description + license: License + from: From + date_created: Date created + date_registered: Date registered + date_modified: Date modified + last_edited: Last edited + member_since: Member since + preprint_provider: Preprint provider + conflict_of_interest: Conflict of Interest response + no_conflict_of_interest: 'Author asserted no Conflict of Interest' + associated_data: Associated data + associated_analysis_plan: Associated preregistration + associated_study_design: Associated study design + project: Project + registration: Registration + preprint: Preprint + file: File + user: User + project_component: Project component + registration_component: Registration component resources-list: add_instructions: 'Link a DOI from a repository to your registration by clicking the green “+” button.' add_instructions_adhere: 'Contributors affirmed to adhere to the criteria for each badge.' diff --git a/types/osf-api.d.ts b/types/osf-api.d.ts index 8e3ce81b0b3..438c2df5134 100644 --- a/types/osf-api.d.ts +++ b/types/osf-api.d.ts @@ -89,6 +89,7 @@ export interface RelatedLinkMeta { id?: string; count?: number; type?: string; + has_highlighted_subjects?: boolean; } export interface NormalLinks extends JSONAPI.Links {