<commit_before>/*globals $, define, _, WebGMEGlobal */
/*jshint browser: true*/

define([
    'blob/BlobClient',
    'js/Utils/SaveToDisk',
    'deepforge/viz/ConfigDialog',
    'js/Constants',
    'panel/FloatingActionButton/FloatingActionButton',
    'deepforge/viz/PipelineControl',
    'deepforge/viz/CodeControl',
    'deepforge/viz/NodePrompter',
    'deepforge/viz/Execute',
    './Actions',
    'widgets/EasyDAG/AddNodeDialog',
    'js/RegistryKeys',
    'js/Panels/MetaEditor/MetaEditorConstants',
    'q',
    'deepforge/globals',
    'deepforge/Constants',
    'plugin/Export/Export/format'
], function (
    BlobClient,
    SaveToDisk,
    ConfigDialog,
    GME_CONSTANTS,
    PluginButton,
    PipelineControl,
    CodeControl,
    NodePrompter,
    Execute,
    ACTIONS,
    AddNodeDialog,
    REGISTRY_KEYS,
    META_CONSTANTS,
    Q,
    DeepForge,
    Constants,
    ExportFormatDict
) {
    'use strict';

    var NEW_OPERATION_ID = '__NEW_OPERATION__';
    var ForgeActionButton= function (layoutManager, params) {
        PluginButton.call(this, layoutManager, params);
        this._client = this.client;
        this._actions = [];
        this._blobClient = new BlobClient({
            logger: this.logger.fork('BlobClient')
        });

        Execute.call(this, this.client, this.logger);
        this.initializeKeyListener();
        this.logger.debug('ctor finished');
    };

    // inherit from PanelBaseWithHeader
    _.extend(
        ForgeActionButton.prototype,
        PluginButton.prototype,
        Execute.prototype,
        PipelineControl.prototype,
        CodeControl.prototype
    );

    ForgeActionButton.prototype.initializeKeyListener = function() {
        // add key listener to parent?
        this.oldOnKeyDown = document.body.onkeydown;
        document.onkeydown = event => {
            var keys = String.fromCharCode(event.which) || '',
                names = Object.keys(this.buttons),
                btn,
                name;

            // Simple button detection
            if (event.which === 13) {
                keys = 'enter';
            }
            if (event.shiftKey) {
                keys = 'shift ' + keys;
            }

            for (var i = names.length; i--;) {
                name = names[i];
                btn = this.buttons[name];
                if (btn.hotkey && btn.hotkey === keys) {
                    btn.action.call(this, event);
                }
            }
            if (this.oldOnKeyDown) {
                this.oldOnKeyDown(event);
            }
        };
    };

    ForgeActionButton.prototype.destroy = function() {
        PluginButton.prototype.destroy.call(this);
        PipelineControl.prototype.destroy.call(this);
        document.body.onclick = this.oldOnKeyDown;
    };

    ForgeActionButton.prototype.findActionsFor = function(nodeId) {
        var node = this.client.getNode(nodeId),
            base = this.client.getNode(node.getMetaTypeId()),
            isMeta = base && base.getId() === node.getId(),
            suffix = isMeta ? '_META' : '',
            actions,
            basename;

        if (!base) {  // must be ROOT or FCO
            basename = node.getAttribute('name') || 'ROOT_NODE';
            actions = this.getDefinedActionsFor(basename, node)
                .filter(action => !action.filter || action.filter.call(this));
            return actions;
        }

        while (base && !(actions && actions.length)) {
            basename = base.getAttribute('name') + suffix;
            base = this.client.getNode(base.getBaseId());
            actions = this.getDefinedActionsFor(basename, node);
            if (actions) {
                actions = actions.filter(action => !action.filter || action.filter.call(this));
            }
        }

        return actions;
    };

    ForgeActionButton.prototype.getDefinedActionsFor = function(basename, node) {
        // Get the actions for the given node from the ACTIONS dictionary
        if (typeof ACTIONS[basename] === 'function') {
            return ACTIONS[basename].call(this, this.client, node);
        }
        return ACTIONS[basename] || [];
    };

    ForgeActionButton.prototype.onNodeLoad = function(nodeId) {
        PluginButton.prototype.onNodeLoad.call(this, nodeId);
        this.addActionsForObject(nodeId);
    };

    ForgeActionButton.prototype.refresh = function() {
        return this.onNodeLoad(this._currentNodeId);
    };

    ForgeActionButton.prototype.addActionsForObject = function(nodeId) {
        var actions = this.findActionsFor(nodeId),
            i;

        // Remove old actions
        for (i = this._actions.length; i--;) {
            delete this.buttons[this._actions[i].name];
        }

        // Get node name and look up actions
        for (i = actions.length; i--;) {
            this.buttons[actions[i].name] = actions[i];
        }

        this._actions = actions;
        this.update();
    };

    // Helper functions REMOVE! FIXME
    ForgeActionButton.prototype.addToMetaSheet = function(nodeId, metasheetName) {
        var root = this.client.getNode(GME_CONSTANTS.PROJECT_ROOT_ID),
            metatabs = root.getRegistry(REGISTRY_KEYS.META_SHEETS),
            metatab = metatabs.find(tab => tab.title === metasheetName) || metatabs[0],
            metatabId = metatab.SetID;

        // Add to the general meta
        this.client.addMember(
            GME_CONSTANTS.PROJECT_ROOT_ID,
            nodeId,
            META_CONSTANTS.META_ASPECT_SET_NAME
        );
        this.client.setMemberRegistry(
            GME_CONSTANTS.PROJECT_ROOT_ID,
            nodeId,
            META_CONSTANTS.META_ASPECT_SET_NAME,
            REGISTRY_KEYS.POSITION,
            {
                x: 100,
                y: 100
            }
        );

        // Add to the specific sheet
        this.client.addMember(GME_CONSTANTS.PROJECT_ROOT_ID, nodeId, metatabId);
        this.client.setMemberRegistry(
            GME_CONSTANTS.PROJECT_ROOT_ID,
            nodeId,
            metatabId,
            REGISTRY_KEYS.POSITION,
            {
                x: 100,
                y: 100
            }
        );
    };

    ForgeActionButton.prototype.createNamedNode = function(baseId, isMeta) {
        var parentId = this._currentNodeId,
            newId = this.client.createNode({parentId, baseId}),
            basename = 'New' + this.client.getNode(baseId).getAttribute('name'),
            newName = this.getUniqueName(parentId, basename);

        // If instance, make the first char lowercase
        if (!isMeta) {
            newName = newName.substring(0, 1).toLowerCase() + newName.substring(1);
        }
        this.client.setAttribute(newId, 'name', newName);
        return newId;
    };

    ForgeActionButton.prototype.getUniqueName = function(parentId, basename) {
        var pNode = this.client.getNode(parentId),
            children = pNode.getChildrenIds().map(id => this.client.getNode(id)),
            name = basename,
            exists = {},
            i = 2;

        children.forEach(child => exists[child.getAttribute('name')] = true);

        while (exists[name]) {
            name = basename + '_' + i;
            i++;
        }

        return name;
    };

    ForgeActionButton.prototype.getLayerTypeDesc = function(node) {
        var decManager = this.client.decoratorManager,
            desc = {};

        desc.id = node.getId();
        desc.name = node.getAttribute('name');
        desc.baseName = desc.name;
        desc.attributes = {};
        desc.pointers = {};

        // Get the decorator
        desc.Decorator = decManager.getDecoratorForWidget('EllipseDecorator', 'EasyDAG');

        // Set the color
        desc.color = '#9e9e9e';
        return desc;
    };

    ForgeActionButton.prototype.promptLayerType = function() {
        // Prompt for the new custom layer's base type
        var metanodes = this.client.getAllMetaNodes(),
            baseLayerId = metanodes.find(n => n.getAttribute('name') === 'Layer').getId(),
            layerType,
            types;

        // PoA:
        //   - Get the layer type ids
        //   - Create the descriptors
        //     - Get the color for the given types
        //       - Move colors to a constants dir?

        // Get the layer type ids
        layerType = metanodes
            .filter(node => node.getBaseId() === baseLayerId);

        //   - Create the descriptors
        types = layerType.map(node => {
            return {
                node: this.getLayerTypeDesc(node)
            };
        });

        return AddNodeDialog.prompt(types);
    };

    ForgeActionButton.prototype.uploadFile = function(event) {
        var deferred = Q.defer(),
            file,

            files,
            afName,
            artifact;

        // cancel event and hover styling
        event.stopPropagation();
        event.preventDefault();

        // fetch FileList object
        files = event.target.files || event.dataTransfer.files;

        // should only receive one file
        if (files && files.length > 0) {
            if (files.length > 1) {
                this.logger.warn('Received multiple files. Using only the first');
            }

            afName = 'imported-architecture';
            artifact = this._blobClient.createArtifact(afName);

            file = files[0];
            artifact.addFileAsSoftLink(file.name, file, (err, hash) => {
                if (err) {
                    deferred.reject(err);
                    return;
                }
                deferred.resolve(hash);
            });
        }
        return deferred.promise;
    };

    /////////////// Expanding containers ///////////////
    ForgeActionButton.prototype.addOperation = function() {
        var ops = this.getValidInitialNodes(),
            newOperation = this.getNewOpNode();

        // Add the 'New op button'
        ops.push(newOperation);

        this.promptNode(ops, (selected, prompter) => {
            if (selected.id === NEW_OPERATION_ID) {
                prompter.destroy();
                DeepForge.create.Operation();
            } else {
                this.createNode(selected.id);
            }
        });
    };

    ForgeActionButton.prototype.getNewOpNode = function() {
        var Decorator = this.client.decoratorManager.getDecoratorForWidget(
            'OperationDecorator', 'EasyDAG');

        return {
            id: NEW_OPERATION_ID,
            class: 'create-node',
            name: 'New Operation...',
            Decorator: Decorator,
            attributes: {}
        };
    };

    ForgeActionButton.prototype.promptNode = function(nodes, selectFn) {
        // Get the absolute location of the given button
        var mainBtn = this.$el[0].children[0],
            rect = mainBtn.getBoundingClientRect(),
            panelRect,
            panelWidth = 400,
            panelHeight = 400,
            btns = this.$el.find('.tooltipped'),
            ids;

        this.$el.closeFAB();

        // Hide the tooltip
        ids = Array.prototype.map.call(btns, el => el.getAttribute('data-tooltip-id'));
        ids.map(id => $('#' + id))
            .filter(matches => matches.length)
            .forEach(tooltip => tooltip.hide());

        panelRect = {
            left: rect.right-panelWidth,
            top: rect.bottom-panelHeight,
            width: panelWidth,
            height: panelHeight
        };

        var cx = panelWidth-rect.width/2,
            cy = panelHeight-rect.width/2,
            prompter = new NodePrompter(panelRect, {cx, cy, padding: 5});

        return prompter.prompt(nodes, selectFn);
    };

    ForgeActionButton.prototype.deleteCurrentNode = function(msg) {
        var nodeId = this._currentNodeId;
        if (nodeId) {
            this.client.startTransaction(msg);
            this.client.deleteNode(nodeId);
            this.client.completeTransaction();
        }
    };

    ForgeActionButton.prototype.downloadFromBlob = function(hash) {
        this._blobClient.getMetadata(hash)
            .then(metadata => {
                var url = this._blobClient.getDownloadURL(hash),
                    name = metadata.name,
                    save = document.createElement('a');

                save.href = url;
                save.target = '_self';
                save.download = name;

                save.click();
                (window.URL || window.webkitURL).revokeObjectURL(save.href);
            })
            .fail(err => this.logger.error(`Blob download failed: ${err}`));
    };

    // Export Pipeline Support
    ForgeActionButton.prototype.exportPipeline = async function() {
        var pluginId = 'Export',
            metadata = WebGMEGlobal.allPluginsMetadata[pluginId],
            id = this._currentNodeId,
            node = this.client.getNode(id),
            inputData,
            inputNames;

        inputData = node.getChildrenIds()
            .map(id => this.client.getNode(id))
            .filter(node => {
                var typeId = node.getMetaTypeId(),
                    type = this.client.getNode(typeId).getAttribute('name');

                return type === Constants.OP.INPUT;
            })
            .map(input => {
                var outputCntr,
                    outputIds;

                outputCntr = input.getChildrenIds()
                    .map(id => this.client.getNode(id))
                    .find(node => {
                        var typeId = node.getMetaTypeId(),
                            type = this.client.getNode(typeId).getAttribute('name');
                        return type === 'Outputs';
                    });

                // input operations only have a single output
                outputIds = outputCntr.getChildrenIds();

                if (outputIds.length === 1) {
                    return outputIds[0];
                } else if (outputIds.length > 1) {
                    this.logger.warn(`Found multiple ids for input op: ${outputIds.join(', ')}`);
                    return;
                }
            })
            .filter(outputId => !!outputId)
            .map(id => this.client.getNode(id))
            .filter(output => output.getAttribute('data'));

        // get the name of node referenced from the input op
        inputNames = inputData
            .map(node => {
                var cntrId = node.getParentId(),
                    opId = this._client.getNode(cntrId).getParentId(),
                    inputOp = this._client.getNode(opId),
                    targetNodeId = inputOp.getPointer('artifact').to;

                return this._client.getNode(targetNodeId).getAttribute('name');
            });

        // create config options from inputs
        var inputOpts = inputNames.map((input, index) => {
            return {
                name: inputData[index].getId(),
                displayName: input,
                description: `Export ${input} as static (non-input) content`,
                value: false,
                valueType: 'boolean',
                readOnly: false
            };
        }).sort((a, b) => a.displayName < b.displayName ? -1 : 1);

        var exportFormats = Object.keys(ExportFormatDict),
            configDialog = new ConfigDialog(this.client),
            inputConfig = _.extend({}, metadata);

        inputConfig.configStructure = inputOpts;

        // Try to get the extension options
        if (inputOpts.length || exportFormats.length > 1) {
            inputConfig.configStructure.unshift({
                name: 'staticInputs',
                displayName: 'Static Artifacts',
                valueType: 'section'
            });
            inputConfig.configStructure.push({
                name: 'exportFormatOptions',
                displayName: 'Export Options',
                valueType: 'section'
            });
            const valueItems = Object.keys(ExportFormatDict)
                .map(id => {
                    const configStructure = ExportFormatDict[id].getConfigStructure ?
                        ExportFormatDict[id].getConfigStructure() : [];
                    return {id, configStructure};
                });

            inputConfig.configStructure.push({
                name: 'exportFormat',
                displayName: 'Export Format',
                description: '',
                value: exportFormats[0],
                valueType: 'dict',
                valueItems: valueItems
            });

            const allConfigs = await configDialog.show(inputConfig);
            const context = this.client.getCurrentPluginContext(pluginId);
            const exportFormat = allConfigs[pluginId].exportFormat.id;
            const staticInputs = Object.keys(allConfigs[pluginId])
                .filter(input => input !== 'exportFormat' && allConfigs[pluginId][input]);

            this.logger.debug('Exporting pipeline to format', exportFormat);
            this.logger.debug('static inputs:', staticInputs);

            context.managerConfig.namespace = 'pipeline';
            context.pluginConfig = {
                format: exportFormat,
                staticInputs: staticInputs,
                extensionConfig: allConfigs[pluginId].exportFormat.config
            };
            return await Q.ninvoke(this.client, 'runBrowserPlugin', pluginId, context);
        } else {  // no options - just run the plugin!
            const context = this.client.getCurrentPluginContext(pluginId);

            this.logger.debug('Exporting pipeline to format', exportFormats[0]);

            context.managerConfig.namespace = 'pipeline';
            context.pluginConfig = {
                format: exportFormats[0],
                staticInputs: []
            };
            return await Q.ninvoke(this.client, 'runBrowserPlugin', pluginId, context);
        }
    };

    return ForgeActionButton;
});
<commit_msg>Execute Export plugin on server. Fixes #1312 (#1313)<commit_after>/*globals $, define, _, WebGMEGlobal */
/*jshint browser: true*/

define([
    'blob/BlobClient',
    'js/Utils/SaveToDisk',
    'deepforge/viz/ConfigDialog',
    'js/Constants',
    'panel/FloatingActionButton/FloatingActionButton',
    'deepforge/viz/PipelineControl',
    'deepforge/viz/CodeControl',
    'deepforge/viz/NodePrompter',
    'deepforge/viz/Execute',
    './Actions',
    'widgets/EasyDAG/AddNodeDialog',
    'js/RegistryKeys',
    'js/Panels/MetaEditor/MetaEditorConstants',
    'q',
    'deepforge/globals',
    'deepforge/Constants',
    'plugin/Export/Export/format'
], function (
    BlobClient,
    SaveToDisk,
    ConfigDialog,
    GME_CONSTANTS,
    PluginButton,
    PipelineControl,
    CodeControl,
    NodePrompter,
    Execute,
    ACTIONS,
    AddNodeDialog,
    REGISTRY_KEYS,
    META_CONSTANTS,
    Q,
    DeepForge,
    Constants,
    ExportFormatDict
) {
    'use strict';

    var NEW_OPERATION_ID = '__NEW_OPERATION__';
    var ForgeActionButton= function (layoutManager, params) {
        PluginButton.call(this, layoutManager, params);
        this._client = this.client;
        this._actions = [];
        this._blobClient = new BlobClient({
            logger: this.logger.fork('BlobClient')
        });

        Execute.call(this, this.client, this.logger);
        this.initializeKeyListener();
        this.logger.debug('ctor finished');
    };

    // inherit from PanelBaseWithHeader
    _.extend(
        ForgeActionButton.prototype,
        PluginButton.prototype,
        Execute.prototype,
        PipelineControl.prototype,
        CodeControl.prototype
    );

    ForgeActionButton.prototype.initializeKeyListener = function() {
        // add key listener to parent?
        this.oldOnKeyDown = document.body.onkeydown;
        document.onkeydown = event => {
            var keys = String.fromCharCode(event.which) || '',
                names = Object.keys(this.buttons),
                btn,
                name;

            // Simple button detection
            if (event.which === 13) {
                keys = 'enter';
            }
            if (event.shiftKey) {
                keys = 'shift ' + keys;
            }

            for (var i = names.length; i--;) {
                name = names[i];
                btn = this.buttons[name];
                if (btn.hotkey && btn.hotkey === keys) {
                    btn.action.call(this, event);
                }
            }
            if (this.oldOnKeyDown) {
                this.oldOnKeyDown(event);
            }
        };
    };

    ForgeActionButton.prototype.destroy = function() {
        PluginButton.prototype.destroy.call(this);
        PipelineControl.prototype.destroy.call(this);
        document.body.onclick = this.oldOnKeyDown;
    };

    ForgeActionButton.prototype.findActionsFor = function(nodeId) {
        var node = this.client.getNode(nodeId),
            base = this.client.getNode(node.getMetaTypeId()),
            isMeta = base && base.getId() === node.getId(),
            suffix = isMeta ? '_META' : '',
            actions,
            basename;

        if (!base) {  // must be ROOT or FCO
            basename = node.getAttribute('name') || 'ROOT_NODE';
            actions = this.getDefinedActionsFor(basename, node)
                .filter(action => !action.filter || action.filter.call(this));
            return actions;
        }

        while (base && !(actions && actions.length)) {
            basename = base.getAttribute('name') + suffix;
            base = this.client.getNode(base.getBaseId());
            actions = this.getDefinedActionsFor(basename, node);
            if (actions) {
                actions = actions.filter(action => !action.filter || action.filter.call(this));
            }
        }

        return actions;
    };

    ForgeActionButton.prototype.getDefinedActionsFor = function(basename, node) {
        // Get the actions for the given node from the ACTIONS dictionary
        if (typeof ACTIONS[basename] === 'function') {
            return ACTIONS[basename].call(this, this.client, node);
        }
        return ACTIONS[basename] || [];
    };

    ForgeActionButton.prototype.onNodeLoad = function(nodeId) {
        PluginButton.prototype.onNodeLoad.call(this, nodeId);
        this.addActionsForObject(nodeId);
    };

    ForgeActionButton.prototype.refresh = function() {
        return this.onNodeLoad(this._currentNodeId);
    };

    ForgeActionButton.prototype.addActionsForObject = function(nodeId) {
        var actions = this.findActionsFor(nodeId),
            i;

        // Remove old actions
        for (i = this._actions.length; i--;) {
            delete this.buttons[this._actions[i].name];
        }

        // Get node name and look up actions
        for (i = actions.length; i--;) {
            this.buttons[actions[i].name] = actions[i];
        }

        this._actions = actions;
        this.update();
    };

    // Helper functions REMOVE! FIXME
    ForgeActionButton.prototype.addToMetaSheet = function(nodeId, metasheetName) {
        var root = this.client.getNode(GME_CONSTANTS.PROJECT_ROOT_ID),
            metatabs = root.getRegistry(REGISTRY_KEYS.META_SHEETS),
            metatab = metatabs.find(tab => tab.title === metasheetName) || metatabs[0],
            metatabId = metatab.SetID;

        // Add to the general meta
        this.client.addMember(
            GME_CONSTANTS.PROJECT_ROOT_ID,
            nodeId,
            META_CONSTANTS.META_ASPECT_SET_NAME
        );
        this.client.setMemberRegistry(
            GME_CONSTANTS.PROJECT_ROOT_ID,
            nodeId,
            META_CONSTANTS.META_ASPECT_SET_NAME,
            REGISTRY_KEYS.POSITION,
            {
                x: 100,
                y: 100
            }
        );

        // Add to the specific sheet
        this.client.addMember(GME_CONSTANTS.PROJECT_ROOT_ID, nodeId, metatabId);
        this.client.setMemberRegistry(
            GME_CONSTANTS.PROJECT_ROOT_ID,
            nodeId,
            metatabId,
            REGISTRY_KEYS.POSITION,
            {
                x: 100,
                y: 100
            }
        );
    };

    ForgeActionButton.prototype.createNamedNode = function(baseId, isMeta) {
        var parentId = this._currentNodeId,
            newId = this.client.createNode({parentId, baseId}),
            basename = 'New' + this.client.getNode(baseId).getAttribute('name'),
            newName = this.getUniqueName(parentId, basename);

        // If instance, make the first char lowercase
        if (!isMeta) {
            newName = newName.substring(0, 1).toLowerCase() + newName.substring(1);
        }
        this.client.setAttribute(newId, 'name', newName);
        return newId;
    };

    ForgeActionButton.prototype.getUniqueName = function(parentId, basename) {
        var pNode = this.client.getNode(parentId),
            children = pNode.getChildrenIds().map(id => this.client.getNode(id)),
            name = basename,
            exists = {},
            i = 2;

        children.forEach(child => exists[child.getAttribute('name')] = true);

        while (exists[name]) {
            name = basename + '_' + i;
            i++;
        }

        return name;
    };

    ForgeActionButton.prototype.getLayerTypeDesc = function(node) {
        var decManager = this.client.decoratorManager,
            desc = {};

        desc.id = node.getId();
        desc.name = node.getAttribute('name');
        desc.baseName = desc.name;
        desc.attributes = {};
        desc.pointers = {};

        // Get the decorator
        desc.Decorator = decManager.getDecoratorForWidget('EllipseDecorator', 'EasyDAG');

        // Set the color
        desc.color = '#9e9e9e';
        return desc;
    };

    ForgeActionButton.prototype.promptLayerType = function() {
        // Prompt for the new custom layer's base type
        var metanodes = this.client.getAllMetaNodes(),
            baseLayerId = metanodes.find(n => n.getAttribute('name') === 'Layer').getId(),
            layerType,
            types;

        // PoA:
        //   - Get the layer type ids
        //   - Create the descriptors
        //     - Get the color for the given types
        //       - Move colors to a constants dir?

        // Get the layer type ids
        layerType = metanodes
            .filter(node => node.getBaseId() === baseLayerId);

        //   - Create the descriptors
        types = layerType.map(node => {
            return {
                node: this.getLayerTypeDesc(node)
            };
        });

        return AddNodeDialog.prompt(types);
    };

    ForgeActionButton.prototype.uploadFile = function(event) {
        var deferred = Q.defer(),
            file,

            files,
            afName,
            artifact;

        // cancel event and hover styling
        event.stopPropagation();
        event.preventDefault();

        // fetch FileList object
        files = event.target.files || event.dataTransfer.files;

        // should only receive one file
        if (files && files.length > 0) {
            if (files.length > 1) {
                this.logger.warn('Received multiple files. Using only the first');
            }

            afName = 'imported-architecture';
            artifact = this._blobClient.createArtifact(afName);

            file = files[0];
            artifact.addFileAsSoftLink(file.name, file, (err, hash) => {
                if (err) {
                    deferred.reject(err);
                    return;
                }
                deferred.resolve(hash);
            });
        }
        return deferred.promise;
    };

    /////////////// Expanding containers ///////////////
    ForgeActionButton.prototype.addOperation = function() {
        var ops = this.getValidInitialNodes(),
            newOperation = this.getNewOpNode();

        // Add the 'New op button'
        ops.push(newOperation);

        this.promptNode(ops, (selected, prompter) => {
            if (selected.id === NEW_OPERATION_ID) {
                prompter.destroy();
                DeepForge.create.Operation();
            } else {
                this.createNode(selected.id);
            }
        });
    };

    ForgeActionButton.prototype.getNewOpNode = function() {
        var Decorator = this.client.decoratorManager.getDecoratorForWidget(
            'OperationDecorator', 'EasyDAG');

        return {
            id: NEW_OPERATION_ID,
            class: 'create-node',
            name: 'New Operation...',
            Decorator: Decorator,
            attributes: {}
        };
    };

    ForgeActionButton.prototype.promptNode = function(nodes, selectFn) {
        // Get the absolute location of the given button
        var mainBtn = this.$el[0].children[0],
            rect = mainBtn.getBoundingClientRect(),
            panelRect,
            panelWidth = 400,
            panelHeight = 400,
            btns = this.$el.find('.tooltipped'),
            ids;

        this.$el.closeFAB();

        // Hide the tooltip
        ids = Array.prototype.map.call(btns, el => el.getAttribute('data-tooltip-id'));
        ids.map(id => $('#' + id))
            .filter(matches => matches.length)
            .forEach(tooltip => tooltip.hide());

        panelRect = {
            left: rect.right-panelWidth,
            top: rect.bottom-panelHeight,
            width: panelWidth,
            height: panelHeight
        };

        var cx = panelWidth-rect.width/2,
            cy = panelHeight-rect.width/2,
            prompter = new NodePrompter(panelRect, {cx, cy, padding: 5});

        return prompter.prompt(nodes, selectFn);
    };

    ForgeActionButton.prototype.deleteCurrentNode = function(msg) {
        var nodeId = this._currentNodeId;
        if (nodeId) {
            this.client.startTransaction(msg);
            this.client.deleteNode(nodeId);
            this.client.completeTransaction();
        }
    };

    ForgeActionButton.prototype.downloadFromBlob = function(hash) {
        this._blobClient.getMetadata(hash)
            .then(metadata => {
                var url = this._blobClient.getDownloadURL(hash),
                    name = metadata.name,
                    save = document.createElement('a');

                save.href = url;
                save.target = '_self';
                save.download = name;

                save.click();
                (window.URL || window.webkitURL).revokeObjectURL(save.href);
            })
            .fail(err => this.logger.error(`Blob download failed: ${err}`));
    };

    // Export Pipeline Support
    ForgeActionButton.prototype.exportPipeline = async function() {
        var pluginId = 'Export',
            metadata = WebGMEGlobal.allPluginsMetadata[pluginId],
            id = this._currentNodeId,
            node = this.client.getNode(id),
            inputData,
            inputNames;

        inputData = node.getChildrenIds()
            .map(id => this.client.getNode(id))
            .filter(node => {
                var typeId = node.getMetaTypeId(),
                    type = this.client.getNode(typeId).getAttribute('name');

                return type === Constants.OP.INPUT;
            })
            .map(input => {
                var outputCntr,
                    outputIds;

                outputCntr = input.getChildrenIds()
                    .map(id => this.client.getNode(id))
                    .find(node => {
                        var typeId = node.getMetaTypeId(),
                            type = this.client.getNode(typeId).getAttribute('name');
                        return type === 'Outputs';
                    });

                // input operations only have a single output
                outputIds = outputCntr.getChildrenIds();

                if (outputIds.length === 1) {
                    return outputIds[0];
                } else if (outputIds.length > 1) {
                    this.logger.warn(`Found multiple ids for input op: ${outputIds.join(', ')}`);
                    return;
                }
            })
            .filter(outputId => !!outputId)
            .map(id => this.client.getNode(id))
            .filter(output => output.getAttribute('data'));

        // get the name of node referenced from the input op
        inputNames = inputData
            .map(node => {
                var cntrId = node.getParentId(),
                    opId = this._client.getNode(cntrId).getParentId(),
                    inputOp = this._client.getNode(opId),
                    targetNodeId = inputOp.getPointer('artifact').to;

                return this._client.getNode(targetNodeId).getAttribute('name');
            });

        // create config options from inputs
        var inputOpts = inputNames.map((input, index) => {
            return {
                name: inputData[index].getId(),
                displayName: input,
                description: `Export ${input} as static (non-input) content`,
                value: false,
                valueType: 'boolean',
                readOnly: false
            };
        }).sort((a, b) => a.displayName < b.displayName ? -1 : 1);

        var exportFormats = Object.keys(ExportFormatDict),
            configDialog = new ConfigDialog(this.client),
            inputConfig = _.extend({}, metadata);

        inputConfig.configStructure = inputOpts;

        // Try to get the extension options
        if (inputOpts.length || exportFormats.length > 1) {
            inputConfig.configStructure.unshift({
                name: 'staticInputs',
                displayName: 'Static Artifacts',
                valueType: 'section'
            });
            inputConfig.configStructure.push({
                name: 'exportFormatOptions',
                displayName: 'Export Options',
                valueType: 'section'
            });
            const valueItems = Object.keys(ExportFormatDict)
                .map(id => {
                    const configStructure = ExportFormatDict[id].getConfigStructure ?
                        ExportFormatDict[id].getConfigStructure() : [];
                    return {id, configStructure};
                });

            inputConfig.configStructure.push({
                name: 'exportFormat',
                displayName: 'Export Format',
                description: '',
                value: exportFormats[0],
                valueType: 'dict',
                valueItems: valueItems
            });

            const allConfigs = await configDialog.show(inputConfig);
            const context = this.client.getCurrentPluginContext(pluginId);
            const exportFormat = allConfigs[pluginId].exportFormat.id;
            const staticInputs = Object.keys(allConfigs[pluginId])
                .filter(input => input !== 'exportFormat' && allConfigs[pluginId][input]);

            this.logger.debug('Exporting pipeline to format', exportFormat);
            this.logger.debug('static inputs:', staticInputs);

            context.managerConfig.namespace = 'pipeline';
            context.pluginConfig = {
                format: exportFormat,
                staticInputs: staticInputs,
                extensionConfig: allConfigs[pluginId].exportFormat.config
            };
            return await Q.ninvoke(this.client, 'runServerPlugin', pluginId, context);
        } else {  // no options - just run the plugin!
            const context = this.client.getCurrentPluginContext(pluginId);

            this.logger.debug('Exporting pipeline to format', exportFormats[0]);

            context.managerConfig.namespace = 'pipeline';
            context.pluginConfig = {
                format: exportFormats[0],
                staticInputs: []
            };
            return await Q.ninvoke(this.client, 'runServerPlugin', pluginId, context);
        }
    };

    return ForgeActionButton;
});
<|endoftext|>


<commit_before>import React from 'react';
import DefaultDivider from './components/DefaultDivider';
import DefaultButton from './components/DividerButton';

import addDivider from './modifiers/addDivider';

import dividerStyles from './dividerStyles.css';

const defaultTheme = {
  divider: dividerStyles.divider
};

const createDividerPlugin = ({
  entityType = 'divider',
  dividerComponent = DefaultDivider,
  buttonComponent = DefaultButton,
  theme = defaultTheme,
  decorator
} = {}) => {
  let Divider = dividerComponent;

  if (typeof decorator === 'function') {
    Divider = decorator(Divider);
  }

  const ThemedDivider = (props) =>
    <Divider {...props} theme={theme} />;
  const Button = buttonComponent;
  const DecoratedButton = (props) =>
    <Button {...props} entityType={entityType} addDivider={addDivider(entityType)} />;

  return {
    blockRendererFn: (block, { getEditorState }) => {
      if (block.getType() === 'atomic') {
        const contentState = getEditorState().getCurrentContent();
        const entity = block.getEntityAt(0);
        if (!entity) return null;
        const type = contentState.getEntity(entity).getType();
        if (type === entityType) {
          return {
            component: ThemedDivider,
            editable: false
          };
        }
      }

      return null;
    },
    DividerButton: DecoratedButton,
    addDivider: addDivider(entityType)
  };
};

export default createDividerPlugin;
<commit_msg>Tweak name<commit_after>import React from 'react';
import DefaultDivider from './components/DefaultDivider';
import DefaultButton from './components/DividerButton';

import addDivider from './modifiers/addDivider';

import dividerStyles from './dividerStyles.css';

const defaultTheme = {
  divider: dividerStyles.divider
};

const createDividerPlugin = ({
  entityType = 'divider',
  dividerComponent = DefaultDivider,
  buttonComponent = DefaultButton,
  theme = defaultTheme,
  decorator
} = {}) => {
  let Divider = dividerComponent;

  if (typeof decorator === 'function') {
    Divider = decorator(Divider);
  }

  const ThemedDivider = (props) =>
    <Divider {...props} theme={theme} />;
  const Button = buttonComponent;
  const DividerButton = (props) =>
    <Button {...props} entityType={entityType} addDivider={addDivider(entityType)} />;

  return {
    blockRendererFn: (block, { getEditorState }) => {
      if (block.getType() === 'atomic') {
        const contentState = getEditorState().getCurrentContent();
        const entity = block.getEntityAt(0);
        if (!entity) return null;
        const type = contentState.getEntity(entity).getType();
        if (type === entityType) {
          return {
            component: ThemedDivider,
            editable: false
          };
        }
      }

      return null;
    },
    DividerButton,
    addDivider: addDivider(entityType)
  };
};

export default createDividerPlugin;
<|endoftext|>


<commit_before>(function ($) {
	$.fn.obfuscateEmail = function (user, domain, cc, subject, body, generateLabel) {
		var emailAddress = user + '@' + domain;
		var href = "mailto:" + emailAddress;

		if (cc) {
			href = href + "?cc=" + encodeURIComponent(cc.replace('/µ/g','@'));
		}
		if (subject) {
			if (cc) {
				href = href + "&subject=";
			} else {
				href = href + "?subject=";
			}
			href = href + encodeURIComponent(subject);
		}
		if (body) {
			if (cc || subject) {
				href = href + "&body=";
			} else {
				href = href + "?body=";
			}
			href = href + encodeURIComponent(body);
		}
		$(this, "a").attr("href", href);
		if (generateLabel) {
			$(this, "a").text(emailAddress);
		}
	};
})(jQuery);<commit_msg>Ajout des CC dans l'obfuscatedEmailLink<commit_after>(function ($) {
	$.fn.obfuscateEmail = function (user, domain, cc, subject, body, generateLabel) {
		var emailAddress = user + '@' + domain;
		var href = "mailto:" + emailAddress;

		if (cc) {
			href = href + "?cc=" + encodeURIComponent(cc.replace(/µ/g,'@'));
		}
		if (subject) {
			if (cc) {
				href = href + "&subject=";
			} else {
				href = href + "?subject=";
			}
			href = href + encodeURIComponent(subject);
		}
		if (body) {
			if (cc || subject) {
				href = href + "&body=";
			} else {
				href = href + "?body=";
			}
			href = href + encodeURIComponent(body);
		}
		$(this, "a").attr("href", href);
		if (generateLabel) {
			$(this, "a").text(emailAddress);
		}
	};
})(jQuery);<|endoftext|>


<commit_before>define(function(require, exports, module) {
    "use strict";

    main.consumes = [
        "Plugin", "tabManager", "menus", "commands", "settings",
        "tree", "save", "ui", "c9"
    ];
    main.provides = ["openfiles"];
    return main;

    function main(options, imports, register) {
        var Plugin   = imports.Plugin;
        var tabs     = imports.tabManager;
        var menus    = imports.menus;
        var commands = imports.commands;
        var settings = imports.settings;
        var tree     = imports.tree;
        var save     = imports.save;
        var ui       = imports.ui;

        var Tree     = require("ace_tree/tree");
        var TreeData = require("./openfilesdp");
        var basename = require("path").basename;

        /***** Initialization *****/

        var plugin        = new Plugin("Ajax.org", main.consumes);
        var emit          = plugin.getEmitter();
        var staticPrefix  = options.staticPrefix;

        // tree maximum height
        var showOpenFiles = false;
        var dragged       = false;

        // UI Elements
        var ofDataProvider, ofTree, treeParent, winFileTree;
        var ctxItem, ctxDiv

        var loaded = false;
        function load(){
            if (loaded) return false;
            loaded = true;

            // Hook events to get the focussed tab
            tabs.on("focusSync", delayedUpdate);
            // tabs.on("tabDestroy", delayedUpdate);
            tabs.on("tabAfterClose", update);
            tabs.on("tabReparent", delayedUpdate);
            tabs.on("tabOrder", delayedUpdate);

            save.on("tabSavingState", refresh);

            commands.addCommand({
                name: "toggleOpenfiles",
                exec: function() {
                    toggleOpenfiles();
                }
            }, plugin);

            menus.addItemByPath("View/Open Files", new ui.item({
                type    : "check",
                checked : "[{settings.model}::user/openfiles/@show]",
                command : "toggleOpenfiles"
            }), 200, plugin);

            settings.on("read", function(e){
                // Defaults
                settings.setDefaults("user/openfiles", [
                    ["show", "false"],
                    ["hidetree", "false"]
                ]);
                showOpenFiles = settings.getBool("user/openfiles/@show");
                updateVisibility(showOpenFiles);
            }, plugin);
        }

        var drawn = false;
        function draw(){
            if (drawn) return;
            drawn = true;

            // ace_tree customization '.openfiles'
            ui.insertCss(require("text!./openfiles.css"), staticPrefix, plugin);

            tree.getElement("winOpenfiles", function(winOpenfiles) {
                treeParent = winOpenfiles;

                if (settings.getBool("state/openfiles/@hidetree"))
                    ui.setStyleClass(treeParent.parentNode.$int, "hidetree");

                tree.getElement("winFileTree", function(winFileTreeL) {
                    winFileTree = winFileTreeL;
                });

                var div = document.createElement("div");
                treeParent.$int.appendChild(div);

                // Create the Ace Tree
                ofTree = new Tree(div);
                ofDataProvider = new TreeData();
                // ofTree.renderer.setScrollMargin(0, 10);
                ofTree.renderer.setTheme({cssClass: "filetree"});
                // Assign the dataprovider
                ofTree.setDataProvider(ofDataProvider);
                // Some global render metadata
                ofDataProvider.staticPrefix = staticPrefix;

                ofTree.on("userSelect", function(){
                    setTimeout(onSelect, 40);
                });

                // make sure scrollbar on windows is in the padding area
                ofTree.renderer.on("scrollbarVisibilityChanged", updateScrollBarSize);
                ofTree.renderer.on("resize", updateScrollBarSize);
                function updateScrollBarSize() {
                    var w = ofTree.renderer.scrollBarV.getWidth();
                    ofTree.renderer.scroller.style.right = Math.max(w, 10) + "px";
                }

                // APF + DOM HACK: close tab with confirmation
                ofTree.on("mousedown", function(e){
                    var domTarget = e.domEvent.target;
                    var pos = e.getDocumentPosition();
                    var node = ofDataProvider.findItemAtOffset(pos.y);
                    if (node.children.length && !~domTarget.className.indexOf("toggler")) {
                        e.preventDefault();
                        return;
                    }

                    if (! (node && node.path && domTarget && ~domTarget.className.indexOf("close")))
                        return;

                    var amlTab = node.tab.aml;
                    amlTab.parentNode.remove(amlTab, {});
                });

                ofTree.focus = function() {};

                var mnuFilesSettings = tree.getElement("mnuFilesSettings");
                ctxItem = ui.insertByIndex(mnuFilesSettings, new ui.item({
                    caption : "Hide Workspace Files",
                    type    : "check",
                    visible : treeParent.visible,
                    checked : "[{settings.model}::state/openfiles/@hidetree]",
                    onclick : function(e){
                        if (this.checked)
                            ui.setStyleClass(treeParent.parentNode.$int, "hidetree");
                        else
                            ui.setStyleClass(treeParent.parentNode.$int, "", ["hidetree"]);

                        ofTree.resize();
                        tree.resize();
                    }
                }), 10, plugin);
                ctxDiv = ui.insertByIndex(mnuFilesSettings, new ui.divider({
                    visible : treeParent.visible
                }), 20, plugin);

                if (showOpenFiles)
                    tabs.on("ready", function(){ show(); });
                else
                    hideOpenFiles();

                emit("draw");
            });
        }

        /***** Methods *****/

        var updateTimer;
        function delayedUpdate() {
            if (updateTimer)
                return;
            updateTimer = setTimeout(function() {
                updateTimer = null;
                update();
            }, 10);
        }

        function update() {
            if (!showOpenFiles)
                return;

            draw();

            if (!ofTree)
                return;

            var activePanes = tabs.getPanes(tabs.container);
            var focussedTab = tabs.focussedTab;
            // focussedTab can be the terminal or output views
            if (focussedTab && activePanes.indexOf(focussedTab.pane) === -1 && activePanes.length)
                focussedTab = activePanes[0].getTab();

            // unhook document change update listeners
            tabs.getTabs().forEach(function (tab) {
                tab.document && tab.document.off("changed", refresh);
            });

            var selected;
            var root = activePanes.map(function (pane, i) {
                return {
                    // name: pane.name (tab0 ...)
                    items: pane.getTabs()
                      .filter(function(tab){
                          return tab.path && tab.loaded && !tab.meta.$closing;
                      })
                      .map(function (tab) {
                        var node = {
                            name  : basename(tab.path),
                            path  : tab.path,
                            items : [],
                            tab   : tab
                        };
                        if (tab == focussedTab)
                            selected = node;

                        tab.document.on("changed", refresh);
                        return node;
                    })
                };
            }).filter(function(pane){
                return pane.items.length;
            }).map(function (node, i) {
                node.name = "GROUP " + (i+1);
                return node;
            });

            // Hide the openfiles
            if (!root.length)
                return hideOpenFiles();

            ui.setStyleClass(treeParent.parentNode.$int, "hasopenfiles");

            treeParent.show();

            if (root.length === 1)
                root = root[0];

            ofDataProvider.setRoot(root);
            ofDataProvider.selection.selectNode(selected);

            ofTree.resize(true);

            var maxHeight = treeParent.parentNode.$int.offsetHeight * 0.5;
            var treeHeight = ofTree.renderer.layerConfig.maxHeight + 23;

            treeParent.$int.style.height = dragged
                ? Math.min(treeParent.getHeight(), treeHeight) + "px"
                : Math.min(treeHeight, maxHeight) + "px";

            ofTree.resize(true);
            tree.resize();

            ofTree.renderer.scrollCaretIntoView(selected, 0.5);
        }

        function refresh() {
            if (!showOpenFiles || !ofDataProvider)
                return;
            ofDataProvider._signal("change");
        }

        function findNode(json, path) {
            for (var i = 0; i < json.length; i++) {
                var elem = json[i];
                if(path === elem.path)
                    return elem;
                var inChilren = findNode(elem.items, path);
                if (inChilren)
                    return inChilren;
            }
            return null;
        }

        function onSelect() {
            var node = ofTree.selection.getCursor();
            tabs.focusTab(node.path);
        }

        function hideOpenFiles() {
            var htmlNode = treeParent && treeParent.parentNode.$int;
            htmlNode && ui.setStyleClass(htmlNode, "", ["hasopenfiles"]);
            treeParent && treeParent.hide();
            tree.resize();
        }

        function toggleOpenfiles() {
            showOpenFiles = !showOpenFiles;
            settings.set("user/openfiles/@show", showOpenFiles);
            updateVisibility(showOpenFiles);
        }

        function updateVisibility(show) {
            if (show) {
                draw();
                update();
                ctxItem && ctxItem.show();
                ctxDiv && ctxDiv.show();

                if (settings.getBool("state/openfiles/@hidetree"))
                    ui.setStyleClass(treeParent.parentNode.$int, "hidetree");
            }
            else {
                hideOpenFiles();
                ctxItem && ctxItem.hide();
                ctxDiv && ctxDiv.hide();
                if (treeParent)
                    ui.setStyleClass(treeParent.parentNode.$int, "", ["hidetree"]);
            }

            emit("visible", { value: show });
        }

        /***** Lifecycle *****/

        plugin.on("load", function(){
            load();
        });
        plugin.on("enable", function(){

        });
        plugin.on("disable", function(){

        });
        plugin.on("unload", function(){
            loaded = false;
            drawn  = false;
        });

        function show() {
            updateVisibility(true);
        }

        function hide() {
            updateVisibility(false);
        }

        /***** Register and define API *****/

        /**
         * Displays the open files above the tree.
         *
         * @singleton
         */
        plugin.freezePublicAPI({
            /**
             * Show the openfiles panel
             */
            show: show,

            /**
             * Hide the openfiles panel
             */
            hide: hide,

            /**
             * Trigger a complete update of the openfiles view, only when the
             * openfiles panel is visible.
             */
            update: update,

            /**
             * Rerender the viewed part of the tree without having to recreate
             * the tree data.
             * Example usage: when the saving state or document content changed
             * Only applies when openfiles is visible
             */
            refresh: refresh
        });

        register(null, {
            openfiles: plugin
        });
    }
});
<commit_msg>Tweaked members UX<commit_after>define(function(require, exports, module) {
    "use strict";

    main.consumes = [
        "Plugin", "tabManager", "menus", "commands", "settings",
        "tree", "save", "ui", "c9"
    ];
    main.provides = ["openfiles"];
    return main;

    function main(options, imports, register) {
        var Plugin   = imports.Plugin;
        var tabs     = imports.tabManager;
        var menus    = imports.menus;
        var commands = imports.commands;
        var settings = imports.settings;
        var tree     = imports.tree;
        var save     = imports.save;
        var ui       = imports.ui;

        var Tree     = require("ace_tree/tree");
        var TreeData = require("./openfilesdp");
        var basename = require("path").basename;

        /***** Initialization *****/

        var plugin        = new Plugin("Ajax.org", main.consumes);
        var emit          = plugin.getEmitter();
        var staticPrefix  = options.staticPrefix;

        // tree maximum height
        var showOpenFiles = false;
        var dragged       = false;

        // UI Elements
        var ofDataProvider, ofTree, treeParent, winFileTree;
        var ctxItem, ctxDiv

        var loaded = false;
        function load(){
            if (loaded) return false;
            loaded = true;

            // Hook events to get the focussed tab
            tabs.on("focusSync", delayedUpdate);
            // tabs.on("tabDestroy", delayedUpdate);
            tabs.on("tabAfterClose", update);
            tabs.on("tabReparent", delayedUpdate);
            tabs.on("tabOrder", delayedUpdate);

            save.on("tabSavingState", refresh);

            commands.addCommand({
                name: "toggleOpenfiles",
                exec: function() {
                    toggleOpenfiles();
                }
            }, plugin);

            menus.addItemByPath("View/Open Files", new ui.item({
                type    : "check",
                checked : "[{settings.model}::user/openfiles/@show]",
                command : "toggleOpenfiles"
            }), 200, plugin);

            settings.on("read", function(e){
                // Defaults
                settings.setDefaults("user/openfiles", [
                    ["show", "false"],
                    ["hidetree", "false"]
                ]);
                showOpenFiles = settings.getBool("user/openfiles/@show");
                updateVisibility(showOpenFiles);
            }, plugin);
        }

        var drawn = false;
        function draw(){
            if (drawn) return;
            drawn = true;

            // ace_tree customization '.openfiles'
            ui.insertCss(require("text!./openfiles.css"), staticPrefix, plugin);

            tree.getElement("winOpenfiles", function(winOpenfiles) {
                treeParent = winOpenfiles;

                if (settings.getBool("state/openfiles/@hidetree"))
                    ui.setStyleClass(treeParent.parentNode.$int, "hidetree");

                tree.getElement("winFileTree", function(winFileTreeL) {
                    winFileTree = winFileTreeL;
                });

                var div = document.createElement("div");
                treeParent.$int.appendChild(div);

                // Create the Ace Tree
                ofTree = new Tree(div);
                ofDataProvider = new TreeData();
                // ofTree.renderer.setScrollMargin(0, 10);
                ofTree.renderer.setTheme({cssClass: "filetree"});
                // Assign the dataprovider
                ofTree.setDataProvider(ofDataProvider);
                // Some global render metadata
                ofDataProvider.staticPrefix = staticPrefix;

                ofDataProvider.on("collapse", function(){ delayedUpdate(); });
                ofDataProvider.on("expand", function(){ delayedUpdate(); });

                ofTree.on("userSelect", function(){
                    setTimeout(onSelect, 40);
                });

                // make sure scrollbar on windows is in the padding area
                ofTree.renderer.on("scrollbarVisibilityChanged", updateScrollBarSize);
                ofTree.renderer.on("resize", updateScrollBarSize);
                function updateScrollBarSize() {
                    var w = ofTree.renderer.scrollBarV.getWidth();
                    ofTree.renderer.scroller.style.right = Math.max(w, 10) + "px";
                }

                // APF + DOM HACK: close tab with confirmation
                ofTree.on("mousedown", function(e){
                    var domTarget = e.domEvent.target;
                    var pos = e.getDocumentPosition();
                    var node = ofDataProvider.findItemAtOffset(pos.y);
                    if (node.children.length && !~domTarget.className.indexOf("toggler")) {
                        e.preventDefault();
                        return;
                    }

                    if (! (node && node.path && domTarget && ~domTarget.className.indexOf("close")))
                        return;

                    var amlTab = node.tab.aml;
                    amlTab.parentNode.remove(amlTab, {});
                });

                ofTree.focus = function() {};

                var mnuFilesSettings = tree.getElement("mnuFilesSettings");
                ctxItem = ui.insertByIndex(mnuFilesSettings, new ui.item({
                    caption : "Hide Workspace Files",
                    type    : "check",
                    visible : treeParent.visible,
                    checked : "[{settings.model}::state/openfiles/@hidetree]",
                    onclick : function(e){
                        if (this.checked)
                            ui.setStyleClass(treeParent.parentNode.$int, "hidetree");
                        else
                            ui.setStyleClass(treeParent.parentNode.$int, "", ["hidetree"]);

                        ofTree.resize();
                        tree.resize();
                    }
                }), 10, plugin);
                ctxDiv = ui.insertByIndex(mnuFilesSettings, new ui.divider({
                    visible : treeParent.visible
                }), 20, plugin);

                if (showOpenFiles)
                    tabs.on("ready", function(){ show(); });
                else
                    hideOpenFiles();

                emit("draw");
            });
        }

        /***** Methods *****/

        var updateTimer;
        function delayedUpdate() {
            if (updateTimer)
                return;
            updateTimer = setTimeout(function() {
                updateTimer = null;
                update();
            }, 10);
        }

        function update() {
            if (!showOpenFiles)
                return;

            draw();

            if (!ofTree)
                return;

            var activePanes = tabs.getPanes(tabs.container);
            var focussedTab = tabs.focussedTab;
            // focussedTab can be the terminal or output views
            if (focussedTab && activePanes.indexOf(focussedTab.pane) === -1 && activePanes.length)
                focussedTab = activePanes[0].getTab();

            // unhook document change update listeners
            tabs.getTabs().forEach(function (tab) {
                tab.document && tab.document.off("changed", refresh);
            });

            var selected;
            var root = activePanes.map(function (pane, i) {
                return {
                    // name: pane.name (tab0 ...)
                    items: pane.getTabs()
                      .filter(function(tab){
                          return tab.path && tab.loaded && !tab.meta.$closing;
                      })
                      .map(function (tab) {
                        var node = {
                            name  : basename(tab.path),
                            path  : tab.path,
                            items : [],
                            tab   : tab
                        };
                        if (tab == focussedTab)
                            selected = node;

                        tab.document.on("changed", refresh);
                        return node;
                    })
                };
            }).filter(function(pane){
                return pane.items.length;
            }).map(function (node, i) {
                node.name = "GROUP " + (i+1);
                return node;
            });

            // Hide the openfiles
            if (!root.length)
                return hideOpenFiles();

            ui.setStyleClass(treeParent.parentNode.$int, "hasopenfiles");

            treeParent.show();

            if (root.length === 1)
                root = root[0];

            ofDataProvider.setRoot(root);
            ofDataProvider.selection.selectNode(selected);

            ofTree.resize(true);

            var maxHeight = treeParent.parentNode.$int.offsetHeight * 0.5;
            var treeHeight = ofTree.renderer.layerConfig.maxHeight + 23;

            treeParent.$int.style.height = dragged
                ? Math.min(treeParent.getHeight(), treeHeight) + "px"
                : Math.min(treeHeight, maxHeight) + "px";

            ofTree.resize(true);
            tree.resize();

            ofTree.renderer.scrollCaretIntoView(selected, 0.5);
        }

        function refresh() {
            if (!showOpenFiles || !ofDataProvider)
                return;
            ofDataProvider._signal("change");
        }

        function findNode(json, path) {
            for (var i = 0; i < json.length; i++) {
                var elem = json[i];
                if(path === elem.path)
                    return elem;
                var inChilren = findNode(elem.items, path);
                if (inChilren)
                    return inChilren;
            }
            return null;
        }

        function onSelect() {
            var node = ofTree.selection.getCursor();
            tabs.focusTab(node.path);
        }

        function hideOpenFiles() {
            var htmlNode = treeParent && treeParent.parentNode.$int;
            htmlNode && ui.setStyleClass(htmlNode, "", ["hasopenfiles"]);
            treeParent && treeParent.hide();
            tree.resize();
        }

        function toggleOpenfiles() {
            showOpenFiles = !showOpenFiles;
            settings.set("user/openfiles/@show", showOpenFiles);
            updateVisibility(showOpenFiles);
        }

        function updateVisibility(show) {
            if (show) {
                draw();
                update();
                ctxItem && ctxItem.show();
                ctxDiv && ctxDiv.show();

                if (settings.getBool("state/openfiles/@hidetree"))
                    ui.setStyleClass(treeParent.parentNode.$int, "hidetree");
            }
            else {
                hideOpenFiles();
                ctxItem && ctxItem.hide();
                ctxDiv && ctxDiv.hide();
                if (treeParent)
                    ui.setStyleClass(treeParent.parentNode.$int, "", ["hidetree"]);
            }

            emit("visible", { value: show });
        }

        /***** Lifecycle *****/

        plugin.on("load", function(){
            load();
        });
        plugin.on("enable", function(){

        });
        plugin.on("disable", function(){

        });
        plugin.on("unload", function(){
            loaded = false;
            drawn  = false;
        });

        function show() {
            updateVisibility(true);
        }

        function hide() {
            updateVisibility(false);
        }

        /***** Register and define API *****/

        /**
         * Displays the open files above the tree.
         *
         * @singleton
         */
        plugin.freezePublicAPI({
            /**
             * Show the openfiles panel
             */
            show: show,

            /**
             * Hide the openfiles panel
             */
            hide: hide,

            /**
             * Trigger a complete update of the openfiles view, only when the
             * openfiles panel is visible.
             */
            update: update,

            /**
             * Rerender the viewed part of the tree without having to recreate
             * the tree data.
             * Example usage: when the saving state or document content changed
             * Only applies when openfiles is visible
             */
            refresh: refresh
        });

        register(null, {
            openfiles: plugin
        });
    }
});
<|endoftext|>


<commit_before>var functions = require('firebase-functions')
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);

const genreSimScore = require('./genres');
const recentSongsSimScore = require('./recentTracks');
const topArtistsSimScore = require('./topArtists');
const topTrackSimScore = require('./topTracks');

exports.getMatches = functions.database.ref('/Users/{uid}/requestMatches')
  .onWrite(event=> {
    const userId = event.params.uid
    console.log('userId in getMatches', userId)

    return admin.database().ref('Users').once('value').then(snapshot => {

      const usersObj = snapshot.val()
      const userNames = Object.keys(usersObj).filter(name => name !== userId)

      const user1Obj = usersObj[userId]

      const user1TopArtists = user1Obj.topArtists.artists
      const user1TopTracks = user1Obj.topTracks.tracks
      const user1RecentSongs = user1Obj.recentSongs.songs

      userNames.forEach(user => {
        user2Obj = usersObj[user];

        let user2TopArtists
        let user2TopTracks
        let user2RecentSongs

        user2TopArtists =  (user2Obj.topArtists ? user2Obj.topArtists.artists : [])
        user2TopTracks =  (user2Obj.topTracks ? user2Obj.topTracks.tracks : [])
        user2RecentSongs =  (user2Obj.recentSongs ? user2Obj.recentSongs.songs : [])

        const genresScore = genreSimScore(user1TopArtists, user2TopArtists)
        const recentSongsScore = recentSongsSimScore(user1RecentSongs, user2RecentSongs)
        const artistsScore = topArtistsSimScore(user1TopArtists, user2TopArtists)
        const tracksScore = topTrackSimScore(user1TopTracks, user2TopTracks)

        const matchScore = (genresScore + recentSongsScore + artistsScore + tracksScore)/4

        console.log(`${user}'s match score: ${matchScore}`)
        console.log(`/Users/${userId}/matches/matchScores/${user}`)
        //masterscore
        admin.database().ref(`/Users/${userId}/matches/matchScores/${user}`).set(matchScore)
        admin.database().ref(`/Users/${user}/matches/matchScores/${userId}`).set(matchScore)
        //genrescore
        admin.database().ref(`/Users/${userId}/matches/genreScores/${user}`).set(genresScore)
        admin.database().ref(`/Users/${user}/matches/genreScores/${userId}`).set(genresScore)
        //recentSongsScore
        admin.database().ref(`/Users/${userId}/matches/recentSongsScores/${user}`).set(recentSongsScore)
        admin.database().ref(`/Users/${user}/matches/recentSongsScores/${userId}`).set(recentSongsScore)
        //artistscore
        admin.database().ref(`/Users/${userId}/matches/artistsScores/${user}`).set(artistsScore)
        admin.database().ref(`/Users/${user}/matches/artistsScores/${userId}`).set(artistsScore)
        //trackScore
        admin.database().ref(`/Users/${userId}/matches/tracksScores/${user}`).set(tracksScore)
        admin.database().ref(`/Users/${user}/matches/tracksScores/${userId}`).set(tracksScore)
      })

    }).catch(console.error)

  })
<commit_msg>delete: recent songs from algorithm<commit_after>var functions = require('firebase-functions')
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);

const genreSimScore = require('./genres');
const recentSongsSimScore = require('./recentTracks');
const topArtistsSimScore = require('./topArtists');
const topTrackSimScore = require('./topTracks');

exports.getMatches = functions.database.ref('/Users/{uid}/requestMatches')
  .onWrite(event=> {
    const userId = event.params.uid
    console.log('userId in getMatches', userId)

    return admin.database().ref('Users').once('value').then(snapshot => {

      const usersObj = snapshot.val()
      const userNames = Object.keys(usersObj).filter(name => name !== userId)

      const user1Obj = usersObj[userId]

      const user1TopArtists = user1Obj.topArtists.artists
      const user1TopTracks = user1Obj.topTracks.tracks
      // const user1RecentSongs = user1Obj.recentSongs.songs

      userNames.forEach(user => {
        user2Obj = usersObj[user];

        let user2TopArtists
        let user2TopTracks
        // let user2RecentSongs

        user2TopArtists =  (user2Obj.topArtists ? user2Obj.topArtists.artists : [])
        user2TopTracks =  (user2Obj.topTracks ? user2Obj.topTracks.tracks : [])
        // user2RecentSongs =  (user2Obj.recentSongs ? user2Obj.recentSongs.songs : [])

        const genresScore = genreSimScore(user1TopArtists, user2TopArtists)
        // const recentSongsScore = recentSongsSimScore(user1RecentSongs, user2RecentSongs)
        const artistsScore = topArtistsSimScore(user1TopArtists, user2TopArtists)
        const tracksScore = topTrackSimScore(user1TopTracks, user2TopTracks)

        const matchScore = (genresScore + artistsScore + tracksScore)/(3/2)

        console.log(`${user}'s match score: ${matchScore}`)
        console.log(`/Users/${userId}/matches/matchScores/${user}`)
        //masterscore
        admin.database().ref(`/Users/${userId}/matches/matchScores/${user}`).set(matchScore)
        admin.database().ref(`/Users/${user}/matches/matchScores/${userId}`).set(matchScore)
        //genrescore
        admin.database().ref(`/Users/${userId}/matches/genreScores/${user}`).set(genresScore)
        admin.database().ref(`/Users/${user}/matches/genreScores/${userId}`).set(genresScore)
        //recentSongsScore
        // admin.database().ref(`/Users/${userId}/matches/recentSongsScores/${user}`).set(recentSongsScore)
        // admin.database().ref(`/Users/${user}/matches/recentSongsScores/${userId}`).set(recentSongsScore)
        //artistscore
        admin.database().ref(`/Users/${userId}/matches/artistsScores/${user}`).set(artistsScore)
        admin.database().ref(`/Users/${user}/matches/artistsScores/${userId}`).set(artistsScore)
        //trackScore
        admin.database().ref(`/Users/${userId}/matches/tracksScores/${user}`).set(tracksScore)
        admin.database().ref(`/Users/${user}/matches/tracksScores/${userId}`).set(tracksScore)
      })

    }).catch(console.error)

  })
<|endoftext|>


<commit_before>"use strict"

const functions = require('firebase-functions')
const admin = require("firebase-admin")
const _ = require("ramda")

admin.initializeApp(functions.config().firebase);

const second = 1000
const minute = 60 * second
const tenMinutes = 10 * minute
const fiveMinutes = 5 * minute

exports.testPush = functions.https.onRequest((req, res) => {
    // admin.database().ref('/messages').push({original: original}).then(snapshot => {
    //     // Redirect with 303 SEE OTHER to the URL of the pushed object in the Firebase console.
    //     res.redirect(303, snapshot.ref);
    // });
    return admin.database().ref("/users").once("value")
                .then(sendTestPushToAllUsersWithRegistrationToken)
                .then(arr => res.json(arr))

});

function sendTestPushToAllUsersWithRegistrationToken(userMap) {
    const promiseList = []
    userMap.forEach(function (userEntry) {
        const userId = userEntry.key
        const userData = userEntry.val()
        if (userData.token) {
            const promise = admin
                .messaging()
                .sendToDevice(
                    userData.token,
                    {
                        data: {
                            todoId: "7aIPoEclCGfR6lPUXb71hGXdoETwthsaETqSK98Bne2qyw2uWJcTgKDj03lpPCDt",
                            uid: userId,
                            timestamp: Date.now() + ""
                        }
                    },
                    {timeToLive: tenMinutes, priority: "high"}
                )
            promiseList.push(promise)
        }
    })
    return Promise.all(promiseList)
}

exports.notificationCorn = functions.https.onRequest((req, res) => {
    return admin
        .database().ref("/notifications")
        .orderByChild("timestamp")
        .endAt(Date.now() + fiveMinutes)
        .once("value")
        .then(sendPushNotifications)
        .then(arr => {
            console.log(arr)
            return res.json(arr)
        })
})

function getUserClients(uid) {
    return admin.database().ref("/users/" + uid + "/clients").once("value")
                .then(getMapFromSnapshot)
                .then(_.values)
}

function getMapFromSnapshot(snapshot) {
    const map = {}
    snapshot.forEach(entry => {
        map[entry.key] = entry.val()
    })
    return map;
}

function sendPushNotifications(notificationMap) {
    const getUserClientsMemo = _.memoize(getUserClients)
    const promiseList = []
    notificationMap.forEach(notificationEntry => {
        const notificationData = notificationEntry.val()
        const uid = notificationData.uid
        const clients = getUserClientsMemo(uid)
        promiseList.push(clients.then(sendNotificationToClients(notificationData)))
    })
    return Promise.all(promiseList)
}

const sendNotificationToClients = notificationData => clients => {
    console.log("clients", clients)
    const {todoId, timestamp, uid} = notificationData

    function sendPushForClient(client) {
        if (client.connected) {
            console
                .log("ignoring push for connected client"
                    , notificationData
                    , client)

            return Promise.resolve({
                msg: "ignoring push for connected client", client, notificationData
            })
        }

        return admin
            .messaging()
            .sendToDevice(
                client.token,
                {data: {todoId, timestamp: "" + timestamp, uid}},
                {timeToLive: tenMinutes, priority: "high"}
            )
            .then(mdRes => {
                const tokenUnregistered = mdRes.results.find(mdResult => {
                    return mdResult.error && mdResult.error.code === "messaging/registration-token-not-registered"
                })
                if (tokenUnregistered) {
                    return deleteToken(uid, client.id)
                        .then(res => ({error: {mdRes, deleteTokenRes: res}}))
                }
                return mdRes
            })
    }

    return Promise.all(_.map(sendPushForClient, clients))
}

function deleteToken(uid, deviceId) {
    return admin.database().ref(`/users/${uid}/clients/${deviceId}/token`).set(null)
}

const sendNotification = data =>
    getUserClients(data.uid)
        .then(sendNotificationToClients(data))

// exports.onTodoUpdated =
//     functions
//         .database.ref("/users/{uid}/todo-db/{todoId}")
//         .onWrite(event => {
//             const oldTodo = event.data.previous.val();
//             const newTodo = event.data.val()
//             const uid = event.params["uid"]
//             const todoId = event.params["todoId"]
//
//             if (oldTodo && newTodo) {
//                 // todo was updated
//                 if (oldTodo.dueAt && newTodo.dueAt
//                     && oldTodo.dueAt === newTodo.dueAt
//                     && oldTodo.reminder && oldTodo.reminder.at
//                     && newTodo.reminder && newTodo.reminder.at
//                     && oldTodo.reminder.at !== newTodo.reminder.at
//                 ) {
//                     const timestamp = oldTodo.reminder.at
//                     //todo was snoozed
//                     const notificationData = {uid, timestamp, todoId}
//                     return getUserClients(uid)
//                         .then(sendNotificationToClients(notificationData))
//                 }
//
//
//             }
//
//         })

const createRef = path => admin.database().ref(path)

const write = _.curry((value, refOrPath) => {
    const ref = _.when(_.is(String), createRef)(refOrPath)
    console.log("write: ref = ", ref.toString(), "value = ", value)
    return ref.set(value)
})

const writeAt = _.flip(write)

const removeAt = write(null)

const notificationPath = (uid, todoId) => `/notifications/${uid}---${todoId}`

const conditionalPromise = (fn, bool) => bool ? fn() : Promise.resolve()

function createNotificationData(uid, todoId, timestamp) {
    return {uid, todoId, timestamp}
}

function addNotification(uid, todoId, newTimestamp, shouldSendPush) {
    const writeNotification = writeAt(notificationPath(uid, todoId))
    const notificationData = createNotificationData(uid, todoId, newTimestamp)
    return conditionalPromise(_ => sendNotification(notificationData), shouldSendPush)
        .then(_ => writeNotification(notificationData))
}

const hasChildChangedToIn = _.curry((snapshot, child, value) => {
        const childSS = snapshot.child(child)
        return childSS.changed() && childSS.val() === value
    }
)
const reminderAtPath = "schedule/reminderAt"
const dueAtPath = "schedule/dueAt"
const donePath = "done"
const deletedPath = "deleted"

const shouldDeleteNotification = (deltaSnapshot) => {
    const hasChildChangedTo = hasChildChangedToIn(deltaSnapshot)
    return hasChildChangedTo(donePath, true)
           || hasChildChangedTo(deletedPath, true)
           || hasChildChangedTo(reminderAtPath, null)
}

const hasTodoJustSnoozed = deltaSnapshot => {
    return !deltaSnapshot.child(dueAtPath).changed()
           && deltaSnapshot.previous.child(dueAtPath).exists()
           && deltaSnapshot.child(reminderAtPath).changed()
}

const shouldAddNotification = deltaSnapshot => {
    const hasChildChangedTo = hasChildChangedToIn(deltaSnapshot)
    const reminderAtSS = deltaSnapshot.child(reminderAtPath)
    return reminderAtSS.exists() &&
           (reminderAtSS.changed()
               || hasChildChangedTo(donePath, false)
               || hasChildChangedTo(deletedPath, false)
           )

}
exports.updateNotificationOnTodoChanged =
    functions
        .database.ref("/users/{uid}/todo-db/{todoId}")
        .onWrite(event => {
            const deltaSnapshot = event.data
            const uid = event.params["uid"]
            const todoId = event.params["todoId"]

            console.log(deltaSnapshot.current.val(), deltaSnapshot.previous.val())

            if (shouldDeleteNotification(deltaSnapshot)) {
                console.log("deleting notification")
                return removeAt(notificationPath(uid, todoId))
            }

            if (shouldAddNotification(deltaSnapshot)) {

                const shouldSendPush = hasTodoJustSnoozed(deltaSnapshot)
                console.log("adding notification: shouldSendPush", shouldSendPush)
                return addNotification(
                    uid, todoId,
                    deltaSnapshot.child(reminderAtPath).val(),
                    shouldSendPush
                )
            }
            console.log("No notification changes to process, returning")
        })
<commit_msg>* ff: change schedule model, so that it is easy to check if todo was just snoozed.<commit_after>"use strict"

const functions = require('firebase-functions')
const admin = require("firebase-admin")
const _ = require("ramda")

admin.initializeApp(functions.config().firebase);

const second = 1000
const minute = 60 * second
const tenMinutes = 10 * minute
const fiveMinutes = 5 * minute

exports.testPush = functions.https.onRequest((req, res) => {
    // admin.database().ref('/messages').push({original: original}).then(snapshot => {
    //     // Redirect with 303 SEE OTHER to the URL of the pushed object in the Firebase console.
    //     res.redirect(303, snapshot.ref);
    // });
    return admin.database().ref("/users").once("value")
                .then(sendTestPushToAllUsersWithRegistrationToken)
                .then(arr => res.json(arr))

});

function sendTestPushToAllUsersWithRegistrationToken(userMap) {
    const promiseList = []
    userMap.forEach(function (userEntry) {
        const userId = userEntry.key
        const userData = userEntry.val()
        if (userData.token) {
            const promise = admin
                .messaging()
                .sendToDevice(
                    userData.token,
                    {
                        data: {
                            todoId: "7aIPoEclCGfR6lPUXb71hGXdoETwthsaETqSK98Bne2qyw2uWJcTgKDj03lpPCDt",
                            uid: userId,
                            timestamp: Date.now() + ""
                        }
                    },
                    {timeToLive: tenMinutes, priority: "high"}
                )
            promiseList.push(promise)
        }
    })
    return Promise.all(promiseList)
}

exports.notificationCorn = functions.https.onRequest((req, res) => {
    return admin
        .database().ref("/notifications")
        .orderByChild("timestamp")
        .endAt(Date.now() + fiveMinutes)
        .once("value")
        .then(sendPushNotifications)
        .then(arr => {
            console.log(arr)
            return res.json(arr)
        })
})

function getUserClients(uid) {
    return admin.database().ref("/users/" + uid + "/clients").once("value")
                .then(getMapFromSnapshot)
                .then(_.values)
}

function getMapFromSnapshot(snapshot) {
    const map = {}
    snapshot.forEach(entry => {
        map[entry.key] = entry.val()
    })
    return map;
}

function sendPushNotifications(notificationMap) {
    const getUserClientsMemo = _.memoize(getUserClients)
    const promiseList = []
    notificationMap.forEach(notificationEntry => {
        const notificationData = notificationEntry.val()
        const uid = notificationData.uid
        const clients = getUserClientsMemo(uid)
        promiseList.push(clients.then(sendNotificationToClients(notificationData)))
    })
    return Promise.all(promiseList)
}

const sendNotificationToClients = notificationData => clients => {
    console.log("clients", clients)
    const {todoId, timestamp, uid} = notificationData

    function sendPushForClient(client) {
        if (client.connected) {
            console
                .log("ignoring push for connected client"
                    , notificationData
                    , client)

            return Promise.resolve({
                msg: "ignoring push for connected client", client, notificationData
            })
        }

        return admin
            .messaging()
            .sendToDevice(
                client.token,
                {data: {todoId, timestamp: "" + timestamp, uid}},
                {timeToLive: tenMinutes, priority: "high"}
            )
            .then(mdRes => {
                const tokenUnregistered = mdRes.results.find(mdResult => {
                    return mdResult.error && mdResult.error.code === "messaging/registration-token-not-registered"
                })
                if (tokenUnregistered) {
                    return deleteToken(uid, client.id)
                        .then(res => ({error: {mdRes, deleteTokenRes: res}}))
                }
                return mdRes
            })
    }

    return Promise.all(_.map(sendPushForClient, clients))
}

function deleteToken(uid, deviceId) {
    return admin.database().ref(`/users/${uid}/clients/${deviceId}/token`).set(null)
}

const sendNotification = data =>
    getUserClients(data.uid)
        .then(sendNotificationToClients(data))

const createRef = path => admin.database().ref(path)

const write = _.curry((value, refOrPath) => {
    const ref = _.when(_.is(String), createRef)(refOrPath)
    console.log("write: ref = ", ref.toString(), "value = ", value)
    return ref.set(value)
})

const writeAt = _.flip(write)

const removeAt = write(null)

const notificationPath = (uid, todoId) => `/notifications/${uid}---${todoId}`

const conditionalPromise = (fn, bool) => bool ? fn() : Promise.resolve()

function createNotificationData(uid, todoId, timestamp) {
    return {uid, todoId, timestamp}
}

function addNotification(uid, todoId, newTimestamp, shouldSendPush) {
    const writeNotification = writeAt(notificationPath(uid, todoId))
    const notificationData = createNotificationData(uid, todoId, newTimestamp)
    return conditionalPromise(_ => sendNotification(notificationData), shouldSendPush)
        .then(_ => writeNotification(notificationData))
}

const hasChildChangedToIn = _.curry((snapshot, child, value) => {
        const childSS = snapshot.child(child)
        return childSS.changed() && childSS.val() === value
    }
)
const reminderAtPath = "schedule/reminderAt"
const dueAtPath = "schedule/dueAt"
const donePath = "done"
const deletedPath = "deleted"

const shouldDeleteNotification = (deltaSnapshot) => {
    const hasChildChangedTo = hasChildChangedToIn(deltaSnapshot)
    return hasChildChangedTo(donePath, true)
           || hasChildChangedTo(deletedPath, true)
           || hasChildChangedTo(reminderAtPath, null)
}

const hasTodoJustSnoozed = deltaSnapshot => {
    return !deltaSnapshot.child(dueAtPath).changed()
           && deltaSnapshot.previous.child(dueAtPath).exists()
           && deltaSnapshot.child(reminderAtPath).changed()
}

const shouldAddNotification = deltaSnapshot => {
    const hasChildChangedTo = hasChildChangedToIn(deltaSnapshot)
    const reminderAtSS = deltaSnapshot.child(reminderAtPath)
    return reminderAtSS.exists() &&
           (reminderAtSS.changed()
               || hasChildChangedTo(donePath, false)
               || hasChildChangedTo(deletedPath, false)
           )

}
exports.updateNotificationOnTodoChanged =
    functions
        .database.ref("/users/{uid}/todo-db/{todoId}")
        .onWrite(event => {
            const deltaSnapshot = event.data
            const uid = event.params["uid"]
            const todoId = event.params["todoId"]

            console.log(deltaSnapshot.current.val(), deltaSnapshot.previous.val())

            if (shouldDeleteNotification(deltaSnapshot)) {
                console.log("deleting notification")
                return removeAt(notificationPath(uid, todoId))
            }

            if (shouldAddNotification(deltaSnapshot)) {

                const shouldSendPush = hasTodoJustSnoozed(deltaSnapshot)
                console.log("adding notification: shouldSendPush", shouldSendPush)
                return addNotification(
                    uid, todoId,
                    deltaSnapshot.child(reminderAtPath).val(),
                    shouldSendPush
                )
            }
            console.log("No notification changes to process, returning")
        })
<|endoftext|>


<commit_before>const functions = require('firebase-functions');
const nodemailer = require('nodemailer');

/*
*	Create and Deploy Cloud Functions
*	https://firebase.google.com/docs/functions/write-firebase-functions
*/

/*
*	basic usage example
*
* exports.helloWorld = functions.https.onRequest((request, response) => {
*		response.send('Hello from Firebase!');
*	});
*/

/*
*	load .env variables
*/
require('dotenv').load();

/*
* nodemailer usage notice:
* To use Gmail you may need to configure "Allow Less Secure Apps" (https://www.google.com/settings/security/lesssecureapps)
* in your Gmail account unless you are using 2FA
* in which case you would have to create an Application Specific password (https://security.google.com/settings/security/apppasswords).
* You may also need to unlock your account with "Allow access to your Google account" (https://accounts.google.com/DisplayUnlockCaptcha)
* to use SMTP.
*/
const smtpConfig = {
	host: process.env.MAILER_HOST,
	port: process.env.MAILER_PORT,
	secure: true, // use SSL
	auth: {
		type: 'OAuth2',
		user: process.env.MAILER_EMAIL,
		clientId: process.env.MAILER_CLIENT_ID,
		clientSecret: process.env.MAILER_CLIENT_SECRET,
		refreshToken: process.env.MAILER_REFRESH_TOKEN,
		accessToken: 'empty'
	}
};
/*
*	reusable transporter object using the default SMTP transport
*/
const mailTransporter = nodemailer.createTransport(smtpConfig);
mailTransporter.verify((err, success) => {
	if (err) {
		console.log('Mail transporter diag error >>', err);
	} else {
		console.log('Mail transporter diag success >>', success);
	}
});

function sendEmail(name, email, header, message, domain, res) {
	const mailOptions = {
		from: '"PORTFOLIO 👥" <' + process.env.MAILER_EMAIL +'>',
		to: process.env.MAILER_RECIPIENT_EMAIL,
		subject: `PORTFOLIO: ${header} ✔`,
		text: `${message}\n\nMessage was sent from domain: ${domain}`,
		html: `<h3>${header}</h3><p>${message}</p><br><br><p>Message was sent from domain: ${domain}</p>`
	};
	mailTransporter.sendMail(mailOptions, (err, info) => {
		if (err) {
			// console.log('Mail transporter error:', err);
			res.status(500).send('Mail transporter error');
		} else {
			// console.log('Message sent: ' + info.response);
			res.status(200).json({success: 'Your message was successfully sent'});
		}
	});
}

/*
* intercept calls to php/contact.php and process
*/
exports.sendEmail = functions.https.onRequest((req, res) => {
	if (req.method !== 'POST') {
		res.status(403).json({error: 'Forbidden method'});
	}
	const name = req.body.name || '';
	const email = req.body.email || '';
	const header = req.body.header || '';
	const message = req.body.message || '';
	const domain = req.body.domain || '';
	if (name.length >= 2 && /\w{2}@\w{2,}(\.)?\w{2,}/.test(email) && header.length >= 5 && message.length >= 75 && domain.length >= 4) {
		// res.status(200).json({'success': 'Your message was successfully sent.'});
		sendEmail(name, email, header, message, domain, res);
	} else {
		res.status(400).json({error: 'Missing mandatory request parameters'});
	}
});
<commit_msg>(fix) firebase cloud functions indes - removed junky comment<commit_after>const functions = require('firebase-functions');
const nodemailer = require('nodemailer');

/*
*	Create and Deploy Cloud Functions
*	https://firebase.google.com/docs/functions/write-firebase-functions
*/

/*
*	basic usage example
*
* exports.helloWorld = functions.https.onRequest((request, response) => {
*		response.send('Hello from Firebase!');
*	});
*/

/*
*	load .env variables
*/
require('dotenv').load();

/*
* nodemailer usage notice:
* To use Gmail you may need to configure "Allow Less Secure Apps" (https://www.google.com/settings/security/lesssecureapps)
* in your Gmail account unless you are using 2FA
* in which case you would have to create an Application Specific password (https://security.google.com/settings/security/apppasswords).
* You may also need to unlock your account with "Allow access to your Google account" (https://accounts.google.com/DisplayUnlockCaptcha)
* to use SMTP.
*/
const smtpConfig = {
	host: process.env.MAILER_HOST,
	port: process.env.MAILER_PORT,
	secure: true, // use SSL
	auth: {
		type: 'OAuth2',
		user: process.env.MAILER_EMAIL,
		clientId: process.env.MAILER_CLIENT_ID,
		clientSecret: process.env.MAILER_CLIENT_SECRET,
		refreshToken: process.env.MAILER_REFRESH_TOKEN,
		accessToken: 'empty'
	}
};
/*
*	reusable transporter object using the default SMTP transport
*/
const mailTransporter = nodemailer.createTransport(smtpConfig);
mailTransporter.verify((err, success) => {
	if (err) {
		console.log('Mail transporter diag error >>', err);
	} else {
		console.log('Mail transporter diag success >>', success);
	}
});

function sendEmail(name, email, header, message, domain, res) {
	const mailOptions = {
		from: '"PORTFOLIO 👥" <' + process.env.MAILER_EMAIL +'>',
		to: process.env.MAILER_RECIPIENT_EMAIL,
		subject: `PORTFOLIO: ${header} ✔`,
		text: `${message}\n\nMessage was sent from domain: ${domain}`,
		html: `<h3>${header}</h3><p>${message}</p><br><br><p>Message was sent from domain: ${domain}</p>`
	};
	mailTransporter.sendMail(mailOptions, (err, info) => {
		if (err) {
			// console.log('Mail transporter error:', err);
			res.status(500).send('Mail transporter error');
		} else {
			// console.log('Message sent: ' + info.response);
			res.status(200).json({success: 'Your message was successfully sent'});
		}
	});
}

exports.sendEmail = functions.https.onRequest((req, res) => {
	if (req.method !== 'POST') {
		res.status(403).json({error: 'Forbidden method'});
	}
	const name = req.body.name || '';
	const email = req.body.email || '';
	const header = req.body.header || '';
	const message = req.body.message || '';
	const domain = req.body.domain || '';
	if (name.length >= 2 && /\w{2}@\w{2,}(\.)?\w{2,}/.test(email) && header.length >= 5 && message.length >= 75 && domain.length >= 4) {
		// res.status(200).json({'success': 'Your message was successfully sent.'});
		sendEmail(name, email, header, message, domain, res);
	} else {
		res.status(400).json({error: 'Missing mandatory request parameters'});
	}
});
<|endoftext|>