<commit_before>/**
 * Curriculum inventory management (cim) application module components.
 *
 * Defines the following namespaces:
 *     ilios.cim
 *
 *  Dependencies:
 *     application/views/scripts/ilios_base.js
 *     ilios_i18nVendor
 *     YUI Dom/Event/Element libs
 *     YUI Container libs
 *     YUI Cookie lib
 *     application/views/curriculum_inventory/js/ilios.cim.model.js
 *     application/views/curriculum_inventory/js/ilios.cim.view.js
 *     application/views/curriculum_inventory/js/ilios.cim.widget.js
 */
(function () {

    ilios.namespace('cim');

    var Lang = YAHOO.lang,
        Event = YAHOO.util.Event,
        Dom = YAHOO.util.Dom;
    /**
     * Creates a client-side application.
     * It's the top level object responsible for instantiating a self-contained application environment
     * and for providing routing and high-level workflow management functionality.
     *
     * @namespace ilios.cim
     * @class App
     * @param {Object} config the application configuration.
     * @param {Object} [payload] The initial page payload. It may have these data points as properties:
     *     "programs" ... an object holding the programs available for reporting on.
     *     "report" ... (optional) an object representing the currently selected report.
     *     "courses" ... (optional) An lookup object (keyed off by course id) holding linked and linkable courses
     *         with/for sequence blocks in this report.
     *     "sequence" ... (optional) An object representing the report sequence.
     *     "sequence_blocks" ... (optional) An array of report sequence blocks.
     *     "academic_levels" ... (optional) An array of academic levels available in the given report.
     * @constructor
     * @todo way to much things going on in there for a single function. chunk it up into separate init functions.
     */
    var App = function (config, payload) {

        var i, n,
            sequenceBlockModel, sequenceBlockView;
        // set module configuration
        this.config = Lang.isObject(config) ? config : {};

        this.statusBar = new ilios.cim.widget.StatusBar({});
        this.statusBar.render('status-toolbar');

        this.programs = payload.programs;

        // wire dialogs to buttons
        Event.addListener('pick_reports_btn', 'click', function (event) {
            if (! this.reportPickerDialog) { // instantiate on demand
                this.reportPickerDialog = new ilios.cim.widget.ReportPickerDialog('report_picker_dialog');
            }
            this.reportPickerDialog.show();
            Event.stopEvent(event);
            return false;
        }, {}, this);

        Event.addListener('create_report_btn', 'click', function (event) {
            if (! this.createReportDialog) {
                this.createReportDialog = new ilios.cim.widget.CreateReportDialog('create_report_dialog', {}, this.programs);
            }
            this.createReportDialog.show();
            Event.stopEvent(event);
            return false;
        }, {}, this);

        if (payload.hasOwnProperty('report')) {

            // process data
            this.academicLevels = payload.academic_levels;
            this.sequenceBlock = payload.sequence_block;
            this.sequenceBlocks = payload.sequence_blocks;
            this.linkableCourses = payload.linkable_courses;
            this.linkedCourses = payload.linked_courses;
            this.reportModel = new ilios.cim.model.ReportModel(payload.report);

            // set up views and widgets

            this.reportView = new ilios.cim.view.ReportView(this.reportModel, {
                finalizeUrl: this.config.controllerUrl + 'finalize',
                deleteUrl: this.config.controllerUrl + 'delete'
            });
            this.reportView.subscribe('exportStarted', function() {
                this.show('Started Report Export …', true);
            }, this.statusBar, true);
            this.reportView.subscribe('downloadStarted', function() {
                this.show('Started Report Download …', true);
            }, this.statusBar, true);
            this.reportView.subscribe('exportFinished', function () {
                this.reset();
            }, this.statusBar, true);
            this.reportView.subscribe('downloadFinished', function () {
                this.reset();
            }, this.statusBar, true);
            this.reportModel.subscribe('afterUpdate', function () {
                this.show('Report updated.', false);
            }, this.statusBar, true);
            this.reportView.subscribe('finalizeStarted', function () {
                this.show('Finalizing report started …', true);
            }, this.statusBar, true);
            this.reportView.subscribe('finalizeSucceeded', function () {
                this.reset();
            }, this.statusBar, true);
            this.reportView.subscribe('finalizeFailed', function () {
                this.show('Finalizing report failed.', false);
            }, this.statusBar, true);
            this.reportView.subscribe('deleteStarted', function () {
                this.show('Deleting report …', true);
            }, this.statusBar, true);
            this.reportView.subscribe('deleteFailed', function () {
                this.show('Failed to delete report.', false);
            }, this.statusBar, true);
            this.reportView.subscribe('deleteSucceeded', function() {
                this.show('Successfully deleted report. Reloading page …', true);
                // reload the page
                window.location = window.location.protocol + "//" + window.location.host + window.location.pathname;

            }, this.statusBar, true);

            this.reportView.render();

            this.sequenceBlockTopToolbar = new ilios.cim.widget.SequenceBlockTopToolbar({});
            this.sequenceBlockTopToolbar.render();

            this.sequenceBlockBottomToolbar = new ilios.cim.widget.SequenceBlockBottomToolbar({});
            this.sequenceBlockBottomToolbar.render(! this.reportModel.get('isFinalized'));

            // more wiring of event handlers

            Event.addListener(this.sequenceBlockTopToolbar.get('expandBtnEl'), 'click', function (event) {
                this.expandAllSequenceBlocks();
            }, {}, this);

            Event.addListener(this.sequenceBlockTopToolbar.get('collapseBtnEl'), 'click', function (event) {
                this.collapseAllSequenceBlocks();
            }, {}, this);

            this.reportView.subscribe('finalizeSucceeded', function () {
                this.disableButtons();
            }, this.sequenceBlockBottomToolbar, true);

            // show views and widgets
            this.reportView.show();
            this.sequenceBlockBottomToolbar.show();
            this.sequenceBlockTopToolbar.show();

            // deal with sequence block models and views
            // @todo break this out into three steps
            // 1. create models
            // 2. create views from models
            // 3. show views
            for (i = 0, n = payload.sequence_blocks.length; i < n; i++) {
                sequenceBlockModel = this.createSequenceBlockModel(payload.sequence_blocks[i]);
                sequenceBlockView = this.createSequenceBlockView(sequenceBlockModel);
                sequenceBlockView.show();
            }

        }
    };

    App.prototype = {

        /**
         * Registry of sequence blocks instances.
         * @var sequenceBlockViewRegistry
         * @type {Object}
         */
        sequenceBlockViewRegistry: {},

        /**
         * The application's status-message bar.
         * @var statusBar
         * @type {ilios.cim.widget.StatusBar}
         */
        statusBar: null,

        /**
         * A map of programs, keyed off by their program id.
         * @param programs
         * @type {Object}
         */
        programs: {},

        /**
         * A dialog widget for selecting and loading existing reports onto the page.
         * @param reportPickerDialog
         * @type {ilios.cim.widget.ReportPickerDialog}
         */
        reportPickerDialog: null,

        /**
         * A dialog widget for creating a new report.
         * @param createReportDialog
         * @type {ilios.cim.widget.CreateReportDialog}
         */
        createReportDialog: null,
        /**
         * @method getConfig
         * Returns the application configuration.
         * @return {Object] The configuration object.
        */
        getConfig: function () {
            return this.config;
        },

        /**
         * @method createSequenceBlockModel
         * Creates a sequence block model object from a given data transfer object representing a sequence block record.
         * @param {Object} oData The data transfer object.
         * @return {ilios.cim.model.SequenceBlockModel} The created model.
         */
        createSequenceBlockModel: function (oData) {
            // @todo add model to registry
            return new ilios.cim.model.SequenceBlockModel(oData);

        },
        /**
         * @method createSequenceBlockView
         * Generates, renders and returns a sequence block view for a given sequence block model.
         * @param {ilios.cim.model.SequenceBlockModel} model
         * @return {ilios.cim.view.SequenceBlockView}
         */
        createSequenceBlockView: function (model) {
            var parentId, id, parentEl, el, view;

            parentId = model.get('parentId');
            id = model.get('id');

            parentEl = parentId ?  document.getElementById('sequence-block-view-children-' + parentId) : document.getElementById('report-sequence-container');
            el = generateSequenceBlockMarkup(id);

            // attach the view element to it's parent in the document.
            parentEl.appendChild(el);

            view = new ilios.cim.view.SequenceBlockView(model, el, { cnumber: id });

            this.sequenceBlockViewRegistry[id] = view; // add the view to the registry so we can pull it up later
            view.render();

            return view;
        },
        /**
         * @method expandAllSequenceBlock
         * Expands all sequence block views.
         */
        expandAllSequenceBlocks: function () {
            var cnumber, view, registry;
            registry = this.sequenceBlockViewRegistry;
            for (cnumber in registry) {
                if (registry.hasOwnProperty(cnumber)) {
                    view = registry[cnumber];
                    view.expand();
                }
            }
        },

        /**
         * @method expandAllSequenceBlock
         * Collapses all sequence block views.
         */
        collapseAllSequenceBlocks: function () {
            var cnumber, view, registry;
            registry = this.sequenceBlockViewRegistry;
            for (cnumber in registry) {
                if (registry.hasOwnProperty(cnumber)) {
                    view = registry[cnumber];
                    view.collapse();
                }
            }
        }
    };

    /**
     * Provides functionality for exchanging data with the server-side backend.
     * All communication with the server will be handled asynchronous via XHR calls.
     * @namespace ilios.cim
     * @class App
     * @uses YAHOO.util.EventProvider
     * @constructor
     */
    var DataSource = function () {};

    DataSource.prototype = {
        /**
         * Fired when the report download request is sent to the server.
         * @event downloadStarted
         * @final
         */
        EVT_DOWNLOAD_STARTED: 'downloadStarted',

        /**
         * Fired when the report download response has been received from the server.
         * @event downloadFinished
         * @final
         */
        EVT_DOWNLOAD_FINISHED: 'downloadFinished',

        /**
         * Fired when the report download request is sent to the server.
         * @event exportStarted
         * @final
         */
        EVT_EXPORT_STARTED: 'exportStarted',

        /**
         * Fired when the report download response has been received from the server.
         * @event exportFinished
         * @final
         */
        EVT_EXPORT_FINISHED: 'exportFinished'
    };

    YAHOO.lang.augmentProto(DataSource, YAHOO.util.EventProvider);

    //
    // Utility methods.
    //

    /**
     * @method generateSequenceBlockMarkup
     * Generates the container markup for a sequence block View.
     * Note that data population and wiring of event handlers are not part of this.
     * @param {Number} cnumber The container number. Used as suffix when creating unique ID attributes for HTML elements
     *      within the container and of the container itself.
     * @returns {HTMLElement} The generated markup.
     * @static
     */
    var generateSequenceBlockMarkup = function (cnumber) {
        var rootEl, headerEl, bodyEl, rowEl, el;

        // the container element
        rootEl = document.createElement('div');
        Dom.setAttribute(rootEl, 'id', 'sequence-block-view-' + cnumber);
        Dom.addClass(rootEl, 'entity_container');
        Dom.addClass(rootEl, 'collapsed');
        Dom.addClass(rootEl, 'hidden');

        // header
        headerEl = rootEl.appendChild(document.createElement('div'));
        Dom.addClass(headerEl, 'hd');
        el = headerEl.appendChild(document.createElement('div'));
        Dom.addClass(el, 'toggle');
        Dom.setAttribute(el, 'id', 'sequence-block-view-toggle-btn-' + cnumber);
        el = headerEl.appendChild(document.createElement('div'));
        Dom.setAttribute(el, 'id', 'sequence-block-view-title-' + cnumber);
        Dom.addClass(el, 'collapsed_summary_text_div');
        el = headerEl.appendChild(document.createElement('div'));
        Dom.setAttribute(el, 'id', 'sequence-block-view-delete-button-' + cnumber);
        Dom.addClass(el, 'delete_widget');
        Dom.addClass(el, 'icon-cancel');

        // body
        bodyEl = rootEl.appendChild(document.createElement('div'));
        Dom.setAttribute(bodyEl, 'id', 'sequence-block-view-body-' + cnumber);
        Dom.addClass(bodyEl, 'bd');
        Dom.addClass(bodyEl, 'collapsible_container');
        Dom.addClass(bodyEl, 'hidden');
        rowEl = bodyEl.appendChild(document.createElement('div'));
        Dom.addClass(rowEl, 'row');
        el = rowEl.appendChild(document.createElement('div'));
        Dom.addClass(el, 'label');
        Dom.addClass(el, 'column');
        el.appendChild(document.createTextNode(ilios_i18nVendor.getI18NString('general.terms.description')));
        el = rowEl.appendChild(document.createElement('div'));
        Dom.setAttribute(el, 'id', 'sequence-block-view-description-' + cnumber);
        Dom.addClass(el, 'data');
        Dom.addClass(el, 'column');
        // ..
        // @todo implement the rest
        rowEl = bodyEl.appendChild(document.createElement('div'));
        Dom.addClass(rowEl, 'row');
        Dom.addClass(rowEl, 'sequence-block-children');
        Dom.setAttribute(rowEl, 'id', 'sequence-block-view-children-' + cnumber);
        return rootEl;
    };

    ilios.cim.DataSource = DataSource;
    ilios.cim.App = App;
}());
<commit_msg>created sequence block view registry object.<commit_after>/**
 * Curriculum inventory management (cim) application module components.
 *
 * Defines the following namespaces:
 *     ilios.cim
 *
 *  Dependencies:
 *     application/views/scripts/ilios_base.js
 *     ilios_i18nVendor
 *     YUI Dom/Event/Element libs
 *     YUI Container libs
 *     YUI Cookie lib
 *     application/views/curriculum_inventory/js/ilios.cim.model.js
 *     application/views/curriculum_inventory/js/ilios.cim.view.js
 *     application/views/curriculum_inventory/js/ilios.cim.widget.js
 */
(function () {

    ilios.namespace('cim');

    var Lang = YAHOO.lang,
        Event = YAHOO.util.Event,
        Dom = YAHOO.util.Dom;
    /**
     * Creates a client-side application.
     * It's the top level object responsible for instantiating a self-contained application environment
     * and for providing routing and high-level workflow management functionality.
     *
     * @namespace ilios.cim
     * @class App
     * @param {Object} config the application configuration.
     * @param {Object} [payload] The initial page payload. It may have these data points as properties:
     *     "programs" ... an object holding the programs available for reporting on.
     *     "report" ... (optional) an object representing the currently selected report.
     *     "courses" ... (optional) An lookup object (keyed off by course id) holding linked and linkable courses
     *         with/for sequence blocks in this report.
     *     "sequence" ... (optional) An object representing the report sequence.
     *     "sequence_blocks" ... (optional) An array of report sequence blocks.
     *     "academic_levels" ... (optional) An array of academic levels available in the given report.
     * @constructor
     * @todo way to much things going on in there for a single function. chunk it up into separate init functions.
     */
    var App = function (config, payload) {

        var i, n,
            sequenceBlockModel, sequenceBlockView;
        // set module configuration
        this.config = Lang.isObject(config) ? config : {};

        this.statusBar = new ilios.cim.widget.StatusBar({});
        this.statusBar.render('status-toolbar');

        this.programs = payload.programs;

        // wire dialogs to buttons
        Event.addListener('pick_reports_btn', 'click', function (event) {
            if (! this.reportPickerDialog) { // instantiate on demand
                this.reportPickerDialog = new ilios.cim.widget.ReportPickerDialog('report_picker_dialog');
            }
            this.reportPickerDialog.show();
            Event.stopEvent(event);
            return false;
        }, {}, this);

        Event.addListener('create_report_btn', 'click', function (event) {
            if (! this.createReportDialog) {
                this.createReportDialog = new ilios.cim.widget.CreateReportDialog('create_report_dialog', {}, this.programs);
            }
            this.createReportDialog.show();
            Event.stopEvent(event);
            return false;
        }, {}, this);

        if (payload.hasOwnProperty('report')) {

            // process data
            this.academicLevels = payload.academic_levels;
            this.sequenceBlock = payload.sequence_block;
            this.sequenceBlocks = payload.sequence_blocks;
            this.linkableCourses = payload.linkable_courses;
            this.linkedCourses = payload.linked_courses;
            this.reportModel = new ilios.cim.model.ReportModel(payload.report);

            // set up views and widgets

            this.reportView = new ilios.cim.view.ReportView(this.reportModel, {
                finalizeUrl: this.config.controllerUrl + 'finalize',
                deleteUrl: this.config.controllerUrl + 'delete'
            });
            this.reportView.subscribe('exportStarted', function() {
                this.show('Started Report Export …', true);
            }, this.statusBar, true);
            this.reportView.subscribe('downloadStarted', function() {
                this.show('Started Report Download …', true);
            }, this.statusBar, true);
            this.reportView.subscribe('exportFinished', function () {
                this.reset();
            }, this.statusBar, true);
            this.reportView.subscribe('downloadFinished', function () {
                this.reset();
            }, this.statusBar, true);
            this.reportModel.subscribe('afterUpdate', function () {
                this.show('Report updated.', false);
            }, this.statusBar, true);
            this.reportView.subscribe('finalizeStarted', function () {
                this.show('Finalizing report started …', true);
            }, this.statusBar, true);
            this.reportView.subscribe('finalizeSucceeded', function () {
                this.reset();
            }, this.statusBar, true);
            this.reportView.subscribe('finalizeFailed', function () {
                this.show('Finalizing report failed.', false);
            }, this.statusBar, true);
            this.reportView.subscribe('deleteStarted', function () {
                this.show('Deleting report …', true);
            }, this.statusBar, true);
            this.reportView.subscribe('deleteFailed', function () {
                this.show('Failed to delete report.', false);
            }, this.statusBar, true);
            this.reportView.subscribe('deleteSucceeded', function() {
                this.show('Successfully deleted report. Reloading page …', true);
                // reload the page
                window.location = window.location.protocol + "//" + window.location.host + window.location.pathname;

            }, this.statusBar, true);

            this.reportView.render();

            this.sequenceBlockTopToolbar = new ilios.cim.widget.SequenceBlockTopToolbar({});
            this.sequenceBlockTopToolbar.render();

            this.sequenceBlockBottomToolbar = new ilios.cim.widget.SequenceBlockBottomToolbar({});
            this.sequenceBlockBottomToolbar.render(! this.reportModel.get('isFinalized'));

            // more wiring of event handlers

            Event.addListener(this.sequenceBlockTopToolbar.get('expandBtnEl'), 'click', function (event) {
                this.expandAllSequenceBlocks();
            }, {}, this);

            Event.addListener(this.sequenceBlockTopToolbar.get('collapseBtnEl'), 'click', function (event) {
                this.collapseAllSequenceBlocks();
            }, {}, this);

            this.reportView.subscribe('finalizeSucceeded', function () {
                this.disableButtons();
            }, this.sequenceBlockBottomToolbar, true);

            // show views and widgets
            this.reportView.show();
            this.sequenceBlockBottomToolbar.show();
            this.sequenceBlockTopToolbar.show();

            // deal with sequence block models and views
            // @todo break this out into three steps
            // 1. create models
            // 2. create views from models
            // 3. show views
            for (i = 0, n = payload.sequence_blocks.length; i < n; i++) {
                sequenceBlockModel = this.createSequenceBlockModel(payload.sequence_blocks[i]);
                sequenceBlockView = this.createSequenceBlockView(sequenceBlockModel);
                sequenceBlockView.show();
            }

        }
    };

    App.prototype = {

        /**
         * Registry of sequence blocks instances.
         * @var sequenceBlockViewRegistry
         * @type {Object}
         */
        sequenceBlockViewRegistry: {},

        /**
         * The application's status-message bar.
         * @var statusBar
         * @type {ilios.cim.widget.StatusBar}
         */
        statusBar: null,

        /**
         * A map of programs, keyed off by their program id.
         * @param programs
         * @type {Object}
         */
        programs: {},

        /**
         * A dialog widget for selecting and loading existing reports onto the page.
         * @param reportPickerDialog
         * @type {ilios.cim.widget.ReportPickerDialog}
         */
        reportPickerDialog: null,

        /**
         * A dialog widget for creating a new report.
         * @param createReportDialog
         * @type {ilios.cim.widget.CreateReportDialog}
         */
        createReportDialog: null,
        /**
         * @method getConfig
         * Returns the application configuration.
         * @return {Object] The configuration object.
        */
        getConfig: function () {
            return this.config;
        },

        /**
         * @method createSequenceBlockModel
         * Creates a sequence block model object from a given data transfer object representing a sequence block record.
         * @param {Object} oData The data transfer object.
         * @return {ilios.cim.model.SequenceBlockModel} The created model.
         */
        createSequenceBlockModel: function (oData) {
            // @todo add model to registry
            return new ilios.cim.model.SequenceBlockModel(oData);

        },
        /**
         * @method createSequenceBlockView
         * Generates, renders and returns a sequence block view for a given sequence block model.
         * @param {ilios.cim.model.SequenceBlockModel} model
         * @return {ilios.cim.view.SequenceBlockView}
         */
        createSequenceBlockView: function (model) {
            var parentId, id, parentEl, el, view;

            parentId = model.get('parentId');
            id = model.get('id');

            parentEl = parentId ?  document.getElementById('sequence-block-view-children-' + parentId) : document.getElementById('report-sequence-container');
            el = generateSequenceBlockMarkup(id);

            // attach the view element to it's parent in the document.
            parentEl.appendChild(el);

            view = new ilios.cim.view.SequenceBlockView(model, el, { cnumber: id });

            this.sequenceBlockViewRegistry[id] = view; // add the view to the registry so we can pull it up later
            view.render();

            return view;
        },
        /**
         * @method expandAllSequenceBlock
         * Expands all sequence block views.
         */
        expandAllSequenceBlocks: function () {
            var cnumber, view, registry;
            registry = this.sequenceBlockViewRegistry;
            for (cnumber in registry) {
                if (registry.hasOwnProperty(cnumber)) {
                    view = registry[cnumber];
                    view.expand();
                }
            }
        },

        /**
         * @method expandAllSequenceBlock
         * Collapses all sequence block views.
         */
        collapseAllSequenceBlocks: function () {
            var cnumber, view, registry;
            registry = this.sequenceBlockViewRegistry;
            for (cnumber in registry) {
                if (registry.hasOwnProperty(cnumber)) {
                    view = registry[cnumber];
                    view.collapse();
                }
            }
        }
    };

    /**
     * Storage container of sequence block views within the application.
     * Keeps track of parent-child relationships between views.
     *
     * @namespace ilios.cim
     * @class SequenceBlockViewRegistry
     * @constructor
     */
    var SequenceBlockViewRegistry = function () {};

    SequenceBlockViewRegistry.protoype = {
        /**
         * The internal registry object.
         * @var _registry
         * @type {Object}
         * @protected
         */
        _registry: {},
        /**
         * @method add
         * @param {ilios.cim.view.SequenceBlockView} view
         * @return {Boolean} TRUE on success, FALSE on failure.
         */
        add: function (view) {
            // @todo implement
        },
        /**
         * @method remove
         * @param {Number} cnumber
         * @return {ilios.cim.view.SequenceBlockView} The removed view object.
         */
        remove: function (cnumber) {
            // @todo implement
        },

        /**
         * @method list
         * @return {Array}
         */
        list: function () {
            // @todo implement
        },

        /**
         * @method map
         * @param {Number} cnumber
         * @return {Object}
         */
        map: function (cnumber) {
            // @todo implement
        },

        /**
         * @method children
         * @param {Number} cnumber
         * @return {Array}
         */
        children: function (cnumber) {
            // @todo implement
        },

        /**
         * @method exists
         * @param {Number} cnumber
         * @return {Boolean}
         */
        exists: function (cnumber) {
            // @todo implement
        }
    };

    /**
     * Provides functionality for exchanging data with the server-side backend.
     * All communication with the server will be handled asynchronous via XHR calls.
     * @namespace ilios.cim
     * @class App
     * @uses YAHOO.util.EventProvider
     * @constructor
     */
    var DataSource = function () {

        // create custom events provided by this object
        this.createEvent(DataSource.EVT_DOWNLOAD_STARTED);
        this.createEvent(DataSource.EVT_DOWNLOAD_FINISHED);
        this.createEvent(DataSource.EVT_EXPORT_STARTED);
        this.createEvent(DataSource.EVT_EXPORT_FINISHED);

    };

    DataSource.prototype = {
        /**
         * Fired when the report download request is sent to the server.
         * @event downloadStarted
         * @final
         */
        EVT_DOWNLOAD_STARTED: 'downloadStarted',

        /**
         * Fired when the report download response has been received from the server.
         * @event downloadFinished
         * @final
         */
        EVT_DOWNLOAD_FINISHED: 'downloadFinished',

        /**
         * Fired when the report download request is sent to the server.
         * @event exportStarted
         * @final
         */
        EVT_EXPORT_STARTED: 'exportStarted',

        /**
         * Fired when the report download response has been received from the server.
         * @event exportFinished
         * @final
         */
        EVT_EXPORT_FINISHED: 'exportFinished'
    };

    YAHOO.lang.augmentProto(DataSource, YAHOO.util.EventProvider);

    //
    // Utility methods.
    //

    /**
     * @method generateSequenceBlockMarkup
     * Generates the container markup for a sequence block View.
     * Note that data population and wiring of event handlers are not part of this.
     * @param {Number} cnumber The container number. Used as suffix when creating unique ID attributes for HTML elements
     *      within the container and of the container itself.
     * @returns {HTMLElement} The generated markup.
     * @static
     */
    var generateSequenceBlockMarkup = function (cnumber) {
        var rootEl, headerEl, bodyEl, rowEl, el;

        // the container element
        rootEl = document.createElement('div');
        Dom.setAttribute(rootEl, 'id', 'sequence-block-view-' + cnumber);
        Dom.addClass(rootEl, 'entity_container');
        Dom.addClass(rootEl, 'collapsed');
        Dom.addClass(rootEl, 'hidden');

        // header
        headerEl = rootEl.appendChild(document.createElement('div'));
        Dom.addClass(headerEl, 'hd');
        el = headerEl.appendChild(document.createElement('div'));
        Dom.addClass(el, 'toggle');
        Dom.setAttribute(el, 'id', 'sequence-block-view-toggle-btn-' + cnumber);
        el = headerEl.appendChild(document.createElement('div'));
        Dom.setAttribute(el, 'id', 'sequence-block-view-title-' + cnumber);
        Dom.addClass(el, 'collapsed_summary_text_div');
        el = headerEl.appendChild(document.createElement('div'));
        Dom.setAttribute(el, 'id', 'sequence-block-view-delete-button-' + cnumber);
        Dom.addClass(el, 'delete_widget');
        Dom.addClass(el, 'icon-cancel');

        // body
        bodyEl = rootEl.appendChild(document.createElement('div'));
        Dom.setAttribute(bodyEl, 'id', 'sequence-block-view-body-' + cnumber);
        Dom.addClass(bodyEl, 'bd');
        Dom.addClass(bodyEl, 'collapsible_container');
        Dom.addClass(bodyEl, 'hidden');
        rowEl = bodyEl.appendChild(document.createElement('div'));
        Dom.addClass(rowEl, 'row');
        el = rowEl.appendChild(document.createElement('div'));
        Dom.addClass(el, 'label');
        Dom.addClass(el, 'column');
        el.appendChild(document.createTextNode(ilios_i18nVendor.getI18NString('general.terms.description')));
        el = rowEl.appendChild(document.createElement('div'));
        Dom.setAttribute(el, 'id', 'sequence-block-view-description-' + cnumber);
        Dom.addClass(el, 'data');
        Dom.addClass(el, 'column');
        // ..
        // @todo implement the rest
        rowEl = bodyEl.appendChild(document.createElement('div'));
        Dom.addClass(rowEl, 'row');
        Dom.addClass(rowEl, 'sequence-block-children');
        Dom.setAttribute(rowEl, 'id', 'sequence-block-view-children-' + cnumber);
        return rootEl;
    };

    ilios.cim.SequenceBlockViewRegistry = SequenceBlockViewRegistry;
    ilios.cim.DataSource = DataSource;
    ilios.cim.App = App;
}());
<|endoftext|>


<commit_before>import React from 'react'
import PropTypes from 'prop-types'
import { QueryRenderer } from 'react-relay'
import graphql from 'babel-plugin-relay/macro'
import { get } from 'lodash/object'
import environment from 'js/relay-env'
import AuthenticationContainer from 'js/components/Authentication/AuthenticationContainer'
import { createNewUser } from 'js/authentication/helpers'
import { ERROR_USER_DOES_NOT_EXIST } from 'js/constants'
import logger from 'js/utils/logger'
import withUser from 'js/components/General/withUser'

// Fetch the user from our database if the user is
// authenticated.
class AuthenticationView extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      refetchCounter: 0,
    }
    this.createNewUserAttempts = 0
  }

  // If we have an authed user with a user ID, fetch the
  // user from our database.
  // This will force Relay to refetch by modifying the
  // variables object. We could also use the `retry`
  // QueryRenderer function instead.
  // Pass this to children to allow a forced refetch after
  // we create a new user in our database.
  async fetchUser() {
    const { refetchCounter } = this.state
    this.setState({
      refetchCounter: refetchCounter + 1,
    })
  }

  render() {
    const { authUser } = this.props
    const { refetchCounter } = this.state
    const userId = authUser && authUser.id ? authUser.id : null
    return (
      <QueryRenderer
        environment={environment}
        query={
          userId
            ? graphql`
                query AuthenticationViewQuery($userId: String!) {
                  user(userId: $userId) {
                    ...AuthenticationContainer_user
                  }
                }
              `
            : undefined
        }
        variables={
          userId
            ? {
                userId: userId,
                refetchCounter: refetchCounter,
              }
            : {}
        }
        render={({ error, props }) => {
          if (error && get(error, 'source.errors')) {
            // If any of the errors is because the user does not exist
            // on the server side, create the user and re-query if
            // possible.
            const userDoesNotExistError = error.source.errors.some(
              err => err.code === ERROR_USER_DOES_NOT_EXIST
            )

            // Limit the number of times we try to refetch a user after
            // trying to create the user to prevent a loop in case of
            // any bugs.
            if (userDoesNotExistError && this.createNewUserAttempts < 3) {
              this.createNewUserAttempts = this.createNewUserAttempts + 1
              createNewUser()
                .then(user => {
                  this.fetchUser()
                })
                .catch(e => {
                  throw e
                })
            } else {
              logger.error(error)
            }
          }

          // Wait for the query to return to render content.
          if (!props) {
            return null
          }
          const user = props.user || null
          return (
            <AuthenticationContainer
              user={user}
              fetchUser={this.fetchUser.bind(this)}
              {...this.props}
            />
          )
        }}
      />
    )
  }
}

AuthenticationView.propTypes = {
  authUser: PropTypes.shape({
    id: PropTypes.string.isRequired,
  }),
}

AuthenticationView.defaultProps = {}

export default withUser({
  renderIfNoUser: true,
  redirectToAuthIfIncomplete: false,
})(AuthenticationView)
<commit_msg>Add FIXME note<commit_after>import React from 'react'
import PropTypes from 'prop-types'
import { QueryRenderer } from 'react-relay'
import graphql from 'babel-plugin-relay/macro'
import { get } from 'lodash/object'
import environment from 'js/relay-env'
import AuthenticationContainer from 'js/components/Authentication/AuthenticationContainer'
import { createNewUser } from 'js/authentication/helpers'
import { ERROR_USER_DOES_NOT_EXIST } from 'js/constants'
import logger from 'js/utils/logger'
import withUser from 'js/components/General/withUser'

// Fetch the user from our database if the user is
// authenticated.
class AuthenticationView extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      refetchCounter: 0,
    }
    this.createNewUserAttempts = 0
  }

  // If we have an authed user with a user ID, fetch the
  // user from our database.
  // This will force Relay to refetch by modifying the
  // variables object. We could also use the `retry`
  // QueryRenderer function instead.
  // Pass this to children to allow a forced refetch after
  // we create a new user in our database.
  async fetchUser() {
    const { refetchCounter } = this.state
    this.setState({
      refetchCounter: refetchCounter + 1,
    })
  }

  render() {
    const { authUser } = this.props
    const { refetchCounter } = this.state
    const userId = authUser && authUser.id ? authUser.id : null
    return (
      <QueryRenderer
        environment={environment}
        query={
          userId
            ? graphql`
                query AuthenticationViewQuery($userId: String!) {
                  user(userId: $userId) {
                    ...AuthenticationContainer_user
                  }
                }
              `
            : undefined
        }
        variables={
          userId
            ? {
                userId: userId,
                refetchCounter: refetchCounter,
              }
            : {}
        }
        render={({ error, props }) => {
          if (error && get(error, 'source.errors')) {
            // If any of the errors is because the user does not exist
            // on the server side, create the user and re-query if
            // possible.
            const userDoesNotExistError = error.source.errors.some(
              err => err.code === ERROR_USER_DOES_NOT_EXIST
            )

            // Limit the number of times we try to refetch a user after
            // trying to create the user to prevent a loop in case of
            // any bugs.
            if (userDoesNotExistError && this.createNewUserAttempts < 3) {
              this.createNewUserAttempts = this.createNewUserAttempts + 1
              createNewUser()
                .then(user => {
                  // FIXME: cancel this on unmount
                  this.fetchUser()
                })
                .catch(e => {
                  throw e
                })
            } else {
              logger.error(error)
            }
          }

          // Wait for the query to return to render content.
          if (!props) {
            return null
          }
          const user = props.user || null
          return (
            <AuthenticationContainer
              user={user}
              fetchUser={this.fetchUser.bind(this)}
              {...this.props}
            />
          )
        }}
      />
    )
  }
}

AuthenticationView.propTypes = {
  authUser: PropTypes.shape({
    id: PropTypes.string.isRequired,
  }),
}

AuthenticationView.defaultProps = {}

export default withUser({
  renderIfNoUser: true,
  redirectToAuthIfIncomplete: false,
})(AuthenticationView)
<|endoftext|>


<commit_before>
import React from 'react'
import PropTypes from 'prop-types'
import moment from 'moment'
import HeartIcon from 'material-ui/svg-icons/action/favorite'
// import HeartBorderIcon from 'material-ui/svg-icons/action/favorite-border'
import RaisedButton from 'material-ui/RaisedButton'
import TextField from 'material-ui-next/TextField'
import { withStyles } from 'material-ui-next/styles'
import appTheme, {
  alternateAccentColor,
  lighterTextColor
} from 'theme/default'
import Logo from '../Logo/Logo'

const styles = theme => ({
  inputInkbar: {
    '&:after': {
      backgroundColor: alternateAccentColor
    }
  },
  inputLabelFocused: {
    color: alternateAccentColor
  }
})

// Note: also hardcoded in StickerCampaignContainer
export const CAMPAIGN_START_TIME_ISO = '2018-02-13T21:00:00.000Z'
export const CAMPAIGN_END_TIME_ISO = '2018-02-22T20:00:00.000Z'

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

    this.state = {
      countdownClock: this.getCountdownClockText()
    }
  }

  getCountdownClockText () {
    const campaignEndDatetime = moment(CAMPAIGN_END_TIME_ISO)
    var timeRemaining = campaignEndDatetime.diff(moment.utc(), 'milliseconds')
    if (timeRemaining < 0) {
      timeRemaining = 0
    }
    const duration = moment.duration(timeRemaining, 'milliseconds')
    return `${duration.days()}d ${duration.hours()}h
      ${duration.minutes()}m ${duration.seconds()}s`
  }

  componentWillMount () {
    const self = this

    // Every second, update the countdown clock
    this.timeRemainingInterval = setInterval(() => {
      self.setState({
        countdownClock: self.getCountdownClockText()
      })
    }, 1000)
  }

  componentWillUnmount () {
    clearInterval(this.timeRemainingInterval)
  }

  render () {
    return {this.state.countdownClock}
  }
}

class StickerCampaign extends React.Component {
  onTextFieldClicked () {
    this.input.select()
  }

  getReferralUrl () {
    return `https://tab.gladly.io/?u=${this.props.user.username}`
  }

  render () {
    const { user, classes } = this.props
    const recruitsActiveForLessThanOneDay = (user.recruits.totalRecruits -
      user.recruits.recruitsActiveForAtLeastOneDay)
    const friendText = recruitsActiveForLessThanOneDay === 1 ? 'friend' : 'friends'
    const referralUrl = this.getReferralUrl()
    const anchorStyle = {
      textDecoration: 'none',
      color: alternateAccentColor
    }

    // Heart icon style
    const successHeartColor = 'red'
    const incompleteHeartColor = 'grey'
    const heartStyle = {
      height: 42,
      width: 42
    }

    return (

            <Logo style={{ height: 30, marginRight: 8 }} />
            Spread the love, get some love!

          Get 2 friends using Tab for a Cause for at least 1 day,
          and
          we'll send you some laptop stickers. Be one of the top recruiters and
           have your pick of Tab gear!

          {
            (user.recruits.recruitsActiveForAtLeastOneDay > 0)
            ? (
              <HeartIcon
                color={successHeartColor}
                style={heartStyle}
              />
            )
            : (
              <HeartIcon
                color={incompleteHeartColor}
                style={heartStyle}
              />
            )
          }
          {
            (user.recruits.recruitsActiveForAtLeastOneDay > 1)
            ? (
              <HeartIcon
                color={successHeartColor}
                style={heartStyle}
              />
            )
            : (
              <HeartIcon
                color={incompleteHeartColor}
                style={heartStyle}
              />
            )
          }

            {user.recruits.recruitsActiveForAtLeastOneDay}/2 Tabbers recruited

            {recruitsActiveForLessThanOneDay} {friendText} active for <1 day

           {
            (user.recruits.recruitsActiveForAtLeastOneDay >= 2)
            ? (

                  Congrats, you did it! (And thanks!) Tell us where we should send your prize:

                  <RaisedButton
                    label='Claim Your Stickers'
                    style={{
                      display: 'block',
                      marginTop: 8
                    }}
                    primary
                  />

            )
            : (

                <TextField
                  id={'sticker-campaign-refer-friend-input'}
                  inputRef={(input) => { this.input = input }}
                  onClick={this.onTextFieldClicked.bind(this)}
                  value={referralUrl}
                  label={'Send this to a few friends:'}
                  fullWidth
                  InputProps={{
                    classes: {
                      inkbar: classes.inputInkbar
                    }
                  }}
                  inputProps={{
                    style: {
                      fontSize: 14,
                      textAlign: 'left'
                    }
                  }}
                  InputLabelProps={{
                    FormControlClasses: {
                      focused: classes.inputLabelFocused
                    }
                  }}
                />

            )
          }

            <CountdownClock /> remaining

    )
  }
}

StickerCampaign.propTypes = {
  user: PropTypes.shape({
    username: PropTypes.string.isRequired,
    recruits: PropTypes.shape({
      totalRecruits: PropTypes.number,
      recruitsActiveForAtLeastOneDay: PropTypes.number
    }).isRequired
  }),
  isCampaignLive: PropTypes.bool,
  classes: PropTypes.object.isRequired
}

StickerCampaign.defautProps = {
  isCampaignLive: false
}

export default withStyles(styles)(StickerCampaign)
<commit_msg>Remove unused prop<commit_after>
import React from 'react'
import PropTypes from 'prop-types'
import moment from 'moment'
import HeartIcon from 'material-ui/svg-icons/action/favorite'
// import HeartBorderIcon from 'material-ui/svg-icons/action/favorite-border'
import RaisedButton from 'material-ui/RaisedButton'
import TextField from 'material-ui-next/TextField'
import { withStyles } from 'material-ui-next/styles'
import appTheme, {
  alternateAccentColor,
  lighterTextColor
} from 'theme/default'
import Logo from '../Logo/Logo'

const styles = theme => ({
  inputInkbar: {
    '&:after': {
      backgroundColor: alternateAccentColor
    }
  },
  inputLabelFocused: {
    color: alternateAccentColor
  }
})

// Note: also hardcoded in StickerCampaignContainer
export const CAMPAIGN_START_TIME_ISO = '2018-02-13T21:00:00.000Z'
export const CAMPAIGN_END_TIME_ISO = '2018-02-22T20:00:00.000Z'

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

    this.state = {
      countdownClock: this.getCountdownClockText()
    }
  }

  getCountdownClockText () {
    const campaignEndDatetime = moment(CAMPAIGN_END_TIME_ISO)
    var timeRemaining = campaignEndDatetime.diff(moment.utc(), 'milliseconds')
    if (timeRemaining < 0) {
      timeRemaining = 0
    }
    const duration = moment.duration(timeRemaining, 'milliseconds')
    return `${duration.days()}d ${duration.hours()}h
      ${duration.minutes()}m ${duration.seconds()}s`
  }

  componentWillMount () {
    const self = this

    // Every second, update the countdown clock
    this.timeRemainingInterval = setInterval(() => {
      self.setState({
        countdownClock: self.getCountdownClockText()
      })
    }, 1000)
  }

  componentWillUnmount () {
    clearInterval(this.timeRemainingInterval)
  }

  render () {
    return {this.state.countdownClock}
  }
}

class StickerCampaign extends React.Component {
  onTextFieldClicked () {
    this.input.select()
  }

  getReferralUrl () {
    return `https://tab.gladly.io/?u=${this.props.user.username}`
  }

  render () {
    const { user, classes } = this.props
    const recruitsActiveForLessThanOneDay = (user.recruits.totalRecruits -
      user.recruits.recruitsActiveForAtLeastOneDay)
    const friendText = recruitsActiveForLessThanOneDay === 1 ? 'friend' : 'friends'
    const referralUrl = this.getReferralUrl()
    const anchorStyle = {
      textDecoration: 'none',
      color: alternateAccentColor
    }

    // Heart icon style
    const successHeartColor = 'red'
    const incompleteHeartColor = 'grey'
    const heartStyle = {
      height: 42,
      width: 42
    }

    return (

            <Logo style={{ height: 30, marginRight: 8 }} />
            Spread the love, get some love!

          Get 2 friends using Tab for a Cause for at least 1 day,
          and
          we'll send you some laptop stickers. Be one of the top recruiters and
           have your pick of Tab gear!

          {
            (user.recruits.recruitsActiveForAtLeastOneDay > 0)
            ? (
              <HeartIcon
                color={successHeartColor}
                style={heartStyle}
              />
            )
            : (
              <HeartIcon
                color={incompleteHeartColor}
                style={heartStyle}
              />
            )
          }
          {
            (user.recruits.recruitsActiveForAtLeastOneDay > 1)
            ? (
              <HeartIcon
                color={successHeartColor}
                style={heartStyle}
              />
            )
            : (
              <HeartIcon
                color={incompleteHeartColor}
                style={heartStyle}
              />
            )
          }

            {user.recruits.recruitsActiveForAtLeastOneDay}/2 Tabbers recruited

            {recruitsActiveForLessThanOneDay} {friendText} active for <1 day

           {
            (user.recruits.recruitsActiveForAtLeastOneDay >= 2)
            ? (

                  Congrats, you did it! (And thanks!) Tell us where we should send your prize:

                  <RaisedButton
                    label='Claim Your Stickers'
                    style={{
                      display: 'block',
                      marginTop: 8
                    }}
                    primary
                  />

            )
            : (

                <TextField
                  id={'sticker-campaign-refer-friend-input'}
                  inputRef={(input) => { this.input = input }}
                  onClick={this.onTextFieldClicked.bind(this)}
                  value={referralUrl}
                  label={'Send this to a few friends:'}
                  fullWidth
                  InputProps={{
                    classes: {
                      inkbar: classes.inputInkbar
                    }
                  }}
                  inputProps={{
                    style: {
                      fontSize: 14,
                      textAlign: 'left'
                    }
                  }}
                  InputLabelProps={{
                    FormControlClasses: {
                      focused: classes.inputLabelFocused
                    }
                  }}
                />

            )
          }

            <CountdownClock /> remaining

    )
  }
}

StickerCampaign.propTypes = {
  user: PropTypes.shape({
    username: PropTypes.string.isRequired,
    recruits: PropTypes.shape({
      totalRecruits: PropTypes.number,
      recruitsActiveForAtLeastOneDay: PropTypes.number
    }).isRequired
  }),
  classes: PropTypes.object.isRequired
}

export default withStyles(styles)(StickerCampaign)
<|endoftext|>


<commit_before>
import React from 'react'
import PropTypes from 'prop-types'
import moment from 'moment'
import RaisedButton from 'material-ui/RaisedButton'
import TextField from 'material-ui-next/TextField'
import { withStyles } from 'material-ui-next/styles'
import appTheme, {
  alternateAccentColor,
  lighterTextColor
} from 'theme/default'
import Logo from '../Logo/Logo'

const styles = theme => ({
  inputInkbar: {
    '&:after': {
      backgroundColor: alternateAccentColor
    }
  },
  inputLabelFocused: {
    color: alternateAccentColor
  }
})

// Note: also hardcoded in StickerCampaignContainer
export const CAMPAIGN_START_TIME_ISO = '2018-02-13T21:00:00.000Z'
export const CAMPAIGN_END_TIME_ISO = '2018-02-22T20:00:00.000Z'

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

    this.state = {
      countdownClock: this.getCountdownClockText()
    }
  }

  getCountdownClockText () {
    const campaignEndDatetime = moment(CAMPAIGN_END_TIME_ISO)
    var timeRemaining = campaignEndDatetime.diff(moment.utc(), 'milliseconds')
    if (timeRemaining < 0) {
      timeRemaining = 0
    }
    const duration = moment.duration(timeRemaining, 'milliseconds')
    return `${duration.days()}d ${duration.hours()}h
      ${duration.minutes()}m ${duration.seconds()}s`
  }

  componentWillMount () {
    const self = this

    // Every second, update the countdown clock
    this.timeRemainingInterval = setInterval(() => {
      self.setState({
        countdownClock: self.getCountdownClockText()
      })
    }, 1000)
  }

  componentWillUnmount () {
    clearInterval(this.timeRemainingInterval)
  }

  render () {
    return {this.state.countdownClock}
  }
}

class StickerCampaign extends React.Component {
  onTextFieldClicked () {
    this.input.select()
  }

  getReferralUrl () {
    return `https://tab.gladly.io/?u=${this.props.user.username}`
  }

  render () {
    const { user, classes } = this.props
    const recruitsActiveForLessThanOneDay = (user.recruits.totalRecruits -
      user.recruits.recruitsActiveForAtLeastOneDay)
    const referralUrl = this.getReferralUrl()
    const anchorStyle = {
      textDecoration: 'none',
      color: alternateAccentColor
    }
    return (

            <Logo style={{ height: 30, marginRight: 8 }} />
            Spread the love, get some love!

          Get 2 friends using Tab for a Cause for at least 1 day,
          and
          we'll send you some laptop stickers. Be one of the top recruiters and
           have your pick of Tab gear!

            {user.recruits.recruitsActiveForAtLeastOneDay}/2 Tabbers recruited

            {recruitsActiveForLessThanOneDay} friends active for <1 day

           {
            (user.recruits.recruitsActiveForAtLeastOneDay >= 2)
            ? (

                  Congrats, you did it! (And thanks!) Tell us where we should send your prize:

                  <RaisedButton
                    label='Claim Your Stickers'
                    style={{
                      display: 'block',
                      marginTop: 8
                    }}
                    primary
                  />

            )
            : (

                <TextField
                  id={'sticker-campaign-refer-friend-input'}
                  inputRef={(input) => { this.input = input }}
                  onClick={this.onTextFieldClicked.bind(this)}
                  value={referralUrl}
                  label={'Send this to a few friends:'}
                  fullWidth
                  InputProps={{
                    classes: {
                      inkbar: classes.inputInkbar
                    }
                  }}
                  inputProps={{
                    style: {
                      fontSize: 14,
                      textAlign: 'left'
                    }
                  }}
                  InputLabelProps={{
                    FormControlClasses: {
                      focused: classes.inputLabelFocused
                    }
                  }}
                />

            )
          }

            <CountdownClock /> remaining

    )
  }
}

StickerCampaign.propTypes = {
  user: PropTypes.shape({
    username: PropTypes.string.isRequired,
    recruits: PropTypes.shape({
      totalRecruits: PropTypes.number,
      recruitsActiveForAtLeastOneDay: PropTypes.number
    }).isRequired
  }),
  isCampaignLive: PropTypes.bool,
  classes: PropTypes.object.isRequired
}

StickerCampaign.defautProps = {
  isCampaignLive: false
}

export default withStyles(styles)(StickerCampaign)
<commit_msg>Add heart icons visualization of recruits<commit_after>
import React from 'react'
import PropTypes from 'prop-types'
import moment from 'moment'
import HeartIcon from 'material-ui/svg-icons/action/favorite'
// import HeartBorderIcon from 'material-ui/svg-icons/action/favorite-border'
import RaisedButton from 'material-ui/RaisedButton'
import TextField from 'material-ui-next/TextField'
import { withStyles } from 'material-ui-next/styles'
import appTheme, {
  alternateAccentColor,
  lighterTextColor
} from 'theme/default'
import Logo from '../Logo/Logo'

const styles = theme => ({
  inputInkbar: {
    '&:after': {
      backgroundColor: alternateAccentColor
    }
  },
  inputLabelFocused: {
    color: alternateAccentColor
  }
})

// Note: also hardcoded in StickerCampaignContainer
export const CAMPAIGN_START_TIME_ISO = '2018-02-13T21:00:00.000Z'
export const CAMPAIGN_END_TIME_ISO = '2018-02-22T20:00:00.000Z'

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

    this.state = {
      countdownClock: this.getCountdownClockText()
    }
  }

  getCountdownClockText () {
    const campaignEndDatetime = moment(CAMPAIGN_END_TIME_ISO)
    var timeRemaining = campaignEndDatetime.diff(moment.utc(), 'milliseconds')
    if (timeRemaining < 0) {
      timeRemaining = 0
    }
    const duration = moment.duration(timeRemaining, 'milliseconds')
    return `${duration.days()}d ${duration.hours()}h
      ${duration.minutes()}m ${duration.seconds()}s`
  }

  componentWillMount () {
    const self = this

    // Every second, update the countdown clock
    this.timeRemainingInterval = setInterval(() => {
      self.setState({
        countdownClock: self.getCountdownClockText()
      })
    }, 1000)
  }

  componentWillUnmount () {
    clearInterval(this.timeRemainingInterval)
  }

  render () {
    return {this.state.countdownClock}
  }
}

class StickerCampaign extends React.Component {
  onTextFieldClicked () {
    this.input.select()
  }

  getReferralUrl () {
    return `https://tab.gladly.io/?u=${this.props.user.username}`
  }

  render () {
    const { user, classes } = this.props
    const recruitsActiveForLessThanOneDay = (user.recruits.totalRecruits -
      user.recruits.recruitsActiveForAtLeastOneDay)
    const referralUrl = this.getReferralUrl()
    const anchorStyle = {
      textDecoration: 'none',
      color: alternateAccentColor
    }

    // Heart icon style
    const successHeartColor = 'red'
    const incompleteHeartColor = 'grey'
    const heartStyle = {
      height: 42,
      width: 42
    }

    return (

            <Logo style={{ height: 30, marginRight: 8 }} />
            Spread the love, get some love!

          Get 2 friends using Tab for a Cause for at least 1 day,
          and
          we'll send you some laptop stickers. Be one of the top recruiters and
           have your pick of Tab gear!

          {
            (user.recruits.recruitsActiveForAtLeastOneDay > 0)
            ? (
              <HeartIcon
                color={successHeartColor}
                style={heartStyle}
              />
            )
            : (
              <HeartIcon
                color={incompleteHeartColor}
                style={heartStyle}
              />
            )
          }
          {
            (user.recruits.recruitsActiveForAtLeastOneDay > 1)
            ? (
              <HeartIcon
                color={successHeartColor}
                style={heartStyle}
              />
            )
            : (
              <HeartIcon
                color={incompleteHeartColor}
                style={heartStyle}
              />
            )
          }

            {user.recruits.recruitsActiveForAtLeastOneDay}/2 Tabbers recruited

            {recruitsActiveForLessThanOneDay} friends active for <1 day

           {
            (user.recruits.recruitsActiveForAtLeastOneDay >= 2)
            ? (

                  Congrats, you did it! (And thanks!) Tell us where we should send your prize:

                  <RaisedButton
                    label='Claim Your Stickers'
                    style={{
                      display: 'block',
                      marginTop: 8
                    }}
                    primary
                  />

            )
            : (

                <TextField
                  id={'sticker-campaign-refer-friend-input'}
                  inputRef={(input) => { this.input = input }}
                  onClick={this.onTextFieldClicked.bind(this)}
                  value={referralUrl}
                  label={'Send this to a few friends:'}
                  fullWidth
                  InputProps={{
                    classes: {
                      inkbar: classes.inputInkbar
                    }
                  }}
                  inputProps={{
                    style: {
                      fontSize: 14,
                      textAlign: 'left'
                    }
                  }}
                  InputLabelProps={{
                    FormControlClasses: {
                      focused: classes.inputLabelFocused
                    }
                  }}
                />

            )
          }

            <CountdownClock /> remaining

    )
  }
}

StickerCampaign.propTypes = {
  user: PropTypes.shape({
    username: PropTypes.string.isRequired,
    recruits: PropTypes.shape({
      totalRecruits: PropTypes.number,
      recruitsActiveForAtLeastOneDay: PropTypes.number
    }).isRequired
  }),
  isCampaignLive: PropTypes.bool,
  classes: PropTypes.object.isRequired
}

StickerCampaign.defautProps = {
  isCampaignLive: false
}

export default withStyles(styles)(StickerCampaign)
<|endoftext|>


<commit_before><commit_msg>Added disptacherRealtime function to the dispatcher Module<commit_after>// Import Modules
const INDEX = require('../../index.js')

// Import Packages
const _ = require('lodash')

// Define helper variables
const ws = INDEX.connection

/**
 * Function to dispatch event whenever a document is updated on MongoDB.
 * @param  {String} type      Type of action. e.g. Update, New, Delete, Fetch
 * @param  {String} model     Mongoose model the function was dispatched from
 * @param  {String} _id       ID of the documento to perform publish
 * @param  {Object} [data={}] Updated data to send
 */
const dispatchRealtime = (type, model, _id, data = {}) => {
  // Builds event string
  const event = _.compact(['connapp', model, type, _id]).join('.')

  // Adds extra info to the data, so the App can update the right document
  const data.meta = {
    type: type,
    model: model,
    _id: _id
  }

  // Publish the event
  connection.publish(event, data)
}

// Exports
module.exports = {
  dispatcher: dispatcher
}
<|endoftext|>


<commit_before>/**
 * Created by ksinger on 23.09.2015.
 *
 * Contains a list of actions to be used with the dispatcher and documentation for their expected payloads.
 */
import { tree2constants, deepFreeze, reduceAndMapTreeKeys, flattenTree } from './utils';
import './service/won';

export const actionTypes = tree2constants({

    /* actions received as responses or push notifications */
    received: {
        /* contains all user-bound data, e.g. ownedPosts,
         * drafts, messages,...
         * This action will likely be caused as a consequence of signing in.
         */
        userData : null
    },
    drafts: {
        /*
         * A new draft was created (either through the view in this client or on another browser)
         */
        new: null,
        /*
         * A draft has changed. Pass along the draftURI and the respective data.
         */
        change: {
            type: null,
            title: null,
            thumbnail: null,
        },
        delete: null,
        select: null,
        publish: null,
        notifyOfSuccessfulPublish: null //triggered by server
    },
    moreWub: null
});

/**
 * actionCreators are functions that take the payload and output
 * an action object, thus prebinding the action-type.
 * This object follows the structure of the actionTypes-object,
 * but is flattened for use with ng-redux. Thus calling
 * `$ngRedux.dispatch(actionCreators.drafts__new(myDraft))` will trigger an action
 * `{type: actionTypes.drafts.new, payload: myDraft}`
 *
 * e.g.:
 *
 * ```javascript
 * function newDraft(draft) {
 *   return { type: 'draft.new', payload: draft }
 * }
 * ```
 */
export const actionCreators = flattenTree(
        reduceAndMapTreeKeys(
            (acc, k) => acc.concat(k), //construct paths, e.g. ['draft', 'new']
            (acc) => createActionCreator(won.lookup(actionTypes, acc)), //lookup constant at path
            [], actionTypes
        ),
    '__');

/*
 * This action creator uses thunk (https://github.com/gaearon/redux-thunk) which
 * allows using it with a normal dispatch(actionCreator(payload)) even though
 *  it does asynchronous calls. This is a requirement for using it with
 *  $ngRedux.connect(..., actionCreators, ...)
 */
actionCreators.delayedWub = (nrOfWubs, milliseconds = 1000) => (dispatch) =>
    delay(milliseconds).then(
        args => dispatch(actionCreators.moreWub(nrOfWubs)),
        error => console.err('actions.js: Error while delaying for delayed Wub.')
    );

import { stateGo, stateReload, stateTransitionTo } from 'redux-ui-router';
actionCreators.router__stateGo = stateGo;
actionCreators.router__stateReload = stateReload;
actionCreators.router__stateTransitionTo = stateTransitionTo;

window.delay = delay;
function delay(milliseconds) {
    return new Promise((resolve, reject) =>
        window.setTimeout(() => resolve(), milliseconds)
    );
}

function createActionCreator(type) {
    return (payload) => {
        console.log('creating instance of actionType ', type, ' with payload: ', payload);
        return {type, payload};
    };
}

/*
 * NOTE: Add any custom action-creators here!
 */

Object.freeze(actionCreators); //to make sure it's not modified elsewhere

/*
 * TODO deletme; for debugging
 */
window.actionCreators = actionCreators;
window.actionTypes = actionTypes;

<commit_msg>Refactored action creation so all actions are visible in the json-object.<commit_after>/**
 * Created by ksinger on 23.09.2015.
 *
 * Contains a list of actions to be used with the dispatcher and documentation for their expected payloads.
 */
import { tree2constants, deepFreeze, reduceAndMapTreeKeys, flattenTree } from './utils';
import './service/won';

import { stateGo, stateReload, stateTransitionTo } from 'redux-ui-router';

const injDefault = 'INJECT_DEFAULT_ACTION_CREATOR';
const actionHierarchy = {
    /* actions received as responses or push notifications */
    received: {
        /* contains all user-bound data, e.g. ownedPosts,
         * drafts, messages,...
         * This action will likely be caused as a consequence of signing in.
         */
        userData : injDefault
    },
    drafts: {
        /*
         * A new draft was created (either through the view in this client or on another browser)
         */
        new: injDefault,
        /*
         * A draft has changed. Pass along the draftURI and the respective data.
         */
        change: {
            type: injDefault,
            title: injDefault,
            thumbnail: injDefault,
        },
        delete: injDefault,
        select: injDefault,
        publish: injDefault,
        notifyOfSuccessfulPublish: injDefault //triggered by server
    },
    router: {
        stateGo,
        stateReload,
        stateTransitionTo
    },
    moreWub: injDefault,
    /*
     * This action creator uses thunk (https://github.com/gaearon/redux-thunk) which
     * allows using it with a normal dispatch(actionCreator(payload)) even though
     *  it does asynchronous calls. This is a requirement for using it with
     *  $ngRedux.connect(..., actionCreators, ...)
     */
    delayedWub : (nrOfWubs, milliseconds = 1000) => (dispatch) =>
        delay(milliseconds).then(
                args => dispatch(actionCreators.moreWub(nrOfWubs)),
                error => console.err('actions.js: Error while delaying for delayed Wub.')
        )
}

//as string constans, e.g. actionTypes.drafts.change.type === "drafts.change.type"
export const actionTypes = tree2constants(actionHierarchy);

/**
 * actionCreators are functions that take the payload and output
 * an action object, thus prebinding the action-type.
 * This object follows the structure of the actionTypes-object,
 * but is flattened for use with ng-redux. Thus calling
 * `$ngRedux.dispatch(actionCreators.drafts__new(myDraft))` will trigger an action
 * `{type: actionTypes.drafts.new, payload: myDraft}`
 *
 * e.g.:
 *
 * ```javascript
 * function newDraft(draft) {
 *   return { type: 'draft.new', payload: draft }
 * }
 * ```
 */
export const actionCreators = Object.freeze(flattenTree(
        reduceAndMapTreeKeys(
            (path, k) => path.concat(k), //construct paths, e.g. ['draft', 'new']
            (path) => {
                /* leaf can either be a defined creator or a
                 * placeholder asking to generate one.
                 */
                const potentialCreator = won.lookup(actionHierarchy, path);
                if(typeof potentialCreator === 'function') {
                    return potentialCreator; //already a defined creator. hopefully.
                } else {
                    const type = won.lookup(actionTypes, path);
                    return createActionCreator(type);
                }
            }, [], actionHierarchy
        ),
    '__'));

function createActionCreator(type) {
    return (payload) => {
        console.log('creating instance of actionType ', type, ' with payload: ', payload);
        return {type, payload};
    };
}

function delay(milliseconds) {
    return new Promise((resolve, reject) =>
        window.setTimeout(() => resolve(), milliseconds)
    );
}

/*
 * TODO deletme; for debugging
 */
window.actionCreators = actionCreators;
window.actionTypes = actionTypes;

<|endoftext|>


<commit_before>/**
 * Module that controls the Google Drive node settings. Includes Knockout view-model
 * for syncing data, and HGrid-folderpicker for selecting a folder.
 */
'use strict';

var ko = require('knockout');
require('knockout-punches');
var $ = require('jquery');
var m = require('mithril');
var bootbox = require('bootbox');
var Raven = require('raven-js');

var FolderPicker = require('folderpicker');
var ZeroClipboard = require('zeroclipboard');
ZeroClipboard.config('/static/vendor/bower_components/zeroclipboard/dist/ZeroClipboard.swf');
var $osf = require('osfHelpers');

ko.punches.enableAll();

/**
 * Knockout view model for the Google Drive node settings widget.
 */
var ViewModel = function(url, selector, folderPicker) {
    var self = this;
    self.url = url;
    self.loaded = false;
    self.nodeHasAuth = ko.observable(false);
    self.userHasAuth = ko.observable(false);
    // whether current user is authorizer of the addon
    self.userIsOwner = ko.observable(false);

    self.showPicker = ko.observable(false);
    self.owner = ko.observable();
    self.ownerName = ko.observable();
    self.urls = ko.observable({});
    self.loadedFolders = ko.observable(false);
    self.loading = ko.observable(false);
    self.currentFolder = ko.observable(null);
    self.currentPath = ko.observable(null);

    //Folderpicker specific
    self.folderPicker =  folderPicker;
    self.selected = ko.observable(null);
    self.showFileTypes = ko.observable(false);
    self.cancelSelection = ko.observable();
    self.loadedSettings = ko.observable(false);
    self.selectedFileTypeOption = ko.observable('');

    // Flashed messages
    self.message = ko.observable('');
    self.messageClass = ko.observable('text-info');

    // Get data from the config GET endpoint
    function onFetchSuccess(response) {
        // Update view model
        self.nodeHasAuth(response.result.nodeHasAuth);
        self.userHasAuth(response.result.userHasAuth);
        self.userIsOwner(response.result.userIsOwner);
        self.urls(response.result.urls);
        self.ownerName(response.result.ownerName);
        self.owner(response.result.urls.owner);
        self.currentPath(response.result.currentPath);
        self.currentFolder(decodeURIComponent(response.result.currentFolder));

        self.loadedSettings(true);
    }

    function onFetchError(xhr, textstatus, error) {
        self.message('Could not fetch settings.');
    }

    function fetch() {
        $.ajax({
            url: self.url,
            type: 'GET',
            dataType: 'json'
        })
        .done(onFetchSuccess)
        .fail(onFetchError);
    }

    fetch();

    /* Change the flashed status message */
    self.changeMessage = function(text, css, timeout) {
        self.message(text);
        var cssClass = css || 'text-info';
        self.messageClass(cssClass);
        if (timeout) {
            // Reset message after timeout period
            setTimeout(function() {
                self.message('');
                self.messageClass('text-info');
            }, timeout);
        }
    };

    /** Whether or not to show the Create Access Token button */
    self.showTokenCreateButton = ko.computed(function() {
        // Invoke the observables to ensure dependency tracking
        var userHasAuth = self.userHasAuth();
        var nodeHasAuth = self.nodeHasAuth();
        var loaded = self.loadedSettings();
        return !userHasAuth && !nodeHasAuth && loaded;
    });

    self.createAuth = function() {
        return $.osf.postJSON(
            self.urls().create
        ).success(function(response){
            window.location.href = response.url;
        }).fail(function(xhr, textStatus, error) {
            self.changeMessage('Could not authorize Google Drive due to an error. Please try again later.',
                               'text-danger');
            Raven.captureMessage('Could not authorize Google Drive.', {
                url: self.urls().create,
                textStatus: textStatus,
                error: error
            });
        });
    };

    /**
     * Whether or not to show the Import Access Token Button
     */
    self.showImport = ko.computed(function() {
        // Invoke the observables to ensure dependency tracking
        var userHasAuth = self.userHasAuth();
        var nodeHasAuth = self.nodeHasAuth();
        var loaded = self.loadedSettings();
        return userHasAuth && !nodeHasAuth && loaded;
    });

    // Callback for when PUT request to import user access token
    function onImportSuccess(response) {
        var msg = response.message || 'Successfully imported access token from profile.';
        onFetchSuccess(response);
        self.changeFolder();
        self.changeMessage(msg, 'text-success', 5000);
    }

    function onImportError() {
        self.message('Error occurred while importing access token.');
        self.messageClass('text-danger');
    }

    /**
     * Send PUT request to import access token from user profile.
     */
    self.importAuth = function() {
        bootbox.confirm({
            title: 'Import Google Drive Access Token?',
            message: 'Are you sure you want to authorize this project with your Google Drive access token?',
            callback: function(confirmed) {
                if (confirmed) {
                    return $.osf.putJSON(self.urls().importAuth, {})
                        .done(onImportSuccess)
                        .fail(onImportError);
               }
            }
        });
    };

    /**
     * Send DELETE request to deauthorize this node.
     */
    function sendDeauth() {
        return $.ajax({
            url: self.urls().deauthorize,
            type: 'DELETE'
        }).done(function () {
            // Update observables
            self.nodeHasAuth(false);
            self.changeMessage('Deauthorized Google Drive.', 'text-warning', 3000);
        }).fail(function (xhr, textStatus, error) {
            self.changeMessage('Could not deauthorize Google Drive due to an error. Please try again later.', 'text-danger');
            Raven.captureMessage('Could not deauthorize Google Drive.', {
                url: self.urls().deauthorize,
                textStatus: textStatus,
                error: error
            });
        });
    }

    /** Pop up a confirmation to deauthorize Google Drive from this node.
     *  Send DELETE request if confirmed.
     */
    self.deauthorize = function() {
        bootbox.confirm({
            title: 'Deauthorize Google Drive?',
            message: 'Are you sure you want to remove this Google Drive authorization?',
            callback: function(confirmed) {
                if (confirmed) {
                    return sendDeauth();
                }
            }
        });
    };

    /** Callback for chooseFolder action.
     *   Just changes the ViewModel's self.selected observable to the selected
     *   folder.
     */
    function onPickFolder(evt, item) {
        evt.preventDefault();
        self.selected({
            id: item.data.id,
            name: '/' + (item.data.path === '/' ? ' (Full Google Drive)' : item.data.path),
            path: item.data.path
        });

        return false; // Prevent event propagation
    }

    /** Calling Google Drive API & arranging data as
     * required for treebeard Hgrid
     */
    self.changeFolder = function() {
        self.showPicker(!self.showPicker());

        if (!self.loaded) {
            self.loaded = true;
            $(self.folderPicker).folderpicker({
                onPickFolder: onPickFolder,
                filesData: self.urls().get_folders,
                initialFolderPath : self.currentPath(),
                // Lazy-load each folder's contents
                // Each row stores its url for fetching the folders it contains

                resolveLazyloadUrl : function(item){
                    return item.data.urls.get_folders;
                },
                ajaxOptions: {
                    error: function (xhr, textStatus, error) {
                        self.loading(false);
                        self.changeMessage(
                            'Could not connect to Google Drive at this time. ' +
                            'Please try again later.', 'text-warning'
                        );
                        Raven.captureMessage('Could not GET get Google Drive contents.', {
                            textStatus: textStatus,
                            error: error
                        });
                    }
                },
                folderPickerOnload: function () {},
                resolveRows: function(item) {
                    item.css = '';
                    return [
                        {
                            data : 'name',  // Data field name
                            folderIcons : true,
                            filter : false,
                            custom : function(item, col) {
                                return m('span', decodeURIComponent(item.data.name));
                            }
                        },
                        {
                            css : 'p-l-xs',
                            sortInclude : false,
                            custom : FolderPicker.selectView
                        }
                    ];
                }
            });
        }
    };

    self.showFolders = ko.computed(function(){
        return self.nodeHasAuth() && self.userIsOwner();
    });

    self.cancelSelection = function() {
        self.selected(null);
    };

    self.selectedFolderName = ko.computed(function() {
        var userIsOwner = self.userIsOwner();
        var selected = self.selected();
        return (userIsOwner && selected) ? decodeURIComponent(selected.name) : '';
    });

    function onSubmitSuccess(response) {
        self.currentFolder(decodeURIComponent(self.selected().name));
        self.changeMessage(
            'Successfully linked "' +
           $osf.htmlEscape(decodeURIComponent(self.selected().name)) +
            '". Go to the Files page to view your files.',
        'text-success', 5000);
        // Update folder in ViewModel
        self.urls(response.result.urls);
    }

    function onSubmitError() {
        self.changeMessage('Could not change settings. Please try again later.', 'text-danger');
    }

    /**
     * Send a PUT request to change the linked Google Drive folder.
     */
    self.submitSettings = function() {
        $osf.putJSON(self.urls().config, ko.toJS(self))
            .done(onSubmitSuccess)
            .fail(onSubmitError);
    };
};

function GoogleDriveNodeConfig(selector, url, folderPicker) {
    // Initialization code
    var self = this;
    self.viewModel = new ViewModel(url, selector, folderPicker);
    $.osf.applyBindings(self.viewModel, selector);
}

module.exports = GoogleDriveNodeConfig;
<commit_msg>decodeURIComponent casts to undefined to string<commit_after>/**
 * Module that controls the Google Drive node settings. Includes Knockout view-model
 * for syncing data, and HGrid-folderpicker for selecting a folder.
 */
'use strict';

var ko = require('knockout');
require('knockout-punches');
var $ = require('jquery');
var m = require('mithril');
var bootbox = require('bootbox');
var Raven = require('raven-js');

var FolderPicker = require('folderpicker');
var ZeroClipboard = require('zeroclipboard');
ZeroClipboard.config('/static/vendor/bower_components/zeroclipboard/dist/ZeroClipboard.swf');
var $osf = require('osfHelpers');

ko.punches.enableAll();

/**
 * Knockout view model for the Google Drive node settings widget.
 */
var ViewModel = function(url, selector, folderPicker) {
    var self = this;
    self.url = url;
    self.loaded = false;
    self.nodeHasAuth = ko.observable(false);
    self.userHasAuth = ko.observable(false);
    // whether current user is authorizer of the addon
    self.userIsOwner = ko.observable(false);

    self.showPicker = ko.observable(false);
    self.owner = ko.observable();
    self.ownerName = ko.observable();
    self.urls = ko.observable({});
    self.loadedFolders = ko.observable(false);
    self.loading = ko.observable(false);
    self.currentFolder = ko.observable(null);
    self.currentPath = ko.observable(null);

    //Folderpicker specific
    self.folderPicker =  folderPicker;
    self.selected = ko.observable(null);
    self.showFileTypes = ko.observable(false);
    self.cancelSelection = ko.observable();
    self.loadedSettings = ko.observable(false);
    self.selectedFileTypeOption = ko.observable('');

    // Flashed messages
    self.message = ko.observable('');
    self.messageClass = ko.observable('text-info');

    // Get data from the config GET endpoint
    function onFetchSuccess(response) {
        // Update view model
        self.nodeHasAuth(response.result.nodeHasAuth);
        self.userHasAuth(response.result.userHasAuth);
        self.userIsOwner(response.result.userIsOwner);
        self.urls(response.result.urls);
        self.ownerName(response.result.ownerName);
        self.owner(response.result.urls.owner);
        self.currentPath(response.result.currentPath);
        self.currentFolder(response.result.currentFolder ?
            decodeURIComponent(response.result.currentFolder):undefined);

        self.loadedSettings(true);
    }

    function onFetchError(xhr, textstatus, error) {
        self.message('Could not fetch settings.');
    }

    function fetch() {
        $.ajax({
            url: self.url,
            type: 'GET',
            dataType: 'json'
        })
        .done(onFetchSuccess)
        .fail(onFetchError);
    }

    fetch();

    /* Change the flashed status message */
    self.changeMessage = function(text, css, timeout) {
        self.message(text);
        var cssClass = css || 'text-info';
        self.messageClass(cssClass);
        if (timeout) {
            // Reset message after timeout period
            setTimeout(function() {
                self.message('');
                self.messageClass('text-info');
            }, timeout);
        }
    };

    /** Whether or not to show the Create Access Token button */
    self.showTokenCreateButton = ko.computed(function() {
        // Invoke the observables to ensure dependency tracking
        var userHasAuth = self.userHasAuth();
        var nodeHasAuth = self.nodeHasAuth();
        var loaded = self.loadedSettings();
        return !userHasAuth && !nodeHasAuth && loaded;
    });

    self.createAuth = function() {
        return $.osf.postJSON(
            self.urls().create
        ).success(function(response){
            window.location.href = response.url;
        }).fail(function(xhr, textStatus, error) {
            self.changeMessage('Could not authorize Google Drive due to an error. Please try again later.',
                               'text-danger');
            Raven.captureMessage('Could not authorize Google Drive.', {
                url: self.urls().create,
                textStatus: textStatus,
                error: error
            });
        });
    };

    /**
     * Whether or not to show the Import Access Token Button
     */
    self.showImport = ko.computed(function() {
        // Invoke the observables to ensure dependency tracking
        var userHasAuth = self.userHasAuth();
        var nodeHasAuth = self.nodeHasAuth();
        var loaded = self.loadedSettings();
        return userHasAuth && !nodeHasAuth && loaded;
    });

    // Callback for when PUT request to import user access token
    function onImportSuccess(response) {
        var msg = response.message || 'Successfully imported access token from profile.';
        onFetchSuccess(response);
        self.changeFolder();
        self.changeMessage(msg, 'text-success', 5000);
    }

    function onImportError() {
        self.message('Error occurred while importing access token.');
        self.messageClass('text-danger');
    }

    /**
     * Send PUT request to import access token from user profile.
     */
    self.importAuth = function() {
        bootbox.confirm({
            title: 'Import Google Drive Access Token?',
            message: 'Are you sure you want to authorize this project with your Google Drive access token?',
            callback: function(confirmed) {
                if (confirmed) {
                    return $.osf.putJSON(self.urls().importAuth, {})
                        .done(onImportSuccess)
                        .fail(onImportError);
               }
            }
        });
    };

    /**
     * Send DELETE request to deauthorize this node.
     */
    function sendDeauth() {
        return $.ajax({
            url: self.urls().deauthorize,
            type: 'DELETE'
        }).done(function () {
            // Update observables
            self.nodeHasAuth(false);
            self.changeMessage('Deauthorized Google Drive.', 'text-warning', 3000);
        }).fail(function (xhr, textStatus, error) {
            self.changeMessage('Could not deauthorize Google Drive due to an error. Please try again later.', 'text-danger');
            Raven.captureMessage('Could not deauthorize Google Drive.', {
                url: self.urls().deauthorize,
                textStatus: textStatus,
                error: error
            });
        });
    }

    /** Pop up a confirmation to deauthorize Google Drive from this node.
     *  Send DELETE request if confirmed.
     */
    self.deauthorize = function() {
        bootbox.confirm({
            title: 'Deauthorize Google Drive?',
            message: 'Are you sure you want to remove this Google Drive authorization?',
            callback: function(confirmed) {
                if (confirmed) {
                    return sendDeauth();
                }
            }
        });
    };

    /** Callback for chooseFolder action.
     *   Just changes the ViewModel's self.selected observable to the selected
     *   folder.
     */
    function onPickFolder(evt, item) {
        evt.preventDefault();
        self.selected({
            id: item.data.id,
            name: '/' + (item.data.path === '/' ? ' (Full Google Drive)' : item.data.path),
            path: item.data.path
        });

        return false; // Prevent event propagation
    }

    /** Calling Google Drive API & arranging data as
     * required for treebeard Hgrid
     */
    self.changeFolder = function() {
        self.showPicker(!self.showPicker());

        if (!self.loaded) {
            self.loaded = true;
            $(self.folderPicker).folderpicker({
                onPickFolder: onPickFolder,
                filesData: self.urls().get_folders,
                initialFolderPath : self.currentPath(),
                // Lazy-load each folder's contents
                // Each row stores its url for fetching the folders it contains

                resolveLazyloadUrl : function(item){
                    return item.data.urls.get_folders;
                },
                ajaxOptions: {
                    error: function (xhr, textStatus, error) {
                        self.loading(false);
                        self.changeMessage(
                            'Could not connect to Google Drive at this time. ' +
                            'Please try again later.', 'text-warning'
                        );
                        Raven.captureMessage('Could not GET get Google Drive contents.', {
                            textStatus: textStatus,
                            error: error
                        });
                    }
                },
                folderPickerOnload: function () {},
                resolveRows: function(item) {
                    item.css = '';
                    return [
                        {
                            data : 'name',  // Data field name
                            folderIcons : true,
                            filter : false,
                            custom : function(item, col) {
                                return m('span', decodeURIComponent(item.data.name));
                            }
                        },
                        {
                            css : 'p-l-xs',
                            sortInclude : false,
                            custom : FolderPicker.selectView
                        }
                    ];
                }
            });
        }
    };

    self.showFolders = ko.computed(function(){
        return self.nodeHasAuth() && self.userIsOwner();
    });

    self.cancelSelection = function() {
        self.selected(null);
    };

    self.selectedFolderName = ko.computed(function() {
        var userIsOwner = self.userIsOwner();
        var selected = self.selected();
        return (userIsOwner && selected) ? decodeURIComponent(selected.name) : '';
    });

    function onSubmitSuccess(response) {
        self.currentFolder(decodeURIComponent(self.selected().name));
        self.changeMessage(
            'Successfully linked "' +
           $osf.htmlEscape(decodeURIComponent(self.selected().name)) +
            '". Go to the Files page to view your files.',
        'text-success', 5000);
        // Update folder in ViewModel
        self.urls(response.result.urls);
    }

    function onSubmitError() {
        self.changeMessage('Could not change settings. Please try again later.', 'text-danger');
    }

    /**
     * Send a PUT request to change the linked Google Drive folder.
     */
    self.submitSettings = function() {
        $osf.putJSON(self.urls().config, ko.toJS(self))
            .done(onSubmitSuccess)
            .fail(onSubmitError);
    };
};

function GoogleDriveNodeConfig(selector, url, folderPicker) {
    // Initialization code
    var self = this;
    self.viewModel = new ViewModel(url, selector, folderPicker);
    $.osf.applyBindings(self.viewModel, selector);
}

module.exports = GoogleDriveNodeConfig;
<|endoftext|>