<commit_before>import {
  LOAD_ALL_PLAYLISTS_START, LOAD_ALL_PLAYLISTS_COMPLETE,
  LOAD_PLAYLIST_START, LOAD_PLAYLIST_COMPLETE,
  SELECT_PLAYLIST,
  ACTIVATE_PLAYLIST_START, ACTIVATE_PLAYLIST_COMPLETE,
  CREATE_PLAYLIST_START, CREATE_PLAYLIST_COMPLETE,
  OPEN_ADD_MEDIA_MENU, CLOSE_ADD_MEDIA_MENU,
  ADD_MEDIA_START, ADD_MEDIA_COMPLETE,
  REMOVE_MEDIA_START, REMOVE_MEDIA_COMPLETE,
  MOVE_MEDIA_START, MOVE_MEDIA_COMPLETE,
  UPDATE_MEDIA_START, UPDATE_MEDIA_COMPLETE
} from '../constants/actionTypes/playlists';
import { openEditMediaDialog } from './DialogActionCreators';
import { del, get, post, put } from '../utils/Request';
import { playlistsSelector } from '../selectors/playlistSelectors';
import { tokenSelector } from '../selectors/userSelectors';

export function setPlaylists(playlists) {
  return {
    type: LOAD_ALL_PLAYLISTS_COMPLETE,
    payload: { playlists }
  };
}

export function flattenPlaylistItem(item) {
  return {
    ...item.media,
    ...item
  };
}

const inFlightPlaylists = {};
export function loadPlaylist(playlistID, page = 0) {
  return (dispatch, getState) => {
    const key = `${playlistID}:${page}`;

    // Prevent duplicate loading.
    if (inFlightPlaylists[key]) return;

    const jwt = tokenSelector(getState());

    dispatch({
      type: LOAD_PLAYLIST_START,
      payload: { playlistID },
      meta: { page }
    });

    inFlightPlaylists[key] = true;
    get(jwt, `/v1/playlists/${playlistID}/media`, { page, limit: 50 })
      .then(res => res.json())
      .then(res => {
        inFlightPlaylists[key] = false;
        dispatch({
          type: LOAD_PLAYLIST_COMPLETE,
          payload: {
            playlistID,
            media: res.result.map(flattenPlaylistItem)
          },
          meta: {
            page: res.page,
            pageSize: res.size
          }
        });
      })
      .catch(e => {
        inFlightPlaylists[key] = false;
        dispatch({
          type: LOAD_PLAYLIST_COMPLETE,
          error: true,
          payload: e,
          meta: { page }
        });
      });
  };
}

export function selectPlaylist(playlistID) {
  return dispatch => {
    dispatch({
      type: SELECT_PLAYLIST,
      payload: { playlistID }
    });

    if (playlistID) {
      dispatch(loadPlaylist(playlistID));
    }
  };
}

export function activatePlaylistStart(playlistID) {
  return {
    type: ACTIVATE_PLAYLIST_START,
    payload: { playlistID }
  };
}

export function activatePlaylistComplete(playlistID) {
  return {
    type: ACTIVATE_PLAYLIST_COMPLETE,
    payload: { playlistID }
  };
}

export function activatePlaylist(playlistID) {
  return (dispatch, getState) => {
    const jwt = tokenSelector(getState());

    dispatch(activatePlaylistStart(playlistID));
    put(jwt, `/v1/playlists/${playlistID}/activate`)
      .then(() => dispatch(activatePlaylistComplete(playlistID)))
      .catch(error => {
        dispatch({
          type: ACTIVATE_PLAYLIST_COMPLETE,
          error: true,
          payload: error,
          meta: { playlistID }
        });
      });
  };
}

export function loadPlaylistsStart() {
  return { type: LOAD_ALL_PLAYLISTS_START };
}

export function loadPlaylistsComplete(playlists) {
  return {
    type: LOAD_ALL_PLAYLISTS_COMPLETE,
    payload: { playlists }
  };
}

export function loadPlaylists() {
  return (dispatch, getState) => {
    const jwt = tokenSelector(getState());

    dispatch(loadPlaylistsStart());
    get(jwt, '/v1/playlists')
      .then(res => res.json())
      .then(playlists => dispatch(loadPlaylistsComplete(playlists)))
      .catch(error => {
        dispatch({
          type: LOAD_ALL_PLAYLISTS_COMPLETE,
          error: true,
          payload: error
        });
      });
  };
}

export function createPlaylistStart(props, tempId) {
  return {
    type: CREATE_PLAYLIST_START,
    payload: props,
    meta: { tempId }
  };
}

export function createPlaylistComplete(playlist, tempId) {
  return {
    type: CREATE_PLAYLIST_COMPLETE,
    payload: { playlist },
    meta: { tempId }
  };
}

function doCreatePlaylist(name) {
  return (dispatch, getState) => {
    const jwt = tokenSelector(getState());

    const tempId = -Date.now();
    const description = '';
    const shared = false;
    dispatch(createPlaylistStart({ name, description, shared }, tempId));

    post(jwt, '/v1/playlists', { name, description, shared })
      .then(res => res.json())
      .then(playlist => dispatch(createPlaylistComplete(playlist, tempId)))
      .catch(error => {
        dispatch({
          type: CREATE_PLAYLIST_COMPLETE,
          error: true,
          payload: error,
          meta: { tempId }
        });
      });
  };
}

export function createPlaylist() {
  return dispatch => {
    const name = prompt('Playlist name?');
    if (name) {
      dispatch(doCreatePlaylist(name));
    }
  };
}

export function addMediaMenu(items, position) {
  return (dispatch, getState) => {
    const playlists = playlistsSelector(getState());
    dispatch({
      type: OPEN_ADD_MEDIA_MENU,
      payload: {
        media: items
      },
      meta: {
        playlists,
        position,
        type: 'add'
      }
    });
  };
}

export function closeAddMediaMenu() {
  return { type: CLOSE_ADD_MEDIA_MENU };
}

export function addMediaStart(playlistID, media, afterID) {
  return {
    type: ADD_MEDIA_START,
    payload: { playlistID, media, afterID }
  };
}

export function addMediaComplete(playlistID, newSize, insert) {
  return {
    type: ADD_MEDIA_COMPLETE,
    payload: {
      playlistID,
      newSize,
      afterID: insert.afterID,
      appendedMedia: insert.media
    }
  };
}

export function addMedia(playlist, items, afterID = null) {
  return (dispatch, getState) => {
    const jwt = tokenSelector(getState());

    dispatch(addMediaStart(playlist._id, items, afterID));

    post(jwt, `/v1/playlists/${playlist._id}/media`, { items, after: afterID })
      .then(res => res.json())
      .then(({ added, playlistSize }) =>
        dispatch(addMediaComplete(
          playlist._id,
          playlistSize,
          { afterID, media: added.map(flattenPlaylistItem) }
        ))
      )
      .catch(error => {
        dispatch({
          type: ADD_MEDIA_COMPLETE,
          error: true,
          payload: error
        });
      });
  };
}

export function editMedia(playlistID, media) {
  return openEditMediaDialog(playlistID, media);
}

export function updateMediaStart(playlistID, mediaID, props) {
  return {
    type: UPDATE_MEDIA_START,
    payload: { playlistID, mediaID, props }
  };
}

export function updateMediaComplete(playlistID, mediaID, media) {
  return {
    type: UPDATE_MEDIA_COMPLETE,
    payload: { playlistID, mediaID, media }
  };
}

export function updateMedia(playlistID, mediaID, props) {
  return (dispatch, getState) => {
    const jwt = tokenSelector(getState());
    dispatch(updateMediaStart(playlistID, mediaID, props));
    put(jwt, `/v1/playlists/${playlistID}/media/${mediaID}`, props)
      .then(res => res.json())
      .then(media => dispatch(updateMediaComplete(playlistID, mediaID, media)))
      .catch(err => dispatch({
        type: UPDATE_MEDIA_COMPLETE,
        payload: err,
        error: true,
        meta: { playlistID, mediaID, props }
      }));
  };
}

export function removeMediaStart(playlistID, items) {
  return {
    type: REMOVE_MEDIA_START,
    payload: { playlistID, medias: items }
  };
}

export function removeMediaComplete(playlistID, newSize, removedMedia) {
  return {
    type: REMOVE_MEDIA_COMPLETE,
    payload: {
      playlistID,
      newSize,
      removedMedia
    }
  };
}

export function removeMedia(playlistID, items) {
  return (dispatch, getState) => {
    const jwt = tokenSelector(getState());
    dispatch(removeMediaStart(playlistID, items));
    const itemIDs = medias.map(media => media._id);
    del(jwt, `/v1/playlists/${playlistID}/media`, { items: itemIDs })
      .then(res => res.json())
      .then(({ removed, playlistSize }) =>
        dispatch(removeMediaComplete(
          playlistID,
          playlistSize,
          removed.map(flattenPlaylistItem)
        ))
      )
      .catch(error => {
        dispatch({
          type: REMOVE_MEDIA_COMPLETE,
          error: true,
          payload: error
        });
      });
  };
}

export function moveMediaStart(playlistID, items, afterID) {
  return {
    type: MOVE_MEDIA_START,
    payload: { playlistID, afterID, medias: items }
  };
}

export function moveMediaComplete(playlistID, items, afterID) {
  return {
    type: MOVE_MEDIA_COMPLETE,
    payload: { playlistID, afterID, medias: items }
  };
}

export function moveMedia(playlistID, medias, afterID) {
  return (dispatch, getState) => {
    const jwt = tokenSelector(getState());

    dispatch(moveMediaStart(playlistID, medias, afterID));
    const items = medias.map(media => media._id);
    put(jwt, `/v1/playlists/${playlistID}/move`, { items, after: afterID })
      .then(res => res.json())
      .then(() => dispatch(moveMediaComplete(playlistID, medias, afterID)))
      .catch(error => {
        dispatch({
          type: MOVE_MEDIA_COMPLETE,
          error: true,
          payload: error,
          meta: { playlistID, medias, afterID }
        });
      });
  };
}
<commit_msg>playlists: fix undefined var when removing media from playlist<commit_after>import {
  LOAD_ALL_PLAYLISTS_START, LOAD_ALL_PLAYLISTS_COMPLETE,
  LOAD_PLAYLIST_START, LOAD_PLAYLIST_COMPLETE,
  SELECT_PLAYLIST,
  ACTIVATE_PLAYLIST_START, ACTIVATE_PLAYLIST_COMPLETE,
  CREATE_PLAYLIST_START, CREATE_PLAYLIST_COMPLETE,
  OPEN_ADD_MEDIA_MENU, CLOSE_ADD_MEDIA_MENU,
  ADD_MEDIA_START, ADD_MEDIA_COMPLETE,
  REMOVE_MEDIA_START, REMOVE_MEDIA_COMPLETE,
  MOVE_MEDIA_START, MOVE_MEDIA_COMPLETE,
  UPDATE_MEDIA_START, UPDATE_MEDIA_COMPLETE
} from '../constants/actionTypes/playlists';
import { openEditMediaDialog } from './DialogActionCreators';
import { del, get, post, put } from '../utils/Request';
import { playlistsSelector } from '../selectors/playlistSelectors';
import { tokenSelector } from '../selectors/userSelectors';

export function setPlaylists(playlists) {
  return {
    type: LOAD_ALL_PLAYLISTS_COMPLETE,
    payload: { playlists }
  };
}

export function flattenPlaylistItem(item) {
  return {
    ...item.media,
    ...item
  };
}

const inFlightPlaylists = {};
export function loadPlaylist(playlistID, page = 0) {
  return (dispatch, getState) => {
    const key = `${playlistID}:${page}`;

    // Prevent duplicate loading.
    if (inFlightPlaylists[key]) return;

    const jwt = tokenSelector(getState());

    dispatch({
      type: LOAD_PLAYLIST_START,
      payload: { playlistID },
      meta: { page }
    });

    inFlightPlaylists[key] = true;
    get(jwt, `/v1/playlists/${playlistID}/media`, { page, limit: 50 })
      .then(res => res.json())
      .then(res => {
        inFlightPlaylists[key] = false;
        dispatch({
          type: LOAD_PLAYLIST_COMPLETE,
          payload: {
            playlistID,
            media: res.result.map(flattenPlaylistItem)
          },
          meta: {
            page: res.page,
            pageSize: res.size
          }
        });
      })
      .catch(e => {
        inFlightPlaylists[key] = false;
        dispatch({
          type: LOAD_PLAYLIST_COMPLETE,
          error: true,
          payload: e,
          meta: { page }
        });
      });
  };
}

export function selectPlaylist(playlistID) {
  return dispatch => {
    dispatch({
      type: SELECT_PLAYLIST,
      payload: { playlistID }
    });

    if (playlistID) {
      dispatch(loadPlaylist(playlistID));
    }
  };
}

export function activatePlaylistStart(playlistID) {
  return {
    type: ACTIVATE_PLAYLIST_START,
    payload: { playlistID }
  };
}

export function activatePlaylistComplete(playlistID) {
  return {
    type: ACTIVATE_PLAYLIST_COMPLETE,
    payload: { playlistID }
  };
}

export function activatePlaylist(playlistID) {
  return (dispatch, getState) => {
    const jwt = tokenSelector(getState());

    dispatch(activatePlaylistStart(playlistID));
    put(jwt, `/v1/playlists/${playlistID}/activate`)
      .then(() => dispatch(activatePlaylistComplete(playlistID)))
      .catch(error => {
        dispatch({
          type: ACTIVATE_PLAYLIST_COMPLETE,
          error: true,
          payload: error,
          meta: { playlistID }
        });
      });
  };
}

export function loadPlaylistsStart() {
  return { type: LOAD_ALL_PLAYLISTS_START };
}

export function loadPlaylistsComplete(playlists) {
  return {
    type: LOAD_ALL_PLAYLISTS_COMPLETE,
    payload: { playlists }
  };
}

export function loadPlaylists() {
  return (dispatch, getState) => {
    const jwt = tokenSelector(getState());

    dispatch(loadPlaylistsStart());
    get(jwt, '/v1/playlists')
      .then(res => res.json())
      .then(playlists => dispatch(loadPlaylistsComplete(playlists)))
      .catch(error => {
        dispatch({
          type: LOAD_ALL_PLAYLISTS_COMPLETE,
          error: true,
          payload: error
        });
      });
  };
}

export function createPlaylistStart(props, tempId) {
  return {
    type: CREATE_PLAYLIST_START,
    payload: props,
    meta: { tempId }
  };
}

export function createPlaylistComplete(playlist, tempId) {
  return {
    type: CREATE_PLAYLIST_COMPLETE,
    payload: { playlist },
    meta: { tempId }
  };
}

function doCreatePlaylist(name) {
  return (dispatch, getState) => {
    const jwt = tokenSelector(getState());

    const tempId = -Date.now();
    const description = '';
    const shared = false;
    dispatch(createPlaylistStart({ name, description, shared }, tempId));

    post(jwt, '/v1/playlists', { name, description, shared })
      .then(res => res.json())
      .then(playlist => dispatch(createPlaylistComplete(playlist, tempId)))
      .catch(error => {
        dispatch({
          type: CREATE_PLAYLIST_COMPLETE,
          error: true,
          payload: error,
          meta: { tempId }
        });
      });
  };
}

export function createPlaylist() {
  return dispatch => {
    const name = prompt('Playlist name?');
    if (name) {
      dispatch(doCreatePlaylist(name));
    }
  };
}

export function addMediaMenu(items, position) {
  return (dispatch, getState) => {
    const playlists = playlistsSelector(getState());
    dispatch({
      type: OPEN_ADD_MEDIA_MENU,
      payload: {
        media: items
      },
      meta: {
        playlists,
        position,
        type: 'add'
      }
    });
  };
}

export function closeAddMediaMenu() {
  return { type: CLOSE_ADD_MEDIA_MENU };
}

export function addMediaStart(playlistID, media, afterID) {
  return {
    type: ADD_MEDIA_START,
    payload: { playlistID, media, afterID }
  };
}

export function addMediaComplete(playlistID, newSize, insert) {
  return {
    type: ADD_MEDIA_COMPLETE,
    payload: {
      playlistID,
      newSize,
      afterID: insert.afterID,
      appendedMedia: insert.media
    }
  };
}

export function addMedia(playlist, items, afterID = null) {
  return (dispatch, getState) => {
    const jwt = tokenSelector(getState());

    dispatch(addMediaStart(playlist._id, items, afterID));

    post(jwt, `/v1/playlists/${playlist._id}/media`, { items, after: afterID })
      .then(res => res.json())
      .then(({ added, playlistSize }) =>
        dispatch(addMediaComplete(
          playlist._id,
          playlistSize,
          { afterID, media: added.map(flattenPlaylistItem) }
        ))
      )
      .catch(error => {
        dispatch({
          type: ADD_MEDIA_COMPLETE,
          error: true,
          payload: error
        });
      });
  };
}

export function editMedia(playlistID, media) {
  return openEditMediaDialog(playlistID, media);
}

export function updateMediaStart(playlistID, mediaID, props) {
  return {
    type: UPDATE_MEDIA_START,
    payload: { playlistID, mediaID, props }
  };
}

export function updateMediaComplete(playlistID, mediaID, media) {
  return {
    type: UPDATE_MEDIA_COMPLETE,
    payload: { playlistID, mediaID, media }
  };
}

export function updateMedia(playlistID, mediaID, props) {
  return (dispatch, getState) => {
    const jwt = tokenSelector(getState());
    dispatch(updateMediaStart(playlistID, mediaID, props));
    put(jwt, `/v1/playlists/${playlistID}/media/${mediaID}`, props)
      .then(res => res.json())
      .then(media => dispatch(updateMediaComplete(playlistID, mediaID, media)))
      .catch(err => dispatch({
        type: UPDATE_MEDIA_COMPLETE,
        payload: err,
        error: true,
        meta: { playlistID, mediaID, props }
      }));
  };
}

export function removeMediaStart(playlistID, items) {
  return {
    type: REMOVE_MEDIA_START,
    payload: { playlistID, medias: items }
  };
}

export function removeMediaComplete(playlistID, newSize, removedMedia) {
  return {
    type: REMOVE_MEDIA_COMPLETE,
    payload: {
      playlistID,
      newSize,
      removedMedia
    }
  };
}

export function removeMedia(playlistID, items) {
  return (dispatch, getState) => {
    const jwt = tokenSelector(getState());
    dispatch(removeMediaStart(playlistID, items));
    const itemIDs = items.map(media => media._id);
    del(jwt, `/v1/playlists/${playlistID}/media`, { items: itemIDs })
      .then(res => res.json())
      .then(({ removed, playlistSize }) =>
        dispatch(removeMediaComplete(
          playlistID,
          playlistSize,
          removed.map(flattenPlaylistItem)
        ))
      )
      .catch(error => {
        dispatch({
          type: REMOVE_MEDIA_COMPLETE,
          error: true,
          payload: error
        });
      });
  };
}

export function moveMediaStart(playlistID, items, afterID) {
  return {
    type: MOVE_MEDIA_START,
    payload: { playlistID, afterID, medias: items }
  };
}

export function moveMediaComplete(playlistID, items, afterID) {
  return {
    type: MOVE_MEDIA_COMPLETE,
    payload: { playlistID, afterID, medias: items }
  };
}

export function moveMedia(playlistID, medias, afterID) {
  return (dispatch, getState) => {
    const jwt = tokenSelector(getState());

    dispatch(moveMediaStart(playlistID, medias, afterID));
    const items = medias.map(media => media._id);
    put(jwt, `/v1/playlists/${playlistID}/move`, { items, after: afterID })
      .then(res => res.json())
      .then(() => dispatch(moveMediaComplete(playlistID, medias, afterID)))
      .catch(error => {
        dispatch({
          type: MOVE_MEDIA_COMPLETE,
          error: true,
          payload: error,
          meta: { playlistID, medias, afterID }
        });
      });
  };
}
<|endoftext|>


<commit_before>const { BaseController } = require('./base_controller');
const { Job } = require('../../core/job');
const { JobForm } = require('../forms/job_form');
const Templates = require('../common/templates');

const typeMap = {
    limit: 'number',
    offset: 'number',
}

class JobController extends BaseController {

    constructor(params) {
        super(params, 'Jobs');
        this.typeMap = typeMap;

        this.model = Job;
        this.formClass = JobForm;
        //this.formTemplate = Templates.jobForm;
        this.listTemplate = Templates.jobList;
        this.nameProperty = 'name';
        this.defaultOrderBy = 'createdAt';
        this.defaultSortDirection = 'desc';
    }

    new() {
        let job = new Job();
        job.save();
        this.params.set('id', job.id);
        console.log(job.id);
        return this.files();
    }

    files() {
        let job = Job.find(this.params.get('id'));
        let errors = '';  //this._getPageLevelErrors(profile);
        let data = {
            alertMessage: this.alertMessage,
            job: job
        }
        this.alertMessage = null;
        let html = Templates.jobFiles(data);
        return this.containerContent(html);
    }

    // update() {
    //     return this.containerContent('Update Job');
    // }

    list() {
        let listParams = this.paramsToHash();
        listParams.orderBy = listParams.sortBy || this.defaultOrderBy;
        listParams.sortDirection = listParams.sortOrder || this.defaultSortDirection;
        let jobs = Job.list(null, listParams);
        this.colorCodeJobs(jobs);
        let data = {
            alertMessage: this.alertMessage,
            items: jobs
        };
        let html = this.listTemplate(data);
        return this.containerContent(html);
    }

    /**
     * This adds some custom display properties to each jobs hash
     * so we can color-code the display.
     */
    colorCodeJobs(jobs) {
        for(let i=0; i < jobs.length; i++) {
            let job = Job.inflateFrom(jobs[i]);
            job.pkgDate = '-';
            job.valDate = '-';
            job.uploadDate = '-';
            if (job.packageAttempted()) {
                job.pkgColor = (job.packageSucceeded() ? 'text-success' : 'text-danger');
                job.pkgDate = dateFormat(job.packagedAt(), 'shortDate');
            }
            if (job.validationAttempted()) {
                job.valColor = (job.validateSucceeded() ? 'text-success' : 'text-danger');
                job.valDate = dateFormat(job.validatedAt(), 'shortDate');
            }
            if (job.uploadAttempted()) {
                job.uploadColor = (job.uploadSucceeded() ? 'text-success' : 'text-danger');
                job.uploadDate = dateFormat(job.uploadedAt(), 'shortDate');
            }
            jobs[i] = job;
        }
    }

    // destroy() {
    //     return this.containerContent('Destroy Job');
    // }
}

module.exports.JobController = JobController;
<commit_msg>Added postRenderCallback and attachDragAndDropEvents to Job controller<commit_after>const { BaseController } = require('./base_controller');
const { Job } = require('../../core/job');
const { JobForm } = require('../forms/job_form');
const Templates = require('../common/templates');

const typeMap = {
    limit: 'number',
    offset: 'number',
}

class JobController extends BaseController {

    constructor(params) {
        super(params, 'Jobs');
        this.typeMap = typeMap;

        this.model = Job;
        this.formClass = JobForm;
        //this.formTemplate = Templates.jobForm;
        this.listTemplate = Templates.jobList;
        this.nameProperty = 'name';
        this.defaultOrderBy = 'createdAt';
        this.defaultSortDirection = 'desc';
    }

    new() {
        let job = new Job();
        job.save();
        this.params.set('id', job.id);
        console.log(job.id);
        return this.files();
    }

    files() {
        let job = Job.find(this.params.get('id'));
        let errors = '';  //this._getPageLevelErrors(profile);
        let data = {
            alertMessage: this.alertMessage,
            job: job
        }
        this.alertMessage = null;
        let html = Templates.jobFiles(data);
        return this.containerContent(html);
    }

    // update() {
    //     return this.containerContent('Update Job');
    // }

    list() {
        let listParams = this.paramsToHash();
        listParams.orderBy = listParams.sortBy || this.defaultOrderBy;
        listParams.sortDirection = listParams.sortOrder || this.defaultSortDirection;
        let jobs = Job.list(null, listParams);
        this.colorCodeJobs(jobs);
        let data = {
            alertMessage: this.alertMessage,
            items: jobs
        };
        let html = this.listTemplate(data);
        return this.containerContent(html);
    }

    /**
     * This adds some custom display properties to each jobs hash
     * so we can color-code the display.
     */
    colorCodeJobs(jobs) {
        for(let i=0; i < jobs.length; i++) {
            let job = Job.inflateFrom(jobs[i]);
            job.pkgDate = '-';
            job.valDate = '-';
            job.uploadDate = '-';
            if (job.packageAttempted()) {
                job.pkgColor = (job.packageSucceeded() ? 'text-success' : 'text-danger');
                job.pkgDate = dateFormat(job.packagedAt(), 'shortDate');
            }
            if (job.validationAttempted()) {
                job.valColor = (job.validateSucceeded() ? 'text-success' : 'text-danger');
                job.valDate = dateFormat(job.validatedAt(), 'shortDate');
            }
            if (job.uploadAttempted()) {
                job.uploadColor = (job.uploadSucceeded() ? 'text-success' : 'text-danger');
                job.uploadDate = dateFormat(job.uploadedAt(), 'shortDate');
            }
            jobs[i] = job;
        }
    }

    // destroy() {
    //     return this.containerContent('Destroy Job');
    // }

    postRenderCallback(fnName) {
        let job = Job.find(this.params.get('id'));
        if (fnName == 'files') {
            this.attachDragAndDropEvents();
        }
    }

    attachDragAndDropEvents() {
        $('#dropZone').on('drop', function(e) {
            e.preventDefault();
            e.stopPropagation();
            console.log('drop');
            // When drag event is attached to document, use
            // e.dataTransfer.files instead of what's below.
            for (let f of e.originalEvent.dataTransfer.files) {
                //jobFiles.addFile(f.path);
                console.log(f.path);
            }
            $(e.currentTarget).removeClass('drop-zone-over');
            return false;
        });
        $('#dropZone').on('dragover', function(e) {
            e.preventDefault();
            e.stopPropagation();
            console.log('dragover');
            $(e.currentTarget).addClass('drop-zone-over');
            return false;
        });
        $('#dropZone').on('dragleave', function(e) {
            e.preventDefault();
            e.stopPropagation();
            console.log('dragleave');
            $(e.currentTarget).removeClass('drop-zone-over');
            return false;
        });
        $('#dropZone').on('dragend', function(e) {
            e.preventDefault();
            e.stopPropagation();
            console.log('dragend');
            $(e.currentTarget).removeClass('drop-zone-over');
            return false;
        });
    }
}

module.exports.JobController = JobController;
<|endoftext|>


<commit_before>/**
 * @fileoverview Implement 'currentTransform' of CanvasRenderingContext2D prototype (polyfill)
 * @author Stefan Goessner (c) 2015
 */

// test modification via github !!

/**
 * extend CanvasRenderingContext2D.prototype by current transformation matrix access.
 */
if (!("currentTransform" in CanvasRenderingContext2D.prototype)) {
/**
 * define property 'currentTransform'
 */
   if ("mozCurrentTransform" in CanvasRenderingContext2D.prototype) {
      Object.defineProperty(CanvasRenderingContext2D.prototype, "currentTransform", {
         get : function() { var m = this.mozCurrentTransform; return {a:m[0],b:m[1],c:m[2],d:m[3],e:m[4],f:m[5]}; },
         set : function(x) { this.mozCurrentTransform = [x.a,x.b,x.c,x.d,x.e,x.f]; },
         enumerable : true,
         configurable : false
      });
   }
   else if ("webkitCurrentTransform" in CanvasRenderingContext2D.prototype) {
      Object.defineProperty(CanvasRenderingContext2D.prototype, "currentTransform", {
         get : function() { return this.webkitCurrentTransform; },
         set : function(x) { this.webkitCurrentTransform = x; },
         enumerable : true,
         configurable : false
      });
   }
   else {  // fully implement it ... hmm ... 'currentTransform', 'save()', 'restore()', 'transform()', 'setTransform()', 'resetTransform()'
      Object.defineProperty(CanvasRenderingContext2D.prototype, "currentTransform", {
         get : function() { return this._t2stack && this._t2stack[this._t2stack.length-1] || {a:1,b:0,c:0,d:1,e:0,f:0}; },
         set : function(x) {
            if (!this._t2stack)
               this._t2stack = [{}];
            this._t2stack[this._t2stack.length-1] = {a:x.a,b:x.b,c:x.c,d:x.d,e:x.e,f:x.f};
         },
         enumerable : true,
         configurable : false
      });
      CanvasRenderingContext2D.prototype.save = function() {
         var save = CanvasRenderingContext2D.prototype.save;
         return function() {
            if (!this._t2stack)
               this._t2stack = [{a:1,b:0,c:0,d:1,e:0,f:0}];
            var t = this._t2stack[this._t2stack.length-1];
            this._t2stack.push(t && {a:t.a,b:t.b,c:t.c,d:t.d,e:t.e,f:t.f});
            save.call(this);
         }
      }();
      CanvasRenderingContext2D.prototype.restore = function() {
         var restore = CanvasRenderingContext2D.prototype.restore;
         return function() {
            if (this._t2stack) this._t2stack.pop();
            restore.call(this);
         }
      }();
      CanvasRenderingContext2D.prototype.transform = function() {
         var transform = CanvasRenderingContext2D.prototype.transform;
         return function(a,b,c,d,e,f) {
            if (!this._t2stack)
               this._t2stack = [{a:1,b:0,c:0,d:1,e:0,f:0}];
            var t = this._t2stack[this._t2stack.length-1], q;
            q = t.a; t.a = a*q+c*t.b,   t.b = b*q+d*t.b;
            q = t.c; t.c = a*q+c*t.d,   t.d = b*q+d*t.d;
            q = t.e; t.e = a*q+c*t.f+e; t.f = b*q+d*t.f+f;
            transform.call(this,a,b,c,d,e,f);
         }
      }();
      CanvasRenderingContext2D.prototype.setTransform = function() {
         var setTransform = CanvasRenderingContext2D.prototype.setTransform;
         return function(a,b,c,d,e,f) {
            if (!this._t2stack)
               this._t2stack = [{}];
            this._t2stack[this._t2stack.length-1] = {a:a,b:b,c:c,d:d,e:e,f:f};
            setTransform.call(this,a,b,c,d,e,f);
         }
      }();
      CanvasRenderingContext2D.prototype.resetTransform = function() {
         var resetTransform = CanvasRenderingContext2D.prototype.resetTransform;
         return function() {
            if (!this._t2stack)
               this._t2stack = [{}];
            this._t2stack[this._t2stack.length-1] = {a:1,b:0,c:0,d:1,e:0,f:0};
            resetTransform && resetTransform.call(this);
         }
      }();
      CanvasRenderingContext2D.prototype.scale = function() {
         var scale = CanvasRenderingContext2D.prototype.scale;
         return function(sx,sy) {
            if (!this._t2stack)
               this._t2stack = [{a:1,b:0,c:0,d:1,e:0,f:0}];
            var t = this._t2stack[this._t2stack.length-1];
            sx = sx || 1;
            sy = sy || sx;
            t.a *= sx; t.c *= sx; t.e *= sx;
            t.b *= sy; t.d *= sy; t.f *= sy;
            scale.call(this,sx,sy);
         }
      }();
      CanvasRenderingContext2D.prototype.rotate = function() {
         var rotate = CanvasRenderingContext2D.prototype.rotate;
         return function(w) {
            if (!this._t2stack)
               this._t2stack = [{a:1,b:0,c:0,d:1,e:0,f:0}];
            var t = this._t2stack[this._t2stack.length-1];
            var sw = w?Math.sin(w):0, cw = w?Math.cos(w):1, q;
            q = t.a; t.a = q*cw-t.b*sw;   t.b = q*sw+t.b*cw;
            q = t.c; t.c = q*cw-t.d*sw;   t.d = q*sw+t.d*cw;
            q = t.e; t.e = q*cw-t.f*sw;   t.f = q*sw+t.f*cw;
            return rotate.call(this,w);
         }
      }();
      CanvasRenderingContext2D.prototype.translate = function() {
         var translate = CanvasRenderingContext2D.prototype.translate;
         return function(x,y) {
            if (!this._t2stack)
               this._t2stack = [{a:1,b:0,c:0,d:1,e:0,f:0}];
            var t = this._t2stack[this._t2stack.length-1];
            t.e += x; t.f += y;
            return translate.call(this,x,y);
         }
      }();
   }
}
<commit_msg>remove github test message ...<commit_after>/**
 * @fileoverview Implement 'currentTransform' of CanvasRenderingContext2D prototype (polyfill)
 * @author Stefan Goessner (c) 2015
 */

/**
 * extend CanvasRenderingContext2D.prototype by current transformation matrix access.
 */
if (!("currentTransform" in CanvasRenderingContext2D.prototype)) {
/**
 * define property 'currentTransform'
 */
   if ("mozCurrentTransform" in CanvasRenderingContext2D.prototype) {
      Object.defineProperty(CanvasRenderingContext2D.prototype, "currentTransform", {
         get : function() { var m = this.mozCurrentTransform; return {a:m[0],b:m[1],c:m[2],d:m[3],e:m[4],f:m[5]}; },
         set : function(x) { this.mozCurrentTransform = [x.a,x.b,x.c,x.d,x.e,x.f]; },
         enumerable : true,
         configurable : false
      });
   }
   else if ("webkitCurrentTransform" in CanvasRenderingContext2D.prototype) {
      Object.defineProperty(CanvasRenderingContext2D.prototype, "currentTransform", {
         get : function() { return this.webkitCurrentTransform; },
         set : function(x) { this.webkitCurrentTransform = x; },
         enumerable : true,
         configurable : false
      });
   }
   else {  // fully implement it ... hmm ... 'currentTransform', 'save()', 'restore()', 'transform()', 'setTransform()', 'resetTransform()'
      Object.defineProperty(CanvasRenderingContext2D.prototype, "currentTransform", {
         get : function() { return this._t2stack && this._t2stack[this._t2stack.length-1] || {a:1,b:0,c:0,d:1,e:0,f:0}; },
         set : function(x) {
            if (!this._t2stack)
               this._t2stack = [{}];
            this._t2stack[this._t2stack.length-1] = {a:x.a,b:x.b,c:x.c,d:x.d,e:x.e,f:x.f};
         },
         enumerable : true,
         configurable : false
      });
      CanvasRenderingContext2D.prototype.save = function() {
         var save = CanvasRenderingContext2D.prototype.save;
         return function() {
            if (!this._t2stack)
               this._t2stack = [{a:1,b:0,c:0,d:1,e:0,f:0}];
            var t = this._t2stack[this._t2stack.length-1];
            this._t2stack.push(t && {a:t.a,b:t.b,c:t.c,d:t.d,e:t.e,f:t.f});
            save.call(this);
         }
      }();
      CanvasRenderingContext2D.prototype.restore = function() {
         var restore = CanvasRenderingContext2D.prototype.restore;
         return function() {
            if (this._t2stack) this._t2stack.pop();
            restore.call(this);
         }
      }();
      CanvasRenderingContext2D.prototype.transform = function() {
         var transform = CanvasRenderingContext2D.prototype.transform;
         return function(a,b,c,d,e,f) {
            if (!this._t2stack)
               this._t2stack = [{a:1,b:0,c:0,d:1,e:0,f:0}];
            var t = this._t2stack[this._t2stack.length-1], q;
            q = t.a; t.a = a*q+c*t.b,   t.b = b*q+d*t.b;
            q = t.c; t.c = a*q+c*t.d,   t.d = b*q+d*t.d;
            q = t.e; t.e = a*q+c*t.f+e; t.f = b*q+d*t.f+f;
            transform.call(this,a,b,c,d,e,f);
         }
      }();
      CanvasRenderingContext2D.prototype.setTransform = function() {
         var setTransform = CanvasRenderingContext2D.prototype.setTransform;
         return function(a,b,c,d,e,f) {
            if (!this._t2stack)
               this._t2stack = [{}];
            this._t2stack[this._t2stack.length-1] = {a:a,b:b,c:c,d:d,e:e,f:f};
            setTransform.call(this,a,b,c,d,e,f);
         }
      }();
      CanvasRenderingContext2D.prototype.resetTransform = function() {
         var resetTransform = CanvasRenderingContext2D.prototype.resetTransform;
         return function() {
            if (!this._t2stack)
               this._t2stack = [{}];
            this._t2stack[this._t2stack.length-1] = {a:1,b:0,c:0,d:1,e:0,f:0};
            resetTransform && resetTransform.call(this);
         }
      }();
      CanvasRenderingContext2D.prototype.scale = function() {
         var scale = CanvasRenderingContext2D.prototype.scale;
         return function(sx,sy) {
            if (!this._t2stack)
               this._t2stack = [{a:1,b:0,c:0,d:1,e:0,f:0}];
            var t = this._t2stack[this._t2stack.length-1];
            sx = sx || 1;
            sy = sy || sx;
            t.a *= sx; t.c *= sx; t.e *= sx;
            t.b *= sy; t.d *= sy; t.f *= sy;
            scale.call(this,sx,sy);
         }
      }();
      CanvasRenderingContext2D.prototype.rotate = function() {
         var rotate = CanvasRenderingContext2D.prototype.rotate;
         return function(w) {
            if (!this._t2stack)
               this._t2stack = [{a:1,b:0,c:0,d:1,e:0,f:0}];
            var t = this._t2stack[this._t2stack.length-1];
            var sw = w?Math.sin(w):0, cw = w?Math.cos(w):1, q;
            q = t.a; t.a = q*cw-t.b*sw;   t.b = q*sw+t.b*cw;
            q = t.c; t.c = q*cw-t.d*sw;   t.d = q*sw+t.d*cw;
            q = t.e; t.e = q*cw-t.f*sw;   t.f = q*sw+t.f*cw;
            return rotate.call(this,w);
         }
      }();
      CanvasRenderingContext2D.prototype.translate = function() {
         var translate = CanvasRenderingContext2D.prototype.translate;
         return function(x,y) {
            if (!this._t2stack)
               this._t2stack = [{a:1,b:0,c:0,d:1,e:0,f:0}];
            var t = this._t2stack[this._t2stack.length-1];
            t.e += x; t.f += y;
            return translate.call(this,x,y);
         }
      }();
   }
}
<|endoftext|>


<commit_before>(function () {
    'use strict';

    angular.module('app.core').controller('BookDetailController', BookDetailController);

    BookDetailController.$inject = ['$scope', '$routeParams', 'BookDetailService', 'BookStatusService', 'AuthorService'];

    function BookDetailController($scope, $routeParams, BookDetailService, BookStatusService, AuthorService) {
        $scope.bookStatus = BookStatusService.query();
        $scope.authors = AuthorService.query();

        $scope.submitBook = function submitBook() {
            var bookDAO = new BookDetailService($scope.book);
            bookDAO.$update({id: $scope.book.id});
        };

        BookDetailService.get({id: $routeParams.bookId}, function (data) {
            $scope.book = data;
        });

        $scope.addAuthor = function () {
            $scope.author = $scope.bookData.author;
            $scope.book.authors.push($scope.author);
        };

        $scope.removeAuthor = function (author) {
            var index = $scope.book.authors.indexOf(author);
            if (index != -1) {
                $scope.book.authors.splice(index, 1);
            }
        };
    }
}());
<commit_msg>add redirect to front page on successful book update<commit_after>(function () {
    'use strict';

    angular.module('app.core').controller('BookDetailController', BookDetailController);

    BookDetailController.$inject = ['$scope', '$routeParams', '$location', 'BookDetailService', 'BookStatusService', 'AuthorService'];

    function BookDetailController($scope, $routeParams, $location, BookDetailService, BookStatusService, AuthorService) {
        $scope.bookStatus = BookStatusService.query();
        $scope.authors = AuthorService.query();

        $scope.submitBook = function submitBook() {
            var bookDAO = new BookDetailService($scope.book);
            bookDAO.$update({id: $scope.book.id});
            $location.path('/library/public/ui/#/books');
        };

        BookDetailService.get({id: $routeParams.bookId}, function (data) {
            $scope.book = data;
        });

        $scope.addAuthor = function () {
            $scope.author = $scope.bookData.author;
            $scope.book.authors.push($scope.author);
        };

        $scope.removeAuthor = function (author) {
            var index = $scope.book.authors.indexOf(author);
            if (index !== -1) {
                $scope.book.authors.splice(index, 1);
            }
        };
    }
}());
<|endoftext|>


<commit_before>'use strict';

// TODO
// better callouts for cycle
// tank provoke messages when cotank has flare

[{
  zoneRegex: /(^Eden's Gate: Descent \(Savage\)$|Unknown Zone \(356\))/,
  timelineFile: 'e2s.txt',
  timelineTriggers: [
    {
      id: 'E2S Punishing Ray',
      regex: /Punishing Ray/,
      beforeSeconds: 9,
      infoText: {
        en: 'Get Puddles',
        fr: 'Prenez les rayons',
      },
    },
    {
      id: 'E2S Buddy Circles',
      regex: /Light\/Dark Circles/,
      beforeSeconds: 5,
      alarmText: {
        en: 'Stack With Partner',
      },
    },
  ],
  triggers: [
    {
      id: 'E2S Spell In Waiting Gain',
      regex: / 1A:\y{ObjectId}:Voidwalker gains the effect of Spell-In-Waiting/,
      run: function(data) {
        data.waiting = true;
      },
    },
    {
      id: 'E2S Spell In Waiting Lose',
      regex: / 1E:\y{ObjectId}:Voidwalker loses the effect of Spell-In-Waiting/,
      run: function(data) {
        data.waiting = false;
      },
    },
    {
      id: 'E2S Entropy',
      regex: / 14:3E6F:Voidwalker starts using (?:Entropy|)/,
      regexFr: / 14:3E6F:Marcheuse Du Néant starts using (?:Entropie|)/,
      condition: function(data, matches) {
        return data.role == 'healer';
      },
      infoText: {
        en: 'aoe',
        fr: 'Dégâts de zone',
      },
    },
    {
      id: 'E2S Quietus',
      regex: / 14:3E71:Voidwalker starts using (?:Quietus|)/,
      condition: function(data, matches) {
        return data.role == 'healer';
      },
      infoText: {
        en: 'aoe',
        fr: 'Dégâts de zone',
      },
    },
    {
      id: 'E2S Shadowflame Tank',
      regex: / 14:3E6[12]:Voidwalker starts using (?:Shadowflame|Unknown_3E6[12]) on (\y{Name})/,
      regexFr: / 14:3E6[12]:Marcheuse Du Néant starts using (?:Flamme D'ombre|Unknown_3E6[12]) on (\y{Name})/,
      condition: function(data, matches) {
        return data.me == matches[1];
      },
      alertText: {
        en: 'Tank Buster on YOU',
        de: 'Tankbuster auf DIR',
        fr: 'Tankbuster sur VOUS',
      },
    },
    {
      id: 'E2S Shadowflame Healer',
      regex: / 14:3E61:Voidwalker starts using (?:Shadowflame|)/,
      regexFr: / 14:3E61:Marcheuse Du Néant starts using (?:Flamme D'ombre|)/,
      condition: function(data, matches) {
        return data.role == 'healer';
      },
      infoText: {
        en: 'tank busters',
        fr: 'Tank busters',
      },
    },
    {
      id: 'E2S Doomvoid Cleaver',
      regex: / 14:3E63:Voidwalker starts using (?:Doomvoid Cleaver|)/,
      alertText: {
        en: 'Protean',
      },
    },
    {
      id: 'E2S Doomvoid Guillotine',
      regex: / 14:3E4F:Voidwalker starts using (?:Doomvoid Guillotine|)/,
      alertText: {
        en: 'Sides',
      },
    },
    {
      id: 'E2S Doomvoid Slicer',
      regex: / 14:3E50:Voidwalker starts using (:?:Doomvoid Slicer|)/,
      regexFr: / 14:3E50:Marcheuse Du Néant starts using (?:Entaille Du Néant Ravageur|)/,
      infoText: {
        en: 'Get Under',
        fr: 'Intérieur',
      },
    },
    {
      id: 'E2S Empty Hate',
      regex: / 14:3E59:The Hand Of Erebos starts using (?:Empty Hate|)/,
      regexFr: / 14:3E59:Bras D'érèbe starts using (?:Vaine Malice)/,
      infoText: {
        en: 'Knockback',
        fr: 'Poussée',
      },
    },
    {
      id: 'E2S Empty Rage',
      regex: / 14:3E6B:The Hand Of Erebos starts using (?:Empty Rage|)/,
      alertText: {
        en: 'Away From Hand',
      },
    },
    {
      id: 'E2S Unholy Darkness No Waiting',
      regex: / 1B:\y{ObjectId}:(\y{Name}):....:....:003E:/,
      condition: function(data) {
        return !data.waiting;
      },
      alertText: function(data, matches) {
        if (matches[1] == data.me) {
          return {
            en: 'Stack on YOU',
            fr: 'Package sur VOUS',
          };
        }
        return {
          en: 'Stack on ' + data.ShortName(matches[1]),
          fr: 'Package sur '+ data.ShortName(matches[1]),
        };
      },
    },
    {
      id: 'E2S Unholy Darkness Collect',
      regex: / 1B:\y{ObjectId}:(\y{Name}):....:....:003E:/,
      condition: function(data) {
        return data.waiting;
      },
      run: function(data, matches) {
        data.spell = data.spell || {};
        data.spell[matches[1]] = 'stack';
      },
    },
    {
      id: 'E2S Unholy Darkness Waiting',
      regex: / 1B:\y{ObjectId}:(\y{Name}):....:....:003E:/,
      condition: function(data, matches) {
        return data.waiting && data.me == matches[1];
      },
      infoText: {
        en: 'Delayed Stack',
        fr: 'Package retardé',
      },
    },
    {
      id: 'E2S Countdown Marker Unholy Darkness',
      regex: / 1B:\y{ObjectId}:(\y{Name}):....:....:00B8:/,
      condition: function(data, matches) {
        return !data.hellWind && data.spell[matches[1]] == 'stack';
      },
      alertText: function(data, matches) {
        if (matches[1] == data.me) {
          return {
            en: 'Stack on YOU',
            fr: 'Package sur VOUS',
          };
        }
        return {
          en: 'Stack on ' + data.ShortName(matches[1]),
          fr: 'Package sur ' + data.ShortName(matches[1]),
        };
      },
    },
    {
      id: 'E2S Dark Fire No Waiting',
      regex: / 1B:\y{ObjectId}:(\y{Name}):....:....:004C:/,
      condition: function(data, matches) {
        return !data.waiting && data.me == matches[1];
      },
      alertText: {
        en: 'Spread',
        fr: 'Dispersez-vous',
      },
    },
    {
      id: 'E2S Dark Fire Collect',
      regex: / 1B:\y{ObjectId}:(\y{Name}):....:....:004C:/,
      condition: function(data) {
        return data.waiting;
      },
      run: function(data, matches) {
        data.spell = data.spell || {};
        data.spell[matches[1]] = 'fire';
      },
    },
    {
      id: 'E2S Dark Fire Waiting',
      regex: / 1B:\y{ObjectId}:(\y{Name}):....:....:004C:/,
      condition: function(data, matches) {
        return data.waiting && data.me == matches[1];
      },
      infoText: {
        en: 'Delayed Fire',
        fr: 'Feu retardé',
      },
    },
    {
      id: 'E2S Countdown Marker Fire',
      regex: / 1B:\y{ObjectId}:(\y{Name}):....:....:00B8:/,
      condition: function(data, matches) {
        return data.me == matches[1] && data.spell[data.me] == 'fire';
      },
      alertText: function(data) {
        return {
          en: 'Spread',
          fr: 'Dispersez-vous',
        };
      },
    },
    {
      id: 'E2S Shadoweye No Waiting',
      regex: / 1B:\y{ObjectId}:(\y{Name}):....:....:00B3:/,
      condition: function(data) {
        return !data.waiting;
      },
      alertText: function(data, matches) {
        if (data.me != matches[1]) {
          return {
            en: 'Look Away from ' + data.ShortName(matches[1]),
            fr: 'Ne regardez pas '+ data.ShortName(matches[1]),
          };
        }
      },
      infoText: function(data, matches) {
        if (data.me == matches[1]) {
          return {
            en: 'Eye on YOU',
            fr: 'Œil de l\'ombre sur VOUS',
          };
        }
      },
    },
    {
      id: 'E2S Shadoweye Collect',
      regex: / 1B:\y{ObjectId}:(\y{Name}):....:....:00B3:/,
      condition: function(data) {
        return data.waiting;
      },
      run: function(data, matches) {
        data.spell = data.spell || {};
        data.spell[matches[1]] = 'eye';
      },
    },
    {
      id: 'E2S Shadoweye Waiting',
      regex: / 1B:\y{ObjectId}:(\y{Name}):....:....:00B3:/,
      condition: function(data, matches) {
        return data.waiting && data.me == matches[1];
      },
      infoText: {
        en: 'Delayed Shadoweye',
        fr: 'Œil de l\'ombre retardé',
      },
    },
    {
      id: 'E2S Countdown Marker Shadoweye',
      regex: / 1B:\y{ObjectId}:(\y{Name}):....:....:00B8:/,
      condition: function(data, matches) {
        return data.spell[matches[1]] == 'eye';
      },
      delaySeconds: 2,
      alarmText: function(data, matches) {
        if (data.me != matches[1]) {
          return {
            en: 'Look Away from ' + data.ShortName(matches[1]),
            fr: 'Ne regardez pas ' + data.ShortName(matches[1]),
          };
        }
      },
      infoText: function(data, matches) {
        if (data.me == matches[1]) {
          return {
            en: 'Eye on YOU',
            fr: 'Œil de l\'ombre sur VOUS',
          };
        }
      },
    },
    {
      id: 'E2S Flare No Waiting',
      regex: / 1B:\y{ObjectId}:(\y{Name}):....:....:0057:/,
      condition: function(data, matches) {
        return !data.waiting && data.me == matches[1];
      },
      alertText: {
        en: 'Flare',
      },
    },
    {
      id: 'E2S Flare Collect',
      regex: / 1B:\y{ObjectId}:(\y{Name}):....:....:0057:/,
      condition: function(data) {
        return data.waiting;
      },
      run: function(data, matches) {
        data.spell = data.spell || {};
        data.spell[matches[1]] = 'flare';
      },
    },
    {
      id: 'E2S Flare Waiting',
      regex: / 1B:\y{ObjectId}:(\y{Name}):....:....:0057:/,
      condition: function(data, matches) {
        return data.waiting && data.me == matches[1];
      },
      infoText: {
        en: 'Delayed Flare',
        fr: 'Feu retardé',
      },
    },
    {
      id: 'E2S Countdown Marker Flare',
      regex: / 1B:\y{ObjectId}:(\y{Name}):....:....:00B8:/,
      condition: function(data, matches) {
        return data.me == matches[1] && data.spell[data.me] == 'flare';
      },
      alertText: function(data) {
        return {
          en: 'Flare',
        };
      },
    },
    {
      id: 'E2S Countdown Marker Flare Healer',
      regex: / 1B:\y{ObjectId}:(\y{Name}):....:....:00B8:/,
      condition: function(data, matches) {
        if (data.role != 'healer')
          return;
        return data.spell[matches[1]] == 'flare' && data.spell[data.me] != 'flare';
      },
      suppressSeconds: 10,
      infoText: function(data) {
        return {
          en: 'Flare aoes',
        };
      },
    },
    {
      id: 'E2S Hell Wind No Waiting',
      regex: / 1B:\y{ObjectId}:(\y{Name}):....:....:001E:/,
      condition: function(data, matches) {
        return !data.waiting && data.me == matches[1];
      },
      // The "no waiting" version comes paired with a stack.
      alarmText: {
        en: 'Hell Wind: Get Out',
      },
      run: function(data) {
        data.hellWind = true;
      },
    },
    {
      id: 'E2S Hell Wind Cleanup',
      regex: / 1B:\y{ObjectId}:(\y{Name}):....:....:001E:/,
      condition: function(data, matches) {
        return !data.waiting && data.me == matches[1];
      },
      delaySeconds: 15,
      run: function(data) {
        delete data.hellWind;
      },
    },
    {
      id: 'E2S Hell Wind Collect',
      regex: / 1B:\y{ObjectId}:(\y{Name}):....:....:001E:/,
      condition: function(data) {
        return data.waiting;
      },
      run: function(data, matches) {
        data.spell = data.spell || {};
        data.spell[matches[1]] = 'wind';
      },
    },
    {
      id: 'E2S Hell Wind Waiting',
      regex: / 1B:\y{ObjectId}:(\y{Name}):....:....:001E:/,
      condition: function(data, matches) {
        return data.waiting && data.me == matches[1];
      },
      infoText: {
        en: 'Delayed Hell Wind',
      },
    },
    {
      id: 'E2S Countdown Marker Hell Wind',
      regex: / 1B:\y{ObjectId}:(\y{Name}):....:....:00B8:/,
      condition: function(data, matches) {
        if (data.role == 'healer')
          return false;
        return data.me == matches[1] && data.spell[data.me] == 'wind';
      },
      alertText: function(data) {
        return {
          en: 'Hell Wind: wait for heals',
        };
      },
    },
    {
      id: 'E2S Countdown Marker Hell Wind Healer',
      regex: / 1B:\y{ObjectId}:(\y{Name}):....:....:00B8:/,
      condition: function(data, matches) {
        if (data.role != 'healer')
          return;
        return data.spell[matches[1]] == 'wind';
      },
      suppressSeconds: 10,
      infoText: function(data) {
        return {
          en: 'Heal Hell Wind Targets',
        };
      },
    },
    {
      id: 'E2S Countdown Marker Cleanup',
      regex: / 1B:\y{ObjectId}:(\y{Name}):....:....:00B8:/,
      delaySeconds: 10,
      run: function(data, matches) {
        delete data.spell[matches[1]];
      },
    },
    {
      // TODO: add callouts for each of these
      id: 'E2S Cycle of Retribution',
      regex: / 14:4659:Voidwalker starts using (?:Cycle Of Retribution|)/,
      infoText: {
        en: 'In => Protean => Sides',
      },
    },
    {
      id: 'E2S Cycle of Chaos',
      regex: / 14:40B9:Voidwalker starts using (?:Cycle Of Chaos|)/,
      infoText: {
        en: 'Sides => In => Protean',
      },
    },
  ],
}];
<commit_msg>raidboss: Fix tts on e2s (#500)<commit_after>'use strict';

// TODO
// better callouts for cycle
// tank provoke messages when cotank has flare

[{
  zoneRegex: /(^Eden's Gate: Descent \(Savage\)$|Unknown Zone \(356\))/,
  timelineFile: 'e2s.txt',
  timelineTriggers: [
    {
      id: 'E2S Punishing Ray',
      regex: /Punishing Ray/,
      beforeSeconds: 9,
      infoText: {
        en: 'Get Puddles',
        fr: 'Prenez les rayons',
      },
    },
    {
      id: 'E2S Buddy Circles',
      regex: /Light\/Dark Circles/,
      beforeSeconds: 5,
      alarmText: {
        en: 'Stack With Partner',
      },
    },
  ],
  triggers: [
    {
      id: 'E2S Spell In Waiting Gain',
      regex: / 1A:\y{ObjectId}:Voidwalker gains the effect of Spell-In-Waiting/,
      run: function(data) {
        data.waiting = true;
      },
    },
    {
      id: 'E2S Spell In Waiting Lose',
      regex: / 1E:\y{ObjectId}:Voidwalker loses the effect of Spell-In-Waiting/,
      run: function(data) {
        data.waiting = false;
      },
    },
    {
      id: 'E2S Entropy',
      regex: / 14:3E6F:Voidwalker starts using (?:Entropy|)/,
      regexFr: / 14:3E6F:Marcheuse Du Néant starts using (?:Entropie|)/,
      condition: function(data, matches) {
        return data.role == 'healer';
      },
      infoText: {
        en: 'aoe',
        fr: 'Dégâts de zone',
      },
    },
    {
      id: 'E2S Quietus',
      regex: / 14:3E71:Voidwalker starts using (?:Quietus|)/,
      condition: function(data, matches) {
        return data.role == 'healer';
      },
      infoText: {
        en: 'aoe',
        fr: 'Dégâts de zone',
      },
    },
    {
      id: 'E2S Shadowflame Tank',
      regex: / 14:3E6[12]:Voidwalker starts using (?:Shadowflame|Unknown_3E6[12]) on (\y{Name})/,
      regexFr: / 14:3E6[12]:Marcheuse Du Néant starts using (?:Flamme D'ombre|Unknown_3E6[12]) on (\y{Name})/,
      condition: function(data, matches) {
        return data.me == matches[1];
      },
      alertText: {
        en: 'Tank Buster on YOU',
        de: 'Tankbuster auf DIR',
        fr: 'Tankbuster sur VOUS',
      },
    },
    {
      id: 'E2S Shadowflame Healer',
      regex: / 14:3E61:Voidwalker starts using (?:Shadowflame|)/,
      regexFr: / 14:3E61:Marcheuse Du Néant starts using (?:Flamme D'ombre|)/,
      condition: function(data, matches) {
        return data.role == 'healer';
      },
      infoText: {
        en: 'tank busters',
        fr: 'Tank busters',
      },
    },
    {
      id: 'E2S Doomvoid Cleaver',
      regex: / 14:3E63:Voidwalker starts using (?:Doomvoid Cleaver|)/,
      alertText: {
        en: 'Protean',
      },
    },
    {
      id: 'E2S Doomvoid Guillotine',
      regex: / 14:3E4F:Voidwalker starts using (?:Doomvoid Guillotine|)/,
      alertText: {
        en: 'Sides',
      },
    },
    {
      id: 'E2S Doomvoid Slicer',
      regex: / 14:3E50:Voidwalker starts using (:?:Doomvoid Slicer|)/,
      regexFr: / 14:3E50:Marcheuse Du Néant starts using (?:Entaille Du Néant Ravageur|)/,
      infoText: {
        en: 'Get Under',
        fr: 'Intérieur',
      },
    },
    {
      id: 'E2S Empty Hate',
      regex: / 14:3E59:The Hand Of Erebos starts using (?:Empty Hate|)/,
      regexFr: / 14:3E59:Bras D'érèbe starts using (?:Vaine Malice)/,
      infoText: {
        en: 'Knockback',
        fr: 'Poussée',
      },
    },
    {
      id: 'E2S Empty Rage',
      regex: / 14:3E6B:The Hand Of Erebos starts using (?:Empty Rage|)/,
      alertText: {
        en: 'Away From Hand',
      },
    },
    {
      id: 'E2S Unholy Darkness No Waiting',
      regex: / 1B:\y{ObjectId}:(\y{Name}):....:....:003E:/,
      condition: function(data) {
        return !data.waiting;
      },
      alertText: function(data, matches) {
        if (matches[1] == data.me) {
          return {
            en: 'Stack on YOU',
            fr: 'Package sur VOUS',
          };
        }
        return {
          en: 'Stack on ' + data.ShortName(matches[1]),
          fr: 'Package sur '+ data.ShortName(matches[1]),
        };
      },
    },
    {
      id: 'E2S Unholy Darkness Collect',
      regex: / 1B:\y{ObjectId}:(\y{Name}):....:....:003E:/,
      condition: function(data) {
        return data.waiting;
      },
      run: function(data, matches) {
        data.spell = data.spell || {};
        data.spell[matches[1]] = 'stack';
      },
    },
    {
      id: 'E2S Unholy Darkness Waiting',
      regex: / 1B:\y{ObjectId}:(\y{Name}):....:....:003E:/,
      condition: function(data, matches) {
        return data.waiting && data.me == matches[1];
      },
      infoText: {
        en: 'Delayed Stack',
        fr: 'Package retardé',
      },
    },
    {
      id: 'E2S Countdown Marker Unholy Darkness',
      regex: / 1B:\y{ObjectId}:(\y{Name}):....:....:00B8:/,
      condition: function(data, matches) {
        return !data.hellWind && data.spell[matches[1]] == 'stack';
      },
      alertText: function(data, matches) {
        if (matches[1] == data.me) {
          return {
            en: 'Stack on YOU',
            fr: 'Package sur VOUS',
          };
        }
        return {
          en: 'Stack on ' + data.ShortName(matches[1]),
          fr: 'Package sur ' + data.ShortName(matches[1]),
        };
      },
    },
    {
      id: 'E2S Dark Fire No Waiting',
      regex: / 1B:\y{ObjectId}:(\y{Name}):....:....:004C:/,
      condition: function(data, matches) {
        return !data.waiting && data.me == matches[1];
      },
      alertText: {
        en: 'Spread',
        fr: 'Dispersez-vous',
      },
    },
    {
      id: 'E2S Dark Fire Collect',
      regex: / 1B:\y{ObjectId}:(\y{Name}):....:....:004C:/,
      condition: function(data) {
        return data.waiting;
      },
      run: function(data, matches) {
        data.spell = data.spell || {};
        data.spell[matches[1]] = 'fire';
      },
    },
    {
      id: 'E2S Dark Fire Waiting',
      regex: / 1B:\y{ObjectId}:(\y{Name}):....:....:004C:/,
      condition: function(data, matches) {
        return data.waiting && data.me == matches[1];
      },
      infoText: {
        en: 'Delayed Fire',
        fr: 'Feu retardé',
      },
    },
    {
      id: 'E2S Countdown Marker Fire',
      regex: / 1B:\y{ObjectId}:(\y{Name}):....:....:00B8:/,
      condition: function(data, matches) {
        return data.me == matches[1] && data.spell[data.me] == 'fire';
      },
      alertText: function(data) {
        return {
          en: 'Spread',
          fr: 'Dispersez-vous',
        };
      },
    },
    {
      id: 'E2S Shadoweye No Waiting',
      regex: / 1B:\y{ObjectId}:(\y{Name}):....:....:00B3:/,
      condition: function(data) {
        return !data.waiting;
      },
      alertText: function(data, matches) {
        if (data.me != matches[1]) {
          return {
            en: 'Look Away from ' + data.ShortName(matches[1]),
            fr: 'Ne regardez pas '+ data.ShortName(matches[1]),
          };
        }
      },
      infoText: function(data, matches) {
        if (data.me == matches[1]) {
          return {
            en: 'Eye on YOU',
            fr: 'Œil de l\'ombre sur VOUS',
          };
        }
      },
    },
    {
      id: 'E2S Shadoweye Collect',
      regex: / 1B:\y{ObjectId}:(\y{Name}):....:....:00B3:/,
      condition: function(data) {
        return data.waiting;
      },
      run: function(data, matches) {
        data.spell = data.spell || {};
        data.spell[matches[1]] = 'eye';
      },
    },
    {
      id: 'E2S Shadoweye Waiting',
      regex: / 1B:\y{ObjectId}:(\y{Name}):....:....:00B3:/,
      condition: function(data, matches) {
        return data.waiting && data.me == matches[1];
      },
      infoText: {
        en: 'Delayed Shadoweye',
        fr: 'Œil de l\'ombre retardé',
      },
    },
    {
      id: 'E2S Countdown Marker Shadoweye',
      regex: / 1B:\y{ObjectId}:(\y{Name}):....:....:00B8:/,
      condition: function(data, matches) {
        return data.spell[matches[1]] == 'eye';
      },
      delaySeconds: 2,
      alarmText: function(data, matches) {
        if (data.me != matches[1]) {
          return {
            en: 'Look Away from ' + data.ShortName(matches[1]),
            fr: 'Ne regardez pas ' + data.ShortName(matches[1]),
          };
        }
      },
      infoText: function(data, matches) {
        if (data.me == matches[1]) {
          return {
            en: 'Eye on YOU',
            fr: 'Œil de l\'ombre sur VOUS',
          };
        }
      },
    },
    {
      id: 'E2S Flare No Waiting',
      regex: / 1B:\y{ObjectId}:(\y{Name}):....:....:0057:/,
      condition: function(data, matches) {
        return !data.waiting && data.me == matches[1];
      },
      alertText: {
        en: 'Flare',
      },
    },
    {
      id: 'E2S Flare Collect',
      regex: / 1B:\y{ObjectId}:(\y{Name}):....:....:0057:/,
      condition: function(data) {
        return data.waiting;
      },
      run: function(data, matches) {
        data.spell = data.spell || {};
        data.spell[matches[1]] = 'flare';
      },
    },
    {
      id: 'E2S Flare Waiting',
      regex: / 1B:\y{ObjectId}:(\y{Name}):....:....:0057:/,
      condition: function(data, matches) {
        return data.waiting && data.me == matches[1];
      },
      infoText: {
        en: 'Delayed Flare',
        fr: 'Feu retardé',
      },
    },
    {
      id: 'E2S Countdown Marker Flare',
      regex: / 1B:\y{ObjectId}:(\y{Name}):....:....:00B8:/,
      condition: function(data, matches) {
        return data.me == matches[1] && data.spell[data.me] == 'flare';
      },
      alertText: function(data) {
        return {
          en: 'Flare',
        };
      },
    },
    {
      id: 'E2S Countdown Marker Flare Healer',
      regex: / 1B:\y{ObjectId}:(\y{Name}):....:....:00B8:/,
      condition: function(data, matches) {
        if (data.role != 'healer')
          return;
        return data.spell[matches[1]] == 'flare' && data.spell[data.me] != 'flare';
      },
      suppressSeconds: 10,
      infoText: function(data) {
        return {
          en: 'Flare aoes',
        };
      },
    },
    {
      id: 'E2S Hell Wind No Waiting',
      regex: / 1B:\y{ObjectId}:(\y{Name}):....:....:001E:/,
      condition: function(data, matches) {
        return !data.waiting && data.me == matches[1];
      },
      // The "no waiting" version comes paired with a stack.
      alarmText: {
        en: 'Hell Wind: Get Out',
      },
      run: function(data) {
        data.hellWind = true;
      },
    },
    {
      id: 'E2S Hell Wind Cleanup',
      regex: / 1B:\y{ObjectId}:(\y{Name}):....:....:001E:/,
      condition: function(data, matches) {
        return !data.waiting && data.me == matches[1];
      },
      delaySeconds: 15,
      run: function(data) {
        delete data.hellWind;
      },
    },
    {
      id: 'E2S Hell Wind Collect',
      regex: / 1B:\y{ObjectId}:(\y{Name}):....:....:001E:/,
      condition: function(data) {
        return data.waiting;
      },
      run: function(data, matches) {
        data.spell = data.spell || {};
        data.spell[matches[1]] = 'wind';
      },
    },
    {
      id: 'E2S Hell Wind Waiting',
      regex: / 1B:\y{ObjectId}:(\y{Name}):....:....:001E:/,
      condition: function(data, matches) {
        return data.waiting && data.me == matches[1];
      },
      infoText: {
        en: 'Delayed Hell Wind',
      },
    },
    {
      id: 'E2S Countdown Marker Hell Wind',
      regex: / 1B:\y{ObjectId}:(\y{Name}):....:....:00B8:/,
      condition: function(data, matches) {
        if (data.role == 'healer')
          return false;
        return data.me == matches[1] && data.spell[data.me] == 'wind';
      },
      alertText: function(data) {
        return {
          en: 'Hell Wind: wait for heals',
        };
      },
    },
    {
      id: 'E2S Countdown Marker Hell Wind Healer',
      regex: / 1B:\y{ObjectId}:(\y{Name}):....:....:00B8:/,
      condition: function(data, matches) {
        if (data.role != 'healer')
          return;
        return data.spell[matches[1]] == 'wind';
      },
      suppressSeconds: 10,
      infoText: function(data) {
        return {
          en: 'Heal Hell Wind Targets',
        };
      },
    },
    {
      id: 'E2S Countdown Marker Cleanup',
      regex: / 1B:\y{ObjectId}:(\y{Name}):....:....:00B8:/,
      delaySeconds: 10,
      run: function(data, matches) {
        delete data.spell[matches[1]];
      },
    },
    {
      // TODO: add callouts for each of these
      id: 'E2S Cycle of Retribution',
      regex: / 14:4659:Voidwalker starts using (?:Cycle Of Retribution|)/,
      infoText: {
        en: 'In, Protean, Sides',
      },
    },
    {
      id: 'E2S Cycle of Chaos',
      regex: / 14:40B9:Voidwalker starts using (?:Cycle Of Chaos|)/,
      infoText: {
        en: 'Sides, In, Protean',
      },
    },
  ],
}];
<|endoftext|>


<commit_before>var _gutil = require('gulp-util');
var _gplumber = require('gulp-plumber');

var PrettyError = (function(customErrorFormat){

    // custom error format function provided ?
    if (typeof customErrorFormat != 'undefined'){

        // proxy
        return _gplumber(function(error){
            customErrorFormat.apply(this, [error, _gutil]);
        });

    }else{
        // default appearance
        return _gplumber(function(error){
            // add indentation
            var msg = error.codeFrame.replace(/\n/g, '\n    ');

            _gutil.log('|- ' + _gutil.colors.bgRed.bold('Build Error in ' + error.plugin));
            _gutil.log('|- ' + _gutil.colors.bgRed.bold(error.message));
            _gutil.log('|- ' + _gutil.colors.bgRed('>>>'));
            _gutil.log('|\n    ' + msg + '\n           |');
            _gutil.log('|- ' + _gutil.colors.bgRed('<<<'));
        });
    }
});

module.exports = PrettyError;<commit_msg>Make sure it will not break with gulp-less<commit_after>var _gutil = require('gulp-util');
var _gplumber = require('gulp-plumber');

var PrettyError = (function(customErrorFormat){

    // custom error format function provided ?
    if (typeof customErrorFormat != 'undefined'){

        // proxy
        return _gplumber(function(error){
            customErrorFormat.apply(this, [error, _gutil]);
        });

    }else{
        // default appearance
        return _gplumber(function(error){
            // log the error message
            _gutil.log('|- ' + _gutil.colors.bgRed.bold('Build Error in ' + error.plugin));
            _gutil.log('|- ' + _gutil.colors.bgRed.bold(error.message));

            // make sure there is codeFrame in the error object.
            // gulp-less does not have it, so it will throw an error
            if(error.codeFrame != 'undefined') {
                // add indentation
                var msg = error.codeFrame.replace(/\n/g, '\n    ');

                _gutil.log('|- ' + _gutil.colors.bgRed('>>>'));
                _gutil.log('|\n    ' + msg + '\n           |');
                _gutil.log('|- ' + _gutil.colors.bgRed('<<<'));
            }
        });
    }
});

module.exports = PrettyError;
<|endoftext|>


<commit_before>import { module, test } from 'qunit';
import { currentURL, settled } from '@ember/test-helpers';
import { setupApplicationTest } from 'ember-qunit';
import { setupMirage } from 'ember-cli-mirage/test-support';
import Tokens from 'nomad-ui/tests/pages/settings/tokens';
import Service from '@ember/service';
import Exec from 'nomad-ui/tests/pages/exec';

module('Acceptance | exec', function(hooks) {
  setupApplicationTest(hooks);
  setupMirage(hooks);

  hooks.beforeEach(async function() {
    localStorage.clear();
    sessionStorage.clear();

    server.create('agent');
    server.create('node');

    this.job = server.create('job', {
      groupsCount: 2,
      groupTaskCount: 5,
      createAllocations: false,
    });

    this.job.task_group_ids.forEach(taskGroupId => {
      server.create('allocation', {
        jobId: this.job.id,
        taskGroup: server.db.taskGroups.find(taskGroupId).name,
        forceRunningClientStatus: true,
      });
    });
  });

  test('/exec/:job should show the region, namespace, and job name', async function(assert) {
    server.create('namespace');
    let namespace = server.create('namespace');

    server.create('region', { id: 'global' });
    server.create('region', { id: 'region-2' });

    this.job = server.create('job', { createAllocations: false, namespaceId: namespace.id });

    await Exec.visitJob({ job: this.job.id, namespace: namespace.id, region: 'region-2' });

    assert.equal(document.title, 'Exec - region-2 - Nomad');

    assert.equal(Exec.header.region.text, this.job.region);
    assert.equal(Exec.header.namespace.text, this.job.namespace);
    assert.equal(Exec.header.job, this.job.name);
  });

  test('/exec/:job should not show region and namespace when there are none', async function(assert) {
    await Exec.visitJob({ job: this.job.id });

    assert.ok(Exec.header.region.isHidden);
    assert.ok(Exec.header.namespace.isHidden);
  });

  test('/exec/:job should show the task groups collapsed by default and allow the tasks to be shown', async function(assert) {
    await Exec.visitJob({ job: this.job.id });

    assert.equal(Exec.taskGroups.length, this.job.task_groups.length);

    assert.equal(Exec.taskGroups[0].name, this.job.task_groups.models[0].name);
    assert.equal(Exec.taskGroups[0].tasks.length, 0);
    assert.ok(Exec.taskGroups[0].chevron.isRight);

    await Exec.taskGroups[0].click();
    assert.equal(Exec.taskGroups[0].tasks.length, this.job.task_groups.models[0].tasks.length);
    assert.notOk(Exec.taskGroups[0].tasks[0].isActive);
    assert.ok(Exec.taskGroups[0].chevron.isDown);

    await Exec.taskGroups[0].click();
    assert.equal(Exec.taskGroups[0].tasks.length, 0);
  });

  test('/exec/:job should require selecting a task', async function(assert) {
    await Exec.visitJob({ job: this.job.id });

    assert.equal(
      window.execTerminal.buffer
        .getLine(0)
        .translateToString()
        .trim(),
      'Select a task to start your session.'
    );
  });

  test('a task group with no running task states should not be shown', async function(assert) {
    let taskGroup = this.job.task_groups.models[0];
    this.server.db.allocations.update({ taskGroup: taskGroup.name }, { clientStatus: 'pending' });

    await Exec.visitJob({ job: this.job.id });
    assert.notEqual(Exec.taskGroups[0].name, taskGroup.name);
  });

  test('an inactive task should not be shown', async function(assert) {
    let notRunningTaskGroup = this.job.task_groups.models[0];
    this.server.db.allocations.update(
      { taskGroup: notRunningTaskGroup.name },
      { clientStatus: 'pending' }
    );

    let runningTaskGroup = this.job.task_groups.models[1];
    runningTaskGroup.tasks.models.forEach((task, index) => {
      if (index > 0) {
        this.server.db.taskStates.update({ name: task.name }, { finishedAt: new Date() });
      }
    });

    await Exec.visitJob({ job: this.job.id });
    await Exec.taskGroups[0].click();

    assert.equal(Exec.taskGroups[0].tasks.length, 1);
  });

  test('visiting a path with a task group should open the group by default', async function(assert) {
    let taskGroup = this.job.task_groups.models[0];
    await Exec.visitTaskGroup({ job: this.job.id, task_group: taskGroup.name });

    assert.equal(Exec.taskGroups[0].tasks.length, this.job.task_groups.models[0].tasks.length);
    assert.ok(Exec.taskGroups[0].chevron.isDown);

    let task = taskGroup.tasks.models[0];
    await Exec.visitTask({ job: this.job.id, task_group: taskGroup.name, task_name: task.name });

    assert.equal(Exec.taskGroups[0].tasks.length, this.job.task_groups.models[0].tasks.length);
    assert.ok(Exec.taskGroups[0].chevron.isDown);
  });

  test('navigating to a task adds its name to the route, chooses an allocation, and assigns a default command', async function(assert) {
    await Exec.visitJob({ job: this.job.id });
    await Exec.taskGroups[0].click();
    await Exec.taskGroups[0].tasks[0].click();

    let taskGroup = this.job.task_groups.models[0];
    let task = taskGroup.tasks.models[0];

    let taskStates = this.server.db.taskStates.where({
      name: task.name,
    });
    let allocationId = taskStates.find(ts => ts.allocationId).allocationId;

    await settled();

    assert.equal(currentURL(), `/exec/${this.job.id}/${taskGroup.name}/${task.name}`);
    assert.ok(Exec.taskGroups[0].tasks[0].isActive);

    assert.equal(
      window.execTerminal.buffer
        .getLine(2)
        .translateToString()
        .trim(),
      'Multiple instances of this task are running. The allocation below was selected by random draw.'
    );

    assert.equal(
      window.execTerminal.buffer
        .getLine(4)
        .translateToString()
        .trim(),
      'Customize your command, then hit ‘return’ to run.'
    );

    assert.equal(
      window.execTerminal.buffer
        .getLine(6)
        .translateToString()
        .trim(),
      `$ nomad alloc exec -i -t -task ${task.name} ${allocationId.split('-')[0]} /bin/bash`
    );
  });

  test('an allocation can be specified', async function(assert) {
    let taskGroup = this.job.task_groups.models[0];
    let task = taskGroup.tasks.models[0];
    let allocations = this.server.db.allocations.where({
      jobId: this.job.id,
      taskGroup: taskGroup.name,
    });
    let allocation = allocations[allocations.length - 1];

    this.server.db.taskStates.update({ name: task.name }, { name: 'spaced name!' });

    task.name = 'spaced name!';
    task.save();

    await Exec.visitTask({
      job: this.job.id,
      task_group: taskGroup.name,
      task_name: task.name,
      allocation: allocation.id.split('-')[0],
    });

    await settled();

    assert.equal(
      window.execTerminal.buffer
        .getLine(4)
        .translateToString()
        .trim(),
      `$ nomad alloc exec -i -t -task spaced\\ name\\! ${allocation.id.split('-')[0]} /bin/bash`
    );
  });

  test('running the command opens the socket for reading/writing and detects it closing', async function(assert) {
    let mockSocket = new MockSocket();
    let mockSockets = Service.extend({
      getTaskStateSocket(taskState, command) {
        assert.equal(taskState.name, task.name);
        assert.equal(taskState.allocation.id, allocation.id);

        assert.equal(command, '/bin/bash');

        assert.step('Socket built');

        return mockSocket;
      },
    });

    this.owner.register('service:sockets', mockSockets);

    let taskGroup = this.job.task_groups.models[0];
    let task = taskGroup.tasks.models[0];
    let allocations = this.server.db.allocations.where({
      jobId: this.job.id,
      taskGroup: taskGroup.name,
    });
    let allocation = allocations[allocations.length - 1];

    await Exec.visitTask({
      job: this.job.id,
      task_group: taskGroup.name,
      task_name: task.name,
      allocation: allocation.id.split('-')[0],
    });

    await settled();

    await Exec.terminal.pressEnter();
    await settled();
    mockSocket.onopen();

    assert.verifySteps(['Socket built']);

    mockSocket.onmessage({
      data: '{"stdout":{"data":"c2gtMy4yIPCfpbMk"}}',
    });

    await settled();

    assert.equal(
      window.execTerminal.buffer
        .getLine(5)
        .translateToString()
        .trim(),
      'sh-3.2 🥳$'
    );

    await Exec.terminal.pressEnter();
    await settled();

    assert.deepEqual(mockSocket.sent, [
      '{"version":1,"auth_token":""}',
      `{"tty_size":{"width":${window.execTerminal.cols},"height":${window.execTerminal.rows}}}`,
      '{"stdin":{"data":"DQ=="}}',
    ]);

    await mockSocket.onclose();
    await settled();

    assert.equal(
      window.execTerminal.buffer
        .getLine(6)
        .translateToString()
        .trim(),
      'The connection has closed.'
    );
  });

  test('running the command opens the socket and authenticates', async function(assert) {
    let managementToken = server.create('token');

    const { secretId } = managementToken;

    await Tokens.visit();
    await Tokens.secret(secretId).submit();

    let mockSocket = new MockSocket();
    let mockSockets = Service.extend({
      getTaskStateSocket(taskState, command) {
        assert.equal(taskState.name, task.name);
        assert.equal(taskState.allocation.id, allocation.id);

        assert.equal(command, '/bin/bash');

        assert.step('Socket built');

        return mockSocket;
      },
    });

    this.owner.register('service:sockets', mockSockets);

    let taskGroup = this.job.task_groups.models[0];
    let task = taskGroup.tasks.models[0];
    let allocations = this.server.db.allocations.where({
      jobId: this.job.id,
      taskGroup: taskGroup.name,
    });
    let allocation = allocations[allocations.length - 1];

    await Exec.visitTask({
      job: this.job.id,
      task_group: taskGroup.name,
      task_name: task.name,
      allocation: allocation.id.split('-')[0],
    });

    await settled();

    await Exec.terminal.pressEnter();
    await settled();
    mockSocket.onopen();

    assert.verifySteps(['Socket built']);

    mockSocket.onmessage({
      data: '{"stdout":{"data":"c2gtMy4yIPCfpbMk"}}',
    });

    await settled();

    assert.equal(
      window.execTerminal.buffer
        .getLine(5)
        .translateToString()
        .trim(),
      'sh-3.2 🥳$'
    );

    await Exec.terminal.pressEnter();
    await settled();

    assert.deepEqual(mockSocket.sent, [
      `{"version":1,"auth_token":"${secretId}"}`,
      `{"tty_size":{"width":${window.execTerminal.cols},"height":${window.execTerminal.rows}}}`,
      '{"stdin":{"data":"DQ=="}}',
    ]);

    await mockSocket.onclose();
    await settled();

    assert.equal(
      window.execTerminal.buffer
        .getLine(6)
        .translateToString()
        .trim(),
      'The connection has closed.'
    );
  });

  test('only one socket is opened after switching between tasks', async function(assert) {
    let mockSockets = Service.extend({
      getTaskStateSocket() {
        assert.step('Socket built');
        return new MockSocket();
      },
    });

    this.owner.register('service:sockets', mockSockets);

    await Exec.visitJob({
      job: this.job.id,
    });

    await settled();

    await Exec.taskGroups[0].click();
    await Exec.taskGroups[0].tasks[0].click();

    await Exec.taskGroups[1].click();
    await Exec.taskGroups[1].tasks[0].click();

    await Exec.terminal.pressEnter();

    assert.verifySteps(['Socket built']);
  });

  test('the command can be customised', async function(assert) {
    let mockSockets = Service.extend({
      getTaskStateSocket(taskState, command) {
        assert.equal(command, '/sh');
        localStorage.getItem('nomadExecCommand', JSON.stringify('/sh'));

        assert.step('Socket built');

        return new MockSocket();
      },
    });

    this.owner.register('service:sockets', mockSockets);

    await Exec.visitJob({ job: this.job.id });
    await Exec.taskGroups[0].click();
    await Exec.taskGroups[0].tasks[0].click();

    let taskGroup = this.job.task_groups.models[0];
    let task = taskGroup.tasks.models[0];
    let allocation = this.server.db.allocations.findBy({
      jobId: this.job.id,
      taskGroup: taskGroup.name,
    });

    await settled();

    // Delete /bash
    await window.execTerminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } });
    await window.execTerminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } });
    await window.execTerminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } });
    await window.execTerminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } });
    await window.execTerminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } });

    // Delete /bin and try to go beyond
    await window.execTerminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } });
    await window.execTerminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } });
    await window.execTerminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } });
    await window.execTerminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } });
    await window.execTerminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } });
    await window.execTerminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } });
    await window.execTerminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } });

    await settled();

    assert.equal(
      window.execTerminal.buffer
        .getLine(6)
        .translateToString()
        .trim(),
      `$ nomad alloc exec -i -t -task ${task.name} ${allocation.id.split('-')[0]}`
    );

    await window.execTerminal.simulateCommandKeyEvent({ key: '/', domEvent: {} });
    await window.execTerminal.simulateCommandKeyEvent({ key: 's', domEvent: {} });
    await window.execTerminal.simulateCommandKeyEvent({ key: 'h', domEvent: {} });

    await Exec.terminal.pressEnter();
    await settled();

    assert.verifySteps(['Socket built']);
  });

  test('a persisted customised command is recalled', async function(assert) {
    localStorage.setItem('nomadExecCommand', JSON.stringify('/bin/sh'));

    let taskGroup = this.job.task_groups.models[0];
    let task = taskGroup.tasks.models[0];
    let allocations = this.server.db.allocations.where({
      jobId: this.job.id,
      taskGroup: taskGroup.name,
    });
    let allocation = allocations[allocations.length - 1];

    await Exec.visitTask({
      job: this.job.id,
      task_group: taskGroup.name,
      task_name: task.name,
      allocation: allocation.id.split('-')[0],
    });

    await settled();

    assert.equal(
      window.execTerminal.buffer
        .getLine(4)
        .translateToString()
        .trim(),
      `$ nomad alloc exec -i -t -task ${task.name} ${allocation.id.split('-')[0]} /bin/sh`
    );
  });
});

class MockSocket {
  constructor() {
    this.sent = [];
  }

  send(message) {
    this.sent.push(message);
  }
}
<commit_msg>Change to setting token directly<commit_after>import { module, test } from 'qunit';
import { currentURL, settled } from '@ember/test-helpers';
import { setupApplicationTest } from 'ember-qunit';
import { setupMirage } from 'ember-cli-mirage/test-support';
import Service from '@ember/service';
import Exec from 'nomad-ui/tests/pages/exec';

module('Acceptance | exec', function(hooks) {
  setupApplicationTest(hooks);
  setupMirage(hooks);

  hooks.beforeEach(async function() {
    localStorage.clear();
    sessionStorage.clear();

    server.create('agent');
    server.create('node');

    this.job = server.create('job', {
      groupsCount: 2,
      groupTaskCount: 5,
      createAllocations: false,
    });

    this.job.task_group_ids.forEach(taskGroupId => {
      server.create('allocation', {
        jobId: this.job.id,
        taskGroup: server.db.taskGroups.find(taskGroupId).name,
        forceRunningClientStatus: true,
      });
    });
  });

  test('/exec/:job should show the region, namespace, and job name', async function(assert) {
    server.create('namespace');
    let namespace = server.create('namespace');

    server.create('region', { id: 'global' });
    server.create('region', { id: 'region-2' });

    this.job = server.create('job', { createAllocations: false, namespaceId: namespace.id });

    await Exec.visitJob({ job: this.job.id, namespace: namespace.id, region: 'region-2' });

    assert.equal(document.title, 'Exec - region-2 - Nomad');

    assert.equal(Exec.header.region.text, this.job.region);
    assert.equal(Exec.header.namespace.text, this.job.namespace);
    assert.equal(Exec.header.job, this.job.name);
  });

  test('/exec/:job should not show region and namespace when there are none', async function(assert) {
    await Exec.visitJob({ job: this.job.id });

    assert.ok(Exec.header.region.isHidden);
    assert.ok(Exec.header.namespace.isHidden);
  });

  test('/exec/:job should show the task groups collapsed by default and allow the tasks to be shown', async function(assert) {
    await Exec.visitJob({ job: this.job.id });

    assert.equal(Exec.taskGroups.length, this.job.task_groups.length);

    assert.equal(Exec.taskGroups[0].name, this.job.task_groups.models[0].name);
    assert.equal(Exec.taskGroups[0].tasks.length, 0);
    assert.ok(Exec.taskGroups[0].chevron.isRight);

    await Exec.taskGroups[0].click();
    assert.equal(Exec.taskGroups[0].tasks.length, this.job.task_groups.models[0].tasks.length);
    assert.notOk(Exec.taskGroups[0].tasks[0].isActive);
    assert.ok(Exec.taskGroups[0].chevron.isDown);

    await Exec.taskGroups[0].click();
    assert.equal(Exec.taskGroups[0].tasks.length, 0);
  });

  test('/exec/:job should require selecting a task', async function(assert) {
    await Exec.visitJob({ job: this.job.id });

    assert.equal(
      window.execTerminal.buffer
        .getLine(0)
        .translateToString()
        .trim(),
      'Select a task to start your session.'
    );
  });

  test('a task group with no running task states should not be shown', async function(assert) {
    let taskGroup = this.job.task_groups.models[0];
    this.server.db.allocations.update({ taskGroup: taskGroup.name }, { clientStatus: 'pending' });

    await Exec.visitJob({ job: this.job.id });
    assert.notEqual(Exec.taskGroups[0].name, taskGroup.name);
  });

  test('an inactive task should not be shown', async function(assert) {
    let notRunningTaskGroup = this.job.task_groups.models[0];
    this.server.db.allocations.update(
      { taskGroup: notRunningTaskGroup.name },
      { clientStatus: 'pending' }
    );

    let runningTaskGroup = this.job.task_groups.models[1];
    runningTaskGroup.tasks.models.forEach((task, index) => {
      if (index > 0) {
        this.server.db.taskStates.update({ name: task.name }, { finishedAt: new Date() });
      }
    });

    await Exec.visitJob({ job: this.job.id });
    await Exec.taskGroups[0].click();

    assert.equal(Exec.taskGroups[0].tasks.length, 1);
  });

  test('visiting a path with a task group should open the group by default', async function(assert) {
    let taskGroup = this.job.task_groups.models[0];
    await Exec.visitTaskGroup({ job: this.job.id, task_group: taskGroup.name });

    assert.equal(Exec.taskGroups[0].tasks.length, this.job.task_groups.models[0].tasks.length);
    assert.ok(Exec.taskGroups[0].chevron.isDown);

    let task = taskGroup.tasks.models[0];
    await Exec.visitTask({ job: this.job.id, task_group: taskGroup.name, task_name: task.name });

    assert.equal(Exec.taskGroups[0].tasks.length, this.job.task_groups.models[0].tasks.length);
    assert.ok(Exec.taskGroups[0].chevron.isDown);
  });

  test('navigating to a task adds its name to the route, chooses an allocation, and assigns a default command', async function(assert) {
    await Exec.visitJob({ job: this.job.id });
    await Exec.taskGroups[0].click();
    await Exec.taskGroups[0].tasks[0].click();

    let taskGroup = this.job.task_groups.models[0];
    let task = taskGroup.tasks.models[0];

    let taskStates = this.server.db.taskStates.where({
      name: task.name,
    });
    let allocationId = taskStates.find(ts => ts.allocationId).allocationId;

    await settled();

    assert.equal(currentURL(), `/exec/${this.job.id}/${taskGroup.name}/${task.name}`);
    assert.ok(Exec.taskGroups[0].tasks[0].isActive);

    assert.equal(
      window.execTerminal.buffer
        .getLine(2)
        .translateToString()
        .trim(),
      'Multiple instances of this task are running. The allocation below was selected by random draw.'
    );

    assert.equal(
      window.execTerminal.buffer
        .getLine(4)
        .translateToString()
        .trim(),
      'Customize your command, then hit ‘return’ to run.'
    );

    assert.equal(
      window.execTerminal.buffer
        .getLine(6)
        .translateToString()
        .trim(),
      `$ nomad alloc exec -i -t -task ${task.name} ${allocationId.split('-')[0]} /bin/bash`
    );
  });

  test('an allocation can be specified', async function(assert) {
    let taskGroup = this.job.task_groups.models[0];
    let task = taskGroup.tasks.models[0];
    let allocations = this.server.db.allocations.where({
      jobId: this.job.id,
      taskGroup: taskGroup.name,
    });
    let allocation = allocations[allocations.length - 1];

    this.server.db.taskStates.update({ name: task.name }, { name: 'spaced name!' });

    task.name = 'spaced name!';
    task.save();

    await Exec.visitTask({
      job: this.job.id,
      task_group: taskGroup.name,
      task_name: task.name,
      allocation: allocation.id.split('-')[0],
    });

    await settled();

    assert.equal(
      window.execTerminal.buffer
        .getLine(4)
        .translateToString()
        .trim(),
      `$ nomad alloc exec -i -t -task spaced\\ name\\! ${allocation.id.split('-')[0]} /bin/bash`
    );
  });

  test('running the command opens the socket for reading/writing and detects it closing', async function(assert) {
    let mockSocket = new MockSocket();
    let mockSockets = Service.extend({
      getTaskStateSocket(taskState, command) {
        assert.equal(taskState.name, task.name);
        assert.equal(taskState.allocation.id, allocation.id);

        assert.equal(command, '/bin/bash');

        assert.step('Socket built');

        return mockSocket;
      },
    });

    this.owner.register('service:sockets', mockSockets);

    let taskGroup = this.job.task_groups.models[0];
    let task = taskGroup.tasks.models[0];
    let allocations = this.server.db.allocations.where({
      jobId: this.job.id,
      taskGroup: taskGroup.name,
    });
    let allocation = allocations[allocations.length - 1];

    await Exec.visitTask({
      job: this.job.id,
      task_group: taskGroup.name,
      task_name: task.name,
      allocation: allocation.id.split('-')[0],
    });

    await settled();

    await Exec.terminal.pressEnter();
    await settled();
    mockSocket.onopen();

    assert.verifySteps(['Socket built']);

    mockSocket.onmessage({
      data: '{"stdout":{"data":"c2gtMy4yIPCfpbMk"}}',
    });

    await settled();

    assert.equal(
      window.execTerminal.buffer
        .getLine(5)
        .translateToString()
        .trim(),
      'sh-3.2 🥳$'
    );

    await Exec.terminal.pressEnter();
    await settled();

    assert.deepEqual(mockSocket.sent, [
      '{"version":1,"auth_token":""}',
      `{"tty_size":{"width":${window.execTerminal.cols},"height":${window.execTerminal.rows}}}`,
      '{"stdin":{"data":"DQ=="}}',
    ]);

    await mockSocket.onclose();
    await settled();

    assert.equal(
      window.execTerminal.buffer
        .getLine(6)
        .translateToString()
        .trim(),
      'The connection has closed.'
    );
  });

  test('running the command opens the socket and authenticates', async function(assert) {
    let managementToken = server.create('token');

    const { secretId } = managementToken;

    window.localStorage.nomadTokenSecret = secretId;

    let mockSocket = new MockSocket();
    let mockSockets = Service.extend({
      getTaskStateSocket(taskState, command) {
        assert.equal(taskState.name, task.name);
        assert.equal(taskState.allocation.id, allocation.id);

        assert.equal(command, '/bin/bash');

        assert.step('Socket built');

        return mockSocket;
      },
    });

    this.owner.register('service:sockets', mockSockets);

    let taskGroup = this.job.task_groups.models[0];
    let task = taskGroup.tasks.models[0];
    let allocations = this.server.db.allocations.where({
      jobId: this.job.id,
      taskGroup: taskGroup.name,
    });
    let allocation = allocations[allocations.length - 1];

    await Exec.visitTask({
      job: this.job.id,
      task_group: taskGroup.name,
      task_name: task.name,
      allocation: allocation.id.split('-')[0],
    });

    await settled();

    await Exec.terminal.pressEnter();
    await settled();
    mockSocket.onopen();

    assert.verifySteps(['Socket built']);

    mockSocket.onmessage({
      data: '{"stdout":{"data":"c2gtMy4yIPCfpbMk"}}',
    });

    await settled();

    assert.equal(
      window.execTerminal.buffer
        .getLine(5)
        .translateToString()
        .trim(),
      'sh-3.2 🥳$'
    );

    await Exec.terminal.pressEnter();
    await settled();

    assert.deepEqual(mockSocket.sent, [
      `{"version":1,"auth_token":"${secretId}"}`,
      `{"tty_size":{"width":${window.execTerminal.cols},"height":${window.execTerminal.rows}}}`,
      '{"stdin":{"data":"DQ=="}}',
    ]);

    await mockSocket.onclose();
    await settled();

    assert.equal(
      window.execTerminal.buffer
        .getLine(6)
        .translateToString()
        .trim(),
      'The connection has closed.'
    );
  });

  test('only one socket is opened after switching between tasks', async function(assert) {
    let mockSockets = Service.extend({
      getTaskStateSocket() {
        assert.step('Socket built');
        return new MockSocket();
      },
    });

    this.owner.register('service:sockets', mockSockets);

    await Exec.visitJob({
      job: this.job.id,
    });

    await settled();

    await Exec.taskGroups[0].click();
    await Exec.taskGroups[0].tasks[0].click();

    await Exec.taskGroups[1].click();
    await Exec.taskGroups[1].tasks[0].click();

    await Exec.terminal.pressEnter();

    assert.verifySteps(['Socket built']);
  });

  test('the command can be customised', async function(assert) {
    let mockSockets = Service.extend({
      getTaskStateSocket(taskState, command) {
        assert.equal(command, '/sh');
        localStorage.getItem('nomadExecCommand', JSON.stringify('/sh'));

        assert.step('Socket built');

        return new MockSocket();
      },
    });

    this.owner.register('service:sockets', mockSockets);

    await Exec.visitJob({ job: this.job.id });
    await Exec.taskGroups[0].click();
    await Exec.taskGroups[0].tasks[0].click();

    let taskGroup = this.job.task_groups.models[0];
    let task = taskGroup.tasks.models[0];
    let allocation = this.server.db.allocations.findBy({
      jobId: this.job.id,
      taskGroup: taskGroup.name,
    });

    await settled();

    // Delete /bash
    await window.execTerminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } });
    await window.execTerminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } });
    await window.execTerminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } });
    await window.execTerminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } });
    await window.execTerminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } });

    // Delete /bin and try to go beyond
    await window.execTerminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } });
    await window.execTerminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } });
    await window.execTerminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } });
    await window.execTerminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } });
    await window.execTerminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } });
    await window.execTerminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } });
    await window.execTerminal.simulateCommandKeyEvent({ domEvent: { key: 'Backspace' } });

    await settled();

    assert.equal(
      window.execTerminal.buffer
        .getLine(6)
        .translateToString()
        .trim(),
      `$ nomad alloc exec -i -t -task ${task.name} ${allocation.id.split('-')[0]}`
    );

    await window.execTerminal.simulateCommandKeyEvent({ key: '/', domEvent: {} });
    await window.execTerminal.simulateCommandKeyEvent({ key: 's', domEvent: {} });
    await window.execTerminal.simulateCommandKeyEvent({ key: 'h', domEvent: {} });

    await Exec.terminal.pressEnter();
    await settled();

    assert.verifySteps(['Socket built']);
  });

  test('a persisted customised command is recalled', async function(assert) {
    localStorage.setItem('nomadExecCommand', JSON.stringify('/bin/sh'));

    let taskGroup = this.job.task_groups.models[0];
    let task = taskGroup.tasks.models[0];
    let allocations = this.server.db.allocations.where({
      jobId: this.job.id,
      taskGroup: taskGroup.name,
    });
    let allocation = allocations[allocations.length - 1];

    await Exec.visitTask({
      job: this.job.id,
      task_group: taskGroup.name,
      task_name: task.name,
      allocation: allocation.id.split('-')[0],
    });

    await settled();

    assert.equal(
      window.execTerminal.buffer
        .getLine(4)
        .translateToString()
        .trim(),
      `$ nomad alloc exec -i -t -task ${task.name} ${allocation.id.split('-')[0]} /bin/sh`
    );
  });
});

class MockSocket {
  constructor() {
    this.sent = [];
  }

  send(message) {
    this.sent.push(message);
  }
}
<|endoftext|>