<commit_before>/***** BEGIN LICENSE BLOCK ***** {{{
Version: MPL 1.1/GPL 2.0/LGPL 2.1

The contents of this file are subject to the Mozilla Public License Version
1.1 (the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.mozilla.org/MPL/

Software distributed under the License is distributed on an "AS IS" basis,
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
for the specific language governing rights and limitations under the
License.

(c) 2006-2008: Martin Stubenschrott <stubenschrott@gmx.net>

Alternatively, the contents of this file may be used under the terms of
either the GNU General Public License Version 2 or later (the "GPL"), or
the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
in which case the provisions of the GPL or the LGPL are applicable instead
of those above. If you wish to allow use of your version of this file only
under the terms of either the GPL or the LGPL, and not to allow others to
use your version of this file under the terms of the MPL, indicate your
decision by deleting the provisions above and replace them with the notice
and other provisions required by the GPL or the LGPL. If you do not delete
the provisions above, a recipient may use your version of this file under
the terms of any one of the MPL, the GPL or the LGPL.
}}} ***** END LICENSE BLOCK *****/

// Do NOT create instances of this class yourself, use the helper method
// mappings.add() instead
function Map(modes, cmds, description, action, extraInfo) //{{{
{
    if (!modes || (!cmds || !cmds.length) || !action)
        return null;

    if (!extraInfo)
        extraInfo = {};

    this.modes = modes;
    // only store keysyms with uppercase modifier strings
    this.names = cmds.map(function (cmd) cmd.replace(/[casm]-/g, String.toUpperCase));
    this.action = action;

    this.flags = extraInfo.flags || 0;
    this.description = description || "";
    this.rhs = extraInfo.rhs || null;
    this.noremap = extraInfo.noremap || false;
    this.silent = extraInfo.silent || false;
};

Map.prototype = {

    hasName: function (name)
    {
        return this.names.indexOf(name) >= 0;
    },

    execute: function (motion, count, argument)
    {
        var args = [];

        if (this.flags & Mappings.flags.MOTION)
            args.push(motion);
        if (this.flags & Mappings.flags.COUNT)
            args.push(count);
        if (this.flags & Mappings.flags.ARGUMENT)
            args.push(argument);

        let self = this;
        // FIXME: Kludge.
        if (this.names[0] != ".")
            mappings.repeat = function () self.action.apply(self, args);

        return this.action.apply(this, args);
    }

}; //}}}

function Mappings() //{{{
{
    ////////////////////////////////////////////////////////////////////////////////
    ////////////////////// PRIVATE SECTION /////////////////////////////////////////
    /////////////////////////////////////////////////////////////////////////////{{{

    var main = []; // default mappings
    var user = []; // user created mappings

    for (let mode in modes)
    {
        main[mode] = [];
        user[mode] = [];
    }

    function addMap(map, userMap)
    {
        var where = userMap ? user : main;
        map.modes.forEach(function (mode) {
                if (!(mode in where))
                    where[mode] = [];
                where[mode].push(map);
        });
    }

    function getMap(mode, cmd, stack)
    {
        var maps = stack[mode] || [];

        for (let [,map] in Iterator(maps))
        {
            if (map.hasName(cmd))
                return map;
        }

        return null;
    }

    function removeMap(mode, cmd)
    {
        var maps = user[mode] || [];
        var names;

        for (let [i, map] in Iterator(maps))
        {
            for (let [j, name] in Iterator(map.names))
            {
                if (name == cmd)
                {
                    map.names.splice(j, 1);
                    if (map.names.length == 0)
                        maps.splice(i, 1);
                    return;
                }
            }
        }
    }

    function expandLeader(keyString) keyString.replace(/<Leader>/i, mappings.getMapLeader())

    function mappingsIterator(modes, stack)
    {
        modes = modes.slice();
        return (map for ([i, map] in Iterator(stack[modes.shift()]))
            if (modes.every(function (mode) stack[mode].some(
                        function (m) m.rhs == map.rhs && m.names[0] == map.names[0]))))
    }

    function addMapCommands(ch, modes, modeDescription)
    {
        // 0 args -> list all maps
        // 1 arg  -> list the maps starting with args
        // 2 args -> map arg1 to arg*
        function map(args, mode, noremap)
        {
            if (!args.arguments.length)
            {
                mappings.list(mode);
                return;
            }

            // ?:\s+ <- don't remember; (...)? optional = rhs
            let [lhs, rhs] = args.arguments;

            if (!rhs) // list the mapping
            {
                mappings.list(mode, expandLeader(lhs));
            }
            else
            {
                for (let [,m] in Iterator(mode))
                {
                    mappings.addUserMap([m], [lhs],
                            "User defined mapping",
                            function (count) { events.feedkeys((count > 1 ? count : "") + this.rhs, this.noremap, this.silent); },
                            {
                                flags: Mappings.flags.COUNT,
                                rhs: rhs,
                                noremap: !!noremap,
                                silent: "<silent>" in args
                            });
                }
            }
        }

        modeDescription = modeDescription ? " in " + modeDescription + " mode" : "";

        const opts = {
                argCount: 2,
                completer: function (context, args) completion.userMapping(context, args, modes),
                options: [
                    [["<silent>", "<Silent>"],  commands.OPTION_NOARG]
                ],
                literal: true,
                serial: function () {
                    let noremap = this.name.indexOf("noremap") > -1;
                    return [
                        {
                            command: this.name,
                            options: map.silent ? {"<silent>": null} : {},
                            arguments: [map.names[0]],
                            literalArg: map.rhs
                        }
                        for (map in mappingsIterator(modes, user))
                        if (map.rhs && map.noremap == noremap)
                    ]
                }
        };

        commands.add([ch ? ch + "m[ap]" : "map"],
            "Map a key sequence" + modeDescription,
            function (args) { map(args, modes, false); },
            opts);

        commands.add([ch + "no[remap]"],
            "Map a key sequence without remapping keys" + modeDescription,
            function (args) { map(args, modes, true); },
            opts);

        commands.add([ch + "mapc[lear]"],
            "Remove all mappings" + modeDescription,
            function () { modes.forEach(function (mode) { mappings.removeAll(mode); }); },
            { argCount: "0" });

        commands.add([ch + "unm[ap]"],
            "Remove a mapping" + modeDescription,
            function (args)
            {
                args = args.arguments[1];

                let found = false;
                for (let [,mode] in Iterator(modes))
                {
                    if (mappings.hasMap(mode, args))
                    {
                        mappings.remove(mode, args);
                        found = true;
                    }
                }
                if (!found)
                    liberator.echoerr("E31: No such mapping");
            },
            {
                argCount: "1",
                completer: function (context, args) completion.userMapping(context, args, modes)
            });
    }

    /////////////////////////////////////////////////////////////////////////////}}}
    ////////////////////// COMMANDS ////////////////////////////////////////////////
    /////////////////////////////////////////////////////////////////////////////{{{

    addMapCommands("",  [modes.NORMAL], "");
    addMapCommands("c", [modes.COMMAND_LINE], "command line");
    addMapCommands("i", [modes.INSERT, modes.TEXTAREA], "insert");
    if (liberator.has("mail"))
        addMapCommands("m", [modes.MESSAGE], "message");

    /////////////////////////////////////////////////////////////////////////////}}}
    ////////////////////// PUBLIC SECTION //////////////////////////////////////////
    /////////////////////////////////////////////////////////////////////////////{{{

    liberator.registerObserver("load_completion", function ()
    {
        completion.setFunctionCompleter(mappings.get,
        [
            null,
            function (obj, filter, args)
            {
                let mode = this.eval(args[0]);
                return util.Array.flatten(
                [
                    [[name, map.description] for ([i, name] in Iterator(map.names))]
                    for ([i, map] in Iterator(user[mode].concat(main[mode])))
                ])
            }
        ]);
    });

    // FIXME:
    Mappings.flags = {
        ALLOW_EVENT_ROUTING: 1 << 0, // if set, return true inside the map command to pass the event further to firefox
        MOTION:              1 << 1,
        COUNT:               1 << 2,
        ARGUMENT:            1 << 3
    };

    return {

        // NOTE: just normal mode for now
        __iterator__: function () mappingsIterator([modes.NORMAL], main),

        // used by :mkvimperatorrc to save mappings
        getUserIterator: function (mode) mappingsIterator(mode, user),

        add: function (modes, keys, description, action, extra)
        {
            addMap(new Map(modes, keys, description, action, extra), false);
        },

        addUserMap: function (modes, keys, description, action, extra)
        {
            keys = keys.map(function (key) expandLeader(key));
            var map = new Map(modes, keys, description || "User defined mapping", action, extra);

            // remove all old mappings to this key sequence
            for (let i = 0; i < map.names.length; i++)
            {
                for (let j = 0; j < map.modes.length; j++)
                    removeMap(map.modes[j], map.names[i]);
            }

            addMap(map, true);
        },

        get: function (mode, cmd)
        {
            mode = mode || modes.NORMAL;
            return getMap(mode, cmd, user) || getMap(mode, cmd, main);
        },

        getDefault: function (mode, cmd)
        {
            mode = mode || modes.NORMAL;
            return getMap(mode, cmd, main);
        },

        // returns an array of mappings with names which START with "cmd" (but are NOT "cmd")
        getCandidates: function (mode, cmd)
        {
            let mappings = user[mode].concat(main[mode]);
            let matches = [];

            for (let [,map] in Iterator(mappings))
            {
                for (let [,name] in Iterator(map.names))
                {
                    if (name.indexOf(cmd) == 0 && name.length > cmd.length)
                    {
                        // for < only return a candidate if it doesn't look like a <c-x> mapping
                        if (cmd != "<" || !/^<.+>/.test(name))
                            matches.push(map);
                    }
                }
            }

            return matches;
        },

        getMapLeader: function ()
        {
            var leaderRef = liberator.variableReference("mapleader");
            return leaderRef[0] ? leaderRef[0][leaderRef[1]] : "\\";
        },

        // returns whether the user added a custom user map
        hasMap: function (mode, cmd)
        {
            return user[mode].some(function (map) map.hasName(cmd));
        },

        remove: function (mode, cmd)
        {
            removeMap(mode, cmd);
        },

        removeAll: function (mode)
        {
            user[mode] = [];
        },

        list: function (modes, filter)
        {
            // modes means, a map must exist in both modes in order to get listed
            var maps = user[modes[0]]; // duplicate (reference) (first mode where it must match)
            var output = [];

            if (!maps || maps.length == 0)
            {
                liberator.echo("No mappings found");
                return;
            }

            for (let [i, map] in Iterator(maps)) // check on maps items (first mode)
            {
                output[i] = !filter || map.names[0] == filter;
                if (!output[i]) // does it match the filter first of all?
                    continue;
                for (let [, mode] in Iterator(modes))
                {
                    output[i] = false; // toggle false, only true whan also found in this mode
                    for (let [, usermode] in Iterator(user[mode]))
                    {
                        // NOTE: when other than user maps, there might be more than only one names[x].
                        //       since only user mappings gets queried here, only names[0] gets checked for equality.
                        if (map.rhs == usermode.rhs && map.names[0] == usermode.names[0])
                        {
                            output[i] = true;
                            break; // found on this mode - ok, check next mode...
                        }
                    }
                    break; // not found in this mode -> map wont' match all modes...
                }
            }

            // anything found?
            var flag = output.some(function (x) x);
            if (!flag)
            {
                liberator.echo("No mappings found");
                return;
            }

            var modeSign = "";
            modes.forEach(function (mode)
            {
                if (mode == modes.NORMAL)
                    modeSign += "n";
                if ((mode == modes.INSERT || mode == modes.TEXTAREA) && modeSign.indexOf("i") == -1)
                    modeSign += "i";
                if (mode == modes.COMMAND_LINE)
                    modeSign += "c";
                if (mode == modes.MESSAGRE)
                    modeSign += "m";
            });

            let _maps = (map for ([i, map] in Iterator(maps))
                             if (output[i]));
            let list =

                    {
                        template.map(_maps, function (map)
                            template.map(map.names, function (name)

                                {modeSign} {name}
                                {map.noremap ? "*" : " "}
                                {map.rhs || "function () { ... }"}

))
                    }

;
            commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE);
        }

    };
    //}}}
}; //}}}

// vim: set fdm=marker sw=4 ts=4 et:
<commit_msg>fix broken :unmap<commit_after>/***** BEGIN LICENSE BLOCK ***** {{{
Version: MPL 1.1/GPL 2.0/LGPL 2.1

The contents of this file are subject to the Mozilla Public License Version
1.1 (the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.mozilla.org/MPL/

Software distributed under the License is distributed on an "AS IS" basis,
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
for the specific language governing rights and limitations under the
License.

(c) 2006-2008: Martin Stubenschrott <stubenschrott@gmx.net>

Alternatively, the contents of this file may be used under the terms of
either the GNU General Public License Version 2 or later (the "GPL"), or
the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
in which case the provisions of the GPL or the LGPL are applicable instead
of those above. If you wish to allow use of your version of this file only
under the terms of either the GPL or the LGPL, and not to allow others to
use your version of this file under the terms of the MPL, indicate your
decision by deleting the provisions above and replace them with the notice
and other provisions required by the GPL or the LGPL. If you do not delete
the provisions above, a recipient may use your version of this file under
the terms of any one of the MPL, the GPL or the LGPL.
}}} ***** END LICENSE BLOCK *****/

// Do NOT create instances of this class yourself, use the helper method
// mappings.add() instead
function Map(modes, cmds, description, action, extraInfo) //{{{
{
    if (!modes || (!cmds || !cmds.length) || !action)
        return null;

    if (!extraInfo)
        extraInfo = {};

    this.modes = modes;
    // only store keysyms with uppercase modifier strings
    this.names = cmds.map(function (cmd) cmd.replace(/[casm]-/g, String.toUpperCase));
    this.action = action;

    this.flags = extraInfo.flags || 0;
    this.description = description || "";
    this.rhs = extraInfo.rhs || null;
    this.noremap = extraInfo.noremap || false;
    this.silent = extraInfo.silent || false;
};

Map.prototype = {

    hasName: function (name)
    {
        return this.names.indexOf(name) >= 0;
    },

    execute: function (motion, count, argument)
    {
        var args = [];

        if (this.flags & Mappings.flags.MOTION)
            args.push(motion);
        if (this.flags & Mappings.flags.COUNT)
            args.push(count);
        if (this.flags & Mappings.flags.ARGUMENT)
            args.push(argument);

        let self = this;
        // FIXME: Kludge.
        if (this.names[0] != ".")
            mappings.repeat = function () self.action.apply(self, args);

        return this.action.apply(this, args);
    }

}; //}}}

function Mappings() //{{{
{
    ////////////////////////////////////////////////////////////////////////////////
    ////////////////////// PRIVATE SECTION /////////////////////////////////////////
    /////////////////////////////////////////////////////////////////////////////{{{

    var main = []; // default mappings
    var user = []; // user created mappings

    for (let mode in modes)
    {
        main[mode] = [];
        user[mode] = [];
    }

    function addMap(map, userMap)
    {
        var where = userMap ? user : main;
        map.modes.forEach(function (mode) {
                if (!(mode in where))
                    where[mode] = [];
                where[mode].push(map);
        });
    }

    function getMap(mode, cmd, stack)
    {
        var maps = stack[mode] || [];

        for (let [,map] in Iterator(maps))
        {
            if (map.hasName(cmd))
                return map;
        }

        return null;
    }

    function removeMap(mode, cmd)
    {
        var maps = user[mode] || [];
        var names;

        for (let [i, map] in Iterator(maps))
        {
            for (let [j, name] in Iterator(map.names))
            {
                if (name == cmd)
                {
                    map.names.splice(j, 1);
                    if (map.names.length == 0)
                        maps.splice(i, 1);
                    return;
                }
            }
        }
    }

    function expandLeader(keyString) keyString.replace(/<Leader>/i, mappings.getMapLeader())

    function mappingsIterator(modes, stack)
    {
        modes = modes.slice();
        return (map for ([i, map] in Iterator(stack[modes.shift()]))
            if (modes.every(function (mode) stack[mode].some(
                        function (m) m.rhs == map.rhs && m.names[0] == map.names[0]))))
    }

    function addMapCommands(ch, modes, modeDescription)
    {
        // 0 args -> list all maps
        // 1 arg  -> list the maps starting with args
        // 2 args -> map arg1 to arg*
        function map(args, mode, noremap)
        {
            if (!args.arguments.length)
            {
                mappings.list(mode);
                return;
            }

            // ?:\s+ <- don't remember; (...)? optional = rhs
            let [lhs, rhs] = args.arguments;

            if (!rhs) // list the mapping
            {
                mappings.list(mode, expandLeader(lhs));
            }
            else
            {
                for (let [,m] in Iterator(mode))
                {
                    mappings.addUserMap([m], [lhs],
                            "User defined mapping",
                            function (count) { events.feedkeys((count > 1 ? count : "") + this.rhs, this.noremap, this.silent); },
                            {
                                flags: Mappings.flags.COUNT,
                                rhs: rhs,
                                noremap: !!noremap,
                                silent: "<silent>" in args
                            });
                }
            }
        }

        modeDescription = modeDescription ? " in " + modeDescription + " mode" : "";

        const opts = {
                argCount: 2,
                completer: function (context, args) completion.userMapping(context, args, modes),
                options: [
                    [["<silent>", "<Silent>"],  commands.OPTION_NOARG]
                ],
                literal: true,
                serial: function () {
                    let noremap = this.name.indexOf("noremap") > -1;
                    return [
                        {
                            command: this.name,
                            options: map.silent ? {"<silent>": null} : {},
                            arguments: [map.names[0]],
                            literalArg: map.rhs
                        }
                        for (map in mappingsIterator(modes, user))
                        if (map.rhs && map.noremap == noremap)
                    ]
                }
        };

        commands.add([ch ? ch + "m[ap]" : "map"],
            "Map a key sequence" + modeDescription,
            function (args) { map(args, modes, false); },
            opts);

        commands.add([ch + "no[remap]"],
            "Map a key sequence without remapping keys" + modeDescription,
            function (args) { map(args, modes, true); },
            opts);

        commands.add([ch + "mapc[lear]"],
            "Remove all mappings" + modeDescription,
            function () { modes.forEach(function (mode) { mappings.removeAll(mode); }); },
            { argCount: "0" });

        commands.add([ch + "unm[ap]"],
            "Remove a mapping" + modeDescription,
            function (args)
            {
                args = args.arguments[0];

                let found = false;
                for (let [,mode] in Iterator(modes))
                {
                    if (mappings.hasMap(mode, args))
                    {
                        mappings.remove(mode, args);
                        found = true;
                    }
                }
                if (!found)
                    liberator.echoerr("E31: No such mapping");
            },
            {
                argCount: "1",
                completer: function (context, args) completion.userMapping(context, args, modes)
            });
    }

    /////////////////////////////////////////////////////////////////////////////}}}
    ////////////////////// COMMANDS ////////////////////////////////////////////////
    /////////////////////////////////////////////////////////////////////////////{{{

    addMapCommands("",  [modes.NORMAL], "");
    addMapCommands("c", [modes.COMMAND_LINE], "command line");
    addMapCommands("i", [modes.INSERT, modes.TEXTAREA], "insert");
    if (liberator.has("mail"))
        addMapCommands("m", [modes.MESSAGE], "message");

    /////////////////////////////////////////////////////////////////////////////}}}
    ////////////////////// PUBLIC SECTION //////////////////////////////////////////
    /////////////////////////////////////////////////////////////////////////////{{{

    liberator.registerObserver("load_completion", function ()
    {
        completion.setFunctionCompleter(mappings.get,
        [
            null,
            function (obj, filter, args)
            {
                let mode = this.eval(args[0]);
                return util.Array.flatten(
                [
                    [[name, map.description] for ([i, name] in Iterator(map.names))]
                    for ([i, map] in Iterator(user[mode].concat(main[mode])))
                ])
            }
        ]);
    });

    // FIXME:
    Mappings.flags = {
        ALLOW_EVENT_ROUTING: 1 << 0, // if set, return true inside the map command to pass the event further to firefox
        MOTION:              1 << 1,
        COUNT:               1 << 2,
        ARGUMENT:            1 << 3
    };

    return {

        // NOTE: just normal mode for now
        __iterator__: function () mappingsIterator([modes.NORMAL], main),

        // used by :mkvimperatorrc to save mappings
        getUserIterator: function (mode) mappingsIterator(mode, user),

        add: function (modes, keys, description, action, extra)
        {
            addMap(new Map(modes, keys, description, action, extra), false);
        },

        addUserMap: function (modes, keys, description, action, extra)
        {
            keys = keys.map(function (key) expandLeader(key));
            var map = new Map(modes, keys, description || "User defined mapping", action, extra);

            // remove all old mappings to this key sequence
            for (let i = 0; i < map.names.length; i++)
            {
                for (let j = 0; j < map.modes.length; j++)
                    removeMap(map.modes[j], map.names[i]);
            }

            addMap(map, true);
        },

        get: function (mode, cmd)
        {
            mode = mode || modes.NORMAL;
            return getMap(mode, cmd, user) || getMap(mode, cmd, main);
        },

        getDefault: function (mode, cmd)
        {
            mode = mode || modes.NORMAL;
            return getMap(mode, cmd, main);
        },

        // returns an array of mappings with names which START with "cmd" (but are NOT "cmd")
        getCandidates: function (mode, cmd)
        {
            let mappings = user[mode].concat(main[mode]);
            let matches = [];

            for (let [,map] in Iterator(mappings))
            {
                for (let [,name] in Iterator(map.names))
                {
                    if (name.indexOf(cmd) == 0 && name.length > cmd.length)
                    {
                        // for < only return a candidate if it doesn't look like a <c-x> mapping
                        if (cmd != "<" || !/^<.+>/.test(name))
                            matches.push(map);
                    }
                }
            }

            return matches;
        },

        getMapLeader: function ()
        {
            var leaderRef = liberator.variableReference("mapleader");
            return leaderRef[0] ? leaderRef[0][leaderRef[1]] : "\\";
        },

        // returns whether the user added a custom user map
        hasMap: function (mode, cmd)
        {
            return user[mode].some(function (map) map.hasName(cmd));
        },

        remove: function (mode, cmd)
        {
            removeMap(mode, cmd);
        },

        removeAll: function (mode)
        {
            user[mode] = [];
        },

        list: function (modes, filter)
        {
            // modes means, a map must exist in both modes in order to get listed
            var maps = user[modes[0]]; // duplicate (reference) (first mode where it must match)
            var output = [];

            if (!maps || maps.length == 0)
            {
                liberator.echo("No mappings found");
                return;
            }

            for (let [i, map] in Iterator(maps)) // check on maps items (first mode)
            {
                output[i] = !filter || map.names[0] == filter;
                if (!output[i]) // does it match the filter first of all?
                    continue;
                for (let [, mode] in Iterator(modes))
                {
                    output[i] = false; // toggle false, only true whan also found in this mode
                    for (let [, usermode] in Iterator(user[mode]))
                    {
                        // NOTE: when other than user maps, there might be more than only one names[x].
                        //       since only user mappings gets queried here, only names[0] gets checked for equality.
                        if (map.rhs == usermode.rhs && map.names[0] == usermode.names[0])
                        {
                            output[i] = true;
                            break; // found on this mode - ok, check next mode...
                        }
                    }
                    break; // not found in this mode -> map wont' match all modes...
                }
            }

            // anything found?
            var flag = output.some(function (x) x);
            if (!flag)
            {
                liberator.echo("No mappings found");
                return;
            }

            var modeSign = "";
            modes.forEach(function (mode)
            {
                if (mode == modes.NORMAL)
                    modeSign += "n";
                if ((mode == modes.INSERT || mode == modes.TEXTAREA) && modeSign.indexOf("i") == -1)
                    modeSign += "i";
                if (mode == modes.COMMAND_LINE)
                    modeSign += "c";
                if (mode == modes.MESSAGRE)
                    modeSign += "m";
            });

            let _maps = (map for ([i, map] in Iterator(maps))
                             if (output[i]));
            let list =

                    {
                        template.map(_maps, function (map)
                            template.map(map.names, function (name)

                                {modeSign} {name}
                                {map.noremap ? "*" : " "}
                                {map.rhs || "function () { ... }"}

))
                    }

;
            commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE);
        }

    };
    //}}}
}; //}}}

// vim: set fdm=marker sw=4 ts=4 et:
<|endoftext|>


<commit_before>import React from "react";
import PropTypes from "prop-types";
import { assign, omit, defaults, uniqueId } from "lodash";
import Portal from "../victory-portal/portal";
import Timer from "../victory-util/timer";

export default class VictoryContainer extends React.Component {
  static displayName = "VictoryContainer";
  static role = "container";
  static propTypes = {
    children: PropTypes.oneOfType([
      PropTypes.arrayOf(PropTypes.node),
      PropTypes.node
    ]),
    className: PropTypes.string,
    containerId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
    desc: PropTypes.string,
    events: PropTypes.object,
    height: PropTypes.number,
    origin: PropTypes.shape({ x: PropTypes.number, y: PropTypes.number }),
    polar: PropTypes.bool,
    portalComponent: PropTypes.element,
    responsive: PropTypes.bool,
    style: PropTypes.object,
    theme: PropTypes.object,
    title: PropTypes.string,
    width: PropTypes.number
  }

  static defaultProps = {
    portalComponent: <Portal/>,
    responsive: true
  }

  static contextTypes = {
    getTimer: PropTypes.func
  }

  static childContextTypes = {
    portalUpdate: PropTypes.func,
    portalRegister: PropTypes.func,
    portalDeregister: PropTypes.func,
    getTimer: PropTypes.func
  }

  constructor(props) {
    super(props);
    this.getTimer = this.getTimer.bind(this);
    this.containerId = typeof this.props.containerId === "undefined" ?
      uniqueId("victory-container-") : this.props.containerId;
  }

  getChildContext() {
    return {
      portalUpdate: this.portalUpdate,
      portalRegister: this.portalRegister,
      portalDeregister: this.portalDeregister,
      getTimer: this.getTimer
    };
  }

  componentWillMount() {
    this.savePortalRef = (portal) => {
      this.portalRef = portal;
      return portal;
    };
    this.portalUpdate = (key, el) => this.portalRef.portalUpdate(key, el);
    this.portalRegister = () => this.portalRef.portalRegister();
    this.portalDeregister = (key) => this.portalRef.portalDeregister(key);
  }

  componentWillUnmount() {
    if (!this.context.getTimer) {
      this.getTimer().stop();
    }
  }

  getTimer() {
    if (this.context.getTimer) {
      return this.context.getTimer();
    }
    if (!this.timer) {
      this.timer = new Timer();
    }
    return this.timer;
  }

  getIdForElement(elementName) {
    return `${this.containerId}-${elementName}`;
  }

  // overridden in custom containers
  getChildren(props) {
    return props.children;
  }

  // Overridden in victory-core-native
  renderContainer(props, svgProps, style) {
    const { title, desc, portalComponent, className } = props;
    const children = this.getChildren(props);
    const parentProps = defaults({ style, className }, svgProps);
    return (
      <svg {...parentProps} overflow="visible">
        <svg {...parentProps}>
          {children}
        </svg>
          {title ? <title id={this.getIdForElement("title")}>{title}</title> : null}
          {desc ? <desc id={this.getIdForElement("desc")}>{desc}</desc> : null}
        {React.cloneElement(portalComponent, { ref: this.savePortalRef })}
      </svg>
    );
  }

  render() {
    const { width, height, responsive, events } = this.props;
    const style = responsive ? this.props.style : omit(this.props.style, ["height", "width"]);
    const touchStyle = assign({}, style, { touchAction: "none" });
    const svgProps = assign(
      {
        width, height, role: "img",
        "aria-labelledby": `${this.getIdForElement("title")} ${this.getIdForElement("desc")}`,
        viewBox: responsive ? `0 0 ${width} ${height}` : undefined
      },
      events
    );
    return this.renderContainer(this.props, svgProps, touchStyle);
  }
}
<commit_msg>allow users to override touchAction style<commit_after>import React from "react";
import PropTypes from "prop-types";
import { assign, omit, defaults, uniqueId } from "lodash";
import Portal from "../victory-portal/portal";
import Timer from "../victory-util/timer";

export default class VictoryContainer extends React.Component {
  static displayName = "VictoryContainer";
  static role = "container";
  static propTypes = {
    children: PropTypes.oneOfType([
      PropTypes.arrayOf(PropTypes.node),
      PropTypes.node
    ]),
    className: PropTypes.string,
    containerId: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
    desc: PropTypes.string,
    events: PropTypes.object,
    height: PropTypes.number,
    origin: PropTypes.shape({ x: PropTypes.number, y: PropTypes.number }),
    polar: PropTypes.bool,
    portalComponent: PropTypes.element,
    responsive: PropTypes.bool,
    style: PropTypes.object,
    theme: PropTypes.object,
    title: PropTypes.string,
    width: PropTypes.number
  }

  static defaultProps = {
    portalComponent: <Portal/>,
    responsive: true
  }

  static contextTypes = {
    getTimer: PropTypes.func
  }

  static childContextTypes = {
    portalUpdate: PropTypes.func,
    portalRegister: PropTypes.func,
    portalDeregister: PropTypes.func,
    getTimer: PropTypes.func
  }

  constructor(props) {
    super(props);
    this.getTimer = this.getTimer.bind(this);
    this.containerId = typeof this.props.containerId === "undefined" ?
      uniqueId("victory-container-") : this.props.containerId;
  }

  getChildContext() {
    return {
      portalUpdate: this.portalUpdate,
      portalRegister: this.portalRegister,
      portalDeregister: this.portalDeregister,
      getTimer: this.getTimer
    };
  }

  componentWillMount() {
    this.savePortalRef = (portal) => {
      this.portalRef = portal;
      return portal;
    };
    this.portalUpdate = (key, el) => this.portalRef.portalUpdate(key, el);
    this.portalRegister = () => this.portalRef.portalRegister();
    this.portalDeregister = (key) => this.portalRef.portalDeregister(key);
  }

  componentWillUnmount() {
    if (!this.context.getTimer) {
      this.getTimer().stop();
    }
  }

  getTimer() {
    if (this.context.getTimer) {
      return this.context.getTimer();
    }
    if (!this.timer) {
      this.timer = new Timer();
    }
    return this.timer;
  }

  getIdForElement(elementName) {
    return `${this.containerId}-${elementName}`;
  }

  // overridden in custom containers
  getChildren(props) {
    return props.children;
  }

  // Overridden in victory-core-native
  renderContainer(props, svgProps, style) {
    const { title, desc, portalComponent, className } = props;
    const children = this.getChildren(props);
    const parentProps = defaults({ style, className }, svgProps);
    return (
      <svg {...parentProps} overflow="visible">
        <svg {...parentProps}>
          {children}
        </svg>
          {title ? <title id={this.getIdForElement("title")}>{title}</title> : null}
          {desc ? <desc id={this.getIdForElement("desc")}>{desc}</desc> : null}
        {React.cloneElement(portalComponent, { ref: this.savePortalRef })}
      </svg>
    );
  }

  render() {
    const { width, height, responsive, events } = this.props;
    const style = responsive ? this.props.style : omit(this.props.style, ["height", "width"]);
    const touchStyle = defaults({}, style, { touchAction: "none" });
    const svgProps = assign(
      {
        width, height, role: "img",
        "aria-labelledby": `${this.getIdForElement("title")} ${this.getIdForElement("desc")}`,
        viewBox: responsive ? `0 0 ${width} ${height}` : undefined
      },
      events
    );
    return this.renderContainer(this.props, svgProps, touchStyle);
  }
}
<|endoftext|>


<commit_before>/*
 * CoGe Progress Dialog
 */

var coge = window.coge = (function(namespace) {
	// Methods
	namespace.progress = {
		init: function(opts) {
			var self = this;
			this.baseUrl = opts.baseUrl;
			this.userName = opts.userName;
			this.supportEmail = opts.supportEmail;
			this.onSuccess = opts.onSuccess;
			this.onError = opts.onError;
			this.onReset = opts.onReset;
			this.formatter = opts.formatter || this.default_formatter;
			this.buttonTemplate = opts.buttonTemplate;

			var c = this.container = $('

');
			c.dialog({
				title: opts.title || 'Progress',
				autoOpen: false
			});

			c.dialog("widget").find('.ui-dialog-titlebar-close').hide(); // hide 'x' button

			var template = $($("#progress-template").html());
			this._render_template(template);

			// Setup resize handler
			var log = this.container.find(".log");
		    log.height( $( window ).height() * 0.5 );
		    c.dialog({
		    	modal: true,
		    	width: '60%',
		    	closeOnEscape: false,
		    	resizeStop: function(event, ui) {
		    		//console.log('resizeStop ' + ui.size.width + ' ' + ui.size.height + ' ' + ui.originalSize.width + ' ' + ui.originalSize.height);
		    		var widthChange = ui.size.width - ui.originalSize.width;
		    		var heightChange = ui.size.height - ui.originalSize.height;
		    		log.css({ width: log.width() + widthChange, height: log.height() + heightChange });
		    	}
		    });

		    // Setup button handlers
		    c.find('.cancel,.ok').click( $.proxy(self.reset, self) );
		},

		reset: function() {
		    this.container.dialog('close');
		    if (this.onReset)
		    	this.onReset();
		},

		reset_log: function() {
			var c = this.container;
			c.find('.log,.progress-link').html('');
			c.find('.msg').show('');
		    c.find('.ok,.error,.finished,.done,.cancel,.logfile').hide();
		},

		begin: function(opts) {
			this.reset_log();

			if (opts && opts.title)
				this.container.dialog({title: opts.title});

			if (opts && opts.width)
				this.container.dialog({width: opts.width});

			if (opts && opts.height)
				this.container.find(".log").height(opts.height);

		    var log = this.container.find('.log');
		    if (opts && opts.content)
		    	log.html(opts.content);
		    else
		    	log.html('Initializing ...');

		    this.container.dialog('open');

		    this.startTime = new Date().getTime();
		},

		end: function() {
			this.container.dialog('close');
		},

		succeeded: function(string) {
			var c = this.container;

		    // Update dialog
		    c.find('.msg,.progress-link').hide();
		    c.find('.finished').fadeIn();
		    if (string)
		    	c.find('.log').append(string)

		    // Show user-specified button template
		    if (this.buttonTemplate) {
		    	var template = $($("#"+this.buttonTemplate).html());
		    	c.find('.buttons').append(template);
		    }
		    else { // default buttons
		    	c.find('.ok').fadeIn();
		    }

		    // User callback
		    if (this.onSuccess)
		    	this.onSuccess();
		},

		failed: function(string) {
			var c = this.container;

			// Show error message
		    c.find('.log')
		    	.append('
' + string + '

')
		    	.append(
		    		'
' +
			        'The CoGe Support Team has been notified of this error but please ' +
			        'feel free to contact us at ' +
			        this.supportEmail + ' ' +
			        'and we can help to determine the cause.' +
			        '
'
		    	);

		    // Show link to log file
		    var logfile = this.baseUrl + 'legacy/download?username=' + this.userName + '&' + 'wid=' + this.job_id;
		    $(".logfile a").attr("href", logfile);
		    $('.logfile').fadeIn();

		    // Update dialog
		    c.find('.msg,.progress-link').hide();
		    c.find('.error,.cancel').fadeIn();

// FIXME restore this email reporting
//		    if (newLoad) { // mdb added check to prevent redundant emails, 8/14/14 issue 458
//		        $.ajax({
//		            data: {
//		                fname: "send_error_report",
//		                load_id: load_id,
//		                job_id: WORKFLOW_ID
//		            }
//		        });
//		    }

		    // User callback
		    if (this.onError)
		    	this.onError();
		},

		_refresh_interval: function() {
	        var interval = 2000;

	        // Set refresh rate based on elapsed time
	        if (!this.startTime)
	        	this.startTime = new Date().getTime();

	        var run_time = new Date().getTime() - this.startTime;
	        if (run_time > 10*60*1000)
	        	interval = 60*1000;
	        else if (run_time > 5*60*1000)
	        	interval = 30*1000;
	        else if (run_time > 60*1000)
	        	interval = 15*1000;
	        //console.log('Refresh run_time=' + run_time + ' refresh_interval=' + refresh_interval);

	        return interval;
		},

		update: function(job_id, url) {
			var self = this;

			self._debug("update");

			if (job_id)
				self.job_id = job_id;
			if (url) {
				self.url = url;
				self.container.find('.progress-link').html('Link: '+url+'').show();
			}

			var update_handler = function(json) {
				self._debug('update_handler');
				var c = self.container;

		        var workflow_status = $("

");
		        var log_content = $("

");
		        var results = [];

		        var refresh_interval = self._refresh_interval();
		        var retry_interval = 5*1000;
		        self._debug('refresh=' + refresh_interval + ', retry=' + retry_interval);

		        // Sanity check -- progress dialog should be open
		        if (!c.dialog('isOpen')) {
		        	self._error('Error: progress dialog is closed');
		            return;
		        }

		        // Handle error
		        if (!json || json.error) {
		        	self.ajaxError++;
		            if ('Auth' in json.error) {
		            	c.find('.msg').html('Login required to continue');
		            	c.find('.log')
		            		.css({'font-size': '1em'})
		            		.html("
Your session has expired.

" +
		            			"Please log in again by clicking " +
		            			"here.");
		            	return;
		            }
		            else {
			            self.alert('Server not responding ('+self.ajaxError+')');
			            setTimeout($.proxy(self.update, self), retry_interval);
			            return;
		            }
		        }

		        self.ajaxError = 0;
		        self.alert();

		        // Retry on missing status (probably won't ever happen)
		        if (!json.status) {
		        	self._error('Error: missing status, retrying ...');
		        	setTimeout($.proxy(self.update, self), retry_interval);
		            return;
		        }

		        // Render status
	            var current_status = json.status.toLowerCase();
	            self._debug('status=' + current_status);
	            workflow_status
	                .html("Workflow status: ")
	                .append( $('').html(json.status) )
	                .addClass('bold');

	            // Retry on JEX error status -- mdb added 6/30/15
	            if (current_status == "error") {
	            	self.alert('JEX error status');
	            	setTimeout($.proxy(self.update, self), retry_interval);
	            	return;
	            }

	            // Render tasks
		        if (json.tasks) {
		            var jobs = json.tasks;
		            for (var index = 0; index < jobs.length; index++) {
		                var item = self.formatter(jobs[index]);
		                if (item)
		                    results.push(item);
		            }
		        }

		        //FIXME Update when a workflow supports elapsed time
		        if (current_status == "completed") {
		            var total = json.tasks.reduce(function(a, b) {
		                if (!b.elapsed) return a;
		                return a + b.elapsed;
		            }, 0);

		            var duration = coge.utils.toPrettyDuration(total);

		            workflow_status.append("
Finished in " + duration);
		            workflow_status.find('span').addClass('completed');
		            self.succeeded(json.results);
		        }
		        else if (current_status == "failed"
		                //|| current_status == "error" // mdb removed 6/30/15 -- now handled first (see above code)
		                || current_status == "terminated"
		                || current_status == "cancelled")
		        {
		            workflow_status.find('span').addClass('alert');

		            if (json.results && json.results.length)
		                self.logfile = json.results[0].path;
		            self.failed();
		        }
		        else if (current_status == "notfound") {
		        	self._error('Error: status is "notfound"');
		        	setTimeout($.proxy(self.update, self), refresh_interval);
		            return;
		        }
		        else {
		            workflow_status.find('span').addClass('running');
		            setTimeout($.proxy(self.update, self), refresh_interval);
		        }

		        results.push(workflow_status);
		        log_content.append(results);

		        if (json.results && json.results.length) {
		        	log_content.append("
Here are the results (click to open):
");
		    	    json.results.forEach(function(result) {
		    	    	var html = self.format_result(result);
		    	    	log_content.append(html);
		    	    });
		        }

		        self.container.find('.log').html(log_content);
		    };

		    setTimeout(
		    	function() { coge.services.fetch_job(self.job_id, update_handler, update_handler); },
		    	10
		    );
		},

		format_result: function(result) {
			if (result.type === 'experiment') {
				var url = 'ExperimentView.pl?eid=' + result.id;
				return "
 Experiment '"+result.name+"'
";
			}
			else if (result.type === 'notebook') {
				var url = 'NotebookView.pl?nid=' + result.id;
				return "
 Notebook '"+result.name+"'
";
			}
		},

		default_formatter: function(item) {
		    var msg;
		    var row = $('
'+ item.description + '
');

		    var job_status = $('');

		    if (item.status == 'scheduled')
		        job_status.append(item.status).addClass('down bold');
		    else if (item.status == 'completed')
		        job_status.append(item.status).addClass('completed bold');
		    else if (item.status == 'running')
		        job_status.append(item.status).addClass('running bold');
		    else if (item.status == 'skipped')
		        job_status.append("already generated").addClass('skipped bold');
		    else if (item.status == 'cancelled')
		        job_status.append(item.status).addClass('alert bold');
		    else if (item.status == 'failed')
		        job_status.append(item.status).addClass('alert bold');
		    else
		        return;

		    row.append(job_status);

		    if (item.elapsed)
		        row.append(" in " + coge.utils.toPrettyDuration(item.elapsed));

		    if (item.log) {
		        var p = item.log.split("\n");

		        var pElements = p.map(function(item) {
		            var norm = item.replace(/\\t/g, " ").replace(/\\'/g, "'");
		            return $("

").html(norm);
		        });

		        var log = $("

").html(pElements).addClass("padded");
		        row.append(log);
		    }

		    return row;
		},

		alert: function(string) {
			var el = this.container.find(".alert");
			if (!string)
				el.html('').hide();
			else
				el.html(string).show();
		},

		_error: function(string) {
			console.error('progress: ' + string);
		},

		_debug: function(string) {
			console.log('progress: ' + string);
		},

		_render_template: function(template) {
		    this.container.empty()
		        .hide()
		        .append(template)
		        .show();//.slideDown();
		}

    };

    return namespace;
})(coge || {});<commit_msg>Add features from load genome<commit_after>/*
 * CoGe Progress Dialog
 */

var coge = window.coge = (function(namespace) {
	// Methods
	namespace.progress = {
		init: function(opts) {
			var self = this;
			this.baseUrl = opts.baseUrl;
			this.userName = opts.userName;
			this.supportEmail = opts.supportEmail;
			this.onSuccess = opts.onSuccess;
			this.onError = opts.onError;
			this.onReset = opts.onReset;
			this.formatter = opts.formatter || this.default_formatter;
			this.buttonTemplate = opts.buttonTemplate;

			var c = this.container = $('

');
			c.dialog({
				title: opts.title || 'Progress',
				autoOpen: false
			});

			c.dialog("widget").find('.ui-dialog-titlebar-close').hide(); // hide 'x' button

			var template = $($("#progress-template").html());
			this._render_template(template);

			// Setup resize handler
			var log = this.container.find(".log");
		    log.height( $( window ).height() * 0.5 );
		    c.dialog({
		    	modal: true,
		    	width: '60%',
		    	closeOnEscape: false,
		    	resizeStop: function(event, ui) {
		    		//console.log('resizeStop ' + ui.size.width + ' ' + ui.size.height + ' ' + ui.originalSize.width + ' ' + ui.originalSize.height);
		    		var widthChange = ui.size.width - ui.originalSize.width;
		    		var heightChange = ui.size.height - ui.originalSize.height;
		    		log.css({ width: log.width() + widthChange, height: log.height() + heightChange });
		    	}
		    });

		    // Setup button handlers
		    c.find('.cancel,.ok').click( $.proxy(self.reset, self) );
		},

		reset: function() {
		    this.container.dialog('close');
		    if (this.onReset)
		    	this.onReset();
		},

		reset_log: function() {
			var c = this.container;
			c.find('.log,.progress-link').html('');
			c.find('.msg').show('');
		    c.find('.ok,.error,.finished,.done,.cancel,.logfile,.buttons').hide();
		},

		begin: function(opts) {
			this.reset_log();

			if (opts && opts.title)
				this.container.dialog({title: opts.title});

			if (opts && opts.width)
				this.container.dialog({width: opts.width});

			if (opts && opts.height)
				this.container.find(".log").height(opts.height);

		    var log = this.container.find('.log');
		    if (opts && opts.content)
		    	log.html(opts.content);
		    else
		    	log.html('Initializing ...');

		    this.container.dialog('open');

		    this.startTime = new Date().getTime();
		},

		end: function() {
			this.container.dialog('close');
		},

		succeeded: function(results) {
			var c = this.container;

		    // Update dialog
		    c.find('.msg,.progress-link').hide();
		    c.find('.finished').fadeIn();

		    // Show user-specified button template
		    if (this.buttonTemplate) {
		    	var template = $($("#"+this.buttonTemplate).html());
		    	c.find('.buttons').html(template).fadeIn();
		    }
		    else { // default buttons
		    	c.find('.ok').fadeIn();
		    }

		    // User callback
		    if (this.onSuccess)
		    	this.onSuccess(results);
		},

		errorToString: function(error) {
			var string = '';
			for (key in error) {
				string += error[key];
			}
			return string;
		},

		failed: function(string, error) {
			var c = this.container;

			var errorMsg = string + (error ? ': ' + this.errorToString(error) : '');

			// Show error message
		    c.find('.log')
		    	.append('
' + errorMsg + '

')
		    	.append(
		    		'
' +
			        'The CoGe Support Team has been notified of this error but please ' +
			        'feel free to contact us at ' +
			        this.supportEmail + ' ' +
			        'and we can help to determine the cause.' +
			        '
'
		    	);

		    // Show link to log file
		    var logfile = this.baseUrl + 'legacy/download?username=' + this.userName + '&' + 'wid=' + this.job_id;
		    $(".logfile a").attr("href", logfile);
		    $('.logfile').fadeIn();

		    // Update dialog
		    c.find('.msg,.progress-link').hide();
		    c.find('.error,.cancel').fadeIn();

// FIXME restore this email reporting
//		    if (newLoad) { // mdb added check to prevent redundant emails, 8/14/14 issue 458
//		        $.ajax({
//		            data: {
//		                fname: "send_error_report",
//		                load_id: load_id,
//		                job_id: WORKFLOW_ID
//		            }
//		        });
//		    }

		    // User callback
		    if (this.onError)
		    	this.onError();
		},

		_refresh_interval: function() {
	        var interval = 2000;

	        // Set refresh rate based on elapsed time
	        if (!this.startTime)
	        	this.startTime = new Date().getTime();

	        var run_time = new Date().getTime() - this.startTime;
	        if (run_time > 10*60*1000)
	        	interval = 60*1000;
	        else if (run_time > 5*60*1000)
	        	interval = 30*1000;
	        else if (run_time > 60*1000)
	        	interval = 15*1000;
	        //console.log('Refresh run_time=' + run_time + ' refresh_interval=' + refresh_interval);

	        return interval;
		},

		update: function(job_id, url) {
			var self = this;

			self._debug("update");

			if (job_id)
				self.job_id = job_id;
			if (url) {
				self.url = url;
				self.container.find('.progress-link').html('Link: '+url+'').show();
			}

			var update_handler = function(json) {
				self._debug('update_handler');
				var c = self.container;

		        var workflow_status = $("

");
		        var log_content = $("

");
		        var results = [];

		        var refresh_interval = self._refresh_interval();
		        var retry_interval = 5*1000;
		        self._debug('refresh=' + refresh_interval + ', retry=' + retry_interval);

		        // Sanity check -- progress dialog should be open
		        if (!c.dialog('isOpen')) {
		        	self._error('Error: progress dialog is closed');
		            return;
		        }

		        // Handle error
		        if (!json || json.error) {
		        	self.ajaxError++;
		            if ('Auth' in json.error) {
		            	c.find('.msg').html('Login required to continue');
		            	c.find('.log')
		            		.css({'font-size': '1em'})
		            		.html("
Your session has expired.

" +
		            			"Please log in again by clicking " +
		            			"here.");
		            	return;
		            }
		            else {
			            self.alert('Server not responding ('+self.ajaxError+')');
			            setTimeout($.proxy(self.update, self), retry_interval);
			            return;
		            }
		        }

		        self.ajaxError = 0;
		        self.alert();

		        // Retry on missing status (probably won't ever happen)
		        if (!json.status) {
		        	self._error('Error: missing status, retrying ...');
		        	setTimeout($.proxy(self.update, self), retry_interval);
		            return;
		        }

		        // Render status
	            var current_status = json.status.toLowerCase();
	            self._debug('status=' + current_status);
	            workflow_status
	                .html("Workflow status: ")
	                .append( $('').html(json.status) )
	                .addClass('bold');

	            // Retry on JEX error status -- mdb added 6/30/15
	            if (current_status == "error") {
	            	self.alert('JEX error status');
	            	setTimeout($.proxy(self.update, self), retry_interval);
	            	return;
	            }

	            // Render tasks
		        if (json.tasks) {
		            var jobs = json.tasks;
		            for (var index = 0; index < jobs.length; index++) {
		                var item = self.formatter(jobs[index]);
		                if (item)
		                    results.push(item);
		            }
		        }

		        //FIXME Update when a workflow supports elapsed time
		        if (current_status == "completed") {
		            var total = json.tasks.reduce(function(a, b) {
		                if (!b.elapsed) return a;
		                return a + b.elapsed;
		            }, 0);

		            var duration = coge.utils.toPrettyDuration(total);

		            workflow_status.append("
Finished in " + duration);
		            workflow_status.find('span').addClass('completed');
		            self.succeeded(json.results);
		        }
		        else if (current_status == "failed"
		                //|| current_status == "error" // mdb removed 6/30/15 -- now handled first (see above code)
		                || current_status == "terminated"
		                || current_status == "cancelled")
		        {
		            workflow_status.find('span').addClass('alert');

		            if (json.results && json.results.length)
		                self.logfile = json.results[0].path;
		            self.failed();
		        }
		        else if (current_status == "notfound") {
		        	self._error('Error: status is "notfound"');
		        	setTimeout($.proxy(self.update, self), refresh_interval);
		            return;
		        }
		        else {
		            workflow_status.find('span').addClass('running');
		            setTimeout($.proxy(self.update, self), refresh_interval);
		        }

		        results.push(workflow_status);
		        log_content.append(results);

		        if (json.results && json.results.length) {
		        	log_content.append("
Here are the results (click to open):
");
		    	    json.results.forEach(function(result) {
		    	    	var html = self.format_result(result);
		    	    	log_content.append(html);
		    	    });
		        }

		        self.container.find('.log').html(log_content);
		    };

		    setTimeout(
		    	function() { coge.services.fetch_job(self.job_id, update_handler, update_handler); },
		    	10
		    );
		},

		format_result: function(result) {
			if (result.type === 'experiment') {
				var url = 'ExperimentView.pl?eid=' + result.id;
				return "
 Experiment '"+result.name+"'
";
			}
			else if (result.type === 'notebook') {
				var url = 'NotebookView.pl?nid=' + result.id;
				return "
 Notebook '"+result.name+"'
";
			}
			else if (result.type === 'genome') {
				var url = 'GenomeInfo.pl?gid=' + result.id;
				return "
 Genome '"+result.info+"'
";
			}
			else if (result.type === 'dataset') {
				var url = 'GenomeInfo.pl?dsid=' + result.id;
				return "
 Dataset '"+result.name+"'
";
			}
		},

		default_formatter: function(item) {
		    var msg;
		    var row = $('
'+ item.description + '
');

		    var job_status = $('');

		    if (item.status == 'scheduled')
		        job_status.append(item.status).addClass('down bold');
		    else if (item.status == 'completed')
		        job_status.append(item.status).addClass('completed bold');
		    else if (item.status == 'running')
		        job_status.append(item.status).addClass('running bold');
		    else if (item.status == 'skipped')
		        job_status.append("already generated").addClass('skipped bold');
		    else if (item.status == 'cancelled')
		        job_status.append(item.status).addClass('alert bold');
		    else if (item.status == 'failed')
		        job_status.append(item.status).addClass('alert bold');
		    else
		        return;

		    row.append(job_status);

		    if (item.elapsed)
		        row.append(" in " + coge.utils.toPrettyDuration(item.elapsed));

		    if (item.log) {
		        var p = item.log.split("\n");

		        var pElements = p.map(function(item) {
		            var norm = item.replace(/\\t/g, " ").replace(/\\'/g, "'");
		            return $("

").html(norm);
		        });

		        var log = $("

").html(pElements).addClass("padded");
		        row.append(log);
		    }

		    return row;
		},

		alert: function(string) {
			var el = this.container.find(".alert");
			if (!string)
				el.html('').hide();
			else
				el.html(string).show();
		},

		_error: function(string) {
			console.error('progress: ' + string);
		},

		_debug: function(string) {
			console.log('progress: ' + string);
		},

		_render_template: function(template) {
		    this.container.empty()
		        .hide()
		        .append(template)
		        .show();//.slideDown();
		}

    };

    return namespace;
})(coge || {});<|endoftext|>


<commit_before>/* eslint-disable no-console */
/*
 * module to provide a contentful like SDK to our local cache
 */
"use strict";

const path = require("path");
const fs = require("fs");
const glob = require("glob");

let clientResolveLinks, defaultLocale, localCachePath, locales, currentSpace;

const resolveLinksLimitMax = 10;

/**
 * sdk relies heavily on sys metadata
 * so we cannot omit the sys property on sdk level
 */
function normalizeSelect (query) {
    if (query.select && !/sys/i.test(query.select)) {
        query.select += ",sys";
    }
}

/**
 * fieldWithLocaleFallback
 */
function fieldWithLocaleFallback (field, value, preferredLocaleCode) {

    let newValue, locale;

    // get the locale record from the current metadata
    locale = locales.filter(l => l.code === preferredLocaleCode).shift();

    if (!locale) {
        throw new Error("Unknown locale: " + preferredLocaleCode);
    }

    if (locale.code in value) {
        newValue = value[locale.code];
    } else if (locale.fallbackCode) {
        newValue = fieldWithLocaleFallback(field, value, locale.fallbackCode);
    }

    return newValue;

}

/**
 * filter record by locale
 */
// TODO fallback locale list
function filterLocale(record, preferredLocaleCode) {

    let clone, field, locale;

    // get the locale record from the current metadata
    locale = locales.filter(l => l.code === preferredLocaleCode).shift();

    if (!locale) {
        throw new Error("Unknown locale: " + preferredLocaleCode);
    }

    // clone record
    clone = JSON.parse(JSON.stringify(record));

    if (clone.fields) {

        for (field in clone.fields) {
            if (clone.fields.hasOwnProperty(field)) {
                clone.fields[field] = fieldWithLocaleFallback(field, clone.fields[field], locale.code);
            }
        }

    }

    return clone;

}

/**
 * resolveLinks
 */
function resolveLinks (record, query) {
    return new Promise((resolve, reject) => {

        let fieldsWithLinks, promisesForRecords;

        // to avoid recursive loops, resolving links has a limit
        if (query.include > 0) {

            // find links in record fields
            if (record.fields) {

                // get a list of fields that are links or arrays of links
                fieldsWithLinks = Object.keys(record.fields).filter(key => {

                    if (Array.isArray(record.fields[key])) {
                        if (record.fields[key][0] && record.fields[key][0].sys) {
                            return record.fields[key][0].sys.type === "Link";
                        }
                    } else {
                        if (record.fields[key] && record.fields[key].sys) {
                            return record.fields[key].sys.type === "Link";
                        }
                    }

                });

                // get list of promises for the linked records
                promisesForRecords = fieldsWithLinks.map(key => {

                    let p, sys;

                    // switch on link type
                    sys = record.fields[key].sys;
                    switch(sys.linkType) {
                    case "Entry":
                        p = getEntry(sys.id, { "include": query.include - 1 });
                        break;
                    case "Asset":
                        p = getAsset(sys.id, { "include": query.include - 1 });
                        break;
                    default:
                        p = Promise.reject(
                            new Error("Unknown link type: %s", sys.linkType)
                        );
                        break;
                    }

                    return p;

                });

                // resolve promises and update record with linked records
                Promise.all(promisesForRecords).then(linkedRecords => {

                    fieldsWithLinks.forEach(key => {

                        if (Array.isArray(record.fields[key])) {

                            // field value has multiple links
                            record.fields[key] = linkedRecords.splice(0, record.fields[key].length);

                        } else {

                            // set the field to the current linked record
                            record.fields[key] = linkedRecords.shift();
                        }

                    });

                    resolve(record);

                }).catch(reject);

            } else {

                resolve(record);

            }

        } else {

            resolve(record);

        }

    });
}

/**
 * read file and optionally filter fields by locale
 */
// TODO filter by query
function readFileFilterLocale (file, query) {
    return new Promise((resolve, reject) => {

        fs.readFile(file, "utf8", function (err, data) {

            if (err) {
                reject(err);
            } else {

                let record = JSON.parse(data);

                getSpace(record)
                    .then(space => {
                        return new Promise((resolve) => {
                            // set default locale
                            locales = space.locales;
                            defaultLocale = locales.filter(locale => locale.default)[0];
                            resolve(defaultLocale);
                        });
                    })
                    .then(defaultLocale => {
                        return new Promise((resolve) => {
                            // filter to locale
                            record = filterLocale(record, query.locale || defaultLocale.code);

                            resolve(record);
                        });
                    })
                    .then(record => resolveLinks(record, query))
                    .then(resolve)
                    .catch(reject);

            }

        });

    });
}

/**
 * helper fuction to get the value of a nest property of an object by "path"
 * path is a string with property dot notation
 * i.e. "sys.createdAt" or "fields.image.title"
 */
function objectValueByPath(obj, path) {
    return path.split(".").reduce(function(acc, v) {
        return acc ? acc[v] : undefined;
    }, obj);
}

/**
 * sort fuction to sort arrays of objects by the value of the property
 * (possibly nested) refernced by the path string
 */
function compareObjectsByValueAtPath (path, reverve) {

    let direction = reverve ? -1 : 1;

    return function (a, b) {

        // get values
        let aValue = objectValueByPath(a, path);
        let bValue = objectValueByPath(b, path);

        // compare values and apply direction
        if (aValue > bValue) {
            return 1 * direction;
        } else if (aValue < bValue) {
            return -1 * direction;
        } else {
            return 0;
        }

    };

}

/**
 * sort item by order property in query
 * @arg {array} items to sort
 * @arg {query} query object
 * @returns {Promise<array>} a promise of an array of items
 */
function sortItemsByQuery (items, query) {
    return new Promise(resolve => {

        let order = query.order;

        if (order) {

            // sort items by order property in query
            // order property document here:
            // https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters/order

            // split up multiple attributes by comma
            // and field name and decending or accending
            let attributes = order.split(",").map(attribute => {

                if (attribute.charAt(0) === "-") {
                    return {
                        "field": attribute.slice(1),
                        "reverse": true
                    };
                } else {
                    return {
                        "field": attribute,
                        "reverse": false
                    };
                }

            });

            // sort the items by the least specified attribute and repeat
            attributes.reverse().forEach(attribute => {

                items.sort(compareObjectsByValueAtPath(
                    attribute.field, attribute.reverse
                ));

            });

            resolve(items);

        } else {

            // no order specified, so just return the items
            resolve(items);

        }

    });
}

function readFilesAndFilter (dir, query) {
    return new Promise((resolve, reject) => {

        glob(path.join(dir, "**/*.json"), function (err, files) {

            if (err) {

                reject(err);

            } else {

                if (files && files.length > 0) {

                    Promise.all(
                        files.map(file => readFileFilterLocale(file, query))
                    )
                    .then(results => sortItemsByQuery(results, query))
                    .then(results => {
                        resolve({
                            "total": results.length,
                            "skip": 0,
                            "limit": Infinity,
                            "items": results
                        });
                    }).catch(reject);

                } else {

                    resolve({
                        "total": 0,
                        "skip": 0,
                        "limit": Infinity,
                        "items": []
                    });

                }

            }

        });

    });
}

/**
 * process query properties
 */
function processQueryProperties (query) {

    if (query.resolveLinks === undefined) {

        // use client setting if unset
        query.resolveLinks = clientResolveLinks;

    }

    if (query.resolveLinks === undefined) {

        // default to true for resolve links
        query.resolveLinks = true;

    }

    if (query.include === undefined) {

        // default to include one level of links
        query.include = 1;

    }

    if (query.include > resolveLinksLimitMax) {

        // limit the maximum number of inclusions
        query.include = resolveLinksLimitMax;

    }

    return query;

}

/**
 * Gets the Space which the client is currently configured to use
 * @memberof ContentfulClientAPI
 * @return {Promise<Entities.Space>} Promise for a Space
 * @example
 * client.getSpace()
 * .then(space => console.log(space))
 */
function getSpace () {
    return new Promise((resolve, reject) => {

        let file = path.resolve(localCachePath, currentSpace, "space.json");

        fs.readFile(file, "utf8", function (err, data) {
            if (err) {
                reject(err);
            } else {
                resolve(JSON.parse(data));
            }
        });

    });
}

/**
 * Gets a Content Type
 * @memberof ContentfulClientAPI
 * @param  {string} id
 * @return {Promise<Entities.ContentType>} Promise for a Content Type
 * @example
 * client.getContentType('contentTypeId')
 * .then(contentType => console.log(contentType))
 */
function getContentType () {
    throw new Error("Not implemented.");
}

/**
 * Gets a collection of Content Types
 * @memberof ContentfulClientAPI
 * @param  {Object=} query - Object with search parameters. Check the JS SDK tutorial and the REST API reference for more details.
 * @return {Promise<Entities.ContentTypeCollection>} Promise for a collection of Content Types
 * @example
 * client.getContentTypes()
 * .then(contentTypes => console.log(contentTypes.items))
 */
function getContentTypes () {
    throw new Error("Not implemented.");
}

/**
 * Gets an Entry
 * @memberof ContentfulClientAPI
 * @param  {string} id
 * @param  {Object=} query - Object with search parameters. In this method it's only useful for `locale`.
 * @return {Promise<Entities.Entry>} Promise for an Entry
 * @example
 * client.getEntry('entryId')
 * .then(entry => console.log(entry))
 */
function getEntry (id, query = {}) {

    let dir, file;

    query = processQueryProperties(query);

    normalizeSelect(query);

    dir = path.resolve(localCachePath, currentSpace, "entries");
    if (query.content_type) {

        // limit to content type in query
        dir = path.resolve(dir, query.content_type);
        file = path.join(dir, `${query.content_type}_${id}.json`);

    } else {

        // search for id in all content types
        let pattern = `${dir}/**/*_${id}.json`;
        let files = glob.sync(pattern);
        if (files && files.length > 0) {

            file = files[0];

        }

    }

    return readFileFilterLocale(file, query);

}

/**
 * Gets a collection of Entries
 * @memberof ContentfulClientAPI
 * @param  {Object=} query - Object with search parameters. Check the JS SDK tutorial and the REST API reference for more details.
 * @param  {boolean=} query.resolveLinks - When true, links to other Entries or Assets are resolved. Default: true.
 * @return {Promise<Entities.EntryCollection>} Promise for a collection of Entries
 * @example
 * client.getEntries({content_type: 'contentTypeId'})
 * .then(entries => console.log(entries.items))
 */
function getEntries (query = {}) {

    let dir;

    query = processQueryProperties(query);

    dir = path.resolve(localCachePath, currentSpace, "entries");
    if (query.content_type) {

        // limit to content type in query
        dir = path.resolve(dir, query.content_type);

    }

    return readFilesAndFilter(dir, query);

}

function getAsset (id, query = {}) {

    query = processQueryProperties(query);

    normalizeSelect(query);
    let dir = path.resolve(localCachePath, currentSpace, "assets");
    return readFileFilterLocale(path.join(dir, `${id}.json`), query);

}

function getAssets (query = {}) {

    query = processQueryProperties(query);

    normalizeSelect(query);
    let dir = path.resolve(localCachePath, currentSpace, "assets");

    return readFilesAndFilter(dir, query);

}

function parseEntries () {
    throw new Error("Not implemented.");
}

function sync () {
    throw new Error("Not implemented.");
}

/**
 * unWrap - Contentful JSON to Plain JSON
 * @arg {Object} entry
 * @returns {Object} plain entry
 */
function unWrap (entry) {

    try {
        return Promise.resolve(unWrapSync(entry));
    } catch (err) {
        return Promise.reject(err);
    }

}

/**
 * unWrap without a promise
 */
function unWrapSync (entry) {

    let po;

    if (entry.items) {

        // return collections with thier items unWrapped
        entry.items = unWrapCollectionSync(entry);
        return entry;

    } else if (entry.fields) {

        po = Object.keys(entry.fields).reduce((acc, v) => {

            if (entry.fields[v] && entry.fields[v].sys) {

                acc[v] = unWrapSync(entry.fields[v]);

            } else {

                acc[v] = entry.fields[v];

            }

            return acc;

        }, {});

    }

    return po;

}

/**
 * unWrapCollectionSync - Contentful JSON Collection to Plain JSON Array
 * @arg {Object} records
 * @returns {Object} plain objects
 */
function unWrapCollectionSync (collection) {

    return collection.items.map(unWrapSync);

}

/**
 * unWrapCollection - Contentful JSON Collection to Plain JSON Array
 * @arg {Object} records
 * @returns {Promise<Object>} Promise of plain objects
 */
function unWrapCollection (collection) {

    try {
        return Promise.resolve(unWrapCollectionSync(collection));
    } catch (err) {
        return Promise.reject(err);
    }

}

function createClient (params = {}) {

    currentSpace = params.space;
    clientResolveLinks = params.resolveLinks;
    localCachePath = params.localPath;

    return {
        getSpace: getSpace,
        getContentType: getContentType,
        getContentTypes: getContentTypes,
        getEntry: getEntry,
        getEntries: getEntries,
        getAsset: getAsset,
        getAssets: getAssets,
        parseEntries: parseEntries,
        sync: sync,
        unWrap: unWrap,
        unWrapCollection: unWrapCollection
    };

}

module.exports = {
    createClient: createClient
};
<commit_msg>fixed misspelled variable<commit_after>/* eslint-disable no-console */
/*
 * module to provide a contentful like SDK to our local cache
 */
"use strict";

const path = require("path");
const fs = require("fs");
const glob = require("glob");

let clientResolveLinks, defaultLocale, localCachePath, locales, currentSpace;

const resolveLinksLimitMax = 10;

/**
 * sdk relies heavily on sys metadata
 * so we cannot omit the sys property on sdk level
 */
function normalizeSelect (query) {
    if (query.select && !/sys/i.test(query.select)) {
        query.select += ",sys";
    }
}

/**
 * fieldWithLocaleFallback
 */
function fieldWithLocaleFallback (field, value, preferredLocaleCode) {

    let newValue, locale;

    // get the locale record from the current metadata
    locale = locales.filter(l => l.code === preferredLocaleCode).shift();

    if (!locale) {
        throw new Error("Unknown locale: " + preferredLocaleCode);
    }

    if (locale.code in value) {
        newValue = value[locale.code];
    } else if (locale.fallbackCode) {
        newValue = fieldWithLocaleFallback(field, value, locale.fallbackCode);
    }

    return newValue;

}

/**
 * filter record by locale
 */
// TODO fallback locale list
function filterLocale(record, preferredLocaleCode) {

    let clone, field, locale;

    // get the locale record from the current metadata
    locale = locales.filter(l => l.code === preferredLocaleCode).shift();

    if (!locale) {
        throw new Error("Unknown locale: " + preferredLocaleCode);
    }

    // clone record
    clone = JSON.parse(JSON.stringify(record));

    if (clone.fields) {

        for (field in clone.fields) {
            if (clone.fields.hasOwnProperty(field)) {
                clone.fields[field] = fieldWithLocaleFallback(field, clone.fields[field], locale.code);
            }
        }

    }

    return clone;

}

/**
 * resolveLinks
 */
function resolveLinks (record, query) {
    return new Promise((resolve, reject) => {

        let fieldsWithLinks, promisesForRecords;

        // to avoid recursive loops, resolving links has a limit
        if (query.include > 0) {

            // find links in record fields
            if (record.fields) {

                // get a list of fields that are links or arrays of links
                fieldsWithLinks = Object.keys(record.fields).filter(key => {

                    if (Array.isArray(record.fields[key])) {
                        if (record.fields[key][0] && record.fields[key][0].sys) {
                            return record.fields[key][0].sys.type === "Link";
                        }
                    } else {
                        if (record.fields[key] && record.fields[key].sys) {
                            return record.fields[key].sys.type === "Link";
                        }
                    }

                });

                // get list of promises for the linked records
                promisesForRecords = fieldsWithLinks.map(key => {

                    let p, sys;

                    // switch on link type
                    sys = record.fields[key].sys;
                    switch(sys.linkType) {
                    case "Entry":
                        p = getEntry(sys.id, { "include": query.include - 1 });
                        break;
                    case "Asset":
                        p = getAsset(sys.id, { "include": query.include - 1 });
                        break;
                    default:
                        p = Promise.reject(
                            new Error("Unknown link type: %s", sys.linkType)
                        );
                        break;
                    }

                    return p;

                });

                // resolve promises and update record with linked records
                Promise.all(promisesForRecords).then(linkedRecords => {

                    fieldsWithLinks.forEach(key => {

                        if (Array.isArray(record.fields[key])) {

                            // field value has multiple links
                            record.fields[key] = linkedRecords.splice(0, record.fields[key].length);

                        } else {

                            // set the field to the current linked record
                            record.fields[key] = linkedRecords.shift();
                        }

                    });

                    resolve(record);

                }).catch(reject);

            } else {

                resolve(record);

            }

        } else {

            resolve(record);

        }

    });
}

/**
 * read file and optionally filter fields by locale
 */
// TODO filter by query
function readFileFilterLocale (file, query) {
    return new Promise((resolve, reject) => {

        fs.readFile(file, "utf8", function (err, data) {

            if (err) {
                reject(err);
            } else {

                let record = JSON.parse(data);

                getSpace(record)
                    .then(space => {
                        return new Promise((resolve) => {
                            // set default locale
                            locales = space.locales;
                            defaultLocale = locales.filter(locale => locale.default)[0];
                            resolve(defaultLocale);
                        });
                    })
                    .then(defaultLocale => {
                        return new Promise((resolve) => {
                            // filter to locale
                            record = filterLocale(record, query.locale || defaultLocale.code);

                            resolve(record);
                        });
                    })
                    .then(record => resolveLinks(record, query))
                    .then(resolve)
                    .catch(reject);

            }

        });

    });
}

/**
 * helper fuction to get the value of a nest property of an object by "path"
 * path is a string with property dot notation
 * i.e. "sys.createdAt" or "fields.image.title"
 */
function objectValueByPath(obj, path) {
    return path.split(".").reduce(function(acc, v) {
        return acc ? acc[v] : undefined;
    }, obj);
}

/**
 * sort fuction to sort arrays of objects by the value of the property
 * (possibly nested) refernced by the path string
 */
function compareObjectsByValueAtPath (path, reverse) {

    let direction = reverse ? -1 : 1;

    return function (a, b) {

        // get values
        let aValue = objectValueByPath(a, path);
        let bValue = objectValueByPath(b, path);

        // compare values and apply direction
        if (aValue > bValue) {
            return 1 * direction;
        } else if (aValue < bValue) {
            return -1 * direction;
        } else {
            return 0;
        }

    };

}

/**
 * sort item by order property in query
 * @arg {array} items to sort
 * @arg {query} query object
 * @returns {Promise<array>} a promise of an array of items
 */
function sortItemsByQuery (items, query) {
    return new Promise(resolve => {

        let order = query.order;

        if (order) {

            // sort items by order property in query
            // order property document here:
            // https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters/order

            // split up multiple attributes by comma
            // and field name and decending or accending
            let attributes = order.split(",").map(attribute => {

                if (attribute.charAt(0) === "-") {
                    return {
                        "field": attribute.slice(1),
                        "reverse": true
                    };
                } else {
                    return {
                        "field": attribute,
                        "reverse": false
                    };
                }

            });

            // sort the items by the least specified attribute and repeat
            attributes.reverse().forEach(attribute => {

                items.sort(compareObjectsByValueAtPath(
                    attribute.field, attribute.reverse
                ));

            });

            resolve(items);

        } else {

            // no order specified, so just return the items
            resolve(items);

        }

    });
}

function readFilesAndFilter (dir, query) {
    return new Promise((resolve, reject) => {

        glob(path.join(dir, "**/*.json"), function (err, files) {

            if (err) {

                reject(err);

            } else {

                if (files && files.length > 0) {

                    Promise.all(
                        files.map(file => readFileFilterLocale(file, query))
                    )
                    .then(results => sortItemsByQuery(results, query))
                    .then(results => {
                        resolve({
                            "total": results.length,
                            "skip": 0,
                            "limit": Infinity,
                            "items": results
                        });
                    }).catch(reject);

                } else {

                    resolve({
                        "total": 0,
                        "skip": 0,
                        "limit": Infinity,
                        "items": []
                    });

                }

            }

        });

    });
}

/**
 * process query properties
 */
function processQueryProperties (query) {

    if (query.resolveLinks === undefined) {

        // use client setting if unset
        query.resolveLinks = clientResolveLinks;

    }

    if (query.resolveLinks === undefined) {

        // default to true for resolve links
        query.resolveLinks = true;

    }

    if (query.include === undefined) {

        // default to include one level of links
        query.include = 1;

    }

    if (query.include > resolveLinksLimitMax) {

        // limit the maximum number of inclusions
        query.include = resolveLinksLimitMax;

    }

    return query;

}

/**
 * Gets the Space which the client is currently configured to use
 * @memberof ContentfulClientAPI
 * @return {Promise<Entities.Space>} Promise for a Space
 * @example
 * client.getSpace()
 * .then(space => console.log(space))
 */
function getSpace () {
    return new Promise((resolve, reject) => {

        let file = path.resolve(localCachePath, currentSpace, "space.json");

        fs.readFile(file, "utf8", function (err, data) {
            if (err) {
                reject(err);
            } else {
                resolve(JSON.parse(data));
            }
        });

    });
}

/**
 * Gets a Content Type
 * @memberof ContentfulClientAPI
 * @param  {string} id
 * @return {Promise<Entities.ContentType>} Promise for a Content Type
 * @example
 * client.getContentType('contentTypeId')
 * .then(contentType => console.log(contentType))
 */
function getContentType () {
    throw new Error("Not implemented.");
}

/**
 * Gets a collection of Content Types
 * @memberof ContentfulClientAPI
 * @param  {Object=} query - Object with search parameters. Check the JS SDK tutorial and the REST API reference for more details.
 * @return {Promise<Entities.ContentTypeCollection>} Promise for a collection of Content Types
 * @example
 * client.getContentTypes()
 * .then(contentTypes => console.log(contentTypes.items))
 */
function getContentTypes () {
    throw new Error("Not implemented.");
}

/**
 * Gets an Entry
 * @memberof ContentfulClientAPI
 * @param  {string} id
 * @param  {Object=} query - Object with search parameters. In this method it's only useful for `locale`.
 * @return {Promise<Entities.Entry>} Promise for an Entry
 * @example
 * client.getEntry('entryId')
 * .then(entry => console.log(entry))
 */
function getEntry (id, query = {}) {

    let dir, file;

    query = processQueryProperties(query);

    normalizeSelect(query);

    dir = path.resolve(localCachePath, currentSpace, "entries");
    if (query.content_type) {

        // limit to content type in query
        dir = path.resolve(dir, query.content_type);
        file = path.join(dir, `${query.content_type}_${id}.json`);

    } else {

        // search for id in all content types
        let pattern = `${dir}/**/*_${id}.json`;
        let files = glob.sync(pattern);
        if (files && files.length > 0) {

            file = files[0];

        }

    }

    return readFileFilterLocale(file, query);

}

/**
 * Gets a collection of Entries
 * @memberof ContentfulClientAPI
 * @param  {Object=} query - Object with search parameters. Check the JS SDK tutorial and the REST API reference for more details.
 * @param  {boolean=} query.resolveLinks - When true, links to other Entries or Assets are resolved. Default: true.
 * @return {Promise<Entities.EntryCollection>} Promise for a collection of Entries
 * @example
 * client.getEntries({content_type: 'contentTypeId'})
 * .then(entries => console.log(entries.items))
 */
function getEntries (query = {}) {

    let dir;

    query = processQueryProperties(query);

    dir = path.resolve(localCachePath, currentSpace, "entries");
    if (query.content_type) {

        // limit to content type in query
        dir = path.resolve(dir, query.content_type);

    }

    return readFilesAndFilter(dir, query);

}

function getAsset (id, query = {}) {

    query = processQueryProperties(query);

    normalizeSelect(query);
    let dir = path.resolve(localCachePath, currentSpace, "assets");
    return readFileFilterLocale(path.join(dir, `${id}.json`), query);

}

function getAssets (query = {}) {

    query = processQueryProperties(query);

    normalizeSelect(query);
    let dir = path.resolve(localCachePath, currentSpace, "assets");

    return readFilesAndFilter(dir, query);

}

function parseEntries () {
    throw new Error("Not implemented.");
}

function sync () {
    throw new Error("Not implemented.");
}

/**
 * unWrap - Contentful JSON to Plain JSON
 * @arg {Object} entry
 * @returns {Object} plain entry
 */
function unWrap (entry) {

    try {
        return Promise.resolve(unWrapSync(entry));
    } catch (err) {
        return Promise.reject(err);
    }

}

/**
 * unWrap without a promise
 */
function unWrapSync (entry) {

    let po;

    if (entry.items) {

        // return collections with thier items unWrapped
        entry.items = unWrapCollectionSync(entry);
        return entry;

    } else if (entry.fields) {

        po = Object.keys(entry.fields).reduce((acc, v) => {

            if (entry.fields[v] && entry.fields[v].sys) {

                acc[v] = unWrapSync(entry.fields[v]);

            } else {

                acc[v] = entry.fields[v];

            }

            return acc;

        }, {});

    }

    return po;

}

/**
 * unWrapCollectionSync - Contentful JSON Collection to Plain JSON Array
 * @arg {Object} records
 * @returns {Object} plain objects
 */
function unWrapCollectionSync (collection) {

    return collection.items.map(unWrapSync);

}

/**
 * unWrapCollection - Contentful JSON Collection to Plain JSON Array
 * @arg {Object} records
 * @returns {Promise<Object>} Promise of plain objects
 */
function unWrapCollection (collection) {

    try {
        return Promise.resolve(unWrapCollectionSync(collection));
    } catch (err) {
        return Promise.reject(err);
    }

}

function createClient (params = {}) {

    currentSpace = params.space;
    clientResolveLinks = params.resolveLinks;
    localCachePath = params.localPath;

    return {
        getSpace: getSpace,
        getContentType: getContentType,
        getContentTypes: getContentTypes,
        getEntry: getEntry,
        getEntries: getEntries,
        getAsset: getAsset,
        getAssets: getAssets,
        parseEntries: parseEntries,
        sync: sync,
        unWrap: unWrap,
        unWrapCollection: unWrapCollection
    };

}

module.exports = {
    createClient: createClient
};
<|endoftext|>


<commit_before>let _ = require('lodash/fp')
let JWT = require('./JWT')
let {method} = require('sails-async')
let F = require('futil-js')

module.exports = {
  login: (authenticate) => method(async (req, res) => {
    let userId = await authenticate(req.allParams(), {req, res})
    if (!_.isNil(userId)) {
      let token = JWT.issue({
        user: userId
      })
      // Set here so client auto uses it.
      res.set(JWT.renewTokenHeader, token)
      return {token}
    } else {
      F.throws({
        statusCode: 400,
        message: 'Invalid user id.'
      })
    }
  }),
  impersonate: (id='id') => method(async (req, res) => {
    let token = JWT.issue({
      // The new user will be whatever the target id is
      user: req.param(id),
      // Add the previous user to the back of the stack of users ids
      // Makes it easy to use with lodash :D
      impersonateStack: [...req.tokenPayload.impersonateStack || [], req.user.id]
    })
    res.set(JWT.renewTokenHeader, token)
    return {token}
  }),
  impersonatePop: method(async (req, res) => {
    let tokenRaw = {
      // Grab last user of the stack and make the new user
      user: _.last(req.tokenPayload.impersonateStack),
      // New stack should be the all but the last user
      // since they're the new user
      impersonateStack: _.initial(req.tokenPayload.impersonateStack)
    }
    let token = JWT.issue(tokenRaw)
    res.set(JWT.renewTokenHeader, token)
    return {token}
  }),
  verify: (fn, verify='verify', values='values') =>
    method(async req => fn(req.param('verify'), req.param('values')))
}
<commit_msg>cleanup<commit_after>let _ = require('lodash/fp')
let JWT = require('./JWT')
let {method} = require('sails-async')
let F = require('futil-js')

/*
  GQ

  The application's user controller creates a login function by invoking the AuthController.login function:
    module.exports.login = AuthController.login(models.user.login)
  The models.user.login function signature is:
    (authPayload, req, res) => {...returns authentication strategy function...}
  The login function selects an authentication strategy based on the authentication payload type.
  The authentication strategy function signature is:
    authenticate(authPayload, {req, res}) => {...returns an user id or responds with an exception...}
  If a user id is provided a JWT token is issued, if an exception occurs the exception contains the
  information needed for the client to decide the next step in the process.
*/
module.exports = {
  login: (authenticate) => method(async (req, res) => {
    let userId = await authenticate(req.allParams(), {req, res})
    if (!_.isNil(userId)) {
      let token = JWT.issue({
        user: userId
      })
      // Set here so client auto uses it.
      res.set(JWT.renewTokenHeader, token)
      return {token}
    }
  }),
  impersonate: (id='id') => method(async (req, res) => {
    let token = JWT.issue({
      // The new user will be whatever the target id is
      user: req.param(id),
      // Add the previous user to the back of the stack of users ids
      // Makes it easy to use with lodash :D
      impersonateStack: [...req.tokenPayload.impersonateStack || [], req.user.id]
    })
    res.set(JWT.renewTokenHeader, token)
    return {token}
  }),
  impersonatePop: method(async (req, res) => {
    let tokenRaw = {
      // Grab last user of the stack and make the new user
      user: _.last(req.tokenPayload.impersonateStack),
      // New stack should be the all but the last user
      // since they're the new user
      impersonateStack: _.initial(req.tokenPayload.impersonateStack)
    }
    let token = JWT.issue(tokenRaw)
    res.set(JWT.renewTokenHeader, token)
    return {token}
  }),
  verify: (fn, verify='verify', values='values') =>
    method(async req => fn(req.param('verify'), req.param('values')))
}
<|endoftext|>