<commit_before>
define([
    'flight/lib/component',
    'flight/lib/registry',
    'data',
    'service/vertex',
    'service/ontology',
    'util/vertexList/list',
    'util/formatters',
    './filters/filters',
    'tpl!./search',
    'tpl!./conceptItem',
    'tpl!./conceptSections',
    'tpl!util/alert',
    'util/jquery.ui.draggable.multiselect',
    'sf'
], function(
    defineComponent,
    registry,
    appData,
    VertexService,
    OntologyService,
    VertexList,
    formatters,
    Filters,
    template,
    conceptItemTemplate,
    conceptSectionsTemplate,
    alertTemplate,
    multiselect,
    sf) {
    'use strict';

    return defineComponent(Search);

    function Search() {
        this.vertexService = new VertexService();
        this.ontologyService = new OntologyService();
        this.currentQuery = null;

        this.defaultAttrs({
            formSelector: '.navbar-search',
            querySelector: '.navbar-search .search-query',
            queryValidationSelector: '.search-query-validation',
            filtersInfoSelector: '.filter-info',
            resultsSummarySelector: '.search-results-summary',
            entitiesHeaderBadgeSelector: '.search-results-summary li.entities .badge',
            summaryResultItemSelector: '.search-results-summary li',
            resultsSelector: '.search-results',
            filtersSelector: '.search-filters'
        });

        this.searchResults = null;

        this.onEntitySearchResultsForConcept = function($searchResultsSummary, concept, entities, count, parentPropertyListElements) {
            var self = this,
                resultsCount = count,
                badge = this.updateCountBadgeForConcept('concept-' + concept.id, resultsCount),
                li = badge.closest('li').toggle(resultsCount > 0);

            parentPropertyListElements = parentPropertyListElements || $();

            this.select('entitiesHeaderBadgeSelector').removeClass('loading');

            if (resultsCount) {
                parentPropertyListElements.show();
            }

            if(concept.children && concept.children.length > 0) {
                var parentLis = parentPropertyListElements.add(li);
                concept.children.forEach(function(childConcept) {
                    self.onEntitySearchResultsForConcept(
                        $searchResultsSummary,
                        childConcept,
                        entities,
                        count,
                        parentLis
                    );
                });
            }
        };

        this.onFormSearch = function(evt) {
            evt.preventDefault();
            var $searchQueryValidation = this.select('queryValidationSelector');
            $searchQueryValidation.html('');

            var query = this.select('querySelector').val();
            if(query) {
                query = $.trim(query);
            }
            if(!query) {
                this.select('resultsSummarySelector').empty();
                return $searchQueryValidation.html(alertTemplate({ error: 'Query cannot be empty' }));
            }
            this.trigger('search', { query: query });
            return false;
        };

        this.getConceptChildrenHtml = function(concept, indent) {
            var self = this,
                html = "";
            (concept.children || []).forEach(function(concept) {
                html += conceptItemTemplate({
                    concept: concept,
                    indent: indent
                });
                if(concept.children && concept.children.length > 0) {
                    html += self.getConceptChildrenHtml(concept, indent + 15);
                }
            });
            return html;
        };

        this.updateCountBadgeForConcept = function(conceptId, count) {
            return this.$node.find('.' + conceptId + ' .badge')
                .removeClass('loading')
                .data('count', count)
                .text(formatters.number.pretty(count));
        };

        this.onSearchByEntity = function (evt, data) {
            this.select('querySelector').val(data.query);
            if (!this.$node.closest(':data(menubarName)').hasClass('visible')) {
                this.trigger ('menubarToggleDisplay', { name : 'search' });
            }
            this.trigger ('search', { query : data.query});
        }

        this.onSearch = function(evt, data) {
            var query = data.query || this.select('querySelector').val();

            if (!this.searchResults) {
                this.searchResults = {};
            }

            if (query != data.query) {
                this.select('querySelector').val(data.query);
            }

            var self = this;

            this.ontologyService.concepts(function(err, concepts) {
                this.updateConceptSections(concepts);

                var paging = { offset:0, size:100 },
                    subTypeFilter = null;

                this.vertexService.graphVertexSearch(query, this.filters, subTypeFilter, paging)
                    .done(function(vertexResults) {
                        var results = {},
                            sortVerticesIntoResults = function(v) {
                                var props = v.properties,
                                    conceptType = props._conceptType,
                                    addToSearchResults = function(conceptType) {
                                        if (!results[conceptType]) results[conceptType] = [];

                                        // Check for an existing result with the same id
                                        var resultFound = results[conceptType].some(function(result) { return result.id === v.id; });

                                        // Only store unique results
                                        if (resultFound === false) {
                                            results[conceptType].push(v);
                                        }
                                    };

                                var vertexConcept = concepts.byId[conceptType];
                                while (vertexConcept) {
                                    addToSearchResults(vertexConcept.id, v);
                                    vertexConcept = vertexConcept.parentId ? concepts.byId[vertexConcept.parentId] : null;
                                }
                            };
                        vertexResults.vertices.forEach(sortVerticesIntoResults);

                        self.searchResults = results;

                        var counts = _.values(vertexResults.verticesCount);
                        if (counts.length === 0 || Math.max.apply([], counts) === 0) {
                            var headerTextNode = self.$node.find('.search-results-summary li.entities');
                            if (headerTextNode.length) {
                                headerTextNode[0].normalize();
                                headerTextNode[0].textContent = 'No Entities';
                            }
                        } else {
                            var countMap = vertexResults.verticesCount,
                                summaryNode = self.select('resultsSummarySelector');

                            concepts.byTitle.forEach(function(concept) {
                                var count = countMap[concept.id] || 0,
                                    childrenCounts = _.pick(countMap, _.pluck(concept.children, 'id')),
                                    total = _.reduce(childrenCounts, function(memo, i) {return memo + i}, 0);

                                self.onEntitySearchResultsForConcept(summaryNode, concept, results.entity, count + total);
                            });
                        }

                    }).fail(function() {
                        var $searchQueryValidation = self.select('queryValidationSelector');
                        self.select('resultsSummarySelector').empty();
                        return $searchQueryValidation.html(alertTemplate({ error: 'Invalid query' }));
                    });
            }.bind(this));
        };

        this.updateConceptSections = function(concepts) {
            var $searchResultsSummary = this.select('resultsSummarySelector'),
                resultsHtml = this.getConceptChildrenHtml(concepts.entityConcept, 15);

            $searchResultsSummary.html(conceptSectionsTemplate({ resultsHtml: resultsHtml }));
            $('.badge', $searchResultsSummary).addClass('loading');
        };

        this.onSummaryResultItemClick = function(evt) {
            evt.preventDefault();

            var $target = $(evt.target).parents('li');
            if ($target.hasClass('active')) {
                return this.close(evt);
            }

            var count = $target.find('.badge').data('count');
            if (count === 0) {
                return this.close(evt);
            }

            this.$node.find('.search-results-summary .active').removeClass('active');
            $target.addClass('active');

            this.trigger('showSearchResults', {
                conceptId: $target.data('conceptId'),
                count: count
            });
        };

        this.onShowSearchResults = function(evt, data) {
            var self = this,
                $searchResults = this.select('resultsSelector'),
                vertexIds = _.pluck(this.searchResults[data.conceptId] || [], 'id'),
                vertices = appData.vertices(vertexIds);

            this.hideSearchResults();
            this.select('filtersSelector').hide();

            if (data.count) {
                VertexList.attachTo($searchResults.find('.content'), {
                    vertices: vertices,
                    infiniteScrolling: true,
                    verticesConceptId: data.conceptId,
                    total: data.count
                });
                this.makeResizable($searchResults);
                $searchResults.show();
                $searchResults.find('.multi-select').focus();
            }
            this.trigger(document, 'paneResized');
        };

        this.makeResizable = function(node) {
            var self = this;

            // Add splitbar to search results
            return node.resizable({
                handles: 'e',
                minWidth: 200,
                maxWidth: 350,
                resize: function() {
                    self.trigger(document, 'paneResized');
                }
            });
        };

        this.onKeyUp = function (evt) {
            var search = this.select('querySelector'),
                query = search.val();

            if (query != this.currentQuery) {
                this.trigger("searchQueryChanged", { query: query});
                this.currentQuery = query;
            }

            if (evt.which === 27) {
                search.blur();
            }
        };

        this.onQueryFocus = function (evt, data) {
            var filters = this.select('filtersSelector');
            Filters.attachTo(filters.find('.content'));

            this.makeResizable(filters);
            filters.show();
            this.hideSearchResults();
            this.$node.find('.search-results-summary .active').removeClass('active');
        };

        this.hideSearchResults = function() {
            registry.findInstanceInfoByNode(this.select('resultsSelector').hide().find('.content')[0]).forEach(function(info) {
                info.instance.teardown();
            });
            this.trigger(document, 'paneResized');
        };

        this.close = function(e) {
            this.hideSearchResults();
            this.$node.find('.search-results-summary .active').removeClass('active');
        };

        this.onPaneVisible = function() {
            this.select('querySelector').focus().select();
        };

        this.after('initialize', function() {
            var self = this;
            this.searchResults = {};
            this.$node.html(template({}));

            this.select('filtersSelector').hide();
            this.hideSearchResults();

            this.on('filterschange', this.onFiltersChange);
            this.on('infiniteScrollRequest', this.onInfiniteScrollRequest);

            this.on(document, 'searchByEntity', this.onSearchByEntity);
            this.on(document, 'search', this.onSearch);
            this.on(document, 'showSearchResults', this.onShowSearchResults);
            this.on(document, 'menubarToggleDisplay', this.onMenubarToggle);
            this.on(document, 'searchPaneVisible', this.onPaneVisible);
            this.on('submit', {
                formSelector: this.onFormSearch
            });
            this.on('click', {
                summaryResultItemSelector: this.onSummaryResultItemClick,
                filtersInfoSelector: this.onFiltersInfoRemoveClick
            });
            this.on('keyup', {
                querySelector: this.onKeyUp
            });

            this.select('querySelector').on('focus', this.onQueryFocus.bind(this));

            this.trigger(document, 'registerKeyboardShortcuts', {
                scope: 'Search',
                shortcuts: {
                    'meta-a': { fire:'selectAll', desc:'Select all search results' },
                    'up': { fire:'up', desc:'Select previous result'},
                    'down': { fire:'down', desc:'Select next result'}
                }
            });
        });

        this.onMenubarToggle = function(evt, data) {
            var pane = this.$node.closest(':data(menubarName)');
            if (data.name === pane.data('menubarName')) {
                if (!pane.hasClass('visible')) {
                    this.$node.find('.search-results-summary .active').removeClass('active');
                    this.select('filtersSelector').hide();
                    this.hideSearchResults();
                }
            }
        };

        this.onInfiniteScrollRequest = function(evt, data) {
            var self = this,
                query = this.select('querySelector').val();

            this.vertexService.graphVertexSearch(
                    query,
                    this.filters,
                    data.conceptType,
                    data.paging
            ).done(function(results) {

                self.trigger(
                    self.select('resultsSelector').find('.content'),
                    'addInfiniteVertices',
                    {
                        vertices: results.vertices
                    }
                );
            });
        };

        this.onFiltersChange = function(evt, data) {
            this.filters = data.filters;

            var query = this.select('querySelector').val() || '*';

            var filterInfo = this.select('filtersInfoSelector'),
                numberOfFilters = this.filters.length;

            filterInfo.find('.message').text(formatters.string.plural(numberOfFilters, 'filter') + ' applied');
            filterInfo.toggle(numberOfFilters > 0);

            this.trigger('search', { query:query });
        };

        this.onFiltersInfoRemoveClick = function() {
            this.trigger(this.select('filtersSelector').find('.content'), 'clearfilters');
        };
    }
});
<commit_msg>Cancel previous search requests when searching<commit_after>
define([
    'flight/lib/component',
    'flight/lib/registry',
    'data',
    'service/vertex',
    'service/ontology',
    'util/vertexList/list',
    'util/formatters',
    './filters/filters',
    'tpl!./search',
    'tpl!./conceptItem',
    'tpl!./conceptSections',
    'tpl!util/alert',
    'util/jquery.ui.draggable.multiselect',
    'sf'
], function(
    defineComponent,
    registry,
    appData,
    VertexService,
    OntologyService,
    VertexList,
    formatters,
    Filters,
    template,
    conceptItemTemplate,
    conceptSectionsTemplate,
    alertTemplate,
    multiselect,
    sf) {
    'use strict';

    return defineComponent(Search);

    function Search() {
        this.vertexService = new VertexService();
        this.ontologyService = new OntologyService();
        this.currentQuery = null;

        this.defaultAttrs({
            formSelector: '.navbar-search',
            querySelector: '.navbar-search .search-query',
            queryValidationSelector: '.search-query-validation',
            filtersInfoSelector: '.filter-info',
            resultsSummarySelector: '.search-results-summary',
            entitiesHeaderBadgeSelector: '.search-results-summary li.entities .badge',
            summaryResultItemSelector: '.search-results-summary li',
            resultsSelector: '.search-results',
            filtersSelector: '.search-filters'
        });

        this.searchResults = null;

        this.onEntitySearchResultsForConcept = function($searchResultsSummary, concept, entities, count, parentPropertyListElements) {
            var self = this,
                resultsCount = count,
                badge = this.updateCountBadgeForConcept('concept-' + concept.id, resultsCount),
                li = badge.closest('li').toggle(resultsCount > 0);

            parentPropertyListElements = parentPropertyListElements || $();

            this.select('entitiesHeaderBadgeSelector').removeClass('loading');

            if (resultsCount) {
                parentPropertyListElements.show();
            }

            if(concept.children && concept.children.length > 0) {
                var parentLis = parentPropertyListElements.add(li);
                concept.children.forEach(function(childConcept) {
                    self.onEntitySearchResultsForConcept(
                        $searchResultsSummary,
                        childConcept,
                        entities,
                        count,
                        parentLis
                    );
                });
            }
        };

        this.onFormSearch = function(evt) {
            evt.preventDefault();
            var $searchQueryValidation = this.select('queryValidationSelector');
            $searchQueryValidation.html('');

            var query = this.select('querySelector').val();
            if(query) {
                query = $.trim(query);
            }
            if(!query) {
                this.select('resultsSummarySelector').empty();
                return $searchQueryValidation.html(alertTemplate({ error: 'Query cannot be empty' }));
            }
            this.trigger('search', { query: query });
            return false;
        };

        this.getConceptChildrenHtml = function(concept, indent) {
            var self = this,
                html = "";
            (concept.children || []).forEach(function(concept) {
                html += conceptItemTemplate({
                    concept: concept,
                    indent: indent
                });
                if(concept.children && concept.children.length > 0) {
                    html += self.getConceptChildrenHtml(concept, indent + 15);
                }
            });
            return html;
        };

        this.updateCountBadgeForConcept = function(conceptId, count) {
            return this.$node.find('.' + conceptId + ' .badge')
                .removeClass('loading')
                .data('count', count)
                .text(formatters.number.pretty(count));
        };

        this.onSearchByEntity = function (evt, data) {
            this.select('querySelector').val(data.query);
            if (!this.$node.closest(':data(menubarName)').hasClass('visible')) {
                this.trigger ('menubarToggleDisplay', { name : 'search' });
            }
            this.trigger ('search', { query : data.query});
        }

        this.onSearch = function(evt, data) {
            var query = data.query || this.select('querySelector').val();

            if (!this.searchResults) {
                this.searchResults = {};
            }

            if (this.previousSearch && this.previousSearch.abort) {
                this.previousSearch.abort();
            }

            if (query != data.query) {
                this.select('querySelector').val(data.query);
            }

            var self = this;

            this.ontologyService.concepts(function(err, concepts) {
                this.updateConceptSections(concepts);

                var paging = { offset:0, size:100 },
                    subTypeFilter = null;

                this.previousSearch = this.vertexService.graphVertexSearch(query, this.filters, subTypeFilter, paging)
                    .done(function(vertexResults) {
                        self.previousSearch = null;

                        var results = {},
                            sortVerticesIntoResults = function(v) {
                                var props = v.properties,
                                    conceptType = props._conceptType,
                                    addToSearchResults = function(conceptType) {
                                        if (!results[conceptType]) results[conceptType] = [];

                                        // Check for an existing result with the same id
                                        var resultFound = results[conceptType].some(function(result) { return result.id === v.id; });

                                        // Only store unique results
                                        if (resultFound === false) {
                                            results[conceptType].push(v);
                                        }
                                    };

                                var vertexConcept = concepts.byId[conceptType];
                                while (vertexConcept) {
                                    addToSearchResults(vertexConcept.id, v);
                                    vertexConcept = vertexConcept.parentId ? concepts.byId[vertexConcept.parentId] : null;
                                }
                            };
                        vertexResults.vertices.forEach(sortVerticesIntoResults);

                        self.searchResults = results;

                        var counts = _.values(vertexResults.verticesCount);
                        if (counts.length === 0 || Math.max.apply([], counts) === 0) {
                            var headerTextNode = self.$node.find('.search-results-summary li.entities');
                            if (headerTextNode.length) {
                                headerTextNode[0].normalize();
                                headerTextNode[0].textContent = 'No Entities';
                            }
                        } else {
                            var countMap = vertexResults.verticesCount,
                                summaryNode = self.select('resultsSummarySelector');

                            concepts.byTitle.forEach(function(concept) {
                                var count = countMap[concept.id] || 0,
                                    childrenCounts = _.pick(countMap, _.pluck(concept.children, 'id')),
                                    total = _.reduce(childrenCounts, function(memo, i) {return memo + i}, 0);

                                self.onEntitySearchResultsForConcept(summaryNode, concept, results.entity, count + total);
                            });
                        }

                    }).fail(function(request, reason) {
                        if (reason === 'abort') return;
                        var $searchQueryValidation = self.select('queryValidationSelector');
                        self.select('resultsSummarySelector').empty();
                        return $searchQueryValidation.html(alertTemplate({ error: 'Invalid query' }));
                    });
            }.bind(this));
        };

        this.updateConceptSections = function(concepts) {
            var $searchResultsSummary = this.select('resultsSummarySelector'),
                resultsHtml = this.getConceptChildrenHtml(concepts.entityConcept, 15);

            $searchResultsSummary.html(conceptSectionsTemplate({ resultsHtml: resultsHtml }));
            $('.badge', $searchResultsSummary).addClass('loading');
        };

        this.onSummaryResultItemClick = function(evt) {
            evt.preventDefault();

            var $target = $(evt.target).parents('li');
            if ($target.hasClass('active')) {
                return this.close(evt);
            }

            var count = $target.find('.badge').data('count');
            if (count === 0) {
                return this.close(evt);
            }

            this.$node.find('.search-results-summary .active').removeClass('active');
            $target.addClass('active');

            this.trigger('showSearchResults', {
                conceptId: $target.data('conceptId'),
                count: count
            });
        };

        this.onShowSearchResults = function(evt, data) {
            var self = this,
                $searchResults = this.select('resultsSelector'),
                vertexIds = _.pluck(this.searchResults[data.conceptId] || [], 'id'),
                vertices = appData.vertices(vertexIds);

            this.hideSearchResults();
            this.select('filtersSelector').hide();

            if (data.count) {
                VertexList.attachTo($searchResults.find('.content'), {
                    vertices: vertices,
                    infiniteScrolling: true,
                    verticesConceptId: data.conceptId,
                    total: data.count
                });
                this.makeResizable($searchResults);
                $searchResults.show();
                $searchResults.find('.multi-select').focus();
            }
            this.trigger(document, 'paneResized');
        };

        this.makeResizable = function(node) {
            var self = this;

            // Add splitbar to search results
            return node.resizable({
                handles: 'e',
                minWidth: 200,
                maxWidth: 350,
                resize: function() {
                    self.trigger(document, 'paneResized');
                }
            });
        };

        this.onKeyUp = function (evt) {
            var search = this.select('querySelector'),
                query = search.val();

            if (query != this.currentQuery) {
                this.trigger("searchQueryChanged", { query: query});
                this.currentQuery = query;
            }

            if (evt.which === 27) {
                search.blur();
            }
        };

        this.onQueryFocus = function (evt, data) {
            var filters = this.select('filtersSelector');
            Filters.attachTo(filters.find('.content'));

            this.makeResizable(filters);
            filters.show();
            this.hideSearchResults();
            this.$node.find('.search-results-summary .active').removeClass('active');
        };

        this.hideSearchResults = function() {
            registry.findInstanceInfoByNode(this.select('resultsSelector').hide().find('.content')[0]).forEach(function(info) {
                info.instance.teardown();
            });
            this.trigger(document, 'paneResized');
        };

        this.close = function(e) {
            this.hideSearchResults();
            this.$node.find('.search-results-summary .active').removeClass('active');
        };

        this.onPaneVisible = function() {
            this.select('querySelector').focus().select();
        };

        this.after('initialize', function() {
            var self = this;
            this.searchResults = {};
            this.$node.html(template({}));

            this.select('filtersSelector').hide();
            this.hideSearchResults();

            this.on('filterschange', this.onFiltersChange);
            this.on('infiniteScrollRequest', this.onInfiniteScrollRequest);

            this.on(document, 'searchByEntity', this.onSearchByEntity);
            this.on(document, 'search', this.onSearch);
            this.on(document, 'showSearchResults', this.onShowSearchResults);
            this.on(document, 'menubarToggleDisplay', this.onMenubarToggle);
            this.on(document, 'searchPaneVisible', this.onPaneVisible);
            this.on('submit', {
                formSelector: this.onFormSearch
            });
            this.on('click', {
                summaryResultItemSelector: this.onSummaryResultItemClick,
                filtersInfoSelector: this.onFiltersInfoRemoveClick
            });
            this.on('keyup', {
                querySelector: this.onKeyUp
            });

            this.select('querySelector').on('focus', this.onQueryFocus.bind(this));

            this.trigger(document, 'registerKeyboardShortcuts', {
                scope: 'Search',
                shortcuts: {
                    'meta-a': { fire:'selectAll', desc:'Select all search results' },
                    'up': { fire:'up', desc:'Select previous result'},
                    'down': { fire:'down', desc:'Select next result'}
                }
            });
        });

        this.onMenubarToggle = function(evt, data) {
            var pane = this.$node.closest(':data(menubarName)');
            if (data.name === pane.data('menubarName')) {
                if (!pane.hasClass('visible')) {
                    this.$node.find('.search-results-summary .active').removeClass('active');
                    this.select('filtersSelector').hide();
                    this.hideSearchResults();
                }
            }
        };

        this.onInfiniteScrollRequest = function(evt, data) {
            var self = this,
                query = this.select('querySelector').val();

            this.vertexService.graphVertexSearch(
                    query,
                    this.filters,
                    data.conceptType,
                    data.paging
            ).done(function(results) {

                self.trigger(
                    self.select('resultsSelector').find('.content'),
                    'addInfiniteVertices',
                    {
                        vertices: results.vertices
                    }
                );
            });
        };

        this.onFiltersChange = function(evt, data) {
            this.filters = data.filters;

            var query = this.select('querySelector').val() || '*';

            var filterInfo = this.select('filtersInfoSelector'),
                numberOfFilters = this.filters.length;

            filterInfo.find('.message').text(formatters.string.plural(numberOfFilters, 'filter') + ' applied');
            filterInfo.toggle(numberOfFilters > 0);

            this.trigger('search', { query:query });
        };

        this.onFiltersInfoRemoveClick = function() {
            this.trigger(this.select('filtersSelector').find('.content'), 'clearfilters');
        };
    }
});
<|endoftext|>


<commit_before>import PropTypes from "prop-types";
import React from "react";
import { connect } from "react-redux";
import IssueDetailUI from "../components/issueDetail";
import {
  postComment,
  invalidateIssue,
  changeIssueStatus,
  selectIssue,
  fetchIssueIfNeeded,
  addIssueFilter,
  removeIssueFilter
} from "../actions";

import Loading from "../components/loading";

import Comments from "./comments";

import isEmpty from "lodash/isEmpty";

class IssueDetailContainer extends React.Component {
  constructor(props) {
    super(props);
  }

  componentWillReceiveProps(newProps) {
    if (this.props.issue_id !== newProps.issue_id) {
      newProps.loadIssue();
    }
  }

  componentDidMount() {
    this.props.loadIssue();
  }

  render() {
    if (!this.props.issue) {
      return <Loading />;
    }
    return (
      <IssueDetailUI {...this.props}>
        <Comments
          issue_id={this.props.issue_id}
          users={this.props.usernames}
          mentions={this.props.mentions}
        />
      </IssueDetailUI>
    );
  }
}

IssueDetailContainer.propTypes = {
  issue_id: PropTypes.number.isRequired,
  issue: PropTypes.object,
  users: PropTypes.array.isRequired,
  orgs: PropTypes.array.isRequired,
  mentions: PropTypes.array.isRequired,
  canComment: PropTypes.bool.isRequired
};

const mapStateToProps = (state, ownProps) => {
  let mentions = [...state.allUsers, ...state.allOrganisations];

  return {
    issue_id: ownProps.issue_id,
    issue: state.issueDetails[ownProps.issue_id],
    users: state.allUsers,
    orgs: state.allOrganisations,
    mentions: mentions,
    canComment: state.currentUser.canComment || false
  };
};

const mapDispatchToProps = (dispatch, ownProps) => {
  const issue_id = ownProps.issue_id;
  return {
    onComment: comment => {
      dispatch(postComment(issue_id, comment));
    },
    invalidateIssue: () => {
      dispatch(invalidateIssue(issue_id));
    },
    changeIssueStatus: status => {
      dispatch(changeIssueStatus(issue_id, status));
    },
    loadIssue: () => {
      dispatch(selectIssue(issue_id));
      dispatch(fetchIssueIfNeeded(issue_id));
    }
  };
};

export default connect(mapStateToProps, mapDispatchToProps)(
  IssueDetailContainer
);
<commit_msg>correctly invalidate issue<commit_after>import PropTypes from "prop-types";
import React from "react";
import { connect } from "react-redux";
import IssueDetailUI from "../components/issueDetail";
import {
  postComment,
  invalidateIssue,
  changeIssueStatus,
  selectIssue,
  fetchIssueIfNeeded,
  addIssueFilter,
  removeIssueFilter
} from "../actions";

import Loading from "../components/loading";

import Comments from "./comments";

import isEmpty from "lodash/isEmpty";

class IssueDetailContainer extends React.Component {
  constructor(props) {
    super(props);
  }

  componentWillReceiveProps(newProps) {
    if (
      this.props.issue_id !== newProps.issue_id ||
      newProps.issue.didInvalidate
    ) {
      newProps.loadIssue();
    }
  }

  componentDidMount() {
    this.props.loadIssue();
  }

  render() {
    if (!this.props.issue) {
      return <Loading />;
    }
    return (
      <IssueDetailUI {...this.props}>
        <Comments
          issue_id={this.props.issue_id}
          users={this.props.usernames}
          mentions={this.props.mentions}
        />
      </IssueDetailUI>
    );
  }
}

IssueDetailContainer.propTypes = {
  issue_id: PropTypes.number.isRequired,
  issue: PropTypes.object,
  users: PropTypes.array.isRequired,
  orgs: PropTypes.array.isRequired,
  mentions: PropTypes.array.isRequired,
  canComment: PropTypes.bool.isRequired
};

const mapStateToProps = (state, ownProps) => {
  let mentions = [...state.allUsers, ...state.allOrganisations];

  return {
    issue_id: ownProps.issue_id,
    issue: state.issueDetails[ownProps.issue_id],
    users: state.allUsers,
    orgs: state.allOrganisations,
    mentions: mentions,
    canComment: state.currentUser.canComment || false
  };
};

const mapDispatchToProps = (dispatch, ownProps) => {
  const issue_id = ownProps.issue_id;
  return {
    onComment: comment => {
      dispatch(postComment(issue_id, comment));
    },
    invalidateIssue: () => {
      dispatch(invalidateIssue(issue_id));
    },
    changeIssueStatus: status => {
      dispatch(changeIssueStatus(issue_id, status));
    },
    loadIssue: () => {
      dispatch(selectIssue(issue_id));
      dispatch(fetchIssueIfNeeded(issue_id));
    }
  };
};

export default connect(mapStateToProps, mapDispatchToProps)(
  IssueDetailContainer
);
<|endoftext|>


<commit_before>import * as firebase from 'firebase/app';

/**
 * Format a timestamp (in milliseconds) into a pretty string with just the time.
 *
 * @param {int} msTimestamp
 * @param {string} timezone
 * @returns {string} Time formatted into a string like '10:19 AM'.
 */
export function timestampToTimeFormatted(msTimestamp, timezone = 'America/New_York') {
  const date = new Date(msTimestamp);
  const formatOptions = {
    hour: 'numeric',
    minute: '2-digit',
    timeZone: timezone
  };
  return date.toLocaleTimeString('en-US', formatOptions);;
}

/**
 * Format a timestamp (in milliseconds) into a pretty string with just the date.
 *
 * @param {int} msTimestamp
 * @param {string} timezone
 * @returns {string} Time formatted into a string like 'Monday, January 19, 1970'.
 */
export function timestampToDateFormatted(msTimestamp, timezone='America/New_York') {
  const date = new Date(msTimestamp);
  const formatOptions = {
    weekday: 'long',
    year: 'numeric',
    month: 'long',
    day: 'numeric',
    timeZone: timezone
  };
  return date.toLocaleDateString('en-US', formatOptions);
}

/**
 * Format a timestamp (in milliseconds) into a pretty string.
 *
 * @param {int} msTimestamp
 * @param {string} timezone
 * @returns {string} Time formatted into a string like
 * "Monday, January 19, 1970, 02:48 AM"
 */
export function timestampToFormatted(msTimestamp, timezone = "America/New_York") {
  let date = new Date(msTimestamp);
  let formatOptions = {
    weekday: 'long',
    year: 'numeric',
    month: 'long',
    day: 'numeric',
    hour: 'numeric',
    minute: '2-digit',
    timeZone: timezone
  };
  return date.toLocaleString('en-US', formatOptions);
}

/**
 * Return a Firestore Timestamp corresponding to the date in `dateStr`.
 *
 * @param {string} dateStr String containing a date in the form 'yyyy-mm-dd'.
 * @return {firebase.firestore.Timestamp} Firestore timestamp object created.
 */
export function getTimestampFromDateString(dateStr) {
  const dateParts = dateStr.split('-').map(str => +str);
  if (dateParts.length === 1 && dateParts[0] === 0) {
    return firebase.firestore.Timestamp.now();
  }

  const date = new Date(dateParts[0], dateParts[1] - 1, dateParts[2]);
  return firebase.firestore.Timestamp.fromDate(date);
}

/**
 * Returns a date string in ISO format.
 *
 * @param {firebase.firestore.Timestamp} timestamp Firestore timestamp object.
 * @return {string} ISO formatted date string.
 */
export function timestampToISOString(timestamp) {
  return timestamp.toDate().toISOString().substring(0,10);
}
<commit_msg>Improve JSDoc for time util timestamp <--> iso date string functions.<commit_after>import * as firebase from 'firebase/app';

/**
 * Format a timestamp (in milliseconds) into a pretty string with just the time.
 *
 * @param {int} msTimestamp
 * @param {string} timezone
 * @returns {string} Time formatted into a string like '10:19 AM'.
 */
export function timestampToTimeFormatted(msTimestamp, timezone = 'America/New_York') {
  const date = new Date(msTimestamp);
  const formatOptions = {
    hour: 'numeric',
    minute: '2-digit',
    timeZone: timezone
  };
  return date.toLocaleTimeString('en-US', formatOptions);;
}

/**
 * Format a timestamp (in milliseconds) into a pretty string with just the date.
 *
 * @param {int} msTimestamp
 * @param {string} timezone
 * @returns {string} Time formatted into a string like 'Monday, January 19, 1970'.
 */
export function timestampToDateFormatted(msTimestamp, timezone='America/New_York') {
  const date = new Date(msTimestamp);
  const formatOptions = {
    weekday: 'long',
    year: 'numeric',
    month: 'long',
    day: 'numeric',
    timeZone: timezone
  };
  return date.toLocaleDateString('en-US', formatOptions);
}

/**
 * Format a timestamp (in milliseconds) into a pretty string.
 *
 * @param {int} msTimestamp
 * @param {string} timezone
 * @returns {string} Time formatted into a string like
 * "Monday, January 19, 1970, 02:48 AM"
 */
export function timestampToFormatted(msTimestamp, timezone = "America/New_York") {
  let date = new Date(msTimestamp);
  let formatOptions = {
    weekday: 'long',
    year: 'numeric',
    month: 'long',
    day: 'numeric',
    hour: 'numeric',
    minute: '2-digit',
    timeZone: timezone
  };
  return date.toLocaleString('en-US', formatOptions);
}

/**
 * Return a Firestore Timestamp corresponding to the date in `dateStr`.
 *
 * @param {string} dateStr String containing a date in the form 'YYYY-MM-DD'.
 * @return {firebase.firestore.Timestamp} Firestore timestamp object created.
 */
export function getTimestampFromDateString(dateStr) {
  const dateParts = dateStr.split('-').map(str => +str);
  if (dateParts.length === 1 && dateParts[0] === 0) {
    return firebase.firestore.Timestamp.now();
  }

  const date = new Date(dateParts[0], dateParts[1] - 1, dateParts[2]);
  return firebase.firestore.Timestamp.fromDate(date);
}

/**
 * Formats a Firestore timestamp into a date string in ISO format.
 *
 * @param {firebase.firestore.Timestamp} timestamp Firestore timestamp object.
 * @return {string} ISO formatted date string: "YYYY-MM-DD or 2020-05-12".
 */
export function timestampToISOString(timestamp) {
  return timestamp.toDate().toISOString().substring(0,10);
}
<|endoftext|>


<commit_before>import * as moment from 'moment-timezone';
import { countryCodes } from '../../constants/countries.js';
import { firestore } from 'firebase';

/**
 * Format a timestamp (in milliseconds) into a pretty string with just the time.
 *
 * @param {int} msTimestamp
 * @param {string} timezone
 * @returns {string} Time formatted into a string like '10:19 AM'.
 */
export function timestampToTimeFormatted(msTimestamp, timezone = 'America/New_York') {
  const date = new Date(msTimestamp);
  const formatOptions = {
    hour: 'numeric',
    minute: '2-digit',
    timeZone: timezone
  };
  return date.toLocaleTimeString('en-US', formatOptions);
}

/**
 * Format a timestamp (in milliseconds) into a pretty string with just the date.
 *
 * @param {int} msTimestamp
 * @param {string} timezone
 * @returns {string} Time formatted into a string like 'Monday, January 19, 1970'.
 */
export function timestampToDateFormatted(msTimestamp, timezone='America/New_York') {
  const date = new Date(msTimestamp);
  const formatOptions = {
    weekday: 'long',
    year: 'numeric',
    month: 'long',
    day: 'numeric',
    timeZone: timezone
  };
  return date.toLocaleDateString('en-US', formatOptions);
}

/**
 * Format a timestamp (in milliseconds) into a pretty string.
 *
 * @param {int} msTimestamp
 * @param {string} timezone
 * @returns {string} Time formatted into a string like
 * 'Monday, January 19, 1970, 02:48 AM'
 */
export function timestampToFormatted(msTimestamp, timezone = 'America/New_York') {
  let date = new Date(msTimestamp);
  let formatOptions = {
    weekday: 'long',
    year: 'numeric',
    month: 'long',
    day: 'numeric',
    hour: 'numeric',
    minute: '2-digit',
    timeZone: timezone
  };
  return date.toLocaleString('en-US', formatOptions);
}

/**
 * Returns all the time zones in a country (in pretty format).
 *
 * @param {string} countryName The name of the country for which to get the time zones.
 * @returns The list of time zones in the provided country.
 */
export function timezonesForCountry(countryName) {
  let zones;
  try {
    const countryCode = countryCodes[countryName];
    zones = moment.tz.zonesForCountry(countryCode);
  } catch (e) {
    zones = moment.tz.names();
  }
  return zones.map(e => {
    return e.replace(/[_]/g, ' ');
  });
}

/**
 * Get a date in 'YYYY-MM-DD' format.
 *
 * @param {string} msTimestamp Timestamp, in milliseconds since epoch.
 * @param {string} timezone The timezone which the string should be returned in.
 * @returns {string} The date in 'YYYY-MM-DD' format.
 */
export function getDateBarebones(msTimestamp, timezone=null) {
  if (timezone === null) {
    return moment.tz(parseFloat(msTimestamp), '').format('YYYY-MM-DD');
  }
  return moment.tz(parseFloat(msTimestamp), timezone).format('YYYY-MM-DD');
}

/**
 * Get a time in 24-hour ('HH:mm') format.
 *
 * @param {string} msTimestamp Timestamp, in milliseconds since epoch.
 * @param {string} timezone The timezone which the string should be returned in.
 * @returns {string} The time in 24-hour (HH:mm) format.
 */
export function get24hTime(msTimestamp, timezone=null) {
  if (timezone === null) {
    return moment.tz(parseFloat(msTimestamp), '').format('HH:mm');
  }
  return moment.tz(parseFloat(msTimestamp), timezone).format('HH:mm');
}

/**
 * Get a Firebase Timestamp object for time.
 *
 * @param {string} time The time in 'HH:mm' format.
 * @param {string} date The date in 'YYYY-MM-DD' format.
 * @param {string} tz The timezone in which the date takes place.
 * @returns {firestore.Timestamp} Firestore timestamp object at the same time.
 */
export function getFirebaseTime(time, date, tz) {
  const mtzDate = moment.tz(time + " " + date, "HH:mm YYYY-MM-DD", tz);
  return new firestore.Timestamp(mtzDate.valueOf() / 1000, 0);
}<commit_msg>Comments<commit_after>import * as moment from 'moment-timezone';
import { countryCodes } from '../../constants/countries.js';
import { firestore } from 'firebase';

/**
 * Format a timestamp (in milliseconds) into a pretty string with just the time.
 *
 * @param {int} msTimestamp
 * @param {string} timezone
 * @returns {string} Time formatted into a string like '10:19 AM'.
 */
export function timestampToTimeFormatted(msTimestamp, timezone = 'America/New_York') {
  const date = new Date(msTimestamp);
  const formatOptions = {
    hour: 'numeric',
    minute: '2-digit',
    timeZone: timezone
  };
  return date.toLocaleTimeString('en-US', formatOptions);
}

/**
 * Format a timestamp (in milliseconds) into a pretty string with just the date.
 *
 * @param {int} msTimestamp
 * @param {string} timezone
 * @returns {string} Time formatted into a string like 'Monday, January 19, 1970'.
 */
export function timestampToDateFormatted(msTimestamp, timezone='America/New_York') {
  const date = new Date(msTimestamp);
  const formatOptions = {
    weekday: 'long',
    year: 'numeric',
    month: 'long',
    day: 'numeric',
    timeZone: timezone
  };
  return date.toLocaleDateString('en-US', formatOptions);
}

/**
 * Format a timestamp (in milliseconds) into a pretty string.
 *
 * @param {int} msTimestamp
 * @param {string} timezone
 * @returns {string} Time formatted into a string like
 * 'Monday, January 19, 1970, 02:48 AM'
 */
export function timestampToFormatted(msTimestamp, timezone = 'America/New_York') {
  let date = new Date(msTimestamp);
  let formatOptions = {
    weekday: 'long',
    year: 'numeric',
    month: 'long',
    day: 'numeric',
    hour: 'numeric',
    minute: '2-digit',
    timeZone: timezone
  };
  return date.toLocaleString('en-US', formatOptions);
}

/**
 * Returns all the time zones in a country (in pretty format).
 *
 * @param {string} countryName The name of the country for which to get the time zones.
 * @returns The list of time zones in the provided country.
 */
export function timezonesForCountry(countryName) {
  let zones;
  try {
    const countryCode = countryCodes[countryName];
    zones = moment.tz.zonesForCountry(countryCode);
  } catch (e) {
    zones = moment.tz.names(); // List of all timezones.
  }
  return zones.map(e => {
    return e.replace(/[_]/g, ' ');
  });
}

/**
 * Get a date in 'YYYY-MM-DD' format.
 *
 * @param {string} msTimestamp Timestamp, in milliseconds since epoch.
 * @param {string} timezone The timezone which the string should be returned in.
 * @returns {string} The date in 'YYYY-MM-DD' format.
 */
export function getDateBarebones(msTimestamp, timezone=null) {
  if (timezone === null) {
    return moment.tz(parseFloat(msTimestamp), '').format('YYYY-MM-DD');
  }
  return moment.tz(parseFloat(msTimestamp), timezone).format('YYYY-MM-DD');
}

/**
 * Get a time in 24-hour ('HH:mm') format.
 *
 * @param {string} msTimestamp Timestamp, in milliseconds since epoch.
 * @param {string} timezone The timezone which the string should be returned in.
 * @returns {string} The time in 24-hour (HH:mm) format.
 */
export function get24hTime(msTimestamp, timezone=null) {
  if (timezone === null) {
    return moment.tz(parseFloat(msTimestamp), '').format('HH:mm');
  }
  return moment.tz(parseFloat(msTimestamp), timezone).format('HH:mm');
}

/**
 * Get a Firebase Timestamp object for time.
 *
 * @param {string} time The time in 'HH:mm' format.
 * @param {string} date The date in 'YYYY-MM-DD' format.
 * @param {string} tz The timezone in which the date takes place.
 * @returns {firestore.Timestamp} Firestore timestamp object at the same time.
 */
export function getFirebaseTime(time, date, tz) {
  const mtzDate = moment.tz(time + " " + date, "HH:mm YYYY-MM-DD", tz);
  return new firestore.Timestamp(mtzDate.valueOf() / 1000, 0);
}<|endoftext|>


<commit_before>import React, {PropTypes} from "react";
import {connect} from "react-redux";
import BuildDetails from "./BuildDetails.es6";
import {toggleBuildDetails as toggleAction} from "actions/BuildDetailActions.es6";
import Moment, {now} from "moment";

import {FormattedDuration} from "./DateAndTime.es6";

const SUCCESS_ICON = "fa-check";
const FAILURE_ICON = "fa-times";
const RUNNING_ICON = "fa-cog";

const icon = (buildState) => {
    switch (buildState) {
        case "success" :
            return SUCCESS_ICON;
        case "failed" :
            return FAILURE_ICON;
        case "running" :
            return RUNNING_ICON;
        default :
            return "";
    }
};

export const renderSummary = (properties) => {
    const {buildId, buildNumber, startTime, state, toggleBuildDetails, open} = properties;
    let classesForState = "row buildSummary " + state;
    if (open) {
        classesForState += " open";
    }
    const iconClassName = "fa " + icon(state);
    let {endTime} = properties;
    if (!endTime) {
        endTime = now();
    }

    const timeToNow = Moment(startTime).diff(Moment(now()));

    const startMoment = Moment.duration(timeToNow).humanize("minutes");
    const duration = Moment.duration(Moment(endTime).diff(Moment(startTime))).seconds();

    return

Build #{buildNumber}

Started: {startMoment}

Duration: <FormattedDuration seconds={duration}/>

        <BuildDetails buildId={buildId}/>

;

};

export class BuildSummary extends React.Component {
    constructor(props) {
        super(props);
    }

    render() {
        if (this.props.state === "running") {
            setTimeout(() => this.forceUpdate(), 1000);
        }

        return renderSummary(this.props);
    }
}

BuildSummary.propTypes = {
    buildId: PropTypes.number.isRequired,
    buildNumber: PropTypes.number.isRequired,
    state: PropTypes.string.isRequired,
    startTime: PropTypes.string.isRequired,
    toggleBuildDetails: PropTypes.func.isRequired,
    endTime: PropTypes.string,
    open: PropTypes.boolean
};

export const mapStateToProps = (state, props) => {
    const {buildId, buildNumber, startTime, endTime} = props.build;
    const buildState = props.build.state;
    const open = state.openedBuilds[buildId] || false;

    return {
        buildId: buildId,
        buildNumber: buildNumber,
        state: buildState,
        startTime: startTime,
        endTime: endTime,
        open: open
    };
};

export const mapDispatchToProps = (dispatch, ownProps) => {
    return {
        toggleBuildDetails: () => {
            dispatch(toggleAction(ownProps.build.buildId));
        }
    };
};

export default connect(mapStateToProps, mapDispatchToProps)(BuildSummary);
<commit_msg>refactoring BuildSummary<commit_after>import React, {PropTypes} from "react";
import {connect} from "react-redux";
import BuildDetails from "./BuildDetails.es6";
import {toggleBuildDetails as toggleAction} from "actions/BuildDetailActions.es6";
import Moment, {now} from "moment";

import {FormattedDuration} from "./DateAndTime.es6";

const SUCCESS_ICON = "fa-check";
const FAILURE_ICON = "fa-times";
const RUNNING_ICON = "fa-cog";

const icon = (buildState) => {
    switch (buildState) {
        case "success" :
            return SUCCESS_ICON;
        case "failed" :
            return FAILURE_ICON;
        case "running" :
            return RUNNING_ICON;
        default :
            return "";
    }
};

export const renderSummary = (properties) => {
    const {buildId, buildNumber, startTime, state, toggleBuildDetails, open} = properties;
    let classesForState = "row buildSummary " + state;
    if (open) {
        classesForState += " open";
    }
    const iconClassName = "fa " + icon(state);
    let {endTime} = properties;
    if (!endTime) {
        endTime = now();
    }

    const timeToNow = Moment(startTime).diff(Moment(now()));

    const startMoment = Moment.duration(timeToNow).humanize("minutes");
    const duration = Moment.duration(Moment(endTime).diff(Moment(startTime))).seconds();

    return

Build #{buildNumber}

Started: {startMoment}

Duration: <FormattedDuration seconds={duration}/>

        <BuildDetails buildId={buildId}/>

;

};

export class BuildSummary extends React.Component {
    constructor(props) {
        super(props);
    }

    render() {
        if (this.props.state === "running") {
            setTimeout(() => this.forceUpdate(), 1000);
        }

        return renderSummary(this.props);
    }
}

BuildSummary.propTypes = {
    buildId: PropTypes.number.isRequired,
    buildNumber: PropTypes.number.isRequired,
    state: PropTypes.string.isRequired,
    startTime: PropTypes.string.isRequired,
    toggleBuildDetails: PropTypes.func.isRequired,
    endTime: PropTypes.string,
    open: PropTypes.bool
};

export const mapStateToProps = (state, props) => {
    const {buildId, buildNumber, startTime, endTime} = props.build;
    const buildState = props.build.state;
    const open = state.openedBuilds[buildId] || false;

    return {
        buildId: buildId,
        buildNumber: buildNumber,
        state: buildState,
        startTime: startTime,
        endTime: endTime,
        open: open
    };
};

export const mapDispatchToProps = (dispatch, ownProps) => {
    return {
        toggleBuildDetails: () => {
            dispatch(toggleAction(ownProps.build.buildId));
        }
    };
};

export default connect(mapStateToProps, mapDispatchToProps)(BuildSummary);
<|endoftext|>


<commit_before>import _ from "underscore";
import inflection from 'inflection';
import MetabaseUtils from "metabase/lib/utils";

const mb_settings = _.clone(window.MetabaseBootstrap);

const settingListeners = {};

// provides access to Metabase application settings
const MetabaseSettings = {

    get: function(propName, defaultValue = null) {
        return mb_settings[propName] !== undefined ? mb_settings[propName] : defaultValue;
    },

    set: function(key, value) {
        if (mb_settings[key] !== value) {
            mb_settings[key] = value;
            if (settingListeners[key]) {
                for (const listener of settingListeners[key]) {
                    setTimeout(() => listener(value));
                }
            }
        }
    },

    setAll: function(settings) {
        for (const key in settings) {
            MetabaseSettings.set(key, settings[key]);
        }
    },

    // these are all special accessors which provide a lookup of a property plus some additional help
    adminEmail: function() {
        return mb_settings.admin_email;
    },

    isEmailConfigured: function() {
        return mb_settings.email_configured;
    },

    isTrackingEnabled: function() {
        return mb_settings.anon_tracking_enabled || false;
    },

    hasSetupToken: function() {
        return (mb_settings.setup_token !== undefined && mb_settings.setup_token !== null);
    },

    ssoEnabled: function() {
        return mb_settings.google_auth_client_id != null;
    },

    newVersionAvailable: function(settings) {
        let versionInfo = _.findWhere(settings, {key: "version-info"}),
            currentVersion = MetabaseSettings.get("version").tag;

        if (versionInfo) versionInfo = versionInfo.value;

        return (versionInfo && currentVersion !== versionInfo.latest.version);
    },

    passwordComplexity: function(capitalize) {
        const complexity = this.get('password_complexity');

        const clauseDescription = function(clause) {
            switch (clause) {
                case "lower": return "lower case letter";
                case "upper": return "upper case letter";
                case "digit": return "number";
                case "special": return "special character";
            }
        };

        let description = (capitalize === false) ? "must be "+complexity.total+" characters long" : "Must be "+complexity.total+" characters long",
            clauses = [];

        ["lower", "upper", "digit", "special"].forEach(function(clause) {
            if (clause in complexity) {
                let desc = (complexity[clause] > 1) ? inflection.pluralize(clauseDescription(clause)) : clauseDescription(clause);
                clauses.push(MetabaseUtils.numberToWord(complexity[clause])+" "+desc);
            }
        });

        if (clauses.length > 0) {
            return description+" and include "+clauses.join(", ");
        } else {
            return description;
        }
    },

    on: function(setting, callback) {
        settingListeners[setting] = settingListeners[setting] || [];
        settingListeners[setting].push(callback);
    }
}

export default MetabaseSettings;
<commit_msg>Fix updates setting item badge<commit_after>import _ from "underscore";
import inflection from 'inflection';
import MetabaseUtils from "metabase/lib/utils";

const mb_settings = _.clone(window.MetabaseBootstrap);

const settingListeners = {};

// provides access to Metabase application settings
const MetabaseSettings = {

    get: function(propName, defaultValue = null) {
        return mb_settings[propName] !== undefined ? mb_settings[propName] : defaultValue;
    },

    set: function(key, value) {
        if (mb_settings[key] !== value) {
            mb_settings[key] = value;
            if (settingListeners[key]) {
                for (const listener of settingListeners[key]) {
                    setTimeout(() => listener(value));
                }
            }
        }
    },

    setAll: function(settings) {
        for (const key in settings) {
            MetabaseSettings.set(key, settings[key]);
        }
    },

    // these are all special accessors which provide a lookup of a property plus some additional help
    adminEmail: function() {
        return mb_settings.admin_email;
    },

    isEmailConfigured: function() {
        return mb_settings.email_configured;
    },

    isTrackingEnabled: function() {
        return mb_settings.anon_tracking_enabled || false;
    },

    hasSetupToken: function() {
        return (mb_settings.setup_token !== undefined && mb_settings.setup_token !== null);
    },

    ssoEnabled: function() {
        return mb_settings.google_auth_client_id != null;
    },

    newVersionAvailable: function(settings) {
        let versionInfo = _.findWhere(settings, {key: "version-info"}),
            currentVersion = MetabaseSettings.get("version").tag;

        if (versionInfo) versionInfo = versionInfo.value;

        return (versionInfo && MetabaseUtils.compareVersions(currentVersion, versionInfo.latest.version) < 0);
    },

    passwordComplexity: function(capitalize) {
        const complexity = this.get('password_complexity');

        const clauseDescription = function(clause) {
            switch (clause) {
                case "lower": return "lower case letter";
                case "upper": return "upper case letter";
                case "digit": return "number";
                case "special": return "special character";
            }
        };

        let description = (capitalize === false) ? "must be "+complexity.total+" characters long" : "Must be "+complexity.total+" characters long",
            clauses = [];

        ["lower", "upper", "digit", "special"].forEach(function(clause) {
            if (clause in complexity) {
                let desc = (complexity[clause] > 1) ? inflection.pluralize(clauseDescription(clause)) : clauseDescription(clause);
                clauses.push(MetabaseUtils.numberToWord(complexity[clause])+" "+desc);
            }
        });

        if (clauses.length > 0) {
            return description+" and include "+clauses.join(", ");
        } else {
            return description;
        }
    },

    on: function(setting, callback) {
        settingListeners[setting] = settingListeners[setting] || [];
        settingListeners[setting].push(callback);
    }
}

export default MetabaseSettings;
<|endoftext|>


<commit_before>import api from '../../data/context-api';
import { dispatchAndThrow } from '../util';

export const RECEIVE_CONTEXT = 'RECEIVE_CONTEXT';
export const ERROR_RECEIVE_CONTEXT = 'ERROR_RECEIVE_CONTEXT';
export const REMOVE_CONTEXT = 'REMOVE_CONTEXT';
export const ERROR_REMOVING_CONTEXT = 'ERROR_REMOVING_CONTEXT';
export const ADD_CONTEXT_FIELD = 'ADD_CONTEXT_FIELD';
export const ERROR_ADD_CONTEXT_FIELD = 'ERROR_ADD_CONTEXT_FIELD';
export const UPDATE_CONTEXT_FIELD = 'UPDATE_CONTEXT_FIELD';
export const ERROR_UPDATE_CONTEXT_FIELD = 'ERROR_UPDATE_CONTEXT_FIELD';

const receiveContext = value => ({ type: RECEIVE_CONTEXT, value });
const addContextField = context => ({ type: ADD_CONTEXT_FIELD, context });
const upContextField = context => ({ type: UPDATE_CONTEXT_FIELD, context });
const createRemoveContext = context => ({ type: REMOVE_CONTEXT, context });

export function fetchContext() {
    return dispatch =>
        api
            .fetchAll()
            .then(json => {
                json.sort((a, b) => a.sortOrder - b.sortOrder);
                dispatch(receiveContext(json))
            })
            .catch(dispatchAndThrow(dispatch, ERROR_RECEIVE_CONTEXT));
}

export function removeContextField(context) {
    return dispatch =>
        api
            .remove(context)
            .then(() => dispatch(createRemoveContext(context)))
            .catch(dispatchAndThrow(dispatch, ERROR_REMOVING_CONTEXT));
}

export function createContextField(context) {
    return dispatch =>
        api
            .create(context)
            .then(() => dispatch(addContextField(context)))
            .catch(dispatchAndThrow(dispatch, ERROR_ADD_CONTEXT_FIELD));
}

export function updateContextField(context) {
    return dispatch =>
        api
            .update(context)
            .then(() => dispatch(upContextField(context)))
            .catch(dispatchAndThrow(dispatch, ERROR_UPDATE_CONTEXT_FIELD));
}

export function validateName(name) {
    return api.validate({ name });
}
<commit_msg>fix: lint errors<commit_after>import api from '../../data/context-api';
import { dispatchAndThrow } from '../util';

export const RECEIVE_CONTEXT = 'RECEIVE_CONTEXT';
export const ERROR_RECEIVE_CONTEXT = 'ERROR_RECEIVE_CONTEXT';
export const REMOVE_CONTEXT = 'REMOVE_CONTEXT';
export const ERROR_REMOVING_CONTEXT = 'ERROR_REMOVING_CONTEXT';
export const ADD_CONTEXT_FIELD = 'ADD_CONTEXT_FIELD';
export const ERROR_ADD_CONTEXT_FIELD = 'ERROR_ADD_CONTEXT_FIELD';
export const UPDATE_CONTEXT_FIELD = 'UPDATE_CONTEXT_FIELD';
export const ERROR_UPDATE_CONTEXT_FIELD = 'ERROR_UPDATE_CONTEXT_FIELD';

const receiveContext = value => ({ type: RECEIVE_CONTEXT, value });
const addContextField = context => ({ type: ADD_CONTEXT_FIELD, context });
const upContextField = context => ({ type: UPDATE_CONTEXT_FIELD, context });
const createRemoveContext = context => ({ type: REMOVE_CONTEXT, context });

export function fetchContext() {
    return dispatch =>
        api
            .fetchAll()
            .then(json => {
                json.sort((a, b) => a.sortOrder - b.sortOrder);
                dispatch(receiveContext(json));
            })
            .catch(dispatchAndThrow(dispatch, ERROR_RECEIVE_CONTEXT));
}

export function removeContextField(context) {
    return dispatch =>
        api
            .remove(context)
            .then(() => dispatch(createRemoveContext(context)))
            .catch(dispatchAndThrow(dispatch, ERROR_REMOVING_CONTEXT));
}

export function createContextField(context) {
    return dispatch =>
        api
            .create(context)
            .then(() => dispatch(addContextField(context)))
            .catch(dispatchAndThrow(dispatch, ERROR_ADD_CONTEXT_FIELD));
}

export function updateContextField(context) {
    return dispatch =>
        api
            .update(context)
            .then(() => dispatch(upContextField(context)))
            .catch(dispatchAndThrow(dispatch, ERROR_UPDATE_CONTEXT_FIELD));
}

export function validateName(name) {
    return api.validate({ name });
}
<|endoftext|>


<commit_before>Ext.define('devilry_student.view.groupinfo.GroupMetadata' ,{
    extend: 'Ext.Component',
    alias: 'widget.groupmetadata',
    cls: 'bootstrap devilry_student_groupmetadata',

    tpl: [
        '<tpl if="groupinfo.name">',
            '
', gettext('Group name'), '
',
            '
{groupinfo.name}
',
        '</tpl>',

        '
', gettext('Students'), '
',
        '
',
        '<tpl for="groupinfo.candidates">',
            '
',
                '',
                    '{user.displayname}',
                '',
            '
',
        '</tpl>',
        '
',

        '
', gettext('Grade'), '
',
        '
',
            '<tpl if="groupinfo.active_feedback == null">',
                '', gettext('No feedback'), '',
            '<tpl else>',
                '<tpl if="groupinfo.active_feedback.feedback.is_passing_grade">',
                    '', gettext('Passed') ,'',
                '<tpl else>',
                    '', gettext('Failed') ,'',
                '</tpl>',
                ' ({groupinfo.active_feedback.feedback.grade})',
                ' - ', gettext('Details'), '',
            '</tpl>',
        '
',

        '
', gettext('Status'), '
',
        '
',
            '<tpl if="groupinfo.is_open">',
                '', gettext('Open'), ' ',
                gettext('You can add more deliveries.'),
            '<tpl else>',
                '', gettext('Closed'), ' ',
                gettext('The current grade is the final grade. You can not add more deliveries unless an {examiner_term} opens the group.'),
            '</tpl>',
        '
',
        '<tpl if="groupinfo.is_open">',
            '
',
                '', gettext('Add delivery') ,'',
            '
',
        '</tpl>'
    ],

    initComponent: function() {
        Ext.apply(this, {

        });

        this.addListener({
            element: 'el',
            delegate: 'a.active_feedback_link',
            scope: this,
            click: function(e) {
                this.fireEvent('active_feedback_link_clicked');
                e.preventDefault();
            }
        });

        this.callParent(arguments);
    }
});
<commit_msg>devilry_student...groupinfo: Hide students list if only one.<commit_after>Ext.define('devilry_student.view.groupinfo.GroupMetadata' ,{
    extend: 'Ext.Component',
    alias: 'widget.groupmetadata',
    cls: 'bootstrap devilry_student_groupmetadata',

    tpl: [
        '<tpl if="groupinfo.name">',
            '
', gettext('Group name'), '
',
            '
{groupinfo.name}
',
        '</tpl>',

        '<tpl if="groupinfo.candidates.length > 1">',
            '
', gettext('Students'), '
',
            '
',
            '<tpl for="groupinfo.candidates">',
                '
',
                    '',
                        '{user.displayname}',
                    '',
                '
',
            '</tpl>',
            '
',
        '</tpl>',

        '
', gettext('Grade'), '
',
        '
',
            '<tpl if="groupinfo.active_feedback == null">',
                '', gettext('No feedback'), '',
            '<tpl else>',
                '<tpl if="groupinfo.active_feedback.feedback.is_passing_grade">',
                    '', gettext('Passed') ,'',
                '<tpl else>',
                    '', gettext('Failed') ,'',
                '</tpl>',
                ' ({groupinfo.active_feedback.feedback.grade})',
                ' - ', gettext('Details'), '',
            '</tpl>',
        '
',

        '
', gettext('Status'), '
',
        '
',
            '<tpl if="groupinfo.is_open">',
                '', gettext('Open'), ' ',
                gettext('You can add more deliveries.'),
            '<tpl else>',
                '', gettext('Closed'), ' ',
                gettext('The current grade is the final grade. You can not add more deliveries unless an {examiner_term} opens the group.'),
            '</tpl>',
        '
',
        '<tpl if="groupinfo.is_open">',
            '
',
                '', gettext('Add delivery') ,'',
            '
',
        '</tpl>'
    ],

    initComponent: function() {
        Ext.apply(this, {

        });

        this.addListener({
            element: 'el',
            delegate: 'a.active_feedback_link',
            scope: this,
            click: function(e) {
                this.fireEvent('active_feedback_link_clicked');
                e.preventDefault();
            }
        });

        this.callParent(arguments);
    }
});
<|endoftext|>


<commit_before>// from https://stackoverflow.com/a/31221374/8100990
if (!String.prototype.includes) {
    String.prototype.includes = function() {
        'use strict';
        return String.prototype.indexOf.apply(this, arguments) !== -1;
    };
}

// from https://stackoverflow.com/a/39744409/8100990
if (!Array.prototype.includes) {
    Object.defineProperty(Array.prototype, "includes", {
        enumerable: false,
        value: function(obj) {
            var newArr = this.filter(function(el) {
                return el == obj;
            });
            return newArr.length > 0;
        }
    });
}

function keygen(data) {
    console.log("Keygen: ")
    console.log(data)
    value = `
; *********************** Header - some configuration  ***********************
#NoEnv  ; Recommended for performance and compatibility with future AutoHotkey releases.
; #Warn  ; Enable warnings to assist with detecting common errors. (disabled by default)
SendMode Input  ; Recommended for new scripts due to its superior speed and reliability.
SetWorkingDir %A_ScriptDir%  ; Ensures a consistent starting directory.
setTitleMatchMode, 2 ; set title match mode to "contains"

; this code was auto generated at:
; ${document.location.toString()}

; *********************** Configured region - selected functions ************

`

    // load in data
    //console.log(data)
    for (i in data) {
        //console.log("Key " + i)
        key = ''

        //console.log(data[i])

        if (data[i]['func'] == 'KEY') {
            // hotkey
            if (data[i]['modifiers[]'].includes('CTRL')) {
                key += '^'
            }
            if (data[i]['modifiers[]'].includes('SHIFT')) {
                key += '+'
            }
            if (data[i]['modifiers[]'].includes('ALT')) {
                key += '!'
            }
            if (data[i]['modifiers[]'].includes('WIN')) {
                key += '#'
            }

            key += data[i]['skeyValue']
            key += '::'

            func = "\r\n    return";
            option = data[i]['option'];
            if (option == 'Send' || option == 'Replace') { // replace doesn't make sense for hotkey, so treat like send
                func = 'send, ' + data[i]["input"];
            } else if (option == 'ActivateOrOpen') {
                func = 'ActivateOrOpen("' + data[i]["Window"] + '", "' + data[i]["Program"] + '")';
            } else if (option == 'ActivateOrOpenChrome') {
                func = 'ActivateOrOpenChrome("' + data[i]["Window"] + '", "' + data[i]["Program"] + '")';
            } else if (option == 'Custom') {
                func = data[i]['Code']
            } else if (option == 'SendUnicodeChar') {
                func = 'SendUnicodeChar(' + data[i]['input'] + ')';
            } else if(option == 'OpenConfig') {
                func = '\r\nOpenConfig()\r\nreturn';
            }

            key += func
        } else {
            // hotstring
            key += ':*c:' + data[i]['skeyValue'] + "::"
            option = data[i]['option'];
            func = "\r\n    return";
            if (option == 'Send') { // replace doesn't make sense for hotkey, so treat like send
                func = '\r\nsend, ' + data[i]["input"] + '\r\nreturn';
            } else if (option == 'ActivateOrOpen') {
                func = '\r\nActivateOrOpen("' + data[i]["Window"] + '", "' + data[i]["Program"] + '")\r\nreturn';
            } else if (option == 'Replace') {
                func = data[i]["input"];
            } else if (option == 'ActivateOrOpenChrome') {
                func = '\r\nActivateOrOpenChrome("' + data[i]["Window"] + '", "' + data[i]["Program"] + '")\r\nreturn';
            } else if (option == 'Custom') {
                func = data[i]['Code']
            } else if (option == 'SendUnicodeChar') {
                func = '\r\nSendUnicodeChar(' + data[i]['input'] + ')\r\nreturn';
            } else if(option == 'OpenConfig') {
                func = '\r\nOpenConfig()\r\nreturn';
            }

            key += func
        }

        if ('comment' in data[i]) {
            key = ';' + data[i]['comment'] + '\r\n' + key;
        }

        value += key + "\r\n\r\n"
    }
    // append custom functions
    value += `
; *********************** Provided Functions ********************************
OpenConfig()
{
    Run, ${document.location.toString()}
}

ActivateOrOpen(window, program)
{
	; check if window exists
	if WinExist(window)
	{
		WinActivate  ; Uses the last found window.
	}
	else
	{   ; else start requested program
		 Run cmd /c "start ^"^" ^"%program%^""
		 WinWait, %window%,,5		; wait up to 5 seconds for window to exist
		 IfWinNotActive, %window%, , WinActivate, %window%
		 {
			  WinActivate  ; Uses the last found window.
		 }
	}
	return
}

ActivateOrOpenChrome(tab, url)
{
    Transform, url, Deref, "%url%" ;expand variables inside url
    Transform, tab, Deref, "%tab%" ;expand variables inside tab
    chrome := "- Google Chrome"
    found := "false"
    tabSearch := tab
    curWinNum := 0

    SetTitleMatchMode, 2
    if WinExist(Chrome)
	{
		WinGet, numOfChrome, Count, %chrome% ; Get the number of chrome windows
		WinActivateBottom, %chrome% ; Activate the least recent window
		WinWaitActive %chrome% ; Wait until the window is active

		ControlFocus, Chrome_RenderWidgetHostHWND1 ; Set the focus to tab control ???

		; Loop until all windows are tried, or until we find it
		while (curWinNum < numOfChrome and found = "false") {
			WinGetTitle, firstTabTitle, A ; The initial tab title
			title := firstTabTitle
			Loop
			{
				if(InStr(title, tabSearch)>0){
					found := "true"
					break
				}
				Send {Ctrl down}{Tab}{Ctrl up}
				Sleep, 50
				WinGetTitle, title, A  ;get active window title
				if(title = firstTabTitle){
					break
				}
			}
			WinActivateBottom, %chrome%
			curWinNum := curWinNum + 1
		}
	}

    ; If we did not find it, start it
    if(found = "false"){
        Run chrome.exe "%url%"
    }
	return
}

SendUnicodeChar(charCode)
{
	VarSetCapacity(ki, 28 * 2, 0)
	EncodeInteger(&ki + 0, 1)
	EncodeInteger(&ki + 6, charCode)
	EncodeInteger(&ki + 8, 4)
	EncodeInteger(&ki +28, 1)
	EncodeInteger(&ki +34, charCode)
	EncodeInteger(&ki +36, 4|2)

	DllCall("SendInput", "UInt", 2, "UInt", &ki, "Int", 28)
}

EncodeInteger(ref, val)
{
	DllCall("ntdll\\RtlFillMemoryUlong", "Uint", ref, "Uint", 4, "Uint", val)
}
`

    //return script
    return value
}<commit_msg>making OpenConfig format url like string<commit_after>// from https://stackoverflow.com/a/31221374/8100990
if (!String.prototype.includes) {
    String.prototype.includes = function() {
        'use strict';
        return String.prototype.indexOf.apply(this, arguments) !== -1;
    };
}

// from https://stackoverflow.com/a/39744409/8100990
if (!Array.prototype.includes) {
    Object.defineProperty(Array.prototype, "includes", {
        enumerable: false,
        value: function(obj) {
            var newArr = this.filter(function(el) {
                return el == obj;
            });
            return newArr.length > 0;
        }
    });
}

function keygen(data) {
    console.log("Keygen: ")
    console.log(data)
    value = `
; *********************** Header - some configuration  ***********************
#NoEnv  ; Recommended for performance and compatibility with future AutoHotkey releases.
; #Warn  ; Enable warnings to assist with detecting common errors. (disabled by default)
SendMode Input  ; Recommended for new scripts due to its superior speed and reliability.
SetWorkingDir %A_ScriptDir%  ; Ensures a consistent starting directory.
setTitleMatchMode, 2 ; set title match mode to "contains"

; this code was auto generated at:
; ${document.location.toString()}

; *********************** Configured region - selected functions ************

`

    // load in data
    //console.log(data)
    for (i in data) {
        //console.log("Key " + i)
        key = ''

        //console.log(data[i])

        if (data[i]['func'] == 'KEY') {
            // hotkey
            if (data[i]['modifiers[]'].includes('CTRL')) {
                key += '^'
            }
            if (data[i]['modifiers[]'].includes('SHIFT')) {
                key += '+'
            }
            if (data[i]['modifiers[]'].includes('ALT')) {
                key += '!'
            }
            if (data[i]['modifiers[]'].includes('WIN')) {
                key += '#'
            }

            key += data[i]['skeyValue']
            key += '::'

            func = "\r\n    return";
            option = data[i]['option'];
            if (option == 'Send' || option == 'Replace') { // replace doesn't make sense for hotkey, so treat like send
                func = 'send, ' + data[i]["input"];
            } else if (option == 'ActivateOrOpen') {
                func = 'ActivateOrOpen("' + data[i]["Window"] + '", "' + data[i]["Program"] + '")';
            } else if (option == 'ActivateOrOpenChrome') {
                func = 'ActivateOrOpenChrome("' + data[i]["Window"] + '", "' + data[i]["Program"] + '")';
            } else if (option == 'Custom') {
                func = data[i]['Code']
            } else if (option == 'SendUnicodeChar') {
                func = 'SendUnicodeChar(' + data[i]['input'] + ')';
            } else if(option == 'OpenConfig') {
                func = '\r\nOpenConfig()\r\nreturn';
            }

            key += func
        } else {
            // hotstring
            key += ':*c:' + data[i]['skeyValue'] + "::"
            option = data[i]['option'];
            func = "\r\n    return";
            if (option == 'Send') { // replace doesn't make sense for hotkey, so treat like send
                func = '\r\nsend, ' + data[i]["input"] + '\r\nreturn';
            } else if (option == 'ActivateOrOpen') {
                func = '\r\nActivateOrOpen("' + data[i]["Window"] + '", "' + data[i]["Program"] + '")\r\nreturn';
            } else if (option == 'Replace') {
                func = data[i]["input"];
            } else if (option == 'ActivateOrOpenChrome') {
                func = '\r\nActivateOrOpenChrome("' + data[i]["Window"] + '", "' + data[i]["Program"] + '")\r\nreturn';
            } else if (option == 'Custom') {
                func = data[i]['Code']
            } else if (option == 'SendUnicodeChar') {
                func = '\r\nSendUnicodeChar(' + data[i]['input'] + ')\r\nreturn';
            } else if(option == 'OpenConfig') {
                func = '\r\nOpenConfig()\r\nreturn';
            }

            key += func
        }

        if ('comment' in data[i]) {
            key = ';' + data[i]['comment'] + '\r\n' + key;
        }

        value += key + "\r\n\r\n"
    }
    // append custom functions
    value += `
; *********************** Provided Functions ********************************
OpenConfig()
{
    Run, "${document.location.toString().replace(/\%/g, '^%')}"
}

ActivateOrOpen(window, program)
{
	; check if window exists
	if WinExist(window)
	{
		WinActivate  ; Uses the last found window.
	}
	else
	{   ; else start requested program
		 Run cmd /c "start ^"^" ^"%program%^""
		 WinWait, %window%,,5		; wait up to 5 seconds for window to exist
		 IfWinNotActive, %window%, , WinActivate, %window%
		 {
			  WinActivate  ; Uses the last found window.
		 }
	}
	return
}

ActivateOrOpenChrome(tab, url)
{
    Transform, url, Deref, "%url%" ;expand variables inside url
    Transform, tab, Deref, "%tab%" ;expand variables inside tab
    chrome := "- Google Chrome"
    found := "false"
    tabSearch := tab
    curWinNum := 0

    SetTitleMatchMode, 2
    if WinExist(Chrome)
	{
		WinGet, numOfChrome, Count, %chrome% ; Get the number of chrome windows
		WinActivateBottom, %chrome% ; Activate the least recent window
		WinWaitActive %chrome% ; Wait until the window is active

		ControlFocus, Chrome_RenderWidgetHostHWND1 ; Set the focus to tab control ???

		; Loop until all windows are tried, or until we find it
		while (curWinNum < numOfChrome and found = "false") {
			WinGetTitle, firstTabTitle, A ; The initial tab title
			title := firstTabTitle
			Loop
			{
				if(InStr(title, tabSearch)>0){
					found := "true"
					break
				}
				Send {Ctrl down}{Tab}{Ctrl up}
				Sleep, 50
				WinGetTitle, title, A  ;get active window title
				if(title = firstTabTitle){
					break
				}
			}
			WinActivateBottom, %chrome%
			curWinNum := curWinNum + 1
		}
	}

    ; If we did not find it, start it
    if(found = "false"){
        Run chrome.exe "%url%"
    }
	return
}

SendUnicodeChar(charCode)
{
	VarSetCapacity(ki, 28 * 2, 0)
	EncodeInteger(&ki + 0, 1)
	EncodeInteger(&ki + 6, charCode)
	EncodeInteger(&ki + 8, 4)
	EncodeInteger(&ki +28, 1)
	EncodeInteger(&ki +34, charCode)
	EncodeInteger(&ki +36, 4|2)

	DllCall("SendInput", "UInt", 2, "UInt", &ki, "Int", 28)
}

EncodeInteger(ref, val)
{
	DllCall("ntdll\\RtlFillMemoryUlong", "Uint", ref, "Uint", 4, "Uint", val)
}
`

    //return script
    return value
}<|endoftext|>


<commit_before>/*!
 * Name: jQuery Clickable
 * Description: Designed to make other elements clickable/tapable/pressable based on a sibling or child $links!
 * Version: 0.9
 *
 * Author: Matt Lawson
 * License: licenced under MIT - http://opensource.org/licenses/mit-license.php
 */

// the semi-colon before function invocation is a safety net against concatenated
// scripts and/or other plugins which may not be closed properly.
;(function ($, window, document, undefined) {

  'use strict';

  // undefined is used here as the undefined global variable in ECMAScript 3 is
  // mutable (ie. it can be changed by someone else). undefined isn't really being
  // passed in so we can ensure the value of it is truly undefined. In ES5, undefined
  // can no longer be modified.

  // window and document are passed through as local variable rather than global
  // as this (slightly) quickens the resolution process and can be more efficiently
  // minified (especially when both are regularly referenced in your plugin).

  // Create the defaults once
  var pluginName = 'jqueryClickable',
      defaults = {
        clickable_child: null,                        // Used to make a child element clickable of which the script was executed
        select_link: null,                            // Used to specify which link to use within the clickable box should more than one link be found
        clickable_class: 'clickable',                 // The class that gets added to elements which have been successfully initialized
        hover_class: 'clickable-hover',               // The class that gets added to the onHover state of clickable elements
        focus_class: 'clickable-focus',               // The class that gets added to the onFocus state of clickable elements
        url_prefixes: ['http://', 'https://', 'www.'] // An array of different prefixes that can appear before each link
      };

  // The actual plugin constructor
  function Plugin(element, options) {
    this.element = element;
    // jQuery has an extend method which merges the contents of two or
    // more objects, storing the result in the first object. The first object
    // is generally empty as we don't want to alter the default options for
    // future instances of the plugin
    this.options = $.extend({}, defaults, options);
    this._defaults = defaults;
    this._name = pluginName;
    this.init();
  }

  Plugin.prototype = {
    init: function () {
      // Place initialization logic here
      // You already have access to the DOM element and
      // the options via the instance, e.g. this.element
      // and this.options
      // you can add more functions like the one below and
      // call them like so: this.yourOtherFunction(this.element, this.options).
      // this.options.propertyName

      var $self = this;

      return $(this.element).each(function() {
        // If the box contains more than one link check to see if all the $links in the box are
        // identical. If they are use one to make the box clickable use plugin defaults e.g.: $self.options.clickable_class
        var $link = null,
            attrs = {};

        if( $(this).find('a[href]').length > 1 ) {
          var $links = $(this).find('a[href]');

          if( $self.identicalHref($self, $links) ) {
            attrs = $self.getAttrs($links.get(0));
          } else {
            // If a fallback function for selecting a link is provided
            if( $self.options.select_link ) {
              $link = $self.options.select_link;

              //if link element is returned get the first link
              if( $link ) {
                attrs = $self.getAttrs($links.get(0));
              }
            }
          }
        } else {
          // If the box contains only one link, use it
          if( $(this).find('a[href]').length === 1 ) {
            attrs = $self.getAttrs($(this).find('a[href]'));
          }
        }

        if( attrs.href !== undefined ) {
          // If the element has a valid link to use, Bind the link and add a clickable_class
          $(this).addClass($self.options.clickable_class);
          $self.bindLink($self, $(this), attrs);
        }

      });
    },

    /*
     * Check that all link href attributes in an array are identical
     * @return boolean: true if all of the $links are identical, otherwise false
     */
    identicalHref: function ($self, $links) {
      var first = this.getTrimmedUrl($self, $links.get(0)),
          curr = null,
          is_identical = true;

      $.each($links, function() {
        curr = $self.getTrimmedUrl($self, $(this));
        if( curr !== first ) {
          is_identical = false;
        }
      });

      return is_identical;
    },

    /*
     * Function for extracting the full url from a link and trimming it
     * @return string: A trimmed url (prefixes removed)
     */
    getTrimmedUrl: function ($self, link) {
      return this.trimUrl($self, $(link).attr('href'));
    },

    /*
     * Function for removing standard URL prefixes specified in defaults
     * @return string: The trimmed url (prefixes removed)
     */
    trimUrl: function ($self, url) {
      url = $.trim(url);

      if( $self.options.url_prefixes ) {
        for( var prefix in $self.options.url_prefixes ) {
          if( url.indexOf($self.options.url_prefixes[prefix]) === 0 ) {
            url = url.substring($self.options.url_prefixes[prefix].length);
          }
        }
      }

      return url;
    },

    /*
     * Binds click handler to the target specified in defaults if not null,
     * otherwise apply the click handler to the clickable box
     */
    bindLink: function ($self, box, link_attrs) {
      var $bind_target = ($self.options.clickable_child && $(box).find($self.options.clickable_child).length > 0) ? $(box).find($self.options.clickable_child) : box;

      if( link_attrs.target === '_blank' ) {
        $bind_target.on('click', function(e) {
          e.preventDefault();
          window.open(link_attrs.href); // removed ,link_attrs.text to fix IE issue
        });
      } else {
        $bind_target.on('click', function(e) {
          e.preventDefault();
          window.location.href = link_attrs.href;
        });
      }

      $bind_target.hover(function() {
        $($bind_target).addClass($self.options.hover_class);
      },function() {
        $bind_target.removeClass($self.options.hover_class);
      });

      $bind_target.find('a').focus(function() {
        $bind_target.addClass($self.options.focus_class);
      });

      $bind_target.find('a').blur(function() {
        $bind_target.removeClass($self.options.focus_class);
      });
    },

    /*
     * Function used to get attributes from a link
     * @return: Object containing the $links attributes
     */
    getAttrs: function (link_element) {
      var href = ($(link_element).attr('href') !== "") ? $(link_element).attr('href') : null,
          target = ($(link_element).attr('target') !== "") ? $(link_element).attr('target') : null,
          text = $(link_element).text();

      return {href : href, target : target, text : text};
    }
  };

  // A really lightweight plugin wrapper around the constructor,
  // preventing against multiple instantiations
  $.fn[pluginName] = function (options) {
    return this.each(function () {
      if (!$.data(this, "plugin_" + pluginName)) {
        $.data(this, "plugin_" + pluginName, new Plugin(this, options));
      }
    });
  };

})(jQuery, window, document);
<commit_msg>Fixed find and replace typo<commit_after>/*!
 * Name: jQuery Clickable
 * Description: Designed to make other elements clickable/tapable/pressable based on a sibling or child links!
 * Version: 0.9
 *
 * Author: Matt Lawson
 * License: licenced under MIT - http://opensource.org/licenses/mit-license.php
 */

// the semi-colon before function invocation is a safety net against concatenated
// scripts and/or other plugins which may not be closed properly.
;(function ($, window, document, undefined) {

  'use strict';

  // undefined is used here as the undefined global variable in ECMAScript 3 is
  // mutable (ie. it can be changed by someone else). undefined isn't really being
  // passed in so we can ensure the value of it is truly undefined. In ES5, undefined
  // can no longer be modified.

  // window and document are passed through as local variable rather than global
  // as this (slightly) quickens the resolution process and can be more efficiently
  // minified (especially when both are regularly referenced in your plugin).

  // Create the defaults once
  var pluginName = 'jqueryClickable',
      defaults = {
        clickable_child: null,                        // Used to make a child element clickable of which the script was executed
        select_link: null,                            // Used to specify which link to use within the clickable box should more than one link be found
        clickable_class: 'clickable',                 // The class that gets added to elements which have been successfully initialized
        hover_class: 'clickable-hover',               // The class that gets added to the onHover state of clickable elements
        focus_class: 'clickable-focus',               // The class that gets added to the onFocus state of clickable elements
        url_prefixes: ['http://', 'https://', 'www.'] // An array of different prefixes that can appear before each link
      };

  // The actual plugin constructor
  function Plugin(element, options) {
    this.element = element;
    // jQuery has an extend method which merges the contents of two or
    // more objects, storing the result in the first object. The first object
    // is generally empty as we don't want to alter the default options for
    // future instances of the plugin
    this.options = $.extend({}, defaults, options);
    this._defaults = defaults;
    this._name = pluginName;
    this.init();
  }

  Plugin.prototype = {
    init: function () {
      // Place initialization logic here
      // You already have access to the DOM element and
      // the options via the instance, e.g. this.element
      // and this.options
      // you can add more functions like the one below and
      // call them like so: this.yourOtherFunction(this.element, this.options).
      // this.options.propertyName

      var $self = this;

      return $(this.element).each(function() {
        // If the box contains more than one link check to see if all the $links in the box are
        // identical. If they are use one to make the box clickable use plugin defaults e.g.: $self.options.clickable_class
        var $link = null,
            attrs = {};

        if( $(this).find('a[href]').length > 1 ) {
          var $links = $(this).find('a[href]');

          if( $self.identicalHref($self, $links) ) {
            attrs = $self.getAttrs($links.get(0));
          } else {
            // If a fallback function for selecting a link is provided
            if( $self.options.select_link ) {
              $link = $self.options.select_link;

              //if link element is returned get the first link
              if( $link ) {
                attrs = $self.getAttrs($links.get(0));
              }
            }
          }
        } else {
          // If the box contains only one link, use it
          if( $(this).find('a[href]').length === 1 ) {
            attrs = $self.getAttrs($(this).find('a[href]'));
          }
        }

        if( attrs.href !== undefined ) {
          // If the element has a valid link to use, Bind the link and add a clickable_class
          $(this).addClass($self.options.clickable_class);
          $self.bindLink($self, $(this), attrs);
        }

      });
    },

    /*
     * Check that all link href attributes in an array are identical
     * @return boolean: true if all of the $links are identical, otherwise false
     */
    identicalHref: function ($self, $links) {
      var first = this.getTrimmedUrl($self, $links.get(0)),
          curr = null,
          is_identical = true;

      $.each($links, function() {
        curr = $self.getTrimmedUrl($self, $(this));
        if( curr !== first ) {
          is_identical = false;
        }
      });

      return is_identical;
    },

    /*
     * Function for extracting the full url from a link and trimming it
     * @return string: A trimmed url (prefixes removed)
     */
    getTrimmedUrl: function ($self, link) {
      return this.trimUrl($self, $(link).attr('href'));
    },

    /*
     * Function for removing standard URL prefixes specified in defaults
     * @return string: The trimmed url (prefixes removed)
     */
    trimUrl: function ($self, url) {
      url = $.trim(url);

      if( $self.options.url_prefixes ) {
        for( var prefix in $self.options.url_prefixes ) {
          if( url.indexOf($self.options.url_prefixes[prefix]) === 0 ) {
            url = url.substring($self.options.url_prefixes[prefix].length);
          }
        }
      }

      return url;
    },

    /*
     * Binds click handler to the target specified in defaults if not null,
     * otherwise apply the click handler to the clickable box
     */
    bindLink: function ($self, box, link_attrs) {
      var $bind_target = ($self.options.clickable_child && $(box).find($self.options.clickable_child).length > 0) ? $(box).find($self.options.clickable_child) : box;

      if( link_attrs.target === '_blank' ) {
        $bind_target.on('click', function(e) {
          e.preventDefault();
          window.open(link_attrs.href); // removed ,link_attrs.text to fix IE issue
        });
      } else {
        $bind_target.on('click', function(e) {
          e.preventDefault();
          window.location.href = link_attrs.href;
        });
      }

      $bind_target.hover(function() {
        $($bind_target).addClass($self.options.hover_class);
      },function() {
        $bind_target.removeClass($self.options.hover_class);
      });

      $bind_target.find('a').focus(function() {
        $bind_target.addClass($self.options.focus_class);
      });

      $bind_target.find('a').blur(function() {
        $bind_target.removeClass($self.options.focus_class);
      });
    },

    /*
     * Function used to get attributes from a link
     * @return: Object containing the $links attributes
     */
    getAttrs: function (link_element) {
      var href = ($(link_element).attr('href') !== "") ? $(link_element).attr('href') : null,
          target = ($(link_element).attr('target') !== "") ? $(link_element).attr('target') : null,
          text = $(link_element).text();

      return {href : href, target : target, text : text};
    }
  };

  // A really lightweight plugin wrapper around the constructor,
  // preventing against multiple instantiations
  $.fn[pluginName] = function (options) {
    return this.each(function () {
      if (!$.data(this, "plugin_" + pluginName)) {
        $.data(this, "plugin_" + pluginName, new Plugin(this, options));
      }
    });
  };

})(jQuery, window, document);
<|endoftext|>