<commit_before>import React from 'react';
import { injectIntl, FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { Grid, Row, Col } from 'react-flexbox-grid/lib';
import * as d3 from 'd3';
import messages from '../../../resources/messages';
import QueryForm from './QueryForm';
import AppButton from '../../common/AppButton';
import ItemSlider from '../../common/ItemSlider';
import QueryPickerItem from './QueryPickerItem';
import { selectQuery, updateQuery, addCustomQuery, saveQuerySet, deleteQuery } from '../../../actions/explorerActions';
import { AddQueryButton } from '../../common/IconButton';
import { getPastTwoWeeksDateRange } from '../../../lib/dateUtil';
import { DEFAULT_COLLECTION_OBJECT_ARRAY } from '../../../lib/explorerUtil';
import { getUserRoles, hasPermissions, PERMISSION_LOGGED_IN } from '../../../lib/auth';

const localMessages = {
  mainTitle: { id: 'explorer.querypicker.mainTitle', defaultMessage: 'Query List' },
  intro: { id: 'explorer.querypicker.intro', defaultMessage: 'Here are all available queries' },
  addQuery: { id: 'explorer.querypicker.addQuery', defaultMessage: 'Add query' },
  querySearch: { id: 'explorer.queryBuilder.advanced', defaultMessage: 'Search' },
  searchHint: { id: 'explorer.queryBuilder.hint', defaultMessage: 'Search' },
};

const MAX_COLORS = 20;

class QueryPicker extends React.Component {
  addAQuery(newQueryObj) {
    const { addAQuery } = this.props;
    addAQuery(newQueryObj);
  }
  updateDemoQueryLabel(query, newValue) {
    // update both label and q for query
    const { updateCurrentQuery } = this.props;
    const updatedQuery = { ...query };
    updatedQuery.label = newValue;
    updatedQuery.q = newValue;
    updateCurrentQuery(updatedQuery);
  }
  // called by query picker to update things like label or color
  updateQueryProperty(query, propertyName, newValue) {
    const { updateCurrentQuery } = this.props;
    const updatedQuery = { ...query };
    updatedQuery[propertyName] = newValue;
    // now update it in the store
    updateCurrentQuery(updatedQuery);
  }

  updateQuery(newInfo) {
    const { updateCurrentQuery, selected } = this.props;
    const updateObject = selected;
    if (newInfo.length) { // assume it's an array, and either sources or collections
      if (newInfo[0].type === 'source' || newInfo[0].media_id !== undefined) {
        updateObject.sources = newInfo; // replace
      } else if (newInfo[0].type === 'collection' || newInfo[0].tags_id !== undefined) {
        updateObject.collections = newInfo;
      }
    } else {
      const fieldName = newInfo.target ? newInfo.target.name : newInfo.name;
      const fieldValue = newInfo.target ? newInfo.target.value : newInfo.value;
      updateObject[fieldName] = fieldValue;
      // TODO check the logic of this. if demo mode and the user changes the q, label should be equivalent?
      if (fieldName === 'q') {
        updateObject.label = fieldValue;
      }
    }
    updateCurrentQuery(updateObject);
  }

  handleSaveQuerySet() {
    const { selected, saveThisQuerySet } = this.props;
    saveThisQuerySet(selected);
  }
  handleOpenStub() {
    return this.props;
  }
  isDeletable() {
    const { queries } = this.props;
    const unDeletedQueries = queries.filter(q => !q.deleted);
    return unDeletedQueries.length >= 2; // because we always have an empty query in the query array
  }
  handleDeleteAndSelectQuery(query) {
    const { queries, handleDeleteQuery } = this.props;
    const queryIndex = queries.findIndex(q => q.index !== null && q.index === query.index);
    const replaceSelectionWithWhichQuery = queryIndex === 0 ? 1 : 0; // replace with the query, not the position
    if (this.isDeletable()) {
      handleDeleteQuery(query, queries[replaceSelectionWithWhichQuery]);
    }
  }

  render() {
    const { selected, queries, user, collectionsResults, sourcesResults, handleQuerySelected, isEditable, handleSearch } = this.props;
    const { formatMessage } = this.props.intl;
    let queryPickerContent; // editable if demo mode
    let queryFormContent; // hidden if demo mode
    let fixedQuerySlides;
    let canSelectMedia = false;
    const userLoggedIn = hasPermissions(getUserRoles(user), PERMISSION_LOGGED_IN);

    const unDeletedQueries = queries.filter(q => !q.deleted);
    if (unDeletedQueries && unDeletedQueries.length > 0 && selected &&
      collectionsResults && collectionsResults.length > 0 &&
      sourcesResults && sourcesResults.length >= 0) {
      fixedQuerySlides = unDeletedQueries.map((query, index) => (

          <QueryPickerItem
            user={user}
            key={index}
            query={query}
            isSelected={selected.index === query.index}
            isLabelEditable={isEditable} // if custom, true for either mode, else if logged in no
            isDeletable={() => this.isDeletable()}
            displayLabel={false}
            onQuerySelected={() => handleQuerySelected(query, query.index ? query.index : index)} // careful! unDeleted has different index
            updateQueryProperty={(propertyName, newValue) => this.updateQueryProperty(query, propertyName, newValue)}
            updateDemoQueryLabel={newValue => this.updateDemoQueryLabel(query, newValue)}
            handleSearch={handleSearch}
            handleDeleteQuery={() => this.handleDeleteAndSelectQuery(query)}
            // loadDialog={loadQueryEditDialog}
          />

      ));
      canSelectMedia = userLoggedIn;
      // provide the add Query button, load with default values when Added is clicked
      if (userLoggedIn) {
        const colorPallette = idx => d3.schemeCategory20[idx < MAX_COLORS ? idx : 0];
        const dateObj = getPastTwoWeeksDateRange();
        const newIndex = queries.length; // NOTE: all queries, including 'deleted' ones
        const genDefColor = colorPallette(newIndex);
        const defaultQuery = { index: newIndex, label: 'enter query', q: '', description: 'new', startDate: dateObj.start, endDate: dateObj.end, collections: DEFAULT_COLLECTION_OBJECT_ARRAY, sources: [], color: genDefColor, custom: true };

        const emptyQuerySlide = (

                <AddQueryButton
                  key={fixedQuerySlides.length} // this isn't working
                  tooltip={formatMessage(localMessages.addQuery)}
                  onClick={() => this.addAQuery(defaultQuery)}
                />
                <FormattedMessage {...localMessages.addQuery} />

        );

        fixedQuerySlides.push(emptyQuerySlide);
      }
      let demoSearchButtonContent; // in demo mode we need a search button outside the form (cause we can't see the form)
      if (!userLoggedIn) {
        demoSearchButtonContent = (
          <Grid>
            <Row>
              <Col lg={10} />
              <Col lg={1}>
                <AppButton
                  style={{ marginTop: 30 }}
                  type="submit"
                  label={formatMessage(messages.search)}
                  primary
                  onTouchTap={handleSearch}
                />
              </Col>
            </Row>
          </Grid>
        );
      }
      queryPickerContent = (

            <Grid>
              <ItemSlider
                title={formatMessage(localMessages.intro)}
                slides={fixedQuerySlides}
                settings={{ height: 60, dots: false, slidesToShow: 4, slidesToScroll: 1, infinite: false, arrows: fixedQuerySlides.length > 4 }}
              />
            </Grid>

          {demoSearchButtonContent}

      );
      if (userLoggedIn) {  // if logged in show full form
        queryFormContent = (
          <QueryForm
            initialValues={selected}
            selected={selected}
            form="queryForm"
            enableReinitialize
            destroyOnUnmount={false}
            buttonLabel={formatMessage(localMessages.querySearch)}
            onSave={handleSearch}
            onChange={event => this.updateQuery(event)}
            handleOpenHelp={this.handleOpenStub}
            handleSaveQuerySet={q => this.handleSaveQuerySet(q)}
            isEditable={canSelectMedia}
          />
        );
      }
      // indicate which queryPickerItem is selected -
      // const selectedWithSandCLabels = queries.find(q => q.index === selected.index);
      return (

          {queryPickerContent}
          {queryFormContent}

      );
    }
    return ('error - no queries ');
  }
}

QueryPicker.propTypes = {
  user: React.PropTypes.object.isRequired,
  queries: React.PropTypes.array,
  sourcesResults: React.PropTypes.array,
  collectionsResults: React.PropTypes.array,
  fetchStatus: React.PropTypes.string.isRequired,
  selected: React.PropTypes.object,
  intl: React.PropTypes.object.isRequired,
  handleQuerySelected: React.PropTypes.func.isRequired,
  isEditable: React.PropTypes.bool.isRequired,
  isDeletable: React.PropTypes.func,
  handleSearch: React.PropTypes.func.isRequired,
  updateCurrentQuery: React.PropTypes.func.isRequired,
  handleDeleteAndSelectQuery: React.PropTypes.func,
  handleDeleteQuery: React.PropTypes.func.isRequired,
  saveThisQuerySet: React.PropTypes.func.isRequired,
  addAQuery: React.PropTypes.func.isRequired,
  handleOpenStub: React.PropTypes.func,
};

const mapStateToProps = state => ({
  selected: state.explorer.selected,
  queries: state.explorer.queries.queries,
  sourcesResults: state.explorer.queries.sources.results ? state.explorer.queries.sources.results : null,
  collectionsResults: state.explorer.queries.collections.results ? state.explorer.queries.collections.results : null,
  fetchStatus: state.explorer.queries.collections.fetchStatus,
  user: state.user,
  // formData: formSelector(state, 'q', 'start_date', 'end_date', 'color'),
});

const mapDispatchToProps = (dispatch, ownProps) => ({
  handleQuerySelected: (query, index) => {
    const queryWithIndex = Object.assign({}, query, { index }); // if this doesn't exist...
    dispatch(selectQuery(queryWithIndex));
  },
  updateCurrentQuery: (query) => {
    if (query) {
      dispatch(updateQuery(query));
    } else {
      dispatch(updateQuery(ownProps.selected)); // this won't e right
    }
  },
  addAQuery: (query) => {
    if (query) {
      dispatch(addCustomQuery(query));
      dispatch(selectQuery(query));
    }
  },
  saveThisQuerySet: (query) => {
    if (query) { // TODO - save as JSON
      dispatch(saveQuerySet({ label: query.label, query_string: query.q }));
    }
  },
  handleDeleteQuery: (query, replacementSelectionQuery) => {
    if (query) {
      dispatch(deleteQuery(query)); // will change queries, but not URL, this is the problem
      dispatch(selectQuery(replacementSelectionQuery));
    }
  },
});

export default
  injectIntl(
    connect(mapStateToProps, mapDispatchToProps)(
      QueryPicker
    )
  );

<commit_msg>Separate label and q updates for logged in users<commit_after>import React from 'react';
import { injectIntl, FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import { Grid, Row, Col } from 'react-flexbox-grid/lib';
import * as d3 from 'd3';
import messages from '../../../resources/messages';
import QueryForm from './QueryForm';
import AppButton from '../../common/AppButton';
import ItemSlider from '../../common/ItemSlider';
import QueryPickerItem from './QueryPickerItem';
import { selectQuery, updateQuery, addCustomQuery, saveQuerySet, deleteQuery } from '../../../actions/explorerActions';
import { AddQueryButton } from '../../common/IconButton';
import { getPastTwoWeeksDateRange } from '../../../lib/dateUtil';
import { DEFAULT_COLLECTION_OBJECT_ARRAY } from '../../../lib/explorerUtil';
import { getUserRoles, hasPermissions, PERMISSION_LOGGED_IN } from '../../../lib/auth';

const localMessages = {
  mainTitle: { id: 'explorer.querypicker.mainTitle', defaultMessage: 'Query List' },
  intro: { id: 'explorer.querypicker.intro', defaultMessage: 'Here are all available queries' },
  addQuery: { id: 'explorer.querypicker.addQuery', defaultMessage: 'Add query' },
  querySearch: { id: 'explorer.queryBuilder.advanced', defaultMessage: 'Search' },
  searchHint: { id: 'explorer.queryBuilder.hint', defaultMessage: 'Search' },
};

const MAX_COLORS = 20;

class QueryPicker extends React.Component {
  addAQuery(newQueryObj) {
    const { addAQuery } = this.props;
    addAQuery(newQueryObj);
  }
  updateDemoQueryLabel(query, newValue) {
    // update both label and q for query
    const { updateCurrentQuery } = this.props;
    const updatedQuery = { ...query };
    updatedQuery.label = newValue;
    updatedQuery.q = newValue;
    updateCurrentQuery(updatedQuery);
  }
  // called by query picker to update things like label or color
  updateQueryProperty(query, propertyName, newValue) {
    const { updateCurrentQuery } = this.props;
    const updatedQuery = { ...query };
    updatedQuery[propertyName] = newValue;
    // now update it in the store
    updateCurrentQuery(updatedQuery);
  }

  updateQuery(newInfo) {
    const { updateCurrentQuery, selected } = this.props;
    const updateObject = selected;
    if (newInfo.length) { // assume it's an array, and either sources or collections
      if (newInfo[0].type === 'source' || newInfo[0].media_id !== undefined) {
        updateObject.sources = newInfo; // replace
      } else if (newInfo[0].type === 'collection' || newInfo[0].tags_id !== undefined) {
        updateObject.collections = newInfo;
      }
    } else {
      const fieldName = newInfo.target ? newInfo.target.name : newInfo.name;
      const fieldValue = newInfo.target ? newInfo.target.value : newInfo.value;
      updateObject[fieldName] = fieldValue;
    }
    updateCurrentQuery(updateObject);
  }

  handleSaveQuerySet() {
    const { selected, saveThisQuerySet } = this.props;
    saveThisQuerySet(selected);
  }
  handleOpenStub() {
    return this.props;
  }
  isDeletable() {
    const { queries } = this.props;
    const unDeletedQueries = queries.filter(q => !q.deleted);
    return unDeletedQueries.length >= 2; // because we always have an empty query in the query array
  }
  handleDeleteAndSelectQuery(query) {
    const { queries, handleDeleteQuery } = this.props;
    const queryIndex = queries.findIndex(q => q.index !== null && q.index === query.index);
    const replaceSelectionWithWhichQuery = queryIndex === 0 ? 1 : 0; // replace with the query, not the position
    if (this.isDeletable()) {
      handleDeleteQuery(query, queries[replaceSelectionWithWhichQuery]);
    }
  }

  render() {
    const { selected, queries, user, collectionsResults, sourcesResults, handleQuerySelected, isEditable, handleSearch } = this.props;
    const { formatMessage } = this.props.intl;
    let queryPickerContent; // editable if demo mode
    let queryFormContent; // hidden if demo mode
    let fixedQuerySlides;
    let canSelectMedia = false;
    const userLoggedIn = hasPermissions(getUserRoles(user), PERMISSION_LOGGED_IN);

    const unDeletedQueries = queries.filter(q => !q.deleted);
    if (unDeletedQueries && unDeletedQueries.length > 0 && selected &&
      collectionsResults && collectionsResults.length > 0 &&
      sourcesResults && sourcesResults.length >= 0) {
      fixedQuerySlides = unDeletedQueries.map((query, index) => (

          <QueryPickerItem
            user={user}
            key={index}
            query={query}
            isSelected={selected.index === query.index}
            isLabelEditable={isEditable} // if custom, true for either mode, else if logged in no
            isDeletable={() => this.isDeletable()}
            displayLabel={false}
            onQuerySelected={() => handleQuerySelected(query, query.index ? query.index : index)} // careful! unDeleted has different index
            updateQueryProperty={(propertyName, newValue) => this.updateQueryProperty(query, propertyName, newValue)}
            updateDemoQueryLabel={newValue => this.updateDemoQueryLabel(query, newValue)}
            handleSearch={handleSearch}
            handleDeleteQuery={() => this.handleDeleteAndSelectQuery(query)}
            // loadDialog={loadQueryEditDialog}
          />

      ));
      canSelectMedia = userLoggedIn;
      // provide the add Query button, load with default values when Added is clicked
      if (userLoggedIn) {
        const colorPallette = idx => d3.schemeCategory20[idx < MAX_COLORS ? idx : 0];
        const dateObj = getPastTwoWeeksDateRange();
        const newIndex = queries.length; // NOTE: all queries, including 'deleted' ones
        const genDefColor = colorPallette(newIndex);
        const defaultQuery = { index: newIndex, label: 'enter query', q: '', description: 'new', startDate: dateObj.start, endDate: dateObj.end, collections: DEFAULT_COLLECTION_OBJECT_ARRAY, sources: [], color: genDefColor, custom: true };

        const emptyQuerySlide = (

                <AddQueryButton
                  key={fixedQuerySlides.length} // this isn't working
                  tooltip={formatMessage(localMessages.addQuery)}
                  onClick={() => this.addAQuery(defaultQuery)}
                />
                <FormattedMessage {...localMessages.addQuery} />

        );

        fixedQuerySlides.push(emptyQuerySlide);
      }
      let demoSearchButtonContent; // in demo mode we need a search button outside the form (cause we can't see the form)
      if (!userLoggedIn) {
        demoSearchButtonContent = (
          <Grid>
            <Row>
              <Col lg={10} />
              <Col lg={1}>
                <AppButton
                  style={{ marginTop: 30 }}
                  type="submit"
                  label={formatMessage(messages.search)}
                  primary
                  onTouchTap={handleSearch}
                />
              </Col>
            </Row>
          </Grid>
        );
      }
      queryPickerContent = (

            <Grid>
              <ItemSlider
                title={formatMessage(localMessages.intro)}
                slides={fixedQuerySlides}
                settings={{ height: 60, dots: false, slidesToShow: 4, slidesToScroll: 1, infinite: false, arrows: fixedQuerySlides.length > 4 }}
              />
            </Grid>

          {demoSearchButtonContent}

      );
      if (userLoggedIn) {  // if logged in show full form
        queryFormContent = (
          <QueryForm
            initialValues={selected}
            selected={selected}
            form="queryForm"
            enableReinitialize
            destroyOnUnmount={false}
            buttonLabel={formatMessage(localMessages.querySearch)}
            onSave={handleSearch}
            onChange={event => this.updateQuery(event)}
            handleOpenHelp={this.handleOpenStub}
            handleSaveQuerySet={q => this.handleSaveQuerySet(q)}
            isEditable={canSelectMedia}
          />
        );
      }
      // indicate which queryPickerItem is selected -
      // const selectedWithSandCLabels = queries.find(q => q.index === selected.index);
      return (

          {queryPickerContent}
          {queryFormContent}

      );
    }
    return ('error - no queries ');
  }
}

QueryPicker.propTypes = {
  user: React.PropTypes.object.isRequired,
  queries: React.PropTypes.array,
  sourcesResults: React.PropTypes.array,
  collectionsResults: React.PropTypes.array,
  fetchStatus: React.PropTypes.string.isRequired,
  selected: React.PropTypes.object,
  intl: React.PropTypes.object.isRequired,
  handleQuerySelected: React.PropTypes.func.isRequired,
  isEditable: React.PropTypes.bool.isRequired,
  isDeletable: React.PropTypes.func,
  handleSearch: React.PropTypes.func.isRequired,
  updateCurrentQuery: React.PropTypes.func.isRequired,
  handleDeleteAndSelectQuery: React.PropTypes.func,
  handleDeleteQuery: React.PropTypes.func.isRequired,
  saveThisQuerySet: React.PropTypes.func.isRequired,
  addAQuery: React.PropTypes.func.isRequired,
  handleOpenStub: React.PropTypes.func,
};

const mapStateToProps = state => ({
  selected: state.explorer.selected,
  queries: state.explorer.queries.queries,
  sourcesResults: state.explorer.queries.sources.results ? state.explorer.queries.sources.results : null,
  collectionsResults: state.explorer.queries.collections.results ? state.explorer.queries.collections.results : null,
  fetchStatus: state.explorer.queries.collections.fetchStatus,
  user: state.user,
  // formData: formSelector(state, 'q', 'start_date', 'end_date', 'color'),
});

const mapDispatchToProps = (dispatch, ownProps) => ({
  handleQuerySelected: (query, index) => {
    const queryWithIndex = Object.assign({}, query, { index }); // if this doesn't exist...
    dispatch(selectQuery(queryWithIndex));
  },
  updateCurrentQuery: (query) => {
    if (query) {
      dispatch(updateQuery(query));
    } else {
      dispatch(updateQuery(ownProps.selected)); // this won't e right
    }
  },
  addAQuery: (query) => {
    if (query) {
      dispatch(addCustomQuery(query));
      dispatch(selectQuery(query));
    }
  },
  saveThisQuerySet: (query) => {
    if (query) { // TODO - save as JSON
      dispatch(saveQuerySet({ label: query.label, query_string: query.q }));
    }
  },
  handleDeleteQuery: (query, replacementSelectionQuery) => {
    if (query) {
      dispatch(deleteQuery(query)); // will change queries, but not URL, this is the problem
      dispatch(selectQuery(replacementSelectionQuery));
    }
  },
});

export default
  injectIntl(
    connect(mapStateToProps, mapDispatchToProps)(
      QueryPicker
    )
  );

<|endoftext|>


<commit_before>/* globals watchFunction */
/* exported FILENAME */
/** The worker for shoot-panel.html
    This is setup and directly communicated to by main.js
    The shot comes from `lib/shooter.js:ShotContext` (and the `Shot` model)
    The outgoing messages are routed to `ShotContext.panelHandlers)
    */

let err = require("./error-utils.js"),
  React = require("react"),
  shot = require("../lib/shot.js");

// For error messages:
let FILENAME = "shoot-panel.js";

let ShootPanel = React.createClass({
  onCopyClick: function (e) {
    self.port.emit("copyLink");
    let node = React.findDOMNode(this.refs.copy);
    node.textContent = node.getAttribute("data-copied-text");
    setTimeout(err.watchFunction(function () {
      node.textContent = node.getAttribute("data-normal-text");
    }), 3000);
  },

  onLinkClick: function (e) {
    self.port.emit("openLink", this.props.shot.viewUrl);
    e.preventDefault();
  },

  onKeyup: function (e) {
    console.log("onKeyup");
    let input = React.findDOMNode(this.refs.input);
    if (e.which == 13) {
      self.port.emit("addComment", input.value);
      input.value = "";
    }
  },

  render: function () {
    let clipNames = this.props.shot.clipNames();
    console.log("render panel", this.props.shot.viewUrl);
    console.log("clipnames", clipNames);
    let snippet = "";
    if (clipNames) {
      snippet = this.props.shot.getClip(clipNames[0]).image.url;
    }
    //console.log("snippet", Object.getOwnPropertyNames(snippet));

    return

          Auto-detect

          Selection

          Visible

        +

        { this.props.shot.viewUrl }
        <button className="copy" ref="copy" type="button" data-normal-text="Copy Link" data-copied-text="Copied!" onClick={ this.onCopyClick }>Copy Link</button>

{ this.props.shot.comment}

      <input className="comment-input" ref="input" type="text" placeholder="Say something" onKeyup={ this.onKeyup }/>

;
  }
});

self.port.on("shotData", err.watchFunction(function (data) {
  console.log("shotData", Object.getOwnPropertyNames(data));
  let myShot = new shot.AbstractShot(data.backend, data.id, data.shot);
  React.render(
    React.createElement(ShootPanel, {shot: myShot}), document.body);
}));

<commit_msg>Fix test for existance of any clips<commit_after>/* globals watchFunction, require */
/* exported FILENAME */
/** The worker for shoot-panel.html
    This is setup and directly communicated to by main.js
    The shot comes from `lib/shooter.js:ShotContext` (and the `Shot` model)
    The outgoing messages are routed to `ShotContext.panelHandlers)
    */

let err = require("./error-utils.js"),
  React = require("react"),
  shot = require("../lib/shot.js");

// For error messages:
let FILENAME = "shoot-panel.js";

let ShootPanel = React.createClass({
  onCopyClick: function (e) {
    self.port.emit("copyLink");
    let node = React.findDOMNode(this.refs.copy);
    node.textContent = node.getAttribute("data-copied-text");
    setTimeout(err.watchFunction(function () {
      node.textContent = node.getAttribute("data-normal-text");
    }), 3000);
  },

  onLinkClick: function (e) {
    self.port.emit("openLink", this.props.shot.viewUrl);
    e.preventDefault();
  },

  onKeyup: function (e) {
    console.log("onKeyup");
    let input = React.findDOMNode(this.refs.input);
    if (e.which == 13) {
      self.port.emit("addComment", input.value);
      input.value = "";
    }
  },

  render: function () {
    let clipNames = this.props.shot.clipNames();
    console.log("render panel", this.props.shot.viewUrl);
    console.log("clipnames", clipNames);
    let snippet = "";
    if (clipNames.length) {
      snippet = this.props.shot.getClip(clipNames[0]).image.url;
    }
    //console.log("snippet", Object.getOwnPropertyNames(snippet));

    return

          Auto-detect

          Selection

          Visible

        +

        { this.props.shot.viewUrl }
        <button className="copy" ref="copy" type="button" data-normal-text="Copy Link" data-copied-text="Copied!" onClick={ this.onCopyClick }>Copy Link</button>

{ this.props.shot.comment}

      <input className="comment-input" ref="input" type="text" placeholder="Say something" onKeyup={ this.onKeyup }/>

;
  }
});

self.port.on("shotData", err.watchFunction(function (data) {
  console.log("shotData", Object.getOwnPropertyNames(data));
  let myShot = new shot.AbstractShot(data.backend, data.id, data.shot);
  React.render(
    React.createElement(ShootPanel, {shot: myShot}), document.body);
}));
<|endoftext|>


<commit_before>const callbacks = [];

const runCallbacks = () => {
  let callback;

  while ((callback = callbacks.shift())) {
    callback();
  }
};

const registerCallback = (callback) => {
  callbacks.push(callback);
};

export const loadScript = (callback) => {
  const embedJs = '//editor.unlayer.com/embed.js?2';
  const scripts = document.querySelectorAll('script');
  let scriptLoaded = false;

  scripts.forEach((script) => {
    if (script.src.includes(embedJs)) {
      scriptLoaded = true;
    }
  });

  registerCallback(callback);

  if (!scriptLoaded) {
    const unlayerScript = document.createElement('script');
    unlayerScript.setAttribute('src', embedJs);
    unlayerScript.onload = () => {
      runCallbacks();
    };
    document.head.appendChild(unlayerScript);
  } else {
    runCallbacks();
  }
};
<commit_msg>Load script only once.<commit_after>const scriptUrl = '//editor.unlayer.com/embed.js?2';
const callbacks = [];
let loaded = false;

const isScriptInjected = () => {
  const scripts = document.querySelectorAll('script');
  let injected = false;

  scripts.forEach((script) => {
    if (script.src.includes(scriptUrl)) {
      injected = true;
    }
  });

  return injected;
};

const addCallback = (callback) => {
  callbacks.push(callback);
};

const runCallbacks = () => {
  if (loaded) {
    let callback;

    while ((callback = callbacks.shift())) {
      callback();
    }
  }
};

export const loadScript = (callback) => {
  addCallback(callback);

  if (!isScriptInjected()) {
    const embedScript = document.createElement('script');
    embedScript.setAttribute('src', scriptUrl);
    embedScript.onload = () => {
      loaded = true;
      runCallbacks();
    };
    document.head.appendChild(embedScript);
  } else {
    runCallbacks();
  }
};
<|endoftext|>


<commit_before>(function(window, $) {
  "use strict";

  /* globals jQuery, dashboardConfig, StatsProcessor, BubbleChart, DetailTable, BarChart, ServiceList, ServiceListView */

  var DashboardOptions;

  /**
   * Dashboard options
   * @typedef {{
   *  bubble_chart_el: HTMLElement|jQuery,
   *  detail_table_el: HTMLElement|jQuery,
   *  modal_handler: ModalHandler,
   *  stats_realtime_endpoint: String
   * }}
   */
  DashboardOptions = {
    bubble_chart_el: null,
    detail_table_el: null,
    modal_handler: null,
    stats_realtime_endpoint: null
  };

  /**
   * Dashboard constructor
   * It is the magic glue between the various graphs, the data and the network updates.
   *
   * @param {DashboardOptions} options
   * @constructor
   */
  var Dashboard = function Dashboard(options) {
    var self = this;
    var $body = $('body');

    /**
     * Reference to the barchart object
     * @type {BarChart}
     */
    self.barChart = null;

    /**
     * Reference to the stats processor  object
     * @type {StatsProcessor}
     */
    self.statsProcessor = null;

    /**
     * Reference to the bubble chart object
     * @type {BubbleChart}
     */
    self.bubbleChart = null;

    /**
     * Reference to the detail table object
     * @type {DetailTable}
     */
    self.detailTable = null;

    /**
     * Reference to the services list object
     * @type {ServiceList}
     */
    self.serviceList = null;

    /**
     * Reference to the services list view object
     * @type {ServiceListView}
     */
    self.serviceListView = null;

    /**
     * Reference to the URL manager object
     * @type {URLManager}
     */
    self.urlManager = null;

    /**
     * Reference to the modal handler object
     * @type {ModalHandler}
     */
    self.modalHandler = options.modal_handler;

    self.$bubbleChartEl = options.bubble_chart_el;
    self.$detailTableEl = options.detail_table_el;

    $body.on('radioItemClick', function (evt){ self.showDetailPanelForChannel(evt.item); });
    $body.on('peakButtonClick', function (evt) { self.showPeakData(evt); });

    var initialServicesToDisplay = Dashboard.INITIAL_SERVICES_TO_DISPLAY;

    // If URL contains a service list then use that for initial services
    self.urlManager = new UrlManager({ key: 'services', delim: ',' });
    if (self.urlManager.hasItems()) {
      initialServicesToDisplay = self.urlManager.items();
    }

    options.initialServicesToDisplay = initialServicesToDisplay;

    self._initServices(initialServicesToDisplay)
        .then(function () {
          self._initStatsProcessor(options);
        });

  };

  Dashboard.INITIAL_SERVICES_TO_DISPLAY = dashboardConfig.initialServices || [];

  /**
   * Initialize the stats processor, grab some initial data and feed the graph layout
   *
   * @param {DashboardOptions} options
   * @private
   */
  Dashboard.prototype._initStatsProcessor = function _initStatsProcessor(options){
    var self = this;

    self.statsProcessor = new StatsProcessor({
      filterServices: options.initialServicesToDisplay,
      endpoint: options.stats_realtime_endpoint
    });

    // Get the Stats objects for the view and augment
    // then with programme data from an external API
    // This is asyncronous and when all the data has
    // been augmented, run initWithData
    self.statsProcessor
        .initialData()
        .then(function () {
          return self._latestStatsObjectsWithProgrammeInfo();
        })
        .then(function (latest) {
          self.initWithData(latest);
        });

    $(self.statsProcessor).on("update", function () {
      self._latestStatsObjectsWithProgrammeInfo().
          then(
            function (latest) {
              self.update(latest);
            }
          );
    });
  };

  /**
   * Fetches all the latest Stats objects and augments them
   * with programme info.
   *
   * Returns a Promise that resolves with the array of
   * augmented latest Stats objects
   *
   * @private
   */
  Dashboard.prototype._latestStatsObjectsWithProgrammeInfo = function latestStatsObjectsWithProgrammeInfo() {
    var self     = this,
        deferred = $.Deferred(),
        latest = self.statsProcessor.latest(),
        promises;

    // For each Stats object in the latest array,
    // run the method that augments with remote programme data
    promises = latest.map(self._augmentStatsWithProgrammeData);

    // When all the Stats objects in the `latest`
    // array augmented, we resolve the promise
    // and pass the latest array through
    $.when.apply(null, promises)
     .then(
      function () {
        deferred.resolve(latest);
      });

    // return the promise so the caller
    // can listen using `then` for the
    // stats objects to be augmented
    return deferred.promise();
  };

  /**
   * Augments a Stats with programme data from an external API.
   * Note: The Stats object is modified directly so any references
   *       to it will gain the programme data
   *
   * @private
   * @param {<Stats>} stats Stats object to be augmented
   * @returns {<jQuery.Deferred>} A promise that resolves when the
                                  Stat object has been augmented.
   */
  Dashboard.prototype._augmentStatsWithProgrammeData = function _augmentStatsWithProgrammeData(stats){
    var deferred = $.Deferred();
    var self = this;
    var now = new Date();
    var time_now = now.toISOString();
    var channel = stats.channel_name;

    var url =  "http://dev.notu.be/2014/05/on_tv/?start_time="+time_now+"&service_key="+channel;

    var jqxhr =  $.getJSON(url)
                  .done(function(data) {
                    if(data["response"]["docs"][0]){
                      var prog = {
                        id: data["response"]["docs"][0]["pid"],
                        title: data["response"]["docs"][0]["title"],
                        subtitle: data["response"]["docs"][0]["title"],
                        service_id: data["response"]["docs"][0]["service_key"],
                        start: data["response"]["docs"][0]["start_time"],
                        end: data["response"]["docs"][0]["end_time"],
                        image: data["response"]["docs"][0]["image_url"]
                      };
                      stats.programme = prog;
                      deferred.resolve(stats);
                    }
                  });

    return deferred.promise();
  };

  /**
   * Initializes services handling.
   * It let the ability to the user to choose the displayed channels on both bubble and detail graph
   *
   * @private
   * @param {Array.<string>} initialServicesToDisplay Service ids to be displayed
   */
  Dashboard.prototype._initServices = function _initServices(initialServicesToDisplay){
    var self = this,
        deferred = $.Deferred();

    // List of all available services
    self.serviceList = new ServiceList(initialServicesToDisplay);

    // View to allow user to select which services they want
    self.serviceListView = new ServiceListView('#services-list ul');

    // Connect the list with the view
    self.serviceList
        .services()
        .then(function (services) {
          self.serviceListView.render(services);
          self.updateBookmark();

          // Link to the Stats class so that Stats objects
          // can be linked to their parent services
          Stats.serviceList = self.serviceList;

          // All done, notify anything waiting for
          // our promise to resolve
          deferred.resolve();
        });

    $(self.serviceList).on('serviceStateChanged', function (evt) {
      self.updateService(evt.service, evt.service.isSelected);
      self.updateBookmark();
    });

    return deferred.promise();
  };

  /**
   * Initialize the dashboard with fresh data.
   * Past this point, all further inbound data may arrive from Websocket connection
   *
   * @param {Array.<Stats>} latest
   */
  Dashboard.prototype.initWithData = function initWithData(latest) {
    this.bubbleChart = new BubbleChart(this.$bubbleChartEl, latest);
    this.detailTable = new DetailTable(this.$detailTableEl, latest);
  };

  /**
   * Updates the dashboard with new data.
   * Everything is automatically redrawn on screen.
   *
   * @api
   * @param {array} latest
   */
  Dashboard.prototype.update = function update(latest) {
    this.bubbleChart.update(latest);
    this.detailTable.update(latest);

    if (this.modalHandler.isVisible( $('#stats') )){
      var serviceId  = this.barChart.getCurrentServiceId(),
          lastUpdate = this.statsProcessor.lastUpdateReceivedForService(serviceId);

      this.barChart.appendGraphData(lastUpdate);
      this.barChart.updateGraph();
    }
  };

  /**
   * Shorthand to alter the displayed services on screen
   *
   * @api
   * @param {Service} service
   */
  Dashboard.prototype.updateService = function updateService(service) {
    if (service.isSelected) {
      this.statsProcessor.addService(service.id);
    } else {
      this.statsProcessor.removeService(service.id);
    }
  };

  /**
   * Update bookmarkable link to the current services
   *
   * @api
   */
  Dashboard.prototype.updateBookmark = function updateBookmark() {
    this.serviceList.selectedServices().then(function (services) {
      var ids = services.map(function (s) { return s.id; }).join(','),
          url = '?services=' + ids;
      $('.bookmark').attr('href', url);
    });
  };

  /**
   * Once a bubble or channel detail row is clicked, we display a detailed view with an historical view
   *
   * @api
   * @param {Stats} item
   */
  Dashboard.prototype.showDetailPanelForChannel = function showDetailPanelForChannel(item) {
    var self = this;

    //jshint devel:true
    console.log('showDetailPanelForChannel', item);

    if (!this.barChart) {
      this.barChart = new BarChart();
      this.barChart.setup({
        mediums: Object.keys(item.audience.platforms),
        programme_uri: dashboardConfig.programmeUri,
        programme_picture_uri: dashboardConfig.programmePictureUri
      });
    }

    this.barChart.updateHeadline(item);
    this.modalHandler.load( $('#stats') );

    this.statsProcessor.historicalByService(item.channel).then(function(response){
      var service_data = self.statsProcessor.parseUpdates(response.stations)[ item.channel ];

      // Swap the last data item form the server with
      // that one passed in as it will have programme
      // data
      service_data[service_data.length - 1] = item;

      self.barChart.update(service_data);
      self.barChart.updateGraph();

      setTimeout(function(){
        self.modalHandler.loaded( $('#stats') );
      }, 1000 );
    });
  };

  /**
   * Shorthand to display the detailed++ chart.
   * Suppose you already have displayed the channel detailed view.
   *
   * @todo move that in the barchart as it's a bit unrelated to the dashboard (legacy code related)
   * @param {MouseEvent=} event
   */
  Dashboard.prototype.showPeakData = function showPeakData(event) {
    // jshint unused:vars
    this.barChart.showFluxChart();
  };

  /*
   Exporting
   */
  window['Dashboard'] = Dashboard;

}(window, jQuery));
<commit_msg>Fix for incomplete historical data<commit_after>(function(window, $) {
  "use strict";

  /* globals jQuery, dashboardConfig, StatsProcessor, BubbleChart, DetailTable, BarChart, ServiceList, ServiceListView */

  var DashboardOptions;

  /**
   * Dashboard options
   * @typedef {{
   *  bubble_chart_el: HTMLElement|jQuery,
   *  detail_table_el: HTMLElement|jQuery,
   *  modal_handler: ModalHandler,
   *  stats_realtime_endpoint: String
   * }}
   */
  DashboardOptions = {
    bubble_chart_el: null,
    detail_table_el: null,
    modal_handler: null,
    stats_realtime_endpoint: null
  };

  /**
   * Dashboard constructor
   * It is the magic glue between the various graphs, the data and the network updates.
   *
   * @param {DashboardOptions} options
   * @constructor
   */
  var Dashboard = function Dashboard(options) {
    var self = this;
    var $body = $('body');

    /**
     * Reference to the barchart object
     * @type {BarChart}
     */
    self.barChart = null;

    /**
     * Reference to the stats processor  object
     * @type {StatsProcessor}
     */
    self.statsProcessor = null;

    /**
     * Reference to the bubble chart object
     * @type {BubbleChart}
     */
    self.bubbleChart = null;

    /**
     * Reference to the detail table object
     * @type {DetailTable}
     */
    self.detailTable = null;

    /**
     * Reference to the services list object
     * @type {ServiceList}
     */
    self.serviceList = null;

    /**
     * Reference to the services list view object
     * @type {ServiceListView}
     */
    self.serviceListView = null;

    /**
     * Reference to the URL manager object
     * @type {URLManager}
     */
    self.urlManager = null;

    /**
     * Reference to the modal handler object
     * @type {ModalHandler}
     */
    self.modalHandler = options.modal_handler;

    self.$bubbleChartEl = options.bubble_chart_el;
    self.$detailTableEl = options.detail_table_el;

    $body.on('radioItemClick', function (evt){ self.showDetailPanelForChannel(evt.item); });
    $body.on('peakButtonClick', function (evt) { self.showPeakData(evt); });

    var initialServicesToDisplay = Dashboard.INITIAL_SERVICES_TO_DISPLAY;

    // If URL contains a service list then use that for initial services
    self.urlManager = new UrlManager({ key: 'services', delim: ',' });
    if (self.urlManager.hasItems()) {
      initialServicesToDisplay = self.urlManager.items();
    }

    options.initialServicesToDisplay = initialServicesToDisplay;

    self._initServices(initialServicesToDisplay)
        .then(function () {
          self._initStatsProcessor(options);
        });

  };

  Dashboard.INITIAL_SERVICES_TO_DISPLAY = dashboardConfig.initialServices || [];

  /**
   * Initialize the stats processor, grab some initial data and feed the graph layout
   *
   * @param {DashboardOptions} options
   * @private
   */
  Dashboard.prototype._initStatsProcessor = function _initStatsProcessor(options){
    var self = this;

    self.statsProcessor = new StatsProcessor({
      filterServices: options.initialServicesToDisplay,
      endpoint: options.stats_realtime_endpoint
    });

    // Get the Stats objects for the view and augment
    // then with programme data from an external API
    // This is asyncronous and when all the data has
    // been augmented, run initWithData
    self.statsProcessor
        .initialData()
        .then(function () {
          return self._latestStatsObjectsWithProgrammeInfo();
        })
        .then(function (latest) {
          self.initWithData(latest);
        });

    $(self.statsProcessor).on("update", function () {
      self._latestStatsObjectsWithProgrammeInfo().
          then(
            function (latest) {
              self.update(latest);
            }
          );
    });
  };

  /**
   * Fetches all the latest Stats objects and augments them
   * with programme info.
   *
   * Returns a Promise that resolves with the array of
   * augmented latest Stats objects
   *
   * @private
   */
  Dashboard.prototype._latestStatsObjectsWithProgrammeInfo = function latestStatsObjectsWithProgrammeInfo() {
    var self     = this,
        deferred = $.Deferred(),
        latest = self.statsProcessor.latest(),
        promises;

    // For each Stats object in the latest array,
    // run the method that augments with remote programme data
    promises = latest.map(self._augmentStatsWithProgrammeData);

    // When all the Stats objects in the `latest`
    // array augmented, we resolve the promise
    // and pass the latest array through
    $.when.apply(null, promises)
     .then(
      function () {
        deferred.resolve(latest);
      });

    // return the promise so the caller
    // can listen using `then` for the
    // stats objects to be augmented
    return deferred.promise();
  };

  /**
   * Augments a Stats with programme data from an external API.
   * Note: The Stats object is modified directly so any references
   *       to it will gain the programme data
   *
   * @private
   * @param {<Stats>} stats Stats object to be augmented
   * @returns {<jQuery.Deferred>} A promise that resolves when the
                                  Stat object has been augmented.
   */
  Dashboard.prototype._augmentStatsWithProgrammeData = function _augmentStatsWithProgrammeData(stats){
    var deferred = $.Deferred();
    var self = this;
    var now = new Date();
    var time_now = now.toISOString();
    var channel = stats.channel_name;

    var url =  "http://dev.notu.be/2014/05/on_tv/?start_time="+time_now+"&service_key="+channel;

    var jqxhr =  $.getJSON(url)
                  .done(function(data) {
                    if(data["response"]["docs"][0]){
                      var prog = {
                        id: data["response"]["docs"][0]["pid"],
                        title: data["response"]["docs"][0]["title"],
                        subtitle: data["response"]["docs"][0]["title"],
                        service_id: data["response"]["docs"][0]["service_key"],
                        start: data["response"]["docs"][0]["start_time"],
                        end: data["response"]["docs"][0]["end_time"],
                        image: data["response"]["docs"][0]["image_url"]
                      };
                      stats.programme = prog;
                      deferred.resolve(stats);
                    }
                  });

    return deferred.promise();
  };

  /**
   * Initializes services handling.
   * It let the ability to the user to choose the displayed channels on both bubble and detail graph
   *
   * @private
   * @param {Array.<string>} initialServicesToDisplay Service ids to be displayed
   */
  Dashboard.prototype._initServices = function _initServices(initialServicesToDisplay){
    var self = this,
        deferred = $.Deferred();

    // List of all available services
    self.serviceList = new ServiceList(initialServicesToDisplay);

    // View to allow user to select which services they want
    self.serviceListView = new ServiceListView('#services-list ul');

    // Connect the list with the view
    self.serviceList
        .services()
        .then(function (services) {
          self.serviceListView.render(services);
          self.updateBookmark();

          // Link to the Stats class so that Stats objects
          // can be linked to their parent services
          Stats.serviceList = self.serviceList;

          // All done, notify anything waiting for
          // our promise to resolve
          deferred.resolve();
        });

    $(self.serviceList).on('serviceStateChanged', function (evt) {
      self.updateService(evt.service, evt.service.isSelected);
      self.updateBookmark();
    });

    return deferred.promise();
  };

  /**
   * Initialize the dashboard with fresh data.
   * Past this point, all further inbound data may arrive from Websocket connection
   *
   * @param {Array.<Stats>} latest
   */
  Dashboard.prototype.initWithData = function initWithData(latest) {
    this.bubbleChart = new BubbleChart(this.$bubbleChartEl, latest);
    this.detailTable = new DetailTable(this.$detailTableEl, latest);
  };

  /**
   * Updates the dashboard with new data.
   * Everything is automatically redrawn on screen.
   *
   * @api
   * @param {array} latest
   */
  Dashboard.prototype.update = function update(latest) {
    this.bubbleChart.update(latest);
    this.detailTable.update(latest);

    if (this.modalHandler.isVisible( $('#stats') )){
      var serviceId  = this.barChart.getCurrentServiceId(),
          lastUpdate = this.statsProcessor.lastUpdateReceivedForService(serviceId);

      this.barChart.appendGraphData(lastUpdate);
      this.barChart.updateGraph();
    }
  };

  /**
   * Shorthand to alter the displayed services on screen
   *
   * @api
   * @param {Service} service
   */
  Dashboard.prototype.updateService = function updateService(service) {
    if (service.isSelected) {
      this.statsProcessor.addService(service.id);
    } else {
      this.statsProcessor.removeService(service.id);
    }
  };

  /**
   * Update bookmarkable link to the current services
   *
   * @api
   */
  Dashboard.prototype.updateBookmark = function updateBookmark() {
    this.serviceList.selectedServices().then(function (services) {
      var ids = services.map(function (s) { return s.id; }).join(','),
          url = '?services=' + ids;
      $('.bookmark').attr('href', url);
    });
  };

  /**
   * Once a bubble or channel detail row is clicked, we display a detailed view with an historical view
   *
   * @api
   * @param {Stats} item
   */
  Dashboard.prototype.showDetailPanelForChannel = function showDetailPanelForChannel(item) {
    var self = this;

    //jshint devel:true
    console.log('showDetailPanelForChannel', item);

    if (!this.barChart) {
      this.barChart = new BarChart();
      this.barChart.setup({
        mediums: Object.keys(item.audience.platforms),
        programme_uri: dashboardConfig.programmeUri,
        programme_picture_uri: dashboardConfig.programmePictureUri
      });
    }

    this.barChart.updateHeadline(item);
    this.modalHandler.load( $('#stats') );

    this.statsProcessor.historicalByService(item.channel).then(function(response){
      var service_data = self.statsProcessor.parseUpdates(response.stations)[ item.channel ];

      // Swap the last data item form the server with
      // that one passed in as it will have programme
      // data
      //sometimess there's no historical data, this helps
      if(!service_data){
        console.log("No service data yet for "+item.channel+" continuing anyway");
        service_data = self.statsProcessor.parseUpdates(item)[ item.channel ];
      }
      service_data[service_data.length - 1] = item;
      self.barChart.update(service_data);
      self.barChart.updateGraph();

      setTimeout(function(){
        self.modalHandler.loaded( $('#stats') );
      }, 1000 );
    });
  };

  /**
   * Shorthand to display the detailed++ chart.
   * Suppose you already have displayed the channel detailed view.
   *
   * @todo move that in the barchart as it's a bit unrelated to the dashboard (legacy code related)
   * @param {MouseEvent=} event
   */
  Dashboard.prototype.showPeakData = function showPeakData(event) {
    // jshint unused:vars
    this.barChart.showFluxChart();
  };

  /*
   Exporting
   */
  window['Dashboard'] = Dashboard;

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


<commit_before>var tml = {
    fillJobCommonFields: function (html, job){

        html.find(".id").addClass("row-" + job._status);

        html.find("[data-id]").text(job._id);
        html.find("[data-target-module]").text(job._targetModule);
        html.find("[data-target-version]").text(job._targetVersion);
        html.find("[data-status]").text(job._status);
        html.find(".label").addClass("label-" + job._status);
        html.find("[data-queued-date]").text(job._queued_ago);
        html.find("[data-updated-date]").text(job._updated_ago);
        // ---- User
        tml.displayTooltip(job._user, html.find("[data-user]"));
        tml.displayTooltip(job._queued_at, html.find("[data-queue]"));
        tml.displayTooltip(job._updated_at, html.find("[data-update]"));

        // ---- Ticket
        tml.displayTicket(job._ticket, html.find("[data-ticket]"));
        // ---- Common actions
        tml.displayActions(job._buildJobUrl, html.find("[data-jenkins]"));
        tml.displayActions(job._deployJobUrl, html.find("[data-deployment]"));
        tml.displayActions(job._testJobUrl, html.find("[data-test-job]"));
        // ---- Start tooltip
        html.find("[data-toggle='tooltip']").tooltip();
    },

    fillRepoFields: function (html, repo){
        html.find("[data-repo]").text(repo._module).attr("href", repo._module_link);
        html.find("[data-testing]").text(repo._testing).attr("href", repo._testing_link);
        html.find("[data-testing-time]").text(repo._testing_time);
        html.find("[data-staging]").text(repo._staging).attr("href", repo._staging_link);
        html.find("[data-staging-time]").text(repo._staging_time);
        html.find("[data-production]").text(repo._production).attr("href", repo._production_link);
        html.find("[data-production-time]").text(repo._production_time);
        // ---- Ticket
        tml.displayTicket(repo._ticket, html.find("[data-ticket]"));
        // ---- Start tooltip
        html.find("[data-toggle='tooltip']").tooltip();
    },

    handleDate: function (jobDate, currentDate){
        var current = currentDate,
            job = new Date(jobDate),
            days,
            hours,
            minutes;

        minutes = (Math.abs(current - job) / 1000) / 60;
        hours = minutes / 60;
        days = hours / 24;

        minutes = parseInt(minutes);
        hours = parseInt(hours);
        days = parseInt(days);

        if (minutes < 60) {
            return minutes + " min ago";
        } else if (hours < 24) {
            return hours + " hours ago";
        } else if (!isNaN(days)) {
            return days + " days ago";
        } else {
            return "-";
        }
    },

    displayTooltip: function (data, selector){
        if (data != "") {
            selector.attr("title", data);
            selector.show();
        }
    },

    displayTicket: function (data, selector){
        if (data != "") {
            selector.attr("title", data);
            selector.attr("href", data);
            selector.show();
        }

    },

    displayActions: function (data, selector, toJson) {
        var toJson = toJson || false;
        if(toJson && data != "") {
            selector.data("job-id", data.id);
            selector.data("job-targetModule", data.targetModule);
            selector.data("job-targetVersion", data.targetVersion);
            selector.show();
        } else {
            if(data != "") {
                selector.attr("href", data);
                selector.show();
            }
        }
    },

    /* -------------------------
       -- Fill job template ---
       ------------------------- */
    preprodQueue: function (job) {
        var newJob = $("#resources .job").clone(),
            remove = "",
            date = new Date(job._serverTime);

        if(job._canCancel) {
            remove = {
                "id": job._id,
                "targetModule": job._targetModule,
                "targetVersion": job._targetVersion
            };
        }

        // convert date to time
        job._queued_ago = tml.handleDate(job._queued_at, date);
        job._updated_ago = tml.handleDate(job._updated_at, date);

        tml.fillJobCommonFields(newJob, job);
        tml.displayActions(remove, newJob.find("[data-remove]"), true);

        return newJob;
    },

    preprodInProgress: function (job) {
        var newJob = $("#resources .job").clone(),
            date = new Date(job._serverTime);

        // convert date to time
        job._queued_ago = tml.handleDate(job._queued_at, date);
        job._updated_ago = tml.handleDate(job._updated_at, date);

        tml.fillJobCommonFields(newJob, job);

        return newJob;
    },

    preprodDeployed: function (job) {
        var newJob = $("#resources .job").clone(),
            canGoLive = job._canGoLive,
            goLive = "",
            date = new Date(job._serverTime);

        if (canGoLive){
            goLive = {
                "id": job._id,
                "targetModule": job._targetModule,
                "targetVersion": job._targetVersion
            };
        }
        // convert date to time
        job._queued_ago = tml.handleDate(job._queued_at, date);
        job._updated_ago = tml.handleDate(job._updated_at, date);

        tml.fillJobCommonFields(newJob, job);
        tml.displayActions(goLive, newJob.find("[data-go-live]"), true);

        return newJob;
    },

    prodQueue: function (job) {
        var newJob = $("#resources .job").clone(),
            remove = "",
            date = new Date(job._serverTime);

        if(job._canCancel) {
            remove = {
                "id": job._id,
                "targetModule": job._targetModule,
                "targetVersion": job._targetVersion
            };
        }

        // convert date to time
        job._queued_ago = tml.handleDate(job._queued_at, date);
        job._updated_ago = tml.handleDate(job._updated_at, date);

        tml.fillJobCommonFields(newJob, job);
        tml.displayActions(remove, newJob.find("[data-remove]"), true);

        return newJob;
    },

    prodInProgress: function (job) {
        var newJob = $("#resources .job").clone(),
            viewLiveJob = job._deployLiveJobUrl,
            date = new Date(job._serverTime);

        // convert date to time
        job._queued_ago = tml.handleDate(job._queued_at, date);
        job._updated_ago = tml.handleDate(job._updated_at, date);

        tml.fillJobCommonFields(newJob, job);
        tml.displayActions(viewLiveJob, newJob.find("[data-live-job]"));

        return newJob;
    },

    prodDeployed: function (job) {
        var newJob = $("#resources .job").clone(),
            viewLiveJob = job._deployLiveJobUrl,
            wentLive = job._canRollback,
            redeploy = "",
            date = new Date(job._serverTime);

        if (wentLive){
            redeploy = {
                "id": job._id,
                "targetModule": job._targetModule,
                "targetVersion": job._targetVersion
            };
        }

        // convert date to time
        job._queued_ago = tml.handleDate(job._queued_at, date);
        job._updated_ago = tml.handleDate(job._updated_at, date);

        tml.fillJobCommonFields(newJob, job);
        tml.displayActions(viewLiveJob, newJob.find("[data-live-job]"));
        tml.displayActions(redeploy, newJob.find("[data-redeploy]"), true);

        return newJob;
    },

    /* -------------------------
       -- Fill repo template ---
       ------------------------- */
    version: function (version) {
        var newRepo = $("#resources .repo").clone(),
            canRollback = false, //repo._canRollback
            goLive = "",
            rollback = "";

        if (canRollback){
            rollback = {
                "id": version._id,
                "targetModule": version._module,
            }
        }
        tml.fillRepoFields(newRepo, version);

        if (typeof version._prod_ready != 'undefined')
          if (version._prod_ready != version._production && version._canGoLive) {
            goLive = {
                "targetModule": version._module,
                "targetVersion": version._prod_ready
            };
            prod_label = newRepo.find("[data-production]");
            prod_label.attr("href", version._prod_ready_link);
            prod_label.css("color", "red");
            tml.displayTooltip("Production ready", prod_label);
        }
        tml.displayActions(goLive, newRepo.find("[data-conf-live]"), true);

        //tml.displayActions(rollback, newRepo.find("[data-rollback]"), true);

        return newRepo;
    }
}
<commit_msg>Fixes 2<commit_after>var tml = {
    fillJobCommonFields: function (html, job){

        html.find(".id").addClass("row-" + job._status);

        html.find("[data-id]").text(job._id);
        html.find("[data-target-module]").text(job._targetModule);
        html.find("[data-target-version]").text(job._targetVersion);
        html.find("[data-status]").text(job._status);
        html.find(".label").addClass("label-" + job._status);
        html.find("[data-queued-date]").text(job._queued_ago);
        html.find("[data-updated-date]").text(job._updated_ago);
        // ---- User
        tml.displayTooltip(job._user, html.find("[data-user]"));
        tml.displayTooltip(job._queued_at, html.find("[data-queue]"));
        tml.displayTooltip(job._updated_at, html.find("[data-update]"));

        // ---- Ticket
        tml.displayTicket(job._ticket, html.find("[data-ticket]"));
        // ---- Common actions
        tml.displayActions(job._buildJobUrl, html.find("[data-jenkins]"));
        tml.displayActions(job._deployJobUrl, html.find("[data-deployment]"));
        tml.displayActions(job._testJobUrl, html.find("[data-test-job]"));
        // ---- Start tooltip
        html.find("[data-toggle='tooltip']").tooltip();
    },

    fillRepoFields: function (html, repo){
        html.find("[data-repo]").text(repo._module).attr("href", repo._module_link);
        html.find("[data-testing]").text(repo._testing).attr("href", repo._testing_link);
        html.find("[data-testing-time]").text(repo._testing_time);
        html.find("[data-staging]").text(repo._staging).attr("href", repo._staging_link);
        html.find("[data-staging-time]").text(repo._staging_time);
        html.find("[data-production]").text(repo._production).attr("href", repo._production_link);
        html.find("[data-production-time]").text(repo._production_time);
        // ---- Ticket
        tml.displayTicket(repo._ticket, html.find("[data-ticket]"));
        // ---- Start tooltip
        html.find("[data-toggle='tooltip']").tooltip();
    },

    handleDate: function (jobDate, currentDate){
        var current = currentDate,
            job = new Date(jobDate),
            days,
            hours,
            minutes;

        minutes = (Math.abs(current - job) / 1000) / 60;
        hours = minutes / 60;
        days = hours / 24;

        minutes = parseInt(minutes);
        hours = parseInt(hours);
        days = parseInt(days);

        if (minutes < 60) {
            return minutes + " min ago";
        } else if (hours < 24) {
            return hours + " hours ago";
        } else if (!isNaN(days)) {
            return days + " days ago";
        } else {
            return "-";
        }
    },

    displayTooltip: function (data, selector){
        if (data != "") {
            selector.attr("title", data);
            selector.show();
        }
    },

    displayTicket: function (data, selector){
        if (data != "") {
            selector.attr("title", data);
            selector.attr("href", data);
            selector.show();
        }

    },

    displayActions: function (data, selector, toJson) {
        var toJson = toJson || false;
        if(toJson && data != "") {
            selector.data("job-id", data.id);
            selector.data("job-targetModule", data.targetModule);
            selector.data("job-targetVersion", data.targetVersion);
            selector.show();
        } else {
            if(data != "") {
                selector.attr("href", data);
                selector.show();
            }
        }
    },

    /* -------------------------
       -- Fill job template ---
       ------------------------- */
    preprodQueue: function (job) {
        var newJob = $("#resources .job").clone(),
            remove = "",
            date = new Date(job._serverTime);

        if(job._canCancel) {
            remove = {
                "id": job._id,
                "targetModule": job._targetModule,
                "targetVersion": job._targetVersion
            };
        }

        // convert date to time
        job._queued_ago = tml.handleDate(job._queued_at, date);
        job._updated_ago = tml.handleDate(job._updated_at, date);

        tml.fillJobCommonFields(newJob, job);
        tml.displayActions(remove, newJob.find("[data-remove]"), true);

        return newJob;
    },

    preprodInProgress: function (job) {
        var newJob = $("#resources .job").clone(),
            date = new Date(job._serverTime);

        // convert date to time
        job._queued_ago = tml.handleDate(job._queued_at, date);
        job._updated_ago = tml.handleDate(job._updated_at, date);

        tml.fillJobCommonFields(newJob, job);

        return newJob;
    },

    preprodDeployed: function (job) {
        var newJob = $("#resources .job").clone(),
            canGoLive = job._canGoLive,
            goLive = "",
            date = new Date(job._serverTime);

        if (canGoLive){
            goLive = {
                "id": job._id,
                "targetModule": job._targetModule,
                "targetVersion": job._targetVersion
            };
        }
        // convert date to time
        job._queued_ago = tml.handleDate(job._queued_at, date);
        job._updated_ago = tml.handleDate(job._updated_at, date);

        tml.fillJobCommonFields(newJob, job);
        tml.displayActions(goLive, newJob.find("[data-go-live]"), true);

        return newJob;
    },

    prodQueue: function (job) {
        var newJob = $("#resources .job").clone(),
            remove = "",
            date = new Date(job._serverTime);

        if(job._canCancel) {
            remove = {
                "id": job._id,
                "targetModule": job._targetModule,
                "targetVersion": job._targetVersion
            };
        }

        // convert date to time
        job._queued_ago = tml.handleDate(job._queued_at, date);
        job._updated_ago = tml.handleDate(job._updated_at, date);

        tml.fillJobCommonFields(newJob, job);
        tml.displayActions(remove, newJob.find("[data-remove]"), true);

        return newJob;
    },

    prodInProgress: function (job) {
        var newJob = $("#resources .job").clone(),
            viewLiveJob = job._deployLiveJobUrl,
            date = new Date(job._serverTime);

        // convert date to time
        job._queued_ago = tml.handleDate(job._queued_at, date);
        job._updated_ago = tml.handleDate(job._updated_at, date);

        tml.fillJobCommonFields(newJob, job);
        tml.displayActions(viewLiveJob, newJob.find("[data-live-job]"));

        return newJob;
    },

    prodDeployed: function (job) {
        var newJob = $("#resources .job").clone(),
            viewLiveJob = job._deployLiveJobUrl,
            wentLive = job._canRollback,
            redeploy = "",
            date = new Date(job._serverTime);

        if (wentLive){
            redeploy = {
                "id": job._id,
                "targetModule": job._targetModule,
                "targetVersion": job._targetVersion
            };
        }

        // convert date to time
        job._queued_ago = tml.handleDate(job._queued_at, date);
        job._updated_ago = tml.handleDate(job._updated_at, date);

        tml.fillJobCommonFields(newJob, job);
        tml.displayActions(viewLiveJob, newJob.find("[data-live-job]"));
        tml.displayActions(redeploy, newJob.find("[data-redeploy]"), true);

        return newJob;
    },

    /* -------------------------
       -- Fill repo template ---
       ------------------------- */
    version: function (version) {
        var newRepo = $("#resources .repo").clone(),
            canRollback = false, //repo._canRollback
            goLive = "",
            rollback = "";

        if (canRollback){
            rollback = {
                "id": version._id,
                "targetModule": version._module,
            }
        }
        tml.fillRepoFields(newRepo, version);

        if (typeof version._prod_ready != 'undefined')
          if (version._prod_ready != version._production)
            if (version._canGoLive) {
              goLive = {
                  "targetModule": version._module,
                  "targetVersion": version._prod_ready
              };
            }
            prod_label = newRepo.find("[data-production]");
            prod_label.attr("href", version._prod_ready_link);
            prod_label.css("color", "red");
            tml.displayTooltip("Production ready", prod_label);
        }
        tml.displayActions(goLive, newRepo.find("[data-conf-live]"), true);

        //tml.displayActions(rollback, newRepo.find("[data-rollback]"), true);

        return newRepo;
    }
}
<|endoftext|>


<commit_before>/**
 * @author       Richard Davey <rich@photonstorm.com>
 * @copyright    2022 Photon Storm Ltd.
 * @license      {@link https://opensource.org/licenses/MIT|MIT License}
 */

var CONST = require('../const');
var Class = require('../utils/Class');
var Frame = require('./Frame');
var TextureSource = require('./TextureSource');

var TEXTURE_MISSING_ERROR = 'Texture "%s" has no frame "%s"';

/**
 * @classdesc
 * A Texture consists of a source, usually an Image from the Cache, and a collection of Frames.
 * The Frames represent the different areas of the Texture. For example a texture atlas
 * may have many Frames, one for each element within the atlas. Where-as a single image would have
 * just one frame, that encompasses the whole image.
 *
 * Every Texture, no matter where it comes from, always has at least 1 frame called the `__BASE` frame.
 * This frame represents the entirety of the source image.
 *
 * Textures are managed by the global TextureManager. This is a singleton class that is
 * responsible for creating and delivering Textures and their corresponding Frames to Game Objects.
 *
 * Sprites and other Game Objects get the texture data they need from the TextureManager.
 *
 * @class Texture
 * @memberof Phaser.Textures
 * @constructor
 * @since 3.0.0
 *
 * @param {Phaser.Textures.TextureManager} manager - A reference to the Texture Manager this Texture belongs to.
 * @param {string} key - The unique string-based key of this Texture.
 * @param {(HTMLImageElement|HTMLCanvasElement|HTMLImageElement[]|HTMLCanvasElement[])} source - An array of sources that are used to create the texture. Usually Images, but can also be a Canvas.
 * @param {number} [width] - The width of the Texture. This is optional and automatically derived from the source images.
 * @param {number} [height] - The height of the Texture. This is optional and automatically derived from the source images.
 */
var Texture = new Class({

    initialize:

    function Texture (manager, key, source, width, height)
    {
        if (!Array.isArray(source))
        {
            source = [ source ];
        }

        /**
         * A reference to the Texture Manager this Texture belongs to.
         *
         * @name Phaser.Textures.Texture#manager
         * @type {Phaser.Textures.TextureManager}
         * @since 3.0.0
         */
        this.manager = manager;

        /**
         * The unique string-based key of this Texture.
         *
         * @name Phaser.Textures.Texture#key
         * @type {string}
         * @since 3.0.0
         */
        this.key = key;

        /**
         * An array of TextureSource instances.
         * These are unique to this Texture and contain the actual Image (or Canvas) data.
         *
         * @name Phaser.Textures.Texture#source
         * @type {Phaser.Textures.TextureSource[]}
         * @since 3.0.0
         */
        this.source = [];

        /**
         * An array of TextureSource data instances.
         * Used to store additional data images, such as normal maps or specular maps.
         *
         * @name Phaser.Textures.Texture#dataSource
         * @type {array}
         * @since 3.0.0
         */
        this.dataSource = [];

        /**
         * A key-value object pair associating the unique Frame keys with the Frames objects.
         *
         * @name Phaser.Textures.Texture#frames
         * @type {object}
         * @since 3.0.0
         */
        this.frames = {};

        /**
         * Any additional data that was set in the source JSON (if any),
         * or any extra data you'd like to store relating to this texture
         *
         * @name Phaser.Textures.Texture#customData
         * @type {object}
         * @since 3.0.0
         */
        this.customData = {};

        /**
         * The name of the first frame of the Texture.
         *
         * @name Phaser.Textures.Texture#firstFrame
         * @type {string}
         * @since 3.0.0
         */
        this.firstFrame = '__BASE';

        /**
         * The total number of Frames in this Texture, including the `__BASE` frame.
         *
         * A Texture will always contain at least 1 frame because every Texture contains a `__BASE` frame by default,
         * in addition to any extra frames that have been added to it, such as when parsing a Sprite Sheet or Texture Atlas.
         *
         * @name Phaser.Textures.Texture#frameTotal
         * @type {number}
         * @default 0
         * @since 3.0.0
         */
        this.frameTotal = 0;

        //  Load the Sources
        for (var i = 0; i < source.length; i++)
        {
            this.source.push(new TextureSource(this, source[i], width, height));
        }
    },

    /**
     * Adds a new Frame to this Texture.
     *
     * A Frame is a rectangular region of a TextureSource with a unique index or string-based key.
     *
     * The name given must be unique within this Texture. If it already exists, this method will return `null`.
     *
     * @method Phaser.Textures.Texture#add
     * @since 3.0.0
     *
     * @param {(number|string)} name - The name of this Frame. The name is unique within the Texture.
     * @param {number} sourceIndex - The index of the TextureSource that this Frame is a part of.
     * @param {number} x - The x coordinate of the top-left of this Frame.
     * @param {number} y - The y coordinate of the top-left of this Frame.
     * @param {number} width - The width of this Frame.
     * @param {number} height - The height of this Frame.
     *
     * @return {?Phaser.Textures.Frame} The Frame that was added to this Texture, or `null` if the given name already exists.
     */
    add: function (name, sourceIndex, x, y, width, height)
    {
        if (this.has(name))
        {
            return null;
        }

        var frame = new Frame(this, name, sourceIndex, x, y, width, height);

        this.frames[name] = frame;

        //  Set the first frame of the Texture (other than __BASE)
        //  This is used to ensure we don't spam the display with entire
        //  atlases of sprite sheets, but instead just the first frame of them
        //  should the dev incorrectly specify the frame index
        if (this.firstFrame === '__BASE')
        {
            this.firstFrame = name;
        }

        this.frameTotal++;

        return frame;
    },

    /**
     * Removes the given Frame from this Texture. The Frame is destroyed immediately.
     *
     * Any Game Objects using this Frame should stop using it _before_ you remove it,
     * as it does not happen automatically.
     *
     * @method Phaser.Textures.Texture#remove
     * @since 3.19.0
     *
     * @param {string} name - The key of the Frame to remove.
     *
     * @return {boolean} True if a Frame with the matching key was removed from this Texture.
     */
    remove: function (name)
    {
        if (this.has(name))
        {
            var frame = this.get(name);

            frame.destroy();

            delete this.frames[name];

            return true;
        }

        return false;
    },

    /**
     * Checks to see if a Frame matching the given key exists within this Texture.
     *
     * @method Phaser.Textures.Texture#has
     * @since 3.0.0
     *
     * @param {string} name - The key of the Frame to check for.
     *
     * @return {boolean} True if a Frame with the matching key exists in this Texture.
     */
    has: function (name)
    {
        return (this.frames[name]);
    },

    /**
     * Gets a Frame from this Texture based on either the key or the index of the Frame.
     *
     * In a Texture Atlas Frames are typically referenced by a key.
     * In a Sprite Sheet Frames are referenced by an index.
     * Passing no value for the name returns the base texture.
     *
     * @method Phaser.Textures.Texture#get
     * @since 3.0.0
     *
     * @param {(string|number)} [name] - The string-based name, or integer based index, of the Frame to get from this Texture.
     *
     * @return {Phaser.Textures.Frame} The Texture Frame.
     */
    get: function (name)
    {
        //  null, undefined, empty string, zero
        if (!name)
        {
            name = this.firstFrame;
        }

        var frame = this.frames[name];

        if (!frame)
        {
            console.warn(TEXTURE_MISSING_ERROR, this.key, name);

            frame = this.frames[this.firstFrame];
        }

        return frame;
    },

    /**
     * Takes the given TextureSource and returns the index of it within this Texture.
     * If it's not in this Texture, it returns -1.
     * Unless this Texture has multiple TextureSources, such as with a multi-atlas, this
     * method will always return zero or -1.
     *
     * @method Phaser.Textures.Texture#getTextureSourceIndex
     * @since 3.0.0
     *
     * @param {Phaser.Textures.TextureSource} source - The TextureSource to check.
     *
     * @return {number} The index of the TextureSource within this Texture, or -1 if not in this Texture.
     */
    getTextureSourceIndex: function (source)
    {
        for (var i = 0; i < this.source.length; i++)
        {
            if (this.source[i] === source)
            {
                return i;
            }
        }

        return -1;
    },

    /**
     * Returns an array of all the Frames in the given TextureSource.
     *
     * @method Phaser.Textures.Texture#getFramesFromTextureSource
     * @since 3.0.0
     *
     * @param {number} sourceIndex - The index of the TextureSource to get the Frames from.
     * @param {boolean} [includeBase=false] - Include the `__BASE` Frame in the output array?
     *
     * @return {Phaser.Textures.Frame[]} An array of Texture Frames.
     */
    getFramesFromTextureSource: function (sourceIndex, includeBase)
    {
        if (includeBase === undefined) { includeBase = false; }

        var out = [];

        for (var frameName in this.frames)
        {
            if (frameName === '__BASE' && !includeBase)
            {
                continue;
            }

            var frame = this.frames[frameName];

            if (frame.sourceIndex === sourceIndex)
            {
                out.push(frame);
            }
        }

        return out;
    },

    /**
     * Returns an array with all of the names of the Frames in this Texture.
     *
     * Useful if you want to randomly assign a Frame to a Game Object, as you can
     * pick a random element from the returned array.
     *
     * @method Phaser.Textures.Texture#getFrameNames
     * @since 3.0.0
     *
     * @param {boolean} [includeBase=false] - Include the `__BASE` Frame in the output array?
     *
     * @return {string[]} An array of all Frame names in this Texture.
     */
    getFrameNames: function (includeBase)
    {
        if (includeBase === undefined) { includeBase = false; }

        var out = Object.keys(this.frames);

        if (!includeBase)
        {
            var idx = out.indexOf('__BASE');

            if (idx !== -1)
            {
                out.splice(idx, 1);
            }
        }

        return out;
    },

    /**
     * Given a Frame name, return the source image it uses to render with.
     *
     * This will return the actual DOM Image or Canvas element.
     *
     * @method Phaser.Textures.Texture#getSourceImage
     * @since 3.0.0
     *
     * @param {(string|number)} [name] - The string-based name, or integer based index, of the Frame to get from this Texture.
     *
     * @return {(HTMLImageElement|HTMLCanvasElement|Phaser.GameObjects.RenderTexture)} The DOM Image, Canvas Element or Render Texture.
     */
    getSourceImage: function (name)
    {
        if (name === undefined || name === null || this.frameTotal === 1)
        {
            name = '__BASE';
        }

        var frame = this.frames[name];

        if (frame)
        {
            return frame.source.image;
        }
        else
        {
            console.warn(TEXTURE_MISSING_ERROR, this.key, name);

            return this.frames['__BASE'].source.image;
        }
    },

    /**
     * Given a Frame name, return the data source image it uses to render with.
     * You can use this to get the normal map for an image for example.
     *
     * This will return the actual DOM Image.
     *
     * @method Phaser.Textures.Texture#getDataSourceImage
     * @since 3.7.0
     *
     * @param {(string|number)} [name] - The string-based name, or integer based index, of the Frame to get from this Texture.
     *
     * @return {(HTMLImageElement|HTMLCanvasElement)} The DOM Image or Canvas Element.
     */
    getDataSourceImage: function (name)
    {
        if (name === undefined || name === null || this.frameTotal === 1)
        {
            name = '__BASE';
        }

        var frame = this.frames[name];
        var idx;

        if (!frame)
        {
            console.warn(TEXTURE_MISSING_ERROR, this.key, name);

            idx = this.frames['__BASE'].sourceIndex;
        }
        else
        {
            idx = frame.sourceIndex;
        }

        return this.dataSource[idx].image;
    },

    /**
     * Adds a data source image to this Texture.
     *
     * An example of a data source image would be a normal map, where all of the Frames for this Texture
     * equally apply to the normal map.
     *
     * @method Phaser.Textures.Texture#setDataSource
     * @since 3.0.0
     *
     * @param {(HTMLImageElement|HTMLCanvasElement|HTMLImageElement[]|HTMLCanvasElement[])} data - The source image.
     */
    setDataSource: function (data)
    {
        if (!Array.isArray(data))
        {
            data = [ data ];
        }

        for (var i = 0; i < data.length; i++)
        {
            var source = this.source[i];

            this.dataSource.push(new TextureSource(this, data[i], source.width, source.height));
        }
    },

    /**
     * Sets the Filter Mode for this Texture.
     *
     * The mode can be either Linear, the default, or Nearest.
     *
     * For pixel-art you should use Nearest.
     *
     * The mode applies to the entire Texture, not just a specific Frame of it.
     *
     * @method Phaser.Textures.Texture#setFilter
     * @since 3.0.0
     *
     * @param {Phaser.Textures.FilterMode} filterMode - The Filter Mode.
     */
    setFilter: function (filterMode)
    {
        var i;

        for (i = 0; i < this.source.length; i++)
        {
            this.source[i].setFilter(filterMode);
        }

        for (i = 0; i < this.dataSource.length; i++)
        {
            this.dataSource[i].setFilter(filterMode);
        }
    },

    /**
     * Destroys this Texture and releases references to its sources and frames.
     *
     * @method Phaser.Textures.Texture#destroy
     * @since 3.0.0
     */
    destroy: function ()
    {
        var i;

        for (i = 0; i < this.source.length; i++)
        {
            this.source[i].destroy();
        }

        for (i = 0; i < this.dataSource.length; i++)
        {
            this.dataSource[i].destroy();
        }

        for (var frameName in this.frames)
        {
            var frame = this.frames[frameName];

            frame.destroy();
        }

        this.source = [];
        this.dataSource = [];
        this.frames = {};

        this.manager.removeKey(this.key);

        var renderer = this.manager.game.renderer;

        if (renderer && renderer.type === CONST.WEBGL)
        {
            renderer.resetTextures(true);
        }

        this.manager = null;
    }

});

module.exports = Texture;
<commit_msg>The `Texture.destroy` method will only destroy sources, dataSources and frames if they exist, protecting against previously destroyed instances.<commit_after>/**
 * @author       Richard Davey <rich@photonstorm.com>
 * @copyright    2022 Photon Storm Ltd.
 * @license      {@link https://opensource.org/licenses/MIT|MIT License}
 */

var CONST = require('../const');
var Class = require('../utils/Class');
var Frame = require('./Frame');
var TextureSource = require('./TextureSource');

var TEXTURE_MISSING_ERROR = 'Texture "%s" has no frame "%s"';

/**
 * @classdesc
 * A Texture consists of a source, usually an Image from the Cache, and a collection of Frames.
 * The Frames represent the different areas of the Texture. For example a texture atlas
 * may have many Frames, one for each element within the atlas. Where-as a single image would have
 * just one frame, that encompasses the whole image.
 *
 * Every Texture, no matter where it comes from, always has at least 1 frame called the `__BASE` frame.
 * This frame represents the entirety of the source image.
 *
 * Textures are managed by the global TextureManager. This is a singleton class that is
 * responsible for creating and delivering Textures and their corresponding Frames to Game Objects.
 *
 * Sprites and other Game Objects get the texture data they need from the TextureManager.
 *
 * @class Texture
 * @memberof Phaser.Textures
 * @constructor
 * @since 3.0.0
 *
 * @param {Phaser.Textures.TextureManager} manager - A reference to the Texture Manager this Texture belongs to.
 * @param {string} key - The unique string-based key of this Texture.
 * @param {(HTMLImageElement|HTMLCanvasElement|HTMLImageElement[]|HTMLCanvasElement[])} source - An array of sources that are used to create the texture. Usually Images, but can also be a Canvas.
 * @param {number} [width] - The width of the Texture. This is optional and automatically derived from the source images.
 * @param {number} [height] - The height of the Texture. This is optional and automatically derived from the source images.
 */
var Texture = new Class({

    initialize:

    function Texture (manager, key, source, width, height)
    {
        if (!Array.isArray(source))
        {
            source = [ source ];
        }

        /**
         * A reference to the Texture Manager this Texture belongs to.
         *
         * @name Phaser.Textures.Texture#manager
         * @type {Phaser.Textures.TextureManager}
         * @since 3.0.0
         */
        this.manager = manager;

        /**
         * The unique string-based key of this Texture.
         *
         * @name Phaser.Textures.Texture#key
         * @type {string}
         * @since 3.0.0
         */
        this.key = key;

        /**
         * An array of TextureSource instances.
         * These are unique to this Texture and contain the actual Image (or Canvas) data.
         *
         * @name Phaser.Textures.Texture#source
         * @type {Phaser.Textures.TextureSource[]}
         * @since 3.0.0
         */
        this.source = [];

        /**
         * An array of TextureSource data instances.
         * Used to store additional data images, such as normal maps or specular maps.
         *
         * @name Phaser.Textures.Texture#dataSource
         * @type {array}
         * @since 3.0.0
         */
        this.dataSource = [];

        /**
         * A key-value object pair associating the unique Frame keys with the Frames objects.
         *
         * @name Phaser.Textures.Texture#frames
         * @type {object}
         * @since 3.0.0
         */
        this.frames = {};

        /**
         * Any additional data that was set in the source JSON (if any),
         * or any extra data you'd like to store relating to this texture
         *
         * @name Phaser.Textures.Texture#customData
         * @type {object}
         * @since 3.0.0
         */
        this.customData = {};

        /**
         * The name of the first frame of the Texture.
         *
         * @name Phaser.Textures.Texture#firstFrame
         * @type {string}
         * @since 3.0.0
         */
        this.firstFrame = '__BASE';

        /**
         * The total number of Frames in this Texture, including the `__BASE` frame.
         *
         * A Texture will always contain at least 1 frame because every Texture contains a `__BASE` frame by default,
         * in addition to any extra frames that have been added to it, such as when parsing a Sprite Sheet or Texture Atlas.
         *
         * @name Phaser.Textures.Texture#frameTotal
         * @type {number}
         * @default 0
         * @since 3.0.0
         */
        this.frameTotal = 0;

        //  Load the Sources
        for (var i = 0; i < source.length; i++)
        {
            this.source.push(new TextureSource(this, source[i], width, height));
        }
    },

    /**
     * Adds a new Frame to this Texture.
     *
     * A Frame is a rectangular region of a TextureSource with a unique index or string-based key.
     *
     * The name given must be unique within this Texture. If it already exists, this method will return `null`.
     *
     * @method Phaser.Textures.Texture#add
     * @since 3.0.0
     *
     * @param {(number|string)} name - The name of this Frame. The name is unique within the Texture.
     * @param {number} sourceIndex - The index of the TextureSource that this Frame is a part of.
     * @param {number} x - The x coordinate of the top-left of this Frame.
     * @param {number} y - The y coordinate of the top-left of this Frame.
     * @param {number} width - The width of this Frame.
     * @param {number} height - The height of this Frame.
     *
     * @return {?Phaser.Textures.Frame} The Frame that was added to this Texture, or `null` if the given name already exists.
     */
    add: function (name, sourceIndex, x, y, width, height)
    {
        if (this.has(name))
        {
            return null;
        }

        var frame = new Frame(this, name, sourceIndex, x, y, width, height);

        this.frames[name] = frame;

        //  Set the first frame of the Texture (other than __BASE)
        //  This is used to ensure we don't spam the display with entire
        //  atlases of sprite sheets, but instead just the first frame of them
        //  should the dev incorrectly specify the frame index
        if (this.firstFrame === '__BASE')
        {
            this.firstFrame = name;
        }

        this.frameTotal++;

        return frame;
    },

    /**
     * Removes the given Frame from this Texture. The Frame is destroyed immediately.
     *
     * Any Game Objects using this Frame should stop using it _before_ you remove it,
     * as it does not happen automatically.
     *
     * @method Phaser.Textures.Texture#remove
     * @since 3.19.0
     *
     * @param {string} name - The key of the Frame to remove.
     *
     * @return {boolean} True if a Frame with the matching key was removed from this Texture.
     */
    remove: function (name)
    {
        if (this.has(name))
        {
            var frame = this.get(name);

            frame.destroy();

            delete this.frames[name];

            return true;
        }

        return false;
    },

    /**
     * Checks to see if a Frame matching the given key exists within this Texture.
     *
     * @method Phaser.Textures.Texture#has
     * @since 3.0.0
     *
     * @param {string} name - The key of the Frame to check for.
     *
     * @return {boolean} True if a Frame with the matching key exists in this Texture.
     */
    has: function (name)
    {
        return (this.frames[name]);
    },

    /**
     * Gets a Frame from this Texture based on either the key or the index of the Frame.
     *
     * In a Texture Atlas Frames are typically referenced by a key.
     * In a Sprite Sheet Frames are referenced by an index.
     * Passing no value for the name returns the base texture.
     *
     * @method Phaser.Textures.Texture#get
     * @since 3.0.0
     *
     * @param {(string|number)} [name] - The string-based name, or integer based index, of the Frame to get from this Texture.
     *
     * @return {Phaser.Textures.Frame} The Texture Frame.
     */
    get: function (name)
    {
        //  null, undefined, empty string, zero
        if (!name)
        {
            name = this.firstFrame;
        }

        var frame = this.frames[name];

        if (!frame)
        {
            console.warn(TEXTURE_MISSING_ERROR, this.key, name);

            frame = this.frames[this.firstFrame];
        }

        return frame;
    },

    /**
     * Takes the given TextureSource and returns the index of it within this Texture.
     * If it's not in this Texture, it returns -1.
     * Unless this Texture has multiple TextureSources, such as with a multi-atlas, this
     * method will always return zero or -1.
     *
     * @method Phaser.Textures.Texture#getTextureSourceIndex
     * @since 3.0.0
     *
     * @param {Phaser.Textures.TextureSource} source - The TextureSource to check.
     *
     * @return {number} The index of the TextureSource within this Texture, or -1 if not in this Texture.
     */
    getTextureSourceIndex: function (source)
    {
        for (var i = 0; i < this.source.length; i++)
        {
            if (this.source[i] === source)
            {
                return i;
            }
        }

        return -1;
    },

    /**
     * Returns an array of all the Frames in the given TextureSource.
     *
     * @method Phaser.Textures.Texture#getFramesFromTextureSource
     * @since 3.0.0
     *
     * @param {number} sourceIndex - The index of the TextureSource to get the Frames from.
     * @param {boolean} [includeBase=false] - Include the `__BASE` Frame in the output array?
     *
     * @return {Phaser.Textures.Frame[]} An array of Texture Frames.
     */
    getFramesFromTextureSource: function (sourceIndex, includeBase)
    {
        if (includeBase === undefined) { includeBase = false; }

        var out = [];

        for (var frameName in this.frames)
        {
            if (frameName === '__BASE' && !includeBase)
            {
                continue;
            }

            var frame = this.frames[frameName];

            if (frame.sourceIndex === sourceIndex)
            {
                out.push(frame);
            }
        }

        return out;
    },

    /**
     * Returns an array with all of the names of the Frames in this Texture.
     *
     * Useful if you want to randomly assign a Frame to a Game Object, as you can
     * pick a random element from the returned array.
     *
     * @method Phaser.Textures.Texture#getFrameNames
     * @since 3.0.0
     *
     * @param {boolean} [includeBase=false] - Include the `__BASE` Frame in the output array?
     *
     * @return {string[]} An array of all Frame names in this Texture.
     */
    getFrameNames: function (includeBase)
    {
        if (includeBase === undefined) { includeBase = false; }

        var out = Object.keys(this.frames);

        if (!includeBase)
        {
            var idx = out.indexOf('__BASE');

            if (idx !== -1)
            {
                out.splice(idx, 1);
            }
        }

        return out;
    },

    /**
     * Given a Frame name, return the source image it uses to render with.
     *
     * This will return the actual DOM Image or Canvas element.
     *
     * @method Phaser.Textures.Texture#getSourceImage
     * @since 3.0.0
     *
     * @param {(string|number)} [name] - The string-based name, or integer based index, of the Frame to get from this Texture.
     *
     * @return {(HTMLImageElement|HTMLCanvasElement|Phaser.GameObjects.RenderTexture)} The DOM Image, Canvas Element or Render Texture.
     */
    getSourceImage: function (name)
    {
        if (name === undefined || name === null || this.frameTotal === 1)
        {
            name = '__BASE';
        }

        var frame = this.frames[name];

        if (frame)
        {
            return frame.source.image;
        }
        else
        {
            console.warn(TEXTURE_MISSING_ERROR, this.key, name);

            return this.frames['__BASE'].source.image;
        }
    },

    /**
     * Given a Frame name, return the data source image it uses to render with.
     * You can use this to get the normal map for an image for example.
     *
     * This will return the actual DOM Image.
     *
     * @method Phaser.Textures.Texture#getDataSourceImage
     * @since 3.7.0
     *
     * @param {(string|number)} [name] - The string-based name, or integer based index, of the Frame to get from this Texture.
     *
     * @return {(HTMLImageElement|HTMLCanvasElement)} The DOM Image or Canvas Element.
     */
    getDataSourceImage: function (name)
    {
        if (name === undefined || name === null || this.frameTotal === 1)
        {
            name = '__BASE';
        }

        var frame = this.frames[name];
        var idx;

        if (!frame)
        {
            console.warn(TEXTURE_MISSING_ERROR, this.key, name);

            idx = this.frames['__BASE'].sourceIndex;
        }
        else
        {
            idx = frame.sourceIndex;
        }

        return this.dataSource[idx].image;
    },

    /**
     * Adds a data source image to this Texture.
     *
     * An example of a data source image would be a normal map, where all of the Frames for this Texture
     * equally apply to the normal map.
     *
     * @method Phaser.Textures.Texture#setDataSource
     * @since 3.0.0
     *
     * @param {(HTMLImageElement|HTMLCanvasElement|HTMLImageElement[]|HTMLCanvasElement[])} data - The source image.
     */
    setDataSource: function (data)
    {
        if (!Array.isArray(data))
        {
            data = [ data ];
        }

        for (var i = 0; i < data.length; i++)
        {
            var source = this.source[i];

            this.dataSource.push(new TextureSource(this, data[i], source.width, source.height));
        }
    },

    /**
     * Sets the Filter Mode for this Texture.
     *
     * The mode can be either Linear, the default, or Nearest.
     *
     * For pixel-art you should use Nearest.
     *
     * The mode applies to the entire Texture, not just a specific Frame of it.
     *
     * @method Phaser.Textures.Texture#setFilter
     * @since 3.0.0
     *
     * @param {Phaser.Textures.FilterMode} filterMode - The Filter Mode.
     */
    setFilter: function (filterMode)
    {
        var i;

        for (i = 0; i < this.source.length; i++)
        {
            this.source[i].setFilter(filterMode);
        }

        for (i = 0; i < this.dataSource.length; i++)
        {
            this.dataSource[i].setFilter(filterMode);
        }
    },

    /**
     * Destroys this Texture and releases references to its sources and frames.
     *
     * @method Phaser.Textures.Texture#destroy
     * @since 3.0.0
     */
    destroy: function ()
    {
        var i;
        var source = this.source;
        var dataSource = this.dataSource;

        for (i = 0; i < source.length; i++)
        {
            if (source[i])
            {
                source[i].destroy();
            }
        }

        for (i = 0; i < dataSource.length; i++)
        {
            if (dataSource[i])
            {
                dataSource[i].destroy();
            }
        }

        for (var frameName in this.frames)
        {
            var frame = this.frames[frameName];

            if (frame)
            {
                frame.destroy();
            }
        }

        this.source = [];
        this.dataSource = [];
        this.frames = {};

        this.manager.removeKey(this.key);

        var renderer = this.manager.game.renderer;

        if (renderer && renderer.type === CONST.WEBGL)
        {
            renderer.resetTextures(true);
        }

        this.manager = null;
    }

});

module.exports = Texture;
<|endoftext|>