From e9c7912f3e5a05384970f86ce53a9c6567c4b637 Mon Sep 17 00:00:00 2001 From: futa-ikeda <51409893+futa-ikeda@users.noreply.github.com> Date: Mon, 28 Aug 2023 17:21:38 -0400 Subject: [PATCH] Feature/search improvements (#1967) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add new route for search (#1830) * [ENG-4450] Add new share-search models (#1835) - Ticket: [ENG-4450] - Feature flag: n/a ## Purpose - Add new models needed for SHARE-powered search page ## Summary of Changes - Add new models - `metadata-record-search` - `metadata-property-search` - `metadata-value-search` - `metadata-record` - `search-match` - New `ShareAdapter` and `ShareSerializer` to be used by these new models - New mirage endpoint for metadata-record-search (other endpoints coming later) * Add basic search page layout (#1850) * [ENG-4465] Left panel facets manager (#1858) - Ticket: [ENG-4465] [ENG-4466] - Feature flag: n/a ## Purpose - Add logic to search page controller to handle active filters and list of filterable properties - Add a component to handle fetching values in a filterable properties in the search page ## Summary of Changes - Add a `filter-facet` component - takes care of fetching filterable property values - `See more` modal * Add route analytics metadata (#1865) * [ENG-4469] Add object filter and sort dropdown to search (#1864) - Ticket: [ENG-4469] - Feature flag: n/a ## Purpose - Add object type filter and sort dropdown to search page ## Summary of Changes - Add tabs to filter by object type (All, Projects, Registrations, Preprints, Files, Users) - Add dropdown to sort results by Relevance, Date modified/created ascending and descending - Change model names to reflect more library-analogy based names - Change how metadata properties are fetched from SHARE models * [ENG-4470] Reroute to new search (#1870) - Ticket: [ENG-4470] - Feature flag: n/a ## Purpose - reroute users to the new search page instead of registries discover - Use OsfLink for navbar link to search page to avoid a full app reload ## Summary of Changes - Change search behavior on registries landing page (osf.io/registries) - Rewrite how osf-navbar handles "Search" button to use OsfLink * [No ticket] Update SHARE endpoints (#1879) - Ticket: [No ticket] - Feature flag: n/a ## Purpose - Update SHAREAdapter to point to correct locations ## Summary of Changes - Update SHAREAdapter parent class to point use config variable for share-url - Update SHAREAdapter parent class to point use api/v3 endpoints - Update search-related adapters to point to singularized endpoint names (e.g. api/v3/index-card-search**es** -> api/v3/index-card-search - Update mirage endpoints to reflect these changes * [ENG-4568] Componentize search page (#1886) - Ticket: [ENG-4568] - Feature flag: n/a ## Purpose - Componentize search page for reuse in branded pages ## Summary of Changes - Move logic and templating from search page route to `search-page` component - No logic for branding and default query-params yet in this PR * [ENG-4570] Add institution search placeholder route (#1888) * add institution search placeholder route * modify route path * modify route path * change route name and path * add route for preprint discover page (#1890) * [ENG-4574] Preprint discover rewrite (#1896) * add brand relationship to preprint provider model (#1887) * Remove unused services from search controller * Use search-page component on preprint discover page * Modifiy branded-navbar for preprints * Error handling and theme resetting * Branded preprint discover part 1 * Branded preprint discover part 2 * Test prerpint discover page * Group CR feedback re: search-page component arguments * Fix test --------- Co-authored-by: Yuhuai Liu * [ENG-4573] Registry discover (#1900) * preliminary * moar * some more * delete unused components * remove top-level aggregate registries discover route * remove top-level registries discover route cont. * remove unused action and variable on registries application route * remove aggregate registries discover page tests * fix tests * remove discover-test.ts * CR followup * [ENG-4574] Preprint discover fixes (#1905) - Ticket: [ENG-4574] - Feature flag: n/a ## Purpose - Add appropriate page title to discover page - Add appropriate analytics scope to discover page - Make provider description now show html entities ## Summary of Changes - Use `{{html-safe}}` when showing provider description - Add `providerTitle` in preprint-provider model - Most branded providers should show their name with their preprint word (e.g. AfricaRxiv Preprints, MarXiv Papers), except Thesis Commons - If it's OSF, we just show "OSF Preprints" - Add page-title and analytics scope using the new `providerTitle` * Track theme service properties (#1908) * [ENG-4575] Institution dashboard page re-write (#1902) - Ticket: https://openscience.atlassian.net/browse/ENG-4575 - Git branch: feature/search-institutions-rewrite ## Purpose The purpose of these changes is to implement the new search page component for the Institutions Discover page. ## Summary of Changes -Used the new search-page component to re-implement the Institutions dashboard/discover page -Added institution header with description and banner for desktop, logo and description for mobile -Added logic for institution colors and default OSF colors when none provided -Added an affiliated institution filter applied by default to SHARE queries * [ENG-4535] Search help feature (#1907) - Ticket: [ENG-4535] - Feature flag: n/a ## Purpose - Add search help feature - Basically a re-implementation of https://github.com/CenterForOpenScience/ember-osf-web/pull/1891 and https://github.com/CenterForOpenScience/ember-osf-web/pull/1877 - Notable difference is moving the Popovers to the end of the file to avoid merge conflicts ## Summary of Changes - Added EmberPopovers to the search-page component - Added getters to search-page component to fetch EmberPopover targets dynamically - Translations - Tests * [No Ticket] Change queryparam passed to SHARE when filtering by resourceType (#1915) * change queryparam passed to SHARE when filtering by resourceType * add types * add some more types * update tests * [No ticket] Preprint branding rework (#1913) - Ticket: [] - Feature flag: n/a ## Purpose - Only rely on `brand` relationship for setting preprint colors ## Summary of Changes - Use `brand.primaryColor` for branded navbar background color - Add styling if the brand's primaryColor does not provide sufficient contrast with white text - Add special-case for BioHackrXiv to change navbar color to white (their primary color would be white, but that creates problems for ` \ No newline at end of file + + +
+ + diff --git a/lib/osf-components/addon/components/button/component.ts b/lib/osf-components/addon/components/button/component.ts index e222d2985d..7ec3c9a85e 100644 --- a/lib/osf-components/addon/components/button/component.ts +++ b/lib/osf-components/addon/components/button/component.ts @@ -1,4 +1,8 @@ import Component from '@glimmer/component'; +import { inject as service } from '@ember/service'; + +import Theme from 'ember-osf-web/services/theme'; +import PreprintProviderModel from 'ember-osf-web/models/preprint-provider'; const layoutClasses = { small: 'SmallButton', @@ -24,6 +28,8 @@ interface Args { } export default class Button extends Component { + @service theme!: Theme; + get classList(): string { const classes = []; const { layout, type } = this.args; @@ -42,4 +48,13 @@ export default class Button extends Component { return classes.join(' '); } + + get primaryColor(): string { + if (!this.theme?.provider) { + return '#337ab7'; // $color-osf-primary; + } + // Only preprint-providers will have brands that need to be checked for color contrast + const brand = (this.theme.provider as PreprintProviderModel).brand; + return brand ? brand.get('primaryColor') : '#337ab7'; + } } diff --git a/lib/osf-components/addon/components/button/styles.scss b/lib/osf-components/addon/components/button/styles.scss index 6f7c1a92a1..99007bcc62 100644 --- a/lib/osf-components/addon/components/button/styles.scss +++ b/lib/osf-components/addon/components/button/styles.scss @@ -47,6 +47,12 @@ } } +// This should only be used in preprint branding as we move away from using custom CSS +// Please don't rely on this class for new brands +.DarkText { + color: $color-text-black; +} + .SecondaryButton { background-color: $color-bg-white; border: 1px solid $color-border-gray-light; diff --git a/lib/osf-components/addon/components/button/template.hbs b/lib/osf-components/addon/components/button/template.hbs index bfdb6b348b..85e9a7a7d1 100644 --- a/lib/osf-components/addon/components/button/template.hbs +++ b/lib/osf-components/addon/components/button/template.hbs @@ -1,3 +1,6 @@ - diff --git a/lib/osf-components/addon/components/editable-field/tags-manager/component.ts b/lib/osf-components/addon/components/editable-field/tags-manager/component.ts index e7f41764c3..718ba80fb7 100644 --- a/lib/osf-components/addon/components/editable-field/tags-manager/component.ts +++ b/lib/osf-components/addon/components/editable-field/tags-manager/component.ts @@ -2,17 +2,16 @@ import { tagName } from '@ember-decorators/component'; import Component from '@ember/component'; import { action, computed } from '@ember/object'; import { alias, and } from '@ember/object/computed'; +import RouterService from '@ember/routing/router-service'; import { inject as service } from '@ember/service'; import { waitFor } from '@ember/test-waiters'; import { task } from 'ember-concurrency'; -import config from 'ember-get-config'; import Intl from 'ember-intl/services/intl'; import Toast from 'ember-toastr/services/toast'; import { layout } from 'ember-osf-web/decorators/component'; import Registration from 'ember-osf-web/models/registration'; import captureException, { getApiErrorMessage } from 'ember-osf-web/utils/capture-exception'; -import pathJoin from 'ember-osf-web/utils/path-join'; import template from './template'; @@ -25,15 +24,10 @@ export interface TagsManager { registration: Registration; } -const { - OSF: { - url: baseUrl, - }, -} = config; - @tagName('') @layout(template) export default class TagsManagerComponent extends Component { + @service router!: RouterService; // required registration!: Registration; @@ -95,7 +89,7 @@ export default class TagsManagerComponent extends Component { @action clickTag(tag: string): void { - window.location.assign(`${pathJoin(baseUrl, 'search')}?q=(tags:"${encodeURIComponent(tag)}")`); + this.router.transitionTo('search', { queryParams: { q: `${encodeURIComponent(tag)}` } }); } @action diff --git a/lib/osf-components/addon/components/node-card/template.hbs b/lib/osf-components/addon/components/node-card/template.hbs index 5a1fdef3a7..6abbf9e90b 100644 --- a/lib/osf-components/addon/components/node-card/template.hbs +++ b/lib/osf-components/addon/components/node-card/template.hbs @@ -271,6 +271,7 @@ @hasPapers={{@node.hasPapers}} @hasSupplements={{@node.hasSupplements}} @registration={{@node.id}} + @verticalLayout={{true}} /> {{/if}} diff --git a/lib/osf-components/addon/components/open-badges-list/open-badge-card/template.hbs b/lib/osf-components/addon/components/open-badges-list/open-badge-card/template.hbs index e77dd16ce0..f3b9c0ea9a 100644 --- a/lib/osf-components/addon/components/open-badges-list/open-badge-card/template.hbs +++ b/lib/osf-components/addon/components/open-badges-list/open-badge-card/template.hbs @@ -2,8 +2,9 @@ - {{#unless this.isMobile}} -
- {{t 'osf-components.open-badges-list.title'}} -
- {{/unless}} - - - - - - +{{#if @verticalLayout}} +
+ {{#unless this.isMobile}} +
+ {{t 'osf-components.open-badges-list.title'}} +
+ {{/unless}} + + + + + +
+{{else}} +
+ {{t 'osf-components.open-badges-list.title'}} +
+
+ + + + + +
+{{/if}} diff --git a/lib/osf-components/addon/components/osf-dialog/heading/styles.scss b/lib/osf-components/addon/components/osf-dialog/heading/styles.scss index 18f21b6f9d..43ffb9281f 100644 --- a/lib/osf-components/addon/components/osf-dialog/heading/styles.scss +++ b/lib/osf-components/addon/components/osf-dialog/heading/styles.scss @@ -3,14 +3,12 @@ justify-content: space-between; max-height: 15vh; overflow-y: auto; - border-bottom: 1px solid #ddd; } .Heading { padding: 1rem 0.75rem 1rem 1.25rem; margin: 0; - color: $color-text-gray-blue; font-size: 24px; font-weight: 600; @@ -18,4 +16,5 @@ .CloseButton.CloseButton { align-self: flex-start; + margin: 10px; } diff --git a/lib/osf-components/addon/components/osf-layout/component.ts b/lib/osf-components/addon/components/osf-layout/component.ts index 10c29704d0..775193dd3e 100644 --- a/lib/osf-components/addon/components/osf-layout/component.ts +++ b/lib/osf-components/addon/components/osf-layout/component.ts @@ -47,6 +47,11 @@ export default class OsfLayout extends Component { this.toggleProperty('sidenavGutterClosed'); } + @action + openSidenavGutter() { + this.set('sidenavGutterClosed', false); + } + @action toggleMetadata() { this.toggleProperty('metadataGutterClosed'); diff --git a/lib/osf-components/addon/components/osf-layout/template.hbs b/lib/osf-components/addon/components/osf-layout/template.hbs index 65e7f382bf..0bf4dc3d09 100644 --- a/lib/osf-components/addon/components/osf-layout/template.hbs +++ b/lib/osf-components/addon/components/osf-layout/template.hbs @@ -2,6 +2,7 @@ heading=(element 'div') sidenavGutterClosed=this.sidenavGutterClosed toggleSidenav=(action this.toggleSidenav) + openSidenavGutter=(action this.openSidenavGutter) metadataGutterClosed=this.metadataGutterClosed toggleMetadata=(action this.toggleMetadata) )}} @@ -18,6 +19,7 @@ @leftClosed={{this.sidenavGutterClosed}} @rightMode={{this.metadataGutterMode}} @rightClosed={{this.metadataGutterClosed}} + ...attributes as |gutters| > {{yield (hash diff --git a/lib/osf-components/addon/components/osf-navbar/auth-dropdown/styles.scss b/lib/osf-components/addon/components/osf-navbar/auth-dropdown/styles.scss index 2ca45b12ba..f61e96a984 100644 --- a/lib/osf-components/addon/components/osf-navbar/auth-dropdown/styles.scss +++ b/lib/osf-components/addon/components/osf-navbar/auth-dropdown/styles.scss @@ -28,6 +28,7 @@ .auth-trigger.auth-trigger.auth-trigger.auth-trigger { padding: 11px 15px; + cursor: pointer; } @media (max-width: 767px) { diff --git a/lib/osf-components/addon/components/osf-navbar/x-links/component.ts b/lib/osf-components/addon/components/osf-navbar/x-links/component.ts index 8dbb3e7009..b713e64391 100644 --- a/lib/osf-components/addon/components/osf-navbar/x-links/component.ts +++ b/lib/osf-components/addon/components/osf-navbar/x-links/component.ts @@ -15,7 +15,6 @@ const osfURL = config.OSF.url; export default class XLinks extends Component { @service session!: Session; - searchURL = `${osfURL}search/`; myProjectsURL = `${osfURL}myprojects/`; myRegistrationsURL = `${osfURL}myprojects/#registrations`; supportURL = `${config.support.faqPageUrl}`; diff --git a/lib/osf-components/addon/components/osf-navbar/x-links/template.hbs b/lib/osf-components/addon/components/osf-navbar/x-links/template.hbs index 5dc77d9e6c..e43191e9d0 100644 --- a/lib/osf-components/addon/components/osf-navbar/x-links/template.hbs +++ b/lib/osf-components/addon/components/osf-navbar/x-links/template.hbs @@ -15,13 +15,6 @@ text=(t 'navbar.my_registrations') onClicked=this.onLinkClicked ) - search=(component 'osf-navbar/x-links/hyper-link' - this.searchURL - analyticsLabel='Search' - tagName='li' - text=(t 'navbar.search') - onClicked=this.onLinkClicked - ) support=(component 'osf-navbar/x-links/hyper-link' this.supportURL analyticsLabel='Support' @@ -42,7 +35,16 @@ {{yield links}} {{else}} - +
  • + + {{t 'navbar.search'}} + +
  • {{/if}} diff --git a/lib/osf-components/addon/components/search-page/component.ts b/lib/osf-components/addon/components/search-page/component.ts new file mode 100644 index 0000000000..4719bfb774 --- /dev/null +++ b/lib/osf-components/addon/components/search-page/component.ts @@ -0,0 +1,286 @@ +import { tracked } from '@glimmer/tracking'; +import Component from '@glimmer/component'; +import { inject as service } from '@ember/service'; +import IndexCardModel from 'ember-osf-web/models/index-card'; +import { waitFor } from '@ember/test-waiters'; +import { taskFor } from 'ember-concurrency-ts'; +import { task, timeout } from 'ember-concurrency'; +import Intl from 'ember-intl/services/intl'; +import { A } from '@ember/array'; +import Store from '@ember-data/store'; +import { action } from '@ember/object'; +import Media from 'ember-responsive'; + +import { ShareMoreThanTenThousand } from 'ember-osf-web/models/index-card-search'; +import SearchResultModel from 'ember-osf-web/models/search-result'; +import ProviderModel from 'ember-osf-web/models/provider'; +import RelatedPropertyPathModel from 'ember-osf-web/models/related-property-path'; +import uniqueId from 'ember-osf-web/utils/unique-id'; + +import { booleanFilterProperties } from './filter-facet/component'; + +interface ResourceTypeOption { + display: string; + value?: ResourceTypeFilterValue | null; +} + +export enum ResourceTypeFilterValue { + Registrations = 'Registration,RegistrationComponent', + Projects = 'Project,ProjectComponent', + Preprints = 'Preprint', + Users = 'Agent', + Files = 'File', +} + +interface SortOption { + display: string; + value: string; +} + +export interface Filter { + propertyVisibleLabel: string; + propertyShortFormLabel: string; // OSFMAP shorthand label + value: string; + label: string; +} + +export interface OnSearchParams { + cardSearchText?: string; + page?: string; + sort?: string; + resourceType?: ResourceTypeFilterValue | null; +} + +interface SearchArgs { + onSearch?: (obj: OnSearchParams) => void; + cardSearchText: string; + cardSearchFilters: Filter[]; + propertyCard: IndexCardModel; + propertySearch: SearchResultModel; + toggleFilter: (filter: Filter) => void; + sort: string; + resourceType?: ResourceTypeFilterValue; + defaultQueryOptions: Record; + provider?: ProviderModel; + showResourceTypeFilter: boolean; + page: string; +} + +const searchDebounceTime = 100; + +export default class SearchPage extends Component { + @service intl!: Intl; + @service toast!: Toastr; + @service store!: Store; + @service media!: Media; + + @tracked cardSearchText?: string; + @tracked searchResults?: SearchResultModel[]; + @tracked relatedProperties?: RelatedPropertyPathModel[] = []; + @tracked page?: string = ''; + @tracked totalResultCount?: string | number; + @tracked firstPageCursor?: string | null; + @tracked prevPageCursor?: string | null; + @tracked nextPageCursor?: string | null; + + @tracked filterQueryObject: Record = {}; + + constructor( owner: unknown, args: SearchArgs) { + super(owner, args); + this.cardSearchText = this.args.cardSearchText; + this.sort = this.args.sort; + this.resourceType = this.args.resourceType; + taskFor(this.search).perform(); + } + + showTooltip1?: boolean; + showTooltip2?: boolean; + showTooltip3?: boolean; + + sidePanelToggleId = uniqueId(['side-panel-toggle']); + leftPanelObjectDropdownId = uniqueId(['left-panel-object-dropdown']); + firstTopbarObjectTypeLinkId = uniqueId(['first-topbar-object-type-link']); + searchInputWrapperId = uniqueId(['search-input-wrapper']); + leftPanelHeaderId = uniqueId(['left-panel-header']); + firstFilterId = uniqueId(['first-filter']); + + get tooltipTarget1Id() { + if (this.showSidePanelToggle) { + return this.sidePanelToggleId; + } + if (this.args.showResourceTypeFilter) { + return this.firstTopbarObjectTypeLinkId; + } + return this.searchInputWrapperId; + } + + get tooltipTarget2Id() { + if (this.showSidePanelToggle) { + return this.sidePanelToggleId; + } + return this.leftPanelHeaderId; + } + + get tooltipTarget3Id() { + if (this.showSidePanelToggle) { + return this.sidePanelToggleId; + } + if (this.relatedProperties) { + return this.firstFilterId; + } + return this.leftPanelHeaderId; + } + + get showSidePanelToggle() { + return this.media.isMobile || this.media.isTablet; + } + + get selectedResourceTypeOption() { + return this.resourceTypeOptions.find(option => option.value === this.resourceType); + } + + get showResultCountMiddle() { + return this.totalResultCount && !this.args.showResourceTypeFilter && !this.showSidePanelToggle; + } + + get showResultCountLeft() { + return this.totalResultCount && this.args.showResourceTypeFilter; + } + + get selectedSortOption() { + return this.sortOptions.find(option => option.value === this.sort);// || this.sortOptions[0]; + } + + // Resource type + resourceTypeOptions: ResourceTypeOption[] = [ + { + display: this.intl.t('search.resource-type.all'), + value: null, + }, + { + display: this.intl.t('search.resource-type.projects'), + value: ResourceTypeFilterValue.Projects, + }, + { + display: this.intl.t('search.resource-type.registrations'), + value: ResourceTypeFilterValue.Registrations, + }, + { + display: this.intl.t('search.resource-type.preprints'), + value: ResourceTypeFilterValue.Preprints, + }, + { + display: this.intl.t('search.resource-type.files'), + value: ResourceTypeFilterValue.Files, + }, + { + display: this.intl.t('search.resource-type.users'), + value: ResourceTypeFilterValue.Users, + }, + ]; + + // Sort + sortOptions: SortOption[] = [ + { display: this.intl.t('search.sort.relevance'), value: '-relevance' }, + { display: this.intl.t('search.sort.created-date-descending'), value: '-dateCreated' }, + { display: this.intl.t('search.sort.created-date-ascending'), value: 'dateCreated' }, + { display: this.intl.t('search.sort.modified-date-descending'), value: '-dateModified' }, + { display: this.intl.t('search.sort.modified-date-ascending'), value: 'dateModified' }, + ]; + + @tracked resourceType?: ResourceTypeFilterValue | null; + @tracked sort: string; + @tracked activeFilters = A([]); + + @task({ restartable: true }) + @waitFor + async search() { + try { + const cardSearchText = this.cardSearchText; + const { page, sort, activeFilters, resourceType } = this; + let filterQueryObject = activeFilters.reduce((acc, filter) => { + // boolean filters should look like cardSearchFilter[hasDataResource][is-present] + if (booleanFilterProperties.includes(filter.propertyShortFormLabel)) { + acc[filter.propertyShortFormLabel] = {}; + acc[filter.propertyShortFormLabel][filter.value] = true; + return acc; + } + // other filters should look like cardSearchFilter[propertyName]=IRI + const currentValue = acc[filter.propertyShortFormLabel]; + acc[filter.propertyShortFormLabel] = currentValue ? currentValue.concat(filter.value) : [filter.value]; + return acc; + }, {} as { [key: string]: any }); + let resourceTypeFilter = this.resourceType as string; + // If resourceType is null, we want to search all resource types + if (!resourceTypeFilter) { + resourceTypeFilter = Object.values(ResourceTypeFilterValue).join(','); + } + filterQueryObject['resourceType'] = resourceTypeFilter; + filterQueryObject = { ...filterQueryObject, ...this.args.defaultQueryOptions }; + this.filterQueryObject = filterQueryObject; + const searchResult = await this.store.queryRecord('index-card-search', { + cardSearchText, + 'page[cursor]': page, + sort, + cardSearchFilter: filterQueryObject, + 'page[size]': 10, + }); + await searchResult.relatedProperties; + this.relatedProperties = searchResult.relatedProperties; + this.firstPageCursor = searchResult.firstPageCursor; + this.nextPageCursor = searchResult.nextPageCursor; + this.prevPageCursor = searchResult.prevPageCursor; + this.searchResults = searchResult.searchResultPage.toArray(); + this.totalResultCount = searchResult.totalResultCount === ShareMoreThanTenThousand ? '10,000+' : + searchResult.totalResultCount; + if (this.args.onSearch) { + this.args.onSearch({cardSearchText, sort, resourceType, page}); + } + } catch (e) { + this.toast.error(e); + } + } + + @action + switchPage(pageCursor: string) { + this.page = pageCursor; + taskFor(this.search).perform(); + document.querySelector('[data-test-topbar-wrapper]')?.scrollIntoView({ behavior: 'smooth' }); + } + + @task({ restartable: true }) + @waitFor + async doDebounceSearch() { + await timeout(searchDebounceTime); + taskFor(this.search).perform(); + } + + @action + toggleFilter(filter: Filter) { + const filterIndex = this.activeFilters.findIndex( + f => f.propertyShortFormLabel === filter.propertyShortFormLabel && f.value === filter.value, + ); + if (filterIndex > -1) { + this.activeFilters.removeAt(filterIndex); + } else { + this.activeFilters.pushObject(filter); + } + this.page = ''; + taskFor(this.search).perform(); + } + + @action + updateSort(sortOption: SortOption) { + this.sort = sortOption.value; + this.page = ''; + taskFor(this.search).perform(); + } + + @action + updateResourceType(resourceTypeOption: ResourceTypeOption) { + this.resourceType = resourceTypeOption.value; + this.activeFilters = A([]); + this.page = ''; + taskFor(this.search).perform(); + } +} diff --git a/lib/osf-components/addon/components/search-page/filter-facet/after-options/template.hbs b/lib/osf-components/addon/components/search-page/filter-facet/after-options/template.hbs new file mode 100644 index 0000000000..9b05720a2e --- /dev/null +++ b/lib/osf-components/addon/components/search-page/filter-facet/after-options/template.hbs @@ -0,0 +1,5 @@ +{{#if @fetchValues.isRunning}} + +{{else if @hasMoreValues}} + +{{/if}} diff --git a/lib/osf-components/addon/components/search-page/filter-facet/component.ts b/lib/osf-components/addon/components/search-page/filter-facet/component.ts new file mode 100644 index 0000000000..ebecb1bfb0 --- /dev/null +++ b/lib/osf-components/addon/components/search-page/filter-facet/component.ts @@ -0,0 +1,159 @@ +import Store from '@ember-data/store'; +import { action } from '@ember/object'; +import { inject as service } from '@ember/service'; +import { waitFor } from '@ember/test-waiters'; +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { task, timeout } from 'ember-concurrency'; +import { taskFor } from 'ember-concurrency-ts'; +import IntlService from 'ember-intl/services/intl'; +import RelatedPropertyPathModel from 'ember-osf-web/models/related-property-path'; + +import SearchResultModel from 'ember-osf-web/models/search-result'; + +import { Filter } from '../component'; + +interface FakeIndexCard { + resourceId: string; + indexCard: { + label: string, + resourceId: string, + }; +} + + +interface FilterFacetArgs { + cardSearchText: string; + cardSearchFilter: Filter[]; + property: RelatedPropertyPathModel; + toggleFilter: (filter: Filter) => void; +} + +const searchDebounceTime = 500; + +export const booleanFilterProperties = [ + 'hasAnalyticCodeResource', // registrations + 'hasMaterialsResource', // registrations + 'hasPapersResource', // registrations + 'hasSupplementalResource', // registrations + 'hasDataResource', // registrations and preprints + 'hasPreregisteredAnalysisPlan', // preprints + 'hasPreregisteredStudyDesign', // preprints + 'isSupplementedBy', // preprints + 'supplements', // projects +]; + +export default class FilterFacet extends Component { + @service store!: Store; + @service intl!: IntlService; + @service toast!: Toastr; + + @tracked page = ''; + @tracked sort = '-relevance'; + @tracked collapsed = true; + @tracked filterableValues: SearchResultModel[] | FakeIndexCard[] = []; + @tracked modalValueOptions: SearchResultModel[] = []; + @tracked seeMoreModalShown = false; + @tracked selectedProperty: SearchResultModel | null = null; + @tracked showSeeMoreButton = false; + @tracked filterString = ''; + @tracked hasMoreValueOptions = false; + @tracked nextPageCursor = ''; + + @action + toggleFacet() { + if (this.filterableValues.length === 0 && !taskFor(this.fetchFacetValues).lastComplete) { + taskFor(this.fetchFacetValues).perform(); + } + this.collapsed = !this.collapsed; + } + + @action + updateSelectedProperty(property: SearchResultModel) { + this.selectedProperty = property; + } + + @action + openSeeMoreModal() { + this.seeMoreModalShown = true; + this.modalValueOptions = [...this.filterableValues] as SearchResultModel[]; + } + + @task + @waitFor + async applySelectedProperty() { + if (this.selectedProperty) { + const { toggleFilter, property } = this.args; + const card = this.selectedProperty.indexCard; + const filter = { + propertyVisibleLabel: property.displayLabel, + propertyShortFormLabel: property.shortFormLabel, + label: card.get('label'), + value: card.get('resourceId'), + }; + toggleFilter(filter); + this.selectedProperty = null; + } + } + + @task({ restartable: true }) + @waitFor + async debouncedValueSearch(filterString: string) { + await timeout(searchDebounceTime); + this.filterString = filterString; + this.page = ''; + this.modalValueOptions = []; + await taskFor(this.fetchFacetValues).perform(); + } + + @task + @waitFor + async loadMoreValues() { + this.page = this.nextPageCursor; + await taskFor(this.fetchFacetValues).perform(); + } + + @task + @waitFor + async fetchFacetValues() { + const { cardSearchText, cardSearchFilter, property } = this.args; + const { page, sort, filterString } = this; + // If the property is a boolean filter (e.g. hasDataResource), we don't want to fetch IRI values + // SHARE API filters on these properties using: + // `share.osf.io/api/v3/index-card-search?cardSearchFilter[hasDataResource][is-present]` + // or cardSearchFilter[hasDataResource][is-absent] (although this one is not used in the app) + if (booleanFilterProperties.includes(property.shortFormLabel)) { + this.filterableValues = [ + { + resourceId: 'is-present', + indexCard: { + label: this.intl.t('search.filter-facet.has-resource', { resource: property.displayLabel }), + resourceId: 'is-present', + }, + }, + ]; + return; + } + const valueSearch = await this.store.queryRecord('index-value-search', { + cardSearchText, + cardSearchFilter, + valueSearchPropertyPath: property.shortFormLabel, + valueSearchText: filterString || '', + 'page[cursor]': page, + sort, + }); + const searchResultPage = valueSearch.get('searchResultPage'); + const results = searchResultPage.toArray(); + if (!this.seeMoreModalShown) { + this.filterableValues = results; + this.showSeeMoreButton = Boolean(searchResultPage.links?.next); + } + this.modalValueOptions = [...this.modalValueOptions, ...results]; + this.hasMoreValueOptions = Boolean(searchResultPage.links?.next); + if (searchResultPage.links?.next) { + this.nextPageCursor = new URL(searchResultPage.links.next.href).searchParams.get('page[cursor]') || ''; + } else { + this.nextPageCursor = ''; + } + } +} diff --git a/lib/osf-components/addon/components/search-page/filter-facet/styles.scss b/lib/osf-components/addon/components/search-page/filter-facet/styles.scss new file mode 100644 index 0000000000..d34f64884f --- /dev/null +++ b/lib/osf-components/addon/components/search-page/filter-facet/styles.scss @@ -0,0 +1,52 @@ +.facet-wrapper { + padding: 0.5rem 0; +} + +.facet-wrapper:not(:first-of-type) { + border-top: 1px solid $color-border-gray; +} + +.facet-expand-button { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + + &:active { + box-shadow: none; + } +} + +.facet-list { + list-style: none; + max-height: 300px; + overflow-y: auto; + margin: 0; + padding: 0.2rem; + + &.collapsed { + display: none; + } +} + +.facet-value { + display: flex; + justify-content: space-between; + margin: 10px 0; + + .facet-link { + margin: 0 5px; + max-width: 90%; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + } + + .facet-count { + flex-shrink: 0; + } +} + +.see-more-dialog { + min-width: 50vw; +} diff --git a/lib/osf-components/addon/components/search-page/filter-facet/template.hbs b/lib/osf-components/addon/components/search-page/filter-facet/template.hbs new file mode 100644 index 0000000000..d7e05aabd0 --- /dev/null +++ b/lib/osf-components/addon/components/search-page/filter-facet/template.hbs @@ -0,0 +1,116 @@ +{{#if @property.cardSearchResultCount}} +
    + {{#let (unique-id @property.displayLabel) as |facetElementId|}} + + {{#if (and this.fetchFacetValues.isRunning (not this.seeMoreModalShown))}} + + {{else if this.fetchFacetValues.isError}} + {{t 'search.filter-facet.facet-load-failed'}} + {{else}} +
      + {{#each this.filterableValues as |value|}} +
    • + + + {{value.cardSearchResultCount}} + +
    • + {{/each}} + {{#if this.showSeeMoreButton}} +
    • + +
    • + {{/if}} +
    + {{/if}} + + + {{@property.displayLabel}} + + + {{t 'search.filter-facet.see-more-modal-text'}} + + {{property.indexCard.label}} ({{property.cardSearchResultCount}}) + + + + + + + + {{/let}} +
    +{{/if}} diff --git a/lib/osf-components/addon/components/search-page/styles.scss b/lib/osf-components/addon/components/search-page/styles.scss new file mode 100644 index 0000000000..df46ea157d --- /dev/null +++ b/lib/osf-components/addon/components/search-page/styles.scss @@ -0,0 +1,416 @@ +// stylelint-disable max-nesting-depth, selector-max-compound-selectors, selector-no-qualifying-type + +.search-page { + background: $color-bg-gray-blue-light; +} + +.search-page-mobile { + background: $color-bg-gray-blue-light; + + > div:first-of-type { + margin-left: 0; + } +} + +.heading-wrapper-mobile { + background-image: url('images/default-brand/bg-dark.jpg'); + background-color: $osf-dark-blue-navbar; + color: $color-text-white; + display: flex; + flex-direction: column; + padding: 0; + + label > h1 { + font-weight: 500; + margin: 0.2rem; + padding: 0.75rem 0 0.5rem 0.5rem; + white-space: nowrap; + } + + span { + display: flex; + flex-direction: row; + justify-content: flex-start; + margin-left: 0; + padding: 0 0 0.5rem 0.5rem; + width: 100%; + + .help-button-mobile { + transform: scale(0.5); + border-radius: 50%; + margin-left: 0; + position: absolute; + right: 0.1rem; + margin-right: -5px; + } + } +} + +.left-panel-mobile { + padding: 1rem; + width: 75vw; + + .object-type-dropdown { + label > div { + width: 195px; + } + } + + .object-sort-dropdown { + margin-bottom: 1rem; + + label > div { + width: 195px; + } + } +} + +.search-main-mobile { + > div { + margin: 25px auto; + padding: 20px; + width: 90vw; + } + + .pagination-buttons { + padding-right: 0; + padding-top: 0; + } +} + +.search-page > div:first-of-type { + max-width: 95vw; + margin-left: 5rem; +} + +.heading-wrapper { + background-image: url('images/default-brand/bg-dark.jpg'); + background-color: $osf-dark-blue-navbar; + color: $color-text-white; + display: flex; + align-items: center; + padding: 35px; + + label { + padding: 30px; + margin: 0 3rem 0 2rem; + } + + .heading-label > h1 { + margin: 0; + white-space: nowrap; + } +} + +.provider-logo { + background: var(--hero-logo-img-url) top center no-repeat; + background-size: contain; + height: 70px; + margin: 2em 0 1em; + width: 100%; +} + +.hero-overlay { + background-color: transparent; + display: flex; + flex-direction: column; + height: 100%; + width: 100%; + min-height: 100%; + min-width: 100%; + position: relative; + + &::after { + background: var(--hero-background-img-url); + background-size: cover; + content: ''; + height: 100%; + width: 100%; + position: absolute; + left: 0; + top: 0; + z-index: -1; + } +} + +.search-input-wrapper { + display: flex; + white-space: nowrap; + width: 70vw; + + .search-input { + color: $color-text-black; + font-size: 1.5em; + min-width: 250px; + width: 90%; + max-width: 70vw; + padding: 9px; + } + + .search-button-wrapper { + .help-button { + border-radius: 5px; + } + + .search-button { + border-radius: 0 5px 5px 0; + margin-right: 30px; + } + } +} + +.topbar { + border-bottom: 1px solid $color-text-black; + display: flex; + margin: 0 auto; + max-height: 55px; + margin-top: 1rem; + width: 60vw; + + .object-type-nav { + display: flex; + width: 100%; + + ul { + display: flex; + align-items: center; + justify-content: space-evenly; + list-style: none; + margin-bottom: 0; + padding-left: 0; + + width: 100%; + } + + li { + display: inline-flex; + white-space: nowrap; + margin-right: 10px; + + &.active, + &:hover { + background-color: $color-bg-gray; + } + } + + a { + padding-bottom: 10px; + } + } + + .help-button { + margin-left: 1.2rem; + } +} + +.search-main { + display: grid; + justify-content: center; +} + +.search-help-tutorial { + background-color: $osf-dark-blue-navbar; + color: $color-text-white; + width: 400px; + max-width: 100%; + z-index: 100; + // prevent overriding popover styling + white-space: wrap; + text-align: initial; + display: flex; + flex-direction: column; + justify-content: space-around; + margin-left: -0.2rem; + + a { + font-weight: bold; + color: var(--primary-color); + } + + .enumeration { + float: left; + white-space: nowrap; + } + + h2 { + font-weight: 400; + height: 22px; + margin-top: 0.5rem; + margin-left: 0.2rem; + } + + p { + white-space: normal; + margin: 25px 5px 0; + } + + .skip-button { + color: $color-text-white; + overflow: visible; + } + + .pagination { + display: flex; + justify-content: space-between; + align-items: baseline; + margin-bottom: 0.5rem; + } + + .help-button-wrapper { + display: flex; + justify-content: flex-end; + padding: 0; + + .skip-button { + margin-right: 20px; + } + } +} + +.object-type-filter-link { + font-size: 1.3em; + padding: 18px 15px; + + &:global(.active), + &:hover { + background-color: $color-bg-white; + border-bottom: 1px solid $color-text-black; + color: $color-text-black; + text-decoration: none; + } +} + +.sort-dropdown { + background-color: $color-bg-gray-blue-light; + align-self: flex-end; + padding-bottom: 10px; + margin-left: 10px; + + p { + margin: 0 0 0 2px; + } + + div { + background-color: $color-bg-gray-blue-light; + border: 0 solid transparent; + color: $color-bg-blue-dark; + width: 170px; + } + + > div { + display: flex; + } +} + +.sidenav-toggle { + float: left; +} + +.no-results { + display: flex; + align-items: center; + justify-content: center; + + p { + font-size: 1.2em; + margin: 10px 20px; + text-align: center; + } +} + +.ember-power-select-options { + max-height: 20em; +} + +.left-panel { + background-color: $color-bg-gray-blue-light; + margin-bottom: 0; + min-width: 300px; + padding: 2rem 12px; + width: 340px; + + .left-panel-header { + border-bottom: 1px solid $alto; + font-weight: 300; + margin: 1rem 0; + padding: 0 0 1.5rem; + } +} + +.search-count { + font-size: 1.5em; + font-weight: 600; + margin: 5px 0; + padding: 0 0.2rem 0.2rem; +} + +.active-filter-list { + padding-left: 0; + list-style: none; +} + +.active-filter-item { + display: flex; + justify-content: space-between; + margin: 0.5rem 0.2rem; + + button { + margin-right: -5px; + } +} + +.no-properties { + padding: 1rem 0; +} + +.pagination-buttons { + display: flex; + padding: 2rem; + align-items: center; + justify-content: flex-end; + height: fit-content; + + button { + color: $color-bg-blue-dark; + margin-left: 5px; + } +} + +// for tablet displays +@media screen and (min-width: 650px) { + .left-panel-mobile { + width: 60vw; + } +} + +// for sizing between mobile and desktop +@media screen and (min-width: 899px) and (max-width: 1275px) { + .search-page > div:first-of-type { + margin: 0 auto; + } + + .heading-wrapper > label { + white-space: nowrap; + padding-left: 0; + margin: 0; + } + + .search-main { + gap: 1rem; + } + + .topbar { + display: flex; + flex-direction: column; + } + + .sort-dropdown { + align-self: flex-end; + margin: 0.5rem 0; + + > div { + background-color: $color-bg-white; + margin-top: 0.4rem; + } + } +} diff --git a/lib/osf-components/addon/components/search-page/template.hbs b/lib/osf-components/addon/components/search-page/template.hbs new file mode 100644 index 0000000000..5c98e110f3 --- /dev/null +++ b/lib/osf-components/addon/components/search-page/template.hbs @@ -0,0 +1,387 @@ + + + {{#if @provider}} +
    +
    +

    + {{html-safe @provider.description}} +

    + {{else if @institution}} +
    + +
    + {{else}} + + {{/if}} + + +
    + + +
    +
    + {{#if this.showSidePanelToggle}} + + {{/if}} +
    + + {{#if this.showResultCountLeft}} +

    + {{t 'search.total-results' count=this.totalResultCount}} +

    + {{/if}} +

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

    + {{#if this.showSidePanelToggle}} + {{#if @showResourceTypeFilter}} +
    + +
    + {{/if}} +
    + +
    + {{/if}} + {{#if this.activeFilters.length}} +
      + {{#each this.activeFilters as |filter|}} +
    • + + {{filter.propertyVisibleLabel}}: + {{filter.label}} + + +
    • + {{/each}} +
    + {{/if}} + {{#if this.search.isRunning}} + + {{else}} + {{#each this.relatedProperties as |filterableProperty index|}} + + {{else}} +

    + {{t 'search.left-panel.no-filterable-properties'}} +

    + {{/each}} + {{/if}} +
    + + {{#unless this.showSidePanelToggle}} +
    + {{#if @showResourceTypeFilter}} + +
    + +

    {{sortOption.display}}

    +
    +
    + {{else if this.showResultCountMiddle}} +

    + {{t 'search.total-results' count=this.totalResultCount}} +

    + {{/if}} +
    + {{/unless}} + {{#if this.search.isRunning}} + + {{else}} + {{#each this.searchResults as |item|}} + + {{else}} +
    +

    {{t 'search.no-results'}}

    +
    + {{/each}} + {{/if}} +
    + {{#if this.firstPageCursor}} + + {{/if}} + {{#if this.prevPageCursor}} + + {{/if}} + {{#if this.nextPageCursor}} + + {{/if}} +
    +
    +
    +{{#if this.showTooltip1}} + +
    +

    + {{t 'search.search-help.header-1'}} +

    +

    {{t 'search.search-help.body-1' htmlSafe=true}}

    + +

    {{t 'search.search-help.index-1'}}

    + + + + +
    +
    +
    +{{/if}} +{{#if this.showTooltip2}} + +
    +

    + {{t 'search.search-help.header-2'}} +

    +

    {{t 'search.search-help.body-2'}}

    + +

    {{t 'search.search-help.index-2'}}

    + + + + +
    +
    +
    +{{/if}} +{{#if this.showTooltip3}} + +
    +

    + {{t 'search.search-help.header-3'}} +

    +

    {{t 'search.search-help.body-3' htmlSafe=true}}

    + +

    {{t 'search.search-help.index-3'}}

    + + + +
    +
    +
    +{{/if}} \ No newline at end of file diff --git a/lib/osf-components/addon/components/search-result-card/component.ts b/lib/osf-components/addon/components/search-result-card/component.ts new file mode 100644 index 0000000000..d487ff36a0 --- /dev/null +++ b/lib/osf-components/addon/components/search-result-card/component.ts @@ -0,0 +1,31 @@ +import { action } from '@ember/object'; +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import SearchResultModel from 'ember-osf-web/models/search-result'; +import { inject as service } from '@ember/service'; +import Intl from 'ember-intl/services/intl'; + +interface Args { + result: SearchResultModel; +} + +export default class SearchResultCard extends Component { + @service intl!: Intl; + @tracked isOpenSecondaryMetadata = false; + + @action + toggleSecondaryMetadata() { + this.isOpenSecondaryMetadata = !this.isOpenSecondaryMetadata; + } + + get cardTypeLabel() { + return this.intl.t(`osf-components.search-result-card.${this.args.result.resourceType}`); + } + + // not sure if this is the best way, as there was a resourceType of "unknown" out in the wild + get secondaryMetadataComponent() { + const { resourceType } = this.args.result; + + return `search-result-card/${resourceType.replace('_component', '')}-secondary-metadata`; + } +} diff --git a/lib/osf-components/addon/components/search-result-card/file-secondary-metadata/styles.scss b/lib/osf-components/addon/components/search-result-card/file-secondary-metadata/styles.scss new file mode 100644 index 0000000000..d10e838958 --- /dev/null +++ b/lib/osf-components/addon/components/search-result-card/file-secondary-metadata/styles.scss @@ -0,0 +1,3 @@ +.description { + white-space: pre-wrap; +} diff --git a/lib/osf-components/addon/components/search-result-card/file-secondary-metadata/template.hbs b/lib/osf-components/addon/components/search-result-card/file-secondary-metadata/template.hbs new file mode 100644 index 0000000000..e8c08b6b2e --- /dev/null +++ b/lib/osf-components/addon/components/search-result-card/file-secondary-metadata/template.hbs @@ -0,0 +1,52 @@ +
    + {{#if @result.fileTitle}} +
    {{t 'osf-components.search-result-card.title'}}
    +
    {{@result.fileTitle}}
    + {{/if}} + {{#if @result.description}} +
    {{t 'osf-components.search-result-card.description'}}
    +
    {{@result.description}}
    + {{/if}} + {{#if @result.nodeFunders}} +
    {{t 'osf-components.search-result-card.funder'}}
    +
    + + {{#if list.item}} + {{list.item.name}} + {{else if list.remainingCount}} + {{t 'osf-components.search-result-card.remaining_count' count=list.remainingCount}} + {{/if}} + +
    + {{/if}} + {{#if @result.resourceNature}} +
    {{t 'osf-components.search-result-card.resource_type'}}
    +
    {{@result.resourceNature}}
    + {{/if}} + {{#if @result.nodeLicense}} +
    {{t 'osf-components.search-result-card.license'}}
    +
    {{@result.nodeLicense.name}}
    + {{/if}} + {{#if @result.absoluteUrl}} +
    {{t 'osf-components.search-result-card.url'}}
    +
    {{@result.absoluteUrl}}
    + {{/if}} + {{#if @result.doi}} +
    {{t 'osf-components.search-result-card.doi'}}
    +
    + + {{list.item}} + +
    + {{/if}} +
    diff --git a/lib/osf-components/addon/components/search-result-card/preprint-secondary-metadata/styles.scss b/lib/osf-components/addon/components/search-result-card/preprint-secondary-metadata/styles.scss new file mode 100644 index 0000000000..d10e838958 --- /dev/null +++ b/lib/osf-components/addon/components/search-result-card/preprint-secondary-metadata/styles.scss @@ -0,0 +1,3 @@ +.description { + white-space: pre-wrap; +} diff --git a/lib/osf-components/addon/components/search-result-card/preprint-secondary-metadata/template.hbs b/lib/osf-components/addon/components/search-result-card/preprint-secondary-metadata/template.hbs new file mode 100644 index 0000000000..5ed73f65c5 --- /dev/null +++ b/lib/osf-components/addon/components/search-result-card/preprint-secondary-metadata/template.hbs @@ -0,0 +1,93 @@ +
    + {{!-- Description --}} + {{#if @result.description}} +
    {{t 'osf-components.search-result-card.description'}}:
    +
    {{@result.description}}
    + {{/if}} + + {{!-- Preprint Provider --}} + {{#if @result.provider}} +
    {{t 'osf-components.search-result-card.preprint_provider'}}
    +
    {{@result.provider.name}}
    + {{/if}} + + {{!-- Associated Data --}} + {{#if @result.resourceMetadata.hasDataResource}} +
    {{t 'osf-components.search-result-card.associated_data'}}
    +
    + + {{list.item.[@id]}} + +
    + {{/if}} + + {{!-- Associated Preregistration: hasPreregisteredAnalysisPlan and/or hasPreregisteredStudyDesign --}} + {{#if @result.resourceMetadata.hasPreregisteredAnalysisPlan}} +
    {{t 'osf-components.search-result-card.associated_analysis_plan'}}
    +
    + + {{list.item.[@id]}} + +
    + {{/if}} + {{#if @result.resourceMetadata.hasPreregisteredStudyDesign}} +
    {{t 'osf-components.search-result-card.associated_study_design'}}
    +
    + + {{list.item.[@id]}} + +
    + {{/if}} + + {{!-- Conflict of interest statement --}} + {{#if @result.resourceMetadata.statedConflictOfInterest}} +
    {{t 'osf-components.search-result-card.conflict_of_interest'}}
    +
    + {{#if @result.resourceMetadata.statedConflictOfInterest.[0].[@value]}} + {{@result.resourceMetadata.statedConflictOfInterest.[0].[@value]}} + {{else}} + {{t 'osf-components.search-result-card.no_conflict_of_interest'}} + {{/if}} +
    + {{/if}} + + {{!-- License --}} + {{#if @result.license}} +
    {{t 'osf-components.search-result-card.license'}}
    +
    {{@result.license.name}}
    + {{/if}} + + {{#if @result.absoluteUrl}} +
    {{t 'osf-components.search-result-card.url'}}
    +
    {{@result.absoluteUrl}}
    + {{/if}} + + {{!-- DOI --}} + {{#if @result.doi}} +
    {{t 'osf-components.search-result-card.doi'}}
    +
    + + {{#if list.item}} + {{list.item}} + {{else if list.remainingCount}} + {{t 'osf-components.search-result-card.remaining_count' count=list.remainingCount}} + {{/if}} + +
    + {{/if}} +
    + diff --git a/lib/osf-components/addon/components/search-result-card/project-secondary-metadata/styles.scss b/lib/osf-components/addon/components/search-result-card/project-secondary-metadata/styles.scss new file mode 100644 index 0000000000..d10e838958 --- /dev/null +++ b/lib/osf-components/addon/components/search-result-card/project-secondary-metadata/styles.scss @@ -0,0 +1,3 @@ +.description { + white-space: pre-wrap; +} diff --git a/lib/osf-components/addon/components/search-result-card/project-secondary-metadata/template.hbs b/lib/osf-components/addon/components/search-result-card/project-secondary-metadata/template.hbs new file mode 100644 index 0000000000..87a2d0a1b6 --- /dev/null +++ b/lib/osf-components/addon/components/search-result-card/project-secondary-metadata/template.hbs @@ -0,0 +1,60 @@ +
    + {{#if @result.description}} +
    {{t 'osf-components.search-result-card.description'}}
    +
    {{@result.description}}
    + {{/if}} + {{#if @result.funders}} +
    {{t 'osf-components.search-result-card.funder'}}
    +
    + + {{#if list.item}} + {{list.item.name}} + {{else if list.remainingCount}} + {{t 'osf-components.search-result-card.remaining_count' count=list.remainingCount}} + {{/if}} + +
    + {{/if}} + {{#if @result.resourceNature}} +
    {{t 'osf-components.search-result-card.resource_type'}}
    +
    {{@result.resourceNature}}
    + {{/if}} + {{#if @result.isPartOfCollection}} +
    {{t 'osf-components.search-result-card.collection'}}
    +
    {{@result.isPartOfCollection.title}}
    + {{/if}} + {{#if @result.languageFromCode}} +
    {{t 'osf-components.search-result-card.language'}}
    +
    {{@result.languageFromCode}}
    + {{/if}} + {{#if @result.license}} +
    {{t 'osf-components.search-result-card.license'}}
    +
    {{@result.license.name}}
    + {{/if}} + {{#if @result.absoluteUrl}} +
    {{t 'osf-components.search-result-card.url'}}
    +
    {{@result.absoluteUrl}}
    + {{/if}} + {{#if @result.doi}} +
    {{t 'osf-components.search-result-card.doi'}}
    +
    + + {{#if list.item}} + {{list.item}} + {{else if list.remainingCount}} + {{t 'osf-components.search-result-card.remaining_count' count=list.remainingCount}} + {{/if}} + +
    + {{/if}} +
    \ No newline at end of file diff --git a/lib/osf-components/addon/components/search-result-card/registration-secondary-metadata/styles.scss b/lib/osf-components/addon/components/search-result-card/registration-secondary-metadata/styles.scss new file mode 100644 index 0000000000..d10e838958 --- /dev/null +++ b/lib/osf-components/addon/components/search-result-card/registration-secondary-metadata/styles.scss @@ -0,0 +1,3 @@ +.description { + white-space: pre-wrap; +} diff --git a/lib/osf-components/addon/components/search-result-card/registration-secondary-metadata/template.hbs b/lib/osf-components/addon/components/search-result-card/registration-secondary-metadata/template.hbs new file mode 100644 index 0000000000..72492d58ed --- /dev/null +++ b/lib/osf-components/addon/components/search-result-card/registration-secondary-metadata/template.hbs @@ -0,0 +1,56 @@ +
    + {{#if @result.description}} +
    {{t 'osf-components.search-result-card.description'}}
    +
    {{@result.description}}
    + {{/if}} + {{#if @result.funders}} +
    {{t 'osf-components.search-result-card.funder'}}
    +
    + + {{#if list.item}} + {{list.item.name}} + {{else if list.remainingCount}} + {{t 'osf-components.search-result-card.remaining_count' count=list.remainingCount}} + {{/if}} + +
    + {{/if}} + {{#if @result.provider}} +
    {{t 'osf-components.search-result-card.registration_provider'}}
    +
    {{@result.provider.name}}
    + {{/if}} + {{#if @result.registrationTemplate}} +
    {{t 'osf-components.search-result-card.registration_template'}}
    +
    {{@result.registrationTemplate}}
    + {{/if}} + {{#if @result.license}} +
    {{t 'osf-components.search-result-card.license'}}
    +
    {{@result.license.name}}
    + {{/if}} + {{#if @result.absoluteUrl}} +
    {{t 'osf-components.search-result-card.url'}}
    +
    {{@result.absoluteUrl}}
    + {{/if}} + {{#if @result.doi}} +
    {{t 'osf-components.search-result-card.doi'}}
    +
    + + {{#if list.item}} + {{list.item}} + {{else if list.remainingCount}} + {{t 'osf-components.search-result-card.remaining_count' count=list.remainingCount}} + {{/if}} + +
    + {{/if}} +
    diff --git a/lib/osf-components/addon/components/search-result-card/styles.scss b/lib/osf-components/addon/components/search-result-card/styles.scss new file mode 100644 index 0000000000..2a53b47ab8 --- /dev/null +++ b/lib/osf-components/addon/components/search-result-card/styles.scss @@ -0,0 +1,87 @@ +.result-card-container { + background-color: $color-bg-white; + margin: 35px 35px 0; + padding: 10px; + width: 60vw; +} + +.primary-metadata-container { + display: flex; + flex-direction: column; + + h4 { + font-weight: bold; + } + + h4, + div:not(:first-child) { + margin: 0 0 15px 15px; + } + + > div:last-child { + margin-bottom: 0; + padding-bottom: 5px; + } +} + +.header { + display: flex; + justify-content: space-between; + + button { + border: 1px solid transparent; + color: $color-text-blue-dark; + height: 30px; + margin: 5px; + padding: 0 10px; + } +} + +.type-label { + background-color: $color-gradient-primary; + margin: 15px; + padding: 0.2rem 0.4rem; + width: fit-content; + text-transform: uppercase; +} + +.orcid-logo { + color: $orcid-logo-color; + margin-left: 3px; +} + +.withdrawn-label { + background-color: $color-bg-gray-darker; + color: $color-text-white; + border-radius: 0.25em; + padding: 0.2em 0.6em 0.3em; + font-size: 75%; + font-weight: bold; + margin-left: 3px; + position: relative; + bottom: 2px; +} + +.date-fields { + padding-bottom: 5px; + + span { + &:not(:last-child)::after { + content: ' | '; + } + } +} + +.cp-panel { + padding: 0 10px; + + dl { + border-top: 2px solid $color-border-gray; + padding-top: 10px; + } + + dt, + dd { + margin: 15px 0 0 5px; + } +} diff --git a/lib/osf-components/addon/components/search-result-card/template.hbs b/lib/osf-components/addon/components/search-result-card/template.hbs new file mode 100644 index 0000000000..0708c16e8c --- /dev/null +++ b/lib/osf-components/addon/components/search-result-card/template.hbs @@ -0,0 +1,99 @@ +
    +
    +
    +
    + {{this.cardTypeLabel}} +
    + {{#if (not-eq @result.resourceType 'user')}} + + {{/if}} +
    +

    + {{@result.displayTitle}} + {{#if @result.isWithdrawn}} + {{t 'osf-components.search-result-card.withdrawn'}} + {{/if}} + {{#if @result.orcids}} + {{#each @result.orcids as |item|}} + + + + {{/each}} + {{/if}} +

    + + {{#if @result.affiliatedEntities}} +
    + + {{#if list.item}} + {{list.item.name}} + {{else if list.remainingCount}} + {{t 'osf-components.search-result-card.remaining_count' count=list.remainingCount}} + {{/if}} + +
    + {{/if}} + {{#if @result.isPartOf}} +
    + {{t 'osf-components.search-result-card.from'}}: {{@result.isPartOfTitleAndUrl.title}} +
    + {{/if}} + {{#if @result.isContainedBy}} +
    + {{t 'osf-components.search-result-card.from'}}: {{@result.isContainedByTitleAndUrl.title}} +
    + {{/if}} +
    + {{#each @result.dateFields as |field|}} + {{#if field.date}} + {{field.label}}: {{field.date}} + {{/if}} + {{/each}} +
    + {{#if @result.context}} +
    + {{t 'osf-components.search-result-card.context'}}: {{@result.context}} +
    + {{/if}} + {{#if (or (eq @result.resourceType 'registration') (eq @result.resourceType 'registration_component'))}} +
    + +
    + {{/if}} +
    + + +
    + {{component this.secondaryMetadataComponent result=@result}} +
    +
    +
    \ No newline at end of file diff --git a/lib/osf-components/addon/components/tags-widget/component.ts b/lib/osf-components/addon/components/tags-widget/component.ts index cba7a0f784..6679bb3532 100644 --- a/lib/osf-components/addon/components/tags-widget/component.ts +++ b/lib/osf-components/addon/components/tags-widget/component.ts @@ -2,25 +2,24 @@ import { attribute } from '@ember-decorators/component'; import Component from '@ember/component'; import { assert } from '@ember/debug'; import { action } from '@ember/object'; +import RouterService from '@ember/routing/router-service'; import { inject as service } from '@ember/service'; -import config from 'ember-get-config'; import { layout } from 'ember-osf-web/decorators/component'; import OsfModel from 'ember-osf-web/models/osf-model'; import Analytics from 'ember-osf-web/services/analytics'; -import pathJoin from 'ember-osf-web/utils/path-join'; import styles from './styles'; import template from './template'; -const { OSF: { url: baseUrl } } = config; - interface Taggable extends OsfModel { tags: string[]; } @layout(template, styles) export default class TagsWidget extends Component.extend({ styles }) { + @service router!: RouterService; + // required arguments taggable!: Taggable; @@ -64,7 +63,7 @@ export default class TagsWidget extends Component.extend({ styles }) { @action _clickTag(tag: string): void { - window.location.assign(`${pathJoin(baseUrl, 'search')}?q=(tags:"${encodeURIComponent(tag)}")`); + this.router.transitionTo('search', { queryParams: { q: `${encodeURIComponent(tag)}` } }); } _onChange() { diff --git a/lib/osf-components/addon/helpers/sufficient-contrast.ts b/lib/osf-components/addon/helpers/sufficient-contrast.ts new file mode 100644 index 0000000000..d167563f00 --- /dev/null +++ b/lib/osf-components/addon/helpers/sufficient-contrast.ts @@ -0,0 +1,79 @@ +import { helper } from '@ember/component/helper'; + +/** + * Criteria is based on WCAG 2.0 Guidelines. + * https://www.w3.org/WAI/WCAG21/quickref/?versions=2.0#qr-visual-audio-contrast-contrast + * + * Relative Luminance is calculated using the formula from WCAG 2.0 Guidelines. + * https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef + * + * @param {Array} backgroundColor The background color in hex format + * @param {Array} foregroundColor The foreground color in hex format + * @param {Object} options {largeText: true if text is at least 18 point if not bold and at least 14 point if bold} + * @return {Boolean} Whether the contrast between the two colors is sufficient + */ + +const wcagAA = { + normalText: 4.5, + largeText: 3, +}; +const wcagAAA = { + normalText: 7, + largeText: 4.5, +}; + +function threeDigitHexToSixDigit(hex: string): string { + if (hex.length === 3) { + return hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]; + } + return hex; +} + +export function sufficientContrast( + [backgroundColor, foregroundColor]: [string, string], { largeText = false, useAAA = false }, +): boolean { + const standard = useAAA ? wcagAAA : wcagAA; + const threshold = largeText ? standard.largeText : standard.normalText; + + if (!backgroundColor || !foregroundColor) { + return false; + } + + let bg = backgroundColor.replace('#', ''); + let fg = foregroundColor.replace('#', ''); + bg = bg.length === 3 ? threeDigitHexToSixDigit(bg) : bg; + fg = fg.length === 3 ? threeDigitHexToSixDigit(fg) : fg; + + // convert background and foreground color hex to sRGB + const bgSRGB = { + r: parseInt('0x' + bg.substring(0, 2), 16) / 255, + g: parseInt('0x' + bg.substring(2, 4), 16) / 255, + b: parseInt('0x' + bg.substring(4, 6), 16) / 255, + }; + const fgSRGB = { + r: parseInt('0x' + fg.substring(0, 2), 16) / 255, + g: parseInt('0x' + fg.substring(2, 4), 16) / 255, + b: parseInt('0x' + fg.substring(4, 6), 16) / 255, + }; + + const bgRGBLuminance = { + r: bgSRGB.r <= 0.03928 ? bgSRGB.r / 12.92 : Math.pow((bgSRGB.r + 0.055) / 1.055, 2.4), + g: bgSRGB.g <= 0.03928 ? bgSRGB.g / 12.92 : Math.pow((bgSRGB.g + 0.055) / 1.055, 2.4), + b: bgSRGB.b <= 0.03928 ? bgSRGB.b / 12.92 : Math.pow((bgSRGB.b + 0.055) / 1.055, 2.4), + }; + const fgRGBLuminance = { + r: fgSRGB.r <= 0.03928 ? fgSRGB.r / 12.92 : Math.pow((fgSRGB.r + 0.055) / 1.055, 2.4), + g: fgSRGB.g <= 0.03928 ? fgSRGB.g / 12.92 : Math.pow((fgSRGB.g + 0.055) / 1.055, 2.4), + b: fgSRGB.b <= 0.03928 ? fgSRGB.b / 12.92 : Math.pow((fgSRGB.b + 0.055) / 1.055, 2.4), + }; + + // calculate relative luminance + const bgLuminance = 0.2126 * bgRGBLuminance.r + 0.7152 * bgRGBLuminance.g + 0.0722 * bgRGBLuminance.b; + const fgLuminance = 0.2126 * fgRGBLuminance.r + 0.7152 * fgRGBLuminance.g + 0.0722 * fgRGBLuminance.b; + + // calculate contrast ratio + const contrastRatio = (Math.max(bgLuminance, fgLuminance) + 0.05) / (Math.min(bgLuminance, fgLuminance) + 0.05); + return contrastRatio >= threshold; +} + +export default helper(sufficientContrast); diff --git a/lib/osf-components/app/components/search-page/component.js b/lib/osf-components/app/components/search-page/component.js new file mode 100644 index 0000000000..2e83fa9360 --- /dev/null +++ b/lib/osf-components/app/components/search-page/component.js @@ -0,0 +1 @@ +export { default } from 'osf-components/components/search-page/component'; diff --git a/lib/osf-components/app/components/search-page/filter-facet/after-options/template.js b/lib/osf-components/app/components/search-page/filter-facet/after-options/template.js new file mode 100644 index 0000000000..ead78170e5 --- /dev/null +++ b/lib/osf-components/app/components/search-page/filter-facet/after-options/template.js @@ -0,0 +1 @@ +export { default } from 'osf-components/components/search-page/filter-facet/after-options/template'; diff --git a/lib/osf-components/app/components/search-page/filter-facet/component.js b/lib/osf-components/app/components/search-page/filter-facet/component.js new file mode 100644 index 0000000000..2aea79fdf2 --- /dev/null +++ b/lib/osf-components/app/components/search-page/filter-facet/component.js @@ -0,0 +1 @@ +export { default } from 'osf-components/components/search-page/filter-facet/component'; diff --git a/lib/osf-components/app/components/search-page/filter-facet/template.js b/lib/osf-components/app/components/search-page/filter-facet/template.js new file mode 100644 index 0000000000..43c36976ad --- /dev/null +++ b/lib/osf-components/app/components/search-page/filter-facet/template.js @@ -0,0 +1 @@ +export { default } from 'osf-components/components/search-page/filter-facet/template'; diff --git a/lib/osf-components/app/components/search-page/template.js b/lib/osf-components/app/components/search-page/template.js new file mode 100644 index 0000000000..ad34c5c2da --- /dev/null +++ b/lib/osf-components/app/components/search-page/template.js @@ -0,0 +1 @@ +export { default } from 'osf-components/components/search-page/template'; diff --git a/lib/osf-components/app/components/search-result-card/component.js b/lib/osf-components/app/components/search-result-card/component.js new file mode 100644 index 0000000000..9ca7a8b5cf --- /dev/null +++ b/lib/osf-components/app/components/search-result-card/component.js @@ -0,0 +1 @@ +export { default } from 'osf-components/components/search-result-card/component'; diff --git a/lib/osf-components/app/components/search-result-card/file-secondary-metadata/template.js b/lib/osf-components/app/components/search-result-card/file-secondary-metadata/template.js new file mode 100644 index 0000000000..7f534b8c7d --- /dev/null +++ b/lib/osf-components/app/components/search-result-card/file-secondary-metadata/template.js @@ -0,0 +1 @@ +export { default } from 'osf-components/components/search-result-card/file-secondary-metadata/template'; diff --git a/lib/osf-components/app/components/search-result-card/preprint-secondary-metadata/template.js b/lib/osf-components/app/components/search-result-card/preprint-secondary-metadata/template.js new file mode 100644 index 0000000000..6ebd4c9640 --- /dev/null +++ b/lib/osf-components/app/components/search-result-card/preprint-secondary-metadata/template.js @@ -0,0 +1 @@ +export { default } from 'osf-components/components/search-result-card/preprint-secondary-metadata/template'; diff --git a/lib/osf-components/app/components/search-result-card/project-secondary-metadata/template.js b/lib/osf-components/app/components/search-result-card/project-secondary-metadata/template.js new file mode 100644 index 0000000000..7ab736d92f --- /dev/null +++ b/lib/osf-components/app/components/search-result-card/project-secondary-metadata/template.js @@ -0,0 +1 @@ +export { default } from 'osf-components/components/search-result-card/project-secondary-metadata/template'; diff --git a/lib/osf-components/app/components/search-result-card/registration-secondary-metadata/template.js b/lib/osf-components/app/components/search-result-card/registration-secondary-metadata/template.js new file mode 100644 index 0000000000..f76c747316 --- /dev/null +++ b/lib/osf-components/app/components/search-result-card/registration-secondary-metadata/template.js @@ -0,0 +1 @@ +export { default } from 'osf-components/components/search-result-card/registration-secondary-metadata/template'; diff --git a/lib/osf-components/app/components/search-result-card/template.js b/lib/osf-components/app/components/search-result-card/template.js new file mode 100644 index 0000000000..f99efc30de --- /dev/null +++ b/lib/osf-components/app/components/search-result-card/template.js @@ -0,0 +1 @@ +export { default } from 'osf-components/components/search-result-card/template'; diff --git a/lib/osf-components/app/helpers/sufficient-contrast.js b/lib/osf-components/app/helpers/sufficient-contrast.js new file mode 100644 index 0000000000..addd1fd326 --- /dev/null +++ b/lib/osf-components/app/helpers/sufficient-contrast.js @@ -0,0 +1 @@ +export { default } from 'osf-components/helpers/sufficient-contrast'; diff --git a/lib/registries/addon/branded/discover/template.hbs b/lib/registries/addon/branded/discover/template.hbs new file mode 100644 index 0000000000..47e7abedca --- /dev/null +++ b/lib/registries/addon/branded/discover/template.hbs @@ -0,0 +1,17 @@ +{{page-title (t 'registries.discover.page_title') prepend=false}} + + + diff --git a/lib/registries/addon/branded/index/route.ts b/lib/registries/addon/branded/index/route.ts index c4adf37659..b2b0de51ff 100644 --- a/lib/registries/addon/branded/index/route.ts +++ b/lib/registries/addon/branded/index/route.ts @@ -1,8 +1,12 @@ import Route from '@ember/routing/route'; - +import config from 'ember-get-config'; export default class BrandedRegistriesIndexRoute extends Route { beforeModel() { const params: { providerId?: string } = this.paramsFor('branded'); + if (params.providerId === config.defaultProvider) { + return this.replaceWith('index'); + } + return this.replaceWith('branded.discover', params.providerId); } } diff --git a/lib/registries/addon/components/registries-recent-list/template.hbs b/lib/registries/addon/components/registries-recent-list/template.hbs index d07ce2928f..a3ec7a5c8c 100644 --- a/lib/registries/addon/components/registries-recent-list/template.hbs +++ b/lib/registries/addon/components/registries-recent-list/template.hbs @@ -4,9 +4,12 @@ {{t 'registries.index.recent.title'}}
    - + {{t 'registries.index.recent.more'}} - +
    diff --git a/lib/registries/addon/components/registries-search-result/template.hbs b/lib/registries/addon/components/registries-search-result/template.hbs index 63ecf039f8..60d924c1f8 100644 --- a/lib/registries/addon/components/registries-search-result/template.hbs +++ b/lib/registries/addon/components/registries-search-result/template.hbs @@ -77,6 +77,7 @@ @hasAnalyticCode={{@result.relatedResourceTypes.analytic_code}} @hasPapers={{@result.relatedResourceTypes.papers}} @hasSupplements={{@result.relatedResourceTypes.supplements}} + @verticalLayout={{true}} /> {{/if}} diff --git a/lib/registries/addon/discover/route.ts b/lib/registries/addon/discover/route.ts index f0755af90c..9c10dfe213 100644 --- a/lib/registries/addon/discover/route.ts +++ b/lib/registries/addon/discover/route.ts @@ -1,6 +1,13 @@ import Route from '@ember/routing/route'; +import RouterService from '@ember/routing/router-service'; +import { inject as service } from '@ember/service'; export default class RegistriesDiscoverRoute extends Route { + @service router!: RouterService; + afterModel() { + this.router.transitionTo('search', { queryParams: { resourceType: 'Registration,RegistrationComponent' }}); + } + buildRouteInfoMetadata() { return { osfMetrics: { diff --git a/lib/registries/addon/drafts/draft/-components/tags-manager/component.ts b/lib/registries/addon/drafts/draft/-components/tags-manager/component.ts index 64d1f2c87d..3ce72e4b55 100644 --- a/lib/registries/addon/drafts/draft/-components/tags-manager/component.ts +++ b/lib/registries/addon/drafts/draft/-components/tags-manager/component.ts @@ -1,25 +1,22 @@ import { tagName } from '@ember-decorators/component'; import Component from '@ember/component'; import { action, set } from '@ember/object'; +import RouterService from '@ember/routing/router-service'; +import { inject as service } from '@ember/service'; import { BufferedChangeset } from 'ember-changeset/types'; -import config from 'ember-get-config'; import { layout } from 'ember-osf-web/decorators/component'; import DraftRegistrationModel from 'ember-osf-web/models/draft-registration'; -import pathJoin from 'ember-osf-web/utils/path-join'; import { TagsManager } from 'osf-components/components/editable-field/tags-manager/component'; import template from './template'; -const { - OSF: { url: baseUrl }, -} = config; - export type MetadataTagsManager = Pick; @tagName('') @layout(template) export default class MetadataTagsManagerComponent extends Component { + @service router!: RouterService; // required changeset!: BufferedChangeset; valuePath!: string; @@ -58,6 +55,6 @@ export default class MetadataTagsManagerComponent extends Component { @action clickTag(tag: string): void { - window.location.assign(`${pathJoin(baseUrl, 'search')}?q=(tags:"${encodeURIComponent(tag)}")`); + this.router.transitionTo('search', { queryParams: { q: `${encodeURIComponent(tag)}` } }); } } diff --git a/lib/registries/addon/engine.js b/lib/registries/addon/engine.js index 641b1c9863..5afdc88298 100644 --- a/lib/registries/addon/engine.js +++ b/lib/registries/addon/engine.js @@ -30,6 +30,7 @@ const engine = Engine.extend({ 'store', ], externalRoutes: [ + 'search', 'guid-registration', 'guid-registration.analytics', 'guid-registration.forks', diff --git a/lib/registries/addon/index/controller.ts b/lib/registries/addon/index/controller.ts index 9a13d76df7..1dc2d89976 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: 'Registration,RegistrationComponent', + }, }); } } diff --git a/mirage/config.ts b/mirage/config.ts index 4fee24ca92..aadb879e9b 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'; diff --git a/mirage/fixtures/preprint-providers.ts b/mirage/fixtures/preprint-providers.ts index 738d808ff6..097df9fd02 100644 --- a/mirage/fixtures/preprint-providers.ts +++ b/mirage/fixtures/preprint-providers.ts @@ -51,6 +51,24 @@ const preprintProviders: Array> = [ preprintWord: 'default', assets: randomAssets(), }, + { + id: 'agrixiv', + name: 'AgriXiv', + preprintWord: 'preprint', + assets: randomAssets(), + }, + { + id: 'biohackrxiv', + name: 'BioHackrXiv', + preprintWord: 'preprint', + assets: randomAssets(), + }, + { + id: 'nutrixiv', + name: 'NutriXiv', + preprintWord: 'preprint', + assets: randomAssets(), + }, ]; export default preprintProviders; diff --git a/mirage/scenarios/default.ts b/mirage/scenarios/default.ts index 3556cb32f2..be9188c86f 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 0000000000..12bbf7f0d1 --- /dev/null +++ b/mirage/scenarios/preprints.ts @@ -0,0 +1,61 @@ +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, +) { + 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, + }); + thesisCommons.update({ + brand, + moderators: [currentUserModerator], + preprints, + description: '

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

    ', + }); + + 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/serializers/preprint-provider.ts b/mirage/serializers/preprint-provider.ts new file mode 100644 index 0000000000..e2349cab28 --- /dev/null +++ b/mirage/serializers/preprint-provider.ts @@ -0,0 +1,73 @@ +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 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}/highlightedSubjects/`, + meta: this.buildRelatedLinkMeta(model, 'highlightedSubjects'), + }, + }, + }, + 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 0000000000..bbdabe24d1 --- /dev/null +++ b/mirage/views/search.ts @@ -0,0 +1,523 @@ +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: { + href: 'https://staging-share.osf.io/api/v3/index-card-search?page%5Bcursor%5D=lmnop', + }, + }, + }, + relatedProperties: { + data: [ + { + type: 'related-property-path', + id: 'propertyMatch1', + }, + { + type: 'related-property-path', + id: 'propertyMatch2', + }, + { + type: 'related-property-path', + id: 'propertyMatch3', + }, + ], + links: { + next: { + href: 'https://staging-share.osf.io/api/v3/index-card-search?page%5Bcursor%5D=lmnop', + }, + }, + }, + }, + }, + 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 + { + type: 'related-property-path', + id: 'propertyMatch1', + attributes: { + matchEvidence: [ + { + '@type': ['https://share.osf.io/vocab/2023/trove/IriMatchEvidence'], + osfmapPropertyPath: 'resourceType', + matchingIri: 'rdf:Property', + }, + ], + cardSearchResultCount: 345, + propertyPath: [ + { + '@id': 'http://purl.org/dc/terms/license', + resourceType: [ + { + '@id': 'Property', + }, + ], + displayLabel: [ + { + '@value': 'License', + '@language': 'en', + }, + ], + shortFormLabel: [ + { + '@value': 'rights', + '@language': 'en', + }, + ], + }, + ], + }, + }, + { + type: 'related-property-path', + id: 'propertyMatch2', + attributes: { + matchEvidence: [ + { + '@type': ['https://share.osf.io/vocab/2023/trove/IriMatchEvidence'], + osfmapPropertyPath: 'resourceType', + matchingIri: 'rdf:Property', + }, + ], + cardSearchResultCount: 123, + propertyPath: [ + { + '@id': 'http://purl.org/dc/terms/published', + resourceType: [ + { + '@id': 'Property', + }, + ], + displayLabel: [ + { + '@value': 'Date Published', + '@language': 'en', + }, + ], + shortFormLabel: [ + { + '@value': 'datePublished', + '@language': 'en', + }, + ], + }, + ], + }, + }, + { + type: 'related-property-path', + id: 'propertyMatch3', + attributes: { + matchEvidence: [ + { + '@type': ['https://share.osf.io/vocab/2023/trove/IriMatchEvidence'], + osfmapPropertyPath: 'resourceType', + matchingIri: 'rdf:Property', + }, + ], + cardSearchResultCount: 33, + propertyPath: [ + { + '@id': 'http://purl.org/dc/terms/funder', + resourceType: [ + { + '@id': 'Property', + }, + ], + displayLabel: [ + { + '@value': 'Funder', + '@language': 'en', + }, + ], + shortFormLabel: [ + { + '@value': 'funder', + '@language': 'en', + }, + ], + }, + ], + }, + }, + ], + }; +} + +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: { + href: 'https://staging-share.osf.io/api/v3/index-value-search?page%5Bcursor%5D=lmnop', + }, + }, + }, + }, + }, + included: [ + { + type: 'search-result', + id: property1Id, + attributes: { + matchEvidence: [ + { + '@type': ['https://share.osf.io/vocab/2023/trove/TextMatchEvidence'], + osfmapPropertyPath: 'title', + matchingHighlight: 'National Institute of Health', + }, + ], + cardSearchResultCount: 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', + }, + ], + cardSearchResultCount: 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 1306487c4a..a6b74de8af 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/tests/acceptance/institutions/discover-test.ts b/tests/acceptance/institutions/discover-test.ts new file mode 100644 index 0000000000..e7925b7322 --- /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, skip } from 'qunit'; +import { click, setupOSFApplicationTest} from 'ember-osf-web/tests/helpers'; + +module('Acceptance | institutions | discover', hooks => { + setupOSFApplicationTest(hooks); + setupMirage(hooks); + + skip('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); + }); + + skip('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 0000000000..0804d81b7e --- /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, skip } 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; + }); + + skip('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); + }); + + skip('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/search/search-filters-test.ts b/tests/acceptance/search/search-filters-test.ts new file mode 100644 index 0000000000..ef31ca35de --- /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 0000000000..9fb1592235 --- /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. More information is available on our help guides.`); + 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('Refine Your Search'); + 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! Learn more in our help guides.`); + 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 0000000000..c0156b960d --- /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=dateCreated', + '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=dateModified', + 'Query-params are updated', + ); + }); + + test('query-parameters from url', async assert => { + await visit('/search?resourceType=Preprint&sort=-dateModified'); + 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 eb96a69feb..3d101d7863 100644 --- a/tests/engines/registries/acceptance/branded/discover-test.ts +++ b/tests/engines/registries/acceptance/branded/discover-test.ts @@ -78,12 +78,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 page'); await visit(`/registries/${osfProvider.id}`); assert.equal(currentRouteName(), - 'registries.discover', - '/registries/osf redirects to registries/discover'); + 'registries.index', + '/registries/osf redirects to registries index page'); }); }); 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 0f51b65892..0000000000 --- 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 1231065a1c..f7f755250b 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 84279fc153..0000000000 --- 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/integration/components/open-badges-list/component-test.ts b/tests/integration/components/open-badges-list/component-test.ts index 6d6847cf8a..971bf322ca 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/helpers/sufficient-contrast-test.ts b/tests/integration/helpers/sufficient-contrast-test.ts new file mode 100644 index 0000000000..8f66b86f7c --- /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 23264f7bd6..bcd5b8d77a 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 de875b8848..c01af574c0 100644 --- a/translations/en-us.yml +++ b/translations/en-us.yml @@ -37,6 +37,7 @@ general: cancel: Cancel add: Add ok: OK + apply: Apply revisions: Revisions md5: MD5 date: Date @@ -208,6 +209,71 @@ 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: 'Refine Your Search' + header-3: 'Add Metadata' + body-1: 'Enter any term in the search box and filter by specific object types. More information is available on our help guides.' + 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! Learn more in our help guides.' + 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: 'Enter search term(s) here' + 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' + first: First + prev: Prev + next: Next + 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} {label}' + 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' + load-more: 'Load more' + has-resource: 'Has {resource}' + no-resource: 'No {resource}' + + 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 +326,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 +334,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 @@ -545,7 +612,7 @@ node_navbar: settings: Settings comments: Comments status: - welcome_message: '

    Welcome to OSF!

    Visit our Guides to learn about creating a project, or get inspiration from popular public projects.

    ' + welcome_message: '

    Welcome to OSF!

    Visit our Guides to learn about creating a project, or get inspiration from popular public projects.

    ' alternate_email_error: 'The email address has NOT been added to your account. Please log out and revisit the link in your email. Thank you.' # remove_addon: 'Because the GitHub add-on for {extra.category} "{extra.title}" was authenticated by {extra.user}, authentication information has been deleted.' project_deleted: 'Project has been successfully deleted.' @@ -735,6 +802,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 +1112,11 @@ collections: accept: accepted reject: rejected remove: 'removed' +preprints: + osf-title: 'OSF Preprints' + provider-title: '{name} {pluralizedPreprintWord}' + discover: + title: 'Search' registries: header: osf_registrations: 'OSF Registrations' @@ -1783,6 +1857,42 @@ routes: institution: Institution email: Email osf-components: + search-result-card: + language: Language + url: URL + resource_type: Resource type + 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 + registration_provider: Registration 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 + link_to_orcid_id: Link to orcid id + withdrawn: Withdrawn + unknown: Unknown + remaining_count: '{count} more' 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.'