<commit_before>var plugins = require('gulp-load-plugins')(scope: ['devDependencies']);

gulp.task('bower', function() {
	return gulp.src('src/scripts/**/*.js')
		.pipe(plugins.jshint('.jshintrc'))
		.pipe(plugins.jshint.reporter('default'))
		.pipe(plugins.concat('main.js'))
		.pipe(gulp.dest('dist/assets/js'))
		.pipe(plugins.rename({suffix: '.min'}))
		.pipe(plugins.uglify())
		.pipe(gulp.dest('dist/assets/js'))
		.pipe(plugins.notify({ message: 'Scripts task complete' }));
});

gulp.task('js', function() {
	return gulp.src('src/js/**/*.js')
		.pipe(plugins.jshint('.jshintrc'))
		.pipe(plugins.jshint.reporter('default'))
		.pipe(plugins.concat('main.js'))
		.pipe(gulp.dest('dist/assets/js'))
		.pipe(plugins.rename({suffix: '.min'}))
		.pipe(plugins.uglify())
		.pipe(gulp.dest('dist/assets/js'))
		.pipe(plugins.notify({ message: 'Scripts task complete' }));
});

gulp.task('styleguide-general', function() {
	return gulp.src('src/css/styleguide.scss')
		.pipe(plugins.rubySass({ style: 'expanded' }))
		.pipe(plugins.autoprefixer('last 2 version', 'safari 5', 'ie 8', 'ie 9', 'opera 12.1', 'ios 6', 'android 4'))
		.pipe(gulp.dest('assets/css/app'))
		.pipe(plugins.rename({suffix: '.min'}))
		.pipe(plugins.minifyCss())
		.pipe(gulp.dest('assets/css/app'))
		.pipe(gulp.dest('../../../public/styleguide/css'))
		.pipe(plugins.notify({ message: 'Styleguide-general task complete' }));
});

gulp.task('styleguide-specific', function() {
	return gulp.src('src/css/styleguide-specific.scss')
		.pipe(plugins.rubySass({ style: 'expanded' }))
		.pipe(plugins.autoprefixer('last 2 version', 'safari 5', 'ie 8', 'ie 9', 'opera 12.1', 'ios 6', 'android 4'))
		.pipe(gulp.dest('assets/css/user'))
		.pipe(plugins.rename({suffix: '.min'}))
		.pipe(plugins.minifyCss())
		.pipe(gulp.dest('assets/css/user'))
		.pipe(gulp.dest('../../../public/styleguide/css'))
		.pipe(plugins.notify({ message: 'Styleguide-specific task complete' }));
});

gulp.task('default', function () {
	gulp.watch(['src/bower_components/**/*'], ['bower']);
	gulp.watch(['src/js/*'], ['js']);
	gulp.watch(['src/css/styleguide.scss'], ['styleguide-general']);
	gulp.watch(['src/css/styleguide-specific.scss'], ['styleguide-specific']);
});
<commit_msg>simplifying the bower task<commit_after>var plugins = require('gulp-load-plugins')(scope: ['devDependencies']);

gulp.task("bower-files", function(){
    plugins.bowerFiles().pipe(gulp.dest("dist/bower_components"));
});

gulp.task('js', function() {
	return gulp.src('src/js/**/*.js')
		.pipe(plugins.jshint('.jshintrc'))
		.pipe(plugins.jshint.reporter('default'))
		.pipe(plugins.concat('main.js'))
		.pipe(gulp.dest('dist/assets/js'))
		.pipe(plugins.rename({suffix: '.min'}))
		.pipe(plugins.uglify())
		.pipe(gulp.dest('dist/assets/js'))
		.pipe(plugins.notify({ message: 'Scripts task complete' }));
});

gulp.task('styleguide-general', function() {
	return gulp.src('src/css/styleguide.scss')
		.pipe(plugins.rubySass({ style: 'expanded' }))
		.pipe(plugins.autoprefixer('last 2 version', 'safari 5', 'ie 8', 'ie 9', 'opera 12.1', 'ios 6', 'android 4'))
		.pipe(gulp.dest('assets/css/app'))
		.pipe(plugins.rename({suffix: '.min'}))
		.pipe(plugins.minifyCss())
		.pipe(gulp.dest('assets/css/app'))
		.pipe(gulp.dest('../../../public/styleguide/css'))
		.pipe(plugins.notify({ message: 'Styleguide-general task complete' }));
});

gulp.task('styleguide-specific', function() {
	return gulp.src('src/css/styleguide-specific.scss')
		.pipe(plugins.rubySass({ style: 'expanded' }))
		.pipe(plugins.autoprefixer('last 2 version', 'safari 5', 'ie 8', 'ie 9', 'opera 12.1', 'ios 6', 'android 4'))
		.pipe(gulp.dest('assets/css/user'))
		.pipe(plugins.rename({suffix: '.min'}))
		.pipe(plugins.minifyCss())
		.pipe(gulp.dest('assets/css/user'))
		.pipe(gulp.dest('../../../public/styleguide/css'))
		.pipe(plugins.notify({ message: 'Styleguide-specific task complete' }));
});

gulp.task('default', function () {
	gulp.watch(['src/bower_components/**/*'], ['bower']);
	gulp.watch(['src/js/*'], ['js']);
	gulp.watch(['src/css/styleguide.scss'], ['styleguide-general']);
	gulp.watch(['src/css/styleguide-specific.scss'], ['styleguide-specific']);
});
<|endoftext|>


<commit_before>var
    gulp    = require('gulp'),
    babel   = require('gulp-babel'),
    watch   = require('gulp-watch'),
    uglify   = require('gulp-uglify'),
    strip   = require('gulp-strip-comments'),
    rename  = require('gulp-rename');

// Transpile ES6 app to ES5 using babel
gulp.task('transpile', function () {
  return gulp.src('app/main.es6.js')
    .pipe(strip())                       // Strip comments
    .pipe(babel())                       // Pipe the file into babel
    .pipe(uglify())                       // Pipe the file into babel
    .pipe(rename('main.dist.js'))             // rename to main.js
    .pipe(gulp.dest('app'))              // Save it in the same directory
});

// // Watch the main.es6.js file for changes and transpile automatically
// gulp.task('watch', function () {
//   watch('app/main.es6.js', ['transpile']);
// })

gulp.task('default', ['transpile'])
<commit_msg>Add task to transpile all general es6 scripts<commit_after>var
    gulp    = require('gulp'),
    babel   = require('gulp-babel'),
    watch   = require('gulp-watch'),
    uglify  = require('gulp-uglify'),
    strip   = require('gulp-strip-comments'),
    rename  = require('gulp-rename');

// Transpile ES6 app to ES5 using babel
gulp.task('transpile', function () {
  return gulp.src('app/main.es6.js')
    .pipe(strip())                       // Strip comments
    .pipe(babel())                       // Pipe the file into babel
    .pipe(uglify())                       // Pipe the file into babel
    .pipe(rename('main.dist.js'))             // rename to main.js
    .pipe(gulp.dest('app'))              // Save it in the same directory
});

// Transpile general ES6 javascripts
gulp.task('transpile-scripts', function () {
  return gulp.src('browser/src/es6-js/**.es6.js')
    .pipe(strip())                       // Strip comments
    .pipe(babel())                       // Babel ES5 -> ES6
    .pipe(uglify())                      // uglify
    .pipe(rename(function (path) {       // Rename files so .es6.js -> .js
      path.basename = path.basename.replace('.es6', '');
    }))
    .pipe(gulp.dest('browser/build/js'))
});

gulp.task('default', ['transpile'])
<|endoftext|>


<commit_before>var assert = require('assert');
var path = require('path');
var mrt = require('./helpers');
var Meteorite = require('../../lib/meteorite');

describe('`mrt run`', function() {

  before(function(done) {
    TestServer.start(done);
  });

  beforeEach(function(done) {
    mrt.uninstall(done);
  });

  describe('invoked in a non-meteor project directory', function(done) {
    it("should display a message about not being in a meteor project dir", function(done) {
      mrt.invoke('run', 'empty-dir', {
        waitForOutput: "You're not in a Meteor project directory"
      }, done);
    })
  });

  describe('invoked in a meteor project without a smart.json', function(done) {
    it("should run the app without installing anything", function(done) {
      mrt.invoke('run', 'app-without-smart-json', {
        waitForOutput: "Running on: http://localhost:"
      }, done);
    })
  });

  describe('invoked in a meteor project with a smart.json', function(done) {
    describe('and the smart.json specifies a smart package dependency', function(done) {
      it("should install all dependencies", function(done) {
        mrt.invoke('run', 'app-with-smart-json', {
          waitForOutput: "meteorite test package installed!"
        }, done);
      })
    });
  });

});
<commit_msg>fixed spec wording<commit_after>var assert = require('assert');
var path = require('path');
var mrt = require('./helpers');
var Meteorite = require('../../lib/meteorite');

describe('`mrt run`', function() {

  before(function(done) {
    TestServer.start(done);
  });

  beforeEach(function(done) {
    mrt.uninstall(done);
  });

  describe('invoked in a non-meteor project directory', function(done) {
    it("should display a message about not being in a meteor project dir", function(done) {
      mrt.invoke('run', 'empty-dir', {
        waitForOutput: "You're not in a Meteor project directory"
      }, done);
    })
  });

  describe('invoked in a meteor project without a smart.json', function(done) {
    it("should run the app without installing anything", function(done) {
      mrt.invoke('run', 'app-without-smart-json', {
        waitForOutput: "Running on: http://localhost:"
      }, done);
    })
  });

  describe('invoked in a meteor project with a smart.json', function(done) {
    describe('and the smart.json specifies a smart package dependency', function(done) {
      it("should install the smart package", function(done) {
        mrt.invoke('run', 'app-with-smart-json', {
          waitForOutput: "meteorite test package installed!"
        }, done);
      })
    });
  });

});
<|endoftext|>


<commit_before>"use strict";

let peg = require("../../lib/peg");

describe("plugin API", function() {
  beforeEach(function() {
    this.addMatchers({
      toBeObject() {
        this.message = () =>
          "Expected " + jasmine.pp(this.actual) + " "
            + (this.isNot ? "not " : "")
            + "to be an object.";

        return this.actual !== null && typeof this.actual === "object";
      },

      toBeArray() {
        this.message = () =>
          "Expected " + jasmine.pp(this.actual) + " "
            + (this.isNot ? "not " : "")
            + "to be an array.";

        return Object.prototype.toString.apply(this.actual) === "[object Array]";
      },

      toBeFunction() {
        this.message = () =>
          "Expected " + jasmine.pp(this.actual) + " "
            + (this.isNot ? "not " : "")
            + "to be a function.";

        return typeof this.actual === "function";
      }
    });
  });

  describe("use", function() {
    let grammar = "start = 'a'";

    it("is called for each plugin", function() {
      let pluginsUsed = [false, false, false];
      let plugins = [
        { use() { pluginsUsed[0] = true; } },
        { use() { pluginsUsed[1] = true; } },
        { use() { pluginsUsed[2] = true; } }
      ];

      peg.generate(grammar, { plugins: plugins });

      expect(pluginsUsed).toEqual([true, true, true]);
    });

    it("receives configuration", function() {
      let plugin = {
        use(config) {
          expect(config).toBeObject();

          expect(config.parser).toBeObject();
          expect(config.parser.parse("start = 'a'")).toBeObject();

          expect(config.passes).toBeObject();

          expect(config.passes.check).toBeArray();
          config.passes.check.forEach(pass => {
            expect(pass).toBeFunction();
          });

          expect(config.passes.transform).toBeArray();
          config.passes.transform.forEach(pass => {
            expect(pass).toBeFunction();
          });

          expect(config.passes.generate).toBeArray();
          config.passes.generate.forEach(pass => {
            expect(pass).toBeFunction();
          });
        }
      };

      peg.generate(grammar, { plugins: [plugin] });
    });

    it("receives options", function() {
      let plugin = {
        use(config, options) {
          expect(options).toEqual(generateOptions);
        }
      };
      let generateOptions = { plugins: [plugin], foo: 42 };

      peg.generate(grammar, generateOptions);
    });

    it("can replace parser", function() {
      let plugin = {
        use(config) {
          let parser = peg.generate([
            "start = .* {",
            "  return {",
            "    type: 'grammar',",
            "    rules: [",
            "      {",
            "        type: 'rule',",
            "        name: 'start',",
            "        expression: { type: 'literal',  value: text(), ignoreCase: false }",
            "      }",
            "    ]",
            "  };",
            "}"
          ].join("\n"));

          config.parser = parser;
        }
      };
      let parser = peg.generate("a", { plugins: [plugin] });

      expect(parser.parse("a")).toBe("a");
    });

    it("can change compiler passes", function() {
      let plugin = {
        use(config) {
          let pass = ast => {
            ast.code = "({ parse: function() { return 42; } })";
          };

          config.passes.generate = [pass];
        }
      };
      let parser = peg.generate(grammar, { plugins: [plugin] });

      expect(parser.parse("a")).toBe(42);
    });

    it("can change options", function() {
      let grammar = [
        "a = 'x'",
        "b = 'x'",
        "c = 'x'"
      ].join("\n");
      let plugin = {
        use(config, options) {
          options.allowedStartRules = ["b", "c"];
        }
      };
      let parser = peg.generate(grammar, {
        allowedStartRules: ["a"],
        plugins: [plugin]
      });

      expect(() => { parser.parse("x", { startRule: "a" }); }).toThrow();
      expect(parser.parse("x", { startRule: "b" })).toBe("x");
      expect(parser.parse("x", { startRule: "c" })).toBe("x");
    });
  });
});
<commit_msg>Code style: Fix ESLint "func-style" errors<commit_after>"use strict";

let peg = require("../../lib/peg");

describe("plugin API", function() {
  beforeEach(function() {
    this.addMatchers({
      toBeObject() {
        this.message = () =>
          "Expected " + jasmine.pp(this.actual) + " "
            + (this.isNot ? "not " : "")
            + "to be an object.";

        return this.actual !== null && typeof this.actual === "object";
      },

      toBeArray() {
        this.message = () =>
          "Expected " + jasmine.pp(this.actual) + " "
            + (this.isNot ? "not " : "")
            + "to be an array.";

        return Object.prototype.toString.apply(this.actual) === "[object Array]";
      },

      toBeFunction() {
        this.message = () =>
          "Expected " + jasmine.pp(this.actual) + " "
            + (this.isNot ? "not " : "")
            + "to be a function.";

        return typeof this.actual === "function";
      }
    });
  });

  describe("use", function() {
    let grammar = "start = 'a'";

    it("is called for each plugin", function() {
      let pluginsUsed = [false, false, false];
      let plugins = [
        { use() { pluginsUsed[0] = true; } },
        { use() { pluginsUsed[1] = true; } },
        { use() { pluginsUsed[2] = true; } }
      ];

      peg.generate(grammar, { plugins: plugins });

      expect(pluginsUsed).toEqual([true, true, true]);
    });

    it("receives configuration", function() {
      let plugin = {
        use(config) {
          expect(config).toBeObject();

          expect(config.parser).toBeObject();
          expect(config.parser.parse("start = 'a'")).toBeObject();

          expect(config.passes).toBeObject();

          expect(config.passes.check).toBeArray();
          config.passes.check.forEach(pass => {
            expect(pass).toBeFunction();
          });

          expect(config.passes.transform).toBeArray();
          config.passes.transform.forEach(pass => {
            expect(pass).toBeFunction();
          });

          expect(config.passes.generate).toBeArray();
          config.passes.generate.forEach(pass => {
            expect(pass).toBeFunction();
          });
        }
      };

      peg.generate(grammar, { plugins: [plugin] });
    });

    it("receives options", function() {
      let plugin = {
        use(config, options) {
          expect(options).toEqual(generateOptions);
        }
      };
      let generateOptions = { plugins: [plugin], foo: 42 };

      peg.generate(grammar, generateOptions);
    });

    it("can replace parser", function() {
      let plugin = {
        use(config) {
          let parser = peg.generate([
            "start = .* {",
            "  return {",
            "    type: 'grammar',",
            "    rules: [",
            "      {",
            "        type: 'rule',",
            "        name: 'start',",
            "        expression: { type: 'literal',  value: text(), ignoreCase: false }",
            "      }",
            "    ]",
            "  };",
            "}"
          ].join("\n"));

          config.parser = parser;
        }
      };
      let parser = peg.generate("a", { plugins: [plugin] });

      expect(parser.parse("a")).toBe("a");
    });

    it("can change compiler passes", function() {
      let plugin = {
        use(config) {
          function pass(ast) {
            ast.code = "({ parse: function() { return 42; } })";
          }

          config.passes.generate = [pass];
        }
      };
      let parser = peg.generate(grammar, { plugins: [plugin] });

      expect(parser.parse("a")).toBe(42);
    });

    it("can change options", function() {
      let grammar = [
        "a = 'x'",
        "b = 'x'",
        "c = 'x'"
      ].join("\n");
      let plugin = {
        use(config, options) {
          options.allowedStartRules = ["b", "c"];
        }
      };
      let parser = peg.generate(grammar, {
        allowedStartRules: ["a"],
        plugins: [plugin]
      });

      expect(() => { parser.parse("x", { startRule: "a" }); }).toThrow();
      expect(parser.parse("x", { startRule: "b" })).toBe("x");
      expect(parser.parse("x", { startRule: "c" })).toBe("x");
    });
  });
});
<|endoftext|>


<commit_before>// @flow

import Icon from '@conveyal/woonerf/components/icon'
import React, {Component} from 'react'
import { LinkContainer } from 'react-router-bootstrap'
import {
  Button,
  Checkbox,
  Col,
  ControlLabel,
  Glyphicon,
  HelpBlock,
  FormControl,
  FormGroup,
  Label as BsLabel,
  Panel,
  Row,
  Table
} from 'react-bootstrap'
import Select from 'react-select'

import * as deploymentActions from '../../actions/deployments'
import * as projectsActions from '../../actions/projects'
import ActiveDeploymentViewer from '../../containers/ActiveDeploymentViewer'
import ConfirmModal from '../../../common/components/ConfirmModal'
import EditableTextField from '../../../common/components/EditableTextField'
import Loading from '../../../common/components/Loading'
import {AUTO_DEPLOY_TYPES} from '../../../common/constants'
import {getComponentMessages} from '../../../common/util/config'
import {formatTimestamp, fromNow} from '../../../common/util/date-time'
import {deploymentsComparator, getServerDeployedTo} from '../../util/deployment'

import type {Props as ContainerProps} from '../../containers/DeploymentsPanel'
import type {Deployment, Project, ReactSelectOption} from '../../../types'

type Props = ContainerProps & {
  createDeployment: typeof deploymentActions.createDeployment,
  deleteDeployment: typeof deploymentActions.deleteDeployment,
  saveDeployment: typeof deploymentActions.saveDeployment,
  updateDeployment: typeof deploymentActions.updateDeployment,
  updateProject: typeof projectsActions.updateProject
}

export default class DeploymentsPanel extends Component<Props> {
  messages = getComponentMessages('DeploymentsPanel')

  _updateProject = (updates: any) => {
    const {project, updateProject} = this.props
    updateProject(project.id, updates, true)
  }

  _onChangePinned = (option: ReactSelectOption) => {
    this._updateProject({pinnedDeploymentId: option ? option.value : null})
  }

  _onToggleAutoDeploy = (options: Array<ReactSelectOption>) => {
    this._updateProject({autoDeployTypes: options.map(o => o.value)})
  }

  _onChangeDeployWithCriticalErrors = (evt: SyntheticInputEvent<HTMLInputElement>) => {
    this._updateProject({
      autoDeployWithCriticalErrors: !this.props.project.autoDeployWithCriticalErrors
    })
  }

  _onDeleteDeployment = (deployment: Deployment) => {
    this.refs.confirmModal.open({
      title: 'Delete Deployment?',
      body: `Are you sure you want to delete the deployment ${deployment.name}?`,
      onConfirm: () => this.props.deleteDeployment(deployment)
    })
  }

  render () {
    const {
      activeSubComponent: deploymentId,
      createDeployment,
      project,
      saveDeployment,
      updateDeployment
    } = this.props
    const {deployments = [], pinnedDeploymentId} = project
    // Deployment is selected, but deployments have not finished loading.
    if (deploymentId && !deployments) return <Loading />
    // FIXME don't sort this on every render
    deployments.sort((a, b) => deploymentsComparator(a, b, pinnedDeploymentId))
    const deployment = deployments.find(d => d.id && d.id === deploymentId)
    // Deployment is selected and found in list.
    if (deployment) {
      return (
        <ActiveDeploymentViewer
          project={project}
          deployment={deployment}
          feedSources={project.feedSources} />
      )
    }
    return (
      <Row>
        <ConfirmModal ref='confirmModal' />
        <Col sm={12} md={9}>
          <DeploymentsList
            createDeployment={createDeployment}
            deleteDeployment={this._onDeleteDeployment}
            deployments={deployments}
            project={project}
            saveDeployment={saveDeployment}
            updateDeployment={updateDeployment} />
        </Col>
        <Col sm={12} md={3}>
          <Panel header={

<Icon type='rocket' /> {this.messages('autoDeploy.title')}

          }>
            <FormGroup controlId='pinnedDeploymentId'>
              <ControlLabel>
                <Icon type='thumb-tack' />{' '}
                {this.messages('pinnedDeployment.label')}
              </ControlLabel>
              <Select
                name='pinnedDeploymentId'
                onChange={this._onChangePinned}
                options={deployments.map(d => ({label: d.name, value: d.id}))}
                placeholder={this.messages('pinnedDeployment.placeholder')}
                value={project.pinnedDeploymentId}
              />
              <HelpBlock>{this.messages('pinnedDeployment.help')}</HelpBlock>
            </FormGroup>
            <FormGroup controlId='autoDeployTypes'>
              <ControlLabel>
                <Icon type='bolt' />{' '}
                {this.messages('autoDeploy.label')}
              </ControlLabel>
              <Select
                multi
                name='autoDeployTypes'
                onChange={this._onToggleAutoDeploy}
                options={Object.values(AUTO_DEPLOY_TYPES).map(t => ({
                  // Satisfy flow with typeof check. Ugh.
                  label: typeof t === 'string'
                    ? this.messages(`autoDeploy.types.${t}`)
                    : t,
                  value: t
                }))}
                placeholder={this.messages('autoDeploy.placeholder')}
                value={project.autoDeployTypes}
              />
              <HelpBlock>{this.messages('autoDeploy.help')}</HelpBlock>
            </FormGroup>
            <FormGroup controlId='autoDeployWithCriticalErrors'>
              <ControlLabel>
                <Icon type='warning' />{' '}
                {this.messages('autoDeploy.deployWithErrors.title')}
              </ControlLabel>
              <Checkbox
                checked={project.autoDeployWithCriticalErrors}
                name='autoDeployWithCriticalErrors'
                onChange={this._onChangeDeployWithCriticalErrors}
              >
                {this.messages('autoDeploy.deployWithErrors.checklabel')}
              </Checkbox>
              <HelpBlock>
                {this.messages('autoDeploy.deployWithErrors.help')}
              </HelpBlock>
            </FormGroup>
          </Panel>
          <Panel header={

<Icon type='cog' /> {this.messages('config.title')}

          }>

{this.messages('config.body')}

            <LinkContainer to={`/project/${project.id}/settings/deployment`}>
              <Button block>
                <Icon type='cog' /> {this.messages('config.editSettings')}
              </Button>
            </LinkContainer>
            <LinkContainer to={`/admin/servers`}>
              <Button block>
                <Icon type='server' /> {this.messages('config.manageServers')}
              </Button>
            </LinkContainer>
          </Panel>
        </Col>
      </Row>
    )
  }
}

type ListProps = {
  createDeployment: typeof deploymentActions.createDeployment,
  deleteDeployment: Deployment => void,
  deployments: Array<Deployment>,
  project: Project,
  saveDeployment: typeof deploymentActions.saveDeployment,
  updateDeployment: typeof deploymentActions.updateDeployment
}

type State = {searchText?: string}

class DeploymentsList extends Component<ListProps, State> {
  messages = getComponentMessages('DeploymentsList')
  state = {}

  _onChangeSearch = (evt: SyntheticInputEvent<HTMLInputElement>) =>
    this.setState({searchText: evt.target.value})

  _onClickNewDeployment = () => this.props.createDeployment(this.props.project.id)

  render () {
    const {
      deleteDeployment,
      deployments,
      project,
      saveDeployment,
      updateDeployment
    } = this.props
    const {searchText} = this.state
    // Filter deployments by search text.
    const visibleDeployments: Array<Deployment> = deployments
      .filter(({name}) =>
        name.toLowerCase().indexOf((searchText || '').toLowerCase()) !== -1
      )
    return (
      <Panel
        header={
          <Row>
            <Col xs={4}>
              <FormControl
                placeholder={this.messages('search')}
                onChange={this._onChangeSearch} />
            </Col>
            <Col xs={8}>
              <Button
                bsStyle='success'
                className='pull-right'
                onClick={this._onClickNewDeployment}>
                <Glyphicon glyph='plus' /> {this.messages('new')}
              </Button>
            </Col>
          </Row>
        }>

              {this.messages('table.name')}
              {this.messages('table.creationDate')}
              {this.messages('table.lastDeployed')}
              {this.messages('table.deployedTo')}
              {this.messages('table.feedCount')}
              {this.messages('table.testDeployment')}

            {visibleDeployments.map((deployment, index) => (
              <DeploymentListItem
                deleteDeployment={deleteDeployment}
                deployment={deployment}
                key={deployment.id || 'new-deployment-' + Math.random()}
                project={project}
                saveDeployment={saveDeployment}
                updateDeployment={updateDeployment}
              />
            ))}

      </Panel>
    )
  }
}

type RowProps = {
  deleteDeployment: Deployment => void,
  deployment: Deployment,
  project: Project,
  saveDeployment: typeof deploymentActions.saveDeployment,
  updateDeployment: typeof deploymentActions.updateDeployment
}

const NA = N/A

class DeploymentListItem extends Component<RowProps> {
  _renderTime = (time: ?number) => time
    ? {fromNow(time)}
    : NA

  _isPinned = () => {
    const {deployment, project} = this.props
    return project.pinnedDeploymentId === deployment.id
  }

  _onChangeName = (name) => {
    const {deployment, saveDeployment, project, updateDeployment} = this.props
    if (deployment.isCreating) saveDeployment({projectId: project.id, name})
    else updateDeployment(deployment, {name})
  }

  _onClickDelete = () => this.props.deleteDeployment(this.props.deployment)

  render () {
    const {deployment, project} = this.props
    const server = getServerDeployedTo(deployment, project)
    return (

          {this._isPinned() ? <Icon type='thumb-tack' /> : null}
          <EditableTextField
            inline
            isEditing={(deployment.isCreating === true)}
            value={deployment.name}
            rejectEmptyValue
            onChange={this._onChangeName}
            link={`/project/${deployment.projectId}/deployments/${deployment.id}`}
          />

        {this._renderTime(deployment.dateCreated)}
        {this._renderTime(deployment.lastDeployed)}
        {server ? <BsLabel>{server.name}</BsLabel> : NA}
        {deployment.feedVersions ? deployment.feedVersions.length : NA}
        {deployment.routerId ? 'Yes' : NA}

          <Button
            bsSize='xsmall'
            bsStyle='danger'
            className='pull-right'
            onClick={this._onClickDelete}>
            <Glyphicon glyph='remove' />
          </Button>

    )
  }
}
<commit_msg>fix(DeploymentListItem): remove label if deployment is inactive<commit_after>// @flow

import Icon from '@conveyal/woonerf/components/icon'
import React, {Component} from 'react'
import { LinkContainer } from 'react-router-bootstrap'
import {
  Button,
  Checkbox,
  Col,
  ControlLabel,
  Glyphicon,
  HelpBlock,
  FormControl,
  FormGroup,
  Label as BsLabel,
  Panel,
  Row,
  Table
} from 'react-bootstrap'
import Select from 'react-select'

import * as deploymentActions from '../../actions/deployments'
import * as projectsActions from '../../actions/projects'
import ActiveDeploymentViewer from '../../containers/ActiveDeploymentViewer'
import ConfirmModal from '../../../common/components/ConfirmModal'
import EditableTextField from '../../../common/components/EditableTextField'
import Loading from '../../../common/components/Loading'
import {AUTO_DEPLOY_TYPES} from '../../../common/constants'
import {getComponentMessages} from '../../../common/util/config'
import {formatTimestamp, fromNow} from '../../../common/util/date-time'
import {deploymentsComparator, getServerDeployedTo} from '../../util/deployment'
import type {Props as ContainerProps} from '../../containers/DeploymentsPanel'
import type {Deployment, Project, ReactSelectOption} from '../../../types'

type Props = ContainerProps & {
  createDeployment: typeof deploymentActions.createDeployment,
  deleteDeployment: typeof deploymentActions.deleteDeployment,
  saveDeployment: typeof deploymentActions.saveDeployment,
  updateDeployment: typeof deploymentActions.updateDeployment,
  updateProject: typeof projectsActions.updateProject
}

export default class DeploymentsPanel extends Component<Props> {
  messages = getComponentMessages('DeploymentsPanel')

  _updateProject = (updates: any) => {
    const {project, updateProject} = this.props
    updateProject(project.id, updates, true)
  }

  _onChangePinned = (option: ReactSelectOption) => {
    this._updateProject({pinnedDeploymentId: option ? option.value : null})
  }

  _onToggleAutoDeploy = (options: Array<ReactSelectOption>) => {
    this._updateProject({autoDeployTypes: options.map(o => o.value)})
  }

  _onChangeDeployWithCriticalErrors = (evt: SyntheticInputEvent<HTMLInputElement>) => {
    this._updateProject({
      autoDeployWithCriticalErrors: !this.props.project.autoDeployWithCriticalErrors
    })
  }

  _onDeleteDeployment = (deployment: Deployment) => {
    this.refs.confirmModal.open({
      title: 'Delete Deployment?',
      body: `Are you sure you want to delete the deployment ${deployment.name}?`,
      onConfirm: () => this.props.deleteDeployment(deployment)
    })
  }

  render () {
    const {
      activeSubComponent: deploymentId,
      createDeployment,
      project,
      saveDeployment,
      updateDeployment
    } = this.props
    const {deployments = [], pinnedDeploymentId} = project
    // Deployment is selected, but deployments have not finished loading.
    if (deploymentId && !deployments) return <Loading />
    // FIXME don't sort this on every render
    deployments.sort((a, b) => deploymentsComparator(a, b, pinnedDeploymentId))
    const deployment = deployments.find(d => d.id && d.id === deploymentId)
    // Deployment is selected and found in list.
    if (deployment) {
      return (
        <ActiveDeploymentViewer
          project={project}
          deployment={deployment}
          feedSources={project.feedSources} />
      )
    }
    return (
      <Row>
        <ConfirmModal ref='confirmModal' />
        <Col sm={12} md={9}>
          <DeploymentsList
            createDeployment={createDeployment}
            deleteDeployment={this._onDeleteDeployment}
            deployments={deployments}
            project={project}
            saveDeployment={saveDeployment}
            updateDeployment={updateDeployment} />
        </Col>
        <Col sm={12} md={3}>
          <Panel header={

<Icon type='rocket' /> {this.messages('autoDeploy.title')}

          }>
            <FormGroup controlId='pinnedDeploymentId'>
              <ControlLabel>
                <Icon type='thumb-tack' />{' '}
                {this.messages('pinnedDeployment.label')}
              </ControlLabel>
              <Select
                name='pinnedDeploymentId'
                onChange={this._onChangePinned}
                options={deployments.map(d => ({label: d.name, value: d.id}))}
                placeholder={this.messages('pinnedDeployment.placeholder')}
                value={project.pinnedDeploymentId}
              />
              <HelpBlock>{this.messages('pinnedDeployment.help')}</HelpBlock>
            </FormGroup>
            <FormGroup controlId='autoDeployTypes'>
              <ControlLabel>
                <Icon type='bolt' />{' '}
                {this.messages('autoDeploy.label')}
              </ControlLabel>
              <Select
                multi
                name='autoDeployTypes'
                onChange={this._onToggleAutoDeploy}
                options={Object.values(AUTO_DEPLOY_TYPES).map(t => ({
                  // Satisfy flow with typeof check. Ugh.
                  label: typeof t === 'string'
                    ? this.messages(`autoDeploy.types.${t}`)
                    : t,
                  value: t
                }))}
                placeholder={this.messages('autoDeploy.placeholder')}
                value={project.autoDeployTypes}
              />
              <HelpBlock>{this.messages('autoDeploy.help')}</HelpBlock>
            </FormGroup>
            <FormGroup controlId='autoDeployWithCriticalErrors'>
              <ControlLabel>
                <Icon type='warning' />{' '}
                {this.messages('autoDeploy.deployWithErrors.title')}
              </ControlLabel>
              <Checkbox
                checked={project.autoDeployWithCriticalErrors}
                name='autoDeployWithCriticalErrors'
                onChange={this._onChangeDeployWithCriticalErrors}
              >
                {this.messages('autoDeploy.deployWithErrors.checklabel')}
              </Checkbox>
              <HelpBlock>
                {this.messages('autoDeploy.deployWithErrors.help')}
              </HelpBlock>
            </FormGroup>
          </Panel>
          <Panel header={

<Icon type='cog' /> {this.messages('config.title')}

          }>

{this.messages('config.body')}

            <LinkContainer to={`/project/${project.id}/settings/deployment`}>
              <Button block>
                <Icon type='cog' /> {this.messages('config.editSettings')}
              </Button>
            </LinkContainer>
            <LinkContainer to={`/admin/servers`}>
              <Button block>
                <Icon type='server' /> {this.messages('config.manageServers')}
              </Button>
            </LinkContainer>
          </Panel>
        </Col>
      </Row>
    )
  }
}

type ListProps = {
  createDeployment: typeof deploymentActions.createDeployment,
  deleteDeployment: Deployment => void,
  deployments: Array<Deployment>,
  project: Project,
  saveDeployment: typeof deploymentActions.saveDeployment,
  updateDeployment: typeof deploymentActions.updateDeployment
}

type State = {searchText?: string}

class DeploymentsList extends Component<ListProps, State> {
  messages = getComponentMessages('DeploymentsList')
  state = {}

  _onChangeSearch = (evt: SyntheticInputEvent<HTMLInputElement>) =>
    this.setState({searchText: evt.target.value})

  _onClickNewDeployment = () => this.props.createDeployment(this.props.project.id)

  render () {
    const {
      deleteDeployment,
      deployments,
      project,
      saveDeployment,
      updateDeployment
    } = this.props
    const {searchText} = this.state
    // Filter deployments by search text.
    const visibleDeployments: Array<Deployment> = deployments
      .filter(({name}) =>
        name.toLowerCase().indexOf((searchText || '').toLowerCase()) !== -1
      )
    return (
      <Panel
        header={
          <Row>
            <Col xs={4}>
              <FormControl
                placeholder={this.messages('search')}
                onChange={this._onChangeSearch} />
            </Col>
            <Col xs={8}>
              <Button
                bsStyle='success'
                className='pull-right'
                onClick={this._onClickNewDeployment}>
                <Glyphicon glyph='plus' /> {this.messages('new')}
              </Button>
            </Col>
          </Row>
        }>

              {this.messages('table.name')}
              {this.messages('table.creationDate')}
              {this.messages('table.lastDeployed')}
              {this.messages('table.deployedTo')}
              {this.messages('table.feedCount')}
              {this.messages('table.testDeployment')}

            {visibleDeployments.map((deployment, index) => (
              <DeploymentListItem
                deleteDeployment={deleteDeployment}
                deployment={deployment}
                key={deployment.id || 'new-deployment-' + Math.random()}
                project={project}
                saveDeployment={saveDeployment}
                updateDeployment={updateDeployment}
              />
            ))}

      </Panel>
    )
  }
}

type RowProps = {
  deleteDeployment: Deployment => void,
  deployment: Deployment,
  project: Project,
  saveDeployment: typeof deploymentActions.saveDeployment,
  updateDeployment: typeof deploymentActions.updateDeployment
}

const NA = N/A

class DeploymentListItem extends Component<RowProps> {
  _renderTime = (time: ?number) => time
    ? {fromNow(time)}
    : NA

  _isPinned = () => {
    const {deployment, project} = this.props
    return project.pinnedDeploymentId === deployment.id
  }

  _onChangeName = (name) => {
    const {deployment, saveDeployment, project, updateDeployment} = this.props
    if (deployment.isCreating) saveDeployment({projectId: project.id, name})
    else updateDeployment(deployment, {name})
  }

  _onClickDelete = () => this.props.deleteDeployment(this.props.deployment)

  render () {
    const {deployment, project} = this.props
    const server = getServerDeployedTo(deployment, project)
    return (

          {this._isPinned() ? <Icon type='thumb-tack' /> : null}
          <EditableTextField
            inline
            isEditing={(deployment.isCreating === true)}
            value={deployment.name}
            rejectEmptyValue
            onChange={this._onChangeName}
            link={`/project/${deployment.projectId}/deployments/${deployment.id}`}
          />

        {this._renderTime(deployment.dateCreated)}
        {this._renderTime(deployment.lastDeployed)}
        {server || (deployment.ec2Instances && deployment.ec2Instances.length > 0) ? <BsLabel>{server.name}</BsLabel> : NA}
        {deployment.feedVersions ? deployment.feedVersions.length : NA}
        {deployment.routerId ? 'Yes' : NA}

          <Button
            bsSize='xsmall'
            bsStyle='danger'
            className='pull-right'
            onClick={this._onClickDelete}>
            <Glyphicon glyph='remove' />
          </Button>

    )
  }
}
<|endoftext|>


<commit_before>/**
 * Tips
 * ====
 * - Set `user-agent` and `accept` headers when sending requests. Some services will not respond as expected without them.
 * - Set `pool` to false if you send lots of requests using "request" library.
 */

function fetch(feedUrl, callback) {

    const request = require('request'),
        FeedParser = require('feedparser')

    // define our streams
    let req = request(feedUrl, {
        timeout: 10000,
        pool: false,
        maxRedirects: 25,
        jar: true,
        gzip: true
    })

    req.setMaxListeners(50)
    req.setHeader('user-agent', 'Winds RSS reader')
    req.setHeader('accept', 'text/html,application/xhtml+xml')

    var feedparser = new FeedParser({
        addmeta: false,
        normalize: true,
        feedurl: feedUrl
    })

    // define our handlers
    req.on('error', callback);
    req.on('response', function(res) {
        if (res.statusCode != 200) return this.emit('error', new Error('Bad status code'));
        let encoding = res.headers['content-encoding'] || 'identity',
            charset = getParams(res.headers['content-type'] || '').charset
        sails.log.info(`Feed content encoding ${encoding}, charset ${charset}`)
        res = maybeDecompress(res, encoding)
        res = maybeTranslate(res, charset)
        // And boom goes the dynamite
        res.pipe(feedparser)
    });

    let feedParseError
    feedparser.on('error', function(err, response) {
        feedParseError = err
    });

    let items = []
    feedparser.on('readable', function() {
        while (item = this.read()) {
            items.push(item)
        }
    });

    feedparser.on('end', function() {
        callback(feedParseError, feedparser.meta, items);
    });

}

function maybeTranslate(res, charset) {
    const iconv = require('iconv-lite')

    // use iconv if its not utf8 already.
    if (charset && !/utf-*8/i.test(charset)) {
        try {
            res.pipe(
                iconv.decodeStream(charset)
            ).pipe(
                iconv.encodeStream('utf-8')
            )
        } catch (err) {
            res.emit('error', err);
        }
    }

    return res;

}

exports.fetch = fetch;

function maybeDecompress(res, encoding) {
    const zlib = require('zlib')
    var decompress;
    if (encoding.match(/\bdeflate\b/)) {
        decompress = zlib.createInflate();
    } else if (encoding.match(/\bgzip\b/)) {
        decompress = zlib.createGunzip();
    }
    return decompress ? res.pipe(decompress) : res;
}

function getParams(str) {

    let params = str.split(';').reduce(function(params, param) {

        var parts = param.split('=').map(function(part) {
            return part.trim();
        });

        if (parts.length === 2) {
            params[parts[0]] = parts[1];
        }

        return params;

    }, {});

    return params;

}
<commit_msg>fixed issue with parse calling its callback twice<commit_after>/**
 * Tips
 * ====
 * - Set `user-agent` and `accept` headers when sending requests. Some services will not respond as expected without them.
 * - Set `pool` to false if you send lots of requests using "request" library.
 */

function fetch(feedUrl, callback) {

    const request = require('request'),
        FeedParser = require('feedparser')

    // define our streams
    let req = request(feedUrl, {
        timeout: 10000,
        pool: false,
        maxRedirects: 25,
        jar: true,
        gzip: true
    })

    req.setMaxListeners(50)
    req.setHeader('user-agent', 'Winds RSS reader')
    req.setHeader('accept', 'text/html,application/xhtml+xml')

    var feedparser = new FeedParser({
        addmeta: false,
        normalize: true,
        feedurl: feedUrl
    })

    // define our handlers
    let feedRequestError
    req.on('error', function(err, response) {
        feedRequestError = err
    })
    req.on('response', function(res) {
        if (res.statusCode != 200) return this.emit('error', new Error('Bad status code'));
        let encoding = res.headers['content-encoding'] || 'identity',
            charset = getParams(res.headers['content-type'] || '').charset
        sails.log.info(`Feed content encoding ${encoding}, charset ${charset}`)
        res = maybeDecompress(res, encoding)
        res = maybeTranslate(res, charset)
        // And boom goes the dynamite
        res.pipe(feedparser)
    })

    let feedParseError
    feedparser.on('error', function(err, response) {
        feedParseError = err
    });

    let items = []
    feedparser.on('readable', function() {
        while (item = this.read()) {
            items.push(item)
        }
    });

    feedparser.on('end', function() {
        callback(feedRequestError || feedParseError, feedparser.meta, items);
    });

}

function maybeTranslate(res, charset) {
    const iconv = require('iconv-lite')

    // use iconv if its not utf8 already.
    if (charset && !/utf-*8/i.test(charset)) {
        try {
            res.pipe(
                iconv.decodeStream(charset)
            ).pipe(
                iconv.encodeStream('utf-8')
            )
        } catch (err) {
            res.emit('error', err);
        }
    }

    return res;

}

exports.fetch = fetch;

function maybeDecompress(res, encoding) {
    const zlib = require('zlib')
    var decompress;
    if (encoding.match(/\bdeflate\b/)) {
        decompress = zlib.createInflate();
    } else if (encoding.match(/\bgzip\b/)) {
        decompress = zlib.createGunzip();
    }
    return decompress ? res.pipe(decompress) : res;
}

function getParams(str) {

    let params = str.split(';').reduce(function(params, param) {

        var parts = param.split('=').map(function(part) {
            return part.trim();
        });

        if (parts.length === 2) {
            params[parts[0]] = parts[1];
        }

        return params;

    }, {});

    return params;

}
<|endoftext|>


<commit_before>import React from 'react'
import {MarkdownPreview} from 'react-marked-markdown';
import Modal from 'app/components/modal'
import ImageIcon from 'app/components/imageicon'
import {to_list} from 'app/utils'
import rpc from 'app/rpc'
import Flash from 'app/flash'
import i18n from 'app/utils/i18n'
import event from 'app/utils/event'
import cache from 'app/utils/cache'
import plugin from 'app/utils/plugin'
import {colorize, capitalize} from 'app/utils'
import HoldButton from 'app/components/holdbutton'
import IconIcon from 'app/components/iconicon'

const icon = require("../../../../imgs/plugins.svg")

function ComponentDetails({component: c}){
  return (

{i18n(c.name || c.id)}

{i18n("Type:")} {c.type}

      {c.description ? (

{i18n("Description:")} <MarkdownPreview value={i18n(c.description)}/>

      ) : [] }

  )
}

const left_pane_style={
  paddingLeft: 20,
  borderRight: "1px solid #ddd",
  height: "calc( 100vh - 60px )",
  background: "white",
  zIndex: 1
}

class PluginDetails extends React.Component{
  constructor(props){
    super(props)

    this.state = this.getInitialState(props)
    this.checkUpdates = this.checkUpdates.bind(this)
  }
  getInitialState(props){
    return {
      enabled: props.plugin.enabled,
      updated: props.updated,
      plugin: props.plugin,
    }
  }
  componentDidMount(){
    let self=this
    $(this.refs.enabled).checkbox({
      onChange(ev){
        const enabled = $(self.refs.enabled).is('.checked')
        self.handleSetEnable(enabled)
        self.setState({enabled})
      }
    })
    event.on("plugins.reloaded", this.checkUpdates)
    this.checkUpdates()
  }
  componentWillUnmount(){
    event.off("plugins.reloaded", this.checkUpdates)
  }
  checkUpdates(){
    const pid = this.props.plugin.id
    const maybe_update_promise = this.state.plugin.tags.indexOf("core")<0 ?
      plugin.call("serverboards.core.update/marketplace", "check_updates", [pid]) :
      Promise.resolve([{updated: true}])

    Promise.all([
      maybe_update_promise,
      cache.plugin(pid)
    ]).then( ([update, plugin]) => {
      if (update.length == 0){
        Flash.error("Error getting current update status.")
        this.setState( this.getInitialState({plugin, updated: false}))
        return;
      }
      if (plugin.enabled)
        $(this.refs.enabled).checkbox("set checked")
      else
        $(this.refs.enabled).checkbox("set unchecked")
      this.setState( this.getInitialState({plugin, updated: update[0].updated}))
    })
  }
  handleSetEnable(enabled){
    rpc.call("action.trigger", ["serverboards.core.update/enable_plugin",  {"plugin_id": this.props.plugin.id, enabled}])
    this.setState({enabled})
  }
  handleUpdate(){
    rpc.call("action.trigger", ["serverboards.core.update/update_plugin",  {"plugin_id": this.props.plugin.id}]).then( () => {
      Flash.info("Plugin updated.")
      // this.props.updateAll()
      this.setState({updated: undefined})
      this.checkUpdates()
    }).catch( (e) => {
      console.log(e)
      Flash.error("Error updating plugin: "+e)
    })
  }
  handleRemove(){
    rpc.call("action.trigger", ["serverboards.core.update/remove_plugin",  {"plugin_id": this.props.plugin.id}]).then( () => {
      Flash.info("Plugin removed.")
      // this.props.updateAll()
      this.props.onClose()
    }).catch( (e) => {
      console.log(e)
      Flash.error("Error removing plugin: "+e)
    })
  }
  render(){
    const {plugin} = this.state
    let author=plugin.author
    if (author && author.indexOf('<')>0){
      let m = author.match(/(.*)<(.*)>/)
      if (m){
        author=(
          <a href={`mailto:${m[2]}`}>{m[1]}
        )
      }
    }
    const tags = plugin.tags
    console.log("tags", tags)

    return (
      <Modal className="wide">

            {plugin.extra.icon ? (
              <IconIcon icon={plugin.extra.icon} plugin={plugin.id} style={{minWidth: 48, minHeight: 48}}/>
            ) : (
              <ImageIcon src={icon} name={i18n(plugin.name)} className="mini"/>
            )}

{i18n(plugin.name)}

{i18n("by")} {author}

            {tags.indexOf("core")<0 && (
              <React.Fragment>
                {this.state.updated == undefined ? (

                    <button className="ui teal basic disabled button" onClick={this.handleUpdate.bind(this)}>
                      {i18n("Checking updates")}
                    </button>

                ) : this.state.updated == false ? (

                    <button className="ui yellow button" onClick={this.handleUpdate.bind(this)}>{i18n("Update now")}</button>

                ) : (

                    <button
                      className="ui teal button"
                      onClick={this.handleUpdate.bind(this)}
                      data-tooltip={i18n("Altough no update has been detected for this plugin, you can force update.")}
                      data-position="bottom right"
                      >
                        {i18n("Force update")}
                    </button>

                ) }
                <HoldButton
                    className="ui basic red button" style={{marginTop: 4}}
                    onHoldClick={this.handleRemove.bind(this)}>
                  {i18n("Hold to remove")}
                </HoldButton>
              </React.Fragment>
            )}
            {tags.indexOf("optional")>=0 && (

                  <input type="checkbox" checked={this.state.enabled}/>

            )}

                {i18n("Version")}

{plugin.version}

              {plugin.url ? (

                  {i18n("URL")}

{plugin.url}

              ) : []}

{i18n("Components")}

              {to_list(plugin.components).map( (nc) => (
                <ComponentDetails key={nc[1].id} component={nc[1]}/>
              ))}

{i18n("Description")}

                <MarkdownPreview value={i18n(plugin.description)}/>

            {plugin.require_update ? (

{i18n("Update changelog")}

                  {plugin.require_update.split('\n').map( (l) => (

{l}

                  ))}

            ) : null }

      </Modal>
    )
  }
}

export default PluginDetails
<commit_msg>Improved resielience upon plugin update check<commit_after>import React from 'react'
import {MarkdownPreview} from 'react-marked-markdown';
import Modal from 'app/components/modal'
import ImageIcon from 'app/components/imageicon'
import {to_list} from 'app/utils'
import rpc from 'app/rpc'
import Flash from 'app/flash'
import i18n from 'app/utils/i18n'
import event from 'app/utils/event'
import cache from 'app/utils/cache'
import plugin from 'app/utils/plugin'
import {colorize, capitalize} from 'app/utils'
import HoldButton from 'app/components/holdbutton'
import IconIcon from 'app/components/iconicon'

const icon = require("../../../../imgs/plugins.svg")

function ComponentDetails({component: c}){
  return (

{i18n(c.name || c.id)}

{i18n("Type:")} {c.type}

      {c.description ? (

{i18n("Description:")} <MarkdownPreview value={i18n(c.description)}/>

      ) : [] }

  )
}

const left_pane_style={
  paddingLeft: 20,
  borderRight: "1px solid #ddd",
  height: "calc( 100vh - 60px )",
  background: "white",
  zIndex: 1
}

class PluginDetails extends React.Component{
  constructor(props){
    super(props)

    this.state = this.getInitialState(props)
    this.checkUpdates = this.checkUpdates.bind(this)
  }
  getInitialState(props){
    return {
      enabled: props.plugin.enabled,
      updated: props.updated,
      plugin: props.plugin,
    }
  }
  componentDidMount(){
    let self=this
    $(this.refs.enabled).checkbox({
      onChange(ev){
        const enabled = $(self.refs.enabled).is('.checked')
        self.handleSetEnable(enabled)
        self.setState({enabled})
      }
    })
    event.on("plugins.reloaded", this.checkUpdates)
    this.checkUpdates()
  }
  componentWillUnmount(){
    event.off("plugins.reloaded", this.checkUpdates)
  }
  checkUpdates(){
    const pid = this.props.plugin.id
    const maybe_update_promise = this.state.plugin.tags.indexOf("core")<0 ?
      plugin.call("serverboards.core.update/marketplace", "check_updates", [pid]) :
      Promise.resolve([{updated: true}])

    Promise.all([
      maybe_update_promise,
      cache.plugin(pid)
    ]).then( ([update, plugin]) => {
      if (update.length == 0){
        Flash.error("Error getting current update status.")
        this.setState( this.getInitialState({plugin, updated: false}))
        return;
      }
      if (plugin.enabled)
        $(this.refs.enabled).checkbox("set checked")
      else
        $(this.refs.enabled).checkbox("set unchecked")
      this.setState( this.getInitialState({plugin, updated: update[0].updated}))
    }).catch(e => {
      Flash.error(e)
      this.setState({updated: "unknown"})
    })
  }
  handleSetEnable(enabled){
    rpc.call("action.trigger", ["serverboards.core.update/enable_plugin",  {"plugin_id": this.props.plugin.id, enabled}])
    this.setState({enabled})
  }
  handleUpdate(){
    rpc.call("action.trigger", ["serverboards.core.update/update_plugin",  {"plugin_id": this.props.plugin.id}]).then( () => {
      Flash.info("Plugin updated.")
      // this.props.updateAll()
      this.setState({updated: undefined})
      this.checkUpdates()
    }).catch( (e) => {
      console.log(e)
      Flash.error("Error updating plugin: "+e)
    })
  }
  handleRemove(){
    rpc.call("action.trigger", ["serverboards.core.update/remove_plugin",  {"plugin_id": this.props.plugin.id}]).then( () => {
      Flash.info("Plugin removed.")
      // this.props.updateAll()
      this.props.onClose()
    }).catch( (e) => {
      console.log(e)
      Flash.error("Error removing plugin: "+e)
    })
  }
  render(){
    const {plugin} = this.state
    let author=plugin.author
    if (author && author.indexOf('<')>0){
      let m = author.match(/(.*)<(.*)>/)
      if (m){
        author=(
          <a href={`mailto:${m[2]}`}>{m[1]}
        )
      }
    }
    const tags = plugin.tags
    console.log("tags", tags)

    return (
      <Modal className="wide">

            {plugin.extra.icon ? (
              <IconIcon icon={plugin.extra.icon} plugin={plugin.id} style={{minWidth: 48, minHeight: 48}}/>
            ) : (
              <ImageIcon src={icon} name={i18n(plugin.name)} className="mini"/>
            )}

{i18n(plugin.name)}

{i18n("by")} {author}

            {tags.indexOf("core")<0 && (
              <React.Fragment>
                {this.state.updated == undefined ? (

                    <button className="ui teal basic disabled button" onClick={this.handleUpdate.bind(this)}>
                      {i18n("Checking updates")}
                    </button>

                ) : this.state.updated == false ? (

                    <button className="ui yellow button" onClick={this.handleUpdate.bind(this)}>{i18n("Update now")}</button>

                ) : this.state.updated == "unknown" ? (

                    <button
                      className="ui teal button"
                      onClick={this.handleUpdate.bind(this)}
                      data-tooltip={i18n("Could not check update status. You can force update anyway.")}
                      data-position="bottom right"
                      >
                        {i18n("Force update")}
                    </button>

                ) : (

                    <button
                      className="ui teal button"
                      onClick={this.handleUpdate.bind(this)}
                      data-tooltip={i18n("Altough no update has been detected for this plugin, you can force update.")}
                      data-position="bottom right"
                      >
                        {i18n("Force update")}
                    </button>

                ) }
                <HoldButton
                    className="ui basic red button" style={{marginTop: 4}}
                    onHoldClick={this.handleRemove.bind(this)}>
                  {i18n("Hold to remove")}
                </HoldButton>
              </React.Fragment>
            )}
            {tags.indexOf("optional")>=0 && (

                  <input type="checkbox" checked={this.state.enabled}/>

            )}

                {i18n("Version")}

{plugin.version}

              {plugin.url ? (

                  {i18n("URL")}

{plugin.url}

              ) : []}

{i18n("Components")}

              {to_list(plugin.components).map( (nc) => (
                <ComponentDetails key={nc[1].id} component={nc[1]}/>
              ))}

{i18n("Description")}

                <MarkdownPreview value={i18n(plugin.description)}/>

            {plugin.require_update ? (

{i18n("Update changelog")}

                  {plugin.require_update.split('\n').map( (l) => (

{l}

                  ))}

            ) : null }

      </Modal>
    )
  }
}

export default PluginDetails
<|endoftext|>


<commit_before>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

define(["jquery", "util", "session", "elementFinder", "eventMaker"], function ($, util, session, elementFinder, eventMaker) {
  var forms = util.Module("forms");
  var assert = util.assert;

  var inRemoteUpdate = false;

  function maybeChange(event) {
    // Called when we get an event that may or may not indicate a real change
    // (like keyup in a textarea)
    var tag = event.target.tagName;
    if (tag == "TEXTAREA" || tag == "INPUT") {
      change(event);
    }
  }

  function change(event) {
    if (inRemoteUpdate) {
      return;
    }
    if (elementFinder.ignoreElement(event.target)) {
      return;
    }
    var el = $(event.target);
    var location = elementFinder.elementLocation(el);
    var value = getValue(el);
    var msg = {
      type: "form-update",
      element: location
    };
    if (isText(el)) {
      var prev = el.data("towtruckPrevValue");
      if (prev) {
        msg.replace = makeDiff(el.data("towtruckPrevValue"), value);
      } else {
        if (typeof prev != "string") {
          console.warn("No previous known value on field", el[0]);
        }
        msg.value = value;
      }
    } else {
      msg.value = value;
    }
    el.data("towtruckPrevValue", value);
    session.send(msg);
  }

  function isCheckable(el) {
    el = $(el);
    var type = (el.prop("type") || "text").toLowerCase();
    if (el.prop("tagName") == "INPUT" && ["radio", "checkbox"].indexOf(type) != -1) {
      return true;
    }
    return false;
  }

  var editTrackers = {};
  var liveTrackers = [];

  var AceEditor = util.Class({

    trackerName: "AceEditor",

    constructor: function (el) {
      this.element = $(el);
      assert(this.element.hasClass("ace_editor"));
      this._change = this._change.bind(this);
      this._editor().document.on("change", this._change);
    },

    destroy: function (el) {
      this._editor().document.removeListener("change", this._change);
    },

    update: function (msg) {
      this._editor().document.getDocument().applyDeltas([msg.delta]);
    },

    init: function (update, msg) {
      this._editor().document.setValue(update.value);
    },

    makeInit: function () {
      return {
        element: elementFinder.elementLocation(this.element),
        tracker: this.trackerName,
        value: this._editor().document.getValue()
      };
    },

    _editor: function () {
      return this.element[0].env;
    },

    _change: function (e) {
      // FIXME: I should have an internal .send() function that automatically
      // asserts !inRemoteUpdate, among other things
      if (inRemoteUpdate) {
        return;
      }
      // FIXME: I want to use a more normalized version of replace instead of
      // ACE's native delta
      session.send({
        type: "form-update",
        tracker: this.trackerName,
        element: elementFinder.elementLocation(this.element),
        delta: JSON.parse(JSON.stringify(e.data))
      });
    }
  });

  AceEditor.scan = function () {
    return $(".ace_editor");
  };

  AceEditor.tracked = function (el) {
    return !! $(el).closest(".ace_editor").length;
  };

  editTrackers[AceEditor.prototype.trackerName] = AceEditor;

  var CodeMirrorEditor = util.Class({
    trackerName: "CodeMirrorEditor",

    constructor: function (el) {
      this.element = $(el);
      assert(this.element[0].CodeMirror);
      this._change = this._change.bind(this);
      this._editor().on("change", this._change);
    },

    destroy: function (el) {
      this._editor().removeListener("change", this._change);
    },

    update: function (msg) {
      this._editor().replaceRange(
        msg.change.text,
        msg.change.from,
        msg.change.to);
    },

    init: function (update, msg) {
      this._editor().setValue(update.value);
    },

    makeInit: function () {
      return {
        element: elementFinder.elementLocation(this.element),
        tracker: this.trackerName,
        value: this._editor().getValue()
      };
    },

    _change: function (editor, change) {
      if (inRemoteUpdate) {
        return;
      }
      delete change.origin;
      session.send({
        type: "form-update",
        tracker: this.trackerName,
        element: elementFinder.elementLocation(this.element),
        change: change
      });
    },

    _editor: function () {
      return this.element[0].CodeMirror;
    }
  });

  CodeMirrorEditor.scan = function () {
    var result = [];
    var els = document.body.getElementsByTagName("*");
    var _len = els.length;
    for (var i=0; i<_len; i++) {
      var el = els[i];
      if (el.CodeMirror) {
        result.push(el);
      }
    }
    return $(result);
  };

  CodeMirrorEditor.tracked = function (el) {
    el = $(el)[0];
    while (el) {
      if (el.CodeMirror) {
        return true;
      }
      el = el.parentNode;
    }
    return false;
  };

  editTrackers[CodeMirrorEditor.prototype.trackerName] = CodeMirrorEditor;

  function buildTrackers() {
    assert(! liveTrackers.length);
    util.forEachAttr(editTrackers, function (TrackerClass) {
      var els = TrackerClass.scan();
      els.each(function () {
        liveTrackers.push(TrackerClass(this));
      });
    });
  }

  function destroyTrackers() {
    liveTrackers.forEach(function (tracker) {
      tracker.destroy();
    });
    liveTrackers = [];
  }

  function elementTracked(el) {
    var result = false;
    util.forEachAttr(editTrackers, function (TrackerClass) {
      if (TrackerClass.tracked(el)) {
        result = true;
      }
    });
    return result;
  }

  function getTracker(el, name) {
    el = $(el)[0];
    for (var i=0; i<liveTrackers.length; i++) {
      var tracker = liveTrackers[i];
      if (tracker.element[0] == el) {
        assert((! name) || name == tracker.trackerName, "Expected to map to a tracker type", name, "but got", tracker.trackerName);
        return tracker;
      }
    }
    return null;
  }

  var TEXT_TYPES = (
    "color date datetime datetime-local email " +
        "tel time week").split(/ /g);

  function isText(el) {
    el = $(el);
    var tag = el.prop("tagName");
    var type = (el.prop("type") || "text").toLowerCase();
    if (tag == "TEXTAREA") {
      return true;
    }
    if (tag == "INPUT" && TEXT_TYPES.indexOf(type) != -1) {
      return true;
    }
    return false;
  }

  function getValue(el) {
    el = $(el);
    if (isCheckable(el)) {
      return el.prop("checked");
    } else {
      return el.val();
    }
  }

  function setValue(el, value) {
    el = $(el);
    if (isCheckable(el)) {
      el.prop("checked", value);
    } else {
      el.val(value);
    }
    el.data("towtruckPrevValue", value);
    eventMaker.fireChange(el);
  }

  session.hub.on("form-update", function (msg) {
    if (! msg.sameUrl) {
      return;
    }
    var el = $(elementFinder.findElement(msg.element));
    if (msg.tracker) {
      var tracker = getTracker(el, msg.tracker);
      assert(tracker);
      inRemoteUpdate = true;
      try {
        tracker.update(msg);
      } finally {
        inRemoteUpdate = false;
      }
      return;
    }
    var text = isText(el);
    var selection;
    if (text) {
      selection = [el[0].selectionStart, el[0].selectionEnd];
    }
    var value;
    if (msg.replace) {
      var cur = getValue(el);
      value = applyDiff(msg.replace, cur);
    } else {
      value = msg.value;
    }
    inRemoteUpdate = true;
    try {
      setValue(el, value);
      if (text && typeof selection[0] == "number" && typeof selection[1] == "number" && msg.replace) {
        if (selection[0] > msg.replace.start) {
          if (selection[0] < msg.replace.start + msg.replace.len) {
            // selection start inside replacement
            selection[0] = msg.replace.start;
          } else {
            // selection start after replacement
            selection[0] += msg.replace.text.length - msg.replace.len;
          }
        } // otherwise selection start is before replacement (no change necessary)
        if (selection[1] > msg.replace.start) {
          if (selection[1] < msg.replace.start + msg.replace.len) {
            // end selection inside replacement
            if (selection[0] <= msg.replace.start) {
              // Since the start is before the selection, select the entire replacement
              selection[1] = msg.replace.start + msg.replace.len;
            } else {
              // Otherwise select nothing
              selection[1] = msg.replace.start;
            }
          } else {
            // end selection after replacement
            selection[1] += msg.replace.text.length - msg.replace.len;
          }
        } // otherwise selection end is before replacement
        el[0].selectionStart = selection[0];
        el[0].selectionEnd = selection[1];
      }
    } finally {
      inRemoteUpdate = false;
    }
  });

  var initSent = false;

  function sendInit() {
    initSent = true;
    var msg = {
      type: "form-init",
      pageAge: Date.now() - TowTruck.pageLoaded,
      updates: []
    };
    var els = $("textarea, input, select");
    els.each(function () {
      if (elementFinder.ignoreElement(this) || elementTracked(this)) {
        return;
      }
      var el = $(this);
      var value = getValue(el);
      el.data("towtruckPrevValue", value);
      msg.updates.push({
        element: elementFinder.elementLocation(this),
        value: value
      });
    });
    liveTrackers.forEach(function (tracker) {
      var init = tracker.makeInit();
      assert(elementFinder.findElement(init.element) == tracker.element[0]);
      msg.updates.push(init);
    });
    if (msg.updates.length) {
      session.send(msg);
    }
  }

  function setInit() {
    var els = $("textarea, input, select");
    els.each(function () {
      if (elementTracked(this)) {
        return;
      }
      if (elementFinder.ignoreElement(this)) {
        return;
      }
      var el = $(this);
      var value = getValue(el);
      el.data("towtruckPrevValue", value);
    });
    destroyTrackers();
    buildTrackers();
  }

  session.on("reinitialize", setInit);

  session.on("ui-ready", setInit);

  function makeDiff(oldValue, newValue) {
    assert(typeof oldValue == "string");
    assert(typeof newValue == "string");
    var commonStart = 0;
    while (commonStart < newValue.length &&
           newValue.charAt(commonStart) == oldValue.charAt(commonStart)) {
      commonStart++;
    }
    var commonEnd = 0;
    while (commonEnd < (newValue.length - commonStart) &&
           newValue.charAt(newValue.length - commonEnd - 1) ==
             oldValue.charAt(oldValue.length - commonEnd - 1)) {
      commonEnd++;
    }
    var removed = oldValue.substr(commonStart, oldValue.length - commonStart - commonEnd);
    var inserted = newValue.substr(commonStart, newValue.length - commonStart - commonEnd);
    return {
      start: commonStart,
      // Not using .length because it makes it seem like an array or string:
      len: removed.length,
      text: inserted
    };
  }

  function applyDiff(diff, value) {
    var start = value.substr(0, diff.start);
    var end = value.substr(diff.start + diff.len);
    return start + diff.text + end;
  }

  session.hub.on("form-init", function (msg) {
    if (! msg.sameUrl) {
      return;
    }
    if (initSent) {
      // In a 3+-peer situation more than one client may init; in this case
      // we're probably the other peer, and not the peer that needs the init
      // A quick check to see if we should init...
      var myAge = Date.now() - TowTruck.pageLoaded;
      if (msg.pageAge < myAge) {
        // We've been around longer than the other person...
        return;
      }
    }
    // FIXME: need to figure out when to ignore inits
    msg.updates.forEach(function (update) {
      var el = elementFinder.findElement(update.element);
      if (update.tracker) {
        var tracker = getTracker(el, update.tracker);
        assert(tracker);
        inRemoteUpdate = true;
        try {
          tracker.init(update, msg);
        } finally {
          inRemoteUpdate = false;
        }
      } else {
        inRemoteUpdate = true;
        try {
          setValue(el, update.value);
        } finally {
          inRemoteUpdate = false;
        }
      }
    });
  });

  session.on("ui-ready", function () {
    $(document).on("change", change);
    $(document).on("textInput keyup cut paste", maybeChange);
  });

  session.on("close", function () {
    $(document).off("change", change);
    $(document).off("textInput keyup cut paste", maybeChange);
  });

  session.hub.on("hello", function (msg) {
    if (msg.sameUrl) {
      sendInit();
    }
  });

  return forms;
});
<commit_msg>ignore form elements inside rich editors (e.g., codemirror uses a textarea to capture key inputs)<commit_after>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

define(["jquery", "util", "session", "elementFinder", "eventMaker"], function ($, util, session, elementFinder, eventMaker) {
  var forms = util.Module("forms");
  var assert = util.assert;

  var inRemoteUpdate = false;

  function maybeChange(event) {
    // Called when we get an event that may or may not indicate a real change
    // (like keyup in a textarea)
    var tag = event.target.tagName;
    if (tag == "TEXTAREA" || tag == "INPUT") {
      change(event);
    }
  }

  function change(event) {
    if (inRemoteUpdate) {
      return;
    }
    if (elementFinder.ignoreElement(event.target) || elementTracked(event.target)) {
      return;
    }
    var el = $(event.target);
    var location = elementFinder.elementLocation(el);
    var value = getValue(el);
    var msg = {
      type: "form-update",
      element: location
    };
    if (isText(el)) {
      var prev = el.data("towtruckPrevValue");
      if (prev) {
        msg.replace = makeDiff(el.data("towtruckPrevValue"), value);
      } else {
        if (typeof prev != "string") {
          console.warn("No previous known value on field", el[0]);
        }
        msg.value = value;
      }
    } else {
      msg.value = value;
    }
    el.data("towtruckPrevValue", value);
    session.send(msg);
  }

  function isCheckable(el) {
    el = $(el);
    var type = (el.prop("type") || "text").toLowerCase();
    if (el.prop("tagName") == "INPUT" && ["radio", "checkbox"].indexOf(type) != -1) {
      return true;
    }
    return false;
  }

  var editTrackers = {};
  var liveTrackers = [];

  var AceEditor = util.Class({

    trackerName: "AceEditor",

    constructor: function (el) {
      this.element = $(el);
      assert(this.element.hasClass("ace_editor"));
      this._change = this._change.bind(this);
      this._editor().document.on("change", this._change);
    },

    destroy: function (el) {
      this._editor().document.removeListener("change", this._change);
    },

    update: function (msg) {
      this._editor().document.getDocument().applyDeltas([msg.delta]);
    },

    init: function (update, msg) {
      this._editor().document.setValue(update.value);
    },

    makeInit: function () {
      return {
        element: elementFinder.elementLocation(this.element),
        tracker: this.trackerName,
        value: this._editor().document.getValue()
      };
    },

    _editor: function () {
      return this.element[0].env;
    },

    _change: function (e) {
      // FIXME: I should have an internal .send() function that automatically
      // asserts !inRemoteUpdate, among other things
      if (inRemoteUpdate) {
        return;
      }
      // FIXME: I want to use a more normalized version of replace instead of
      // ACE's native delta
      session.send({
        type: "form-update",
        tracker: this.trackerName,
        element: elementFinder.elementLocation(this.element),
        delta: JSON.parse(JSON.stringify(e.data))
      });
    }
  });

  AceEditor.scan = function () {
    return $(".ace_editor");
  };

  AceEditor.tracked = function (el) {
    return !! $(el).closest(".ace_editor").length;
  };

  editTrackers[AceEditor.prototype.trackerName] = AceEditor;

  var CodeMirrorEditor = util.Class({
    trackerName: "CodeMirrorEditor",

    constructor: function (el) {
      this.element = $(el);
      assert(this.element[0].CodeMirror);
      this._change = this._change.bind(this);
      this._editor().on("change", this._change);
    },

    destroy: function (el) {
      this._editor().removeListener("change", this._change);
    },

    update: function (msg) {
      this._editor().replaceRange(
        msg.change.text,
        msg.change.from,
        msg.change.to);
    },

    init: function (update, msg) {
      this._editor().setValue(update.value);
    },

    makeInit: function () {
      return {
        element: elementFinder.elementLocation(this.element),
        tracker: this.trackerName,
        value: this._editor().getValue()
      };
    },

    _change: function (editor, change) {
      if (inRemoteUpdate) {
        return;
      }
      delete change.origin;
      session.send({
        type: "form-update",
        tracker: this.trackerName,
        element: elementFinder.elementLocation(this.element),
        change: change
      });
    },

    _editor: function () {
      return this.element[0].CodeMirror;
    }
  });

  CodeMirrorEditor.scan = function () {
    var result = [];
    var els = document.body.getElementsByTagName("*");
    var _len = els.length;
    for (var i=0; i<_len; i++) {
      var el = els[i];
      if (el.CodeMirror) {
        result.push(el);
      }
    }
    return $(result);
  };

  CodeMirrorEditor.tracked = function (el) {
    el = $(el)[0];
    while (el) {
      if (el.CodeMirror) {
        return true;
      }
      el = el.parentNode;
    }
    return false;
  };

  editTrackers[CodeMirrorEditor.prototype.trackerName] = CodeMirrorEditor;

  function buildTrackers() {
    assert(! liveTrackers.length);
    util.forEachAttr(editTrackers, function (TrackerClass) {
      var els = TrackerClass.scan();
      els.each(function () {
        liveTrackers.push(TrackerClass(this));
      });
    });
  }

  function destroyTrackers() {
    liveTrackers.forEach(function (tracker) {
      tracker.destroy();
    });
    liveTrackers = [];
  }

  function elementTracked(el) {
    var result = false;
    util.forEachAttr(editTrackers, function (TrackerClass) {
      if (TrackerClass.tracked(el)) {
        result = true;
      }
    });
    return result;
  }

  function getTracker(el, name) {
    el = $(el)[0];
    for (var i=0; i<liveTrackers.length; i++) {
      var tracker = liveTrackers[i];
      if (tracker.element[0] == el) {
        assert((! name) || name == tracker.trackerName, "Expected to map to a tracker type", name, "but got", tracker.trackerName);
        return tracker;
      }
    }
    return null;
  }

  var TEXT_TYPES = (
    "color date datetime datetime-local email " +
        "tel time week").split(/ /g);

  function isText(el) {
    el = $(el);
    var tag = el.prop("tagName");
    var type = (el.prop("type") || "text").toLowerCase();
    if (tag == "TEXTAREA") {
      return true;
    }
    if (tag == "INPUT" && TEXT_TYPES.indexOf(type) != -1) {
      return true;
    }
    return false;
  }

  function getValue(el) {
    el = $(el);
    if (isCheckable(el)) {
      return el.prop("checked");
    } else {
      return el.val();
    }
  }

  function setValue(el, value) {
    el = $(el);
    if (isCheckable(el)) {
      el.prop("checked", value);
    } else {
      el.val(value);
    }
    el.data("towtruckPrevValue", value);
    eventMaker.fireChange(el);
  }

  session.hub.on("form-update", function (msg) {
    if (! msg.sameUrl) {
      return;
    }
    var el = $(elementFinder.findElement(msg.element));
    if (msg.tracker) {
      var tracker = getTracker(el, msg.tracker);
      assert(tracker);
      inRemoteUpdate = true;
      try {
        tracker.update(msg);
      } finally {
        inRemoteUpdate = false;
      }
      return;
    }
    var text = isText(el);
    var selection;
    if (text) {
      selection = [el[0].selectionStart, el[0].selectionEnd];
    }
    var value;
    if (msg.replace) {
      var cur = getValue(el);
      value = applyDiff(msg.replace, cur);
    } else {
      value = msg.value;
    }
    inRemoteUpdate = true;
    try {
      setValue(el, value);
      if (text && typeof selection[0] == "number" && typeof selection[1] == "number" && msg.replace) {
        if (selection[0] > msg.replace.start) {
          if (selection[0] < msg.replace.start + msg.replace.len) {
            // selection start inside replacement
            selection[0] = msg.replace.start;
          } else {
            // selection start after replacement
            selection[0] += msg.replace.text.length - msg.replace.len;
          }
        } // otherwise selection start is before replacement (no change necessary)
        if (selection[1] > msg.replace.start) {
          if (selection[1] < msg.replace.start + msg.replace.len) {
            // end selection inside replacement
            if (selection[0] <= msg.replace.start) {
              // Since the start is before the selection, select the entire replacement
              selection[1] = msg.replace.start + msg.replace.len;
            } else {
              // Otherwise select nothing
              selection[1] = msg.replace.start;
            }
          } else {
            // end selection after replacement
            selection[1] += msg.replace.text.length - msg.replace.len;
          }
        } // otherwise selection end is before replacement
        el[0].selectionStart = selection[0];
        el[0].selectionEnd = selection[1];
      }
    } finally {
      inRemoteUpdate = false;
    }
  });

  var initSent = false;

  function sendInit() {
    initSent = true;
    var msg = {
      type: "form-init",
      pageAge: Date.now() - TowTruck.pageLoaded,
      updates: []
    };
    var els = $("textarea, input, select");
    els.each(function () {
      if (elementFinder.ignoreElement(this) || elementTracked(this)) {
        return;
      }
      var el = $(this);
      var value = getValue(el);
      el.data("towtruckPrevValue", value);
      msg.updates.push({
        element: elementFinder.elementLocation(this),
        value: value
      });
    });
    liveTrackers.forEach(function (tracker) {
      var init = tracker.makeInit();
      assert(elementFinder.findElement(init.element) == tracker.element[0]);
      msg.updates.push(init);
    });
    if (msg.updates.length) {
      session.send(msg);
    }
  }

  function setInit() {
    var els = $("textarea, input, select");
    els.each(function () {
      if (elementTracked(this)) {
        return;
      }
      if (elementFinder.ignoreElement(this)) {
        return;
      }
      var el = $(this);
      var value = getValue(el);
      el.data("towtruckPrevValue", value);
    });
    destroyTrackers();
    buildTrackers();
  }

  session.on("reinitialize", setInit);

  session.on("ui-ready", setInit);

  function makeDiff(oldValue, newValue) {
    assert(typeof oldValue == "string");
    assert(typeof newValue == "string");
    var commonStart = 0;
    while (commonStart < newValue.length &&
           newValue.charAt(commonStart) == oldValue.charAt(commonStart)) {
      commonStart++;
    }
    var commonEnd = 0;
    while (commonEnd < (newValue.length - commonStart) &&
           newValue.charAt(newValue.length - commonEnd - 1) ==
             oldValue.charAt(oldValue.length - commonEnd - 1)) {
      commonEnd++;
    }
    var removed = oldValue.substr(commonStart, oldValue.length - commonStart - commonEnd);
    var inserted = newValue.substr(commonStart, newValue.length - commonStart - commonEnd);
    return {
      start: commonStart,
      // Not using .length because it makes it seem like an array or string:
      len: removed.length,
      text: inserted
    };
  }

  function applyDiff(diff, value) {
    var start = value.substr(0, diff.start);
    var end = value.substr(diff.start + diff.len);
    return start + diff.text + end;
  }

  session.hub.on("form-init", function (msg) {
    if (! msg.sameUrl) {
      return;
    }
    if (initSent) {
      // In a 3+-peer situation more than one client may init; in this case
      // we're probably the other peer, and not the peer that needs the init
      // A quick check to see if we should init...
      var myAge = Date.now() - TowTruck.pageLoaded;
      if (msg.pageAge < myAge) {
        // We've been around longer than the other person...
        return;
      }
    }
    // FIXME: need to figure out when to ignore inits
    msg.updates.forEach(function (update) {
      var el = elementFinder.findElement(update.element);
      if (update.tracker) {
        var tracker = getTracker(el, update.tracker);
        assert(tracker);
        inRemoteUpdate = true;
        try {
          tracker.init(update, msg);
        } finally {
          inRemoteUpdate = false;
        }
      } else {
        inRemoteUpdate = true;
        try {
          setValue(el, update.value);
        } finally {
          inRemoteUpdate = false;
        }
      }
    });
  });

  session.on("ui-ready", function () {
    $(document).on("change", change);
    $(document).on("textInput keyup cut paste", maybeChange);
  });

  session.on("close", function () {
    $(document).off("change", change);
    $(document).off("textInput keyup cut paste", maybeChange);
  });

  session.hub.on("hello", function (msg) {
    if (msg.sameUrl) {
      sendInit();
    }
  });

  return forms;
});
<|endoftext|>