<commit_before>import _ from 'lodash';
import {
  UPDATE_HEATMAP_TILES,
  ADD_REFERENCE_TILE,
  REMOVE_REFERENCE_TILE,
  ADD_HEATMAP_LAYER,
  REMOVE_HEATMAP_LAYER,
  INIT_HEATMAP_LAYERS,
  UPDATE_HEATMAP_LAYER_TEMPORAL_EXTENTS_LOADED_INDICES,
  HIGHLIGHT_VESSELS
} from 'actions';
import {
  getTilePelagosPromises,
  getCleanVectorArrays,
  groupData,
  addWorldCoordinates,
  getTilePlaybackData,
  selectVesselsAt
} from 'actions/helpers/heatmapTileData';
import { LAYER_TYPES, LOADERS } from 'constants';
import { clearVesselInfo, addVessel, hideVesselsInfoPanel } from 'actions/vesselInfo';
import { trackMapClicked } from 'actions/analytics';
import { addLoader, removeLoader, zoomIntoVesselCenter } from 'actions/map';

/**
 * getTemporalExtentsVisibleIndices - Compares timebar outer extent with temporal extents present on the layer header
 * @param  {array} currentOuterExtent Current timebar outer extent
 * @param  {array} temporalExtents    Temporal extents present on the layer's header (an array of extent arrays)
 * @return {array}                    Indices of the layer's temporal extents that should be visible
 */
function getTemporalExtentsVisibleIndices(currentOuterExtent, temporalExtents) {
  const currentExtentStart = currentOuterExtent[0].getTime();
  const currentExtentEnd = currentOuterExtent[1].getTime();
  const indices = [];
  temporalExtents.forEach((temporalExtent, index) => {
    const temporalExtentStart = temporalExtent[0];
    const temporalExtentEnd = temporalExtent[1];
    if (temporalExtentEnd >= currentExtentStart && temporalExtentStart <= currentExtentEnd) {
      indices.push(index);
    }
  });
  return indices;
}

/**
 * initHeatmapLayers - Creates the base reducer state, extracting heatmap layers from all layers
 */
export function initHeatmapLayers() {
  return (dispatch, getState) => {
    const currentOuterExtent = getState().filters.timelineOuterExtent;
    const workspaceLayers = getState().layers.workspaceLayers;
    const heatmapLayers = {};

    workspaceLayers.forEach((workspaceLayer) => {
      if (workspaceLayer.type === LAYER_TYPES.Heatmap && workspaceLayer.added === true) {
        heatmapLayers[workspaceLayer.id] = {
          url: workspaceLayer.url,
          tiles: [],
          // initially attach which of the temporal extents indices are visible with initial outerExtent
          visibleTemporalExtentsIndices: getTemporalExtentsVisibleIndices(currentOuterExtent, workspaceLayer.header.temporalExtents)
        };
      }
    });

    dispatch({
      type: INIT_HEATMAP_LAYERS,
      payload: heatmapLayers
    });
  };
}

/**
 * loadLayerTile - loads an heatmap tile.
 *
 * @param  {object} tileCoordinates        tile coordinates from reference tile
 * @param  {string} layerUrl             the base layer url
 * @param  {string} token                the user's token
 * @param  {array} temporalExtents       all of the layer's header temporal extents
 * @param  {array} temporalExtentsIndices which of the temporal extents from  temporalExtents should be loaded
 * @return {Promise}                     a Promise that will be resolved when tile is loaded
 */
function loadLayerTile(tileCoordinates, layerUrl, token, temporalExtents, temporalExtentsIndices) {
  // const tileCoordinates = referenceTile.tileCoordinates;
  const pelagosPromises = getTilePelagosPromises(layerUrl, token, temporalExtents, { tileCoordinates, temporalExtentsIndices });
  const allLayerPromises = Promise.all(pelagosPromises);

  const layerTilePromise = new Promise((resolve) => {
    allLayerPromises.then((rawTileData) => {
      resolve(rawTileData);
    });
  });

  return layerTilePromise;
}

/**
 * parseLayerTile - parses an heatmap tile to a playback-ready format.
 *
 * @param  {object} tileCoordinates      tile coordinates from reference tile
 * @param  {array} columns               names of the columns present in the raw tiles that need to be included in the final playback data
 * @param  {object} map                  a reference to the Google Map object. This is required to access projection data.
 * @param  {Object} rawTileData
 * @param  {array} prevPlaybackData      (optional) in case some time extent was already loaded for this tile, append to this data
 * @return {Object}                      playback-ready merged data
 */
function parseLayerTile(tileCoordinates, columns, map, rawTileData, prevPlaybackData) {
  // console.time('test')
  const cleanVectorArrays = getCleanVectorArrays(rawTileData);
  const groupedData = groupData(cleanVectorArrays, columns);
  const vectorArray = addWorldCoordinates(groupedData, map);
  const data = getTilePlaybackData(
    tileCoordinates.zoom,
    vectorArray,
    columns,
    prevPlaybackData
  );
  return data;
  // console.timeEnd('test');
}

/**
 * getTiles - loads a bunch of heatmap tiles
 * @param  {array} layerIds                 list of layer Ids that need to be loaded for this/these tiles
 * @param  {array} referenceTiles           list of reference tiles (tile data regardless of layer) that need to be loaded
 * @param  {object} newTemporalExtentsToLoad (optional) a dict (layerId is the key) of temporal extents indices that should be
 * appended to existing data
 */
function getTiles(layerIds, referenceTiles, newTemporalExtentsToLoad) {
  return (dispatch, getState) => {
    const loaderId = LOADERS.HEATMAP_TILES + new Date().getTime();
    dispatch(addLoader(loaderId));
    const layers = getState().heatmap.heatmapLayers;
    const token = getState().user.token;
    const map = getState().map.googleMaps;
    const allPromises = [];

    layerIds.forEach((layerId) => {
      const workspaceLayers = getState().layers.workspaceLayers;
      const workspaceLayer = workspaceLayers.find(layer => layer.id === layerId);
      const layerHeader = workspaceLayer.header;
      if (!layerHeader) {
        console.warn('no header has been set on this heatmap layer');
      }
      referenceTiles.forEach((referenceTile) => {
        // check if tile does not already exist first
        let tile = layers[layerId].tiles.find(t => t.uid === referenceTile.uid);
        if (!tile) {
          tile = {
            uid: referenceTile.uid,
            canvas: referenceTile.canvas,
            temporalExtentsIndicesLoaded: []
          };
          layers[layerId].tiles.push(tile);
        }

        const queriedTemporalExtentsIndices = (newTemporalExtentsToLoad === undefined)
          ? layers[layerId].visibleTemporalExtentsIndices
          : newTemporalExtentsToLoad[layerId];

        const temporalExtentsIndicesToLoad = _.difference(queriedTemporalExtentsIndices, tile.temporalExtentsIndicesLoaded);

        const tilePromise = loadLayerTile(
          referenceTile.tileCoordinates,
          // TODO use URL from header
          layers[layerId].url,
          token,
          layerHeader.temporalExtents,
          temporalExtentsIndicesToLoad
        );

        allPromises.push(tilePromise);

        tilePromise.then((rawTileData) => {
          tile.temporalExtentsIndicesLoaded = _.uniq(tile.temporalExtentsIndicesLoaded.concat(temporalExtentsIndicesToLoad));
          tile.data = parseLayerTile(
            referenceTile.tileCoordinates,
            Object.keys(layerHeader.colsByName),
            map,
            rawTileData,
            tile.data
          );
          dispatch({
            type: UPDATE_HEATMAP_TILES, payload: layers
          });
        });
      });
    });

    Promise.all(allPromises).then(() => {
      dispatch(removeLoader(loaderId));
    });
  };
}

/**
 * getTile - This action is emitted when a new tile is queried from panning or zooming the map
 * This will load a tile for all currently visible heatmap layers
 *
 * @param  {number} uid             the reference tile uid
 * @param  {object} tileCoordinates the reference tiles coordinates
 * @param  {object} canvas          the tiles DOM canvas
 */
export function getTile(uid, tileCoordinates, canvas) {
  return (dispatch, getState) => {
    const referenceTile = {
      uid,
      tileCoordinates,
      canvas
    };

    dispatch({
      type: ADD_REFERENCE_TILE,
      payload: referenceTile
    });

    const visibleHeatmapLayers = getState().layers.workspaceLayers.filter(workspaceLayer =>
      workspaceLayer.type === LAYER_TYPES.Heatmap && workspaceLayer.added === true && workspaceLayer.visible === true)
      .map(layer => layer.id);

    dispatch(getTiles(visibleHeatmapLayers, [referenceTile]));
  };
}

/**
* releaseTile - This action is emitted when an existing tile is removed from panning or zooming the map
 * @param  {type} uid the reference tile uid
 */
export function releaseTile(uid) {
  return (dispatch, getState) => {
    dispatch({
      type: REMOVE_REFERENCE_TILE,
      payload: uid
    });

    const layers = getState().heatmap.heatmapLayers;
    Object.keys(layers).forEach((layerId) => {
      const layer = layers[layerId];
      const tiles = layer.tiles;
      const releasedTileIndex = tiles.findIndex(tile => tile.uid === uid);
      if (releasedTileIndex === -1) {
        return;
      }
      tiles.splice(releasedTileIndex, 1);
    });
    dispatch({
      type: UPDATE_HEATMAP_TILES, payload: layers
    });
  };
}

export function loadAllTilesForLayer(layerId) {
  return (dispatch, getState) => {
    //                current layer, all reference tiles
    dispatch(getTiles([layerId], getState().heatmap.referenceTiles));
  };
}

export function addHeatmapLayerFromLibrary(layerId, url) {
  return (dispatch) => {
    dispatch({
      type: ADD_HEATMAP_LAYER,
      payload: {
        layerId,
        url
      }
    });

    dispatch(loadAllTilesForLayer(layerId));
  };
}

export function removeHeatmapLayerFromLibrary(id) {
  return (dispatch) => {
    dispatch({
      type: REMOVE_HEATMAP_LAYER,
      payload: {
        id
      }
    });
  };
}

/**
 * loadTilesExtraTimeRange - when outer time extent changes, checks if more tiles needs to be loaded
 * by comparing the outer time range with the temporalExtent already loaded on each layer
 */
export function loadTilesExtraTimeRange() {
  return (dispatch, getState) => {
    const currentOuterExtent = getState().filters.timelineOuterExtent;
    const heatmapLayers = getState().heatmap.heatmapLayers;
    const layersToUpdate = {};
    Object.keys(heatmapLayers).forEach((layerId) => {
      const workspaceLayer = getState().layers.workspaceLayers.find(layer => layer.id === layerId);
      const heatmapLayer = heatmapLayers[layerId];
      const oldVisibleTemporalExtents = heatmapLayer.visibleTemporalExtentsIndices;
      const newVisibleTemporalExtents = getTemporalExtentsVisibleIndices(currentOuterExtent, workspaceLayer.header.temporalExtents);
      const diff = _.difference(newVisibleTemporalExtents, oldVisibleTemporalExtents);
      if (diff.length) {
        // add new loaded indices to heatmap layer if applicable
        layersToUpdate[layerId] = diff;
        dispatch({
          type: UPDATE_HEATMAP_LAYER_TEMPORAL_EXTENTS_LOADED_INDICES,
          payload: {
            layerId,
            diff
          }
        });
      }
    });

    // getTiles with indices diff
    if (Object.keys(layersToUpdate).length) {
      dispatch(getTiles(Object.keys(layersToUpdate), getState().heatmap.referenceTiles, layersToUpdate));
    }
  };
}

const _getCurrentFlagsForLayer = (state, layerId) => {
  if (layerId === undefined) {
    return undefined;
  }
  const flags = state.filters.flagsLayers[layerId].map(flagLayer => flagLayer.flag);
  return (flags.length === 1 && flags[0] === 'ALL') ? undefined : flags;
};

const _queryHeatmap = (state, tileQuery) => {
  const layers = state.heatmap.heatmapLayers;
  const timelineExtent = state.filters.timelineInnerExtentIndexes;
  const startIndex = timelineExtent[0];
  const endIndex = timelineExtent[1];
  const layersVessels = [];
  Object.keys(layers).forEach((layerId) => {
    const layer = layers[layerId];
    const queriedTile = layer.tiles.find(tile => tile.uid === tileQuery.uid);
    const currentFlags = _getCurrentFlagsForLayer(state, layerId);
    if (queriedTile !== undefined && queriedTile.data !== undefined) {
      layersVessels.push({
        layerId,
        vessels: selectVesselsAt(queriedTile.data, state.map.zoom, tileQuery.worldX, tileQuery.worldY, startIndex, endIndex, currentFlags)
      });
    }
  });

  const layersVesselsResult = layersVessels.filter(layerVessels => layerVessels.vessels.length > 0);

  // it's a cluster because of aggregation on the server side
  let isCluster;
  // its a cluster because or multiple vessels under mouse
  let isMouseCluster;
  let isEmpty;
  let layerId;
  let seriesUids;

  if (layersVesselsResult.length === 0) {
    isEmpty = true;
  } else if (layersVesselsResult.length > 1) {
    // if there are points over multiple layers, consider this a cluster
    isCluster = true;
  } else {
    // we can get multiple points with similar series and seriesgroup, in which case
    // we should treat that as a successful vessel query, not a cluster
    layerId = layersVesselsResult[0].layerId;
    const vessels = layersVesselsResult[0].vessels;

    if (vessels.length === 0) {
      isEmpty = true;
    } else {
      // look up for any non-negative seriesgroup (not clusters on the server side)
      const nonClusteredVessels = vessels.filter(v => v.seriesgroup > 0);

      if (nonClusteredVessels.length) {
        seriesUids = _.uniq(nonClusteredVessels.map(v => v.seriesUid));
        isMouseCluster = seriesUids.length > 1;
      } else {
        isCluster = true;
      }
    }
  }

  return { isEmpty, isCluster, isMouseCluster, seriesUids, layerId };
};

export function highlightVesselFromHeatmap(tileQuery, latLng) {
  return (dispatch, getState) => {
    const state = getState();
    const { layerId, isEmpty, isCluster, isMouseCluster, seriesUids } = _queryHeatmap(state, tileQuery);

    dispatch({
      type: HIGHLIGHT_VESSELS,
      payload: {
        layerId,
        isEmpty,
        clickableCluster: isCluster === true || isMouseCluster === true,
        highlightableCluster: isCluster !== true,
        seriesUids,
        latLng,
        currentFlags: _getCurrentFlagsForLayer(state, layerId)
      }
    });
  };
}

export function getVesselFromHeatmap(tileQuery, latLng) {
  return (dispatch, getState) => {
    const state = getState();

    if (state.user.userPermissions !== null && state.user.userPermissions.indexOf('selectVessel') === -1) {
      return;
    }

    const { isEmpty, isCluster, isMouseCluster, layerId, seriesUids } = _queryHeatmap(state, tileQuery);

    dispatch(clearVesselInfo());

    if (isEmpty === true) {
      // nothing to see here
    } else if (isCluster === true || isMouseCluster === true) {
      dispatch(trackMapClicked(latLng.lat(), latLng.lng(), 'cluster'));
      dispatch(hideVesselsInfoPanel());
      dispatch(zoomIntoVesselCenter(latLng));
      dispatch({
        type: HIGHLIGHT_VESSELS,
        payload: { isEmpty: true }
      });
    } else {
      dispatch(trackMapClicked(latLng.lat(), latLng.lng(), 'vessel'));
      const seriesUid = seriesUids[0];
      const seriesgroup = parseInt(seriesUid.split('-')[0], 10);
      const series = parseInt(seriesUid.split('-')[1], 10);

      dispatch(addVessel(layerId, seriesgroup, series));
    }
  };
}
<commit_msg>do not hover over a disabled heatmap layer<commit_after>import _ from 'lodash';
import {
  UPDATE_HEATMAP_TILES,
  ADD_REFERENCE_TILE,
  REMOVE_REFERENCE_TILE,
  ADD_HEATMAP_LAYER,
  REMOVE_HEATMAP_LAYER,
  INIT_HEATMAP_LAYERS,
  UPDATE_HEATMAP_LAYER_TEMPORAL_EXTENTS_LOADED_INDICES,
  HIGHLIGHT_VESSELS
} from 'actions';
import {
  getTilePelagosPromises,
  getCleanVectorArrays,
  groupData,
  addWorldCoordinates,
  getTilePlaybackData,
  selectVesselsAt
} from 'actions/helpers/heatmapTileData';
import { LAYER_TYPES, LOADERS } from 'constants';
import { clearVesselInfo, addVessel, hideVesselsInfoPanel } from 'actions/vesselInfo';
import { trackMapClicked } from 'actions/analytics';
import { addLoader, removeLoader, zoomIntoVesselCenter } from 'actions/map';

/**
 * getTemporalExtentsVisibleIndices - Compares timebar outer extent with temporal extents present on the layer header
 * @param  {array} currentOuterExtent Current timebar outer extent
 * @param  {array} temporalExtents    Temporal extents present on the layer's header (an array of extent arrays)
 * @return {array}                    Indices of the layer's temporal extents that should be visible
 */
function getTemporalExtentsVisibleIndices(currentOuterExtent, temporalExtents) {
  const currentExtentStart = currentOuterExtent[0].getTime();
  const currentExtentEnd = currentOuterExtent[1].getTime();
  const indices = [];
  temporalExtents.forEach((temporalExtent, index) => {
    const temporalExtentStart = temporalExtent[0];
    const temporalExtentEnd = temporalExtent[1];
    if (temporalExtentEnd >= currentExtentStart && temporalExtentStart <= currentExtentEnd) {
      indices.push(index);
    }
  });
  return indices;
}

/**
 * initHeatmapLayers - Creates the base reducer state, extracting heatmap layers from all layers
 */
export function initHeatmapLayers() {
  return (dispatch, getState) => {
    const currentOuterExtent = getState().filters.timelineOuterExtent;
    const workspaceLayers = getState().layers.workspaceLayers;
    const heatmapLayers = {};

    workspaceLayers.forEach((workspaceLayer) => {
      if (workspaceLayer.type === LAYER_TYPES.Heatmap && workspaceLayer.added === true) {
        heatmapLayers[workspaceLayer.id] = {
          url: workspaceLayer.url,
          tiles: [],
          // initially attach which of the temporal extents indices are visible with initial outerExtent
          visibleTemporalExtentsIndices: getTemporalExtentsVisibleIndices(currentOuterExtent, workspaceLayer.header.temporalExtents)
        };
      }
    });

    dispatch({
      type: INIT_HEATMAP_LAYERS,
      payload: heatmapLayers
    });
  };
}

/**
 * loadLayerTile - loads an heatmap tile.
 *
 * @param  {object} tileCoordinates        tile coordinates from reference tile
 * @param  {string} layerUrl             the base layer url
 * @param  {string} token                the user's token
 * @param  {array} temporalExtents       all of the layer's header temporal extents
 * @param  {array} temporalExtentsIndices which of the temporal extents from  temporalExtents should be loaded
 * @return {Promise}                     a Promise that will be resolved when tile is loaded
 */
function loadLayerTile(tileCoordinates, layerUrl, token, temporalExtents, temporalExtentsIndices) {
  // const tileCoordinates = referenceTile.tileCoordinates;
  const pelagosPromises = getTilePelagosPromises(layerUrl, token, temporalExtents, { tileCoordinates, temporalExtentsIndices });
  const allLayerPromises = Promise.all(pelagosPromises);

  const layerTilePromise = new Promise((resolve) => {
    allLayerPromises.then((rawTileData) => {
      resolve(rawTileData);
    });
  });

  return layerTilePromise;
}

/**
 * parseLayerTile - parses an heatmap tile to a playback-ready format.
 *
 * @param  {object} tileCoordinates      tile coordinates from reference tile
 * @param  {array} columns               names of the columns present in the raw tiles that need to be included in the final playback data
 * @param  {object} map                  a reference to the Google Map object. This is required to access projection data.
 * @param  {Object} rawTileData
 * @param  {array} prevPlaybackData      (optional) in case some time extent was already loaded for this tile, append to this data
 * @return {Object}                      playback-ready merged data
 */
function parseLayerTile(tileCoordinates, columns, map, rawTileData, prevPlaybackData) {
  // console.time('test')
  const cleanVectorArrays = getCleanVectorArrays(rawTileData);
  const groupedData = groupData(cleanVectorArrays, columns);
  const vectorArray = addWorldCoordinates(groupedData, map);
  const data = getTilePlaybackData(
    tileCoordinates.zoom,
    vectorArray,
    columns,
    prevPlaybackData
  );
  return data;
  // console.timeEnd('test');
}

/**
 * getTiles - loads a bunch of heatmap tiles
 * @param  {array} layerIds                 list of layer Ids that need to be loaded for this/these tiles
 * @param  {array} referenceTiles           list of reference tiles (tile data regardless of layer) that need to be loaded
 * @param  {object} newTemporalExtentsToLoad (optional) a dict (layerId is the key) of temporal extents indices that should be
 * appended to existing data
 */
function getTiles(layerIds, referenceTiles, newTemporalExtentsToLoad) {
  return (dispatch, getState) => {
    const loaderId = LOADERS.HEATMAP_TILES + new Date().getTime();
    dispatch(addLoader(loaderId));
    const layers = getState().heatmap.heatmapLayers;
    const token = getState().user.token;
    const map = getState().map.googleMaps;
    const allPromises = [];

    layerIds.forEach((layerId) => {
      const workspaceLayers = getState().layers.workspaceLayers;
      const workspaceLayer = workspaceLayers.find(layer => layer.id === layerId);
      const layerHeader = workspaceLayer.header;
      if (!layerHeader) {
        console.warn('no header has been set on this heatmap layer');
      }
      referenceTiles.forEach((referenceTile) => {
        // check if tile does not already exist first
        let tile = layers[layerId].tiles.find(t => t.uid === referenceTile.uid);
        if (!tile) {
          tile = {
            uid: referenceTile.uid,
            canvas: referenceTile.canvas,
            temporalExtentsIndicesLoaded: []
          };
          layers[layerId].tiles.push(tile);
        }

        const queriedTemporalExtentsIndices = (newTemporalExtentsToLoad === undefined)
          ? layers[layerId].visibleTemporalExtentsIndices
          : newTemporalExtentsToLoad[layerId];

        const temporalExtentsIndicesToLoad = _.difference(queriedTemporalExtentsIndices, tile.temporalExtentsIndicesLoaded);

        const tilePromise = loadLayerTile(
          referenceTile.tileCoordinates,
          // TODO use URL from header
          layers[layerId].url,
          token,
          layerHeader.temporalExtents,
          temporalExtentsIndicesToLoad
        );

        allPromises.push(tilePromise);

        tilePromise.then((rawTileData) => {
          tile.temporalExtentsIndicesLoaded = _.uniq(tile.temporalExtentsIndicesLoaded.concat(temporalExtentsIndicesToLoad));
          tile.data = parseLayerTile(
            referenceTile.tileCoordinates,
            Object.keys(layerHeader.colsByName),
            map,
            rawTileData,
            tile.data
          );
          dispatch({
            type: UPDATE_HEATMAP_TILES, payload: layers
          });
        });
      });
    });

    Promise.all(allPromises).then(() => {
      dispatch(removeLoader(loaderId));
    });
  };
}

/**
 * getTile - This action is emitted when a new tile is queried from panning or zooming the map
 * This will load a tile for all currently visible heatmap layers
 *
 * @param  {number} uid             the reference tile uid
 * @param  {object} tileCoordinates the reference tiles coordinates
 * @param  {object} canvas          the tiles DOM canvas
 */
export function getTile(uid, tileCoordinates, canvas) {
  return (dispatch, getState) => {
    const referenceTile = {
      uid,
      tileCoordinates,
      canvas
    };

    dispatch({
      type: ADD_REFERENCE_TILE,
      payload: referenceTile
    });

    const visibleHeatmapLayers = getState().layers.workspaceLayers.filter(workspaceLayer =>
      workspaceLayer.type === LAYER_TYPES.Heatmap && workspaceLayer.added === true && workspaceLayer.visible === true)
      .map(layer => layer.id);

    dispatch(getTiles(visibleHeatmapLayers, [referenceTile]));
  };
}

/**
* releaseTile - This action is emitted when an existing tile is removed from panning or zooming the map
 * @param  {type} uid the reference tile uid
 */
export function releaseTile(uid) {
  return (dispatch, getState) => {
    dispatch({
      type: REMOVE_REFERENCE_TILE,
      payload: uid
    });

    const layers = getState().heatmap.heatmapLayers;
    Object.keys(layers).forEach((layerId) => {
      const layer = layers[layerId];
      const tiles = layer.tiles;
      const releasedTileIndex = tiles.findIndex(tile => tile.uid === uid);
      if (releasedTileIndex === -1) {
        return;
      }
      tiles.splice(releasedTileIndex, 1);
    });
    dispatch({
      type: UPDATE_HEATMAP_TILES, payload: layers
    });
  };
}

export function loadAllTilesForLayer(layerId) {
  return (dispatch, getState) => {
    //                current layer, all reference tiles
    dispatch(getTiles([layerId], getState().heatmap.referenceTiles));
  };
}

export function addHeatmapLayerFromLibrary(layerId, url) {
  return (dispatch) => {
    dispatch({
      type: ADD_HEATMAP_LAYER,
      payload: {
        layerId,
        url
      }
    });

    dispatch(loadAllTilesForLayer(layerId));
  };
}

export function removeHeatmapLayerFromLibrary(id) {
  return (dispatch) => {
    dispatch({
      type: REMOVE_HEATMAP_LAYER,
      payload: {
        id
      }
    });
  };
}

/**
 * loadTilesExtraTimeRange - when outer time extent changes, checks if more tiles needs to be loaded
 * by comparing the outer time range with the temporalExtent already loaded on each layer
 */
export function loadTilesExtraTimeRange() {
  return (dispatch, getState) => {
    const currentOuterExtent = getState().filters.timelineOuterExtent;
    const heatmapLayers = getState().heatmap.heatmapLayers;
    const layersToUpdate = {};
    Object.keys(heatmapLayers).forEach((layerId) => {
      const workspaceLayer = getState().layers.workspaceLayers.find(layer => layer.id === layerId);
      const heatmapLayer = heatmapLayers[layerId];
      const oldVisibleTemporalExtents = heatmapLayer.visibleTemporalExtentsIndices;
      const newVisibleTemporalExtents = getTemporalExtentsVisibleIndices(currentOuterExtent, workspaceLayer.header.temporalExtents);
      const diff = _.difference(newVisibleTemporalExtents, oldVisibleTemporalExtents);
      if (diff.length) {
        // add new loaded indices to heatmap layer if applicable
        layersToUpdate[layerId] = diff;
        dispatch({
          type: UPDATE_HEATMAP_LAYER_TEMPORAL_EXTENTS_LOADED_INDICES,
          payload: {
            layerId,
            diff
          }
        });
      }
    });

    // getTiles with indices diff
    if (Object.keys(layersToUpdate).length) {
      dispatch(getTiles(Object.keys(layersToUpdate), getState().heatmap.referenceTiles, layersToUpdate));
    }
  };
}

const _getCurrentFlagsForLayer = (state, layerId) => {
  if (layerId === undefined) {
    return undefined;
  }
  const flags = state.filters.flagsLayers[layerId].map(flagLayer => flagLayer.flag);
  return (flags.length === 1 && flags[0] === 'ALL') ? undefined : flags;
};

const _queryHeatmap = (state, tileQuery) => {
  const layers = state.heatmap.heatmapLayers;
  const timelineExtent = state.filters.timelineInnerExtentIndexes;
  const startIndex = timelineExtent[0];
  const endIndex = timelineExtent[1];
  const layersVessels = [];

  Object.keys(layers).forEach((layerId) => {
    const workspaceLayer = state.layers.workspaceLayers.find(l => l.id === layerId);
    if (workspaceLayer.added === true && workspaceLayer.visible === true) {
      const layer = layers[layerId];
      const queriedTile = layer.tiles.find(tile => tile.uid === tileQuery.uid);
      const currentFlags = _getCurrentFlagsForLayer(state, layerId);
      if (queriedTile !== undefined && queriedTile.data !== undefined) {
        layersVessels.push({
          layerId,
          vessels: selectVesselsAt(queriedTile.data, state.map.zoom, tileQuery.worldX, tileQuery.worldY, startIndex, endIndex, currentFlags)
        });
      }
    }
  });

  const layersVesselsResult = layersVessels.filter(layerVessels => layerVessels.vessels.length > 0);

  // it's a cluster because of aggregation on the server side
  let isCluster;
  // its a cluster because or multiple vessels under mouse
  let isMouseCluster;
  let isEmpty;
  let layerId;
  let seriesUids;

  if (layersVesselsResult.length === 0) {
    isEmpty = true;
  } else if (layersVesselsResult.length > 1) {
    // if there are points over multiple layers, consider this a cluster
    isCluster = true;
  } else {
    // we can get multiple points with similar series and seriesgroup, in which case
    // we should treat that as a successful vessel query, not a cluster
    layerId = layersVesselsResult[0].layerId;
    const vessels = layersVesselsResult[0].vessels;

    if (vessels.length === 0) {
      isEmpty = true;
    } else {
      // look up for any non-negative seriesgroup (not clusters on the server side)
      const nonClusteredVessels = vessels.filter(v => v.seriesgroup > 0);

      if (nonClusteredVessels.length) {
        seriesUids = _.uniq(nonClusteredVessels.map(v => v.seriesUid));
        isMouseCluster = seriesUids.length > 1;
      } else {
        isCluster = true;
      }
    }
  }

  return { isEmpty, isCluster, isMouseCluster, seriesUids, layerId };
};

export function highlightVesselFromHeatmap(tileQuery, latLng) {
  return (dispatch, getState) => {
    const state = getState();
    const { layerId, isEmpty, isCluster, isMouseCluster, seriesUids } = _queryHeatmap(state, tileQuery);

    dispatch({
      type: HIGHLIGHT_VESSELS,
      payload: {
        layerId,
        isEmpty,
        clickableCluster: isCluster === true || isMouseCluster === true,
        highlightableCluster: isCluster !== true,
        seriesUids,
        latLng,
        currentFlags: _getCurrentFlagsForLayer(state, layerId)
      }
    });
  };
}

export function getVesselFromHeatmap(tileQuery, latLng) {
  return (dispatch, getState) => {
    const state = getState();

    if (state.user.userPermissions !== null && state.user.userPermissions.indexOf('selectVessel') === -1) {
      return;
    }

    const { isEmpty, isCluster, isMouseCluster, layerId, seriesUids } = _queryHeatmap(state, tileQuery);

    dispatch(clearVesselInfo());

    if (isEmpty === true) {
      // nothing to see here
    } else if (isCluster === true || isMouseCluster === true) {
      dispatch(trackMapClicked(latLng.lat(), latLng.lng(), 'cluster'));
      dispatch(hideVesselsInfoPanel());
      dispatch(zoomIntoVesselCenter(latLng));
      dispatch({
        type: HIGHLIGHT_VESSELS,
        payload: { isEmpty: true }
      });
    } else {
      dispatch(trackMapClicked(latLng.lat(), latLng.lng(), 'vessel'));
      const seriesUid = seriesUids[0];
      const seriesgroup = parseInt(seriesUid.split('-')[0], 10);
      const series = parseInt(seriesUid.split('-')[1], 10);

      dispatch(addVessel(layerId, seriesgroup, series));
    }
  };
}
<|endoftext|>


<commit_before>import {IframeNode, IframeCommentNode} from '../utils/iframe'
import Handlebars from 'handlebars'
import math from '../../../../cli/'
import translate from '../../../../cli/'

Handlebars.registerHelper('math', math) // HandlebarsJS unique text helper
Handlebars.registerHelper('i18nAbe', translate) // HandlebarsJS unique text helper

export default class EditorUtils {

  static checkAttribute() {
    let formAbes = document.querySelectorAll('.form-abe')

    Array.prototype.forEach.call(formAbes, (formAbe) => {
      var hide = IframeNode('#page-template', '[data-if-empty-clear="' + formAbe.getAttribute('data-id') + '"]')[0]
      if(typeof hide !== 'undefined' && hide !== null) {
        if (formAbe.value === '') {
          hide.style.display = 'none'
        }else {
          hide.style.display = ''
        }
      }
    })
  }

  static scrollToInputElement(target) {
    var visible = target.getAttribute('data-visible')
    if (visible === 'false' || visible === false) {
      return
    }
    var dataLink = target.getAttribute('data-id-link')
    var id = target.getAttribute('data-id')
    var nodes = IframeNode('#page-template', '[data-abe-' + id + ']')

    if(typeof nodes === 'undefined' || nodes === null || nodes.length === 0) {
      var nodesComment = [].slice.call(IframeCommentNode('#page-template', id.split('[')[0]))
      if(typeof nodesComment !== 'undefined' && nodesComment !== null
        && typeof nodesComment.textContent !== 'undefined' && nodesComment.textContent !== null) {
        var blockHtml = unescape(nodesComment.textContent.replace(/\[\[([\S\s]*?)\]\]/, ''))

        var newBlock = document.createElement('abe')
        newBlock.innerHTML = blockHtml

        var childs = [].slice.call(newBlock.childNodes)
        Array.prototype.forEach.call(childs, (child) => {
          nodesComment.parentNode.insertBefore(child, nodesComment)
        })
      }else if(typeof nodesComment !== 'undefined' && nodesComment !== null) {
        Array.prototype.forEach.call(nodesComment, (nodeComment) => {
          if(typeof nodeComment.parentNode.offsetParent !== 'undefined' && nodeComment.parentNode.offsetParent !== null) {
            var bounds = nodeComment.parentNode.getBoundingClientRect()
            var w = document.getElementById('page-template').contentWindow
            w.scroll(0, w.scrollY + bounds.top + (bounds.height * 0.5) - (window.innerHeight * 0.5))
          }
        })
      }

      nodes = IframeNode('#page-template', '[data-abe-' + id + ']')
    }

    Array.prototype.forEach.call(nodes, (node) => {
      node.classList.add('select-border')
    })

    // scroll to DOM node
    if(typeof nodes[0] !== 'undefined' && nodes[0] !== null) {
      var bounds = nodes[0].getBoundingClientRect()
      var w = document.getElementById('page-template').contentWindow
      w.scroll(0, w.scrollY + bounds.top + (bounds.height * 0.5) - (window.innerHeight * 0.5))
    }
  }

  static getAttr(target) {
    var dataLink = target.getAttribute('data-id-link')
    var id = target.getAttribute('data-id')

    return {
      abe: 'data-abe-' + id.replace(/\[([0-9]*)\]/g, '$1'),
      id: id
    }
  }

  static getNode(attr) {
    var nodes = IframeNode('#page-template', '[' + attr.abe + ']')

    if(typeof nodes === 'undefined' || nodes === null) {
      var blockContent = IframeNode('#page-template', '.insert-' + attr.id.split('[')[0])[0]
      var blockHtml = unescape(blockContent.innerHTML).replace(/\[0\]\./g, attr.id.split('[')[0] + '[0]-')
      blockContent.insertBefore(blockHtml, blockContent)
      nodes = IframeNode('#page-template', '[' + attr.abe + '="' + attr.id + '"]')
    }

    Array.prototype.forEach.call(nodes, (node) => {
      node.classList.add('select-border')
    })

    return nodes
  }

  /**
   * get input value and set to iframe html
   * @param  {Object} node  html node
   * @param  {Object} input input elem
   * @return {null}
   */
  static formToHtml(node, input) {
    var val = input.value
    var id = input.id
    var placeholder = input.getAttribute('placeholder')
    if(typeof placeholder === 'undefined' || placeholder === 'undefined' || placeholder === null) {
      placeholder = ''
    }
    if(val.replace(/^\s+|\s+$/g, '').length < 1) {
      val = placeholder
    }

    switch(input.nodeName.toLowerCase()){
    case 'input':
      var dataAbeAttr = node.getAttribute('data-abe-attr-' + id.replace(/\[([0-9]*)\]/g, '$1'))
      if(typeof dataAbeAttr !== 'undefined' && dataAbeAttr !== null) {
        node.setAttribute(dataAbeAttr, val)
      }else {
        node.innerHTML = val
      }
      break
    case 'textarea':
      node.innerHTML = (input.classList.contains('form-rich')) ? input.parentNode.querySelector('[contenteditable]').innerHTML : val
      break
    case 'select':
      var key = node.getAttribute('data-abe-' + id)
      var dataAbeAttr = node.getAttribute('data-abe-attr-' + id.replace(/\[([0-9]*)\]/g, '$1'))
      var dataAbeAttrEscaped = unescape(node.getAttribute('data-abe-attr-escaped'))
      var option = input.querySelector('option:checked')
      if(typeof option !== 'undefined' && option !== null) {
        val = option.value
        if(typeof dataAbeAttr !== 'undefined' && dataAbeAttr !== null) {
          try {
            var template = Handlebars.compile(dataAbeAttrEscaped, {noEscape: true})
            var json = {}
            json[key] = val
            var compiled = template(json)
            node.setAttribute(dataAbeAttr, compiled)
          } catch(e) {
            console.log(e)
          }
        }else {
          node.innerHTML = val
        }
      }
      break
    }
  }
}<commit_msg>fix require only file<commit_after>import {IframeNode, IframeCommentNode} from '../utils/iframe'
import Handlebars from 'handlebars'
import math from '../../../../cli/cms/templates/handlebars/math'
import translate from '../../../../cli/cms/templates/handlebars/translate-front'

Handlebars.registerHelper('math', math) // HandlebarsJS unique text helper
Handlebars.registerHelper('i18nAbe', translate) // HandlebarsJS unique text helper

export default class EditorUtils {

  static checkAttribute() {
    let formAbes = document.querySelectorAll('.form-abe')

    Array.prototype.forEach.call(formAbes, (formAbe) => {
      var hide = IframeNode('#page-template', '[data-if-empty-clear="' + formAbe.getAttribute('data-id') + '"]')[0]
      if(typeof hide !== 'undefined' && hide !== null) {
        if (formAbe.value === '') {
          hide.style.display = 'none'
        }else {
          hide.style.display = ''
        }
      }
    })
  }

  static scrollToInputElement(target) {
    var visible = target.getAttribute('data-visible')
    if (visible === 'false' || visible === false) {
      return
    }
    var dataLink = target.getAttribute('data-id-link')
    var id = target.getAttribute('data-id')
    var nodes = IframeNode('#page-template', '[data-abe-' + id + ']')

    if(typeof nodes === 'undefined' || nodes === null || nodes.length === 0) {
      var nodesComment = [].slice.call(IframeCommentNode('#page-template', id.split('[')[0]))
      if(typeof nodesComment !== 'undefined' && nodesComment !== null
        && typeof nodesComment.textContent !== 'undefined' && nodesComment.textContent !== null) {
        var blockHtml = unescape(nodesComment.textContent.replace(/\[\[([\S\s]*?)\]\]/, ''))

        var newBlock = document.createElement('abe')
        newBlock.innerHTML = blockHtml

        var childs = [].slice.call(newBlock.childNodes)
        Array.prototype.forEach.call(childs, (child) => {
          nodesComment.parentNode.insertBefore(child, nodesComment)
        })
      }else if(typeof nodesComment !== 'undefined' && nodesComment !== null) {
        Array.prototype.forEach.call(nodesComment, (nodeComment) => {
          if(typeof nodeComment.parentNode.offsetParent !== 'undefined' && nodeComment.parentNode.offsetParent !== null) {
            var bounds = nodeComment.parentNode.getBoundingClientRect()
            var w = document.getElementById('page-template').contentWindow
            w.scroll(0, w.scrollY + bounds.top + (bounds.height * 0.5) - (window.innerHeight * 0.5))
          }
        })
      }

      nodes = IframeNode('#page-template', '[data-abe-' + id + ']')
    }

    Array.prototype.forEach.call(nodes, (node) => {
      node.classList.add('select-border')
    })

    // scroll to DOM node
    if(typeof nodes[0] !== 'undefined' && nodes[0] !== null) {
      var bounds = nodes[0].getBoundingClientRect()
      var w = document.getElementById('page-template').contentWindow
      w.scroll(0, w.scrollY + bounds.top + (bounds.height * 0.5) - (window.innerHeight * 0.5))
    }
  }

  static getAttr(target) {
    var dataLink = target.getAttribute('data-id-link')
    var id = target.getAttribute('data-id')

    return {
      abe: 'data-abe-' + id.replace(/\[([0-9]*)\]/g, '$1'),
      id: id
    }
  }

  static getNode(attr) {
    var nodes = IframeNode('#page-template', '[' + attr.abe + ']')

    if(typeof nodes === 'undefined' || nodes === null) {
      var blockContent = IframeNode('#page-template', '.insert-' + attr.id.split('[')[0])[0]
      var blockHtml = unescape(blockContent.innerHTML).replace(/\[0\]\./g, attr.id.split('[')[0] + '[0]-')
      blockContent.insertBefore(blockHtml, blockContent)
      nodes = IframeNode('#page-template', '[' + attr.abe + '="' + attr.id + '"]')
    }

    Array.prototype.forEach.call(nodes, (node) => {
      node.classList.add('select-border')
    })

    return nodes
  }

  /**
   * get input value and set to iframe html
   * @param  {Object} node  html node
   * @param  {Object} input input elem
   * @return {null}
   */
  static formToHtml(node, input) {
    var val = input.value
    var id = input.id
    var placeholder = input.getAttribute('placeholder')
    if(typeof placeholder === 'undefined' || placeholder === 'undefined' || placeholder === null) {
      placeholder = ''
    }
    if(val.replace(/^\s+|\s+$/g, '').length < 1) {
      val = placeholder
    }

    switch(input.nodeName.toLowerCase()){
    case 'input':
      var dataAbeAttr = node.getAttribute('data-abe-attr-' + id.replace(/\[([0-9]*)\]/g, '$1'))
      if(typeof dataAbeAttr !== 'undefined' && dataAbeAttr !== null) {
        node.setAttribute(dataAbeAttr, val)
      }else {
        node.innerHTML = val
      }
      break
    case 'textarea':
      node.innerHTML = (input.classList.contains('form-rich')) ? input.parentNode.querySelector('[contenteditable]').innerHTML : val
      break
    case 'select':
      var key = node.getAttribute('data-abe-' + id)
      var dataAbeAttr = node.getAttribute('data-abe-attr-' + id.replace(/\[([0-9]*)\]/g, '$1'))
      var dataAbeAttrEscaped = unescape(node.getAttribute('data-abe-attr-escaped'))
      var option = input.querySelector('option:checked')
      if(typeof option !== 'undefined' && option !== null) {
        val = option.value
        if(typeof dataAbeAttr !== 'undefined' && dataAbeAttr !== null) {
          try {
            var template = Handlebars.compile(dataAbeAttrEscaped, {noEscape: true})
            var json = {}
            json[key] = val
            var compiled = template(json)
            node.setAttribute(dataAbeAttr, compiled)
          } catch(e) {
            console.log(e)
          }
        }else {
          node.innerHTML = val
        }
      }
      break
    }
  }
}<|endoftext|>


<commit_before>/* jslint node: true */
'use strict';

var PluginModule		= require('./plugin_module.js').PluginModule;
var theme				= require('./theme.js');
var art					= require('./art.js');
var Log					= require('./logger.js').log;
var ansi				= require('./ansi_term.js');
var asset				= require('./asset.js');
var ViewController		= require('./view_controller.js').ViewController;
var menuUtil			= require('./menu_util.js');
var Config				= require('./config.js').config;

var async				= require('async');
var assert				= require('assert');
var _					= require('lodash');

exports.MenuModule		= MenuModule;

//	:TODO: some of this is a bit off... should pause after finishedLoading()

function MenuModule(options) {
	PluginModule.call(this, options);

	var self				= this;
	this.menuName			= options.menuName;
	this.menuConfig			= options.menuConfig;
	this.client				= options.client;

    //  :TODO: this and the line below with .config creates empty ({}) objects in the theme --
    //  ...which we really should not do. If they aren't there already, don't use 'em.
	this.menuConfig.options	= options.menuConfig.options || {};
	this.menuMethods		= {};	//	methods called from @method's

	this.cls				= _.isBoolean(this.menuConfig.options.cls) ?
		this.menuConfig.options.cls :
		Config.menus.cls;

	this.menuConfig.config = this.menuConfig.config || {};

	this.initViewControllers();

	this.initSequence = function() {
		var mciData = { };

		async.series(
			[
				function beforeDisplayArt(callback) {
					self.beforeArt(callback);
				},
				function displayMenuArt(callback) {
					if(_.isString(self.menuConfig.art)) {
						theme.displayThemedAsset(
							self.menuConfig.art,
							self.client,
							self.menuConfig.options,	//	can include .font, .trailingLF, etc.
							function displayed(err, artData) {
								if(err) {
									self.client.log.debug( { art : self.menuConfig.art, err : err }, 'Could not display art');
								} else {
									mciData.menu = artData.mciMap;
								}
								callback(null);	//	non-fatal
							}
						);
					} else {
						callback(null);
					}
				},
				function moveToPromptLocation(callback) {
					if(self.menuConfig.prompt) {
						//	:TODO: fetch and move cursor to prompt location, if supplied. See notes/etc. on placements
					}

					callback(null);
				},
				function displayPromptArt(callback) {
					if(_.isString(self.menuConfig.prompt)) {
						//	If a prompt is specified, we need the configuration
						if(!_.isObject(self.menuConfig.promptConfig)) {
							callback(new Error('Prompt specified but configuraiton not found!'));
							return;
						}

						//	Prompts *must* have art. If it's missing it's an error
						//	:TODO: allow inline prompts in the future, e.g. @inline:memberName -> { "memberName" : { "text" : "stuff", ... } }
						var promptConfig = self.menuConfig.promptConfig;
						theme.displayThemedAsset(
							promptConfig.art,
							self.client,
							self.menuConfig.options,	//	can include .font, .trailingLF, etc.
							function displayed(err, artData) {
								if(!err) {
									mciData.prompt = artData.mciMap;
								}
								callback(err);
							});
					} else {
						callback(null);
					}
				},
				function recordCursorPosition(callback) {
					if(self.shouldPause()) {
						self.client.once('cursor position report', function cpr(pos) {
							self.afterArtPos = pos;
							self.client.log.trace( { position : pos }, 'After art position recorded');
							callback(null);
						});
						self.client.term.write(ansi.queryPos());
					} else {
						callback(null);
					}
				},
				function afterArtDisplayed(callback) {
					self.mciReady(mciData, callback);
				},
				function displayPauseIfRequested(callback) {
					if(self.shouldPause()) {
						self.client.term.write(ansi.goto(self.afterArtPos[0], 1));

						//	:TODO: really need a client.term.pause() that uses the correct art/etc.
						theme.displayThemedPause( { client : self.client }, function keyPressed() {
							callback(null);
						});
					} else {
						callback(null);
					}
				}
			],
			function complete(err) {
				if(err) {
					console.log(err)
					//	:TODO: what to do exactly?????
					return self.prevMenu();
				}

				self.finishedLoading();
				self.autoNextMenu();
			}
		);
	};

	this.shouldPause = function() {
		return 'end' === self.menuConfig.options.pause || true === self.menuConfig.options.pause;
	};

	this.hasNextTimeout = function() {
		return _.isNumber(self.menuConfig.options.nextTimeout);
	};

	this.autoNextMenu = function() {
		function goNext() {
			if(_.isString(self.menuConfig.next) || _.isArray(self.menuConfig.next)) {
				menuUtil.handleNext(self.client, self.menuConfig.next);
			} else {
				self.prevMenu();
			}
		}

        if(_.has(self.menuConfig, 'runtime.autoNext') && true === self.menuConfig.runtime.autoNext) {
			/*
				If 'next' is supplied, we'll use it. Otherwise, utlize fallback which
				may be explicit (supplied) or non-explicit (previous menu)

				'next' may be a simple asset, or a object with next.asset and
				extrArgs

				next: assetSpec

				-or-

				next: {
					asset: assetSpec
					extraArgs: ...
				}
			*/
			if(self.hasNextTimeout()) {
				setTimeout(function nextTimeout() {
					goNext();
				}, this.menuConfig.options.nextTimeout);
			} else {
				goNext();
			}
		}
	};

	this.haveNext = function() {
		return (_.isString(this.menuConfig.next) || _.isArray(this.menuConfig.next));
	};
}

require('util').inherits(MenuModule, PluginModule);

require('./mod_mixins.js').ViewControllerManagement.call(MenuModule.prototype);

MenuModule.prototype.enter = function() {
	if(_.isString(this.menuConfig.status)) {
		this.client.currentStatus = this.menuConfig.status;
	} else {
		this.client.currentStatus = 'Browsing menus';
	}

	this.initSequence();
};

MenuModule.prototype.getSaveState = function() {
	//	nothing in base
};

MenuModule.prototype.restoreSavedState = function(savedState) {
	//	nothing in base
};

MenuModule.prototype.nextMenu = function(cb) {
	//
	//	If we don't actually have |next|, we'll go previous
	//
	if(!this.haveNext()) {
		this.prevMenu(cb);
		return;
	}

	//	:TODO: this, prevMenu(), and gotoMenu() need a default |cb| handler if none is supplied.
	//	...if the error is that we do not meet ACS requirements and did not get a match, then what?
	if(!cb) {
		cb = function(err) {
			if(err) {
				//	:TODO: Don't console.log() here!
				console.log(err)
			}
		}
	}
	this.client.menuStack.next(cb);
};

MenuModule.prototype.prevMenu = function(cb) {
	this.client.menuStack.prev(cb);
};

MenuModule.prototype.gotoMenu = function(name, options, cb) {
	this.client.menuStack.goto(name, options, cb);
};

MenuModule.prototype.leave = function() {
	this.detachViewControllers();
};

MenuModule.prototype.beforeArt = function(cb) {
	if(this.cls) {
		this.client.term.write(ansi.resetScreen());
	}

	if(_.isNumber(this.menuConfig.options.baudRate)) {
		this.client.term.write(ansi.setEmulatedBaudRate(this.menuConfig.options.baudRate));
	}

	return cb(null);
};

MenuModule.prototype.mciReady = function(mciData, cb) {
	//	Reserved for sub classes
	cb(null);
};

MenuModule.prototype.standardMCIReadyHandler = function(mciData, cb) {
	//
	//	A quick rundown:
	//	*	We may have mciData.menu, mciData.prompt, or both.
	//	*	Prompt form is favored over menu form if both are present.
	//	*	Standard/prefdefined MCI entries must load both (e.g. %BN is expected to resolve)
	//
	var self = this;

	async.series(
		[
			function addViewControllers(callback) {
				_.forEach(mciData, function entry(mciMap, name) {
					assert('menu' === name || 'prompt' === name);
					self.addViewController(name, new ViewController( { client : self.client } ));
				});
				callback(null);
			},
			function createMenu(callback) {
				if(self.viewControllers.menu) {
					var menuLoadOpts = {
						mciMap		: mciData.menu,
						callingMenu	: self,
						withoutForm	: _.isObject(mciData.prompt),
					};

					self.viewControllers.menu.loadFromMenuConfig(menuLoadOpts, function menuLoaded(err) {
						callback(err);
					});
				} else {
					callback(null);
				}
			},
			function createPrompt(callback) {
				if(self.viewControllers.prompt) {
					var promptLoadOpts = {
						callingMenu		: self,
						mciMap			: mciData.prompt,
					};

					self.viewControllers.prompt.loadFromPromptConfig(promptLoadOpts, function promptLoaded(err) {
						callback(err);
					});
				} else {
					callback(null);
				}
			}
		],
		function complete(err) {
			cb(err);
		}
	);
};

MenuModule.prototype.finishedLoading = function() {
};<commit_msg>Set baud emu before cls such that terms without support don't show garbage... most of the time<commit_after>/* jslint node: true */
'use strict';

var PluginModule		= require('./plugin_module.js').PluginModule;
var theme				= require('./theme.js');
var art					= require('./art.js');
var Log					= require('./logger.js').log;
var ansi				= require('./ansi_term.js');
var asset				= require('./asset.js');
var ViewController		= require('./view_controller.js').ViewController;
var menuUtil			= require('./menu_util.js');
var Config				= require('./config.js').config;

var async				= require('async');
var assert				= require('assert');
var _					= require('lodash');

exports.MenuModule		= MenuModule;

//	:TODO: some of this is a bit off... should pause after finishedLoading()

function MenuModule(options) {
	PluginModule.call(this, options);

	var self				= this;
	this.menuName			= options.menuName;
	this.menuConfig			= options.menuConfig;
	this.client				= options.client;

    //  :TODO: this and the line below with .config creates empty ({}) objects in the theme --
    //  ...which we really should not do. If they aren't there already, don't use 'em.
	this.menuConfig.options	= options.menuConfig.options || {};
	this.menuMethods		= {};	//	methods called from @method's

	this.cls				= _.isBoolean(this.menuConfig.options.cls) ?
		this.menuConfig.options.cls :
		Config.menus.cls;

	this.menuConfig.config = this.menuConfig.config || {};

	this.initViewControllers();

	this.initSequence = function() {
		var mciData = { };

		async.series(
			[
				function beforeDisplayArt(callback) {
					self.beforeArt(callback);
				},
				function displayMenuArt(callback) {
					if(_.isString(self.menuConfig.art)) {
						theme.displayThemedAsset(
							self.menuConfig.art,
							self.client,
							self.menuConfig.options,	//	can include .font, .trailingLF, etc.
							function displayed(err, artData) {
								if(err) {
									self.client.log.debug( { art : self.menuConfig.art, err : err }, 'Could not display art');
								} else {
									mciData.menu = artData.mciMap;
								}
								callback(null);	//	non-fatal
							}
						);
					} else {
						callback(null);
					}
				},
				function moveToPromptLocation(callback) {
					if(self.menuConfig.prompt) {
						//	:TODO: fetch and move cursor to prompt location, if supplied. See notes/etc. on placements
					}

					callback(null);
				},
				function displayPromptArt(callback) {
					if(_.isString(self.menuConfig.prompt)) {
						//	If a prompt is specified, we need the configuration
						if(!_.isObject(self.menuConfig.promptConfig)) {
							callback(new Error('Prompt specified but configuraiton not found!'));
							return;
						}

						//	Prompts *must* have art. If it's missing it's an error
						//	:TODO: allow inline prompts in the future, e.g. @inline:memberName -> { "memberName" : { "text" : "stuff", ... } }
						var promptConfig = self.menuConfig.promptConfig;
						theme.displayThemedAsset(
							promptConfig.art,
							self.client,
							self.menuConfig.options,	//	can include .font, .trailingLF, etc.
							function displayed(err, artData) {
								if(!err) {
									mciData.prompt = artData.mciMap;
								}
								callback(err);
							});
					} else {
						callback(null);
					}
				},
				function recordCursorPosition(callback) {
					if(self.shouldPause()) {
						self.client.once('cursor position report', function cpr(pos) {
							self.afterArtPos = pos;
							self.client.log.trace( { position : pos }, 'After art position recorded');
							callback(null);
						});
						self.client.term.write(ansi.queryPos());
					} else {
						callback(null);
					}
				},
				function afterArtDisplayed(callback) {
					self.mciReady(mciData, callback);
				},
				function displayPauseIfRequested(callback) {
					if(self.shouldPause()) {
						self.client.term.write(ansi.goto(self.afterArtPos[0], 1));

						//	:TODO: really need a client.term.pause() that uses the correct art/etc.
						theme.displayThemedPause( { client : self.client }, function keyPressed() {
							callback(null);
						});
					} else {
						callback(null);
					}
				}
			],
			function complete(err) {
				if(err) {
					console.log(err)
					//	:TODO: what to do exactly?????
					return self.prevMenu();
				}

				self.finishedLoading();
				self.autoNextMenu();
			}
		);
	};

	this.shouldPause = function() {
		return 'end' === self.menuConfig.options.pause || true === self.menuConfig.options.pause;
	};

	this.hasNextTimeout = function() {
		return _.isNumber(self.menuConfig.options.nextTimeout);
	};

	this.autoNextMenu = function() {
		function goNext() {
			if(_.isString(self.menuConfig.next) || _.isArray(self.menuConfig.next)) {
				menuUtil.handleNext(self.client, self.menuConfig.next);
			} else {
				self.prevMenu();
			}
		}

        if(_.has(self.menuConfig, 'runtime.autoNext') && true === self.menuConfig.runtime.autoNext) {
			/*
				If 'next' is supplied, we'll use it. Otherwise, utlize fallback which
				may be explicit (supplied) or non-explicit (previous menu)

				'next' may be a simple asset, or a object with next.asset and
				extrArgs

				next: assetSpec

				-or-

				next: {
					asset: assetSpec
					extraArgs: ...
				}
			*/
			if(self.hasNextTimeout()) {
				setTimeout(function nextTimeout() {
					goNext();
				}, this.menuConfig.options.nextTimeout);
			} else {
				goNext();
			}
		}
	};

	this.haveNext = function() {
		return (_.isString(this.menuConfig.next) || _.isArray(this.menuConfig.next));
	};
}

require('util').inherits(MenuModule, PluginModule);

require('./mod_mixins.js').ViewControllerManagement.call(MenuModule.prototype);

MenuModule.prototype.enter = function() {
	if(_.isString(this.menuConfig.status)) {
		this.client.currentStatus = this.menuConfig.status;
	} else {
		this.client.currentStatus = 'Browsing menus';
	}

	this.initSequence();
};

MenuModule.prototype.getSaveState = function() {
	//	nothing in base
};

MenuModule.prototype.restoreSavedState = function(savedState) {
	//	nothing in base
};

MenuModule.prototype.nextMenu = function(cb) {
	//
	//	If we don't actually have |next|, we'll go previous
	//
	if(!this.haveNext()) {
		this.prevMenu(cb);
		return;
	}

	//	:TODO: this, prevMenu(), and gotoMenu() need a default |cb| handler if none is supplied.
	//	...if the error is that we do not meet ACS requirements and did not get a match, then what?
	if(!cb) {
		cb = function(err) {
			if(err) {
				//	:TODO: Don't console.log() here!
				console.log(err)
			}
		}
	}
	this.client.menuStack.next(cb);
};

MenuModule.prototype.prevMenu = function(cb) {
	this.client.menuStack.prev(cb);
};

MenuModule.prototype.gotoMenu = function(name, options, cb) {
	this.client.menuStack.goto(name, options, cb);
};

MenuModule.prototype.leave = function() {
	this.detachViewControllers();
};

MenuModule.prototype.beforeArt = function(cb) {
	//
	//	Set emulated baud rate - note that some terminals will display
	//	part of the ESC sequence here (generally a single 'r') if they
	//	do not support cterm style baud rates
	//
	if(_.isNumber(this.menuConfig.options.baudRate)) {
		this.client.term.write(ansi.setEmulatedBaudRate(this.menuConfig.options.baudRate));
	}

	if(this.cls) {
		this.client.term.write(ansi.resetScreen());
	}

	return cb(null);
};

MenuModule.prototype.mciReady = function(mciData, cb) {
	//	Reserved for sub classes
	cb(null);
};

MenuModule.prototype.standardMCIReadyHandler = function(mciData, cb) {
	//
	//	A quick rundown:
	//	*	We may have mciData.menu, mciData.prompt, or both.
	//	*	Prompt form is favored over menu form if both are present.
	//	*	Standard/prefdefined MCI entries must load both (e.g. %BN is expected to resolve)
	//
	var self = this;

	async.series(
		[
			function addViewControllers(callback) {
				_.forEach(mciData, function entry(mciMap, name) {
					assert('menu' === name || 'prompt' === name);
					self.addViewController(name, new ViewController( { client : self.client } ));
				});
				callback(null);
			},
			function createMenu(callback) {
				if(self.viewControllers.menu) {
					var menuLoadOpts = {
						mciMap		: mciData.menu,
						callingMenu	: self,
						withoutForm	: _.isObject(mciData.prompt),
					};

					self.viewControllers.menu.loadFromMenuConfig(menuLoadOpts, function menuLoaded(err) {
						callback(err);
					});
				} else {
					callback(null);
				}
			},
			function createPrompt(callback) {
				if(self.viewControllers.prompt) {
					var promptLoadOpts = {
						callingMenu		: self,
						mciMap			: mciData.prompt,
					};

					self.viewControllers.prompt.loadFromPromptConfig(promptLoadOpts, function promptLoaded(err) {
						callback(err);
					});
				} else {
					callback(null);
				}
			}
		],
		function complete(err) {
			cb(err);
		}
	);
};

MenuModule.prototype.finishedLoading = function() {
};<|endoftext|>


<commit_before>"use strict";

describe("close:countrydropdown event:", function() {

  var spy;

  beforeEach(function() {
    intlSetup();
    input = $("<input>");
    spy = spyOnEvent(input, 'close:countrydropdown');
  });

  afterEach(function() {
    input.intlTelInput("destroy");
    input = null;
  });

  describe("init plugin", function() {

    beforeEach(function() {
      input.intlTelInput();
    });

    it("does not trigger the event", function() {
      expect(spy).not.toHaveBeenTriggered();
    });

    it("clicking outside the dropdown to close it triggers the event", function() {
      openCountryDropDown();
      $("<body>").click();
      expect(spy).toHaveBeenTriggered();
    });

    it("pressing esc triggers the event", function() {
      openCountryDropDown();
      triggerKeyOnBody("ESC");
      expect(spy).toHaveBeenTriggered();
    });

    it("selecting another country triggers the event", function() {
      selectFlag("af");
      expect(spy).toHaveBeenTriggered();
    });

  });

});
<commit_msg>fix issue where input wasn't in DOM before tests began.<commit_after>"use strict";

describe("close:countrydropdown event:", function() {

  var spy;

  beforeEach(function() {
    intlSetup();
    input = $("<input>").appendTo("body");
    spy = spyOnEvent(input, 'close:countrydropdown');
  });

  afterEach(function() {
    input.intlTelInput("destroy").remove();
    input = null;
  });

  describe("init plugin", function() {

    beforeEach(function() {
      input.intlTelInput();
    });

    it("does not trigger the event", function() {
      expect(spy).not.toHaveBeenTriggered();
    });

    it("clicking outside the dropdown to close it triggers the event", function() {
      openCountryDropDown();
      input.click();
      expect(spy).toHaveBeenTriggered();
    });

    it("pressing esc triggers the event", function() {
      openCountryDropDown();
      triggerKeyOnBody("ESC");
      expect(spy).toHaveBeenTriggered();
    });

    it("selecting another country triggers the event", function() {
      selectFlag("af");
      expect(spy).toHaveBeenTriggered();
    });

  });

});
<|endoftext|>


<commit_before>/**
 * Copyright (C) 2018-2019  The Software Heritage developers
 * See the AUTHORS file at the top-level directory of this distribution
 * License: GNU Affero General Public License version 3, or any later version
 * See top-level LICENSE file for more information
 */

import {handleFetchError, csrfPost, isGitRepoUrl, removeUrlFragment} from 'utils/functions';
import {validate} from 'validate.js';

let saveRequestsTable;

function originSaveRequest(originType, originUrl,
                           acceptedCallback, pendingCallback, errorCallback) {
  let addSaveOriginRequestUrl = Urls.browse_origin_save_request(originType, originUrl);
  let grecaptchaData = {};
  if (swh.webapp.isReCaptchaActivated()) {
    grecaptchaData['g-recaptcha-response'] = grecaptcha.getResponse();
  }
  let headers = {
    'Accept': 'application/json',
    'Content-Type': 'application/json'
  };
  let body = JSON.stringify(grecaptchaData);
  $('.swh-processing-save-request').css('display', 'block');
  csrfPost(addSaveOriginRequestUrl, headers, body)
    .then(handleFetchError)
    .then(response => response.json())
    .then(data => {
      $('.swh-processing-save-request').css('display', 'none');
      if (data.save_request_status === 'accepted') {
        acceptedCallback();
      } else {
        pendingCallback();
      }
      if (swh.webapp.isReCaptchaActivated()) {
        grecaptcha.reset();
      }
    })
    .catch(response => {
      $('.swh-processing-save-request').css('display', 'none');
      if (response.status === 403) {
        errorCallback();
      }
      if (swh.webapp.isReCaptchaActivated()) {
        grecaptcha.reset();
      }
    });
}

export function initOriginSave() {

  $(document).ready(() => {

    $.fn.dataTable.ext.errMode = 'none';

    fetch(Urls.browse_origin_save_types_list())
      .then(response => response.json())
      .then(data => {
        for (let originType of data) {
          $('#swh-input-origin-type').append(`<option value="${originType}">${originType}</option>`);
        }
      });

    saveRequestsTable = $('#swh-origin-save-requests')
      .on('error.dt', (e, settings, techNote, message) => {
        $('#swh-origin-save-request-list-error').text('An error occurred while retrieving the save requests list');
        console.log(message);
      })
      .DataTable({
        serverSide: true,
        ajax: Urls.browse_origin_save_requests_list('all'),
        columns: [
          {
            data: 'save_request_date',
            name: 'request_date',
            render: (data, type, row) => {
              if (type === 'display') {
                let date = new Date(data);
                return date.toLocaleString();
              }
              return data;
            }
          },
          {
            data: 'origin_type',
            name: 'origin_type'

          },
          {
            data: 'origin_url',
            name: 'origin_url',
            render: (data, type, row) => {
              if (type === 'display') {
                return `<a href="${data}">${data}</a>`;
              }
              return data;
            }
          },
          {
            data: 'save_request_status',
            name: 'status'
          },
          {
            data: 'save_task_status',
            name: 'loading_task_status',
            render: (data, type, row) => {
              if (data === 'succeed') {
                let browseOriginUrl = Urls.browse_origin(row.origin_url);
                if (row.visit_date) {
                  browseOriginUrl += `visit/${row.visit_date}/`;
                }
                return `<a href="${browseOriginUrl}">${data}</a>`;
              }
              return data;
            }
          }
        ],
        scrollY: '50vh',
        scrollCollapse: true,
        order: [[0, 'desc']]
      });

    $('#swh-origin-save-requests-list-tab').on('shown.bs.tab', () => {
      saveRequestsTable.draw();
      window.location.hash = '#requests';
    });

    $('#swh-origin-save-request-create-tab').on('shown.bs.tab', () => {
      removeUrlFragment();
    });

    let saveRequestAcceptedAlert =
      `

        The "save code now" request has been accepted and will be processed as soon as possible.

`;

    let saveRequestPendingAlert =
      `

        The "save code now" request has been put in pending state and may be accepted for processing after manual review.

`;

    let saveRequestRejectedAlert =
      `

        The "save code now" request has been rejected because the reCAPTCHA could not be validated or the provided origin url is blacklisted.

`;

    $('#swh-save-origin-form').submit(event => {
      event.preventDefault();
      event.stopPropagation();
      $('.alert').alert('close');
      if (event.target.checkValidity()) {
        $(event.target).removeClass('was-validated');
        let originType = $('#swh-input-origin-type').val();
        let originUrl = $('#swh-input-origin-url').val();

        originSaveRequest(originType, originUrl,
                          () => $('#swh-origin-save-request-status').html(saveRequestAcceptedAlert),
                          () => $('#swh-origin-save-request-status').html(saveRequestPendingAlert),
                          () => {
                            $('#swh-origin-save-request-status').css('color', 'red');
                            $('#swh-origin-save-request-status').html(saveRequestRejectedAlert);
                          });
      } else {
        $(event.target).addClass('was-validated');
      }
    });

    $('#swh-show-origin-save-requests-list').on('click', (event) => {
      event.preventDefault();
      $('.nav-tabs a[href="#swh-origin-save-requests-list"]').tab('show');
    });

    $('#swh-input-origin-url').on('input', function(event) {
      let originUrl = $(this).val().trim();
      $(this).val(originUrl);
      $('#swh-input-origin-type option').each(function() {
        let val = $(this).val();
        if (val && originUrl.includes(val)) {
          $(this).prop('selected', true);
        }
      });
    });

    if (window.location.hash === '#requests') {
      $('.nav-tabs a[href="#swh-origin-save-requests-list"]').tab('show');
    }

  });

}

export function validateSaveOriginUrl(input) {
  let validUrl = validate({website: input.value}, {
    website: {
      url: {
        schemes: ['http', 'https', 'svn', 'git']
      }
    }
  }) === undefined;
  let originType = $('#swh-input-origin-type').val();
  if (originType === 'git' && validUrl) {
    // additional checks for well known code hosting providers
    let githubIdx = input.value.indexOf('://github.com');
    let gitlabIdx = input.value.indexOf('://gitlab.');
    let gitSfIdx = input.value.indexOf('://git.code.sf.net');
    let bitbucketIdx = input.value.indexOf('://bitbucket.org');
    if (githubIdx !== -1 && githubIdx <= 5) {
      validUrl = isGitRepoUrl(input.value, 'github.com');
    } else if (gitlabIdx !== -1 && gitlabIdx <= 5) {
      let startIdx = gitlabIdx + 3;
      let idx = input.value.indexOf('/', startIdx);
      if (idx !== -1) {
        let gitlabDomain = input.value.substr(startIdx, idx - startIdx);
        // GitLab repo url needs to be suffixed by '.git' in order to be successfully loaded
        validUrl = isGitRepoUrl(input.value, gitlabDomain) && input.value.endsWith('.git');
      } else {
        validUrl = false;
      }
    } else if (gitSfIdx !== -1 && gitSfIdx <= 5) {
      validUrl = isGitRepoUrl(input.value, 'git.code.sf.net/p');
    } else if (bitbucketIdx !== -1 && bitbucketIdx <= 5) {
      validUrl = isGitRepoUrl(input.value, 'bitbucket.org');
    }
  }
  if (validUrl) {
    input.setCustomValidity('');
  } else {
    input.setCustomValidity('The origin url is not valid or does not reference a code repository');
  }
}

export function initTakeNewSnapshot() {

  let newSnapshotRequestAcceptedAlert =
    `

      The "take new snapshot" request has been accepted and will be processed as soon as possible.

`;

  let newSnapshotRequestPendingAlert =
    `

      The "take new snapshot" request has been put in pending state and may be accepted for processing after manual review.

`;

  let newSnapshotRequestRejectedAlert =
    `

      The "take new snapshot" request has been rejected because the reCAPTCHA could not be validated.

`;

  $(document).ready(() => {
    $('#swh-take-new-snapshot-form').submit(event => {
      event.preventDefault();
      event.stopPropagation();

      let originType = $('#swh-input-origin-type').val();
      let originUrl = $('#swh-input-origin-url').val();

      originSaveRequest(originType, originUrl,
                        () => $('#swh-take-new-snapshot-request-status').html(newSnapshotRequestAcceptedAlert),
                        () => $('#swh-take-new-snapshot-request-status').html(newSnapshotRequestPendingAlert),
                        () => {
                          $('#swh-take-new-snapshot-request-status').css('color', 'red');
                          $('#swh-take-new-snapshot-request-status').html(newSnapshotRequestRejectedAlert);
                        });
    });
  });
}
<commit_msg>assets/origin-save: Fix an issue regarding url validation<commit_after>/**
 * Copyright (C) 2018-2019  The Software Heritage developers
 * See the AUTHORS file at the top-level directory of this distribution
 * License: GNU Affero General Public License version 3, or any later version
 * See top-level LICENSE file for more information
 */

import {handleFetchError, csrfPost, isGitRepoUrl, removeUrlFragment} from 'utils/functions';
import {validate} from 'validate.js';

let saveRequestsTable;

function originSaveRequest(originType, originUrl,
                           acceptedCallback, pendingCallback, errorCallback) {
  let addSaveOriginRequestUrl = Urls.browse_origin_save_request(originType, originUrl);
  let grecaptchaData = {};
  if (swh.webapp.isReCaptchaActivated()) {
    grecaptchaData['g-recaptcha-response'] = grecaptcha.getResponse();
  }
  let headers = {
    'Accept': 'application/json',
    'Content-Type': 'application/json'
  };
  let body = JSON.stringify(grecaptchaData);
  $('.swh-processing-save-request').css('display', 'block');
  csrfPost(addSaveOriginRequestUrl, headers, body)
    .then(handleFetchError)
    .then(response => response.json())
    .then(data => {
      $('.swh-processing-save-request').css('display', 'none');
      if (data.save_request_status === 'accepted') {
        acceptedCallback();
      } else {
        pendingCallback();
      }
      if (swh.webapp.isReCaptchaActivated()) {
        grecaptcha.reset();
      }
    })
    .catch(response => {
      $('.swh-processing-save-request').css('display', 'none');
      if (response.status === 403) {
        errorCallback();
      }
      if (swh.webapp.isReCaptchaActivated()) {
        grecaptcha.reset();
      }
    });
}

export function initOriginSave() {

  $(document).ready(() => {

    $.fn.dataTable.ext.errMode = 'none';

    fetch(Urls.browse_origin_save_types_list())
      .then(response => response.json())
      .then(data => {
        for (let originType of data) {
          $('#swh-input-origin-type').append(`<option value="${originType}">${originType}</option>`);
        }
      });

    saveRequestsTable = $('#swh-origin-save-requests')
      .on('error.dt', (e, settings, techNote, message) => {
        $('#swh-origin-save-request-list-error').text('An error occurred while retrieving the save requests list');
        console.log(message);
      })
      .DataTable({
        serverSide: true,
        ajax: Urls.browse_origin_save_requests_list('all'),
        columns: [
          {
            data: 'save_request_date',
            name: 'request_date',
            render: (data, type, row) => {
              if (type === 'display') {
                let date = new Date(data);
                return date.toLocaleString();
              }
              return data;
            }
          },
          {
            data: 'origin_type',
            name: 'origin_type'

          },
          {
            data: 'origin_url',
            name: 'origin_url',
            render: (data, type, row) => {
              if (type === 'display') {
                return `<a href="${data}">${data}</a>`;
              }
              return data;
            }
          },
          {
            data: 'save_request_status',
            name: 'status'
          },
          {
            data: 'save_task_status',
            name: 'loading_task_status',
            render: (data, type, row) => {
              if (data === 'succeed') {
                let browseOriginUrl = Urls.browse_origin(row.origin_url);
                if (row.visit_date) {
                  browseOriginUrl += `visit/${row.visit_date}/`;
                }
                return `<a href="${browseOriginUrl}">${data}</a>`;
              }
              return data;
            }
          }
        ],
        scrollY: '50vh',
        scrollCollapse: true,
        order: [[0, 'desc']]
      });

    $('#swh-origin-save-requests-list-tab').on('shown.bs.tab', () => {
      saveRequestsTable.draw();
      window.location.hash = '#requests';
    });

    $('#swh-origin-save-request-create-tab').on('shown.bs.tab', () => {
      removeUrlFragment();
    });

    let saveRequestAcceptedAlert =
      `

        The "save code now" request has been accepted and will be processed as soon as possible.

`;

    let saveRequestPendingAlert =
      `

        The "save code now" request has been put in pending state and may be accepted for processing after manual review.

`;

    let saveRequestRejectedAlert =
      `

        The "save code now" request has been rejected because the reCAPTCHA could not be validated or the provided origin url is blacklisted.

`;

    $('#swh-save-origin-form').submit(event => {
      event.preventDefault();
      event.stopPropagation();
      $('.alert').alert('close');
      if (event.target.checkValidity()) {
        $(event.target).removeClass('was-validated');
        let originType = $('#swh-input-origin-type').val();
        let originUrl = $('#swh-input-origin-url').val();

        originSaveRequest(originType, originUrl,
                          () => $('#swh-origin-save-request-status').html(saveRequestAcceptedAlert),
                          () => $('#swh-origin-save-request-status').html(saveRequestPendingAlert),
                          () => {
                            $('#swh-origin-save-request-status').css('color', 'red');
                            $('#swh-origin-save-request-status').html(saveRequestRejectedAlert);
                          });
      } else {
        $(event.target).addClass('was-validated');
      }
    });

    $('#swh-show-origin-save-requests-list').on('click', (event) => {
      event.preventDefault();
      $('.nav-tabs a[href="#swh-origin-save-requests-list"]').tab('show');
    });

    $('#swh-input-origin-url').on('input', function(event) {
      let originUrl = $(this).val().trim();
      $(this).val(originUrl);
      $('#swh-input-origin-type option').each(function() {
        let val = $(this).val();
        if (val && originUrl.includes(val)) {
          $(this).prop('selected', true);
        }
      });
    });

    if (window.location.hash === '#requests') {
      $('.nav-tabs a[href="#swh-origin-save-requests-list"]').tab('show');
    }

  });

}

export function validateSaveOriginUrl(input) {
  let originUrl = input.value.trim();
  let validUrl = validate({website: originUrl}, {
    website: {
      url: {
        schemes: ['http', 'https', 'svn', 'git']
      }
    }
  }) === undefined;
  let originType = $('#swh-input-origin-type').val();
  if (originType === 'git' && validUrl) {
    // additional checks for well known code hosting providers
    let githubIdx = originUrl.indexOf('://github.com');
    let gitlabIdx = originUrl.indexOf('://gitlab.');
    let gitSfIdx = originUrl.indexOf('://git.code.sf.net');
    let bitbucketIdx = originUrl.indexOf('://bitbucket.org');
    if (githubIdx !== -1 && githubIdx <= 5) {
      validUrl = isGitRepoUrl(originUrl, 'github.com');
    } else if (gitlabIdx !== -1 && gitlabIdx <= 5) {
      let startIdx = gitlabIdx + 3;
      let idx = originUrl.indexOf('/', startIdx);
      if (idx !== -1) {
        let gitlabDomain = originUrl.substr(startIdx, idx - startIdx);
        // GitLab repo url needs to be suffixed by '.git' in order to be successfully loaded
        // This is due to a bug in dulwich < 0.19.11.
        // TODO: remove this check once dulwich >= 0.19.11 is used in production
        validUrl = isGitRepoUrl(originUrl, gitlabDomain) && originUrl.endsWith('.git');
      } else {
        validUrl = false;
      }
    } else if (gitSfIdx !== -1 && gitSfIdx <= 5) {
      validUrl = isGitRepoUrl(originUrl, 'git.code.sf.net/p');
    } else if (bitbucketIdx !== -1 && bitbucketIdx <= 5) {
      validUrl = isGitRepoUrl(originUrl, 'bitbucket.org');
    }
  }
  if (validUrl) {
    input.setCustomValidity('');
  } else {
    input.setCustomValidity('The origin url is not valid or does not reference a code repository');
  }
}

export function initTakeNewSnapshot() {

  let newSnapshotRequestAcceptedAlert =
    `

      The "take new snapshot" request has been accepted and will be processed as soon as possible.

`;

  let newSnapshotRequestPendingAlert =
    `

      The "take new snapshot" request has been put in pending state and may be accepted for processing after manual review.

`;

  let newSnapshotRequestRejectedAlert =
    `

      The "take new snapshot" request has been rejected because the reCAPTCHA could not be validated.

`;

  $(document).ready(() => {
    $('#swh-take-new-snapshot-form').submit(event => {
      event.preventDefault();
      event.stopPropagation();

      let originType = $('#swh-input-origin-type').val();
      let originUrl = $('#swh-input-origin-url').val();

      originSaveRequest(originType, originUrl,
                        () => $('#swh-take-new-snapshot-request-status').html(newSnapshotRequestAcceptedAlert),
                        () => $('#swh-take-new-snapshot-request-status').html(newSnapshotRequestPendingAlert),
                        () => {
                          $('#swh-take-new-snapshot-request-status').css('color', 'red');
                          $('#swh-take-new-snapshot-request-status').html(newSnapshotRequestRejectedAlert);
                        });
    });
  });
}
<|endoftext|>


<commit_before>/**
 * @module Animatronics
 */

import React from 'react'
import PropTypes from 'prop-types'

import withAnimatronics from './withAnimatronics'

class Animatronics extends React.Component {
  render() {
    const { createAnimationSequences, children } = this.props;
    const enhance = withAnimatronics(createAnimationSequences);
    class BaseComponent extends React.Component {
      render() {
        const { children, ...props } = this.props;
        return React.cloneElement(children, props);
      }
    }
    const AnimatronicsComponent = enhance(BaseComponent);
    return <AnimatronicsComponent children={ children }/>;
  }
}

Animatronics.propTypes = {
  createAnimationSequences: PropTypes.func.isRequired,
};

export default Animatronics;
<commit_msg>:hammer: [Animatronics] No need to pass in children, already available<commit_after>/**
 * @module Animatronics
 */

import React from 'react'
import PropTypes from 'prop-types'

import withAnimatronics from './withAnimatronics'

class Animatronics extends React.Component {
  render() {
    const { createAnimationSequences, children } = this.props;
    const enhance = withAnimatronics(createAnimationSequences);
    class BaseComponent extends React.Component {
      render() {
        const { ...props } = this.props;
        return React.cloneElement(children, props);
      }
    }
    const AnimatronicsComponent = enhance(BaseComponent);
    return <AnimatronicsComponent/>;
  }
}

Animatronics.propTypes = {
  createAnimationSequences: PropTypes.func.isRequired,
};

export default Animatronics;
<|endoftext|>


<commit_before>'use strict';
var express      = require('express')
  , basicAuth    = require('node-basicauth')
  , bodyParser   = require('body-parser')
  , cookieParser = require('cookie-parser')
  , compression  = require('compression')
  , fs           = require('fs')
  , errorHandler = require('magic-errorHandler')
  , headers      = require('magic-headers')
  , log          = require('magic-log')
  , blog         = require('magic-blog')
  , db           = require('magic-db')
  , router       = require('magic-router')
  , utils        = require('magic-utils')
  , morgan       = require('morgan')
  , path         = require('path')
  , favicon      = require('serve-favicon')
  , stylus       = require('stylus')
;

module.exports = function(M, app, dir) {
  var css         = ( app.get('css') || stylus )
    , env         = app.get('env') || 'production'
    , dbSetUp     = false
    , faviconPath = path.join(dir, 'public', 'favicon.ico')
    , dirs        = app.get('dirs') || {
        public: path.join(dir, app.get('publicDir') || 'public')
      , views : path.join(dir, app.get('viewsDir') || 'views')
    }
  ;

  if ( app.get('db') && ! dbSetUp ) {
    dbSetUp = true;
    let dbConfig = app.get('db');

    if ( dbConfig.name ) {
      app.use(function (req, res, next) {
        db(dbConfig, function (err) {
          if ( err ) { throw err; }
          next();
        });
      });
    }
  }

  app.set('dirs', dirs);

  app.use(function (req, res, next) {
    req.app = app;
    next();
  });

  //set expiry headers
  app.use(headers);

  if ( app.get('basicAuth') ) {
    app.use(
      basicAuth( app.get('basicAuth') )
    );
  }

  //fs.existsSync only gets called once on first request
  if ( ! app.get('faviconChecked') && ! app.get('faviconExists') ) {
    app.set('faviconChecked', true);
    app.set('faviconExists', fs.existsSync(faviconPath));
  }

  if ( app.get('faviconExists') ) {
    app.use( favicon(faviconPath) );
  }

  app.set('views', dirs.views);
  app.set('view engine', app.get('view engine') || 'jade');

  app.use(compression({ threshold: 128 }));

  app.use( css.middleware(dirs.public, {maxAge: '1d'}) );
  app.use( express.static(dirs.public, {maxAge: '1d'}) );

  if ( app.get('blogRoot') ) {
    let blogRoot = app.get('blogRoot');
    if ( typeof blogRoot !== 'string' && typeof blogRoot !== 'number' ) {
      blogRoot = 'blog';
    }
    if ( blogRoot.charAt(0) !== '/' ) {
      blogRoot = '/' + blogRoot;
    } else {
      app.set('blogRoot', blogRoot.substr(1) );
    }
    app.use( blogRoot, blog );
  }

  //logging
  app.use(morgan('combined'));

  //if host sets bodyparser to true, init it
  if ( app.enabled('bodyParser') ) {
    app.use(bodyParser.json());
    app.use(bodyParser.urlencoded({ extended: false }));
  }

  //if host sets cookieparser to true, init it:
  if ( app.enabled('cookieParser') ) {
    app.use(cookieParser());
  }

  //load host specific router
  if ( app.get('router') ) {
    let routes = app.get('router');

    if ( typeof routes === 'array' || typeof routes === 'object' ) {
      utils.each(routes, function (route) {
        app.use(route);
      });
    } else if ( typeof routes === 'function' ) {
      app.use(routes);
    }
  }

  //default router
  app.use(router);

  //we are in a 404 error
  app.use(errorHandler.handle404);

  //oops, worst case fallback, 500 server error.
  app.use(errorHandler.handle500);

  return app;
}
<commit_msg>redid db setup variables to use app.get<commit_after>'use strict';
var express      = require('express')
  , basicAuth    = require('node-basicauth')
  , bodyParser   = require('body-parser')
  , cookieParser = require('cookie-parser')
  , compression  = require('compression')
  , fs           = require('fs')
  , errorHandler = require('magic-errorHandler')
  , headers      = require('magic-headers')
  , log          = require('magic-log')
  , blog         = require('magic-blog')
  , db           = require('magic-db')
  , router       = require('magic-router')
  , utils        = require('magic-utils')
  , morgan       = require('morgan')
  , path         = require('path')
  , favicon      = require('serve-favicon')
  , stylus       = require('stylus')
;

module.exports = function(M, app, dir) {
  var css         = ( app.get('css') || stylus )
    , env         = app.get('env') || 'production'
    , faviconPath = path.join(dir, 'public', 'favicon.ico')
    , dirs        = app.get('dirs') || {
        public: path.join(dir, app.get('publicDir') || 'public')
      , views : path.join(dir, app.get('viewsDir') || 'views')
    }
  ;

  if ( app.get('db') && app.disabled('dbSetUp') ) {
    app.enable('dbSetUp');
    let dbConfig = app.get('db');

    if ( dbConfig.name ) {
      app.use(function (req, res, next) {
        db(dbConfig, function (err) {
          if ( err ) { throw err; }
          next();
        });
      });
    }
  }

  app.set('dirs', dirs);

  app.use(function (req, res, next) {
    req.app = app;
    next();
  });

  //set expiry headers
  app.use(headers);

  if ( app.get('basicAuth') ) {
    app.use(
      basicAuth( app.get('basicAuth') )
    );
  }

  //fs.existsSync only gets called once on first request
  if ( ! app.get('faviconChecked') && ! app.get('faviconExists') ) {
    app.set('faviconChecked', true);
    app.set('faviconExists', fs.existsSync(faviconPath));
  }

  if ( app.get('faviconExists') ) {
    app.use( favicon(faviconPath) );
  }

  app.set('views', dirs.views);
  app.set('view engine', app.get('view engine') || 'jade');

  app.use(compression({ threshold: 128 }));

  app.use( css.middleware(dirs.public, {maxAge: '1d'}) );
  app.use( express.static(dirs.public, {maxAge: '1d'}) );

  if ( app.get('blogRoot') ) {
    let blogRoot = app.get('blogRoot');
    if ( typeof blogRoot !== 'string' && typeof blogRoot !== 'number' ) {
      blogRoot = 'blog';
    }
    if ( blogRoot.charAt(0) !== '/' ) {
      blogRoot = '/' + blogRoot;
    } else {
      app.set('blogRoot', blogRoot.substr(1) );
    }
    app.use( blogRoot, blog );
  }

  //logging
  app.use(morgan('combined'));

  //if host sets bodyparser to true, init it
  if ( app.enabled('bodyParser') ) {
    app.use(bodyParser.json());
    app.use(bodyParser.urlencoded({ extended: false }));
  }

  //if host sets cookieparser to true, init it:
  if ( app.enabled('cookieParser') ) {
    app.use(cookieParser());
  }

  //load host specific router
  if ( app.get('router') ) {
    let routes = app.get('router');

    if ( typeof routes === 'array' || typeof routes === 'object' ) {
      utils.each(routes, function (route) {
        app.use(route);
      });
    } else if ( typeof routes === 'function' ) {
      app.use(routes);
    }
  }

  //default router
  app.use(router);

  //we are in a 404 error
  app.use(errorHandler.handle404);

  //oops, worst case fallback, 500 server error.
  app.use(errorHandler.handle500);

  return app;
}
<|endoftext|>


<commit_before>'use strict';

const Botkit = require('botkit');
const mvn = require('./lib/mvn');
const git = require('./lib/git');
const tsm = require('./lib/tsm');
const killall = require('./lib/killall');
const rm = require('./lib/rm');
const ssh = require('./lib/ssh');

const controller = Botkit.slackbot({
    debug: false
});

const erroMsg = 'I got a error, please contact some admin to check that.';
let updating = false;

const errorOnCallBack = (bot, message, code) => {
    if (code != 0) {
        bot.reply(message, `<@${message.user}>, ${erroMsg}`);
        updating = false;
        return true;
    }
    return false;
};

controller.spawn({
    token: process.env.SLACK_API_TOKEN || '',
}).startRTM();

controller.hears(['^update qa-master$'], ['direct_message', 'direct_mention', 'mention'], (bot, message) => {
    try {
        if (updating) {
            bot.reply(message, `<@${message.user}>, I'm doing it exactly now, process still running. Keep calm and drink a water.`);
            return;
        }

        bot.reply(message, `<@${message.user}> Sure.. give me few minutes I'm updating it.`);

        killall.kill('java');
        ssh.sshCommand(process.env.SLACK_BOT_SSH_KEY, process.env.SLACK_BOT_SSH_USER, process.env.SLACK_BOT_SSH_SERVER, 'killall java');
        updating = true;

        let gitCloneResult = git.clone(process.env.SLACK_BOT_USERNAME, process.env.SLACK_BOT_PASSWORD, process.env.SLACK_BOT_USER_REPO, process.env.SLACK_BOT_PROJECT);

        //const { code, signal } = await defineOnExit(gitCloneResult)

        gitCloneResult.gitResult.on('exit', (code, signal) => {
            if (errorOnCallBack(bot, message, code)) return;

            let compileResult = mvn.run(`${gitCloneResult.folder}/${gitCloneResult.projectName}`, ['clean', 'install', '-DskipTests', '-Dsettings.offline=true']);

            compileResult.on('exit', (code, signal) => {
                if (errorOnCallBack(bot, message, code)) {
                    rm.remove(gitCloneResult.folder)
                    return;
                }

                let tsmResult = tsm.runAction('qa-tag', 'deployall', process.env.SLACK_TSM_PATH);
                tsmResult.on('exit', (code, signal) => {
                    if (errorOnCallBack(bot, message, code)) return;

                    bot.reply(message, `<@${message.user}> qa-master updated. :)`);
                    updating = false;
                    return;
                });
            });
        });
    } catch (err) {
        console.log(err);
        updating = false;
    }
}); <commit_msg>Fix compile result<commit_after>'use strict';

const Botkit = require('botkit');
const mvn = require('./lib/mvn');
const git = require('./lib/git');
const tsm = require('./lib/tsm');
const killall = require('./lib/killall');
const rm = require('./lib/rm');
const ssh = require('./lib/ssh');

const controller = Botkit.slackbot({
    debug: false
});

const erroMsg = 'I got a error, please contact some admin to check that.';
let updating = false;

const errorOnCallBack = (bot, message, code) => {
    if (code != 0) {
        bot.reply(message, `<@${message.user}>, ${erroMsg}`);
        updating = false;
        return true;
    }
    return false;
};

controller.spawn({
    token: process.env.SLACK_API_TOKEN || '',
}).startRTM();

controller.hears(['^update qa-master$'], ['direct_message', 'direct_mention', 'mention'], (bot, message) => {
    try {
        if (updating) {
            bot.reply(message, `<@${message.user}>, I'm doing it exactly now, process still running. Keep calm and drink a water.`);
            return;
        }

        bot.reply(message, `<@${message.user}> Sure.. give me few minutes I'm updating it.`);

        killall.kill('java');
        ssh.sshCommand(process.env.SLACK_BOT_SSH_KEY, process.env.SLACK_BOT_SSH_USER, process.env.SLACK_BOT_SSH_SERVER, 'killall java');
        updating = true;

        let gitCloneResult = git.clone(process.env.SLACK_BOT_USERNAME, process.env.SLACK_BOT_PASSWORD, process.env.SLACK_BOT_USER_REPO, process.env.SLACK_BOT_PROJECT);

        //const { code, signal } = await defineOnExit(gitCloneResult)

        gitCloneResult.gitResult.on('exit', (code, signal) => {
            if (errorOnCallBack(bot, message, code)) return;

            let compileResult = mvn.run(`${gitCloneResult.folder}/${gitCloneResult.projectName}`, ['clean', 'install', '-DskipTests', '-Dsettings.offline=true']);

            compileResult.on('exit', (code, signal) => {
                rm.remove(gitCloneResult.folder)
                if (errorOnCallBack(bot, message, code)) {
                    return;
                }

                let tsmResult = tsm.runAction('qa-tag', 'deployall', process.env.SLACK_TSM_PATH);
                tsmResult.on('exit', (code, signal) => {
                    if (errorOnCallBack(bot, message, code)) return;

                    bot.reply(message, `<@${message.user}> qa-master updated. :)`);
                    updating = false;
                    return;
                });
            });
        });
    } catch (err) {
        console.log(err);
        updating = false;
    }
}); <|endoftext|>