<commit_before>const path = require('path');

module.exports = {
  '@tags': ['search-page'],

  before: function (browser) {
    browser
      .url('https://soundcloud.com/search?q=test')
      .dismissCookiePolicyNotification();
  },

  after: function (browser) {
    browser.end();
  },

  'Adds a Download button for every track or playlist item in the Everything list': function (browser) {
    browser
      .waitForElementVisible('.searchResultGroupHeading')
      .elements('css selector', '.searchList__item .sound__body', function (results) {
        browser.globals.numSoundItemsInitial = results.value.length;
      })
      .elements('css selector', '.searchList__item .sound__body .sc-button-share + button.zc-button-download', function (results) {
        browser.globals.numZcBtnsInitial = results.value.length;
      })
      .perform(function () {
        browser.assert.ok(browser.globals.numSoundItemsInitial > 1,
          `Number of track or playlist items loaded (${browser.globals.numSoundItemsInitial}) should be more than 1.`);
        browser.assert.strictEqual(browser.globals.numSoundItemsInitial, browser.globals.numZcBtnsInitial,
          'Should add a Download button for every track or playlist item in the list.');
      });
  },

  'Continues adding Download buttons as more items are loaded into the list': function (browser) {
    browser
      .loadMoreItemsIntoList('.searchList .searchList__item:last-of-type')
      .elements('css selector', '.searchList__item .sound__body', function (results) {
        browser.globals.numSoundItemsFinal = results.value.length;
      })
      .elements('css selector', '.searchList__item .sound__body .sc-button-share + button.zc-button-download', function (results) {
        browser.globals.numZcBtnsFinal = results.value.length;
      })
      .perform(function () {
        browser.assert.ok(browser.globals.numSoundItemsFinal > browser.globals.numSoundItemsInitial,
          `Number of track or playlist items loaded (${browser.globals.numSoundItemsFinal}) `
          + `should be more than initial (${browser.globals.numSoundItemsInitial}).`);
        browser.assert.strictEqual(browser.globals.numSoundItemsFinal, browser.globals.numZcBtnsFinal,
          'Should add a Download button for every track or playlist item in the list.');
      });
  },

  'Adds a Download button for every track in the SoundCloud Go+ tracks list': function (browser) {
    browser
      .useXpath()
      .click('//*[contains(@class,\'g-nav-item\')]//a[contains(text(),\'SoundCloud Go+ tracks\')]')
      .useCss()
      .waitForElementVisible('.searchPremiumContentHeader')
      .elements('css selector', '.searchList__item', function (results) {
        browser.globals.numTracks = results.value.length;
      })
      .elements('css selector', '.searchList__item .sc-button-share + button.zc-button-download', function (results) {
        browser.globals.numZcBtns = results.value.length;
      })
      .perform(function () {
        browser.assert.ok(browser.globals.numTracks > 1,
          `Number of tracks loaded (${browser.globals.numTracks}) should be more than 1.`);
        browser.assert.strictEqual(browser.globals.numTracks, browser.globals.numZcBtns,
          'Should add a Download button for every track in the list.');
      });
  },

  'Adds a Download button for every track in the Tracks list': function (browser) {
    browser
      .useXpath()
      .click('//*[contains(@class,\'g-nav-item\')]//a[contains(text(),\'Tracks\')]')
      .useCss()
      .waitForElementVisible('.searchResultGroupHeading')
      .elements('css selector', '.searchList__item', function (results) {
        browser.globals.numTracks = results.value.length;
      })
      .elements('css selector', '.searchList__item .sc-button-share + button.zc-button-download', function (results) {
        browser.globals.numZcBtns = results.value.length;
      })
      .perform(function () {
        browser.assert.ok(browser.globals.numTracks > 1,
          `Number of tracks loaded (${browser.globals.numTracks}) should be more than 1.`);
        browser.assert.strictEqual(browser.globals.numTracks, browser.globals.numZcBtns,
          'Should add a Download button for every track in the list.');
      });
  },

  'Downloads the track when the Download button in the list is clicked': function (browser) {
    const trackPath = path.join(browser.globals.downloadDir, '07 Lil Baby & Marlo- Time After Time Ft. Tk Kravitz [Prod. By Quay Global].mp3');
    browser
      .setValue('input[type="search"]', ['07 Lil Baby & Marlo- Time After Time Ft. Tk Kravitz [Prod. By Quay Global]', browser.Keys.ENTER])
      .waitForElementVisible('.searchResultGroupHeading')
      .click('.searchList__item button.zc-button-download')
      .assert.fileDownloaded(trackPath)
      .verify.fileHasSize(trackPath, 3360581)
      .clearValue('input[type="search"]')
      .setValue('input[type="search"]', ['test', browser.Keys.ENTER]) // reset search back to 'test'
  },

  'Adds a Download button for every album in the Albums list': function (browser) {
    browser
      .useXpath()
      .click('//*[contains(@class,\'g-nav-item\')]//a[contains(text(),\'Albums\')]')
      .useCss()
      .waitForElementVisible('.searchResultGroupHeading')
      .elements('css selector', '.searchList__item', function (results) {
        browser.globals.numAlbums = results.value.length;
      })
      .elements('css selector', '.searchList__item .sc-button-share + button.zc-button-download', function (results) {
        browser.globals.numZcBtns = results.value.length;
      })
      .perform(function () {
        browser.assert.ok(browser.globals.numAlbums > 1,
          `Number of albums loaded (${browser.globals.numAlbums}) should be more than 1.`);
        browser.assert.strictEqual(browser.globals.numAlbums, browser.globals.numZcBtns,
          'Should add a Download button for every album in the list.');
      });
  },

  'Adds a Download button for every playlist in the Playlists list': function (browser) {
    browser
      .useXpath()
      .click('//*[contains(@class,\'g-nav-item\')]//a[contains(text(),\'Playlists\')]')
      .useCss()
      .waitForElementVisible('.searchResultGroupHeading')
      .elements('css selector', '.searchList__item', function (results) {
        browser.globals.numPlaylists = results.value.length;
      })
      .elements('css selector', '.searchList__item .sc-button-share + button.zc-button-download', function (results) {
        browser.globals.numZcBtns = results.value.length;
      })
      .perform(function () {
        browser.assert.ok(browser.globals.numPlaylists > 1,
          `Number of playlists loaded (${browser.globals.numPlaylists}) should be more than 1.`);
        browser.assert.strictEqual(browser.globals.numPlaylists, browser.globals.numZcBtns,
          'Should add a Download button for every playlist in the list.');
      });
  },

  'Downloads the playlist when the Download button in the list is clicked': function (browser) {
    const playlistDir = path.join(browser.globals.downloadDir, 'xtangle - Zqc9JceJT11d27T3ukAM');
    const track1Path = path.join(playlistDir, 'STOOPID (Feat. Bobby Shmurda).mp3');
    const track2Path = path.join(playlistDir, 'Lil Peep & XXXTENTACION - Falling Down.mp3');
    browser
      .setValue('input[type="search"]', ['xtangle Zqc9JceJT11d27T3ukAM', browser.Keys.ENTER])
      .waitForElementVisible('.searchResultGroupHeading')
      .click('.searchList__item button.zc-button-download')
      .assert.fileDownloaded(track1Path)
      .verify.fileHasSize(track1Path, 2489082)
      .assert.fileDownloaded(track2Path)
      .verify.fileHasSize(track2Path, 3215457);
  }
};
<commit_msg>fix(e2e): add waits to make test less brittle<commit_after>const path = require('path');

module.exports = {
  '@tags': ['search-page'],

  before: function (browser) {
    browser
      .url('https://soundcloud.com/search?q=test')
      .dismissCookiePolicyNotification();
  },

  after: function (browser) {
    browser.end();
  },

  'Adds a Download button for every track or playlist item in the Everything list': function (browser) {
    browser
      .waitForElementVisible('.searchResultGroupHeading')
      .elements('css selector', '.searchList__item .sound__body', function (results) {
        browser.globals.numSoundItemsInitial = results.value.length;
      })
      .elements('css selector', '.searchList__item .sound__body .sc-button-share + button.zc-button-download', function (results) {
        browser.globals.numZcBtnsInitial = results.value.length;
      })
      .perform(function () {
        browser.assert.ok(browser.globals.numSoundItemsInitial > 1,
          `Number of track or playlist items loaded (${browser.globals.numSoundItemsInitial}) should be more than 1.`);
        browser.assert.strictEqual(browser.globals.numSoundItemsInitial, browser.globals.numZcBtnsInitial,
          'Should add a Download button for every track or playlist item in the list.');
      });
  },

  'Continues adding Download buttons as more items are loaded into the list': function (browser) {
    browser
      .loadMoreItemsIntoList('.searchList .searchList__item:last-of-type')
      .elements('css selector', '.searchList__item .sound__body', function (results) {
        browser.globals.numSoundItemsFinal = results.value.length;
      })
      .elements('css selector', '.searchList__item .sound__body .sc-button-share + button.zc-button-download', function (results) {
        browser.globals.numZcBtnsFinal = results.value.length;
      })
      .perform(function () {
        browser.assert.ok(browser.globals.numSoundItemsFinal > browser.globals.numSoundItemsInitial,
          `Number of track or playlist items loaded (${browser.globals.numSoundItemsFinal}) `
          + `should be more than initial (${browser.globals.numSoundItemsInitial}).`);
        browser.assert.strictEqual(browser.globals.numSoundItemsFinal, browser.globals.numZcBtnsFinal,
          'Should add a Download button for every track or playlist item in the list.');
      });
  },

  'Adds a Download button for every track in the SoundCloud Go+ tracks list': function (browser) {
    browser
      .useXpath()
      .click('//*[contains(@class,\'g-nav-item\')]//a[contains(text(),\'SoundCloud Go+ tracks\')]')
      .useCss()
      .waitForElementVisible('.searchPremiumContentHeader')
      .elements('css selector', '.searchList__item', function (results) {
        browser.globals.numTracks = results.value.length;
      })
      .elements('css selector', '.searchList__item .sc-button-share + button.zc-button-download', function (results) {
        browser.globals.numZcBtns = results.value.length;
      })
      .perform(function () {
        browser.assert.ok(browser.globals.numTracks > 1,
          `Number of tracks loaded (${browser.globals.numTracks}) should be more than 1.`);
        browser.assert.strictEqual(browser.globals.numTracks, browser.globals.numZcBtns,
          'Should add a Download button for every track in the list.');
      });
  },

  'Adds a Download button for every track in the Tracks list': function (browser) {
    browser
      .useXpath()
      .click('//*[contains(@class,\'g-nav-item\')]//a[contains(text(),\'Tracks\')]')
      .useCss()
      .waitForElementVisible('.searchResultGroupHeading')
      .elements('css selector', '.searchList__item', function (results) {
        browser.globals.numTracks = results.value.length;
      })
      .elements('css selector', '.searchList__item .sc-button-share + button.zc-button-download', function (results) {
        browser.globals.numZcBtns = results.value.length;
      })
      .perform(function () {
        browser.assert.ok(browser.globals.numTracks > 1,
          `Number of tracks loaded (${browser.globals.numTracks}) should be more than 1.`);
        browser.assert.strictEqual(browser.globals.numTracks, browser.globals.numZcBtns,
          'Should add a Download button for every track in the list.');
      });
  },

  'Downloads the track when the Download button in the list is clicked': function (browser) {
    const trackPath = path.join(browser.globals.downloadDir, '07 Lil Baby & Marlo- Time After Time Ft. Tk Kravitz [Prod. By Quay Global].mp3');
    browser
      .setValue('input[type="search"]', ['07 Lil Baby & Marlo- Time After Time Ft. Tk Kravitz [Prod. By Quay Global]', browser.Keys.ENTER])
      .pause(500) // wait for search to initiate
      .waitForElementVisible('.searchResultGroupHeading')
      .click('.searchList__item button.zc-button-download')
      .assert.fileDownloaded(trackPath)
      .verify.fileHasSize(trackPath, 3360581)
      .clearValue('input[type="search"]')
      .setValue('input[type="search"]', ['test', browser.Keys.ENTER]) // reset search back to 'test'
  },

  'Adds a Download button for every album in the Albums list': function (browser) {
    browser
      .useXpath()
      .click('//*[contains(@class,\'g-nav-item\')]//a[contains(text(),\'Albums\')]')
      .useCss()
      .waitForElementVisible('.searchResultGroupHeading')
      .elements('css selector', '.searchList__item', function (results) {
        browser.globals.numAlbums = results.value.length;
      })
      .elements('css selector', '.searchList__item .sc-button-share + button.zc-button-download', function (results) {
        browser.globals.numZcBtns = results.value.length;
      })
      .perform(function () {
        browser.assert.ok(browser.globals.numAlbums > 1,
          `Number of albums loaded (${browser.globals.numAlbums}) should be more than 1.`);
        browser.assert.strictEqual(browser.globals.numAlbums, browser.globals.numZcBtns,
          'Should add a Download button for every album in the list.');
      });
  },

  'Adds a Download button for every playlist in the Playlists list': function (browser) {
    browser
      .useXpath()
      .click('//*[contains(@class,\'g-nav-item\')]//a[contains(text(),\'Playlists\')]')
      .useCss()
      .waitForElementVisible('.searchResultGroupHeading')
      .elements('css selector', '.searchList__item', function (results) {
        browser.globals.numPlaylists = results.value.length;
      })
      .elements('css selector', '.searchList__item .sc-button-share + button.zc-button-download', function (results) {
        browser.globals.numZcBtns = results.value.length;
      })
      .perform(function () {
        browser.assert.ok(browser.globals.numPlaylists > 1,
          `Number of playlists loaded (${browser.globals.numPlaylists}) should be more than 1.`);
        browser.assert.strictEqual(browser.globals.numPlaylists, browser.globals.numZcBtns,
          'Should add a Download button for every playlist in the list.');
      });
  },

  'Downloads the playlist when the Download button in the list is clicked': function (browser) {
    const playlistDir = path.join(browser.globals.downloadDir, 'xtangle - Zqc9JceJT11d27T3ukAM');
    const track1Path = path.join(playlistDir, 'STOOPID (Feat. Bobby Shmurda).mp3');
    const track2Path = path.join(playlistDir, 'Lil Peep & XXXTENTACION - Falling Down.mp3');
    browser
      .setValue('input[type="search"]', ['xtangle Zqc9JceJT11d27T3ukAM', browser.Keys.ENTER])
      .pause(500) // wait for search to initiate
      .waitForElementVisible('.searchResultGroupHeading')
      .click('.searchList__item button.zc-button-download')
      .assert.fileDownloaded(track1Path)
      .verify.fileHasSize(track1Path, 2489082)
      .assert.fileDownloaded(track2Path)
      .verify.fileHasSize(track2Path, 3215457);
  }
};
<|endoftext|>


<commit_before>/*
 * agileSprint.js
 *
 * Copyright (c) 2014 James G Jenner
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

// "use strict"

if (typeof module == "undefined") {
  var module = function () {};
  var exports = this.agileSprint = {};
  module.exports = exports;
}
if (typeof require != "undefined") {

}

module.exports = AgileSprint;

AgileSprint.MESSAGE_STATS = 'agileSprintStats';

AgileSprint.MESSAGE_TYPE_SUMMARY = 'agileSprintSummary';
AgileSprint.MESSAGE_TYPE_BREAKDOWN = 'agileSprintBreakdown';

function AgileSprint(options) {
  options = options || {};

  this.messageType = ((options.messageType !== null && options.messageType !== undefined) ? options.messageType : AgileSprint.MESSAGE_TYPE_OVERVIEW);
  this.blockedItems = ((options.blockedItems !== null && options.blockedItems !== undefined) ? options.blockedItems : 0);
  this.backlogStoryPoints = ((options.backlogStoryPoints !== null && options.backlogStoryPoints !== undefined) ? options.backlogStoryPoints : 0);
  this.wipStoryPoints = ((options.wipStoryPoints !== null && options.wipStoryPoints !== undefined) ? options.wipStoryPoints : 0);
  this.completedStoryPoints = ((options.completedStoryPoints !== null && options.completedStoryPoints !== undefined) ? options.completedStoryPoints : 0);
  this.percentageDone = ((options.percentageDone !== null && options.percentageDone !== undefined) ? options.percentageDone : 0);
  this.percentageTarget = ((options.percentageTarget !== null && options.percentageTarget !== undefined) ? options.percentageTarget : 0);
  this.sprintStartDate = ((options.sprintStartDate !== null && options.sprintStartDate !== undefined) ? options.sprintStartDate : '');
  this.sprintEndDate = ((options.sprintEndDate !== null && options.sprintEndDate !== undefined) ? options.sprintEndDate : '');
  this.sprintDuration = ((options.sprintDuration !== null && options.sprintDuration !== undefined) ? options.sprintDuration : 0);
  this.data = ((options.data !== null && options.data !== undefined) ? options.data : []);
}<commit_msg>add new message type (COMBINED) and add estimatedWork<commit_after>/*
 * agileSprint.js
 *
 * Copyright (c) 2014 James G Jenner
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

// "use strict"

if (typeof module == "undefined") {
  var module = function () {};
  var exports = this.agileSprint = {};
  module.exports = exports;
}
if (typeof require != "undefined") {

}

module.exports = AgileSprint;

AgileSprint.MESSAGE_STATS = 'agileSprintStats';

AgileSprint.MESSAGE_TYPE_COMBINED = 'agileSprintCombined';
AgileSprint.MESSAGE_TYPE_SUMMARY = 'agileSprintSummary';
AgileSprint.MESSAGE_TYPE_BREAKDOWN = 'agileSprintBreakdown';

function AgileSprint(options) {
  options = options || {};

  this.messageType = ((options.messageType !== null && options.messageType !== undefined) ? options.messageType : AgileSprint.MESSAGE_TYPE_OVERVIEW);
  this.blockedItems = ((options.blockedItems !== null && options.blockedItems !== undefined) ? options.blockedItems : 0);
  this.backlogStoryPoints = ((options.backlogStoryPoints !== null && options.backlogStoryPoints !== undefined) ? options.backlogStoryPoints : 0);
  this.wipStoryPoints = ((options.wipStoryPoints !== null && options.wipStoryPoints !== undefined) ? options.wipStoryPoints : 0);
  this.completedStoryPoints = ((options.completedStoryPoints !== null && options.completedStoryPoints !== undefined) ? options.completedStoryPoints : 0);
  this.percentageDone = ((options.percentageDone !== null && options.percentageDone !== undefined) ? options.percentageDone : 0);
  this.percentageTarget = ((options.percentageTarget !== null && options.percentageTarget !== undefined) ? options.percentageTarget : 0);
  this.sprintStartDate = ((options.sprintStartDate !== null && options.sprintStartDate !== undefined) ? options.sprintStartDate : '');
  this.sprintEndDate = ((options.sprintEndDate !== null && options.sprintEndDate !== undefined) ? options.sprintEndDate : '');
  this.sprintDuration = ((options.sprintDuration !== null && options.sprintDuration !== undefined) ? options.sprintDuration : 0);
  this.estimatedWork = ((options.estimatedWork !== null && options.estimatedWork !== undefined) ? options.estimatedWork : 0);
  this.data = ((options.data !== null && options.data !== undefined) ? options.data : []);
}<|endoftext|>


<commit_before>'use babel';

/* @flow */

import { Emitter, CompositeDisposable } from 'atom';
/* flow-include
import type { TextEditor, Disposable } from 'atom'
import type { Tester } from './types'
*/

export default class TesterRegistry {
    /* ::
    emitter: Emitter;
    testers: Set<Tester>;
    locks: Set<Tester>;
    testOnChange: boolean;
    subscriptions: CompositeDisposable;
    */

  constructor() {
    this.emitter = new Emitter();
    this.testers = new Set();
    this.locks = new Set();
    this.subscriptions = new CompositeDisposable();

    this.subscriptions.add(atom.config.observe('tester.testOnChange', (testOnChange) => {
      this.testOnChange = testOnChange;
    }));
  }

  hasTester(tester /* : Tester*/) /* : boolean*/ {
    return this.testers.has(tester);
  }

  addTester(tester /* : Tester*/) {
    console.log('addTester', tester);
    this.testers.add(tester);
  }

  deleteTester(tester /* : Tester*/) {
    console.log('deleteTester', tester);
    this.testers.delete(tester);
  }

  onDidUpdateMessages(callback /* : Function*/) /* : Disposable*/ {
    return this.emitter.on('did-update-messages', callback);
  }
  onDidBeginTesting(callback /* : Function*/) /* : Disposable*/ {
    return this.emitter.on('did-begin-testing', callback);
  }
  onDidFinishTesting(callback /* : Function*/) /* : Disposable*/ {
    return this.emitter.on('did-finish-testing', callback);
  }
  dispose() {
    this.testers.clear();
    this.subscriptions.dispose();
  }
  test(onChange/* : Event*/, editorTester/* : TextEditor*/) {
    console.log('TEST', onChange, editorTester);
    const editor = editorTester.getEditor();

    if (!editor.getPath() ||
        editor !== atom.workspace.getActiveTextEditor() ||
        this.locks.has(editorTester)) {
      console.log('Nothing todo');
      return Promise.resolve();
    }

    this.locks.add(editorTester);

    const promises = [];
    this.testers.forEach((tester) => {
      promises.push(new Promise((resolve) => {
        resolve(tester.test(editor));
      }).then((results) => {
        console.log('did-update-messages', results);
        if (results) {
          this.emitter.emit('did-update-messages', { tester, messages: results, editor });
        }
      }));
    });
    console.log('Promises all', promises);
    return Promise.all(promises).then((results) => {
      console.log('All DONE', results);
      this.locks.delete(editorTester);
    });
  }
}
<commit_msg>catch this<commit_after>'use babel';

/* @flow */

import { Emitter, CompositeDisposable } from 'atom';
/* flow-include
import type { TextEditor, Disposable } from 'atom'
import type { Tester } from './types'
*/

export default class TesterRegistry {
    /* ::
    emitter: Emitter;
    testers: Set<Tester>;
    locks: Set<Tester>;
    testOnChange: boolean;
    subscriptions: CompositeDisposable;
    */

  constructor() {
    this.emitter = new Emitter();
    this.testers = new Set();
    this.locks = new Set();
    this.subscriptions = new CompositeDisposable();

    this.subscriptions.add(atom.config.observe('tester.testOnChange', (testOnChange) => {
      this.testOnChange = testOnChange;
    }));
  }

  hasTester(tester /* : Tester*/) /* : boolean*/ {
    return this.testers.has(tester);
  }

  addTester(tester /* : Tester*/) {
    console.log('addTester', tester);
    this.testers.add(tester);
  }

  deleteTester(tester /* : Tester*/) {
    console.log('deleteTester', tester);
    this.testers.delete(tester);
  }

  onDidUpdateMessages(callback /* : Function*/) /* : Disposable*/ {
    return this.emitter.on('did-update-messages', callback);
  }
  onDidBeginTesting(callback /* : Function*/) /* : Disposable*/ {
    return this.emitter.on('did-begin-testing', callback);
  }
  onDidFinishTesting(callback /* : Function*/) /* : Disposable*/ {
    return this.emitter.on('did-finish-testing', callback);
  }
  dispose() {
    this.testers.clear();
    this.subscriptions.dispose();
  }
  test(onChange/* : Event*/, editorTester/* : TextEditor*/) {
    console.log('TEST', onChange, editorTester);
    const editor = editorTester.getEditor();

    if (!editor.getPath() ||
        editor !== atom.workspace.getActiveTextEditor() ||
        this.locks.has(editorTester)) {
      console.log('Nothing todo');
      return Promise.resolve();
    }

    this.locks.add(editorTester);

    const promises = [];
    this.testers.forEach((tester) => {
      promises.push(new Promise((resolve) => {
        resolve(tester.test(editor));
      }).then((results) => {
        console.log('did-update-messages', results);
        if (results) {
          this.emitter.emit('did-update-messages', { tester, messages: results, editor });
        }
      }));
    });
    console.log('Promises all', promises);
    return Promise.all(promises)
      .then((results) => {
        console.log('All DONE', results);
        this.locks.delete(editorTester);
      })
      .catch(err => {
          console.log('All catch', err);
      });
  }
}
<|endoftext|>


<commit_before>import {checkRendering} from '../../lib/utils.js';

const usage = `Usage:
var customMenu = connectMenu(function render(params, isFirstRendering) {
  // params = {
  //   items,
  //   state,
  //   createURL,
  //   refine,
  //   helper,
  //   instantSearchInstance,
  //   canRefine,
  // }
});
search.addWidget(
  customMenu({
    attributeName,
    [ limit ],
    [ sortBy = ['count:desc', 'name:asc'] ]
  });
);
Full documentation available at https://community.algolia.com/instantsearch.js/connectors/connectMenu.html
`;

/**
 * @typedef {Object} CustomMenuWidgetOptions
 * @param {string} attributeName Name of the attribute for faceting (eg. "free_shipping")
 * @param {number} [limit = 10] How many facets values to retrieve [*]
 * @param {string[]|function} [sortBy = ['count:desc', 'name:asc']] How to sort refinements. Possible values: `count|isRefined|name:asc|name:desc`.
 *   You can also use a sort function that behaves like the standard Javascript [compareFunction](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#Syntax). [*]
 */

/**
 * @typedef {Object} MenuRenderingOptions
 * @property {Object[]} items
 * @property {Object} state
 * @property {function} createURL
 * @property {function} refine
 * @property {AlgoliaSearchHelper} helper
 * @property {InstantSearch} instantSearchInstance
 * @property {boolean} canRefine
 * @property {Object} widgetParams all original options forwarded to rendering
 */

 /**
  * Connects a rendering function with the menu business logic.
  * @param {function(MenuRenderingOptions)} renderFn function that renders the menu widget
  * @return {function(CustomMenuWidgetOptions)} a widget factory for menu widget
  */
export default function connectMenu(renderFn) {
  checkRendering(renderFn, usage);

  return widgetParams => {
    const {
      attributeName,
      limit = 10,
      sortBy = ['count:desc', 'name:asc'],
    } = widgetParams;

    if (!attributeName) {
      throw new Error(usage);
    }

    return {
      getConfiguration(configuration) {
        const widgetConfiguration = {
          hierarchicalFacets: [{
            name: attributeName,
            attributes: [attributeName],
          }],
        };

        const currentMaxValuesPerFacet = configuration.maxValuesPerFacet || 0;
        widgetConfiguration.maxValuesPerFacet = Math.max(currentMaxValuesPerFacet, limit);

        return widgetConfiguration;
      },

      init({helper, createURL, instantSearchInstance}) {
        this._createURL = facetValue =>
          createURL(helper.state.toggleRefinement(attributeName, facetValue));

        this._refine = facetValue => helper
          .toggleRefinement(attributeName, facetValue)
          .search();

        this._helper = helper;

        renderFn({
          items: [],
          state: helper.state,
          createURL: this._createURL,
          refine: this._refine,
          helper: this._helper,
          instantSearchInstance,
          canRefine: false,
          widgetParams,
        }, true);
      },

      render({results, state, instantSearchInstance}) {
        const items = results.getFacetValues(attributeName, {sortBy}).data || [];

        renderFn({
          items,
          state,
          createURL: this._createURL,
          refine: this._refine,
          helper: this._helper,
          instantSearchInstance,
          canRefine: items.length > 0,
          widgetParams,
        }, false);
      },
    };
  };
}
<commit_msg>feat(connector): jsDoc for connectMenu<commit_after>import {checkRendering} from '../../lib/utils.js';

const usage = `Usage:
var customMenu = connectMenu(function render(params, isFirstRendering) {
  // params = {
  //   items,
  //   state,
  //   createURL,
  //   refine,
  //   helper,
  //   instantSearchInstance,
  //   canRefine,
  // }
});
search.addWidget(
  customMenu({
    attributeName,
    [ limit ],
    [ sortBy = ['count:desc', 'name:asc'] ]
  });
);
Full documentation available at https://community.algolia.com/instantsearch.js/connectors/connectMenu.html
`;

/**
 * @typedef {Object} CustomMenuWidgetOptions
 * @param {string} attributeName Name of the attribute for faceting (eg. "free_shipping")
 * @param {number} [limit = 10] How many facets values to retrieve [*]
 * @param {string[]|function} [sortBy = ['count:desc', 'name:asc']] How to sort refinements. Possible values: `count|isRefined|name:asc|name:desc`.
 *   You can also use a sort function that behaves like the standard Javascript [compareFunction](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#Syntax). [*]
 */

/**
 * @typedef {Object} MenuRenderingOptions
 * @property {Object[]} items the elements that can be refined for the current search results
 * @property {function} createURL creates the URL for a single item name in the list
 * @property {function} refine filter the search to item.name value
 * @property {boolean} canRefine true if refinement can be applied
 * @property {Object} widgetParams all original options forwarded to rendering
 * @property {InstantSearch} instantSearchInstance the instance of instantsearch on which the widget is attached
 */

 /**
  * Connects a rendering function with the menu business logic.
  * @param {function(MenuRenderingOptions)} renderFn function that renders the menu widget
  * @return {function(CustomMenuWidgetOptions)} a widget factory for menu widget
  */
export default function connectMenu(renderFn) {
  checkRendering(renderFn, usage);

  return widgetParams => {
    const {
      attributeName,
      limit = 10,
      sortBy = ['count:desc', 'name:asc'],
    } = widgetParams;

    if (!attributeName) {
      throw new Error(usage);
    }

    return {
      getConfiguration(configuration) {
        const widgetConfiguration = {
          hierarchicalFacets: [{
            name: attributeName,
            attributes: [attributeName],
          }],
        };

        const currentMaxValuesPerFacet = configuration.maxValuesPerFacet || 0;
        widgetConfiguration.maxValuesPerFacet = Math.max(currentMaxValuesPerFacet, limit);

        return widgetConfiguration;
      },

      init({helper, createURL, instantSearchInstance}) {
        this._createURL = facetValue =>
          createURL(helper.state.toggleRefinement(attributeName, facetValue));

        this._refine = facetValue => helper
          .toggleRefinement(attributeName, facetValue)
          .search();

        this._helper = helper;

        renderFn({
          items: [],
          createURL: this._createURL,
          refine: this._refine,
          instantSearchInstance,
          canRefine: false,
          widgetParams,
        }, true);
      },

      render({results, instantSearchInstance}) {
        const items = results.getFacetValues(attributeName, {sortBy}).data || [];

        renderFn({
          items,
          createURL: this._createURL,
          refine: this._refine,
          instantSearchInstance,
          canRefine: items.length > 0,
          widgetParams,
        }, false);
      },
    };
  };
}
<|endoftext|>


<commit_before>/*
 * Copyright (C) 2019 - present Instructure, Inc.
 *
 * This file is part of Canvas.
 *
 * Canvas is free software: you can redistribute it and/or modify it under
 * the terms of the GNU Affero General Public License as published by the Free
 * Software Foundation, version 3 of the License.
 *
 * Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
 * details.
 *
 * You should have received a copy of the GNU Affero General Public License along
 * with this program. If not, see <http://www.gnu.org/licenses/>.
 */
import {arrayOf, bool, func, instanceOf, shape, string} from 'prop-types'
import React, {Suspense} from 'react'
import ReactDOM from 'react-dom'
import {isEqual} from 'lodash'

import {Button, CloseButton} from '@instructure/ui-buttons'
import {Heading} from '@instructure/ui-heading'
import {Modal} from '@instructure/ui-modal'
import {Tabs} from '@instructure/ui-tabs'
import {px} from '@instructure/ui-utils'
import {ProgressBar} from '@instructure/ui-progress'
import {Text} from '@instructure/ui-text'

import {
  RCS_MAX_BODY_SIZE,
  RCS_REQUEST_SIZE_BUFFER
} from '@instructure/canvas-rce/src/rce/plugins/shared/Upload/constants'
import {ACCEPTED_FILE_TYPES} from './acceptedMediaFileTypes'
import LoadingIndicator from './shared/LoadingIndicator'
import saveMediaRecording, {saveClosedCaptions} from './saveMediaRecording'
import translationShape from './translationShape'

const ComputerPanel = React.lazy(() => import('./ComputerPanel'))
const MediaRecorder = React.lazy(() => import('./MediaRecorder'))

export const PANELS = {
  COMPUTER: 0,
  RECORD: 1
}

export default class UploadMedia extends React.Component {
  static propTypes = {
    disableSubmitWhileUploading: bool,
    languages: arrayOf(
      shape({
        id: string,
        label: string
      })
    ),
    liveRegion: func,
    rcsConfig: shape({
      contextId: string,
      contextType: string,
      origin: string,
      headers: shape({Authorization: string})
    }),
    onStartUpload: func,
    onUploadComplete: func,
    onDismiss: func,
    open: bool,
    tabs: shape({
      record: bool,
      upload: bool
    }),
    uploadMediaTranslations: translationShape,
    // for testing
    computerFile: instanceOf(File)
  }

  static defaultProps = {
    disableSubmitWhileUploading: false
  }

  constructor(props) {
    super(props)

    let defaultSelectedPanel = this.inferSelectedPanel(props.tabs)

    if (props.computerFile) {
      props.computerFile.title = props.computerFile.name
    }

    this.state = {
      hasUploadedFile: !!props.computerFile,
      uploading: false,
      progress: 0,
      selectedPanel: defaultSelectedPanel,
      computerFile: props.computerFile || null,
      subtitles: [],
      recordedFile: null,
      modalBodySize: {width: NaN, height: NaN}
    }

    this.modalBodyRef = React.createRef()
  }

  inferSelectedPanel = tabs => {
    let selectedPanel = -1

    if (tabs.upload) {
      selectedPanel = 0
    } else if (tabs.record) {
      selectedPanel = 1
    }

    return selectedPanel
  }

  isReady = () => {
    if (this.props.disableSubmitWhileUploading && this.state.uploading) {
      return false
    }

    switch (this.state.selectedPanel) {
      case PANELS.COMPUTER:
        return !!this.state.computerFile
      case PANELS.RECORD:
        return !!this.state.recordedFile
      default:
        return false
    }
  }

  handleSubmit = () => {
    switch (this.state.selectedPanel) {
      case PANELS.COMPUTER:
        this.uploadFile(this.state.computerFile)
        break
      case PANELS.RECORD:
        this.uploadFile(this.state.recordedFile)
        break
      default:
        throw new Error('Selected Panel is invalid') // Should never get here
    }
  }

  submitEnabled = () => {
    switch (this.state.selectedPanel) {
      case PANELS.COMPUTER:
        return this.isReady() && !!this.state.computerFile?.title
      default:
        return this.isReady()
    }
  }

  uploadFile(file) {
    this.setState({uploading: true}, () => {
      this.props.onStartUpload && this.props.onStartUpload(file)
      saveMediaRecording(
        file,
        this.props.rcsConfig,
        this.saveMediaCallback,
        this.onSaveMediaProgress
      )
    })
  }

  onSaveMediaProgress = progress => {
    this.setState({progress})
  }

  saveMediaCallback = async (err, data) => {
    if (err) {
      this.props.onUploadComplete && this.props.onUploadComplete(err, data)
    } else {
      try {
        if (this.state.selectedPanel === PANELS.COMPUTER && this.state.subtitles.length > 0) {
          await saveClosedCaptions(
            data.mediaObject.media_object.media_id,
            this.state.subtitles,
            this.props.rcsConfig,
            RCS_MAX_BODY_SIZE - RCS_REQUEST_SIZE_BUFFER
          )
        }
        this.props.onUploadComplete && this.props.onUploadComplete(null, data)
      } catch (ex) {
        this.props.onUploadComplete && this.props.onUploadComplete(ex, null)
      }
    }
    this.props.onDismiss && this.props.onDismiss()
  }

  componentDidMount() {
    this.setBodySize(this.state)
  }

  componentDidUpdate(prevProps, prevState) {
    this.setBodySize(prevState)

    // If the specified tabs have not changed, don't attempt
    // to set the selected panel state (this would trigger
    // and endless loop).
    if (isEqual(prevProps.tabs, this.props.tabs)) return

    if (prevState.selectedPanel === -1) {
      // The tabs prop has changed and the selectedPanel was
      // never set in the constructor. Attempt to infer the
      // selected panel based on the new tabs list
      this.setState({selectedPanel: this.inferSelectedPanel(this.props.tabs)})
    }
  }

  setBodySize(state) {
    if (this.modalBodyRef.current) {
      // eslint-disable-next-line react/no-find-dom-node
      const thebody = ReactDOM.findDOMNode(this.modalBodyRef.current)
      const modalBodySize = thebody.getBoundingClientRect()
      modalBodySize.height -= px('3rem') // leave room for the tabs
      if (
        modalBodySize.width !== state.modalBodySize.width ||
        modalBodySize.height !== state.modalBodySize.height
      ) {
        if (modalBodySize.width > 0 && modalBodySize.height > 0) {
          this.setState({modalBodySize})
        }
      }
    }
  }

  renderModalBody = () => {
    const {
      COMPUTER_PANEL_TITLE,
      DRAG_FILE_TEXT,
      LOADING_MEDIA,
      RECORD_PANEL_TITLE,
      MEDIA_RECORD_NOT_AVAILABLE
    } = this.props.uploadMediaTranslations.UploadMediaStrings

    return (
      <Tabs
        maxWidth="large"
        onRequestTabChange={(_, {index}) => {
          this.setState({selectedPanel: index})
        }}
      >
        {this.props.tabs.upload && (
          <Tabs.Panel
            key="computer"
            isSelected={this.state.selectedPanel === PANELS.COMPUTER}
            renderTitle={() => COMPUTER_PANEL_TITLE}
          >
            <Suspense fallback={LoadingIndicator(LOADING_MEDIA)}>
              <ComputerPanel
                theFile={this.state.computerFile}
                setFile={file => this.setState({computerFile: file})}
                hasUploadedFile={this.state.hasUploadedFile}
                setHasUploadedFile={uploadFileState =>
                  this.setState({hasUploadedFile: uploadFileState})
                }
                label={DRAG_FILE_TEXT}
                uploadMediaTranslations={this.props.uploadMediaTranslations}
                accept={ACCEPTED_FILE_TYPES}
                languages={this.props.languages}
                liveRegion={this.props.liveRegion}
                updateSubtitles={subtitles => {
                  this.setState({subtitles})
                }}
                bounds={this.state.modalBodySize}
              />
            </Suspense>
          </Tabs.Panel>
        )}
        {this.props.tabs.record && (
          <Tabs.Panel
            key="record"
            isSelected={this.state.selectedPanel === PANELS.RECORD}
            renderTitle={() => RECORD_PANEL_TITLE}
          >
            <Suspense fallback={LoadingIndicator(LOADING_MEDIA)}>
              <MediaRecorder
                MediaCaptureStrings={this.props.uploadMediaTranslations.MediaCaptureStrings}
                errorMessage={MEDIA_RECORD_NOT_AVAILABLE}
                onSave={file => this.setState({recordedFile: file}, this.handleSubmit)}
              />
            </Suspense>
          </Tabs.Panel>
        )}
      </Tabs>
    )
  }

  onModalClose = () => {
    this.setState({
      hasUploadedFile: false,
      selectedPanel: 0,
      computerFile: null
    })
    this.props.onDismiss()
  }

  renderModalFooter = () => {
    if (this.state.selectedPanel === PANELS.RECORD) {
      return null
    }

    const {CLOSE_TEXT, SUBMIT_TEXT, PROGRESS_LABEL} =
      this.props.uploadMediaTranslations.UploadMediaStrings
    return (
      <Modal.Footer>
        {this.state.uploading && (
          <ProgressBar
            screenReaderLabel={PROGRESS_LABEL}
            valueNow={this.state.progress}
            valueMax={100}
            renderValue={({valueNow}) => {
              return <Text>{valueNow}%</Text>
            }}
          />
        )}

        <Button onClick={this.onModalClose}> {CLOSE_TEXT} </Button>

        <Button
          onClick={e => {
            e.preventDefault()
            this.handleSubmit()
          }}
          color="primary"
          type="submit"
          interaction={this.submitEnabled() ? 'enabled' : 'disabled'}
        >
          {SUBMIT_TEXT}
        </Button>
      </Modal.Footer>
    )
  }

  render() {
    const {CLOSE_TEXT, UPLOAD_MEDIA_LABEL} = this.props.uploadMediaTranslations.UploadMediaStrings

    return (
      <Modal
        label={UPLOAD_MEDIA_LABEL}
        size="large"
        onDismiss={this.onModalClose}
        open={this.props.open}
        shouldCloseOnDocumentClick={false}
        liveRegion={this.props.liveRegion}
      >
        <Modal.Header>
          <CloseButton
            onClick={this.onModalClose}
            offset="medium"
            placement="end"
            screenReaderLabel={CLOSE_TEXT}
          />
          <Heading>{UPLOAD_MEDIA_LABEL}</Heading>
        </Modal.Header>
        <Modal.Body ref={this.modalBodyRef}>{this.renderModalBody()}</Modal.Body>
        {this.renderModalFooter()}
      </Modal>
    )
  }
}
<commit_msg>stop requesting media access after modal close<commit_after>/*
 * Copyright (C) 2019 - present Instructure, Inc.
 *
 * This file is part of Canvas.
 *
 * Canvas is free software: you can redistribute it and/or modify it under
 * the terms of the GNU Affero General Public License as published by the Free
 * Software Foundation, version 3 of the License.
 *
 * Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
 * details.
 *
 * You should have received a copy of the GNU Affero General Public License along
 * with this program. If not, see <http://www.gnu.org/licenses/>.
 */
import {arrayOf, bool, func, instanceOf, shape, string} from 'prop-types'
import React, {Suspense} from 'react'
import ReactDOM from 'react-dom'
import {isEqual} from 'lodash'

import {Button, CloseButton} from '@instructure/ui-buttons'
import {Heading} from '@instructure/ui-heading'
import {Modal} from '@instructure/ui-modal'
import {Tabs} from '@instructure/ui-tabs'
import {px} from '@instructure/ui-utils'
import {ProgressBar} from '@instructure/ui-progress'
import {Text} from '@instructure/ui-text'

import {
  RCS_MAX_BODY_SIZE,
  RCS_REQUEST_SIZE_BUFFER
} from '@instructure/canvas-rce/src/rce/plugins/shared/Upload/constants'
import {ACCEPTED_FILE_TYPES} from './acceptedMediaFileTypes'
import LoadingIndicator from './shared/LoadingIndicator'
import saveMediaRecording, {saveClosedCaptions} from './saveMediaRecording'
import translationShape from './translationShape'

const ComputerPanel = React.lazy(() => import('./ComputerPanel'))
const MediaRecorder = React.lazy(() => import('./MediaRecorder'))

export const PANELS = {
  COMPUTER: 0,
  RECORD: 1
}

export default class UploadMedia extends React.Component {
  static propTypes = {
    disableSubmitWhileUploading: bool,
    languages: arrayOf(
      shape({
        id: string,
        label: string
      })
    ),
    liveRegion: func,
    rcsConfig: shape({
      contextId: string,
      contextType: string,
      origin: string,
      headers: shape({Authorization: string})
    }),
    onStartUpload: func,
    onUploadComplete: func,
    onDismiss: func,
    open: bool,
    tabs: shape({
      record: bool,
      upload: bool
    }),
    uploadMediaTranslations: translationShape,
    // for testing
    computerFile: instanceOf(File)
  }

  static defaultProps = {
    disableSubmitWhileUploading: false
  }

  constructor(props) {
    super(props)

    const defaultSelectedPanel = this.inferSelectedPanel(props.tabs)

    if (props.computerFile) {
      props.computerFile.title = props.computerFile.name
    }

    this.state = {
      hasUploadedFile: !!props.computerFile,
      uploading: false,
      progress: 0,
      selectedPanel: defaultSelectedPanel,
      computerFile: props.computerFile || null,
      subtitles: [],
      recordedFile: null,
      modalBodySize: {width: NaN, height: NaN}
    }

    this.modalBodyRef = React.createRef()
  }

  inferSelectedPanel = tabs => {
    let selectedPanel = -1

    if (tabs.upload) {
      selectedPanel = 0
    } else if (tabs.record) {
      selectedPanel = 1
    }

    return selectedPanel
  }

  isReady = () => {
    if (this.props.disableSubmitWhileUploading && this.state.uploading) {
      return false
    }

    switch (this.state.selectedPanel) {
      case PANELS.COMPUTER:
        return !!this.state.computerFile
      case PANELS.RECORD:
        return !!this.state.recordedFile
      default:
        return false
    }
  }

  handleSubmit = () => {
    switch (this.state.selectedPanel) {
      case PANELS.COMPUTER:
        this.uploadFile(this.state.computerFile)
        break
      case PANELS.RECORD:
        this.uploadFile(this.state.recordedFile)
        break
      default:
        throw new Error('Selected Panel is invalid') // Should never get here
    }
  }

  submitEnabled = () => {
    switch (this.state.selectedPanel) {
      case PANELS.COMPUTER:
        return this.isReady() && !!this.state.computerFile?.title
      default:
        return this.isReady()
    }
  }

  uploadFile(file) {
    this.setState({uploading: true}, () => {
      this.props.onStartUpload && this.props.onStartUpload(file)
      saveMediaRecording(
        file,
        this.props.rcsConfig,
        this.saveMediaCallback,
        this.onSaveMediaProgress
      )
    })
  }

  onSaveMediaProgress = progress => {
    this.setState({progress})
  }

  saveMediaCallback = async (err, data) => {
    if (err) {
      this.props.onUploadComplete && this.props.onUploadComplete(err, data)
    } else {
      try {
        if (this.state.selectedPanel === PANELS.COMPUTER && this.state.subtitles.length > 0) {
          await saveClosedCaptions(
            data.mediaObject.media_object.media_id,
            this.state.subtitles,
            this.props.rcsConfig,
            RCS_MAX_BODY_SIZE - RCS_REQUEST_SIZE_BUFFER
          )
        }
        this.props.onUploadComplete && this.props.onUploadComplete(null, data)
      } catch (ex) {
        this.props.onUploadComplete && this.props.onUploadComplete(ex, null)
      }
    }
    this.props.onDismiss && this.props.onDismiss()
  }

  componentDidMount() {
    this.setBodySize(this.state)
  }

  componentDidUpdate(prevProps, prevState) {
    this.setBodySize(prevState)

    // If the specified tabs have not changed, don't attempt
    // to set the selected panel state (this would trigger
    // and endless loop).
    if (isEqual(prevProps.tabs, this.props.tabs)) return

    if (prevState.selectedPanel === -1) {
      // The tabs prop has changed and the selectedPanel was
      // never set in the constructor. Attempt to infer the
      // selected panel based on the new tabs list
      this.setState({selectedPanel: this.inferSelectedPanel(this.props.tabs)})
    }
  }

  setBodySize(state) {
    if (this.modalBodyRef.current) {
      // eslint-disable-next-line react/no-find-dom-node
      const thebody = ReactDOM.findDOMNode(this.modalBodyRef.current)
      const modalBodySize = thebody.getBoundingClientRect()
      modalBodySize.height -= px('3rem') // leave room for the tabs
      if (
        modalBodySize.width !== state.modalBodySize.width ||
        modalBodySize.height !== state.modalBodySize.height
      ) {
        if (modalBodySize.width > 0 && modalBodySize.height > 0) {
          this.setState({modalBodySize})
        }
      }
    }
  }

  renderModalBody = () => {
    const {
      COMPUTER_PANEL_TITLE,
      DRAG_FILE_TEXT,
      LOADING_MEDIA,
      RECORD_PANEL_TITLE,
      MEDIA_RECORD_NOT_AVAILABLE
    } = this.props.uploadMediaTranslations.UploadMediaStrings

    if (!this.props.open) {
      return null
    }

    return (
      <Tabs
        maxWidth="large"
        onRequestTabChange={(_, {index}) => {
          this.setState({selectedPanel: index})
        }}
      >
        {this.props.tabs.upload && (
          <Tabs.Panel
            key="computer"
            isSelected={this.state.selectedPanel === PANELS.COMPUTER}
            renderTitle={() => COMPUTER_PANEL_TITLE}
          >
            <Suspense fallback={LoadingIndicator(LOADING_MEDIA)}>
              <ComputerPanel
                theFile={this.state.computerFile}
                setFile={file => this.setState({computerFile: file})}
                hasUploadedFile={this.state.hasUploadedFile}
                setHasUploadedFile={uploadFileState =>
                  this.setState({hasUploadedFile: uploadFileState})
                }
                label={DRAG_FILE_TEXT}
                uploadMediaTranslations={this.props.uploadMediaTranslations}
                accept={ACCEPTED_FILE_TYPES}
                languages={this.props.languages}
                liveRegion={this.props.liveRegion}
                updateSubtitles={subtitles => {
                  this.setState({subtitles})
                }}
                bounds={this.state.modalBodySize}
              />
            </Suspense>
          </Tabs.Panel>
        )}
        {this.props.tabs.record && (
          <Tabs.Panel
            key="record"
            isSelected={this.state.selectedPanel === PANELS.RECORD}
            renderTitle={() => RECORD_PANEL_TITLE}
          >
            <Suspense fallback={LoadingIndicator(LOADING_MEDIA)}>
              <MediaRecorder
                MediaCaptureStrings={this.props.uploadMediaTranslations.MediaCaptureStrings}
                errorMessage={MEDIA_RECORD_NOT_AVAILABLE}
                onSave={file => this.setState({recordedFile: file}, this.handleSubmit)}
              />
            </Suspense>
          </Tabs.Panel>
        )}
      </Tabs>
    )
  }

  onModalClose = () => {
    this.setState({
      hasUploadedFile: false,
      selectedPanel: 0,
      computerFile: null
    })
    this.props.onDismiss()
  }

  renderModalFooter = () => {
    if (this.state.selectedPanel === PANELS.RECORD) {
      return null
    }

    const {CLOSE_TEXT, SUBMIT_TEXT, PROGRESS_LABEL} =
      this.props.uploadMediaTranslations.UploadMediaStrings
    return (
      <Modal.Footer>
        {this.state.uploading && (
          <ProgressBar
            screenReaderLabel={PROGRESS_LABEL}
            valueNow={this.state.progress}
            valueMax={100}
            renderValue={({valueNow}) => {
              return <Text>{valueNow}%</Text>
            }}
          />
        )}

        <Button onClick={this.onModalClose}> {CLOSE_TEXT} </Button>

        <Button
          onClick={e => {
            e.preventDefault()
            this.handleSubmit()
          }}
          color="primary"
          type="submit"
          interaction={this.submitEnabled() ? 'enabled' : 'disabled'}
        >
          {SUBMIT_TEXT}
        </Button>
      </Modal.Footer>
    )
  }

  render() {
    const {CLOSE_TEXT, UPLOAD_MEDIA_LABEL} = this.props.uploadMediaTranslations.UploadMediaStrings

    return (
      <Modal
        label={UPLOAD_MEDIA_LABEL}
        size="large"
        onDismiss={this.onModalClose}
        open={this.props.open}
        shouldCloseOnDocumentClick={false}
        liveRegion={this.props.liveRegion}
      >
        <Modal.Header>
          <CloseButton
            onClick={this.onModalClose}
            offset="medium"
            placement="end"
            screenReaderLabel={CLOSE_TEXT}
          />
          <Heading>{UPLOAD_MEDIA_LABEL}</Heading>
        </Modal.Header>
        <Modal.Body ref={this.modalBodyRef}>{this.renderModalBody()}</Modal.Body>
        {this.renderModalFooter()}
      </Modal>
    )
  }
}
<|endoftext|>


<commit_before>/*
 * TODO: migrate database functionality to 'com.phonegap.plugins.sqlite'
 * @see https://github.com/brodysoft/Cordova-SQLitePlugin
 */
(function () {
    /* yes please */
    'use strict';
    /* ClassSection object maker */
    var makeClassSection = function (_name, _title, _instructor, _location, _times) {
        // create a new object from the Object prototype
        var that = Object.create(null);
        // add class section properties to it
        that.name = _name;
        that.title = _title;
        that.instructor = _instructor;
        that.location = _location;
        that.times = _times;
        that.assignments = [];
        // return the extended object
        return that;
    };
    /* Our primary namespace, bound to 'window' so we can access it from in HTML */
    window.app = {
        // Entry point.  This function is called from bottom of HTML page.
        initialize: function () {
            this.bindEvents();
        },
        /* Bind Event Listeners
         * Bind any events that are required on startup. Common events are:
         * 'load', 'deviceready', 'offline', and 'online'.
         * TODO: How to use both jQuery '*ready' events AND the phonegap ready events?
         */
        bindEvents: function () {
            document.addEventListener('deviceready', this.onDeviceReady, false);
            $("#edit-class").on("popupafterclose", this.classPopupOnCloseHandler);
            $("#edit-class-delete").on("click", this.classPopupDeleteHandler);
            $("#edit-class-save").on("click", this.classPopupSaveHandler);
        },
        /* Initialize the local storage database */
        initializeLDB: function () {
            html5sql.openDatabase("edu.radford.agenda.db", "RU-Agena-DB", 1024 * 1024 * 5);
            html5sql.process(
                [
                    "create table if not exists Classes ( cname TEXT PRIMARY KEY NOT NULL, ctitle TEXT DEFAULT '' NOT NULL, instructor TEXT, location TEXT, times TEXT );",
                    "insert or ignore into Classes ( cname ) values ( 'none' );",
                    "create table if not exists Assignments ( id INTEGER PRIMARY KEY AUTOINCREMENT, course TEXT NOT NULL REFERENCES Classes(cname) DEFAULT 'none', name TEXT NOT NULL DEFAULT 'Assignment', description TEXT DEFAULT '', dueDate INTEGER, notify INTEGER NOT NULL DEFAULT 0 CHECK(notify == 0 or notify == 1), whenNotify  INTEGER );"
                ],
                function () {
                    console.log("Initialized local database tables");
                },
                app.logSqlError
            );
        },
        /***********************************************************************
         * Event handlers
         **********************************************************************/
        /* deviceready Event Handler
         * (the phonegap api is not ready until this function is called!)
         * The scope of 'this' is the event. In order to call the 'receivedEvent'
         * function, we must explicity call 'app.receivedEvent(...);'
         */
        onDeviceReady: function () {
            app.initializeLDB();
            /* use for initial table population if you need more data
             * app.makeSampleClasses(); */
            app.classList.populateClassList();
        },
        /* handler when we tapclick the "Add new class" list item */
        addNewClassHandler: function (/* event */) {
            // hide the delete/remove button
            $("#edit-class-delete").hide();
            // enable the class title field
            $("#edit-class-id").prop("disabled", false);
            // set the 'legend' text
            $("div#edit-class h3").text("Add a new class");
            // now open the popup
            $("#edit-class").popup("open");
        },
        /* handler when we tapclick an existing item in the list of classes */
        editClassHandler: function (event) {
            var lid, cls;
            // show the delete/remove button
            $("#edit-class-delete").show();
            // disable editing the name/id of the class (it's the key)
            $("#edit-class-id").prop("disabled", true);
            // set the 'legend' text
            $("div#edit-class h3").text("Edit class details");
            // need to pre-populate the form w/ values
            lid = event.currentTarget.id;
            cls = app.classList.getClassById(lid);
            $("#edit-class-id").val(cls.name);
            $("#edit-class-title").val(cls.title);
            $("#edit-class-who").val(cls.instructor);
            $("#edit-class-where").val(cls.location);
            $("#edit-class-when").val(cls.times);
            // now open the popup
            $("#edit-class").popup("open");
        },
        /* handler for the delete button in the class detail view */
        classPopupDeleteHandler: function () {
            var cid = $("#edit-class-id").val();
            if (cid !== "") {
                app.classList.removeClass(cid);
            }
            $("#edit-class").popup("close");
        },
        /* handler for the save button in the class detail view */
        classPopupSaveHandler: function () {
            var section = makeClassSection(
                $("#edit-class-id").val(),
                $("#edit-class-title").val(),
                $("#edit-class-who").val(),
                $("#edit-class-where").val(),
                $("#edit-class-when").val()
            );
            /* rudimentary opaque validation, no empty class names */
            if (section.name !== "") {
                app.classList.saveClass(section);
            }
            $("#edit-class").popup("close");
        },
        /* clear the text fields in the class detail popup when it closes */
        classPopupOnCloseHandler: function (/*event, ui*/) {
            $("#edit-class :text").val("");
        },
        /***************************************************************************
         * Dev/Debug stuff
         **************************************************************************/
        /* generic and mostly useless SQL error printing callback */
        logSqlError: function (error, statement) {
            console.log(error);
            console.log(statement);
        },
        /* DELETE ME LATER!
         * Creates some sample class rows to query against.
         */
        makeSampleClasses: function () {
            var someClasses = [
                makeClassSection("ITEC 110", "Principles of Information Technology", "Dr. Htay", "MG 203", "TR 3:30 - 4:45 PM"),
                makeClassSection("ITEC 120", "Principles of Computer Science I", "Dr. Braffitt", "DA 225", "MWRF 11 - 11:50 AM"),
                makeClassSection("MATH 151", "Calculus I", "Cabbage", "RU 314", "MWF 1 - 1:50 PM"),
                makeClassSection("ART 111", "Art Appreciation", "Pop", "Porterfield", "MWF 9 - 9:50 AM")
            ], i = 0, scl = someClasses.length;
            for (i; i < scl; i += 1) {
                app.classList.addClass(someClasses[i]);
            }
        },
        /***************************************************************************
         * ClassList singleton object
         * 'Mega-list' object that keeps in-memory class/assignment objects
         * and has public methods for adding/deleting stuff and things.
         * Note: The private members are comma-separated; all part of the same 'var' declaration
         **************************************************************************/
        classList: (function () {
            // table of classes, so we don't have to re-query the db all the time
            var allClasses = {},
            // generate html to represent a single class in our list of classes
                getClassListItemHtml = function (cls) {
                    var piecesArr = ["
", cls.name, "
",
                        cls.title, "

", cls.instructor, "

",
                        cls.location, "

", cls.times, "

"];
                    return piecesArr.join("");
                },
                // rebuild the displayed jQuery listView in the classes tab
                updateClassListDom = function () {
                    var id, oneClass, $newBits,
                        $addBtn = $("
Add new class...
");
                    $("ul#classList").empty();
                    for (id in allClasses) {
                        if (allClasses.hasOwnProperty(id)) {
                            oneClass = allClasses[id];
                            $newBits = $(getClassListItemHtml(oneClass));
                            $("ul#classList").append($newBits);
                        }
                    }
                    $("ul#classList").append($addBtn);
                    $("#add-new-class-btn").on("click", app.addNewClassHandler);
                    $("ul#classList").on("click", "li a.class-list-item", app.editClassHandler);
                    $("ul#classList").listview("refresh");
                };
            // return public methods in an object literal
            return {
                /* Given a classSection object, insert to table, add in list,
                 * and rebuild the view if all that was successful
                 */
                saveClass: function (cls) {
                    html5sql.process(
                        [{
                            "sql": "Insert or Replace into Classes Values (?, ?, ?, ?, ?)",
                            "data": [ cls.name, cls.title, cls.instructor, cls.location, cls.times ],
                            "success": function (transaction, results) {} // don't care...
                        }],
                        function () {
                            allClasses[cls.name] = cls;
                            updateClassListDom();
                        },
                        app.logSqlError
                    );
                },
                /* Given a class id, remove that class from memory and db
                 * and redraw the class list view
                 */
                removeClass: function (clsId) {
                    html5sql.process(
                        [{
                            "sql": "Delete From Classes Where cname = ?",
                            "data": [ clsId ],
                            "success": function (transaction, results) {} // don't care...
                        }],
                        function () {
                            delete allClasses[clsId];
                            updateClassListDom();
                        },
                        app.logSqlError
                    );
                },
                /** queries the database for all class data,
                 * populates allClasses list, and rebuilds the 'view'
                 */
                populateClassList: function () {
                    // clear the allClasses table, jic
                    allClasses = {};
                    // call the local db; callbacks re-populat allClasses
                    html5sql.process(
                        [{
                            "sql": "Select * From Classes Where cname != 'none'",
                            "data": [],
                            "success": function (transaction, result) {
                                var i = 0, rl = result.rows.length, r, someClass;
                                for (i; i < rl; i += 1) {
                                    r = result.rows.item(i);
                                    someClass = makeClassSection(r.cname, r.ctitle, r.instructor, r.location, r.times);
                                    allClasses[someClass.name] = someClass;
                                }
                            }
                        }],
                        function () {
                            // if our sql executes, update the 'view'
                            updateClassListDom();
                        },
                        app.logSqlError
                    );
                },
                /* Grab an array of the class id's we are storing in the list
                 */
                getClassIds: function () {
                    var id, idArr = [];
                    for (id in allClasses) {
                        if (allClasses.hasOwnProperty(id)) {
                            idArr.push(id);
                        }
                    }
                    return idArr;
                },
                /* pull one class object from the list by its id
                 */
                getClassById: function (cid) {
                    return allClasses[cid];
                }
            };  // end public classList methods
        }())
    };
}(this));   // end everything
<commit_msg>Mostly refactors<commit_after>/*
 * TODO: migrate database functionality to 'com.phonegap.plugins.sqlite'
 * @see https://github.com/brodysoft/Cordova-SQLitePlugin
 */
(function () {
    /* yes please */
    'use strict';
    /* Structure for a single class/section/course */
    var makeCourse = function (_name, _title, _instructor, _location, _times) {
        // create a new object from the Object prototype
        var that = Object.create(null);
        // add class section properties to it
        that.name = _name;
        that.title = _title;
        that.instructor = _instructor;
        that.location = _location;
        that.times = _times;
        // return the extended object
        return that;
    };
    /* Structure for a single assginment's attributes */
    var makeAssignment = function (_title, _desc, _due, _notify, _course) {
        var that = Object.create(null);
        that.title = _title;
        that.desc = _desc;
        that.dueDate = _due;
        that.notifyDate = _notify;
        that.course = _course;
        return that;
    };
    /* Our primary namespace, bound to 'window' so we can access it from in HTML */
    window.app = {
        // Entry point.  This function is called from bottom of HTML page.
        initialize: function () {
            this.bindEvents();
        },
        /* Bind Event Listeners
         * Bind any events that are required on startup. Common events are:
         * 'load', 'deviceready', 'offline', and 'online'.
         * TODO: How to use both jQuery '*ready' events AND the phonegap ready events?
         */
        bindEvents: function () {
            document.addEventListener('deviceready', this.onDeviceReady, false);
            $("#edit-class").on("popupafterclose", this.classPopupOnCloseHandler);
            $("#edit-class-delete").on("click", this.classPopupDeleteHandler);
            $("#edit-class-save").on("click", this.classPopupSaveHandler);
        },
        /* Initialize the local storage database */
        initializeLDB: function () {
            html5sql.openDatabase("edu.radford.agenda.db", "RU-Agena-DB", 1024 * 1024 * 5);
            html5sql.process(
                [
                    "create table if not exists Classes ( cname TEXT PRIMARY KEY NOT NULL, ctitle TEXT DEFAULT '' NOT NULL, instructor TEXT, location TEXT, times TEXT );",
                    "insert or ignore into Classes ( cname ) values ( 'none' );",
                    "create table if not exists Assignments ( id INTEGER PRIMARY KEY AUTOINCREMENT, course TEXT NOT NULL REFERENCES Classes(cname) DEFAULT 'none', name TEXT NOT NULL DEFAULT 'Assignment', description TEXT DEFAULT '', dueDate INTEGER, notify INTEGER NOT NULL DEFAULT 0 CHECK(notify == 0 or notify == 1), whenNotify  INTEGER );"
                ],
                function () {
                    console.log("Initialized local database tables");
                },
                app.logSqlError
            );
        },
        /***********************************************************************
         * Event handlers
         **********************************************************************/
        /* deviceready Event Handler
         * (the phonegap api is not ready until this function is called!)
         * The scope of 'this' is the event. In order to call the 'receivedEvent'
         * function, we must explicity call 'app.receivedEvent(...);'
         */
        onDeviceReady: function () {
            app.initializeLDB();
            /* use for initial table population if you need more data
             * app.makeSampleClasses(); */
            app.courseList.readCourseList();
        },
        /* handler when we tapclick the "Add new class" list item */
        addNewClassHandler: function (/* event */) {
            // hide the delete/remove button
            $("#edit-class-delete").hide();
            // enable the class title field
            $("#edit-class-id").prop("disabled", false);
            // set the 'legend' text
            $("div#edit-class h3").text("Add a new class");
            // now open the popup
            $("#edit-class").popup("open");
        },
        /* handler when we tapclick an existing item in the list of classes */
        editClassHandler: function (event) {
            var lid, cls;
            // show the delete/remove button
            $("#edit-class-delete").show();
            // disable editing the name/id of the class (it's the key)
            $("#edit-class-id").prop("disabled", true);
            // set the 'legend' text
            $("div#edit-class h3").text("Edit class details");
            // need to pre-populate the form w/ values
            lid = event.currentTarget.id;
            cls = app.courseList.getOne(lid);
            $("#edit-class-id").val(cls.name);
            $("#edit-class-title").val(cls.title);
            $("#edit-class-who").val(cls.instructor);
            $("#edit-class-where").val(cls.location);
            $("#edit-class-when").val(cls.times);
            // now open the popup
            $("#edit-class").popup("open");
        },
        /* handler for the delete button in the class detail view */
        classPopupDeleteHandler: function () {
            var cid = $("#edit-class-id").val();
            if (cid !== "") {
                app.courseList.deleteOne(cid);
            }
            $("#edit-class").popup("close");
        },
        /* handler for the save button in the class detail view */
        classPopupSaveHandler: function () {
            var section = makeCourse(
                $("#edit-class-id").val(),
                $("#edit-class-title").val(),
                $("#edit-class-who").val(),
                $("#edit-class-where").val(),
                $("#edit-class-when").val()
            );
            /* rudimentary opaque validation, no empty class names */
            if (section.name !== "") {
                app.courseList.save(section);
            }
            $("#edit-class").popup("close");
        },
        /* clear the text fields in the class detail popup when it closes */
        classPopupOnCloseHandler: function (/*event, ui*/) {
            $("#edit-class :text").val("");
        },
        /***************************************************************************
         * Dev/Debug stuff
         **************************************************************************/
        /* generic and mostly useless SQL error printing callback */
        logSqlError: function (error, statement) {
            console.log(error);
            console.log(statement);
        },
        /* DELETE ME LATER!
         * Creates some sample class rows to query against.
         */
        makeSampleClasses: function () {
            var someClasses = [
                makeCourse("ITEC 110", "Principles of Information Technology", "Dr. Htay", "MG 203", "TR 3:30 - 4:45 PM"),
                makeCourse("ITEC 120", "Principles of Computer Science I", "Dr. Braffitt", "DA 225", "MWRF 11 - 11:50 AM"),
                makeCourse("MATH 151", "Calculus I", "Cabbage", "RU 314", "MWF 1 - 1:50 PM"),
                makeCourse("ART 111", "Art Appreciation", "Pop", "Porterfield", "MWF 9 - 9:50 AM")
            ], i = 0, scl = someClasses.length;
            for (i; i < scl; i += 1) {
                app.courseList.addClass(someClasses[i]);
            }
        },
        /***************************************************************************
         * courseList singleton object
         * 'Mega-list' object that keeps in-memory class/assignment objects
         * and has public methods for adding/deleting stuff and things.
         * Note: The private members are comma-separated; all part of the same 'var' declaration
         **************************************************************************/
        courseList: (function () {
            var allCourses = {};
            // return public methods in an object literal
            return {
                /* Given a classSection object, insert to table, add in list,
                 * and rebuild the view if all that was successful
                 */
                save: function (course) {
                    html5sql.process(
                        [{
                            "sql": "Insert or Replace into Classes Values (?, ?, ?, ?, ?)",
                            "data": [ course.name, course.title, course.instructor, course.location, course.times ],
                            "success": function (transaction, results) {} // don't care...
                        }],
                        function () {
                            allCourses[course.name] = course;
                            app.domFuncs.updateCourseListDom();
                        },
                        app.logSqlError
                    );
                },
                /* Given a class id, remove that class from memory and db
                 * and redraw the class list view
                 */
                deleteOne: function (courseId) {
                    html5sql.process(
                        [{
                            "sql": "Delete From Classes Where cname = ?",
                            "data": [ courseId ],
                            "success": function (transaction, results) {} // don't care...
                        }],
                        function () {
                            delete allCourses[courseId];
                            app.domFuncs.updateCourseListDom();
                        },
                        app.logSqlError
                    );
                },
                /* Be careful with this!
                 * This will delete every entry in the classes datatbase!
                  */
                deleteAll: function () {
                    for (var courseId in allCourses) {
                        if (allCourses.hasOwnProperty(courseId)) {
                            courseList.deleteOne(courseId);
                        }
                    }
                },
                getOne: function (courseId) {
                    // if that id is not in our list, try to fetch it
                    return allCourses[courseId];
                },
                getAll: function () {
                    var res = [];
                    for (var id in allCourses) {
                        if (allCourses.hasOwnProperty(id)) {
                            res.push(allCourses[id]);
                        }
                    }
                    return res;
                },
                /** queries the database for all class data,
                 * populates allCourses list, and rebuilds the 'view'
                 */
                readCourseList: function () {
                    // clear the allCourses table, jic
                    allCourses = {};
                    // call the local db; callbacks re-populate allCourses
                    html5sql.process(
                        [{
                            "sql": "Select * From Classes Where cname != 'none'",
                            "data": [],
                            "success": function (transaction, result) {
                                var i = 0, rl = result.rows.length, r, someClass;
                                for (i; i < rl; i += 1) {
                                    r = result.rows.item(i);
                                    someClass = makeCourse(r.cname, r.ctitle, r.instructor, r.location, r.times);
                                    allCourses[someClass.name] = someClass;
                                }
                            }
                        }],
                        function () {
                            app.domFuncs.updateCourseListDom();
                        },
                        app.logSqlError
                    );
                },
                /* Grab an array of the class id's we are storing in the list
                 */
                getClassIds: function () {
                    var id, idArr = [];
                    for (id in allCourses) {
                        if (allCourses.hasOwnProperty(id)) {
                            idArr.push(id);
                        }
                    }
                    return idArr;
                }
            };  // end public courseList methods
        }()),
        /* contains the funcs for updating dom */
        domFuncs: (function () {
            var getCourseListItemHtml = function (cls) {
                var piecesArr = ["
", cls.name, "
",
                    cls.title, "

", cls.instructor, "

",
                    cls.location, "

", cls.times, "

"];
                return piecesArr.join("");
            };
            return {
                // rebuild the displayed jQuery listView in the classes tab
                updateCourseListDom: function () {
                    var id, oneClass, $newBits,
                        allClasses = app.courseList.getAll(),
                        $addBtn = $("
Add new class...
");
                    $("ul#classList").empty();
                    for (id in allClasses) {
                        if (allClasses.hasOwnProperty(id)) {
                            oneClass = allClasses[id];
                            $newBits = $(getCourseListItemHtml(oneClass));
                            $("ul#classList").append($newBits);
                        }
                    }
                    $("ul#classList").append($addBtn);
                    $("#add-new-class-btn").on("click", app.addNewClassHandler);
                    $("ul#classList").on("click", "li a.class-list-item", app.editClassHandler);
                    $("ul#classList").listview("refresh");
                }
            };

        }())

    };
}(this));   // end everything
<|endoftext|>


<commit_before>import coreUtils from '@opentripplanner/core-utils'
import React from 'react'
import styled from 'styled-components'

import ItineraryBody from './connected-itinerary-body'
import ItinerarySummary from './itin-summary'
import NarrativeItinerary from '../narrative-itinerary'
import SimpleRealtimeAnnotation from '../simple-realtime-annotation'

const { getLegModeLabel, getTimeZoneOffset, isTransit } = coreUtils.itinerary

export const LineItineraryContainer = styled.div`
  margin-bottom: 20px;
`

export default class LineItinerary extends NarrativeItinerary {
  _headerText () {
    const { itinerary } = this.props
    return itinerary.summary || this._getSummary(itinerary)
  }

  _getSummary (itinerary) {
    let summary = ''
    let transitModes = []
    itinerary.legs.forEach((leg, index) => {
      if (isTransit(leg.mode)) {
        const modeStr = getLegModeLabel(leg)
        if (transitModes.indexOf(modeStr) === -1) transitModes.push(modeStr)
      }
    })

    // check for access mode
    if (!isTransit(itinerary.legs[0].mode)) {
      summary += getLegModeLabel(itinerary.legs[0])
    }

    // append transit modes, if applicable
    if (transitModes.length > 0) {
      summary += ' to ' + transitModes.join(', ')
    }

    return summary
  }

  render () {
    const {
      active,
      companies,
      expanded,
      itinerary,
      itineraryFooter,
      LegIcon,
      setActiveLeg,
      showRealtimeAnnotation,
      onClick,
      timeFormat
    } = this.props

    if (!itinerary) {
      return
No Itinerary!

    }

    const timeOptions = {
      format: timeFormat,
      offset: getTimeZoneOffset(itinerary)
    }

    return (
      <LineItineraryContainer className='line-itin'>
        <ItinerarySummary
          companies={companies}
          itinerary={itinerary}
          LegIcon={LegIcon}
          timeOptions={timeOptions}
          onClick={onClick}
        />
        {showRealtimeAnnotation && <SimpleRealtimeAnnotation />}
        {active || expanded
          ? <ItineraryBody
            itinerary={itinerary}
            LegIcon={LegIcon}
            // Don't use setActiveLeg as an import
            // (will cause error when clicking on itinerary suymmary).
            // Use the one passed by NarrativeItineraries instead.
            setActiveLeg={setActiveLeg}
          />
          : null}
        {itineraryFooter}
      </LineItineraryContainer>
    )
  }
}
<commit_msg>refactor(LineItinerary): Extend Trip save button to LineItinerary.<commit_after>import coreUtils from '@opentripplanner/core-utils'
import React from 'react'
import styled from 'styled-components'

import ItineraryBody from './connected-itinerary-body'
import ItinerarySummary from './itin-summary'
import NarrativeItinerary from '../narrative-itinerary'
import SimpleRealtimeAnnotation from '../simple-realtime-annotation'
import LinkButton from '../../user/link-button'

const { getLegModeLabel, getTimeZoneOffset, isTransit } = coreUtils.itinerary

export const LineItineraryContainer = styled.div`
  margin-bottom: 20px;
`

export default class LineItinerary extends NarrativeItinerary {
  _headerText () {
    const { itinerary } = this.props
    return itinerary.summary || this._getSummary(itinerary)
  }

  _getSummary (itinerary) {
    let summary = ''
    let transitModes = []
    itinerary.legs.forEach((leg, index) => {
      if (isTransit(leg.mode)) {
        const modeStr = getLegModeLabel(leg)
        if (transitModes.indexOf(modeStr) === -1) transitModes.push(modeStr)
      }
    })

    // check for access mode
    if (!isTransit(itinerary.legs[0].mode)) {
      summary += getLegModeLabel(itinerary.legs[0])
    }

    // append transit modes, if applicable
    if (transitModes.length > 0) {
      summary += ' to ' + transitModes.join(', ')
    }

    return summary
  }

  render () {
    const {
      active,
      companies,
      expanded,
      itinerary,
      itineraryFooter,
      LegIcon,
      setActiveLeg,
      showRealtimeAnnotation,
      onClick,
      timeFormat
    } = this.props

    if (!itinerary) {
      return
No Itinerary!

    }

    const timeOptions = {
      format: timeFormat,
      offset: getTimeZoneOffset(itinerary)
    }

    return (
      <LineItineraryContainer className='line-itin'>
        <ItinerarySummary
          companies={companies}
          itinerary={itinerary}
          LegIcon={LegIcon}
          timeOptions={timeOptions}
          onClick={onClick}
        />

        <LinkButton to='/savetrip'>Save this option</LinkButton>

        {showRealtimeAnnotation && <SimpleRealtimeAnnotation />}
        {active || expanded
          ? <ItineraryBody
            itinerary={itinerary}
            LegIcon={LegIcon}
            // Don't use setActiveLeg as an import
            // (will cause error when clicking on itinerary suymmary).
            // Use the one passed by NarrativeItineraries instead.
            setActiveLeg={setActiveLeg}
          />
          : null}
        {itineraryFooter}
      </LineItineraryContainer>
    )
  }
}
<|endoftext|>