API Docs for: 0.25.0
Show:

File: addon/components/discover-page/component.js

import Ember from 'ember';
import layout from './template';

import config from 'ember-get-config';
import { task } from 'ember-concurrency';

import Analytics from '../../mixins/analytics';
import hostAppName from '../../mixins/host-app-name';

/**
 * @module ember-osf
 * @submodule components
 */

/**
 *  Discover-page component. Builds a search interface utilizing SHARE.
 *  See retraction-watch, registries, and preprints discover pages for working examples.
 *
 *  Majority adapted from Ember-SHARE https://github.com/CenterForOpenScience/ember-share, with additions from PREPRINTS
 *  and REGISTRIES discover pages. Original Ember-SHARE facets and PREPRINTS/REGISTRIES facets behave differently at this time.
 *  You can build a discover-page that uses Ember-SHARE type facets -OR- PREPRINTS/REGISTRIES type facets.  Would not recommend
 *  mixing until code is combined.
 *
 *  How to Use:
 *  Pass in custom text like searchPlaceholder.  The facets property will enable you to customize the filters
 *  on the left-hand side of the discover page. Sort options are the sort dropdown options. Each query parameter must be passed in individually,
 *  so they are reflected in the URL.  Logo and custom colors must be placed in the consuming application's stylesheet. Individual components
 *  can additionally be overridden in your application.
 *
 * Sample usage:
 * ```handlebars
 *{{discover-page
 *    consumingService=consumingService
 *    searchPlaceholder=searchPlaceholder
 *    detailRoute=detailRoute
 *    discoverHeader=discoverHeader
 *    themeProvider=themeProvider
 *
 *    sortOptions=sortOptions
 *    filterReplace=filterReplace
 *    whiteListedProviders=whiteListedProviders
 *    fetchedProviders=externalProviders
 *
 *    facets=facets
 *    results=results
 *    numberOfResults=numberOfResults
 *    aggregations=aggregations
 *    queryParamsState=queryParamsState
 *
 *    showActiveFilters=showActiveFilters
 *    loading=fetchData.isRunning
 *
 *    clearFilters=(action 'clearFilters')
 *    search=(action 'search')
 *
 *    size=size
 *    page=page
 *    q=q
 *    sort=sort
 * }}
 * {{!-- plus any query params (e.g. provider=provider) --}}
 * ```
 * @class discover-page
 */

const MAX_SOURCES = 500;

export default Ember.Component.extend(Analytics, hostAppName, {
    layout,
    currentUser: Ember.inject.service('current-user'),
    theme: Ember.inject.service(),
    i18n: Ember.inject.service(),

    classNames: ['discover-page'],

    // ************************************************************
    // PROPERTIES
    // ************************************************************

    /**
     * Size query parameter.  If "size" is one of your query params, it must be passed to the component so it can be updated.
     * @property {Integer} size
     * @default 10
     */
    size: 10,

    /**
     * Sort query parameter.  If "sort" is one of your query params, it must be passed to the component so it can be updated.
     * @property {String} sort
     * @default ''
     */
    sort: '',

    /**
     * Page query parameter.  If "page" is one of your query params, it must be passed to the component so it can be updated.
     * @property {Integer} page
     * @default 1
     */
    page: 1,

    /**
     * q query parameter.  If "q" is one of your query params, it must be passed to the component so it can be updated.
     * @property {String} q
     * @default ''
     */
    q: '',

    /**
     * Name of detail route for consuming application, like "content" or "detail". Override if search result title should link to detail route.
     * @property {String} detailRoute
     */
    detailRoute: null,

    /**
     * Text header for top of discover page.
     * @property {String} discoverHeader
     */
    discoverHeader: null,


    /**
     * For PREPRINTS ONLY.  Pass in the providers fetched in preprints app so they can be used in the provider carousel
     * @property {Object} fetchedProviders
     */
    fetchedProviders: null,

    /**
     * For PREPRINTS and REGISTRIES. A mapping of filter names for front-end display. Ex. {OSF: 'OSF Preprints'}.
     * @property {Object} filterReplace
     */
    filterReplace: {},

    /**
     * For PREPRINTS and REGISTRIES.  Displays activeFilters box above search facets.
     * @property {boolean} showActiveFilters
     */
    showActiveFilters: false,

    showLuceneHelp: false,

    numberOfResults: 0,
    numberOfSources: 0,

    results: Ember.ArrayProxy.create({ content: [] }), // Results from SHARE query

    /**
     * Sort dropdown options - Array of dictionaries.  Each dictionary should have display and sortBy keys.
     * @property {Array} sortOptions
     * @default [{
        display: 'Relevance',
        sortBy: ''
       }]
     */
    sortOptions: [{
        display: 'Relevance',
        sortBy: ''
    }, {
        display: 'Date Updated (Desc)',
        sortBy: '-date_updated'
    }, {
        display: 'Date Updated (Asc)',
        sortBy: 'date_updated'
    }, {
        display: 'Ingest Date (Asc)',
        sortBy: 'date_created'
    }, {
        display: 'Ingest Date (Desc)',
        sortBy: '-date_created'
    }],

    /**
     * themeProvider
     * @property {Object} Preprint provider loaded from theme service. Should be passed from consuming service so it is loaded before SHARE is queried.
     * @default ''
     */
    themeProvider: null,


    // ************************************************************
    // COMPUTED PROPERTIES
    // ************************************************************

    domainRedirectProviders: Ember.computed(function() {
        let providerDomains = [];
        let providers = this.get('fetchedProviders');
        for (let providerVal of providers) {
            if (providerVal.get('domain') && providerVal.get('domainRedirectEnabled') === true) {
                providerDomains.push(providerVal.get('domain'));
            }
        }
        return providerDomains;
    }),

    clampedPages: Ember.computed('totalPages', 'size', function() {
        // requesting over 10000 will error due to elastic limitations
        // https://www.elastic.co/guide/en/elasticsearch/guide/current/pagination.html
        let maxPages = Math.ceil(10000 / this.get('size'));
        let totalPages = this.get('totalPages');

        return totalPages < maxPages ? totalPages : maxPages;
    }),

    totalPages: Ember.computed('numberOfResults', 'size', function() {
        // Total pages of search results
        return Math.ceil(this.get('numberOfResults') / this.get('size'));
    }),

    actions: {
        clearFilters() {
            this.get('metrics').trackEvent({
                category: 'button',
                action: 'click',
                label: 'Discover - Clear Filters',
            });

            this.get('clearFilters')();
        },

        search() {
            this.get('metrics').trackEvent({
                category: 'button',
                action: 'click',
                label: 'Discover - Search',
                extra: this.get('q'),
            });

            this.get('search')();
        },

        selectSortOption(option) {
            // Runs when sort option changed in dropdown
            this.get('metrics').trackEvent({
                category: 'dropdown',
                action: 'select',
                label: `Sort by: ${option || 'relevance'}`
            });

            this.set('sort', option);
        },

        selectPage(pageNumber) {
            // When paginating, sets page and scrolls to top of results.
            this.set('page', pageNumber);
            if (scroll) {
                this.scrollToResults();
            }
        },

        toggleShowLuceneHelp() {
            this.toggleProperty('showLuceneHelp');
        },

        updateFilters(filterType, item) {
            item = typeof item === 'object' ? item.text : item;
            const currentState = this.get(`queryParamsState.${filterType}.value`).slice(0);
            const hasItem = currentState.includes(item);

            if (hasItem) {
                this.set(filterType, currentState.filter(x => !item.includes(x)));
            } else {
                currentState.pushObject(item)
                this.set(filterType, currentState);
            }

            this.get('metrics').trackEvent({
                category: 'filter',
                action: hasItem ? 'remove' : 'add',
                label: `Discover - ${filterType} ${item}`
            });
        },

        updateParams(key, value) {
            this.set(key, value);
        },
    },

    // ************************************************************
    // Discover-page METHODS and HOOKS
    // ************************************************************

    scrollToResults() {
        // Scrolls to top of search results
        Ember.$('html, body').scrollTop(this.$('.results-top').position().top);
    },

    isTypeFacet(obj) {
        return obj.key === 'type';
    },

    getTypes: task(function* () {
        const response = yield Ember.$.ajax({
            url: `${config.OSF.shareApiUrl}/schema/creativework/hierarchy/`,
            crossDomain: true,
            type: 'GET',
            contentType: 'application/vnd.api+json',
        });
        if (response.data) {
            const types = response.data.CreativeWork ? response.data.CreativeWork.children : {};
            this.get('facets').find(this.isTypeFacet).data = this.transformTypes(types);
        }
    }),

    transformTypes(types) {
        const tmpTypes = types;
        if (typeof (tmpTypes) !== 'object') {
            return tmpTypes;
        }

        for (const key of Object.keys(tmpTypes)) {
            const lowKey = key.replace(/([A-Z])/g, ' $1').trim().toLowerCase();
            tmpTypes[lowKey] = this.transformTypes(tmpTypes[key]);
            if (key !== lowKey) {
                delete tmpTypes[key];
            }
        }

        return tmpTypes;
    },

    getCounts: task(function* () {
        const searchUrl = `${config.OSF.shareSearchUrl}?preference=${this.get('currentUser.sessionKey')}`;
        const queryBody = JSON.stringify({
            size: 0,
            aggregations: {
                sources: {
                    cardinality: {
                        field: 'sources',
                        precision_threshold: MAX_SOURCES,
                    },
                },
            },
        });
        const response = yield Ember.$.ajax({
            url: searchUrl,
            crossDomain: true,
            type: 'POST',
            contentType: 'application/json',
            data: queryBody,
        });
        this.setProperties({
            numberOfEvents: response.hits.total,
            numberOfSources: response.aggregations.sources.value,
        });
    }),

    init() {
        // Runs on initial render.
        this._super(...arguments);
        if (this.get('facets').find(this.isTypeFacet)) {
            this.get('getTypes').perform();
        }
        this.get('getCounts').perform();
    },
});