<commit_before>/*
 *  Copyright 2020 Google LLC
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      https:www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

function initialise() {
    populateRows();
}

function populateRows() {
    getExistingTimeSlots().then(rows => {
        rows.forEach(row => addRow(row))
    });

    //TODO add delete button to each row

    //TODO validate fields so they're not empty, and start time is before end time
}
//TODO to fix: weird bug where re-fetching slots, the start loses 1 hour???
async function getExistingTimeSlots() {
    const slots = await (await fetch('/volunteer-availability')).json();
    const rows = slots.map(slot => slotToRow(slot));
    // Sort timeslots by start time
    return [...rows].sort((a, b) => (new Date(a.children["ISO-start-time"].value)).getTime() >
    (new Date(b.children["ISO-start-time"].value)).getTime() ? 1 : -1);
}

function slotToRow(slot) {
    //parse start.seconds and end.seconds, getting the date and the day in the local timezone
    const startTime = new Date(slot.start.seconds * 1000);
    const endTime = new Date(slot.end.seconds * 1000);

    // Get local time in HH:MM format
    const hours = String(endTime.getHours()).padStart(2, "0");
    const minutes = String(endTime.getMinutes()).padStart(2, "0");
    const end = hours + ":" + minutes;

    return constructNewRow(startTime.getTime(), end);
}

function fillISOStartTime(inputElement) {
    if (inputElement.value == null || inputElement.value === "") return;
    inputElement.parentNode.children["ISO-start-time"].value = new Date(inputElement.value).toISOString();
    //TODO also update end time accordingly to date change
    //TODO resort this slot in list after changing?
}

function fillISOEndTime(inputElement) {
    const children = inputElement.parentNode.children;
    const endValue = inputElement.value;
    // Get the date from the start datetime object
    const date = new Date(children["ISO-start-time"].value);

    if (endValue == null || !endValue || date == null) return;

    const timeParts = endValue.split(':');

    date.setHours(timeParts[0]);
    date.setMinutes(timeParts[1]);
    children["ISO-end-time"].value = date.toISOString();
}

function constructNewRow(startTimeValue, endTimeValue) {
    const newRow = document.createElement("div");

    const startTime = document.createElement("input");
    startTime.type = "datetime-local";
    startTime.name = "start-time";
    startTime.valueAsNumber = startTimeValue;
    startTime.required = true;

    const ISOStartTime = document.createElement("input");
    ISOStartTime.name = "ISO-start-time";
    ISOStartTime.hidden = true;

    startTime.addEventListener("change", (e) => fillISOStartTime(e.target));

    const endTime = document.createElement("input");
    endTime.type = "time";
    endTime.name = "end-time";
    endTime.value = endTimeValue;
    endTime.required = true;

    const ISOEndTime = document.createElement("input");
    ISOEndTime.name = "ISO-end-time";
    ISOEndTime.hidden = true;

    endTime.addEventListener("change", (e) => fillISOEndTime(e.target));

    newRow.appendChild(startTime);
    newRow.appendChild(endTime);
    newRow.appendChild(ISOStartTime);
    newRow.appendChild(ISOEndTime);

    fillISOStartTime(startTime); // initialise with a value for submission
    fillISOEndTime(endTime); // initialise with a value for submission
    return newRow;
}

function addRow(row) {
    document.getElementById("availability-form").appendChild(row);
}

function addEmptyRow() {
    addRow(constructNewRow());
}<commit_msg>Document availability Javascript functions<commit_after>/*
 *  Copyright 2020 Google LLC
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      https:www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

/**
 * Fills the webpage with the existing timeslots etc.
 */
function initialise() {
    populateRows();
}

/**
 * Inserts existing timeslots in webpage.
 */
function populateRows() {
    getExistingTimeSlots().then(rows => rows.forEach(row => addRow(row)));

    //TODO add delete button to each row

    //TODO validate fields so they're not empty, and start time is before end time
}

/**
 * Fetches the list of existing timeslots.
 * @returns {Promise<Array>} Array of timeslot objects.
 */
async function getExistingTimeSlots() {
    const slots = await (await fetch('/volunteer-availability')).json();
    const rows = slots.map(slot => slotToRow(slot));

    // Sort timeslots by start time
    return [...rows].sort((a, b) => (new Date(a.children["ISO-start-time"].value)).getTime() >
    (new Date(b.children["ISO-start-time"].value)).getTime() ? 1 : -1);
}

/**
 * Convert slot element to row div for insertion intro form.
 * @param slot The slot element to convert.
 * @returns {HTMLDivElement} The row containing the slot data.
 */
function slotToRow(slot) {
    // Get start and end instant and display as local timezone.
    const startTime = new Date(slot.start.seconds * 1000);
    const endTime = new Date(slot.end.seconds * 1000);

    // Get local time in HH:MM format.
    const hours = String(endTime.getHours()).padStart(2, "0");
    const minutes = String(endTime.getMinutes()).padStart(2, "0");
    const end = hours + ":" + minutes;

    return constructNewRow(startTime.getTime(), end);
}

/**
 * Set value of hidden ISO start time element by parsing start date and time.
 * @param {HTMLInputElement} inputElement The start datetime element.
 */
function fillISOStartTime(inputElement) {
    if (inputElement.value == null || inputElement.value === "") return;
    inputElement.parentNode.children["ISO-start-time"].value = new Date(inputElement.value).toISOString();
    //TODO also update end time accordingly to date change
    //TODO re-sort this slot in list after changing?
}

/**
 * Set value of hidden ISO end time element by parsing start date and end time.
 * @param {HTMLInputElement} inputElement The end time element.
 */
function fillISOEndTime(inputElement) {
    const children = inputElement.parentNode.children;
    const endValue = inputElement.value;

    // Get the date from the start datetime object
    const date = new Date(children["ISO-start-time"].value);

    if (endValue == null || !endValue || date == null) return;

    const timeParts = endValue.split(':');

    date.setHours(timeParts[0]);
    date.setMinutes(timeParts[1]);
    children["ISO-end-time"].value = date.toISOString();
}

/**
 * Creates a new row element with the timeslots details.
 * @param {String} startTimeValue The value for datetime-local start date and time.
 * @param {String} endTimeValue The value for local end time.
 * @returns {HTMLDivElement} The div containing the row.
 */
function constructNewRow(startTimeValue, endTimeValue) {
    const newRow = document.createElement("div");

    const startTime = document.createElement("input");
    startTime.type = "datetime-local";
    startTime.name = "start-time";
    startTime.valueAsNumber = startTimeValue;
    startTime.required = true;

    const ISOStartTime = document.createElement("input");
    ISOStartTime.name = "ISO-start-time";
    ISOStartTime.hidden = true;

    startTime.addEventListener("change", (e) => fillISOStartTime(e.target));

    const endTime = document.createElement("input");
    endTime.type = "time";
    endTime.name = "end-time";
    endTime.value = endTimeValue;
    endTime.required = true;

    const ISOEndTime = document.createElement("input");
    ISOEndTime.name = "ISO-end-time";
    ISOEndTime.hidden = true;

    endTime.addEventListener("change", (e) => fillISOEndTime(e.target));

    newRow.appendChild(startTime);
    newRow.appendChild(endTime);
    newRow.appendChild(ISOStartTime);
    newRow.appendChild(ISOEndTime);

    // Initialise with fetches values, if any
    fillISOStartTime(startTime);
    fillISOEndTime(endTime);
    return newRow;
}

/**
 * Add a row to the submission form.
 * @param {HTMLDivElement} row The row to add.
 */
function addRow(row) {
    document.getElementById("availability-form").appendChild(row);
}

/**
 * Insert an empty row in the submission form.
 */
function addEmptyRow() {
    addRow(constructNewRow());
}<|endoftext|>


<commit_before>/*!
GPII XML Settings Handler

Copyright 2012 Raising the Floor - International

Licensed under the New BSD license. You may not use this file except in
compliance with this License.

You may obtain a copy of the License at
https://github.com/gpii/universal/LICENSE.txt
*/

/*global require */
(function () {
    var fluid = require("infusion"),
        fs = require("fs"),
        xm = require('xml-mapping'),
        gpii = fluid.registerNamespace("gpii");

    fluid.registerNamespace("gpii.settingsHandlers.XMLHandler");

    /* -------- Transformation functions -------------------- */

    /**
     * Transform the given array to a hash, using the expandSpec.options.key values from each
     * object in the array as new keys. So for expandSpec.options.key = "name" and
     * source {b: [ { name: b1, v: v1 } { name: b2, v: v2 } ] } we get the transformed json
     * {b: { b1: { v: v1 }}, { b2: { v: v2 }}}
     */
    gpii.settingsHandlers.XMLHandler.arrayToHash = function (arr, expander, expandSpec) {
        if (fluid.get(expandSpec, "options.key") === undefined) {
            fluid.fail("arrayToHash requires a hash of options, one of which is named 'key', at path named \"options\" supplied ", expandSpec);
        }
        if (!fluid.isArrayable(arr)) {
            fluid.fail("arrayToHash didn't find array at inputPath, so failing", expandSpec);
        }
        var newHash = {};
        var pivot = expandSpec.options.key;
        var inputPrefixOp = expander.inputPrefixOp;
        var outputPrefixOp = expander.outputPrefixOp;
        var innerValue = expandSpec.options.innerValue;

        //check that options.key is set
        fluid.each(arr, function (v, k) {
            //check that we have a pivot entry in the object and it's a valid type:
            var newKey = v[pivot];
            var keyType = typeof(newKey);
            if (keyType !== "string" && keyType !== "boolean" && keyType !== "number") {
                fluid.fail("arrayToHash encountered unhashable array due to missing or invalid key", v);
            }
            //use the value of the key element as key and use the remaining content as value
            var content = fluid.copy(v);
            delete content[pivot];
            //fix sub Arrays if needed:
            if (innerValue) {
                var expanded = {};

                //build inputPrefix:
                inputPrefixOp.push(expander.outputPrefix);
                inputPrefixOp.push(k.toString()); //push doesn't accept ints
                outputPrefixOp.push(newKey);
                for (var i=0; i<innerValue.length; ++i) {
                    $.extend(true, expanded, expander.expand(innerValue[i]));
                }
                outputPrefixOp.pop();
                inputPrefixOp.pop();
                inputPrefixOp.pop();
                content = expanded;
            }
            newHash[newKey] = content;
        });
        return newHash;
    };

    gpii.settingsHandlers.XMLHandler.arrayToHash.invertRules = function (expandSpec, expander) {
        var togo = fluid.copy(expandSpec);
        togo.type = "gpii.settingsHandlers.XMLHandler.hashToArray";
        togo.inputPath = fluid.model.composePaths(expander.outputPrefix, expandSpec.outputPath);
        togo.outputPath = fluid.model.composePaths(expander.inputPrefix, expandSpec.inputPath);
        //invert expanders from innerValue as well:
        if (togo.options && togo.options.innerValue) {
            var innerValue = togo.options.innerValue;
            for (var i=0; i<innerValue.length; ++i) {
                innerValue[i] = fluid.model.transform.invertConfiguration(innerValue[i]);
            }
        }
        return togo;
    };

    fluid.defaults("gpii.settingsHandlers.XMLHandler.arrayToHash", {
        gradeNames: ["fluid.standardTransformFunction", "fluid.lens" ],
        invertConfiguration: "gpii.settingsHandlers.XMLHandler.arrayToHash.invertRules"
    });

    /**
     * Inverse of gpii.settingsHandlers.XMLHandler.arrayToHash
     */
    gpii.settingsHandlers.XMLHandler.hashToArray = function (hash, expander, expandSpec) {
        if (!expandSpec.options || !expandSpec.options.key) {
            fluid.fail("hashToArray requires a hash of options, one of which is named 'key', at path named \"options\" supplied ", expandSpec);
        }
        var newArray = [];
        var pivot = expandSpec.options.key;
        var expanded = {};
        var inputPrefixOp = expander.inputPrefixOp;
        var outputPrefixOp = expander.outputPrefixOp;
        var innerValue = expandSpec.options.innerValue;

        //check that options.key is set
        fluid.each(hash, function (v, k) {
            //Due to the xm.toxml function being dependent on the order of key/value pairs in objects (sigh)
            //the pivot/key element needs to be first
            var content = {};
            content[pivot] = k;
            if (expandSpec.options.innerValue) {
                inputPrefixOp.push(expandSpec.inputPath);
                inputPrefixOp.push(k);
                outputPrefixOp.push(expandSpec.inputPath);
                outputPrefixOp.push(newArray.length.toString());
                for (var i=0; i<innerValue.length; ++i) {
                    $.extend(true, expanded, expander.expand(innerValue[i]));
                }
                outputPrefixOp.pop();
                outputPrefixOp.pop();
                inputPrefixOp.pop();
                inputPrefixOp.pop();
                v = expanded;
            }
            fluid.merge("replace", content, v);
            newArray.push(content);
        });
        return newArray;
    };

    fluid.defaults("gpii.settingsHandlers.XMLHandler.hashToArray", {
        gradeNames: "fluid.standardTransformFunction"
    });

    /* ------------------ XML handler specific ---------------------- */
    //Used when fluid.get'ing and set'ing, to allow escaped strings
    var getParser = {
        parser: {
            parse: fluid.pathUtil.parseEL,
            compose: fluid.pathUtil.composePath
        },
        strategies: [fluid.model.defaultFetchStrategy]
    };

    var setParser = {
        parser: {
            parse: fluid.pathUtil.parseEL,
            compose: fluid.pathUtil.composePath
        },
        strategies: [fluid.model.defaultFetchStrategy, fluid.model.defaultCreatorStrategy]
    };
    //http://stackoverflow.com/questions/376373/pretty-printing-xml-with-javascript
    var formatXml = function (xml) {
        var formatted = '';
        var reg = /(>)(<)(\/*)/g;
        xml = xml.replace(reg, '$1\r\n$2$3');
        var pad = 0;
        fluid.each(xml.split('\r\n'), function(node, index) {
            var indent = 0;
            if (node.match( /.+<\/\w[^>]*>$/ )) {
                indent = 0;
            } else if (node.match( /^<\/\w/ )) {
                if (pad !== 0) {
                    pad -= 1;
                }
            } else if (node.match( /^<\w[^>]*[^\/]>.*$/ )) {
                indent = 1;
            } else {
                indent = 0;
            }

            var padding = '';
            for (var i = 0; i < pad; i++) {
                padding += '  ';
            }

            formatted += padding + node + '\r\n';
            pad += indent;
        });
        return formatted;
    };

    //Gets a settings array and a json-ified version of the XML,
    //sets each the relevant settings and returns the settings object with
    //{oldValue: XXXX, newValue: YYYY} as values
    //WARNING: Destructively modifies obj
    gpii.settingsHandlers.XMLHandler.applySettings = function (settings, obj) {
        var returnPayload = {};
        //edit each of the settings
        fluid.each(settings, function(value, el) {
            //store the previous value
            var prev = fluid.get(obj, el, getParser);
            fluid.set(obj, el, value, setParser);

            //create entry in the return payload
            returnPayload[el] = { oldValue: prev, newValue: value };
        });
        return returnPayload;
    };

    /**
     * Reads XML file and returns the equivalent transformed JSON structure
     * (array transformed to objects if given in knownArrays or detected
     * as using 'name' as pivot element)
    */
    gpii.settingsHandlers.XMLHandler.readXMLFile = function (options) {
        if (options === undefined) {
            fluid.fail("readXMLFile: expeded an options block defining filename and encoding");
        }
         //TODO check for and handle read errors
        var xmlString = fs.readFileSync(options.filename, options.encoding || "utf-8");
        // //parse XML to JSON and apply the settings
        var json = xm.tojson(xmlString);
        return (options && options.rules) ? fluid.model.transformWithRules(json, options.rules) : json;
    };

    gpii.settingsHandlers.XMLHandler.writeXMLFile = function (json, options) {
        //Create XML - first line is the xml-tag from options if set
        var xml = options["xml-tag"] || "";
        //transform back
        var inverseRules = fluid.model.transform.invertConfiguration(options.rules);
        var inversedJSON = fluid.model.transformWithRules(json, inverseRules);

        xml += xm.toxml(inversedJSON);
        //and fix indentation/newlines so it's readable
        var formattedXML = formatXml(xml);
        //TODO check for and handler write errors
        fs.writeFileSync(options.filename, formattedXML, options.encoding || "utf-8");
    };

    gpii.settingsHandlers.XMLHandler.set = function (data) {
        var returnObj = fluid.copy(data);
        //each solution block
        return fluid.transform(returnObj, function (solution, solutionId) {
            //each array entry in the solution block
            return fluid.transform(solution, function (solutionEntry) {
                var options = solutionEntry.options;
                var settings = solutionEntry.settings;
                //only valid if a filename is set in options, else ignore this block
                if (options && options.filename) {
                    //parse XML to JSON and apply the settings
                    var json = gpii.settingsHandlers.XMLHandler.readXMLFile(options);
                    //apply settings:
                    var newSettingsResponse = gpii.settingsHandlers.XMLHandler.applySettings(settings, json);
                    //Save to XML file
                    gpii.settingsHandlers.XMLHandler.writeXMLFile(json, options);
                    //save the response from settings handler (ie. with old/new values)
                    solutionEntry.settings = newSettingsResponse;
                }
                return solutionEntry;
            });
        });
    };
}());
<commit_msg>GPII-32: Fixed another (expandSpec.options || expandSpec.options.key) that I had missed when fixing first review comments<commit_after>/*!
GPII XML Settings Handler

Copyright 2012 Raising the Floor - International

Licensed under the New BSD license. You may not use this file except in
compliance with this License.

You may obtain a copy of the License at
https://github.com/gpii/universal/LICENSE.txt
*/

/*global require */
(function () {
    var fluid = require("infusion"),
        fs = require("fs"),
        xm = require('xml-mapping'),
        gpii = fluid.registerNamespace("gpii");

    fluid.registerNamespace("gpii.settingsHandlers.XMLHandler");

    /* -------- Transformation functions -------------------- */

    /**
     * Transform the given array to a hash, using the expandSpec.options.key values from each
     * object in the array as new keys. So for expandSpec.options.key = "name" and
     * source {b: [ { name: b1, v: v1 } { name: b2, v: v2 } ] } we get the transformed json
     * {b: { b1: { v: v1 }}, { b2: { v: v2 }}}
     */
    gpii.settingsHandlers.XMLHandler.arrayToHash = function (arr, expander, expandSpec) {
        if (fluid.get(expandSpec, "options.key") === undefined) {
            fluid.fail("arrayToHash requires a hash of options, one of which is named 'key', at path named \"options\" supplied ", expandSpec);
        }
        if (!fluid.isArrayable(arr)) {
            fluid.fail("arrayToHash didn't find array at inputPath, so failing", expandSpec);
        }
        var newHash = {};
        var pivot = expandSpec.options.key;
        var inputPrefixOp = expander.inputPrefixOp;
        var outputPrefixOp = expander.outputPrefixOp;
        var innerValue = expandSpec.options.innerValue;

        //check that options.key is set
        fluid.each(arr, function (v, k) {
            //check that we have a pivot entry in the object and it's a valid type:
            var newKey = v[pivot];
            var keyType = typeof(newKey);
            if (keyType !== "string" && keyType !== "boolean" && keyType !== "number") {
                fluid.fail("arrayToHash encountered unhashable array due to missing or invalid key", v);
            }
            //use the value of the key element as key and use the remaining content as value
            var content = fluid.copy(v);
            delete content[pivot];
            //fix sub Arrays if needed:
            if (innerValue) {
                var expanded = {};

                //build inputPrefix:
                inputPrefixOp.push(expander.outputPrefix);
                inputPrefixOp.push(k.toString()); //push doesn't accept ints
                outputPrefixOp.push(newKey);
                for (var i=0; i<innerValue.length; ++i) {
                    $.extend(true, expanded, expander.expand(innerValue[i]));
                }
                outputPrefixOp.pop();
                inputPrefixOp.pop();
                inputPrefixOp.pop();
                content = expanded;
            }
            newHash[newKey] = content;
        });
        return newHash;
    };

    gpii.settingsHandlers.XMLHandler.arrayToHash.invertRules = function (expandSpec, expander) {
        var togo = fluid.copy(expandSpec);
        togo.type = "gpii.settingsHandlers.XMLHandler.hashToArray";
        togo.inputPath = fluid.model.composePaths(expander.outputPrefix, expandSpec.outputPath);
        togo.outputPath = fluid.model.composePaths(expander.inputPrefix, expandSpec.inputPath);
        //invert expanders from innerValue as well:
        if (togo.options && togo.options.innerValue) {
            var innerValue = togo.options.innerValue;
            for (var i=0; i<innerValue.length; ++i) {
                innerValue[i] = fluid.model.transform.invertConfiguration(innerValue[i]);
            }
        }
        return togo;
    };

    fluid.defaults("gpii.settingsHandlers.XMLHandler.arrayToHash", {
        gradeNames: ["fluid.standardTransformFunction", "fluid.lens" ],
        invertConfiguration: "gpii.settingsHandlers.XMLHandler.arrayToHash.invertRules"
    });

    /**
     * Inverse of gpii.settingsHandlers.XMLHandler.arrayToHash
     */
    gpii.settingsHandlers.XMLHandler.hashToArray = function (hash, expander, expandSpec) {
        if (fluid.get(expandSpec, "options.key") === undefined) {
            fluid.fail("hashToArray requires a hash of options, one of which is named 'key', at path named \"options\" supplied ", expandSpec);
        }
        var newArray = [];
        var pivot = expandSpec.options.key;
        var expanded = {};
        var inputPrefixOp = expander.inputPrefixOp;
        var outputPrefixOp = expander.outputPrefixOp;
        var innerValue = expandSpec.options.innerValue;

        //check that options.key is set
        fluid.each(hash, function (v, k) {
            //Due to the xm.toxml function being dependent on the order of key/value pairs in objects (sigh)
            //the pivot/key element needs to be first
            var content = {};
            content[pivot] = k;
            if (expandSpec.options.innerValue) {
                inputPrefixOp.push(expandSpec.inputPath);
                inputPrefixOp.push(k);
                outputPrefixOp.push(expandSpec.inputPath);
                outputPrefixOp.push(newArray.length.toString());
                for (var i=0; i<innerValue.length; ++i) {
                    $.extend(true, expanded, expander.expand(innerValue[i]));
                }
                outputPrefixOp.pop();
                outputPrefixOp.pop();
                inputPrefixOp.pop();
                inputPrefixOp.pop();
                v = expanded;
            }
            fluid.merge("replace", content, v);
            newArray.push(content);
        });
        return newArray;
    };

    fluid.defaults("gpii.settingsHandlers.XMLHandler.hashToArray", {
        gradeNames: "fluid.standardTransformFunction"
    });

    /* ------------------ XML handler specific ---------------------- */
    //Used when fluid.get'ing and set'ing, to allow escaped strings
    var getParser = {
        parser: {
            parse: fluid.pathUtil.parseEL,
            compose: fluid.pathUtil.composePath
        },
        strategies: [fluid.model.defaultFetchStrategy]
    };

    var setParser = {
        parser: {
            parse: fluid.pathUtil.parseEL,
            compose: fluid.pathUtil.composePath
        },
        strategies: [fluid.model.defaultFetchStrategy, fluid.model.defaultCreatorStrategy]
    };
    //http://stackoverflow.com/questions/376373/pretty-printing-xml-with-javascript
    var formatXml = function (xml) {
        var formatted = '';
        var reg = /(>)(<)(\/*)/g;
        xml = xml.replace(reg, '$1\r\n$2$3');
        var pad = 0;
        fluid.each(xml.split('\r\n'), function(node, index) {
            var indent = 0;
            if (node.match( /.+<\/\w[^>]*>$/ )) {
                indent = 0;
            } else if (node.match( /^<\/\w/ )) {
                if (pad !== 0) {
                    pad -= 1;
                }
            } else if (node.match( /^<\w[^>]*[^\/]>.*$/ )) {
                indent = 1;
            } else {
                indent = 0;
            }

            var padding = '';
            for (var i = 0; i < pad; i++) {
                padding += '  ';
            }

            formatted += padding + node + '\r\n';
            pad += indent;
        });
        return formatted;
    };

    //Gets a settings array and a json-ified version of the XML,
    //sets each the relevant settings and returns the settings object with
    //{oldValue: XXXX, newValue: YYYY} as values
    //WARNING: Destructively modifies obj
    gpii.settingsHandlers.XMLHandler.applySettings = function (settings, obj) {
        var returnPayload = {};
        //edit each of the settings
        fluid.each(settings, function(value, el) {
            //store the previous value
            var prev = fluid.get(obj, el, getParser);
            fluid.set(obj, el, value, setParser);

            //create entry in the return payload
            returnPayload[el] = { oldValue: prev, newValue: value };
        });
        return returnPayload;
    };

    /**
     * Reads XML file and returns the equivalent transformed JSON structure
     * (array transformed to objects if given in knownArrays or detected
     * as using 'name' as pivot element)
    */
    gpii.settingsHandlers.XMLHandler.readXMLFile = function (options) {
        if (options === undefined) {
            fluid.fail("readXMLFile: expeded an options block defining filename and encoding");
        }
         //TODO check for and handle read errors
        var xmlString = fs.readFileSync(options.filename, options.encoding || "utf-8");
        // //parse XML to JSON and apply the settings
        var json = xm.tojson(xmlString);
        return (options && options.rules) ? fluid.model.transformWithRules(json, options.rules) : json;
    };

    gpii.settingsHandlers.XMLHandler.writeXMLFile = function (json, options) {
        //Create XML - first line is the xml-tag from options if set
        var xml = options["xml-tag"] || "";
        //transform back
        var inverseRules = fluid.model.transform.invertConfiguration(options.rules);
        var inversedJSON = fluid.model.transformWithRules(json, inverseRules);

        xml += xm.toxml(inversedJSON);
        //and fix indentation/newlines so it's readable
        var formattedXML = formatXml(xml);
        //TODO check for and handler write errors
        fs.writeFileSync(options.filename, formattedXML, options.encoding || "utf-8");
    };

    gpii.settingsHandlers.XMLHandler.set = function (data) {
        var returnObj = fluid.copy(data);
        //each solution block
        return fluid.transform(returnObj, function (solution, solutionId) {
            //each array entry in the solution block
            return fluid.transform(solution, function (solutionEntry) {
                var options = solutionEntry.options;
                var settings = solutionEntry.settings;
                //only valid if a filename is set in options, else ignore this block
                if (options && options.filename) {
                    //parse XML to JSON and apply the settings
                    var json = gpii.settingsHandlers.XMLHandler.readXMLFile(options);
                    //apply settings:
                    var newSettingsResponse = gpii.settingsHandlers.XMLHandler.applySettings(settings, json);
                    //Save to XML file
                    gpii.settingsHandlers.XMLHandler.writeXMLFile(json, options);
                    //save the response from settings handler (ie. with old/new values)
                    solutionEntry.settings = newSettingsResponse;
                }
                return solutionEntry;
            });
        });
    };
}());
<|endoftext|>


<commit_before>const path = require("path");
const regularFs = require("fs");
const fs = require("fs-extra");
const assert = require("assert");
const inquirer = require("inquirer");
const sinon = require("sinon");
const Box = require("../");
const TRUFFLE_BOX_DEFAULT =
  "git@github.com:trufflesuite/truffle-init-default.git";
const utils = require("../lib/utils");
let options, cleanupCallback;

describe("truffle-box Box", () => {
  const destination = path.join(__dirname, ".truffle_test_tmp");

  beforeEach(() => {
    fs.ensureDirSync(destination);
  });
  afterEach(() => {
    fs.removeSync(destination);
  });

  describe(".unbox()", () => {
    beforeEach(() => {
      sinon.stub(Box, "checkDir").returns(Promise.resolve());
      fs.emptyDirSync(destination);
    });
    afterEach(() => {
      Box.checkDir.restore();
    });

    it("unboxes truffle box from github", done => {
      Box.unbox(TRUFFLE_BOX_DEFAULT, destination).then(truffleConfig => {
        assert.ok(truffleConfig);

        assert(
          fs.existsSync(path.join(destination, "truffle-config.js")),
          "Unboxed project should have truffle config."
        );
        done();
      });
    });

    it("does not copy the config files or ignored files in the config", () => {
      // Assert the file is not there first.
      assert(
        fs.existsSync(path.join(destination, "truffle-init.json")) === false,
        "truffle-init.json shouldn't be available to the user!"
      );

      // Now assert the README.md and the .gitignore file were removed.
      assert(
        fs.existsSync(path.join(destination, "README.md")) === false,
        "README.md didn't get removed!"
      );
      assert(
        fs.existsSync(path.join(destination, ".gitignore")) === false,
        ".gitignore didn't get removed!"
      );
    });

    describe("when an error is thrown in the try block", () => {
      beforeEach(() => {
        cleanupCallback = sinon.spy();
        sinon.stub(utils, "downloadBox").throws();
        sinon.stub(utils, "setUpTempDirectory").returns(
          new Promise(resolve => {
            resolve({
              path: destination,
              cleanupCallback
            });
          })
        );
      });
      afterEach(() => {
        utils.setUpTempDirectory.restore();
        utils.downloadBox.restore();
      });

      it("calls the cleanup function if it is available", function(done) {
        Box.unbox(TRUFFLE_BOX_DEFAULT, destination).catch(() => {
          assert(cleanupCallback.called);
          done();
        });
      });
    });
  });

  describe("--force", () => {
    beforeEach(() => {
      sinon.stub(inquirer, "prompt").returns(Promise.resolve(true));
    });
    afterEach(() => {
      inquirer.prompt.restore();
    });

    it("unboxes truffle box when used", done => {
      Box.unbox(TRUFFLE_BOX_DEFAULT, destination, { force: true }).then(
        truffleConfig => {
          assert.ok(truffleConfig);

          assert(
            fs.existsSync(path.join(destination, "truffle-config.js")),
            "Unboxed project should have truffle config."
          );
          done();
        }
      );
    });

    it("runs without a prompt", done => {
      Box.unbox(TRUFFLE_BOX_DEFAULT, destination, { force: true }).then(() => {
        console.log("the inquirer value %o", inquirer.prompt.called);
        assert.strictEqual(inquirer.prompt.called, false);
        done();
      });
    });

    it("overwrites redundant files if init/unbox force flag used", done => {
      const truffleConfigPath = path.join(destination, "truffle-config.js");

      // preconditions
      fs.writeFileSync(
        truffleConfigPath,
        "this truffle-config.js file is different than the default box file",
        "utf8"
      );
      assert(
        fs.existsSync(truffleConfigPath),
        "mock truffle-config.js wasn't created!"
      );
      const mockConfig = fs.readFileSync(truffleConfigPath, "utf8");

      Box.unbox(TRUFFLE_BOX_DEFAULT, destination, { force: true }).then(() => {
        assert(
          fs.existsSync(truffleConfigPath),
          "truffle-config.js wasn't recreated!"
        );
        const newConfig = fs.readFileSync(truffleConfigPath, "utf8");
        assert(
          newConfig !== mockConfig,
          "truffle-config.js wasn't overwritten!"
        );
        done();
      });
    });
  });

  describe("init/unbox prompt", () => {
    const contractDirPath = path.join(destination, "contracts");

    beforeEach(() => {
      options = { logger: { log: () => {} } };
      // preconditions
      sinon
        .stub(inquirer, "prompt")
        .returns(Promise.resolve({ proceed: true }));
      fs.ensureDirSync(contractDirPath);
      assert(
        fs.existsSync(contractDirPath),
        "contracts folder wasn't created!"
      );
    });
    afterEach(() => {
      inquirer.prompt.restore();
      fs.removeSync(contractDirPath);
    });

    it("prompts when redundant files/folders exist in target directory", done => {
      Box.unbox(TRUFFLE_BOX_DEFAULT, destination, options).then(() => {
        assert.strictEqual(inquirer.prompt.called, true);
        assert.strictEqual(inquirer.prompt.callCount, 2);
        done();
      });
    });

    it("prompt questions call correctly", done => {
      Box.unbox(TRUFFLE_BOX_DEFAULT, destination, options).then(() => {
        assert(
          inquirer.prompt.getCall(0).args[0],
          "Prompt questions weren't called!"
        );
        done();
      });
    });
  });

  describe("Box.checkDir()", () => {
    beforeEach(() => {
      sinon
        .stub(inquirer, "prompt")
        .returns(Promise.resolve({ proceed: true }));
    });
    afterEach(() => {
      inquirer.prompt.restore();
    });

    describe("when the directory is empty", () => {
      beforeEach(() => {
        sinon.stub(regularFs, "readdirSync").returns([]);
      });
      afterEach(() => {
        regularFs.readdirSync.restore();
      });

      it("doesn't prompt the user", async () => {
        await Box.checkDir();
        assert.strictEqual(inquirer.prompt.called, false);
      });
    });

    describe("when the directory is non-empty", () => {
      beforeEach(() => {
        sinon.stub(regularFs, "readdirSync").returns(["someCrappyFile.js"]);
      });
      afterEach(() => {
        regularFs.readdirSync.restore();
      });

      it("prompts the user", () => {
        Box.checkDir();
        assert(inquirer.prompt.called);
      });
    });
  });
});
<commit_msg>Resolve truffle-box tests<commit_after>const path = require("path");
const regularFs = require("fs");
const fs = require("fs-extra");
const assert = require("assert");
const inquirer = require("inquirer");
const sinon = require("sinon");
const Config = require("truffle-config");
const Box = require("../");
const TRUFFLE_BOX_DEFAULT =
  "git@github.com:trufflesuite/truffle-init-default.git";
const utils = require("../lib/utils");
let options, cleanupCallback, config;

describe("truffle-box Box", () => {
  const destination = path.join(__dirname, ".truffle_test_tmp");

  beforeEach(() => {
    fs.ensureDirSync(destination);
    config = Config.default();
  });
  afterEach(() => {
    fs.removeSync(destination);
  });

  describe(".unbox()", () => {
    beforeEach(() => {
      sinon.stub(Box, "checkDir").returns(Promise.resolve());
      fs.emptyDirSync(destination);
    });
    afterEach(() => {
      Box.checkDir.restore();
    });

    it("unboxes truffle box from github", done => {
      Box.unbox(TRUFFLE_BOX_DEFAULT, destination, {}, config).then(
        truffleConfig => {
          assert.ok(truffleConfig);

          assert(
            fs.existsSync(path.join(destination, "truffle-config.js")),
            "Unboxed project should have truffle config."
          );
          done();
        }
      );
    });

    it("does not copy the config files or ignored files in the config", () => {
      // Assert the file is not there first.
      assert(
        fs.existsSync(path.join(destination, "truffle-init.json")) === false,
        "truffle-init.json shouldn't be available to the user!"
      );

      // Now assert the README.md and the .gitignore file were removed.
      assert(
        fs.existsSync(path.join(destination, "README.md")) === false,
        "README.md didn't get removed!"
      );
      assert(
        fs.existsSync(path.join(destination, ".gitignore")) === false,
        ".gitignore didn't get removed!"
      );
    });

    describe("when an error is thrown in the try block", () => {
      beforeEach(() => {
        cleanupCallback = sinon.spy();
        sinon.stub(utils, "downloadBox").throws();
        sinon.stub(config.eventManager, "emit");
        sinon.stub(utils, "setUpTempDirectory").returns(
          new Promise(resolve => {
            resolve({
              path: destination,
              cleanupCallback
            });
          })
        );
      });
      afterEach(() => {
        utils.setUpTempDirectory.restore();
        config.eventManager.emit.restore();
        utils.downloadBox.restore();
      });

      it("calls the cleanup function if it is available", function(done) {
        Box.unbox(TRUFFLE_BOX_DEFAULT, destination, {}, config).catch(() => {
          assert(cleanupCallback.called);
          done();
        });
      });
    });
  });

  describe("--force", () => {
    beforeEach(() => {
      sinon.stub(inquirer, "prompt").returns(Promise.resolve(true));
    });
    afterEach(() => {
      inquirer.prompt.restore();
    });

    it("unboxes truffle box when used", done => {
      Box.unbox(TRUFFLE_BOX_DEFAULT, destination, { force: true }, config).then(
        truffleConfig => {
          assert.ok(truffleConfig);

          assert(
            fs.existsSync(path.join(destination, "truffle-config.js")),
            "Unboxed project should have truffle config."
          );
          done();
        }
      );
    });

    it("runs without a prompt", done => {
      Box.unbox(TRUFFLE_BOX_DEFAULT, destination, { force: true }, config).then(
        () => {
          assert.strictEqual(inquirer.prompt.called, false);
          done();
        }
      );
    });

    it("overwrites redundant files if init/unbox force flag used", done => {
      const truffleConfigPath = path.join(destination, "truffle-config.js");

      // preconditions
      fs.writeFileSync(
        truffleConfigPath,
        "this truffle-config.js file is different than the default box file",
        "utf8"
      );
      assert(
        fs.existsSync(truffleConfigPath),
        "mock truffle-config.js wasn't created!"
      );
      const mockConfig = fs.readFileSync(truffleConfigPath, "utf8");

      Box.unbox(TRUFFLE_BOX_DEFAULT, destination, { force: true }, config).then(
        () => {
          assert(
            fs.existsSync(truffleConfigPath),
            "truffle-config.js wasn't recreated!"
          );
          const newConfig = fs.readFileSync(truffleConfigPath, "utf8");
          assert(
            newConfig !== mockConfig,
            "truffle-config.js wasn't overwritten!"
          );
          done();
        }
      );
    });
  });

  describe("init/unbox prompt", () => {
    const contractDirPath = path.join(destination, "contracts");

    beforeEach(() => {
      options = { logger: { log: () => {} } };
      // preconditions
      sinon
        .stub(inquirer, "prompt")
        .returns(Promise.resolve({ proceed: true }));
      fs.ensureDirSync(contractDirPath);
      assert(
        fs.existsSync(contractDirPath),
        "contracts folder wasn't created!"
      );
    });
    afterEach(() => {
      inquirer.prompt.restore();
      fs.removeSync(contractDirPath);
    });

    it("prompts when redundant files/folders exist in target directory", done => {
      Box.unbox(TRUFFLE_BOX_DEFAULT, destination, options, config).then(() => {
        assert.strictEqual(inquirer.prompt.called, true);
        assert.strictEqual(inquirer.prompt.callCount, 2);
        done();
      });
    });

    it("prompt questions call correctly", done => {
      Box.unbox(TRUFFLE_BOX_DEFAULT, destination, options, config).then(() => {
        assert(
          inquirer.prompt.getCall(0).args[0],
          "Prompt questions weren't called!"
        );
        done();
      });
    });
  });

  describe("Box.checkDir()", () => {
    beforeEach(() => {
      sinon
        .stub(inquirer, "prompt")
        .returns(Promise.resolve({ proceed: true }));
    });
    afterEach(() => {
      inquirer.prompt.restore();
    });

    describe("when the directory is empty", () => {
      beforeEach(() => {
        sinon.stub(regularFs, "readdirSync").returns([]);
      });
      afterEach(() => {
        regularFs.readdirSync.restore();
      });

      it("doesn't prompt the user", async () => {
        await Box.checkDir();
        assert.strictEqual(inquirer.prompt.called, false);
      });
    });

    describe("when the directory is non-empty", () => {
      beforeEach(() => {
        sinon.stub(regularFs, "readdirSync").returns(["someCrappyFile.js"]);
      });
      afterEach(() => {
        regularFs.readdirSync.restore();
      });

      it("prompts the user", () => {
        Box.checkDir();
        assert(inquirer.prompt.called);
      });
    });
  });
});
<|endoftext|>


<commit_before>// Separate Numbers with Commas in JavaScript **Pairing Challenge**

// I worked on this challenge with: .

// Pseudocode
//separateComma(1569743) == "1,569,743"

// Initial Solution
function separateComma(){

}

// Refactored Solution

// Your Own Tests (OPTIONAL)

// Reflection

// What was it like to approach the problem from the perspective of JavaScript? Did you approach the problem differently?
// What did you learn about iterating over arrays in JavaScript?
// What was different about solving this problem in JavaScript?
// What built-in methods did you find to incorporate in your refactored solution?<commit_msg>added numb comma, refactored, and lost my mind<commit_after>// Separate Numbers with Commas in JavaScript **Pairing Challenge**

// I worked on this challenge with: .

// Pseudocode
//separateComma(1569743) == "1,569,743"

// Initial Solution

// var number = 123456789;

// function seprateComma(num) {

// // var num = 123456789;

// num = num.toString().split("").reverse().join("");;
// console.log(num)

// var revNum = num;

// var corNum = [];

// //Create a way to take 3 add a comma
// corNum.push(revNum.substring(0,3) + ",");
// console.log(corNum);
// //Remove 3 from reverse number
// revNum = (revNum.slice(3));

// corNum.push(revNum.substring(0,3) + ",");
// console.log(corNum);
// revNum = (revNum.slice(3));

// //Driver code
// console.log(revNum.length)

// // If less than 4 do this.
// corNum.push(revNum.substring(0,3));

// corNum = corNum.join("");
// corNum = corNum.split("").reverse();
// corNum = corNum.join("")

// console.log(corNum)

// };

// //Driver code
// seprateComma(number);

// Refactored Solution

function seprateComma(num) {

  num = num.toString().split("").reverse().join("");

  var revNum = num;
  var corNum = [];

  for ( ; revNum.length >= 4;){
    corNum.push(revNum.substring(0,3) + ",");
    revNum = (revNum.slice(3));
    }

  corNum.push(revNum.substring());

  console.log(corNum.join("").split("").reverse().join(""));

};

// Your Own Tests (OPTIONAL)

//Driver code
seprateComma(123456111117189);

// Reflection

// What was it like to approach the problem from the perspective of JavaScript? Did you approach the problem differently?

// I ran into so much issues with JavaScript.  There aren’t as much build in methods as Ruby and a lot of things was very foreign to me because I never learn JavaScript and its a big world out there.  But with the knowledge of Ruby’s concepts we were able to tackle this problem.

// What did you learn about iterating over arrays in JavaScript?

//   I learned how to use a FOR loop and what I was able to do with it.  I didn’t do the index like most people did.  I took a different approach to the problem.

// What was different about solving this problem in JavaScript?

//   I had to use different loop but the concept was pretty much the same as the last one we did in Ruby.  I think I did a better job in JavaScript even with less tools.  I hope its just skill level of programing has increased a bit.

// What built-in methods did you find to incorporate in your refactored solution?

//   I used the for loop.  Instead of using the whole 3 argument of the for loop I only used the middle condition of the for loop which made it work.  I was fuzzy about knowing how to use the FOR loop but I was able to get some clarification on it.<|endoftext|>


<commit_before>'use strict';

var pkg = require('../../../package.json'),
    gulp = require('gulp'),
    config = require('../../config'),
    path = require('path');

// performs all required operations to distribute the js files
gulp.task('scripts', function(cb) {

    var build = gulp.args.build || gulp.args.emulate || gulp.args.run,
        targetSrc = config.paths.scripts,
        targetDir = path.resolve(build ? './www/' : './.tmp/');

    return gulp.src(targetSrc)
        .pipe(gulp.plugins.concat(config.scripts.name))
        .pipe(gulp.plugins.if(build, gulp.plugins.stripDebug()))
        .pipe(gulp.plugins.header(config.build.closureStart))
        .pipe(gulp.plugins.footer(config.build.closureEnd))
        .pipe(gulp.plugins.if(build, gulp.plugins.rename({extname: '.min.js'})))
        .pipe(gulp.plugins.if(build, gulp.plugins.uglify()))
        .pipe(gulp.plugins.header(config.build.banner, {pkg: pkg}))
        .pipe(gulp.dest(targetDir + '/js'));
    cb();
});

<commit_msg>#29-gulp-scripts-build - * Bug The `scripts` task now does not attempt to remove the traces.<commit_after>'use strict';

var pkg = require('../../../package.json'),
    gulp = require('gulp'),
    config = require('../../config'),
    path = require('path');

// performs all required operations to distribute the js files
gulp.task('scripts', function(cb) {

    var build = gulp.args.build || gulp.args.emulate || gulp.args.run,
        targetSrc = config.paths.scripts,
        targetDir = path.resolve(build ? './www/' : './.tmp/');

    return gulp.src(targetSrc)
        .pipe(gulp.plugins.concat(config.scripts.name))
        //.pipe(gulp.plugins.if(build, gulp.plugins.stripDebug()))
        .pipe(gulp.plugins.header(config.build.closureStart))
        .pipe(gulp.plugins.footer(config.build.closureEnd))
        .pipe(gulp.plugins.if(build, gulp.plugins.rename({extname: '.min.js'})))
        .pipe(gulp.plugins.header(config.build.banner, {pkg: pkg}))
        .pipe(gulp.dest(targetDir + '/js'));
    cb();
});

<|endoftext|>


<commit_before>import Actions from '../actions/actions';
import mapStore from '../stores/mapStore';
import { ModeConstants } from '../constants/constants';

export default class Marker {
  constructor(latLng, icon) {
    this.icon = icon;
    let options = {
      icon,
      draggable: true
    }
    let marker = new L.Marker(latLng, options);
    return this.withHooks(marker);
  }

  withHooks(marker) {
    let iconMargin = 0;
    let icon = this.icon;

    marker.on('click', (e) => {
      e.target.getPopup()._isOpen = false;
      $(e.target.getPopup()._container).css('display', 'block');
    })

    marker.on('add', () => {
      $('.leaflet-popup').css('display', 'none')
    });

    marker.on('dragstart', (e) => {
      let icon = e.target._icon;

      if (!iconMargin) {
        iconMargin = parseInt(L.DomUtil.getStyle(icon, 'marginTop'));
      }
      icon.style.marginTop = (iconMargin - 15)  + 'px';
    });

    marker.on('dragend', (e) => {
      e.target._icon.style.marginTop = iconMargin + 'px';
    });

    return marker;
  }
}
<commit_msg>Fix icon bounding box drag.<commit_after>import L from 'leaflet';
import Actions from '../actions/actions';
import mapStore from '../stores/mapStore';
import { ModeConstants } from '../constants/constants';

export default class Marker {
  constructor(latLng, icon) {
    this.icon = icon;
    let options = {
      icon,
      draggable: true
    }
    let marker = new L.Marker(latLng, options);
    return this.withHooks(marker);
  }

  withHooks(marker) {
    let iconMargin = 0;
    let icon = this.icon;
    let mode = null;
    let map = mapStore.getState().map;

    marker.on('click', (e) => {
      e.target.getPopup()._isOpen = false;
      $(e.target.getPopup()._container).css('display', 'block');
    })

    marker.on('add', () => {
      $('.leaflet-popup').css('display', 'none')
    });

    marker.on('dragstart', (e) => {
      let icon = e.target._icon;

      mode = mapStore.getState().mode;
      Actions.changeMode(ModeConstants);

      if (!iconMargin) {
        iconMargin = parseInt(L.DomUtil.getStyle(icon, 'marginTop'));
      }
      icon.style.marginTop = `${iconMargin - 15}px`;
    });

    marker.on('dragend', (e) => {
      let map = mapStore.getState().map;
      e.target._icon.style.marginTop = `${iconMargin}px`;

      map.once('click', () => {
        Actions.changeMode(mode);
      })
    });

    return marker;
  }
}
<|endoftext|>


<commit_before>'use strict'

import durate, {pattern} from 'durate'

module.exports = class Entry {
  constructor(str) {
    this.original = str
    this.parseDurate(str)
  }

  parseDurate(str) {
    if (pattern.test(str)) {
      this.setDates(durate(this.original))
    }
  }

  setDates(opts) {
    this.hasDates = true
    this.startDate = opts.start
    this.endDate = opts.end
  }

  getDates() {
    let start = this.startDate
    let end = this.endDate
    return {start, end}
  }
}
<commit_msg>rename parameter to msg<commit_after>'use strict'

import durate, {pattern} from 'durate'

module.exports = class Entry {
  constructor(msg) {
    this.original = msg
    this.parseDurate(msg)
  }

  parseDurate(msg) {
    if (pattern.test(msg)) {
      this.setDates(durate(msg))
    }
  }

  setDates(opts) {
    this.hasDates = true
    this.startDate = opts.start
    this.endDate = opts.end
  }

  getDates() {
    let start = this.startDate
    let end = this.endDate
    return {start, end}
  }
}
<|endoftext|>


<commit_before>// @flow

import {
  type EntryInfo,
  entryInfoPropType,
  type CreateEntryInfo,
  type SaveEntryInfo,
  type SaveEntryResponse,
  type CreateEntryPayload,
  type DeleteEntryInfo,
  type DeleteEntryResponse,
  type CalendarQuery,
} from 'lib/types/entry-types';
import type { ThreadInfo } from 'lib/types/thread-types';
import { threadInfoPropType } from 'lib/types/thread-types';
import type { LoadingStatus } from 'lib/types/loading-types';
import type { AppState } from '../redux/redux-setup';
import type {
  DispatchActionPayload,
  DispatchActionPromise,
} from 'lib/utils/action-utils';

import * as React from 'react';
import classNames from 'classnames';
import invariant from 'invariant';
import PropTypes from 'prop-types';

import { entryKey } from 'lib/shared/entry-utils';
import { colorIsDark } from 'lib/shared/thread-utils';
import { connect } from 'lib/utils/redux-utils';
import {
  createEntryActionTypes,
  createEntry,
  saveEntryActionTypes,
  saveEntry,
  deleteEntryActionTypes,
  deleteEntry,
  concurrentModificationResetActionType,
} from 'lib/actions/entry-actions';
import { ServerError } from 'lib/utils/errors';
import { dateString } from 'lib/utils/date-utils';
import { threadInfoSelector } from 'lib/selectors/thread-selectors';

import css from './calendar.css';
import LoadingIndicator from '../loading-indicator.react';
import ConcurrentModificationModal from '../modals/concurrent-modification-modal.react';
import HistoryModal from '../modals/history/history-modal.react';
import { HistoryVector, DeleteVector } from '../vectors.react';
import LogInFirstModal from '../modals/account/log-in-first-modal.react';
import { nonThreadCalendarQuery } from '../selectors/nav-selectors';

type Props = {|
  innerRef: (key: string, me: Entry) => void,
  entryInfo: EntryInfo,
  focusOnFirstEntryNewerThan: (time: number) => void,
  setModal: (modal: ?React.Node) => void,
  tabIndex: number,
  // Redux state
  threadInfo: ThreadInfo,
  loggedIn: boolean,
  calendarQuery: () => CalendarQuery,
  online: boolean,
  // Redux dispatch functions
  dispatchActionPayload: DispatchActionPayload,
  dispatchActionPromise: DispatchActionPromise,
  // async functions that hit server APIs
  createEntry: (info: CreateEntryInfo) => Promise<CreateEntryPayload>,
  saveEntry: (info: SaveEntryInfo) => Promise<SaveEntryResponse>,
  deleteEntry: (info: DeleteEntryInfo) => Promise<DeleteEntryResponse>,
|};
type State = {|
  focused: boolean,
  loadingStatus: LoadingStatus,
  text: string,
|};
class Entry extends React.PureComponent<Props, State> {
  static propTypes = {
    innerRef: PropTypes.func.isRequired,
    entryInfo: entryInfoPropType.isRequired,
    focusOnFirstEntryNewerThan: PropTypes.func.isRequired,
    setModal: PropTypes.func.isRequired,
    tabIndex: PropTypes.number.isRequired,
    threadInfo: threadInfoPropType.isRequired,
    loggedIn: PropTypes.bool.isRequired,
    calendarQuery: PropTypes.func.isRequired,
    online: PropTypes.bool.isRequired,
    dispatchActionPayload: PropTypes.func.isRequired,
    dispatchActionPromise: PropTypes.func.isRequired,
    createEntry: PropTypes.func.isRequired,
    saveEntry: PropTypes.func.isRequired,
    deleteEntry: PropTypes.func.isRequired,
  };
  textarea: ?HTMLTextAreaElement;
  creating: boolean;
  needsUpdateAfterCreation: boolean;
  needsDeleteAfterCreation: boolean;
  nextSaveAttemptIndex: number;
  mounted: boolean;
  currentlySaving: ?string;

  constructor(props: Props) {
    super(props);
    this.state = {
      focused: false,
      loadingStatus: 'inactive',
      text: props.entryInfo.text,
    };
    this.creating = false;
    this.needsUpdateAfterCreation = false;
    this.needsDeleteAfterCreation = false;
    this.nextSaveAttemptIndex = 0;
  }

  guardedSetState(input: $Shape<State>) {
    if (this.mounted) {
      this.setState(input);
    }
  }

  componentDidMount() {
    this.mounted = true;
    this.props.innerRef(entryKey(this.props.entryInfo), this);
    this.updateHeight();
    // Whenever a new Entry is created, focus on it
    if (!this.props.entryInfo.id) {
      this.focus();
    }
  }

  componentDidUpdate(prevProps: Props) {
    if (
      !this.state.focused &&
      this.props.entryInfo.text !== this.state.text &&
      this.props.entryInfo.text !== prevProps.entryInfo.text
    ) {
      this.setState({ text: this.props.entryInfo.text });
      this.currentlySaving = null;
    }

    if (
      this.props.online &&
      !prevProps.online &&
      this.state.loadingStatus === 'error'
    ) {
      this.save();
    }
  }

  focus() {
    invariant(
      this.textarea instanceof HTMLTextAreaElement,
      'textarea ref not set',
    );
    this.textarea.focus();
  }

  onMouseDown = (event: SyntheticEvent<HTMLDivElement>) => {
    if (this.state.focused && event.target !== this.textarea) {
      // Don't lose focus when some non-textarea part is clicked
      event.preventDefault();
    }
  };

  componentWillUnmount() {
    this.mounted = false;
  }

  updateHeight() {
    invariant(
      this.textarea instanceof HTMLTextAreaElement,
      'textarea ref not set',
    );
    this.textarea.style.height = 'auto';
    this.textarea.style.height = this.textarea.scrollHeight + 'px';
  }

  render() {
    let actionLinks = null;
    if (this.state.focused) {
      let historyButton = null;
      if (this.props.entryInfo.id) {
        historyButton = (

            <HistoryVector className={css.history} />
            History

        );
      }
      const rightActionLinksClassName = `${css.rightActionLinks} ${css.actionLinksText}`;
      actionLinks = (

            <DeleteVector />
            Delete

          {historyButton}

            {this.props.threadInfo.uiName}

      );
    }

    const darkColor = colorIsDark(this.props.threadInfo.color);
    const entryClasses = classNames({
      [css.entry]: true,
      [css.darkEntry]: darkColor,
      [css.focusedEntry]: this.state.focused,
    });
    const style = { backgroundColor: '#' + this.props.threadInfo.color };
    const loadingIndicatorColor = darkColor ? 'white' : 'black';
    return (

        <textarea
          rows="1"
          className={css.entryText}
          onChange={this.onChange}
          onKeyDown={this.onKeyDown}
          value={this.state.text}
          onFocus={this.onFocus}
          onBlur={this.onBlur}
          tabIndex={this.props.tabIndex}
          ref={this.textareaRef}
        />
        <LoadingIndicator
          status={this.state.loadingStatus}
          color={loadingIndicatorColor}
          loadingClassName={css.entryLoading}
          errorClassName={css.entryError}
        />
        {actionLinks}

    );
  }

  textareaRef = (textarea: ?HTMLTextAreaElement) => {
    this.textarea = textarea;
  };

  onFocus = () => {
    if (!this.state.focused) {
      this.setState({ focused: true });
    }
  };

  onBlur = () => {
    this.setState({ focused: false });
    if (this.state.text.trim() === '') {
      this.delete();
    } else if (this.props.entryInfo.text !== this.state.text) {
      this.save();
    }
  };

  delete() {
    this.dispatchDelete(this.props.entryInfo.id, false);
  }

  save() {
    this.dispatchSave(this.props.entryInfo.id, this.state.text);
  }

  onChange = (event: SyntheticEvent<HTMLTextAreaElement>) => {
    if (!this.props.loggedIn) {
      this.props.setModal(
        <LogInFirstModal
          inOrderTo="edit this calendar"
          setModal={this.props.setModal}
        />,
      );
      return;
    }
    const target = event.target;
    invariant(target instanceof HTMLTextAreaElement, 'target not textarea');
    this.setState({ text: target.value }, this.updateHeight.bind(this));
  };

  onKeyDown = (event: SyntheticKeyboardEvent<HTMLTextAreaElement>) => {
    if (event.keyCode === 27) {
      invariant(
        this.textarea instanceof HTMLTextAreaElement,
        'textarea ref not set',
      );
      this.textarea.blur();
    }
  };

  dispatchSave(serverID: ?string, newText: string) {
    if (this.currentlySaving === newText) {
      return;
    }
    this.currentlySaving = newText;

    if (newText.trim() === '') {
      // We don't save the empty string, since as soon as the element loses
      // focus it'll get deleted
      return;
    }

    if (!serverID) {
      if (this.creating) {
        // We need the first save call to return so we know the ID of the entry
        // we're updating, so we'll need to handle this save later
        this.needsUpdateAfterCreation = true;
        return;
      } else {
        this.creating = true;
      }
    }

    if (!serverID) {
      this.props.dispatchActionPromise(
        createEntryActionTypes,
        this.createAction(newText),
      );
    } else {
      this.props.dispatchActionPromise(
        saveEntryActionTypes,
        this.saveAction(serverID, newText),
      );
    }
  }

  async createAction(text: string) {
    const localID = this.props.entryInfo.localID;
    invariant(localID, "if there's no serverID, there should be a localID");
    const curSaveAttempt = this.nextSaveAttemptIndex++;
    this.guardedSetState({ loadingStatus: 'loading' });
    try {
      const response = await this.props.createEntry({
        text,
        timestamp: this.props.entryInfo.creationTime,
        date: dateString(
          this.props.entryInfo.year,
          this.props.entryInfo.month,
          this.props.entryInfo.day,
        ),
        threadID: this.props.entryInfo.threadID,
        localID,
        calendarQuery: this.props.calendarQuery(),
      });
      if (curSaveAttempt + 1 === this.nextSaveAttemptIndex) {
        this.guardedSetState({ loadingStatus: 'inactive' });
      }
      this.creating = false;
      if (this.needsUpdateAfterCreation) {
        this.needsUpdateAfterCreation = false;
        this.dispatchSave(response.entryID, this.state.text);
      }
      if (this.needsDeleteAfterCreation) {
        this.needsDeleteAfterCreation = false;
        this.dispatchDelete(response.entryID, false);
      }
      return response;
    } catch (e) {
      if (curSaveAttempt + 1 === this.nextSaveAttemptIndex) {
        this.guardedSetState({ loadingStatus: 'error' });
      }
      this.currentlySaving = null;
      this.creating = false;
      throw e;
    }
  }

  async saveAction(entryID: string, newText: string) {
    const curSaveAttempt = this.nextSaveAttemptIndex++;
    this.guardedSetState({ loadingStatus: 'loading' });
    try {
      const response = await this.props.saveEntry({
        entryID,
        text: newText,
        prevText: this.props.entryInfo.text,
        timestamp: Date.now(),
        calendarQuery: this.props.calendarQuery(),
      });
      if (curSaveAttempt + 1 === this.nextSaveAttemptIndex) {
        this.guardedSetState({ loadingStatus: 'inactive' });
      }
      return { ...response, threadID: this.props.entryInfo.threadID };
    } catch (e) {
      if (curSaveAttempt + 1 === this.nextSaveAttemptIndex) {
        this.guardedSetState({ loadingStatus: 'error' });
      }
      this.currentlySaving = null;
      if (e instanceof ServerError && e.message === 'concurrent_modification') {
        const onRefresh = () => {
          this.setState(
            { loadingStatus: 'inactive' },
            this.updateHeight.bind(this),
          );
          this.props.dispatchActionPayload(
            concurrentModificationResetActionType,
            { id: entryID, dbText: e.payload.db },
          );
          this.clearModal();
        };
        this.props.setModal(
          <ConcurrentModificationModal
            onClose={this.clearModal}
            onRefresh={onRefresh}
          />,
        );
      }
      throw e;
    }
  }

  onDelete = (event: SyntheticEvent<HTMLAnchorElement>) => {
    event.preventDefault();
    if (!this.props.loggedIn) {
      this.props.setModal(
        <LogInFirstModal
          inOrderTo="edit this calendar"
          setModal={this.props.setModal}
        />,
      );
      return;
    }
    this.dispatchDelete(this.props.entryInfo.id, true);
  };

  dispatchDelete(serverID: ?string, focusOnNextEntry: boolean) {
    const { localID } = this.props.entryInfo;
    this.props.dispatchActionPromise(
      deleteEntryActionTypes,
      this.deleteAction(serverID, focusOnNextEntry),
      undefined,
      { localID, serverID },
    );
  }

  async deleteAction(serverID: ?string, focusOnNextEntry: boolean) {
    invariant(
      this.props.loggedIn,
      'user should be logged in if delete triggered',
    );
    if (focusOnNextEntry) {
      this.props.focusOnFirstEntryNewerThan(this.props.entryInfo.creationTime);
    }
    if (serverID) {
      return await this.props.deleteEntry({
        entryID: serverID,
        prevText: this.props.entryInfo.text,
        calendarQuery: this.props.calendarQuery(),
      });
    } else if (this.creating) {
      this.needsDeleteAfterCreation = true;
    }
    return null;
  }

  onHistory = (event: SyntheticEvent<HTMLAnchorElement>) => {
    event.preventDefault();
    this.props.setModal(
      <HistoryModal
        mode="entry"
        dayString={dateString(
          this.props.entryInfo.year,
          this.props.entryInfo.month,
          this.props.entryInfo.day,
        )}
        onClose={this.clearModal}
        currentEntryID={this.props.entryInfo.id}
      />,
    );
  };

  clearModal = () => {
    this.props.setModal(null);
  };
}

export type InnerEntry = Entry;

export default connect(
  (state: AppState, ownProps: { entryInfo: EntryInfo }) => ({
    threadInfo: threadInfoSelector(state)[ownProps.entryInfo.threadID],
    loggedIn: !!(
      state.currentUserInfo &&
      !state.currentUserInfo.anonymous &&
      true
    ),
    calendarQuery: nonThreadCalendarQuery(state),
    online: state.connection.status === 'connected',
  }),
  { createEntry, saveEntry, deleteEntry },
)(Entry);
<commit_msg>[web] Don't show buttons for entries when no edit_entries permission<commit_after>// @flow

import {
  type EntryInfo,
  entryInfoPropType,
  type CreateEntryInfo,
  type SaveEntryInfo,
  type SaveEntryResponse,
  type CreateEntryPayload,
  type DeleteEntryInfo,
  type DeleteEntryResponse,
  type CalendarQuery,
} from 'lib/types/entry-types';
import type { ThreadInfo } from 'lib/types/thread-types';
import { threadInfoPropType, threadPermissions } from 'lib/types/thread-types';
import type { LoadingStatus } from 'lib/types/loading-types';
import type { AppState } from '../redux/redux-setup';
import type {
  DispatchActionPayload,
  DispatchActionPromise,
} from 'lib/utils/action-utils';

import * as React from 'react';
import classNames from 'classnames';
import invariant from 'invariant';
import PropTypes from 'prop-types';

import { entryKey } from 'lib/shared/entry-utils';
import { colorIsDark, threadHasPermission } from 'lib/shared/thread-utils';
import { connect } from 'lib/utils/redux-utils';
import {
  createEntryActionTypes,
  createEntry,
  saveEntryActionTypes,
  saveEntry,
  deleteEntryActionTypes,
  deleteEntry,
  concurrentModificationResetActionType,
} from 'lib/actions/entry-actions';
import { ServerError } from 'lib/utils/errors';
import { dateString } from 'lib/utils/date-utils';
import { threadInfoSelector } from 'lib/selectors/thread-selectors';

import css from './calendar.css';
import LoadingIndicator from '../loading-indicator.react';
import ConcurrentModificationModal from '../modals/concurrent-modification-modal.react';
import HistoryModal from '../modals/history/history-modal.react';
import { HistoryVector, DeleteVector } from '../vectors.react';
import LogInFirstModal from '../modals/account/log-in-first-modal.react';
import { nonThreadCalendarQuery } from '../selectors/nav-selectors';

type Props = {|
  innerRef: (key: string, me: Entry) => void,
  entryInfo: EntryInfo,
  focusOnFirstEntryNewerThan: (time: number) => void,
  setModal: (modal: ?React.Node) => void,
  tabIndex: number,
  // Redux state
  threadInfo: ThreadInfo,
  loggedIn: boolean,
  calendarQuery: () => CalendarQuery,
  online: boolean,
  // Redux dispatch functions
  dispatchActionPayload: DispatchActionPayload,
  dispatchActionPromise: DispatchActionPromise,
  // async functions that hit server APIs
  createEntry: (info: CreateEntryInfo) => Promise<CreateEntryPayload>,
  saveEntry: (info: SaveEntryInfo) => Promise<SaveEntryResponse>,
  deleteEntry: (info: DeleteEntryInfo) => Promise<DeleteEntryResponse>,
|};
type State = {|
  focused: boolean,
  loadingStatus: LoadingStatus,
  text: string,
|};
class Entry extends React.PureComponent<Props, State> {
  static propTypes = {
    innerRef: PropTypes.func.isRequired,
    entryInfo: entryInfoPropType.isRequired,
    focusOnFirstEntryNewerThan: PropTypes.func.isRequired,
    setModal: PropTypes.func.isRequired,
    tabIndex: PropTypes.number.isRequired,
    threadInfo: threadInfoPropType.isRequired,
    loggedIn: PropTypes.bool.isRequired,
    calendarQuery: PropTypes.func.isRequired,
    online: PropTypes.bool.isRequired,
    dispatchActionPayload: PropTypes.func.isRequired,
    dispatchActionPromise: PropTypes.func.isRequired,
    createEntry: PropTypes.func.isRequired,
    saveEntry: PropTypes.func.isRequired,
    deleteEntry: PropTypes.func.isRequired,
  };
  textarea: ?HTMLTextAreaElement;
  creating: boolean;
  needsUpdateAfterCreation: boolean;
  needsDeleteAfterCreation: boolean;
  nextSaveAttemptIndex: number;
  mounted: boolean;
  currentlySaving: ?string;

  constructor(props: Props) {
    super(props);
    this.state = {
      focused: false,
      loadingStatus: 'inactive',
      text: props.entryInfo.text,
    };
    this.creating = false;
    this.needsUpdateAfterCreation = false;
    this.needsDeleteAfterCreation = false;
    this.nextSaveAttemptIndex = 0;
  }

  guardedSetState(input: $Shape<State>) {
    if (this.mounted) {
      this.setState(input);
    }
  }

  componentDidMount() {
    this.mounted = true;
    this.props.innerRef(entryKey(this.props.entryInfo), this);
    this.updateHeight();
    // Whenever a new Entry is created, focus on it
    if (!this.props.entryInfo.id) {
      this.focus();
    }
  }

  componentDidUpdate(prevProps: Props) {
    if (
      !this.state.focused &&
      this.props.entryInfo.text !== this.state.text &&
      this.props.entryInfo.text !== prevProps.entryInfo.text
    ) {
      this.setState({ text: this.props.entryInfo.text });
      this.currentlySaving = null;
    }

    if (
      this.props.online &&
      !prevProps.online &&
      this.state.loadingStatus === 'error'
    ) {
      this.save();
    }
  }

  focus() {
    invariant(
      this.textarea instanceof HTMLTextAreaElement,
      'textarea ref not set',
    );
    this.textarea.focus();
  }

  onMouseDown = (event: SyntheticEvent<HTMLDivElement>) => {
    if (this.state.focused && event.target !== this.textarea) {
      // Don't lose focus when some non-textarea part is clicked
      event.preventDefault();
    }
  };

  componentWillUnmount() {
    this.mounted = false;
  }

  updateHeight() {
    invariant(
      this.textarea instanceof HTMLTextAreaElement,
      'textarea ref not set',
    );
    this.textarea.style.height = 'auto';
    this.textarea.style.height = this.textarea.scrollHeight + 'px';
  }

  render() {
    let actionLinks = null;
    if (this.state.focused) {
      let historyButton = null;
      if (this.props.entryInfo.id) {
        historyButton = (

            <HistoryVector className={css.history} />
            History

        );
      }
      const rightActionLinksClassName = `${css.rightActionLinks} ${css.actionLinksText}`;
      actionLinks = (

            <DeleteVector />
            Delete

          {historyButton}

            {this.props.threadInfo.uiName}

      );
    }

    const darkColor = colorIsDark(this.props.threadInfo.color);
    const entryClasses = classNames({
      [css.entry]: true,
      [css.darkEntry]: darkColor,
      [css.focusedEntry]: this.state.focused,
    });
    const style = { backgroundColor: '#' + this.props.threadInfo.color };
    const loadingIndicatorColor = darkColor ? 'white' : 'black';
    const canEditEntry = threadHasPermission(
      this.props.threadInfo,
      threadPermissions.EDIT_ENTRIES,
    );
    return (

        <textarea
          rows="1"
          className={css.entryText}
          onChange={this.onChange}
          onKeyDown={this.onKeyDown}
          value={this.state.text}
          onFocus={this.onFocus}
          onBlur={this.onBlur}
          tabIndex={this.props.tabIndex}
          ref={this.textareaRef}
          disabled={!canEditEntry}
        />
        <LoadingIndicator
          status={this.state.loadingStatus}
          color={loadingIndicatorColor}
          loadingClassName={css.entryLoading}
          errorClassName={css.entryError}
        />
        {actionLinks}

    );
  }

  textareaRef = (textarea: ?HTMLTextAreaElement) => {
    this.textarea = textarea;
  };

  onFocus = () => {
    if (!this.state.focused) {
      this.setState({ focused: true });
    }
  };

  onBlur = () => {
    this.setState({ focused: false });
    if (this.state.text.trim() === '') {
      this.delete();
    } else if (this.props.entryInfo.text !== this.state.text) {
      this.save();
    }
  };

  delete() {
    this.dispatchDelete(this.props.entryInfo.id, false);
  }

  save() {
    this.dispatchSave(this.props.entryInfo.id, this.state.text);
  }

  onChange = (event: SyntheticEvent<HTMLTextAreaElement>) => {
    if (!this.props.loggedIn) {
      this.props.setModal(
        <LogInFirstModal
          inOrderTo="edit this calendar"
          setModal={this.props.setModal}
        />,
      );
      return;
    }
    const target = event.target;
    invariant(target instanceof HTMLTextAreaElement, 'target not textarea');
    this.setState({ text: target.value }, this.updateHeight.bind(this));
  };

  onKeyDown = (event: SyntheticKeyboardEvent<HTMLTextAreaElement>) => {
    if (event.keyCode === 27) {
      invariant(
        this.textarea instanceof HTMLTextAreaElement,
        'textarea ref not set',
      );
      this.textarea.blur();
    }
  };

  dispatchSave(serverID: ?string, newText: string) {
    if (this.currentlySaving === newText) {
      return;
    }
    this.currentlySaving = newText;

    if (newText.trim() === '') {
      // We don't save the empty string, since as soon as the element loses
      // focus it'll get deleted
      return;
    }

    if (!serverID) {
      if (this.creating) {
        // We need the first save call to return so we know the ID of the entry
        // we're updating, so we'll need to handle this save later
        this.needsUpdateAfterCreation = true;
        return;
      } else {
        this.creating = true;
      }
    }

    if (!serverID) {
      this.props.dispatchActionPromise(
        createEntryActionTypes,
        this.createAction(newText),
      );
    } else {
      this.props.dispatchActionPromise(
        saveEntryActionTypes,
        this.saveAction(serverID, newText),
      );
    }
  }

  async createAction(text: string) {
    const localID = this.props.entryInfo.localID;
    invariant(localID, "if there's no serverID, there should be a localID");
    const curSaveAttempt = this.nextSaveAttemptIndex++;
    this.guardedSetState({ loadingStatus: 'loading' });
    try {
      const response = await this.props.createEntry({
        text,
        timestamp: this.props.entryInfo.creationTime,
        date: dateString(
          this.props.entryInfo.year,
          this.props.entryInfo.month,
          this.props.entryInfo.day,
        ),
        threadID: this.props.entryInfo.threadID,
        localID,
        calendarQuery: this.props.calendarQuery(),
      });
      if (curSaveAttempt + 1 === this.nextSaveAttemptIndex) {
        this.guardedSetState({ loadingStatus: 'inactive' });
      }
      this.creating = false;
      if (this.needsUpdateAfterCreation) {
        this.needsUpdateAfterCreation = false;
        this.dispatchSave(response.entryID, this.state.text);
      }
      if (this.needsDeleteAfterCreation) {
        this.needsDeleteAfterCreation = false;
        this.dispatchDelete(response.entryID, false);
      }
      return response;
    } catch (e) {
      if (curSaveAttempt + 1 === this.nextSaveAttemptIndex) {
        this.guardedSetState({ loadingStatus: 'error' });
      }
      this.currentlySaving = null;
      this.creating = false;
      throw e;
    }
  }

  async saveAction(entryID: string, newText: string) {
    const curSaveAttempt = this.nextSaveAttemptIndex++;
    this.guardedSetState({ loadingStatus: 'loading' });
    try {
      const response = await this.props.saveEntry({
        entryID,
        text: newText,
        prevText: this.props.entryInfo.text,
        timestamp: Date.now(),
        calendarQuery: this.props.calendarQuery(),
      });
      if (curSaveAttempt + 1 === this.nextSaveAttemptIndex) {
        this.guardedSetState({ loadingStatus: 'inactive' });
      }
      return { ...response, threadID: this.props.entryInfo.threadID };
    } catch (e) {
      if (curSaveAttempt + 1 === this.nextSaveAttemptIndex) {
        this.guardedSetState({ loadingStatus: 'error' });
      }
      this.currentlySaving = null;
      if (e instanceof ServerError && e.message === 'concurrent_modification') {
        const onRefresh = () => {
          this.setState(
            { loadingStatus: 'inactive' },
            this.updateHeight.bind(this),
          );
          this.props.dispatchActionPayload(
            concurrentModificationResetActionType,
            { id: entryID, dbText: e.payload.db },
          );
          this.clearModal();
        };
        this.props.setModal(
          <ConcurrentModificationModal
            onClose={this.clearModal}
            onRefresh={onRefresh}
          />,
        );
      }
      throw e;
    }
  }

  onDelete = (event: SyntheticEvent<HTMLAnchorElement>) => {
    event.preventDefault();
    if (!this.props.loggedIn) {
      this.props.setModal(
        <LogInFirstModal
          inOrderTo="edit this calendar"
          setModal={this.props.setModal}
        />,
      );
      return;
    }
    this.dispatchDelete(this.props.entryInfo.id, true);
  };

  dispatchDelete(serverID: ?string, focusOnNextEntry: boolean) {
    const { localID } = this.props.entryInfo;
    this.props.dispatchActionPromise(
      deleteEntryActionTypes,
      this.deleteAction(serverID, focusOnNextEntry),
      undefined,
      { localID, serverID },
    );
  }

  async deleteAction(serverID: ?string, focusOnNextEntry: boolean) {
    invariant(
      this.props.loggedIn,
      'user should be logged in if delete triggered',
    );
    if (focusOnNextEntry) {
      this.props.focusOnFirstEntryNewerThan(this.props.entryInfo.creationTime);
    }
    if (serverID) {
      return await this.props.deleteEntry({
        entryID: serverID,
        prevText: this.props.entryInfo.text,
        calendarQuery: this.props.calendarQuery(),
      });
    } else if (this.creating) {
      this.needsDeleteAfterCreation = true;
    }
    return null;
  }

  onHistory = (event: SyntheticEvent<HTMLAnchorElement>) => {
    event.preventDefault();
    this.props.setModal(
      <HistoryModal
        mode="entry"
        dayString={dateString(
          this.props.entryInfo.year,
          this.props.entryInfo.month,
          this.props.entryInfo.day,
        )}
        onClose={this.clearModal}
        currentEntryID={this.props.entryInfo.id}
      />,
    );
  };

  clearModal = () => {
    this.props.setModal(null);
  };
}

export type InnerEntry = Entry;

export default connect(
  (state: AppState, ownProps: { entryInfo: EntryInfo }) => ({
    threadInfo: threadInfoSelector(state)[ownProps.entryInfo.threadID],
    loggedIn: !!(
      state.currentUserInfo &&
      !state.currentUserInfo.anonymous &&
      true
    ),
    calendarQuery: nonThreadCalendarQuery(state),
    online: state.connection.status === 'connected',
  }),
  { createEntry, saveEntry, deleteEntry },
)(Entry);
<|endoftext|>


<commit_before>// creates subscription class
'use strict';

import {validateSubscriber} from "validators";
import {BUS , STRATEGY, SUBSCRIBERS} from "symbols";
import {each, strategies, isDefined} from "utilites";
import {MESSAGE_STRATEGY, MESSAGE_ARGUMENTS} from "messages";

export default class Subscription extends Operation {
  constructor(bus, subscribers) {
    each(subscribers, validateSubscriber);

    this[BUS] = bus;
    this[SUBSCRIBERS] = subscribers;
    this[STRATEGY] = strategies.simultaneously();

    //TODO: Verify the equivalence of the results to the old version
    super(bus, []).onDispose(() => this[SUBSCRIBERS].length = 0)).onTrigger(trigger);
  }
  cyclically() {
    this[STRATEGY] = strategies.cyclically();
    return this;
  }
  get strategy() {
    return this[STRATEGY];
  }
  // returns clone of subscribers array
  get subscribers() {
    return _ArraySlice.call(this[SUBSCRIBERS]);
  }
  randomly() {
    this[STRATEGY] = strategies.randomly();
    return this;
  }
  simultaneously() {
    this[STRATEGY] = strategies.simultaneously();
    return this;
  }
  set strategy(value) {
    let factory = strategies[value];
    if (!factory) throw new Error(MESSAGE_STRATEGY);
    this[STRATEGY] = factory();
  }
  subscribe(subscriber1, subscriber2, subscriberN) {
    if (!arguments.length) throw new Error(MESSAGE_ARGUMENTS);
    each(arguments, validateSubscriber);
    this[SUBSCRIBERS].push(...arguments)
    return this;
  }
  trigger(message, next) {
    each(this[STRATEGY](this[SUBSCRIBERS]), deliver);
    next();

    deliver(subscriber) {
      if (isDefined(message.error)) subscriber(message.error, message);
      else try {
        subscriber(message.data, message);
      } catch (error) {
        this[BUS].error.trigger(new Message(message, error));
      }
    }
  }
  // unsubscribes all specified subscribers from this subscription
  unsubscribe(subscriber1, subscriber2, subscriberN) {
    let subscribers = this[SUBSCRIBERS];
    each(arguments, function(subscriber) {
      let index = subscribers.indexOf(subscriber);
      if (-1 !== index) subscribers.splice(index, 1);
    });
    if (!subscribers.length) subscribers.length = 0;
  }
}
<commit_msg>Remove each partially<commit_after>// creates subscription class
'use strict';

import {validateSubscriber} from "validators";
import {BUS , STRATEGY, SUBSCRIBERS} from "symbols";
import {each, strategies, isDefined} from "utilites";
import {MESSAGE_STRATEGY, MESSAGE_ARGUMENTS} from "messages";

export default class Subscription extends Operation {
  constructor(bus, subscribers) {
    for (let subscriber of subscribers) validateSubscriber(subscriber);

    this[BUS] = bus;
    this[SUBSCRIBERS] = subscribers;
    this[STRATEGY] = strategies.simultaneously();

    //TODO: Verify the equivalence of the results to the old version
    super(bus, []).onDispose(() => this[SUBSCRIBERS].length = 0)).onTrigger(trigger);
  }
  cyclically() {
    this[STRATEGY] = strategies.cyclically();
    return this;
  }
  get strategy() {
    return this[STRATEGY];
  }
  // returns clone of subscribers array
  get subscribers() {
    return _ArraySlice.call(this[SUBSCRIBERS]);
  }
  randomly() {
    this[STRATEGY] = strategies.randomly();
    return this;
  }
  simultaneously() {
    this[STRATEGY] = strategies.simultaneously();
    return this;
  }
  set strategy(value) {
    let factory = strategies[value];
    if (!factory) throw new Error(MESSAGE_STRATEGY);
    this[STRATEGY] = factory();
  }
  subscribe(...subscribers) {
    if (!subscribers.length) throw new Error(MESSAGE_ARGUMENTS);
    for (let subscriber of subscribers) validateSubscriber(subscriber);
    this[SUBSCRIBERS].push(...subscribers)
    return this;
  }
  trigger(message, next) {
    each(this[STRATEGY](this[SUBSCRIBERS]), deliver);
    next();

    deliver(subscriber) {
      if (isDefined(message.error)) subscriber(message.error, message);
      else try {
        subscriber(message.data, message);
      } catch (error) {
        this[BUS].error.trigger(new Message(message, error));
      }
    }
  }
  // unsubscribes all specified subscribers from this subscription
  unsubscribe(...unsubscribers) {
    let subscribers = this[SUBSCRIBERS];
    for (let subscriber of unsubscribers) {
      let index = subscribers.indexOf(subscriber);
      if (-1 !== index) subscribers.splice(index, 1);
    }

    if (!subscribers.length) subscribers.length = 0;
  }
}
<|endoftext|>


<commit_before>// Tally Votes in JavaScript Pairing Challenge.

// I worked on this challenge with:
// This challenge took me [#] hours.

// These are the votes cast by each student. Do not alter these objects here.
var votes = {
  "Alex": { president: "Bob", vicePresident: "Devin", secretary: "Gail", treasurer: "Kerry" },
  "Bob": { president: "Mary", vicePresident: "Hermann", secretary: "Fred", treasurer: "Ivy" },
  "Cindy": { president: "Cindy", vicePresident: "Hermann", secretary: "Bob", treasurer: "Bob" },
  "Devin": { president: "Louise", vicePresident: "John", secretary: "Bob", treasurer: "Fred" },
  "Ernest": { president: "Fred", vicePresident: "Hermann", secretary: "Fred", treasurer: "Ivy" },
  "Fred": { president: "Louise", vicePresident: "Alex", secretary: "Ivy", treasurer: "Ivy" },
  "Gail": { president: "Fred", vicePresident: "Alex", secretary: "Ivy", treasurer: "Bob" },
  "Hermann": { president: "Ivy", vicePresident: "Kerry", secretary: "Fred", treasurer: "Ivy" },
  "Ivy": { president: "Louise", vicePresident: "Hermann", secretary: "Fred", treasurer: "Gail" },
  "John": { president: "Louise", vicePresident: "Hermann", secretary: "Fred", treasurer: "Kerry" },
  "Kerry": { president: "Fred", vicePresident: "Mary", secretary: "Fred", treasurer: "Ivy" },
  "Louise": { president: "Nate", vicePresident: "Alex", secretary: "Mary", treasurer: "Ivy" },
  "Mary": { president: "Louise", vicePresident: "Oscar", secretary: "Nate", treasurer: "Ivy" },
  "Nate": { president: "Oscar", vicePresident: "Hermann", secretary: "Fred", treasurer: "Tracy" },
  "Oscar": { president: "Paulina", vicePresident: "Nate", secretary: "Fred", treasurer: "Ivy" },
  "Paulina": { president: "Louise", vicePresident: "Bob", secretary: "Devin", treasurer: "Ivy" },
  "Quintin": { president: "Fred", vicePresident: "Hermann", secretary: "Fred", treasurer: "Bob" },
  "Romanda": { president: "Louise", vicePresident: "Steve", secretary: "Fred", treasurer: "Ivy" },
  "Steve": { president: "Tracy", vicePresident: "Kerry", secretary: "Oscar", treasurer: "Xavier" },
  "Tracy": { president: "Louise", vicePresident: "Hermann", secretary: "Fred", treasurer: "Ivy" },
  "Ullyses": { president: "Louise", vicePresident: "Hermann", secretary: "Ivy", treasurer: "Bob" },
  "Valorie": { president: "Wesley", vicePresident: "Bob", secretary: "Alex", treasurer: "Ivy" },
  "Wesley": { president: "Bob", vicePresident: "Yvonne", secretary: "Valorie", treasurer: "Ivy" },
  "Xavier": { president: "Steve", vicePresident: "Hermann", secretary: "Fred", treasurer: "Ivy" },
  "Yvonne": { president: "Bob", vicePresident: "Zane", secretary: "Fred", treasurer: "Hermann" },
  "Zane": { president: "Louise", vicePresident: "Hermann", secretary: "Fred", treasurer: "Mary" }
}

// Tally the votes in voteCount.
var voteCount = {
  president: {},
  vicePresident: {},
  secretary: {},
  treasurer: {}
}

/* The name of each student receiving a vote for an office should become a property
of the respective office in voteCount.  After Alex's votes have been tallied,
voteCount would be ...
  var voteCount = {
    president: { Bob: 1 },
    vicePresident: { Devin: 1 },
    secretary: { Gail: 1 },
    treasurer: { Kerry: 1 }
  }
*/

/* Once the votes have been tallied, assign each officer position the name of the
student who received the most votes. */
var officers = {
  president: undefined,
  vicePresident: undefined,
  secretary: undefined,
  treasurer: undefined
}

// Pseudocode

// __________________________________________
// Initial Solution

// __________________________________________
// Refactored Solution

// __________________________________________
// Reflection

// __________________________________________
// Test Code:  Do not alter code below this line.

function assert(test, message, test_number) {
  if (!test) {
    console.log(test_number + "false");
    throw "ERROR: " + message;
  }
  console.log(test_number + "true");
  return true;
}

assert(
  (voteCount.president["Bob"] === 3),
  "Bob should receive three votes for President.",
  "1. "
)

assert(
  (voteCount.vicePresident["Bob"] === 2),
  "Bob should receive two votes for Vice President.",
  "2. "
)

assert(
  (voteCount.secretary["Bob"] === 2),
  "Bob should receive two votes for Secretary.",
  "3. "
)

assert(
  (voteCount.treasurer["Bob"] === 4),
  "Bob should receive four votes for Treasurer.",
  "4. "
)

assert(
  (officers.president === "Louise"),
  "Louise should be elected President.",
  "5. "
)

assert(
  (officers.vicePresident === "Hermann"),
  "Hermann should be elected Vice President.",
  "6. "
)

assert(
  (officers.secretary === "Fred"),
  "Fred should be elected Secretary.",
  "7. "
)

assert(
  (officers.treasurer === "Ivy"),
  "Ivy should be elected Treasurer.",
  "8. "
<commit_msg>Create 8.3 Tally votes<commit_after>// I worked on this challenge with:
// This challenge took me [#] hours.

// These are the votes cast by each student. Do not alter these objects here.
var votes = {
  "Alex": { president: "Bob",
           vicePresident: "Devin",
           secretary: "Gail",
           treasurer: "Kerry" },

  "Bob": { president: "Mary",
          vicePresident: "Hermann",
          secretary: "Fred",
          treasurer: "Ivy" },

  "Cindy": { president: "Cindy",
            vicePresident: "Hermann",
            secretary: "Bob",
            treasurer: "Bob" },

  "Devin": { president: "Louise",
            vicePresident: "John",
            secretary: "Bob",
            treasurer: "Fred" },

  "Ernest": { president: "Fred",
             vicePresident: "Hermann",
             secretary: "Fred",
             treasurer: "Ivy" },

  "Fred": { president: "Louise",
           vicePresident: "Alex",
           secretary: "Ivy",
           treasurer: "Ivy" },

  "Gail": { president: "Fred", vicePresident: "Alex", secretary: "Ivy", treasurer: "Bob" },
  "Hermann": { president: "Ivy", vicePresident: "Kerry", secretary: "Fred", treasurer: "Ivy" },
  "Ivy": { president: "Louise", vicePresident: "Hermann", secretary: "Fred", treasurer: "Gail" },
  "John": { president: "Louise", vicePresident: "Hermann", secretary: "Fred", treasurer: "Kerry" },
  "Kerry": { president: "Fred", vicePresident: "Mary", secretary: "Fred", treasurer: "Ivy" },
  "Louise": { president: "Nate", vicePresident: "Alex", secretary: "Mary", treasurer: "Ivy" },
  "Mary": { president: "Louise", vicePresident: "Oscar", secretary: "Nate", treasurer: "Ivy" },
  "Nate": { president: "Oscar", vicePresident: "Hermann", secretary: "Fred", treasurer: "Tracy" },
  "Oscar": { president: "Paulina", vicePresident: "Nate", secretary: "Fred", treasurer: "Ivy" },
  "Paulina": { president: "Louise", vicePresident: "Bob", secretary: "Devin", treasurer: "Ivy" },
  "Quintin": { president: "Fred", vicePresident: "Hermann", secretary: "Fred", treasurer: "Bob" },
  "Romanda": { president: "Louise", vicePresident: "Steve", secretary: "Fred", treasurer: "Ivy" },
  "Steve": { president: "Tracy", vicePresident: "Kerry", secretary: "Oscar", treasurer: "Xavier" },
  "Tracy": { president: "Louise", vicePresident: "Hermann", secretary: "Fred", treasurer: "Ivy" },
  "Ullyses": { president: "Louise", vicePresident: "Hermann", secretary: "Ivy", treasurer: "Bob" },
  "Valorie": { president: "Wesley", vicePresident: "Bob", secretary: "Alex", treasurer: "Ivy" },
  "Wesley": { president: "Bob", vicePresident: "Yvonne", secretary: "Valorie", treasurer: "Ivy" },
  "Xavier": { president: "Steve", vicePresident: "Hermann", secretary: "Fred", treasurer: "Ivy" },
  "Yvonne": { president: "Bob", vicePresident: "Zane", secretary: "Fred", treasurer: "Hermann" },
  "Zane": { president: "Louise", vicePresident: "Hermann", secretary: "Fred", treasurer: "Mary" }
};

//Tally the votes in voteCount.
var voteCount = {

  president: {"Bob": 0,
             "Mary": 0,
             "Cindy": 0,
              "Louise":0,
              "Fred": 0,
              "Steve": 0,
              "Wesley": 0,
              "Tracy":  0,
              "Paulina": 0,
              "Ivy": 0,},

  vicePresident: {"Devin": 0,
                 "Hermann": 0,
                 "John": 0,
                 "Alex": 0,
                 "Kerry": 0,
                 "Mary": 0,
                 "Oscar": 0,
                 "Nate": 0,
                 "Steve": 0,
                 "Yvonne": 0,
                 "Zane": 0,},

  secretary: {"Devin": 0,
                 "Gail": 0,
                 "Fred": 0,
                 "Alex": 0,
                 "Bob": 0,
                 "Mary": 0,
                 "Oscar": 0,
                 "Nate": 0,
                 "Ivy": 0,
                 "Valorie": 0,},

  treasurer: {"Kerry": 0,
                 "Ivy": 0,
                 "Bob": 0,
                 "Fred": 0,
                 "Gail": 0,
                 "Tracy": 0,
                 "Xavier": 0,
                 "Hermann": 0,
                 "Mary": 0,},}

//votes[name]["president"][electee] === votecount["president"][name] --> update value
//votes["Bob"]["President"]

for (var name in position in votes) {
     var name = votes[president];
       if (voteCount.president.name == "Bob") {
         voteCount.president.Bob += 1;
         console.log(voteCount);
       };
};

var vote = (votes.Bob.president);
voteCount.president.vote += 1;
console.log(voteCount.president.vote)

// for(var category in window.books) {
//   if(window.books.hasOwnProperty(category)) {
//     console.log(category); // will log "Business Books" etc.
//     for (var i = 0, j = window.books[category].length; i < j; i++) {
//       console.log("Title: %s, Description: %s", window.books[category][i].Title, window.books[category][i].Description);
//     }
//   }
// }

// // if (condition) {
// //     block of code to be executed if the condition is true
// // }
// console.log(voteCount.president.Bob);

 /* The name of each student receiving a vote for an office should become a property
of the respective office in voteCount.  After Alex's votes have been tallied,
voteCount would be ...
  var voteCount = {
    president: { Bob: 1 },
    vicePresident: { Devin: 1 },
    secretary: { Gail: 1 },
    treasurer: { Kerry: 1 }
  }
*/

/* Once the votes have been tallied, assign each officer position the name of the
student who received the most votes. */
// var officers = {
//   president: undefined,
//   vicePresident: undefined,
//   secretary: undefined,
//   treasurer: undefined
// }

// Pseudocode
/*

*/

// __________________________________________
// Initial Solution

// __________________________________________
// Refactored Solution

// __________________________________________
// Reflection

// __________________________________________
// Test Code:  Do not alter code below this line.

function assert(test, message, test_number) {
  if (!test) {
    console.log(test_number + "false");
    throw "ERROR: " + message;
  }
  console.log(test_number + "true");
  return true;
}

assert(
  (voteCount.president["Bob"] === 3),
  "Bob should receive three votes for President.",
  "1. "
)

assert(
  (voteCount.vicePresident["Bob"] === 2),
  "Bob should receive two votes for Vice President.",
  "2. "
)

assert(
  (voteCount.secretary["Bob"] === 2),
  "Bob should receive two votes for Secretary.",
  "3. "
)

assert(
  (voteCount.treasurer["Bob"] === 4),
  "Bob should receive four votes for Treasurer.",
  "4. "
)

assert(
  (officers.president === "Louise"),
  "Louise should be elected President.",
  "5. "
)

assert(
  (officers.vicePresident === "Hermann"),
  "Hermann should be elected Vice President.",
  "6. "
)

assert(
  (officers.secretary === "Fred"),
  "Fred should be elected Secretary.",
  "7. "
)

assert(
  (officers.treasurer === "Ivy"),
  "Ivy should be elected Treasurer.",
  "8. "
)<|endoftext|>