<commit_before>/* exported ContentGroup */

// Copyright 2016 Endless Mobile, Inc.

const Gdk = imports.gi.Gdk;
const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const GObject = imports.gi.GObject;
const Gtk = imports.gi.Gtk;

const Actions = imports.app.actions;
const Dispatcher = imports.app.dispatcher;
const HistoryStore = imports.app.historyStore;
const Module = imports.app.interfaces.module;
const Overflow = imports.app.modules.arrangement.overflow;
const Utils = imports.app.utils;

const BATCH_SIZE = 10;

const CONTENT_PAGE_NAME = 'content';
const NO_RESULTS_PAGE_NAME = 'no-results';
const ERROR_PAGE_NAME = 'error';

const ContentGroup = new Module.Class({
    Name: 'ContentGroup.ContentGroup',
    Extends: Gtk.Grid,

    Slots: {
        'arrangement': {},
        'selection': {},
        'title': {},
        'trigger': {},
        'no-results': {},  // optional
    },

    Properties: {
        /**
         * Property: has-more-content
         * Whether this content group has more content to show
         */
        'has-more-content': GObject.ParamSpec.boolean('has-more-content',
            'Has more content', 'Has more content',
            GObject.ParamFlags.READABLE),
    },

    _init: function (props={}) {
        this.parent(props);

        this._title = this.create_submodule('title');
        if (this._title) {
            // You can't have a trigger without a title
            this._trigger = this.create_submodule('trigger');
            if (this._trigger) {
                // Title is clickable if and only if trigger exists
                let [title_button, trigger_button] = [this._title, this._trigger].map((module) => {
                    let button = new Gtk.Button({
                        halign: module.halign,
                    });

                    button.add(module);
                    Utils.set_hand_cursor_on_widget(button);
                    button.connect('clicked', () => this._selection.show_more());
                    return button;
                });

                // Style class needs to go on the button itself, so that we get :hover states and the like.
                title_button.get_style_context().add_class(Utils.get_element_style_class(ContentGroup, 'title'));
                trigger_button.get_style_context().add_class(Utils.get_element_style_class(ContentGroup, 'trigger'));
                this.attach(title_button, 0, 0, 1, 1);
                this.attach(trigger_button, 1, 0, 1, 1);
            } else {
                this._title.get_style_context().add_class(Utils.get_element_style_class(ContentGroup, 'title'));
                this.attach(this._title, 0, 0, 1, 1);
            }
        }

        this._arrangement = this.create_submodule('arrangement');
        this._arrangement.connect('card-clicked', (arrangement, model) => {
            Dispatcher.get_default().dispatch({
                action_type: Actions.ITEM_CLICKED,
                model: model,
                context: this._selection.get_models(),
            });
        });

        this._no_results = this.create_submodule('no-results');

        // FIXME: use a composite template for this. Currently not possible
        // because Card.ContentGroup must inherit from this class.
        // https://bugzilla.gnome.org/show_bug.cgi?id=768790
        let builder = Gtk.Builder.new_from_resource('/com/endlessm/knowledge/data/widgets/contentGroup/contentGroup.ui');
        this._stack = builder.get_object('stack');
        this._log_button = builder.get_object('log-button');

        // FIXME: extend the stack clip to cover its children clip.
        // https://bugzilla.gnome.org/show_bug.cgi?id=771436
        this._stack.connect('size-allocate', (stack, stack_alloc) => {
            let child = stack.get_visible_child();
            if (!child)
                return;

            let child_clip = child.get_clip();
            // translate to the stack's allocation coordinates space
            child_clip.x += stack_alloc.x;
            child_clip.y += stack_alloc.y;

            let stack_clip = stack.get_clip();
            stack_clip = stack_clip.union(child_clip);
            stack.set_clip(stack_clip);
        });

        let spinner = new Gtk.Spinner({
            visible: false,
            no_show_all: true,
            vexpand: true,
        });
        this._stack.add_named(this._arrangement, CONTENT_PAGE_NAME);

        if (this._no_results)
            this._stack.add_named(this._no_results, NO_RESULTS_PAGE_NAME);

        // Make sure we begin on the content page.
        this._stack.visible_child_name = CONTENT_PAGE_NAME;
        this._selection = this.create_submodule('selection', {
            model: this.model || null,
        });
        this._selection.connect('models-changed',
            this._on_models_changed.bind(this));
        this._selection.connect('notify::loading', () => {
            // When the spinner is not being shown on screen, set it to
            // be inactive to help with performance.
            spinner.active = spinner.visible = this._selection.loading;
            if (this._selection.error)
                this._stack.visible_child_name = ERROR_PAGE_NAME;
            else
                this._stack.visible_child_name = CONTENT_PAGE_NAME;
        });

        this._ensure_synced();
        this._selection.connect('notify::needs-refresh', this._ensure_synced.bind(this));

        this.attach(this._stack, 0, 1, 2, 1);
        this.attach(spinner, 0, 2, 2, 1);

        [[this._selection, 'can-load-more'], [this._arrangement, 'all-visible']].forEach((arr) => {
            let obj = arr[0];
            let property = arr[1];
            obj.connect('notify::' + property, () => {
                this.notify('has-more-content');
            });
        });
        this._selection.connect('notify::in-error-state',
            this._on_selection_error.bind(this));
        this._log_button.connect('clicked',
            this._on_log_button_click.bind(this));
        HistoryStore.get_default().connect('changed',
            this._on_history_changed.bind(this));
    },

    _ensure_synced: function () {
        if (this._selection.needs_refresh) {
            this.load();
        }
    },

    get has_more_content () {
        return (!this._arrangement.all_visible || this._selection.can_load_more);
    },

    make_ready: function (cb=function () {}) {
        [this._title, this._trigger, this._no_results].forEach((module) => {
            if (module)
                module.make_ready();
        });
        if (!this._selection.loading) {
            cb();
        } else {
            let id = this._selection.connect('notify::loading', () => {
                if (!this._selection.loading) {
                    cb();
                    this._selection.disconnect(id); // only invoke callback once
                }
            });
        }
    },

    get_selection: function () {
        return this._selection;
    },

    _on_models_changed: function () {
        let models = this._selection.get_models();
        this.visible = true;
        if (models.length === 0) {
            this._arrangement.set_models([]);
            if (this._no_results) {
                this._stack.visible_child_name = NO_RESULTS_PAGE_NAME;
            } else {
                this.visible = false;
            }
        } else {
            let max_cards = this._arrangement.get_max_cards();
            if (max_cards > -1)
                models.splice(max_cards);
            this._arrangement.set_models(models);
            let item = HistoryStore.get_default().get_current_item();
            if (item && item.model) {
                this._arrangement.highlight(item.model);
            }
            this._stack.visible_child_name = CONTENT_PAGE_NAME;
        }

    },

    _on_history_changed: function () {
        let item = HistoryStore.get_default().get_current_item();
        if (item.model)
            this._arrangement.highlight(item.model);
        this._arrangement.highlight_string(item.query);
    },

    _on_selection_error: function () {
        if (!this._selection.in_error_state)
            return;
        this._stack.visible_child_name = ERROR_PAGE_NAME;
    },

    _on_log_button_click: function () {
        let timestamp = new Date(Date.now());
        let stamp = 'eos-knowledge-lib log ' + timestamp.toISOString() +
            ' XXXXXX.txt';
        let [log_file, stream] = Gio.File.new_tmp(stamp);
        let exception = this._selection.get_error();
        let app = Gio.Application.get_default();
        let log = [
            'Log file for Xapian failure',
            '===========================',
            'Endless customer support: Please report to Apps team on Phabricator',
            'App ID: ' + (app ? app.application_id : 'unknown'),
            'Module path: ' + this.factory_path,
            'Timestamp: ' + timestamp.toISOString(),
            'Error message: ' + (exception ? exception.message : 'none'),
            '',
        ].join('\n');
        log += exception ? exception.stack : '';
        let os = new Gio.DataOutputStream({
            base_stream: stream.output_stream,
        });
        os.put_string(log, null);
        Gtk.show_uri(Gdk.Screen.get_default(), log_file.get_uri(),
            Gdk.CURRENT_TIME);
    },

    load: function () {
        this._selection.clear();
        let cards_to_load = BATCH_SIZE;
        let max_cards = this._arrangement.get_max_cards();
        if (max_cards > -1)
            cards_to_load = max_cards;
        this._selection.queue_load_more(cards_to_load);
    },
});
<commit_msg>Add comment saying that title and trigger are optional slots<commit_after>/* exported ContentGroup */

// Copyright 2016 Endless Mobile, Inc.

const Gdk = imports.gi.Gdk;
const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const GObject = imports.gi.GObject;
const Gtk = imports.gi.Gtk;

const Actions = imports.app.actions;
const Dispatcher = imports.app.dispatcher;
const HistoryStore = imports.app.historyStore;
const Module = imports.app.interfaces.module;
const Overflow = imports.app.modules.arrangement.overflow;
const Utils = imports.app.utils;

const BATCH_SIZE = 10;

const CONTENT_PAGE_NAME = 'content';
const NO_RESULTS_PAGE_NAME = 'no-results';
const ERROR_PAGE_NAME = 'error';

const ContentGroup = new Module.Class({
    Name: 'ContentGroup.ContentGroup',
    Extends: Gtk.Grid,

    Slots: {
        'arrangement': {},
        'selection': {},
        'title': {}, // optional
        'trigger': {}, // optional
        'no-results': {},  // optional
    },

    Properties: {
        /**
         * Property: has-more-content
         * Whether this content group has more content to show
         */
        'has-more-content': GObject.ParamSpec.boolean('has-more-content',
            'Has more content', 'Has more content',
            GObject.ParamFlags.READABLE),
    },

    _init: function (props={}) {
        this.parent(props);

        this._title = this.create_submodule('title');
        if (this._title) {
            // You can't have a trigger without a title
            this._trigger = this.create_submodule('trigger');
            if (this._trigger) {
                // Title is clickable if and only if trigger exists
                let [title_button, trigger_button] = [this._title, this._trigger].map((module) => {
                    let button = new Gtk.Button({
                        halign: module.halign,
                    });

                    button.add(module);
                    Utils.set_hand_cursor_on_widget(button);
                    button.connect('clicked', () => this._selection.show_more());
                    return button;
                });

                // Style class needs to go on the button itself, so that we get :hover states and the like.
                title_button.get_style_context().add_class(Utils.get_element_style_class(ContentGroup, 'title'));
                trigger_button.get_style_context().add_class(Utils.get_element_style_class(ContentGroup, 'trigger'));
                this.attach(title_button, 0, 0, 1, 1);
                this.attach(trigger_button, 1, 0, 1, 1);
            } else {
                this._title.get_style_context().add_class(Utils.get_element_style_class(ContentGroup, 'title'));
                this.attach(this._title, 0, 0, 1, 1);
            }
        }

        this._arrangement = this.create_submodule('arrangement');
        this._arrangement.connect('card-clicked', (arrangement, model) => {
            Dispatcher.get_default().dispatch({
                action_type: Actions.ITEM_CLICKED,
                model: model,
                context: this._selection.get_models(),
            });
        });

        this._no_results = this.create_submodule('no-results');

        // FIXME: use a composite template for this. Currently not possible
        // because Card.ContentGroup must inherit from this class.
        // https://bugzilla.gnome.org/show_bug.cgi?id=768790
        let builder = Gtk.Builder.new_from_resource('/com/endlessm/knowledge/data/widgets/contentGroup/contentGroup.ui');
        this._stack = builder.get_object('stack');
        this._log_button = builder.get_object('log-button');

        // FIXME: extend the stack clip to cover its children clip.
        // https://bugzilla.gnome.org/show_bug.cgi?id=771436
        this._stack.connect('size-allocate', (stack, stack_alloc) => {
            let child = stack.get_visible_child();
            if (!child)
                return;

            let child_clip = child.get_clip();
            // translate to the stack's allocation coordinates space
            child_clip.x += stack_alloc.x;
            child_clip.y += stack_alloc.y;

            let stack_clip = stack.get_clip();
            stack_clip = stack_clip.union(child_clip);
            stack.set_clip(stack_clip);
        });

        let spinner = new Gtk.Spinner({
            visible: false,
            no_show_all: true,
            vexpand: true,
        });
        this._stack.add_named(this._arrangement, CONTENT_PAGE_NAME);

        if (this._no_results)
            this._stack.add_named(this._no_results, NO_RESULTS_PAGE_NAME);

        // Make sure we begin on the content page.
        this._stack.visible_child_name = CONTENT_PAGE_NAME;
        this._selection = this.create_submodule('selection', {
            model: this.model || null,
        });
        this._selection.connect('models-changed',
            this._on_models_changed.bind(this));
        this._selection.connect('notify::loading', () => {
            // When the spinner is not being shown on screen, set it to
            // be inactive to help with performance.
            spinner.active = spinner.visible = this._selection.loading;
            if (this._selection.error)
                this._stack.visible_child_name = ERROR_PAGE_NAME;
            else
                this._stack.visible_child_name = CONTENT_PAGE_NAME;
        });

        this._ensure_synced();
        this._selection.connect('notify::needs-refresh', this._ensure_synced.bind(this));

        this.attach(this._stack, 0, 1, 2, 1);
        this.attach(spinner, 0, 2, 2, 1);

        [[this._selection, 'can-load-more'], [this._arrangement, 'all-visible']].forEach((arr) => {
            let obj = arr[0];
            let property = arr[1];
            obj.connect('notify::' + property, () => {
                this.notify('has-more-content');
            });
        });
        this._selection.connect('notify::in-error-state',
            this._on_selection_error.bind(this));
        this._log_button.connect('clicked',
            this._on_log_button_click.bind(this));
        HistoryStore.get_default().connect('changed',
            this._on_history_changed.bind(this));
    },

    _ensure_synced: function () {
        if (this._selection.needs_refresh) {
            this.load();
        }
    },

    get has_more_content () {
        return (!this._arrangement.all_visible || this._selection.can_load_more);
    },

    make_ready: function (cb=function () {}) {
        [this._title, this._trigger, this._no_results].forEach((module) => {
            if (module)
                module.make_ready();
        });
        if (!this._selection.loading) {
            cb();
        } else {
            let id = this._selection.connect('notify::loading', () => {
                if (!this._selection.loading) {
                    cb();
                    this._selection.disconnect(id); // only invoke callback once
                }
            });
        }
    },

    get_selection: function () {
        return this._selection;
    },

    _on_models_changed: function () {
        let models = this._selection.get_models();
        this.visible = true;
        if (models.length === 0) {
            this._arrangement.set_models([]);
            if (this._no_results) {
                this._stack.visible_child_name = NO_RESULTS_PAGE_NAME;
            } else {
                this.visible = false;
            }
        } else {
            let max_cards = this._arrangement.get_max_cards();
            if (max_cards > -1)
                models.splice(max_cards);
            this._arrangement.set_models(models);
            let item = HistoryStore.get_default().get_current_item();
            if (item && item.model) {
                this._arrangement.highlight(item.model);
            }
            this._stack.visible_child_name = CONTENT_PAGE_NAME;
        }

    },

    _on_history_changed: function () {
        let item = HistoryStore.get_default().get_current_item();
        if (item.model)
            this._arrangement.highlight(item.model);
        this._arrangement.highlight_string(item.query);
    },

    _on_selection_error: function () {
        if (!this._selection.in_error_state)
            return;
        this._stack.visible_child_name = ERROR_PAGE_NAME;
    },

    _on_log_button_click: function () {
        let timestamp = new Date(Date.now());
        let stamp = 'eos-knowledge-lib log ' + timestamp.toISOString() +
            ' XXXXXX.txt';
        let [log_file, stream] = Gio.File.new_tmp(stamp);
        let exception = this._selection.get_error();
        let app = Gio.Application.get_default();
        let log = [
            'Log file for Xapian failure',
            '===========================',
            'Endless customer support: Please report to Apps team on Phabricator',
            'App ID: ' + (app ? app.application_id : 'unknown'),
            'Module path: ' + this.factory_path,
            'Timestamp: ' + timestamp.toISOString(),
            'Error message: ' + (exception ? exception.message : 'none'),
            '',
        ].join('\n');
        log += exception ? exception.stack : '';
        let os = new Gio.DataOutputStream({
            base_stream: stream.output_stream,
        });
        os.put_string(log, null);
        Gtk.show_uri(Gdk.Screen.get_default(), log_file.get_uri(),
            Gdk.CURRENT_TIME);
    },

    load: function () {
        this._selection.clear();
        let cards_to_load = BATCH_SIZE;
        let max_cards = this._arrangement.get_max_cards();
        if (max_cards > -1)
            cards_to_load = max_cards;
        this._selection.queue_load_more(cards_to_load);
    },
});
<|endoftext|>


<commit_before>
function airline_init_pre() {
  prop.airline = {};
  prop.airline.airlines = {};
}

function airline_init() {
  // Civil Airlines
  airline_load("UAL");
  airline_load("BAW");
  airline_load("AAL");
  airline_load("AWE");
  airline_load("CESSNA");
  airline_load("ACA");

  airline_load("KLM");
  airline_load("KLC");
  airline_load("DAL");
  airline_load("LIGHTGA");
  airline_load("FASTGA");
  airline_load("SAS");
  airline_load("NAX");

  // South American
  airline_load("ONE");
  airline_load("GLO");
  airline_load("TAM");
  airline_load("AVA");
  airline_load("AZU");
  airline_load("EMBRAER");

  // Russian
  airline_load("AFL");
  airline_load("MOV");
  airline_load("RLU");
  airline_load("SBI");
  airline_load("SVR");
  airline_load("TYA");

  // Cargo Airlines
  airline_load("FDX");
  airline_load("UPS");
  airline_load("CWC");

  // Military Air Forces
  airline_load("RFF");
  airline_load("FAB");
  airline_load("USAF");

    // Internationals to South America
  airline_load("SA-ACA");
  airline_load("SA-AAL");
  airline_load("SA-AEA");
  airline_load("SA-AFR");
  airline_load("SA-AMX");
  airline_load("SA-AVA");
  airline_load("SA-AZA");
  airline_load("SA-BAW");
  airline_load("SA-CCA");
  airline_load("SA-DLH");
  airline_load("SA-IBE");
  airline_load("SA-KAL");
  airline_load("SA-KLM");
  airline_load("SA-LAN");
  airline_load("SA-UAE");
  airline_load("SA-UAL");

  airline_load("DLH");

  airline_load("THY");

}

function airline_load(icao) {
  icao = icao.toLowerCase();
  new Content({
    type: "json",
    url: "assets/airlines/"+icao+".json",
    payload: icao.toLowerCase(),
    callback: function(status, data, payload) {
      if(status == "ok") {
        prop.airline.airlines[payload] = data;
      }
    }
  });
}

function airline_get(icao) {
  icao = icao.toLowerCase();
  return prop.airline.airlines[icao];
}

function airline_get_aircraft(icao) {
  var airline     = airline_get(icao);
  var aircraft    = airline.aircraft;

  return choose_weight(aircraft);
}

function airline_ready() {
  for(var i in prop.airline.airlines) {
    var airline = prop.airline.airlines[i];
    for(var j=0;j<airline.aircraft.length;j++) {
      if(!(airline.aircraft[j][0].toLowerCase() in prop.aircraft.models)) {
        console.warn("Airline "+i.toUpperCase()+" uses nonexistent aircraft "+airline.aircraft[j][0]+", expect errors");
      }
    }
  }
}
<commit_msg>Airlines loaded<commit_after>
function airline_init_pre() {
  prop.airline = {};
  prop.airline.airlines = {};
}

function airline_init() {
  // Civil Airlines
  airline_load("UAL");
  airline_load("BAW");
  airline_load("AAL");
  airline_load("AWE");
  airline_load("CESSNA");
  airline_load("ACA");

  airline_load("KLM");
  airline_load("KLC");
  airline_load("DAL");
  airline_load("LIGHTGA");
  airline_load("FASTGA");
  airline_load("SAS");
  airline_load("NAX");
  airline_load("EIN");
  airline_load("RYR");

  // South American
  airline_load("ONE");
  airline_load("GLO");
  airline_load("TAM");
  airline_load("AVA");
  airline_load("AZU");
  airline_load("EMBRAER");

  // Russian
  airline_load("AFL");
  airline_load("MOV");
  airline_load("RLU");
  airline_load("SBI");
  airline_load("SVR");
  airline_load("TYA");

  // Cargo Airlines
  airline_load("FDX");
  airline_load("UPS");
  airline_load("CWC");

  // Military Air Forces
  airline_load("RFF");
  airline_load("FAB");
  airline_load("USAF");

    // Internationals to South America
  airline_load("SA-ACA");
  airline_load("SA-AAL");
  airline_load("SA-AEA");
  airline_load("SA-AFR");
  airline_load("SA-AMX");
  airline_load("SA-AVA");
  airline_load("SA-AZA");
  airline_load("SA-BAW");
  airline_load("SA-CCA");
  airline_load("SA-DLH");
  airline_load("SA-IBE");
  airline_load("SA-KAL");
  airline_load("SA-KLM");
  airline_load("SA-LAN");
  airline_load("SA-UAE");
  airline_load("SA-UAL");

  airline_load("DLH");

  airline_load("THY");

}

function airline_load(icao) {
  icao = icao.toLowerCase();
  new Content({
    type: "json",
    url: "assets/airlines/"+icao+".json",
    payload: icao.toLowerCase(),
    callback: function(status, data, payload) {
      if(status == "ok") {
        prop.airline.airlines[payload] = data;
      }
    }
  });
}

function airline_get(icao) {
  icao = icao.toLowerCase();
  return prop.airline.airlines[icao];
}

function airline_get_aircraft(icao) {
  var airline     = airline_get(icao);
  var aircraft    = airline.aircraft;

  return choose_weight(aircraft);
}

function airline_ready() {
  for(var i in prop.airline.airlines) {
    var airline = prop.airline.airlines[i];
    for(var j=0;j<airline.aircraft.length;j++) {
      if(!(airline.aircraft[j][0].toLowerCase() in prop.aircraft.models)) {
        console.warn("Airline "+i.toUpperCase()+" uses nonexistent aircraft "+airline.aircraft[j][0]+", expect errors");
      }
    }
  }
}
<|endoftext|>


<commit_before>(function (w, d) {

  //The set() method of the Collections can be used for updating in a smart way the models.
  //It means:
  // If I use this method passing an array of models:
  //      The properties of the models that are present in the collection, are merged.
  //      If a model is not in the collection, it will be added
  //      If a model is in the collection, but is not in the list, it will be removed from the collection

  // Define a model of type task with title void, and completed false
  var Task = Backbone.Model.extend({
    defaults: {
      title: '',
      completed: false
    }
  });

// Create five tasks
  var task1 = new Task({
    title: 'Go to the store',
    completed: false
  });

  var task2 = new Task({
    title: 'Go to the gym',
    completed: true
  });

  var task3 = new Task({
    title: 'Homework',
    completed: false
  });

// Create a collection using our models
  var tasksList = new Backbone.Collection([task1, task2, task3]);

  var task4 = new Task({
    title: 'Buy oranges',
    completed: false
  });

  tasksList.on('add', function(model) {
    console.log('task with title: ' + model.get('title') + ' was added');
  });

  tasksList.on('remove', function(model) {
    console.log('task with title: ' + model.get('title') + ' was removed');
  });

  tasksList.on('change', function(model) {
    console.log('task with title: ' + model.get('title') + ' was changed');
  });

  task1.set({
    title: 'New title task1'
  });

  task2.set({
    title: 'New title task2'
  });

// Update the collection
  tasksList.set([task1,task2,task4]);

  //This last statement, will fire:
    //Add for: task4.
    //Change for task1 and task2
    //Removed for task3

})(window, document);
<commit_msg>Cleand for study Underscore utility fns<commit_after>(function (w, d) {

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


<commit_before>import React, { Component } from 'react';
import './App.css';
import { connect } from 'react-redux'
// import connect from 'react-redux-fetch';
// import { Route } from 'react-router'
// import { Link, Switch, Redirect } from 'react-router-dom'

// import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
import htmlToJson from 'html-to-json';
import attachPoller from './utils/bbs.js'
import $ from 'jquery'
import StockMessage from './components/StockMessage';
import StockHeader from './components/StockHeader';
import TopBar from './container/TopBar';
import StockCard from './container/StockCard';
import keywords from './config/keywords';
import Scroll from 'react-scroll';
// import { updateAppLastPage } from './actions';
// import LinearProgress from 'material-ui/LinearProgress';
// var request = require('request');
// var rp = require('request-promise');

var ScrollLink   = Scroll.Link;
var ScrollElement    = Scroll.Element;

class App extends Component {
    constructor(props) {
        super(props);

        // let article = "M.1505435457.A.858";
        // if(props.match && props.match.params && props.match.params.id){
        //     article = props.match.params.id;
        // }

        this.state = {
            article: false,
            messages: undefined,
            polling: undefined,
            $polling: false,
            openSearchDialog: false,
            searchArticleValue: "",
            validDate: [],
            articles: []
        };
    }

    _attachPoller = () => {
        if(this.state.$polling === false){
            let main = this;
            const $polling = $('#article-polling');
            if($polling.html() === undefined){
                return;
            }
            this.setState({
                $polling: $polling
            }, () => {
                attachPoller(this.state.$polling, (content) => {
                    htmlToJson.parse(content, {
                        'messages': function ($doc) {
                            // console.log(this);
                            return this.map('.push', function($item, index){
                                return {
                                    tag: $item.find('.push-tag').text(),
                                    userid: $item.find('.push-userid').text(),
                                    content: $item.find('.push-content').text().substr(1).trim(),
                                    ipdatetime: $item.find('.push-ipdatetime').text().trim()
                                };
                            })
                        }
                    })
                    .then(function(data){
                        main.setState({
                            messages: main.state.messages.concat(data.messages)
                        }, () => {
                            setTimeout( () => {
                                $('#allContainer').scrollTop($('#allContainer')[0].scrollHeight);
                            }, 1500);
                        });
                    });
                });
            });
        }
    }

    _fetchData = (article) => {
        const main = this;
        let query = null;

        main.setState({
            infoStatus: 'loading'
        });

        if (!article || article === '') {
            query = this.state.article;
        } else {
            query = article;
        }

        // fetch(`http://api.openweathermap.org/data/2.5/weather?q=${query}&units=metric&appid=50a34e070dd5c09a99554b57ab7ea7e2`)
        fetch(`https://www.ptt.cc/bbs/Stock/${query}.html`)
        .then( function(response) {
            return response.text();
        })
        .then( function(html) {
            return htmlToJson.parse(html, {
                'messages': function ($doc) {
                    return this.map('.push', function($item, index){
                        return {
                            tag: $item.find('.push-tag').text(),
                            userid: $item.find('.push-userid').text(),
                            content: $item.find('.push-content').text().substr(1).trim(),
                            ipdatetime: $item.find('.push-ipdatetime').text().trim()
                        };
                    })
                },
                'polling': function ($doc) {
                    return $doc.find('#article-polling');
                },
                'meta': function ($doc) {
                    return $doc.find('.article-metaline');
                }
            });
        })
        .then( function(data) {
            console.log(data);
            main.setState({
                article: query,
                infoStatus: 'loaded',
                messages: data.messages,
                polling: data.polling[0].attribs,
                article_title: data.meta[1].children[1].children[0].data
            }, () => {
                setTimeout(() => main._attachPoller(), 3000);
            });
        })
        .catch( function() {
            main.setState({
                infoStatus: 'error'
            });
        })
    }

    _findTags = str => {
        let idTags = [];
        let keyTags = [];
        let typeTags = [];

        // match type
        const chatRegExp = new RegExp('^\\[閒聊\\].+盤.+閒聊');
        if(str.match(chatRegExp)){
            typeTags.push('chat');
        }

        // match keywords
        for(let stock_id in keywords){
            const re = new RegExp(keywords[stock_id].keys.join('|'));
            const match = str.match(re);
            if(match && keyTags.indexOf(match[0]) === -1){
               keyTags.push(match[0]);
                if(idTags.indexOf(stock_id) === -1){
                    idTags.push(stock_id);
                }
            }
        }

        // match id
        const re2 = new RegExp(/.*(\d{4}).*/);
        const match2 = str.match(re2);
        if(match2 && idTags.indexOf(match2[1]) === -1){
            idTags.push(match2[1]);
        }

        return {
            idTags,
            keyTags,
            typeTags
        }
    }

    _parseArticles = (articles) => {
        articles = articles.map(article => {
            return {
                article: article,
                tags: this._findTags(article.title)
            }
        });

        this.setState({
            articles: this.state.articles.concat(articles)
        })
    }

    _fetchArticles = (page, callback) => {
        // console.log('page', page);

        this.setState({
            infoStatus: 'loading'
        });

        const promise = fetch(`https://www.ptt.cc/bbs/Stock/index${page}.html`)
        .then( response => {
            return response.text();
        })
        .then( html => {
            return htmlToJson.parse(html, {
                'articles': function ($doc) {
                // ex. link would be Re: [問題] 職等
                return this.map('.r-ent', function($item, index){
                    const $titleA = $item.find('.title a');
                    const href = $titleA.attr('href');
                    return {
                        title: $titleA.text(),
                        id: href ? href.split('/')[3].replace('.html', '') : false,
                        href: href,
                        date: $item.find('.meta .date').text().trim(),
                        mark: $item.find('.mark').text().trim(),
                    };
                })
            }
            });
        })
        .then( data => {

            let done = false;

            const articles = data.articles.reverse().filter((article, index) => {
                let result = true;
                if(!article.id || article.mark === '!'){
                    result = false;
                }
                if(result && this.state.validDate.length > 0){

                    if(this.state.validDate.indexOf(article.date) === -1){
                        result = false;
                        done = true;
                    }
                }
                return result;
            })
            if(callback instanceof Function){
                // console.log(articles);
                callback(articles);
            }
            if(!done){
                return this._fetchArticles(page-1, callback);
            }
        })
        .catch( error => {
            // console.log(error);
            this.setState({
                infoStatus: 'error'
            });
        })

        return promise;
    }

    _init = (startPage) => {
        startPage = startPage || "";
        const validDays = 3;

        // create valid date array
        let validDate = [];
        let now = new Date();
        validDate.push(now);
        for(let i = 1 ; i <= validDays ; i++){
            let d = new Date();
            d.setDate(now.getDate() - i);
            validDate.push(d);
        }
        validDate = validDate.map(d => (d.getMonth()+1) + '/' + d.getDate());

        this.setState({
            validDate: validDate
        }, () => {
            this._fetchArticles(startPage, articles => this._parseArticles(articles))
            .then( () => {
                const chatArticles = this.state.articles.filter(obj => obj.tags.typeTags.indexOf('chat') !== -1);
                const match = this.props.match;
                let query_id = false;
                if(match && match.params && match.params.id){
                    query_id = match.params.id;
                }else if(chatArticles.length > 0){
                    query_id = chatArticles[0].article.id;
                }

                if(query_id){
                    console.log('query_id', query_id);
                    this._fetchData(query_id);
                }
            })
        })
    }

    componentWillMount() {
    }

    componentDidUpdate(){
    }

    componentDidMount() {
        this.props.app._findLastPagePromise()
        .then(page => {
            this._init(page);
        });
    }

    benchTime = time => {
        if(time.length === 0){
            console.warn('benchTime is empty', time);
            time = "00/00 00:00";
        }
        const arr = time.split(' ')[1].split(":");
        return (arr[0]*60) + (arr[1]*1);
    }

    parseMessages = messages => {
        // set tags
        // messages = messages.sort((a, b) => this.benchTime(a.ipdatetime) - this.benchTime(b.ipdatetime));
        messages.forEach((msg, index) => {
            const tags = this._findTags(msg.content);
            msg.idTags = tags.idTags;
            msg.keyTags = tags.keyTags;
            msg.message_id = index+1;
        });

        let allStockIds = [];
        messages.forEach(msg => {
            msg.idTags.forEach(id => {
                if(allStockIds.indexOf(id) === -1){
                    allStockIds.push(id);
                }
            })
        });

        let allStock = allStockIds.map(stock_id => {
            let stockMessages = messages.filter(msg => msg.idTags.indexOf(stock_id) !== -1);
            stockMessages = stockMessages.map(msg => {
                let relatedMessages = messages.filter(message => {
                    if(message.userid === msg.userid){
                        if(Math.abs(this.benchTime(message.ipdatetime) - this.benchTime(msg.ipdatetime)) <= 1){
                            return true;
                        }
                    }
                    return false;
                });

                // stock msg obj
                return {
                    message_id: msg.message_id,
                    userid: msg.userid,
                    ipdatetime: msg.ipdatetime,
                    content: relatedMessages.sort((a, b) => this.benchTime(a.ipdatetime) - this.benchTime(b.ipdatetime)).map(item => item.content)
                }
            });

            // stock data obj
            return {
                stock_id: stock_id,
                messages: stockMessages.sort((a, b) => this.benchTime(b.ipdatetime) - this.benchTime(a.ipdatetime)),
                targetArticles: this.state.articles.filter( obj => obj.tags.idTags.indexOf(stock_id) !== -1 )
            }
        });

        return allStock.sort((a, b) => b.messages.length - a.messages.length);
    }

    render() {
        const {
          infoStatus
        } = this.state;

        // if (infoStatus === 'loaded' && this.props.app.lastPage) {
        if (infoStatus === 'loaded') {
            // return
loaded...
;

            const messages = this.state.messages.filter(msg => msg.userid !== "");
            const stocks = this.parseMessages(messages);
            const polling = this.state.polling;
            const stockChildren = stocks.map( (stock, index) => <StockCard key={index} stock_id={stock.stock_id} messages={stock.messages} targetArticles={stock.targetArticles}/>);

            let totalMessages = 0;
            stocks.forEach(stock => {
                const l = stock.messages.length;
                if(l > totalMessages) {
                    totalMessages = l;
                }
            });
            const rankChildren = stocks.map( (stock, index) => {
                const keys = keywords[stock.stock_id] ? keywords[stock.stock_id].keys : [];
                return (
                    <ScrollLink
                        key={'rank-'+index}
                        activeClass="active"
                        className='rank-scroll-link'
                        to={`stock-${stock.stock_id}`}
                        // spy={true}
                        smooth={true}
                        offset={0}
                        duration={500}
                        containerId='stockContainer'>
                            <StockHeader
                                stock_id={stock.stock_id}
                                comments={stock.messages.length}
                                percentage={Math.floor(stock.messages.length/totalMessages*70)}
                                keys={keys}
                            />
                    </ScrollLink>
                )
            });

            // const allMessages = this.state.messages.sort((a, b) => this.benchTime(b.ipdatetime) - this.benchTime(a.ipdatetime))
            const msgChildren = messages.map( (msg, index) => {
                return (
                    <ScrollLink
                        key={'msg-'+index}
                        // activeClass="active"
                        // className='rank-scroll-link'
                        to={`stock-${msg.idTags[0]}`}
                        // spy={true}
                        smooth={true}
                        offset={0}
                        duration={500}
                        isDynamic={true}
                        containerId='stockContainer'>
                            <ScrollElement name={`msg-${msg.message_id}`}>
                                <StockMessage
                                    index={msg.message_id}
                                    userid={msg.userid}
                                    content={msg.content}
                                    ipdatetime={msg.ipdatetime.split(' ')[1]}
                                    idTags={msg.idTags}
                                    keyTags={msg.keyTags}
                                >
                                </StockMessage>
                            </ScrollElement>
                    </ScrollLink>
                )
            });

            const chatArticles = this.state.articles.filter( obj => obj.tags.typeTags.indexOf('chat') !== -1 );
            let topChatArticles = [];
            chatArticles.forEach( obj => {
                for( let i = 0 ; i < topChatArticles.length ; i++ ){
                    if(topChatArticles[i].article.title === obj.article.title){
                        return;
                    }
                }
                topChatArticles.push(obj);
            })
            topChatArticles = topChatArticles.filter((obj, index) => index < 4);

            return

                <TopBar {...this.props} className="TopBar" title={this.state.article_title} chatArticles={topChatArticles}>

                </TopBar>

                        {rankChildren}

                        {stockChildren}

                        {msgChildren}

        }else if (infoStatus === 'loading') {
            return
Loading...
;
        }else if (infoStatus === 'error') {
            return
Error!!!
;
        }else{
            return
Nothing!!!
;
        }

    }
}

const mapStateToProps = state => {
  return { app: state.app }
}
App = connect(mapStateToProps)(App);
export default App;<commit_msg>fix: update container id of poller<commit_after>import React, { Component } from 'react';
import './App.css';
import { connect } from 'react-redux'
// import connect from 'react-redux-fetch';
// import { Route } from 'react-router'
// import { Link, Switch, Redirect } from 'react-router-dom'

// import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
import htmlToJson from 'html-to-json';
import attachPoller from './utils/bbs.js'
import $ from 'jquery'
import StockMessage from './components/StockMessage';
import StockHeader from './components/StockHeader';
import TopBar from './container/TopBar';
import StockCard from './container/StockCard';
import keywords from './config/keywords';
import Scroll from 'react-scroll';
// import { updateAppLastPage } from './actions';
// import LinearProgress from 'material-ui/LinearProgress';
// var request = require('request');
// var rp = require('request-promise');

var ScrollLink   = Scroll.Link;
var ScrollElement    = Scroll.Element;

class App extends Component {
    constructor(props) {
        super(props);

        // let article = "M.1505435457.A.858";
        // if(props.match && props.match.params && props.match.params.id){
        //     article = props.match.params.id;
        // }

        this.state = {
            article: false,
            messages: undefined,
            polling: undefined,
            $polling: false,
            openSearchDialog: false,
            searchArticleValue: "",
            validDate: [],
            articles: []
        };
    }

    _attachPoller = () => {
        if(this.state.$polling === false){
            let main = this;
            const $polling = $('#article-polling');
            if($polling.html() === undefined){
                return;
            }
            this.setState({
                $polling: $polling
            }, () => {
                attachPoller(this.state.$polling, (content) => {
                    htmlToJson.parse(content, {
                        'messages': function ($doc) {
                            // console.log(this);
                            return this.map('.push', function($item, index){
                                return {
                                    tag: $item.find('.push-tag').text(),
                                    userid: $item.find('.push-userid').text(),
                                    content: $item.find('.push-content').text().substr(1).trim(),
                                    ipdatetime: $item.find('.push-ipdatetime').text().trim()
                                };
                            })
                        }
                    })
                    .then(function(data){
                        main.setState({
                            messages: main.state.messages.concat(data.messages)
                        }, () => {
                            setTimeout( () => {
                                $('#msgContainer').scrollTop($('#msgContainer')[0].scrollHeight);
                            }, 1500);
                        });
                    });
                });
            });
        }
    }

    _fetchData = (article) => {
        const main = this;
        let query = null;

        main.setState({
            infoStatus: 'loading'
        });

        if (!article || article === '') {
            query = this.state.article;
        } else {
            query = article;
        }

        // fetch(`http://api.openweathermap.org/data/2.5/weather?q=${query}&units=metric&appid=50a34e070dd5c09a99554b57ab7ea7e2`)
        fetch(`https://www.ptt.cc/bbs/Stock/${query}.html`)
        .then( function(response) {
            return response.text();
        })
        .then( function(html) {
            return htmlToJson.parse(html, {
                'messages': function ($doc) {
                    return this.map('.push', function($item, index){
                        return {
                            tag: $item.find('.push-tag').text(),
                            userid: $item.find('.push-userid').text(),
                            content: $item.find('.push-content').text().substr(1).trim(),
                            ipdatetime: $item.find('.push-ipdatetime').text().trim()
                        };
                    })
                },
                'polling': function ($doc) {
                    return $doc.find('#article-polling');
                },
                'meta': function ($doc) {
                    return $doc.find('.article-metaline');
                }
            });
        })
        .then( function(data) {
            console.log(data);
            main.setState({
                article: query,
                infoStatus: 'loaded',
                messages: data.messages,
                polling: data.polling[0].attribs,
                article_title: data.meta[1].children[1].children[0].data
            }, () => {
                setTimeout(() => main._attachPoller(), 3000);
            });
        })
        .catch( function() {
            main.setState({
                infoStatus: 'error'
            });
        })
    }

    _findTags = str => {
        let idTags = [];
        let keyTags = [];
        let typeTags = [];

        // match type
        const chatRegExp = new RegExp('^\\[閒聊\\].+盤.+閒聊');
        if(str.match(chatRegExp)){
            typeTags.push('chat');
        }

        // match keywords
        for(let stock_id in keywords){
            const re = new RegExp(keywords[stock_id].keys.join('|'));
            const match = str.match(re);
            if(match && keyTags.indexOf(match[0]) === -1){
               keyTags.push(match[0]);
                if(idTags.indexOf(stock_id) === -1){
                    idTags.push(stock_id);
                }
            }
        }

        // match id
        const re2 = new RegExp(/.*(\d{4}).*/);
        const match2 = str.match(re2);
        if(match2 && idTags.indexOf(match2[1]) === -1){
            idTags.push(match2[1]);
        }

        return {
            idTags,
            keyTags,
            typeTags
        }
    }

    _parseArticles = (articles) => {
        articles = articles.map(article => {
            return {
                article: article,
                tags: this._findTags(article.title)
            }
        });

        this.setState({
            articles: this.state.articles.concat(articles)
        })
    }

    _fetchArticles = (page, callback) => {
        // console.log('page', page);

        this.setState({
            infoStatus: 'loading'
        });

        const promise = fetch(`https://www.ptt.cc/bbs/Stock/index${page}.html`)
        .then( response => {
            return response.text();
        })
        .then( html => {
            return htmlToJson.parse(html, {
                'articles': function ($doc) {
                // ex. link would be Re: [問題] 職等
                return this.map('.r-ent', function($item, index){
                    const $titleA = $item.find('.title a');
                    const href = $titleA.attr('href');
                    return {
                        title: $titleA.text(),
                        id: href ? href.split('/')[3].replace('.html', '') : false,
                        href: href,
                        date: $item.find('.meta .date').text().trim(),
                        mark: $item.find('.mark').text().trim(),
                    };
                })
            }
            });
        })
        .then( data => {

            let done = false;

            const articles = data.articles.reverse().filter((article, index) => {
                let result = true;
                if(!article.id || article.mark === '!'){
                    result = false;
                }
                if(result && this.state.validDate.length > 0){

                    if(this.state.validDate.indexOf(article.date) === -1){
                        result = false;
                        done = true;
                    }
                }
                return result;
            })
            if(callback instanceof Function){
                // console.log(articles);
                callback(articles);
            }
            if(!done){
                return this._fetchArticles(page-1, callback);
            }
        })
        .catch( error => {
            // console.log(error);
            this.setState({
                infoStatus: 'error'
            });
        })

        return promise;
    }

    _init = (startPage) => {
        startPage = startPage || "";
        const validDays = 3;

        // create valid date array
        let validDate = [];
        let now = new Date();
        validDate.push(now);
        for(let i = 1 ; i <= validDays ; i++){
            let d = new Date();
            d.setDate(now.getDate() - i);
            validDate.push(d);
        }
        validDate = validDate.map(d => (d.getMonth()+1) + '/' + d.getDate());

        this.setState({
            validDate: validDate
        }, () => {
            this._fetchArticles(startPage, articles => this._parseArticles(articles))
            .then( () => {
                const chatArticles = this.state.articles.filter(obj => obj.tags.typeTags.indexOf('chat') !== -1);
                const match = this.props.match;
                let query_id = false;
                if(match && match.params && match.params.id){
                    query_id = match.params.id;
                }else if(chatArticles.length > 0){
                    query_id = chatArticles[0].article.id;
                }

                if(query_id){
                    console.log('query_id', query_id);
                    this._fetchData(query_id);
                }
            })
        })
    }

    componentWillMount() {
    }

    componentDidUpdate(){
    }

    componentDidMount() {
        this.props.app._findLastPagePromise()
        .then(page => {
            this._init(page);
        });
    }

    benchTime = time => {
        if(time.length === 0){
            console.warn('benchTime is empty', time);
            time = "00/00 00:00";
        }
        const arr = time.split(' ')[1].split(":");
        return (arr[0]*60) + (arr[1]*1);
    }

    parseMessages = messages => {
        // set tags
        // messages = messages.sort((a, b) => this.benchTime(a.ipdatetime) - this.benchTime(b.ipdatetime));
        messages.forEach((msg, index) => {
            const tags = this._findTags(msg.content);
            msg.idTags = tags.idTags;
            msg.keyTags = tags.keyTags;
            msg.message_id = index+1;
        });

        let allStockIds = [];
        messages.forEach(msg => {
            msg.idTags.forEach(id => {
                if(allStockIds.indexOf(id) === -1){
                    allStockIds.push(id);
                }
            })
        });

        let allStock = allStockIds.map(stock_id => {
            let stockMessages = messages.filter(msg => msg.idTags.indexOf(stock_id) !== -1);
            stockMessages = stockMessages.map(msg => {
                let relatedMessages = messages.filter(message => {
                    if(message.userid === msg.userid){
                        if(Math.abs(this.benchTime(message.ipdatetime) - this.benchTime(msg.ipdatetime)) <= 1){
                            return true;
                        }
                    }
                    return false;
                });

                // stock msg obj
                return {
                    message_id: msg.message_id,
                    userid: msg.userid,
                    ipdatetime: msg.ipdatetime,
                    content: relatedMessages.sort((a, b) => this.benchTime(a.ipdatetime) - this.benchTime(b.ipdatetime)).map(item => item.content)
                }
            });

            // stock data obj
            return {
                stock_id: stock_id,
                messages: stockMessages.sort((a, b) => this.benchTime(b.ipdatetime) - this.benchTime(a.ipdatetime)),
                targetArticles: this.state.articles.filter( obj => obj.tags.idTags.indexOf(stock_id) !== -1 )
            }
        });

        return allStock.sort((a, b) => b.messages.length - a.messages.length);
    }

    render() {
        const {
          infoStatus
        } = this.state;

        // if (infoStatus === 'loaded' && this.props.app.lastPage) {
        if (infoStatus === 'loaded') {
            // return
loaded...
;

            const messages = this.state.messages.filter(msg => msg.userid !== "");
            const stocks = this.parseMessages(messages);
            const polling = this.state.polling;
            const stockChildren = stocks.map( (stock, index) => <StockCard key={index} stock_id={stock.stock_id} messages={stock.messages} targetArticles={stock.targetArticles}/>);

            let totalMessages = 0;
            stocks.forEach(stock => {
                const l = stock.messages.length;
                if(l > totalMessages) {
                    totalMessages = l;
                }
            });
            const rankChildren = stocks.map( (stock, index) => {
                const keys = keywords[stock.stock_id] ? keywords[stock.stock_id].keys : [];
                return (
                    <ScrollLink
                        key={'rank-'+index}
                        activeClass="active"
                        className='rank-scroll-link'
                        to={`stock-${stock.stock_id}`}
                        // spy={true}
                        smooth={true}
                        offset={0}
                        duration={500}
                        containerId='stockContainer'>
                            <StockHeader
                                stock_id={stock.stock_id}
                                comments={stock.messages.length}
                                percentage={Math.floor(stock.messages.length/totalMessages*70)}
                                keys={keys}
                            />
                    </ScrollLink>
                )
            });

            // const allMessages = this.state.messages.sort((a, b) => this.benchTime(b.ipdatetime) - this.benchTime(a.ipdatetime))
            const msgChildren = messages.map( (msg, index) => {
                return (
                    <ScrollLink
                        key={'msg-'+index}
                        // activeClass="active"
                        // className='rank-scroll-link'
                        to={`stock-${msg.idTags[0]}`}
                        // spy={true}
                        smooth={true}
                        offset={0}
                        duration={500}
                        isDynamic={true}
                        containerId='stockContainer'>
                            <ScrollElement name={`msg-${msg.message_id}`}>
                                <StockMessage
                                    index={msg.message_id}
                                    userid={msg.userid}
                                    content={msg.content}
                                    ipdatetime={msg.ipdatetime.split(' ')[1]}
                                    idTags={msg.idTags}
                                    keyTags={msg.keyTags}
                                >
                                </StockMessage>
                            </ScrollElement>
                    </ScrollLink>
                )
            });

            const chatArticles = this.state.articles.filter( obj => obj.tags.typeTags.indexOf('chat') !== -1 );
            let topChatArticles = [];
            chatArticles.forEach( obj => {
                for( let i = 0 ; i < topChatArticles.length ; i++ ){
                    if(topChatArticles[i].article.title === obj.article.title){
                        return;
                    }
                }
                topChatArticles.push(obj);
            })
            topChatArticles = topChatArticles.filter((obj, index) => index < 4);

            return

                <TopBar {...this.props} className="TopBar" title={this.state.article_title} chatArticles={topChatArticles}>

                </TopBar>

                        {rankChildren}

                        {stockChildren}

                        {msgChildren}

        }else if (infoStatus === 'loading') {
            return
Loading...
;
        }else if (infoStatus === 'error') {
            return
Error!!!
;
        }else{
            return
Nothing!!!
;
        }

    }
}

const mapStateToProps = state => {
  return { app: state.app }
}
App = connect(mapStateToProps)(App);
export default App;<|endoftext|>


<commit_before>/* eslint-env jest */
import React from 'react'
import { cleanup, fireEvent } from '@testing-library/react'
import { renderWithReduxAndIntl } from '../../../../test/helpers/render'
import UndoRedo from '../UndoRedo'
import { replaceUndoStack } from '../../store/actions/undo'

// `getRemixOnFirstEdit` is a legacy function, so for the purposes of this
// test suite it always returns `false` (aka, assume user owns the street)
jest.mock('../../streets/remix', () => ({
  getRemixOnFirstEdit: () => false
}))

describe('UndoRedo', () => {
  afterEach(() => {
    cleanup()
  })

  it('renders two buttons', () => {
    const wrapper = renderWithReduxAndIntl(<UndoRedo />)
    expect(wrapper.asFragment()).toMatchSnapshot()
  })

  it('enables undo button when an undo stack is created', () => {
    const wrapper = renderWithReduxAndIntl(<UndoRedo />, {
      initialState: {
        undo: {
          stack: [],
          position: 0
        }
      }
    })
    const undoButton = wrapper.getByTitle('Undo')
    const redoButton = wrapper.getByTitle('Redo')

    // Initial button states are disabled
    expect(undoButton.disabled).toEqual(true)
    expect(redoButton.disabled).toEqual(true)

    // Update undo stack state from "elsewhere", and `<UndoRedo>` should
    // receive new props. This is akin to a user editing the street once
    wrapper.store.dispatch(replaceUndoStack([{ foo: 'bar' }], 1))

    // Undo button state should become enabled
    // Redo button should remain disabled
    expect(undoButton.disabled).toEqual(false)
    expect(redoButton.disabled).toEqual(true)
  })

  it('enables redo button when undo stack position is lower than the top of the stack', () => {
    const wrapper = renderWithReduxAndIntl(<UndoRedo />, {
      initialState: {
        undo: {
          stack: [
            { foo: 'bar' },
            { foo: 'baz' }
          ],
          position: 0
        }
      }
    })

    // In this scenario, redo is available, but undo is not.
    expect(wrapper.getByTitle('Undo').disabled).toEqual(true)
    expect(wrapper.getByTitle('Redo').disabled).toEqual(false)
  })

  it('calls undo action when undo button is clicked', () => {
    const wrapper = renderWithReduxAndIntl(<UndoRedo />, {
      initialState: {
        undo: {
          stack: [
            { foo: 'bar' },
            { foo: 'baz' }
          ],
          position: 1
        }
      }
    })

    // Expect the undo position to decrement when undo button is clicked
    fireEvent.click(wrapper.getByTitle('Undo'))
    expect(wrapper.store.getState().undo.position).toEqual(0)
  })

  it('calls redo action when redo button is clicked', () => {
    const wrapper = renderWithReduxAndIntl(<UndoRedo />, {
      initialState: {
        undo: {
          stack: [
            { foo: 'bar' },
            { foo: 'baz' }
          ],
          position: 0
        }
      }
    })

    // Expect the undo position to increment when redo button is clicked
    fireEvent.click(wrapper.getByTitle('Redo'))
    expect(wrapper.store.getState().undo.position).toEqual(1)
  })
})
<commit_msg>test(UndoRedo): further simplify tests<commit_after>/* eslint-env jest */
import React from 'react'
import { cleanup, fireEvent } from '@testing-library/react'
import { renderWithReduxAndIntl } from '../../../../test/helpers/render'
import UndoRedo from '../UndoRedo'

// `getRemixOnFirstEdit` is a legacy function, so for the purposes of this
// test suite it always returns `false` (aka, assume user owns the street)
jest.mock('../../streets/remix', () => ({
  getRemixOnFirstEdit: () => false
}))

describe('UndoRedo', () => {
  afterEach(() => {
    cleanup()
  })

  // TODO: Remove snapshot after having a snapshot on the parent component
  it('renders two buttons', () => {
    const wrapper = renderWithReduxAndIntl(<UndoRedo />)
    expect(wrapper.asFragment()).toMatchSnapshot()
  })

  it('handles clicking undo button', () => {
    const wrapper = renderWithReduxAndIntl(<UndoRedo />, {
      initialState: {
        undo: {
          stack: [
            { foo: 'bar' },
            { foo: 'baz' }
          ],
          position: 1
        }
      }
    })

    // Click the undo button
    fireEvent.click(wrapper.getByTitle('Undo'))

    // Redo should now be available, but undo is not.
    expect(wrapper.getByTitle('Undo').disabled).toEqual(true)
    expect(wrapper.getByTitle('Redo').disabled).toEqual(false)
  })

  it('handles clicking redo button', () => {
    const wrapper = renderWithReduxAndIntl(<UndoRedo />, {
      initialState: {
        undo: {
          stack: [
            { foo: 'bar' },
            { foo: 'baz' }
          ],
          position: 0
        }
      }
    })

    // Expect the undo position to increment when redo button is clicked
    // Click the redo button
    fireEvent.click(wrapper.getByTitle('Redo'))

    // Undo should now be available, but redo is not.
    expect(wrapper.getByTitle('Undo').disabled).toEqual(false)
    expect(wrapper.getByTitle('Redo').disabled).toEqual(true)
  })
})
<|endoftext|>


<commit_before>import React from 'react';
import PropTypes from 'prop-types';
import Relay from 'react-relay/classic';
import DocumentTitle from 'react-document-title';

import Panel from '../../../shared/Panel';
import Button from '../../../shared/Button';
import permissions from '../../../../lib/permissions';

import Row from './row';

class Index extends React.Component {
  static propTypes = {
    pipeline: PropTypes.shape({
      name: PropTypes.string.isRequired,
      schedules: PropTypes.shape({
        edges: PropTypes.arrayOf(
          PropTypes.shape({
            node: PropTypes.shape({
              id: PropTypes.string.isRequired,
              failedAt: PropTypes.string
            }).isRequired
          }).isRequired
        )
      }).isRequired,
      permissions: PropTypes.shape({
        pipelineScheduleCreate: PropTypes.shape({
          allowed: PropTypes.bool.isRequired
        }).isRequired
      }).isRequired
    }).isRequired,
    params: PropTypes.shape({
      organization: PropTypes.string.isRequired,
      pipeline: PropTypes.string.isRequired
    }).isRequired
  };

  render() {
    return (
      <DocumentTitle title={`Schedules · ${this.props.pipeline.name}`}>
        <React.Fragment>
          {this.renderFailureMessage()}
          <Panel>
            <Panel.Header>Schedules</Panel.Header>

            <Panel.IntroWithButton>
              Build schedules automatically create builds at specified intervals.
              {this.renderNewScheduleButton()}
            </Panel.IntroWithButton>
            {this.renderScheduleRows()}
          </Panel>
        </React.Fragment>
      </DocumentTitle>
    );
  }

  renderFailureMessage() {
    const schedules = this.props.pipeline.schedules.edges;

    const failedSchedules = schedules.filter((edge) => edge.node.failedAt);

    const plural = failedSchedules.length !== 1;

    if (failedSchedules.length > 0) {
      return (

            {plural ? 'Several of your schedules have' : 'One of your schedules has'} been automatically disabled due to {plural ? 'errors' : 'an error'}.
            Please review the schedule{plural && 's'} to allow {plural ? 'them' : 'it'} to continue creating builds.

      );
    }

    return null;
  }

  renderNewScheduleButton() {
    return permissions(this.props.pipeline.permissions).check(
      {
        allowed: "pipelineScheduleCreate",
        render: () => <Button link={`/${this.props.params.organization}/${this.props.params.pipeline}/settings/schedules/new`}>New Schedule</Button>
      }
    );
  }

  renderScheduleRows() {
    const schedules = this.props.pipeline.schedules.edges;

    if (schedules.length > 0) {
      return schedules.map((edge) => {
        return (
          <Row key={edge.node.id} pipelineSchedule={edge.node} />
        );
      });
    }

    return null;
  }
}

export default Relay.createContainer(Index, {
  fragments: {
    pipeline: () => Relay.QL`
      fragment on Pipeline {
        name
        schedules(first: 100) {
          edges {
            node {
              id
              failedAt
              ${Row.getFragment("pipelineSchedule")}
            }
          }
        }
        permissions {
          pipelineScheduleCreate {
            allowed
            code
            message
          }
        }
      }
    `
  }
});
<commit_msg>Fix lints<commit_after>import React from 'react';
import PropTypes from 'prop-types';
import Relay from 'react-relay/classic';
import DocumentTitle from 'react-document-title';

import Panel from '../../../shared/Panel';
import Button from '../../../shared/Button';
import permissions from '../../../../lib/permissions';

import Row from './row';

class Index extends React.Component {
  static propTypes = {
    pipeline: PropTypes.shape({
      name: PropTypes.string.isRequired,
      schedules: PropTypes.shape({
        edges: PropTypes.arrayOf(
          PropTypes.shape({
            node: PropTypes.shape({
              id: PropTypes.string.isRequired,
              failedAt: PropTypes.string
            }).isRequired
          }).isRequired
        )
      }).isRequired,
      permissions: PropTypes.shape({
        pipelineScheduleCreate: PropTypes.shape({
          allowed: PropTypes.bool.isRequired
        }).isRequired
      }).isRequired
    }).isRequired,
    params: PropTypes.shape({
      organization: PropTypes.string.isRequired,
      pipeline: PropTypes.string.isRequired
    }).isRequired
  };

  render() {
    return (
      <DocumentTitle title={`Schedules · ${this.props.pipeline.name}`}>
        <React.Fragment>
          {this.renderFailureMessage()}
          <Panel>
            <Panel.Header>Schedules</Panel.Header>

            <Panel.IntroWithButton>
              Build schedules automatically create builds at specified intervals.
              {this.renderNewScheduleButton()}
            </Panel.IntroWithButton>
            {this.renderScheduleRows()}
          </Panel>
        </React.Fragment>
      </DocumentTitle>
    );
  }

  renderFailureMessage() {
    const schedules = this.props.pipeline.schedules.edges;

    const failedSchedules = schedules.filter((edge) => edge.node.failedAt);

    const plural = failedSchedules.length !== 1;

    if (failedSchedules.length > 0) {
      return (

            {plural ? 'Several of your schedules have' : 'One of your schedules has'} been automatically disabled due to {plural ? 'errors' : 'an error'}.
            Please review the schedule{plural && 's'} to allow {plural ? 'them' : 'it'} to continue creating builds.

      );
    }

    return null;
  }

  renderNewScheduleButton() {
    return permissions(this.props.pipeline.permissions).check(
      {
        allowed: "pipelineScheduleCreate",
        render: () => <Button link={`/${this.props.params.organization}/${this.props.params.pipeline}/settings/schedules/new`}>New Schedule</Button>
      }
    );
  }

  renderScheduleRows() {
    const schedules = this.props.pipeline.schedules.edges;

    if (schedules.length > 0) {
      return schedules.map((edge) => {
        return (
          <Row key={edge.node.id} pipelineSchedule={edge.node} />
        );
      });
    }

    return null;
  }
}

export default Relay.createContainer(Index, {
  fragments: {
    pipeline: () => Relay.QL`
      fragment on Pipeline {
        name
        schedules(first: 100) {
          edges {
            node {
              id
              failedAt
              ${Row.getFragment("pipelineSchedule")}
            }
          }
        }
        permissions {
          pipelineScheduleCreate {
            allowed
            code
            message
          }
        }
      }
    `
  }
});
<|endoftext|>


<commit_before>/***************************************************************************
 *
 * Copyright (c) 2014 Baidu.com, Inc. All Rights Reserved
 * $Id$
 *
 **************************************************************************/

/**
 * lib/check-output.js ~ 2014/02/12 21:27:18
 * @author leeight(liyubei@baidu.com)
 * @version $Revision$
 * @description
 * 检查项目build完毕之后output的内容是否正确
 * 1. css引用的img是否正确
 * 2. html引用的css是否正确（TODO）
 **/
var edp = require( 'edp-core' );

var fs = require( 'fs' );
var path = require( 'path' );

/**
 * 检查css引用的图片是否正确
 * @param {string} file css文件的路径.
 */
function checkResouce( file ) {
    var data = edp.fs.readFileSync( file );
    var pattern = /\burl\s*\((["']?)([^\)]+)\1\)/g;
    var match = null;

    var invalidResource = [];
    while ( match = pattern.exec( data ) ) {
        var url = match[2];
        // edp ERROR → output/dep/font-awesome/3.1.0/font/fontawesome-webfont.eot?v=3.1.0
        // edp ERROR → output/dep/font-awesome/3.1.0/font/fontawesome-webfont.eot?#iefix&v=3.1.0
        // edp ERROR → output/dep/font-awesome/3.1.0/font/fontawesome-webfont.woff?v=3.1.0
        // edp ERROR → output/dep/font-awesome/3.1.0/font/fontawesome-webfont.ttf?v=3.1.0
        // edp ERROR → output/dep/font-awesome/3.1.0/font/fontawesome-webfont.svg#fontawesomeregular?v=3.1.0
        if ( edp.path.isLocalPath( url ) ) {
            var resourceUrl = path.normalize( path.join(path.dirname(file), url) );
            resourceUrl = resourceUrl.replace(/[#\?].*$/g, '');
            if ( !fs.existsSync( resourceUrl ) ) {
                var relative = path.relative( process.cwd(), resourceUrl );
                if ( invalidResource.indexOf( relative )  === -1 ) {
                    invalidResource.push( relative );
                }
            }
        }
    };

    if (invalidResource.length) {
        edp.log.info("%s", path.relative(process.cwd(), file));
        invalidResource.forEach(function( resourceUrl ){
            edp.log.error( "→ %s", resourceUrl);
        });
    }
}

/**
 * 从output目录的html文件提取require.config的内容，生成module.conf，后续
 * 检查的时候计算模板路径依赖module.conf的内容.
 */
function prepareModuleConfig() {
    var ok = false;
    var dir = 'output';

    if ( fs.existsSync( edp.path.join( dir, 'module.conf' ) ) ) {
        return true;
    }

    fs.readdirSync( dir ).every(
        function ( file ) {
            // 如果不是html文件，忽略之
            if ( !/.html?$/.test( file ) ) {
                // continue the loop
                return true;
            }

            var config = edp.esl.readLoaderConfig(
                fs.readFileSync( edp.path.resolve( dir, file ), 'utf-8' ) );
            if ( !config ) {
                // continue the loop
                return true;
            }

            var filename = edp.path.resolve( dir, 'module.conf' );
            var data = JSON.stringify( config.data, null, 4 );
            fs.writeFileSync( filename, data );
            ok = true;

            // break the loop
            return false;
        }
    );

    return ok;
}

/**
 * 检查js引用的模板路径是否正确
 */
function checkTemplate( file ) {
    var data = edp.fs.readFileSync( file );

    var ast = edp.esl.getAst( data, file );
    var moduleInfos = edp.esl.analyseModule( ast );
    if ( !moduleInfos ) {
        return;
    }

    if ( !(moduleInfos instanceof Array) ) {
        moduleInfos = [ moduleInfos ];
    }

    var invalidResource = [];
    for ( var i = 0; i < moduleInfos.length; i ++ ) {
        var moduleInfo = moduleInfos[ i ];
        invalidResource = invalidResource.concat(
            checkTemplateFromModuleInfo( moduleInfo, file )
        );
    }

    if (invalidResource.length) {
        edp.log.info("%s", path.relative(process.cwd(), file));
        invalidResource.forEach(function( resourceUrl ){
            edp.log.error( "→ %s", resourceUrl);
        });
    }
}

function checkTemplateFromModuleInfo( moduleInfo, file ) {
    var invalidResource = [];

    var actualDependencies = moduleInfo.actualDependencies
    if ( !actualDependencies || actualDependencies.length <= 0 ) {
        return invalidResource;
    }

    for ( var i = 0; i < actualDependencies.length; i ++) {
        var depId = actualDependencies[ i ];
        if ( depId.indexOf( '!' ) === -1 ) {
            continue;
        }

        var parts = depId.split( '!' );
        var pluginId = parts[ 0 ];
        var resourceId = parts[ 1 ];

        if ( !edp.path.isLocalPath( resourceId ) ) {
            // resourceId = http://...
            // resourceId = https://...
            continue;
        }

        var tpl = null;
        if ( resourceId[0] === '.' ) {
            // 相对路径，相对于当前的js文件
            // tpl!./tpl/list.tpl.html
            // tpl!../tpl/list.tpl.html
            if ( !moduleInfo.id ) {
                tpl = path.normalize( path.join( path.dirname( file ), resourceId ) );
            }
            else {
                resourceId = edp.esl.resolveModuleId( resourceId, moduleInfo.id );
                tpl = getModuleFile( resourceId, edp.path.join( process.cwd(), 'output' ) );
            }
        }
        else {
            // 不是相对路径，例如
            // tpl!common/tpl/list.tpl.html
            tpl = getModuleFile( resourceId, edp.path.join( process.cwd(), 'output' ) );
        }

        // 有的项目里面会把tpl编译成js的
        if ( tpl && !( fs.existsSync( tpl ) || fs.existsSync( tpl + '.js' ) ) ) {
            if ( invalidResource.indexOf( tpl ) === -1 ) {
                invalidResource.push( tpl );
            }
        }
    }

    return invalidResource;
}

function getModuleFile( resourceId, baseDir ) {
    var configFile = path.resolve( baseDir, 'module.conf' );
    tpl = edp.esl.getModuleFile( resourceId, configFile );
    // 因为总是会追加.js后缀，因此我们需要删掉
    tpl = tpl.replace(/\.js$/, '');

    return tpl;
}

module.exports = function(args, opts) {
    edp.log.info('Checking output directory...');

    if ( !fs.existsSync( 'output' ) ) {
        return;
    }

    var FLAGS_enableCheckTemplate = true;
    if ( !prepareModuleConfig() ) {
        edp.log.warn( "Prepare module config failed, skip `checkTemplate'" );
        FLAGS_enableCheckTemplate = false;
    }

    function scanDir( dir ) {
        fs.readdirSync( dir )
            .sort(
                function( file ) {
                    var fullPath = path.resolve( dir, file );
                    if ( fs.statSync( fullPath ).isDirectory() ) {
                        return 1;
                    }

                    return -1;
                }
            )
            .forEach(
                function ( file ) {
                    var fullPath = path.resolve( dir, file );
                    var stat = fs.statSync( fullPath );
                    var extName = path.extname( file );
                    var name = path.basename( file, extName );

                    if ( stat.isFile() && /\.css$/i.test( extName ) ) {
                        checkResouce( fullPath );
                    }
                    else if ( FLAGS_enableCheckTemplate && stat.isFile() && /\.js$/.test( extName ) ) {
                        checkTemplate( fullPath );
                    }
                    else if ( stat.isDirectory() && name != 'node_modules' ) {
                        scanDir( fullPath );
                    }
                }
            );
    }

    scanDir( 'output' );

    // 清除output/module.conf文件
    try {
        fs.unlinkSync( 'output/module.conf' );
    } catch( ex ) {}
}

/* vim: set ts=4 sw=4 sts=4 tw=100: */
<commit_msg>Fix ecomfe/edp#155<commit_after>/***************************************************************************
 *
 * Copyright (c) 2014 Baidu.com, Inc. All Rights Reserved
 * $Id$
 *
 **************************************************************************/

/**
 * lib/check-output.js ~ 2014/02/12 21:27:18
 * @author leeight(liyubei@baidu.com)
 * @version $Revision$
 * @description
 * 检查项目build完毕之后output的内容是否正确
 * 1. css引用的img是否正确
 * 2. html引用的css是否正确（TODO）
 **/
var edp = require( 'edp-core' );

var fs = require( 'fs' );
var path = require( 'path' );

/**
 * 检查css引用的图片是否正确
 * @param {string} file css文件的路径.
 */
function checkResouce( file ) {
    var data = edp.fs.readFileSync( file );
    var pattern = /\burl\s*\((["']?)([^\)]+)\1\)/g;
    var match = null;

    var invalidResource = [];
    while ( match = pattern.exec( data ) ) {
        var url = match[2];
        // edp ERROR → output/dep/font-awesome/3.1.0/font/fontawesome-webfont.eot?v=3.1.0
        // edp ERROR → output/dep/font-awesome/3.1.0/font/fontawesome-webfont.eot?#iefix&v=3.1.0
        // edp ERROR → output/dep/font-awesome/3.1.0/font/fontawesome-webfont.woff?v=3.1.0
        // edp ERROR → output/dep/font-awesome/3.1.0/font/fontawesome-webfont.ttf?v=3.1.0
        // edp ERROR → output/dep/font-awesome/3.1.0/font/fontawesome-webfont.svg#fontawesomeregular?v=3.1.0
        if ( edp.path.isLocalPath( url ) ) {
            var resourceUrl = path.normalize( path.join(path.dirname(file), url) );
            resourceUrl = resourceUrl.replace(/[#\?].*$/g, '');
            if ( !fs.existsSync( resourceUrl ) ) {
                var relative = path.relative( process.cwd(), resourceUrl );
                if ( invalidResource.indexOf( relative )  === -1 ) {
                    invalidResource.push( relative );
                }
            }
        }
    };

    if (invalidResource.length) {
        edp.log.info("%s", path.relative(process.cwd(), file));
        invalidResource.forEach(function( resourceUrl ){
            edp.log.error( "→ %s", resourceUrl);
        });
    }
}

/**
 * 从output目录的html文件提取require.config的内容，生成module.conf，后续
 * 检查的时候计算模板路径依赖module.conf的内容.
 *
 * @see https://github.com/ecomfe/edp/issues/131
 * FIXME 我们从index.html里面读取了require.config的内容之后，写入到module.conf
 * 之前。需要给paths里面的配置项目添加baseUrl的前缀，否则可能导致计算路径的时候
 * 出错，但是这个逻辑以后是会修改的，因此记得参考ecomfe/edp#131这个issue的状态.
 */
function prepareModuleConfig() {
    var ok = false;
    var dir = 'output';

    if ( fs.existsSync( edp.path.join( dir, 'module.conf' ) ) ) {
        return true;
    }

    fs.readdirSync( dir ).every(
        function ( file ) {
            // 如果不是html文件，忽略之
            if ( !/.html?$/.test( file ) ) {
                // continue the loop
                return true;
            }

            var config = edp.esl.readLoaderConfig(
                fs.readFileSync( edp.path.resolve( dir, file ), 'utf-8' ) );
            if ( !config ) {
                // continue the loop
                return true;
            }

            var filename = edp.path.resolve( dir, 'module.conf' );

            // @see ecomfe/edp#131
            if ( 'paths' in config.data ) {
                var baseUrl = config.data.baseUrl;
                if ( baseUrl ) {
                    for ( var key in config.data.paths ) {
                        var value = config.data.paths[ key ];
                        if ( edp.path.isLocalPath( value ) ) {
                            config.data.paths[ key ] = baseUrl + '/' + value;
                        }
                    }
                }
            }
            // TODO packages里面的路径如何修正呢？

            var data = JSON.stringify( config.data, null, 4 );
            fs.writeFileSync( filename, data );
            ok = true;

            // break the loop
            return false;
        }
    );

    return ok;
}

/**
 * 检查js引用的模板路径是否正确
 */
function checkTemplate( file ) {
    var data = edp.fs.readFileSync( file );

    var ast = edp.esl.getAst( data, file );
    var moduleInfos = edp.esl.analyseModule( ast );
    if ( !moduleInfos ) {
        return;
    }

    if ( !(moduleInfos instanceof Array) ) {
        moduleInfos = [ moduleInfos ];
    }

    var invalidResource = [];
    for ( var i = 0; i < moduleInfos.length; i ++ ) {
        var moduleInfo = moduleInfos[ i ];
        invalidResource = invalidResource.concat(
            checkTemplateFromModuleInfo( moduleInfo, file )
        );
    }

    if (invalidResource.length) {
        edp.log.info("%s", path.relative(process.cwd(), file));
        invalidResource.forEach(function( resourceUrl ){
            edp.log.error( "→ %s", resourceUrl);
        });
    }
}

function checkTemplateFromModuleInfo( moduleInfo, file ) {
    var invalidResource = [];

    var actualDependencies = moduleInfo.actualDependencies
    if ( !actualDependencies || actualDependencies.length <= 0 ) {
        return invalidResource;
    }

    for ( var i = 0; i < actualDependencies.length; i ++) {
        var depId = actualDependencies[ i ];
        if ( depId.indexOf( '!' ) === -1 ) {
            continue;
        }

        var parts = depId.split( '!' );
        var pluginId = parts[ 0 ];
        var resourceId = parts[ 1 ];

        if ( !edp.path.isLocalPath( resourceId ) ) {
            // resourceId = http://...
            // resourceId = https://...
            continue;
        }

        var tpl = null;
        if ( resourceId[0] === '.' ) {
            // 相对路径，相对于当前的js文件
            // tpl!./tpl/list.tpl.html
            // tpl!../tpl/list.tpl.html
            if ( !moduleInfo.id ) {
                tpl = path.normalize( path.join( path.dirname( file ), resourceId ) );
            }
            else {
                resourceId = edp.esl.resolveModuleId( resourceId, moduleInfo.id );
                tpl = getModuleFile( resourceId, edp.path.join( process.cwd(), 'output' ) );
            }
        }
        else {
            // 不是相对路径，例如
            // tpl!common/tpl/list.tpl.html
            tpl = getModuleFile( resourceId, edp.path.join( process.cwd(), 'output' ) );
        }

        // 有的项目里面会把tpl编译成js的
        if ( tpl && !( fs.existsSync( tpl ) || fs.existsSync( tpl + '.js' ) ) ) {
            if ( invalidResource.indexOf( tpl ) === -1 ) {
                invalidResource.push( tpl );
            }
        }
    }

    return invalidResource;
}

function getModuleFile( resourceId, baseDir ) {
    var configFile = path.resolve( baseDir, 'module.conf' );
    tpl = edp.esl.getModuleFile( resourceId, configFile );
    // 因为总是会追加.js后缀，因此我们需要删掉
    tpl = tpl.replace(/\.js$/, '');

    return tpl;
}

module.exports = function(args, opts) {
    edp.log.info('Checking output directory...');

    if ( !fs.existsSync( 'output' ) ) {
        return;
    }

    var FLAGS_enableCheckTemplate = true;
    if ( !prepareModuleConfig() ) {
        edp.log.warn( "Prepare module config failed, skip `checkTemplate'" );
        FLAGS_enableCheckTemplate = false;
    }

    function scanDir( dir ) {
        fs.readdirSync( dir )
            .sort(
                function( file ) {
                    var fullPath = path.resolve( dir, file );
                    if ( fs.statSync( fullPath ).isDirectory() ) {
                        return 1;
                    }

                    return -1;
                }
            )
            .forEach(
                function ( file ) {
                    var fullPath = path.resolve( dir, file );
                    var stat = fs.statSync( fullPath );
                    var extName = path.extname( file );
                    var name = path.basename( file, extName );

                    if ( stat.isFile() && /\.css$/i.test( extName ) ) {
                        checkResouce( fullPath );
                    }
                    else if ( FLAGS_enableCheckTemplate && stat.isFile() && /\.js$/.test( extName ) ) {
                        checkTemplate( fullPath );
                    }
                    else if ( stat.isDirectory() && name != 'node_modules' ) {
                        scanDir( fullPath );
                    }
                }
            );
    }

    scanDir( 'output' );

    // 清除output/module.conf文件
    try {
        fs.unlinkSync( 'output/module.conf' );
    } catch( ex ) {}
}

/* vim: set ts=4 sw=4 sts=4 tw=100: */
<|endoftext|>