<commit_before>// jest is a unit testing framework.
// 2011 (c) Ben Brooks Scholz. MIT Licensed.

(function (window) {

jest = function (moduleName) {
	window.previousModule = window.currentModule;
    window.currentModule = moduleName;
    window.results = [];
    window.failed = 0;
};

var Jest = {
    yes : function (given, message) {
        // cast to boolean
        var result = !!given;
        var testResult = {
            boolResult : result,
            msgResult : message
        };
        if (!result) window.failed += 1;
        window.results.push(testResult);
    },

    no : function (given, message) {
        // cast to boolean
        var result = !given;
        var testResult = {
            boolResult : result,
            msgResult : message
        };
        if (!result) window.failed += 1;
        window.results.push(testResult);
    },

    alike : function (given, expected, message) {
        var result = (given == expected);
        var testResult = {
            boolResult : result,
            msgResult : message
        };
        if (!result) window.failed += 1;
        window.results.push(testResult);
    },

    unlike : function (given, expected, message) {
        var result = (given != expected);
        var testResult = {
            boolResult : result,
            msgResult : message
        }
        if (!result) window.failed += 1;
        window.results.push(testResult);
    },

    same : function (given, expected, message) {
        var result = Jest.isEqual(given, expected);
        var testResult = {
            boolResult : result,
            msgResult : message
        };
        if (!result) window.failed += 1;
        window.results.push(testResult);
    },

    different : function (given, expected, message) {
        var result = !(Jest.isEqual(given, expected));
        var testResult = {
            boolResult : result,
            msgResult : message
        };
        if (!result) window.failed += 1;
        window.results.push(testResult);
    }
};

// extend an object (extendee) with the properties of another object (extender)
var extend = function (extendee, extender) {
    var property;
    for (property in extender)
        extendee[property] = extender[property];
    return extendee;
};

extend(window, Jest);

extend(Jest, {
    parseStats : function () {
        var statsHtml = [],
            bodyInit = '

Jest Test Suite

';
        statsHtml.push(bodyInit);
        statsHtml.push(getBrowserString());
        statsHtml.push('
');
        statsHtml.push('
');
        statsHtml.push((window.results.length - window.failed) + ' out of ' + window.results.length.toString());
        statsHtml.push(' tests passed.');
        statsHtml.push('

time: 86 milliseconds.

');
        statsHtml.push(currentModule);
        statsHtml.push('
');
        return statsHtml.join('');
    },

    parseTests : function (currentTests) {
        var result,
            resultsHtml = [];
        resultsHtml.push('

');
        resultsHtml.push(currentTests);
        resultsHtml.push('
');
        for (result = 0; result < window.results.length; result += 1) {
            resultsHtml.push('

');
            if (window.results[result].boolResult === true)
                resultsHtml.push('passed');
            else
                resultsHtml.push('failed');
            resultsHtml.push(' -- ');
            resultsHtml.push(window.results[result].msgResult);
            resultsHtml.push('

');
        }
        resultsHtml.push('
');
        return resultsHtml.join('');
    },

    isEqual : function (a, b) {
        var i,
            prop,
            aType = typeof a,
            bType = typeof b;

        if (a === b) return true;
        if (aType !== bType) return false;

        if (this.isArray(a) && this.isArray(b)) {
            if (a.length !== b.length) return false;

            for (i = 0; i < a.length; i+=1)
                if (a[i] !== b[i]) return false;

            return true;
        }

        if (aType !== 'object') return false;
        for (prop in a) {
            if (!b.hasOwnProperty(prop)) return false;
            if (a.hasOwnProperty(prop) && b.hasOwnProperty(prop))
                return this.isEqual(a[prop], b[prop]);
        }
    },

    isArray :  function (a) {
        if (a && typeof a === 'object' && a.constructor === Array)
            return true;
        else if (Object.prototype.toString.call(a) == '[object Array]')
            return true;
        else
            return false;
    }
});

test = function (testsName, expectedTests, tests) {
    var currentTests = testsName;
    var expectedTests = expectedTests;

    tests();
    document.body.innerHTML = Jest.parseStats();
    document.body.innerHTML += Jest.parseTests(currentTests);
};

var getBrowserString = function () {
    var browser,
        version = navigator.userAgent.split(' '),
        parseVersion = function (position) {
            return version[version.length-position].split('/')[1];
        };

    if (navigator.vendor === 'Apple Computer, Inc.') {
        version = parseVersion(2);
        browser = 'Safari';
    } else if (navigator.vendor === 'Google Inc.') {
        version = parseVersion(2);
        browser = 'Chrome';
    } else if (window.opera) {
        version = parseVersion(1);
        browser = 'Opera';
    } else if (version[1] === 'MSIE' ||
               version[2] === 'MSIE' ||
               version[3] === 'MSIE' ||
               version[4] === 'MSIE') {
        version = parseVersion(3);
        browser = 'Internet Explorer';
    } else if (navigator.product === 'Gecko') {
        version = parseVersion(1);
        browser = 'Firefox';
    }
    return browser + ' ' + version;
};

}(this));

<commit_msg>queue tests -- allows loading of scripts in the head<commit_after>// jest is a unit testing framework.
// 2011 (c) Ben Brooks Scholz. MIT Licensed.

(function (window) {

window.queue = {};

var Test = function (name, expected, callback) {
    this.name = name;
    this.expected = expected;
    this.actual = 0;
    this.callback = callback;
    this.tests = [];
};

Test.prototype = {
    setup : function () {

    },

    finish : function () {

    },

    queue : function () {
        var q = window.queue,
            module = window.module;

        if (Jest.isArray(window.queue[module]))
            window.queue[module].push(this.callback);
        else
            window.queue[module] = [];
    }
};

var Jest = {
    jest : function (moduleName) {
        window.module = moduleName;
    },

    test : function (testsName, expectedTests, tests) {
        var test = new Test(testsName, expectedTests, tests);
        test.queue();
    },

    yes : function (given, message) {
        var result = !!given;
        Jest.process(result, message);
    },

    no : function (given, message) {
        var result = !given;
        Jest.process(result, message);
    },

    alike : function (given, expected, message) {
        var result = !!(given == expected);
        Jest.process(result, message);
    },

    unlike : function (given, expected, message) {
        var result = !(given == expected);
        Jest.process(result, message);
    },

    same : function (given, expected, message) {
        var result = !!Jest.isEqual(given, expected);
        Jest.process(result, message);
    },

    different : function (given, expected, message) {
        var result = !Jest.isEqual(given, expected);
        Jest.process(result, message);
    }
};

expose(window, Jest);

expose(Jest, {
    load : function () {
        Jest.run();
    },

    process : function (result, message) {
        console.log(result.toString() + ' ' + message);
    },

    isEqual : function (a, b) {
        var i,
            prop,
            aType = typeof a,
            bType = typeof b;

        if (a === b)
            return true;

        if (aType !== bType)
            return false;

        if (Jest.isArray(a) && Jest.isArray(b)) {
            if (a.length !== b.length)
                return false;

            for (i = 0; i < a.length; i+=1) {
                if (a[i] !== b[i])
                    return false;
            }

            return true;
        }

        if (aType !== 'object')
            return false;

        for (prop in a) {
            if (a.hasOwnProperty(prop) && !b.hasOwnProperty(prop))
                return false;

            if (a.hasOwnProperty(prop) && b.hasOwnProperty(prop))
                return Jest.isEqual(a[prop], b[prop]);
        }

        return true;
    },

    isArray :  function (a) {
        if (a && typeof a === 'object' && a.constructor === Array)
            return true;
        else if (Object.prototype.toString.call(a) == '[object Array]')
            return true;
        else
            return false;
    },

    run : function () {
        for (var module in window.queue) {
            while (window.queue[module].length)
                window.queue[module].shift()();
        }
    }
});

addEvent(window, 'load', Jest.load);

function expose (exposee, exposer) {
    for (var property in exposer)
        exposee[property] = exposer[property];
    return exposee;
}

function addEvent (element, type, callback) {
    if (element.addEventListener)
        element.addEventListener(type, callback, false);
    else if (element.attachEvent)
        element.attachEvent('on' + type, callback);
}

function getBrowser () {
    var browser = '',
        version = navigator.userAgent.split(' '),
        parseVersion = function (position) {
            return version[version.length-position].split('/')[1];
        };

    if (navigator.vendor === 'Apple Computer, Inc.') {
        version = parseVersion(2);
        browser = 'Safari';
    } else if (navigator.vendor === 'Google Inc.') {
        version = parseVersion(2);
        browser = 'Chrome';
    } else if (window.opera) {
        version = parseVersion(1);
        browser = 'Opera';
    } else if (navigator.appName === "Microsoft Internet Explorer") {
        version = parseVersion(3);
        browser = 'Internet Explorer';
    } else if (navigator.product === 'Gecko') {
        version = parseVersion(1);
        browser = 'Firefox';
    }
    return browser + ' ' + version;
}

}(this));

<|endoftext|>


<commit_before>import React, { useMemo } from 'react';
import { Field, getIn } from 'formik';
import { Select as FMuiSelect } from 'formik-material-ui';
import MenuItem from '@material-ui/core/MenuItem';
import Checkbox from '@material-ui/core/Checkbox';
import ListItemText from '@material-ui/core/ListItemText';
import ListSubheader from '@material-ui/core/ListSubheader';
import InputLabel from '@material-ui/core/InputLabel';
import FormControl from '@material-ui/core/FormControl';
import PropTypes from 'prop-types';
import { useFormikContext } from 'formik';
import HelperText from './HelperText';

function ContextCheckbox({ value, field }) {
    const { values } = useFormikContext();
    const checked = (getIn(values, field) || []).some(val => val === value);
    return <Checkbox checked={checked} />;
}

ContextCheckbox.propTypes = {
    value: PropTypes.string,
    field: PropTypes.string
};

export default function Select({
    choices,
    label,
    required,
    placeholder,
    renderValue,
    ...rest
}) {
    const { name: fieldName, type, hint } = rest,
        { errors, touched } = useFormikContext(),
        showError = !!getIn(errors, fieldName) && getIn(touched, fieldName),
        multiple = type === 'select';

    if (multiple && !renderValue) {
        renderValue = sel => sel.map(getLabel).join(', ');
    }
    if (placeholder && !renderValue) {
        renderValue = sel => getLabel(sel) || sel || placeholder;
    }

    const getLabel = useMemo(() => {
        const labels = {};
        choices.forEach(({ name, label }) => {
            labels[name] = label;
        });
        return name => labels[name];
    }, [choices]);

    const choiceGroups = useMemo(() => {
        if (!choices.some(choice => choice.group)) {
            return choices;
        }
        let lastGroup,
            choiceGroups = [];
        choices.forEach(choice => {
            if (choice.group && choice.group != lastGroup) {
                choiceGroups.push({ name: '__group__', label: choice.group });
                lastGroup = choice.group;
            }
            choiceGroups.push(choice);
        });
        return choiceGroups;
    }, [choices]);

    return (
        <FormControl fullWidth margin="dense">
            <InputLabel
                htmlFor={fieldName}
                required={required}
                error={showError}
            >
                {label}
            </InputLabel>
            <Field
                component={FMuiSelect}
                multiple={multiple}
                required={required}
                renderValue={renderValue}
                displayEmpty={!!placeholder}
                {...rest}
            >
                {!multiple && (
                    <MenuItem value="" disabled={!!required}>
                        {required ? 'Select one...' : '(No Selection)'}
                    </MenuItem>
                )}
                {choiceGroups.map(({ name, label }) =>
                    name === '__group__' ? (
                        <ListSubheader>{label}</ListSubheader>
                    ) : (
                        <MenuItem key={name} value={name}>
                            {multiple && (
                                <ContextCheckbox
                                    value={name}
                                    field={fieldName}
                                />
                            )}
                            <ListItemText primary={label} />
                        </MenuItem>
                    )
                )}
            </Field>
            <HelperText name={fieldName} hint={hint} />
        </FormControl>
    );
}

Select.propTypes = {
    choices: PropTypes.arrayOf(PropTypes.object),
    label: PropTypes.string,
    placeholder: PropTypes.string,
    required: PropTypes.bool,
    renderValue: PropTypes.func
};
<commit_msg>support InputLabelProps in Select<commit_after>import React, { useMemo } from 'react';
import { Field, getIn } from 'formik';
import { Select as FMuiSelect } from 'formik-material-ui';
import MenuItem from '@material-ui/core/MenuItem';
import Checkbox from '@material-ui/core/Checkbox';
import ListItemText from '@material-ui/core/ListItemText';
import ListSubheader from '@material-ui/core/ListSubheader';
import InputLabel from '@material-ui/core/InputLabel';
import FormControl from '@material-ui/core/FormControl';
import PropTypes from 'prop-types';
import { useFormikContext } from 'formik';
import HelperText from './HelperText';

function ContextCheckbox({ value, field }) {
    const { values } = useFormikContext();
    const checked = (getIn(values, field) || []).some(val => val === value);
    return <Checkbox checked={checked} />;
}

ContextCheckbox.propTypes = {
    value: PropTypes.string,
    field: PropTypes.string
};

export default function Select({
    choices,
    label,
    required,
    placeholder,
    renderValue,
    InputLabelProps,
    ...rest
}) {
    const { name: fieldName, type, hint } = rest,
        { errors, touched } = useFormikContext(),
        showError = !!getIn(errors, fieldName) && getIn(touched, fieldName),
        multiple = type === 'select';

    if (multiple && !renderValue) {
        renderValue = sel => sel.map(getLabel).join(', ');
    }
    if (placeholder && !renderValue) {
        renderValue = sel => getLabel(sel) || sel || placeholder;
    }

    const getLabel = useMemo(() => {
        const labels = {};
        choices.forEach(({ name, label }) => {
            labels[name] = label;
        });
        return name => labels[name];
    }, [choices]);

    const choiceGroups = useMemo(() => {
        if (!choices.some(choice => choice.group)) {
            return choices;
        }
        let lastGroup,
            choiceGroups = [];
        choices.forEach(choice => {
            if (choice.group && choice.group != lastGroup) {
                choiceGroups.push({ name: '__group__', label: choice.group });
                lastGroup = choice.group;
            }
            choiceGroups.push(choice);
        });
        return choiceGroups;
    }, [choices]);

    return (
        <FormControl fullWidth margin="dense">
            <InputLabel
                htmlFor={fieldName}
                required={required}
                error={showError}
                {...InputLabelProps}
            >
                {label}
            </InputLabel>
            <Field
                component={FMuiSelect}
                multiple={multiple}
                required={required}
                renderValue={renderValue}
                displayEmpty={!!placeholder}
                {...rest}
            >
                {!multiple && (
                    <MenuItem value="" disabled={!!required}>
                        {required ? 'Select one...' : '(No Selection)'}
                    </MenuItem>
                )}
                {choiceGroups.map(({ name, label }) =>
                    name === '__group__' ? (
                        <ListSubheader>{label}</ListSubheader>
                    ) : (
                        <MenuItem key={name} value={name}>
                            {multiple && (
                                <ContextCheckbox
                                    value={name}
                                    field={fieldName}
                                />
                            )}
                            <ListItemText primary={label} />
                        </MenuItem>
                    )
                )}
            </Field>
            <HelperText name={fieldName} hint={hint} />
        </FormControl>
    );
}

Select.propTypes = {
    choices: PropTypes.arrayOf(PropTypes.object),
    label: PropTypes.string,
    placeholder: PropTypes.string,
    required: PropTypes.bool,
    renderValue: PropTypes.func,
    InputLabelProps: PropTypes.object
};
<|endoftext|>


<commit_before>/**
 * Created by AMore on 2015-04-24.
 */

'use strict';

angular.module('hopsWorksApp')
        .controller('DatasetsCtrl', ['$cookies', '$rootScope', '$modal', '$scope', '$q', '$mdSidenav', '$mdUtil', '$log',
          'DataSetService', '$routeParams', 'ModalService', 'growl', 'MetadataActionService', '$location', '$filter',
          'MetadataHelperService',
          function ($cookies, $rootScope, $modal, $scope, $q, $mdSidenav, $mdUtil, $log, DataSetService, $routeParams,
                  ModalService, growl, MetadataActionService, $location, $filter, MetadataHelperService) {

            var self = this;

            //Some variables to keep track of state.
            self.files = []; //A list of files currently displayed to the user.
            self.projectId = $routeParams.projectID; //The id of the project we're currently working in.
            self.pathArray; //An array containing all the path components of the current path. If empty: project root directory.
            self.selected; //The index of the selected file in the files array.
            self.fileDetail; //The details about the currently selected file.

            var dataSetService = DataSetService(self.projectId); //The datasetservice for the current project.
            var metaHelperService = MetadataHelperService();

            self.metadataView = {};
            self.availableTemplates = [];
            $scope.extendedFrom = {};

            self.extendedFromBoard = {};
            self.currentBoard = {};

            $scope.$watch('metaHelperService.availableTemplates', function (availableTemplates) {
              console.log("OTINANAI RE I MALAKIA 1 " + JSON.stringify(availableTemplates));
              if (!angular.isUndefined(availableTemplates)) {
                console.log("OTINANAI RE I MALAKIA " + JSON.stringify(availableTemplates));
              }
            });

            /*
             * Get all datasets under the current project.
             * @returns {undefined}
             */
            self.getAllDatasets = function () {
              //Get the path for an empty patharray: will get the datasets
              var path = getPath([]);
              dataSetService.getContents(path).then(
                      function (success) {
                        self.files = success.data;
                        self.pathArray = [];
                        console.log(success);
                      }, function (error) {
                console.log("Error getting all datasets in project " + self.projectId);
                console.log(error);
              });
            };

            /**
             * Get the contents of the directory at the path with the given path components and load it into the frontend.
             * @param {type} The array of path compontents to fetch. If empty, fetches the current path.
             * @returns {undefined}
             */
            var getDirContents = function (pathComponents) {
              //Construct the new path array
              var newPathArray;
              if (pathComponents) {
                newPathArray = pathComponents;
              } else {
                newPathArray = self.pathArray;
              }
              //Convert into a path
              var newPath = getPath(newPathArray);
              //Get the contents and load them
              dataSetService.getContents(newPath).then(
                      function (success) {
                        //Reset the selected file
                        self.selected = null;
                        self.fileDetail = null;
                        //Set the current files and path
                        self.files = success.data;
                        self.pathArray = newPathArray;
                        console.log(success);
                      }, function (error) {
                console.log("Error getting the contents of the path " + getPath(newPathArray));
                console.log(error);
              });
            };

            var init = function () {
              //Check if the current dataset is set
              if ($routeParams.datasetName) {
                //Dataset is set: get the contents
                self.pathArray = [$routeParams.datasetName];
              } else {
                //No current dataset is set: get all datasets.
                self.pathArray = [];
              }
              getDirContents();
            };

            init();

            /**
             * Download a file.
             * @param {type} file Can either be a filename or a path.
             * @returns {undefined}
             */
            var download = function (file) {
              dataSetService.download(file).then(
                      function (success) {
                        console.log("download success");
                        console.log(success);
                      }, function (error) {
                console.log("Error downloading file " + file);
                console.log(error);
              });
            };

            /**
             * Upload a file to the specified path.
             * @param {type} path
             * @returns {undefined}
             */
            var upload = function (path) {
              dataSetService.upload(path).then(
                      function (success) {
                        console.log("upload success");
                        console.log(success);
                        getDirContents();
                      }, function (error) {
                console.log("upload error");
                console.log(error);
              });
            };

            /**
             * Remove the inode at the given path. If called on a folder, will
             * remove the folder and all its contents recursively.
             * @param {type} path. The project-relative path to the inode to be removed.
             * @returns {undefined}
             */
            var removeInode = function (path) {
              dataSetService.removeDataSetDir(path).then(
                      function (success) {
                        growl.success(success.data.successMessage, {title: 'Success', ttl: 15000});
                        getDirContents();
                      }, function (error) {
                growl.error(error.data.errorMsg, {title: 'Error', ttl: 15000});
              });
            };

            /**
             * Open a modal dialog for folder creation. The folder is created at the current path.
             * @returns {undefined}
             */
            self.newDataSetModal = function () {
              ModalService.newFolder('md', getPath(self.pathArray)).then(
                      function (success) {
                        growl.success(success.data.successMessage, {title: 'Success', ttl: 15000});
                        getDirContents();
                      }, function (error) {
                //The user changed his/her mind. Don't really need to do anything.
                getDirContents();
              });
            };

            /**
             * Delete the file with the given name under the current path. If called on a folder, will remove the folder
             * and all its contents recursively.
             * @param {type} fileName
             * @returns {undefined}
             */
            self.deleteFile = function (fileName) {
              var removePathArray = self.pathArray.slice(0);
              removePathArray.push(fileName);
              removeInode(getPath(removePathArray));
            };

            /**
             * Opens a modal dialog for file upload.
             * @returns {undefined}
             */
            self.uploadFile = function () {
              var templateId = -1;

              ModalService.selectTemplate('sm', true, templateId).then(
                      function (success) {
                        templateId = success.templateId;
                        console.log("RETURNED TEMPLATE ID " + templateId);

                        ModalService.upload('lg', self.projectId, getPath(self.pathArray), templateId).then(
                                function (success) {
                                  growl.success(success.data.successMessage, {title: 'Success', ttl: 15000});
                                  getDirContents();
                                }, function (error) {
                          growl.info("Closed without saving.", {title: 'Info', ttl: 5000});
                          getDirContents();
                        });
                      });
            };

            /**
             * Opens a modal dialog for sharing.
             * @returns {undefined}
             */
            self.share = function (name) {
              ModalService.shareDataset('md', name).then(
                      function (success) {
                        growl.success(success.data.successMessage, {title: 'Success', ttl: 15000});
                      }, function (error) {
              });
            };

            /**
             * Upon click on a inode in the browser:
             *  + If folder: open folder, fetch contents from server and display.
             *  + If file: open a confirm dialog prompting for download.
             * @param {type} name
             * @param {type} isDir
             * @returns {undefined}
             */
            self.openDir = function (name, isDir) {
              if (isDir) {
                var newPathArray = self.pathArray.slice(0);
                newPathArray.push(name);
                getDirContents(newPathArray);
              } else {
                ModalService.confirm('sm', 'Confirm', 'Do you want to download this file?').then(
                        function (success) {
                          var downloadPathArray = self.pathArray.slice(0);
                          downloadPathArray.push(name);
                          download(getPath(downloadPathArray));
                        }
                );
              }
            };

            /**
             * Go up to parent directory.
             * @returns {undefined}
             */
            self.back = function () {
              var newPathArray = self.pathArray.slice(0);
              newPathArray.pop();
              if (newPathArray.length == 0) {
                $location.path('/project/' + self.projectId + '/datasets');
              } else {
                getDirContents(newPathArray);
              }
            };

            /**
             * Go to the folder at the index in the pathArray array.
             * @param {type} index
             * @returns {undefined}
             */
            self.goToFolder = function (index) {
              var newPathArray = self.pathArray.slice(0);
              newPathArray.splice(index + 1, newPathArray.length - index - 1);
              getDirContents(newPathArray);
            };

            /**
             * Select an inode; updates details panel.
             * @param {type} selectedIndex
             * @param {type} file
             * @returns {undefined}
             */
            self.select = function (selectedIndex, file) {
              self.selected = selectedIndex;
              self.fileDetail = file;
            };

            self.toggleLeft = buildToggler('left');
            self.toggleRight = buildToggler('right');

            function buildToggler(navID) {
              var debounceFn = $mdUtil.debounce(function () {
                $mdSidenav(navID).toggle()
                        .then(function () {
                          metaHelperService.getAvailableTemplates()
                                  .then(function (response) {
                                    self.availableTemplates = JSON.parse(response.board).templates;
                                  });
                        });
              }, 300);
              return debounceFn;
            };

            self.close = function () {
              $mdSidenav('right').close()
                      .then(function () {
                        $log.debug("Closed metadata designer");
                      });
            };

            self.selectChanged = function (extendFromThisID) {
              console.log('selectChanged - start: ' + extendFromThisID);

              MetadataActionService.fetchTemplate($cookies['email'], parseInt(extendFromThisID))
                      .then(function (success) {
                        console.log('Fetched data - success.board.column:');
                        self.extendedFromBoard = JSON.parse(success.board);
                        console.log(self.extendedFromBoard);

                        console.log('Fetched data - success:');
                        console.log(success);

                      }, function (error) {
                        console.log('Fetched data - error:');
                        console.log(error);
                      });
            };

          }]);

/**
 * Turn the array <i>pathArray</i> containing, path components, into a path string.
 * @param {type} pathArray
 * @returns {String}
 */
var getPath = function (pathArray) {
  return pathArray.join("/");
};
<commit_msg>removed trash<commit_after>/**
 * Created by AMore on 2015-04-24.
 */

'use strict';

angular.module('hopsWorksApp')
        .controller('DatasetsCtrl', ['$cookies', '$rootScope', '$modal', '$scope', '$q', '$mdSidenav', '$mdUtil', '$log',
          'DataSetService', '$routeParams', 'ModalService', 'growl', 'MetadataActionService', '$location', '$filter',
          'MetadataHelperService',
          function ($cookies, $rootScope, $modal, $scope, $q, $mdSidenav, $mdUtil, $log, DataSetService, $routeParams,
                  ModalService, growl, MetadataActionService, $location, $filter, MetadataHelperService) {

            var self = this;

            //Some variables to keep track of state.
            self.files = []; //A list of files currently displayed to the user.
            self.projectId = $routeParams.projectID; //The id of the project we're currently working in.
            self.pathArray; //An array containing all the path components of the current path. If empty: project root directory.
            self.selected; //The index of the selected file in the files array.
            self.fileDetail; //The details about the currently selected file.

            var dataSetService = DataSetService(self.projectId); //The datasetservice for the current project.
            var metaHelperService = MetadataHelperService();

            self.metadataView = {};
            self.availableTemplates = [];
            $scope.extendedFrom = {};

            self.extendedFromBoard = {};
            self.currentBoard = {};

            $scope.$watch('metaHelperService.availableTemplates', function (availableTemplates) {
              if (!angular.isUndefined(availableTemplates)) {
                console.log(JSON.stringify(availableTemplates));
              }
            });

            /*
             * Get all datasets under the current project.
             * @returns {undefined}
             */
            self.getAllDatasets = function () {
              //Get the path for an empty patharray: will get the datasets
              var path = getPath([]);
              dataSetService.getContents(path).then(
                      function (success) {
                        self.files = success.data;
                        self.pathArray = [];
                        console.log(success);
                      }, function (error) {
                console.log("Error getting all datasets in project " + self.projectId);
                console.log(error);
              });
            };

            /**
             * Get the contents of the directory at the path with the given path components and load it into the frontend.
             * @param {type} The array of path compontents to fetch. If empty, fetches the current path.
             * @returns {undefined}
             */
            var getDirContents = function (pathComponents) {
              //Construct the new path array
              var newPathArray;
              if (pathComponents) {
                newPathArray = pathComponents;
              } else {
                newPathArray = self.pathArray;
              }
              //Convert into a path
              var newPath = getPath(newPathArray);
              //Get the contents and load them
              dataSetService.getContents(newPath).then(
                      function (success) {
                        //Reset the selected file
                        self.selected = null;
                        self.fileDetail = null;
                        //Set the current files and path
                        self.files = success.data;
                        self.pathArray = newPathArray;
                        console.log(success);
                      }, function (error) {
                console.log("Error getting the contents of the path " + getPath(newPathArray));
                console.log(error);
              });
            };

            var init = function () {
              //Check if the current dataset is set
              if ($routeParams.datasetName) {
                //Dataset is set: get the contents
                self.pathArray = [$routeParams.datasetName];
              } else {
                //No current dataset is set: get all datasets.
                self.pathArray = [];
              }
              getDirContents();
            };

            init();

            /**
             * Download a file.
             * @param {type} file Can either be a filename or a path.
             * @returns {undefined}
             */
            var download = function (file) {
              dataSetService.download(file).then(
                      function (success) {
                        console.log("download success");
                        console.log(success);
                      }, function (error) {
                console.log("Error downloading file " + file);
                console.log(error);
              });
            };

            /**
             * Upload a file to the specified path.
             * @param {type} path
             * @returns {undefined}
             */
            var upload = function (path) {
              dataSetService.upload(path).then(
                      function (success) {
                        console.log("upload success");
                        console.log(success);
                        getDirContents();
                      }, function (error) {
                console.log("upload error");
                console.log(error);
              });
            };

            /**
             * Remove the inode at the given path. If called on a folder, will
             * remove the folder and all its contents recursively.
             * @param {type} path. The project-relative path to the inode to be removed.
             * @returns {undefined}
             */
            var removeInode = function (path) {
              dataSetService.removeDataSetDir(path).then(
                      function (success) {
                        growl.success(success.data.successMessage, {title: 'Success', ttl: 15000});
                        getDirContents();
                      }, function (error) {
                growl.error(error.data.errorMsg, {title: 'Error', ttl: 15000});
              });
            };

            /**
             * Open a modal dialog for folder creation. The folder is created at the current path.
             * @returns {undefined}
             */
            self.newDataSetModal = function () {
              ModalService.newFolder('md', getPath(self.pathArray)).then(
                      function (success) {
                        growl.success(success.data.successMessage, {title: 'Success', ttl: 15000});
                        getDirContents();
                      }, function (error) {
                //The user changed his/her mind. Don't really need to do anything.
                getDirContents();
              });
            };

            /**
             * Delete the file with the given name under the current path. If called on a folder, will remove the folder
             * and all its contents recursively.
             * @param {type} fileName
             * @returns {undefined}
             */
            self.deleteFile = function (fileName) {
              var removePathArray = self.pathArray.slice(0);
              removePathArray.push(fileName);
              removeInode(getPath(removePathArray));
            };

            /**
             * Opens a modal dialog for file upload.
             * @returns {undefined}
             */
            self.uploadFile = function () {
              var templateId = -1;

              ModalService.selectTemplate('sm', true, templateId).then(
                      function (success) {
                        templateId = success.templateId;
                        console.log("RETURNED TEMPLATE ID " + templateId);

                        ModalService.upload('lg', self.projectId, getPath(self.pathArray), templateId).then(
                                function (success) {
                                  growl.success(success.data.successMessage, {title: 'Success', ttl: 15000});
                                  getDirContents();
                                }, function (error) {
                          growl.info("Closed without saving.", {title: 'Info', ttl: 5000});
                          getDirContents();
                        });
                      });
            };

            /**
             * Opens a modal dialog for sharing.
             * @returns {undefined}
             */
            self.share = function (name) {
              ModalService.shareDataset('md', name).then(
                      function (success) {
                        growl.success(success.data.successMessage, {title: 'Success', ttl: 15000});
                      }, function (error) {
              });
            };

            /**
             * Upon click on a inode in the browser:
             *  + If folder: open folder, fetch contents from server and display.
             *  + If file: open a confirm dialog prompting for download.
             * @param {type} name
             * @param {type} isDir
             * @returns {undefined}
             */
            self.openDir = function (name, isDir) {
              if (isDir) {
                var newPathArray = self.pathArray.slice(0);
                newPathArray.push(name);
                getDirContents(newPathArray);
              } else {
                ModalService.confirm('sm', 'Confirm', 'Do you want to download this file?').then(
                        function (success) {
                          var downloadPathArray = self.pathArray.slice(0);
                          downloadPathArray.push(name);
                          download(getPath(downloadPathArray));
                        }
                );
              }
            };

            /**
             * Go up to parent directory.
             * @returns {undefined}
             */
            self.back = function () {
              var newPathArray = self.pathArray.slice(0);
              newPathArray.pop();
              if (newPathArray.length == 0) {
                $location.path('/project/' + self.projectId + '/datasets');
              } else {
                getDirContents(newPathArray);
              }
            };

            /**
             * Go to the folder at the index in the pathArray array.
             * @param {type} index
             * @returns {undefined}
             */
            self.goToFolder = function (index) {
              var newPathArray = self.pathArray.slice(0);
              newPathArray.splice(index + 1, newPathArray.length - index - 1);
              getDirContents(newPathArray);
            };

            /**
             * Select an inode; updates details panel.
             * @param {type} selectedIndex
             * @param {type} file
             * @returns {undefined}
             */
            self.select = function (selectedIndex, file) {
              self.selected = selectedIndex;
              self.fileDetail = file;
            };

            self.toggleLeft = buildToggler('left');
            self.toggleRight = buildToggler('right');

            function buildToggler(navID) {
              var debounceFn = $mdUtil.debounce(function () {
                $mdSidenav(navID).toggle()
                        .then(function () {
                          metaHelperService.getAvailableTemplates()
                                  .then(function (response) {
                                    self.availableTemplates = JSON.parse(response.board).templates;
                                  });
                        });
              }, 300);
              return debounceFn;
            };

            self.close = function () {
              $mdSidenav('right').close()
                      .then(function () {
                        $log.debug("Closed metadata designer");
                      });
            };

            self.selectChanged = function (extendFromThisID) {
              console.log('selectChanged - start: ' + extendFromThisID);

              MetadataActionService.fetchTemplate($cookies['email'], parseInt(extendFromThisID))
                      .then(function (success) {
                        console.log('Fetched data - success.board.column:');
                        self.extendedFromBoard = JSON.parse(success.board);
                        console.log(self.extendedFromBoard);

                        console.log('Fetched data - success:');
                        console.log(success);

                      }, function (error) {
                        console.log('Fetched data - error:');
                        console.log(error);
                      });
            };

          }]);

/**
 * Turn the array <i>pathArray</i> containing, path components, into a path string.
 * @param {type} pathArray
 * @returns {String}
 */
var getPath = function (pathArray) {
  return pathArray.join("/");
};
<|endoftext|>


<commit_before>IonicModule

/**
 * @ngdoc directive
 * @name ionSideMenus
 * @module ionic
 * @delegate ionic.service:$ionicSideMenuDelegate
 * @restrict E
 *
 * @description
 * A container element for side menu(s) and the main content. Allows the left
 * and/or right side menu to be toggled by dragging the main content area side
 * to side.
 *
 * To automatically close an opened menu you can add the {@link ionic.directive:menuClose}
 * attribute directive. Including the `menu-close` attribute is usually added to
 * links and buttons within `ion-side-menu` content, so that when the element is
 * clicked then the opened side menu will automatically close.
 *
 * ![Side Menu](http://ionicframework.com.s3.amazonaws.com/docs/controllers/sidemenu.gif)
 *
 * For more information on side menus, check out:
 *
 * - {@link ionic.directive:ionSideMenuContent}
 * - {@link ionic.directive:ionSideMenu}
 * - {@link ionic.directive:menuClose}
 *
 * @usage
 * To use side menus, add an `<ion-side-menus>` parent element,
 * an `<ion-side-menu-content>` for the center content,
 * and one or more `<ion-side-menu>` directives.
 *
 * ```html
 * <ion-side-menus>
 *   <!-- Center content -->
 *   <ion-side-menu-content ng-controller="ContentController">
 *   </ion-side-menu-content>
 *
 *   <!-- Left menu -->
 *   <ion-side-menu side="left">
 *   </ion-side-menu>
 *
 *   <!-- Right menu -->
 *   <ion-side-menu side="right">
 *   </ion-side-menu>
 * </ion-side-menus>
 * ```
 * ```js
 * function ContentController($scope, $ionicSideMenuDelegate) {
 *   $scope.toggleLeft = function() {
 *     $ionicSideMenuDelegate.toggleLeft();
 *   };
 * }
 * ```
 *
 * @param {string=} delegate-handle The handle used to identify this side menu
 * with {@link ionic.service:$ionicSideMenuDelegate}.
 *
 */
.directive('ionSideMenus', [function() {
  return {
    restrict: 'ECA',
    controller: '$ionicSideMenus',
    compile: function(element, attr) {
      attr.$set('class', (attr['class'] || '') + ' view');
    }
  };
}]);
<commit_msg>fix(sideMenu): remove .menu-open on destroy<commit_after>IonicModule

/**
 * @ngdoc directive
 * @name ionSideMenus
 * @module ionic
 * @delegate ionic.service:$ionicSideMenuDelegate
 * @restrict E
 *
 * @description
 * A container element for side menu(s) and the main content. Allows the left
 * and/or right side menu to be toggled by dragging the main content area side
 * to side.
 *
 * To automatically close an opened menu you can add the {@link ionic.directive:menuClose}
 * attribute directive. Including the `menu-close` attribute is usually added to
 * links and buttons within `ion-side-menu` content, so that when the element is
 * clicked then the opened side menu will automatically close.
 *
 * ![Side Menu](http://ionicframework.com.s3.amazonaws.com/docs/controllers/sidemenu.gif)
 *
 * For more information on side menus, check out:
 *
 * - {@link ionic.directive:ionSideMenuContent}
 * - {@link ionic.directive:ionSideMenu}
 * - {@link ionic.directive:menuClose}
 *
 * @usage
 * To use side menus, add an `<ion-side-menus>` parent element,
 * an `<ion-side-menu-content>` for the center content,
 * and one or more `<ion-side-menu>` directives.
 *
 * ```html
 * <ion-side-menus>
 *   <!-- Center content -->
 *   <ion-side-menu-content ng-controller="ContentController">
 *   </ion-side-menu-content>
 *
 *   <!-- Left menu -->
 *   <ion-side-menu side="left">
 *   </ion-side-menu>
 *
 *   <!-- Right menu -->
 *   <ion-side-menu side="right">
 *   </ion-side-menu>
 * </ion-side-menus>
 * ```
 * ```js
 * function ContentController($scope, $ionicSideMenuDelegate) {
 *   $scope.toggleLeft = function() {
 *     $ionicSideMenuDelegate.toggleLeft();
 *   };
 * }
 * ```
 *
 * @param {string=} delegate-handle The handle used to identify this side menu
 * with {@link ionic.service:$ionicSideMenuDelegate}.
 *
 */
.directive('ionSideMenus', ['$document', function($document) {
  return {
    restrict: 'ECA',
    controller: '$ionicSideMenus',
    compile: function(element, attr) {
      attr.$set('class', (attr['class'] || '') + ' view');

      return function($scope) {
        $scope.$on('$destroy', function(){
          $document[0].body.classList.remove('menu-open');
        });

      }
    }
  };
}]);
<|endoftext|>


<commit_before>/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 * @format
 */

'use strict';

const RelayConnectionInterface = require('./RelayConnectionInterface');

const generateRelayClientID = require('../../store/generateRelayClientID');
const getRelayHandleKey = require('../../util/getRelayHandleKey');
const invariant = require('invariant');
const warning = require('warning');

import type {
  HandleFieldPayload,
  RecordProxy,
  RecordSourceProxy,
} from '../../store/RelayStoreTypes';
import type {DataID, Variables} from '../../util/RelayRuntimeTypes';

export type ConnectionMetadata = {
  path: ?Array<string>,
  direction: ?('forward' | 'backward' | 'bidirectional'),
  cursor: ?string,
  count: ?string,
};

const CONNECTION = 'connection';

// Per-instance incrementing index used to generate unique edge IDs
const NEXT_EDGE_INDEX = '__connection_next_edge_index';

/**
 * @public
 *
 * A default runtime handler for connection fields that appends newly fetched
 * edges onto the end of a connection, regardless of the arguments used to fetch
 * those edges.
 */
function update(store: RecordSourceProxy, payload: HandleFieldPayload): void {
  const record = store.get(payload.dataID);
  if (!record) {
    return;
  }

  const {
    EDGES,
    END_CURSOR,
    HAS_NEXT_PAGE,
    HAS_PREV_PAGE,
    PAGE_INFO,
    PAGE_INFO_TYPE,
    START_CURSOR,
  } = RelayConnectionInterface.get();

  const serverConnection = record.getLinkedRecord(payload.fieldKey);
  const serverPageInfo =
    serverConnection && serverConnection.getLinkedRecord(PAGE_INFO);
  if (!serverConnection) {
    record.setValue(null, payload.handleKey);
    return;
  }
  const clientConnection = record.getLinkedRecord(payload.handleKey);
  let clientPageInfo =
    clientConnection && clientConnection.getLinkedRecord(PAGE_INFO);
  if (!clientConnection) {
    // Initial fetch with data: copy fields from the server record
    const connection = store.create(
      generateRelayClientID(record.getDataID(), payload.handleKey),
      serverConnection.getType(),
    );
    connection.setValue(0, NEXT_EDGE_INDEX);
    connection.copyFieldsFrom(serverConnection);
    let serverEdges = serverConnection.getLinkedRecords(EDGES);
    if (serverEdges) {
      serverEdges = serverEdges.map(edge =>
        buildConnectionEdge(store, connection, edge),
      );
      connection.setLinkedRecords(serverEdges, EDGES);
    }
    record.setLinkedRecord(connection, payload.handleKey);

    clientPageInfo = store.create(
      generateRelayClientID(connection.getDataID(), PAGE_INFO),
      PAGE_INFO_TYPE,
    );
    clientPageInfo.setValue(false, HAS_NEXT_PAGE);
    clientPageInfo.setValue(false, HAS_PREV_PAGE);
    clientPageInfo.setValue(null, END_CURSOR);
    clientPageInfo.setValue(null, START_CURSOR);
    if (serverPageInfo) {
      clientPageInfo.copyFieldsFrom(serverPageInfo);
    }
    connection.setLinkedRecord(clientPageInfo, PAGE_INFO);
  } else {
    const connection = clientConnection;
    // Subsequent fetches:
    // - updated fields on the connection
    // - merge prev/next edges, de-duplicating by node id
    // - synthesize page info fields
    let serverEdges = serverConnection.getLinkedRecords(EDGES);
    if (serverEdges) {
      serverEdges = serverEdges.map(edge =>
        buildConnectionEdge(store, connection, edge),
      );
    }
    const prevEdges = connection.getLinkedRecords(EDGES);
    const prevPageInfo = connection.getLinkedRecord(PAGE_INFO);
    connection.copyFieldsFrom(serverConnection);
    // Reset EDGES and PAGE_INFO fields
    if (prevEdges) {
      connection.setLinkedRecords(prevEdges, EDGES);
    }
    if (prevPageInfo) {
      connection.setLinkedRecord(prevPageInfo, PAGE_INFO);
    }

    let nextEdges = [];
    const args = payload.args;
    if (prevEdges && serverEdges) {
      if (args.after != null) {
        // Forward pagination from the end of the connection: append edges
        if (
          clientPageInfo &&
          args.after === clientPageInfo.getValue(END_CURSOR)
        ) {
          const nodeIDs = new Set();
          mergeEdges(prevEdges, nextEdges, nodeIDs);
          mergeEdges(serverEdges, nextEdges, nodeIDs);
        } else {
          warning(
            false,
            'RelayConnectionHandler: Unexpected after cursor `%s`, edges must ' +
              'be fetched from the end of the list (`%s`).',
            args.after,
            clientPageInfo && clientPageInfo.getValue(END_CURSOR),
          );
          return;
        }
      } else if (args.before != null) {
        // Backward pagination from the start of the connection: prepend edges
        if (
          clientPageInfo &&
          args.before === clientPageInfo.getValue(START_CURSOR)
        ) {
          const nodeIDs = new Set();
          mergeEdges(serverEdges, nextEdges, nodeIDs);
          mergeEdges(prevEdges, nextEdges, nodeIDs);
        } else {
          warning(
            false,
            'RelayConnectionHandler: Unexpected before cursor `%s`, edges must ' +
              'be fetched from the beginning of the list (`%s`).',
            args.before,
            clientPageInfo && clientPageInfo.getValue(START_CURSOR),
          );
          return;
        }
      } else {
        // The connection was refetched from the beginning/end: replace edges
        nextEdges = serverEdges;
      }
    } else if (serverEdges) {
      nextEdges = serverEdges;
    } else {
      nextEdges = prevEdges;
    }
    // Update edges only if they were updated, the null check is
    // for Flow (prevEdges could be null).
    if (nextEdges != null && nextEdges !== prevEdges) {
      connection.setLinkedRecords(nextEdges, EDGES);
    }
    // Page info should be updated even if no new edge were returned.
    if (clientPageInfo && serverPageInfo) {
      if (args.after == null && args.before == null) {
        // The connection was refetched from the beginning/end: replace
        // page_info
        clientPageInfo.copyFieldsFrom(serverPageInfo);
      } else if (args.before != null || (args.after == null && args.last)) {
        clientPageInfo.setValue(
          !!serverPageInfo.getValue(HAS_PREV_PAGE),
          HAS_PREV_PAGE,
        );
        const startCursor = serverPageInfo.getValue(START_CURSOR);
        if (typeof startCursor === 'string') {
          clientPageInfo.setValue(startCursor, START_CURSOR);
        }
      } else if (args.after != null || (args.before == null && args.first)) {
        clientPageInfo.setValue(
          !!serverPageInfo.getValue(HAS_NEXT_PAGE),
          HAS_NEXT_PAGE,
        );
        const endCursor = serverPageInfo.getValue(END_CURSOR);
        if (typeof endCursor === 'string') {
          clientPageInfo.setValue(endCursor, END_CURSOR);
        }
      }
    }
  }
}

/**
 * @public
 *
 * Given a record and the name of the schema field for which a connection was
 * fetched, returns the linked connection record.
 *
 * Example:
 *
 * Given that data has already been fetched on some user `<id>` on the `friends`
 * field:
 *
 * ```
 * fragment FriendsFragment on User {
 *   friends(first: 10) @connection(key: "FriendsFragment_friends") {
 *    edges {
 *      node {
 *        id
 *        }
 *      }
 *   }
 * }
 * ```
 *
 * The `friends` connection record can be accessed with:
 *
 * ```
 * store => {
 *   const user = store.get('<id>');
 *   const friends = RelayConnectionHandler.getConnection(user, 'FriendsFragment_friends');
 *   // Access fields on the connection:
 *   const edges = friends.getLinkedRecords('edges');
 * }
 * ```
 *
 * TODO: t15733312
 * Currently we haven't run into this case yet, but we need to add a `getConnections`
 * that returns an array of the connections under the same `key` regardless of the variables.
 */
function getConnection(
  record: RecordProxy,
  key: string,
  filters?: ?Variables,
): ?RecordProxy {
  const handleKey = getRelayHandleKey(CONNECTION, key, null);
  return record.getLinkedRecord(handleKey, filters);
}

/**
 * @public
 *
 * Inserts an edge after the given cursor, or at the end of the list if no
 * cursor is provided.
 *
 * Example:
 *
 * Given that data has already been fetched on some user `<id>` on the `friends`
 * field:
 *
 * ```
 * fragment FriendsFragment on User {
 *   friends(first: 10) @connection(key: "FriendsFragment_friends") {
 *    edges {
 *      node {
 *        id
 *        }
 *      }
 *   }
 * }
 * ```
 *
 * An edge can be appended with:
 *
 * ```
 * store => {
 *   const user = store.get('<id>');
 *   const friends = RelayConnectionHandler.getConnection(user, 'FriendsFragment_friends');
 *   const edge = store.create('<edge-id>', 'FriendsEdge');
 *   RelayConnectionHandler.insertEdgeAfter(friends, edge);
 * }
 * ```
 */
function insertEdgeAfter(
  record: RecordProxy,
  newEdge: RecordProxy,
  cursor?: ?string,
): void {
  const {CURSOR, EDGES} = RelayConnectionInterface.get();

  const edges = record.getLinkedRecords(EDGES);
  if (!edges) {
    record.setLinkedRecords([newEdge], EDGES);
    return;
  }
  let nextEdges;
  if (cursor == null) {
    nextEdges = edges.concat(newEdge);
  } else {
    nextEdges = [];
    let foundCursor = false;
    for (let ii = 0; ii < edges.length; ii++) {
      const edge = edges[ii];
      nextEdges.push(edge);
      if (edge == null) {
        continue;
      }
      const edgeCursor = edge.getValue(CURSOR);
      if (cursor === edgeCursor) {
        nextEdges.push(newEdge);
        foundCursor = true;
      }
    }
    if (!foundCursor) {
      nextEdges.push(newEdge);
    }
  }
  record.setLinkedRecords(nextEdges, EDGES);
}

/**
 * @public
 *
 * Creates an edge for a connection record, given a node and edge type.
 */
function createEdge(
  store: RecordSourceProxy,
  record: RecordProxy,
  node: RecordProxy,
  edgeType: string,
): RecordProxy {
  const {NODE} = RelayConnectionInterface.get();

  // An index-based client ID could easily conflict (unless it was
  // auto-incrementing, but there is nowhere to the store the id)
  // Instead, construct a client ID based on the connection ID and node ID,
  // which will only conflict if the same node is added to the same connection
  // twice. This is acceptable since the `insertEdge*` functions ignore
  // duplicates.
  const edgeID = generateRelayClientID(record.getDataID(), node.getDataID());
  let edge = store.get(edgeID);
  if (!edge) {
    edge = store.create(edgeID, edgeType);
  }
  edge.setLinkedRecord(node, NODE);
  return edge;
}

/**
 * @public
 *
 * Inserts an edge before the given cursor, or at the beginning of the list if
 * no cursor is provided.
 *
 * Example:
 *
 * Given that data has already been fetched on some user `<id>` on the `friends`
 * field:
 *
 * ```
 * fragment FriendsFragment on User {
 *   friends(first: 10) @connection(key: "FriendsFragment_friends") {
 *    edges {
 *      node {
 *        id
 *        }
 *      }
 *   }
 * }
 * ```
 *
 * An edge can be prepended with:
 *
 * ```
 * store => {
 *   const user = store.get('<id>');
 *   const friends = RelayConnectionHandler.getConnection(user, 'FriendsFragment_friends');
 *   const edge = store.create('<edge-id>', 'FriendsEdge');
 *   RelayConnectionHandler.insertEdgeBefore(friends, edge);
 * }
 * ```
 */
function insertEdgeBefore(
  record: RecordProxy,
  newEdge: RecordProxy,
  cursor?: ?string,
): void {
  const {CURSOR, EDGES} = RelayConnectionInterface.get();

  const edges = record.getLinkedRecords(EDGES);
  if (!edges) {
    record.setLinkedRecords([newEdge], EDGES);
    return;
  }
  let nextEdges;
  if (cursor == null) {
    nextEdges = [newEdge].concat(edges);
  } else {
    nextEdges = [];
    let foundCursor = false;
    for (let ii = 0; ii < edges.length; ii++) {
      const edge = edges[ii];
      if (edge != null) {
        const edgeCursor = edge.getValue(CURSOR);
        if (cursor === edgeCursor) {
          nextEdges.push(newEdge);
          foundCursor = true;
        }
      }
      nextEdges.push(edge);
    }
    if (!foundCursor) {
      nextEdges.unshift(newEdge);
    }
  }
  record.setLinkedRecords(nextEdges, EDGES);
}

/**
 * @public
 *
 * Remove any edges whose `node.id` matches the given id.
 */
function deleteNode(record: RecordProxy, nodeID: DataID): void {
  const {EDGES, NODE} = RelayConnectionInterface.get();

  const edges = record.getLinkedRecords(EDGES);
  if (!edges) {
    return;
  }
  let nextEdges;
  for (let ii = 0; ii < edges.length; ii++) {
    const edge = edges[ii];
    const node = edge && edge.getLinkedRecord(NODE);
    if (node != null && node.getDataID() === nodeID) {
      if (nextEdges === undefined) {
        nextEdges = edges.slice(0, ii);
      }
    } else if (nextEdges !== undefined) {
      nextEdges.push(edge);
    }
  }
  if (nextEdges !== undefined) {
    record.setLinkedRecords(nextEdges, EDGES);
  }
}

/**
 * @internal
 *
 * Creates a copy of an edge with a unique ID based on per-connection-instance
 * incrementing edge index. This is necessary to avoid collisions between edges,
 * which can occur because (edge) client IDs are assigned deterministically
 * based on the path from the nearest node with an id.
 *
 * Example: if the first N edges of the same connection are refetched, the edges
 * from the second fetch will be assigned the same IDs as the first fetch, even
 * though the nodes they point to may be different (or the same and in different
 * order).
 */
function buildConnectionEdge(
  store: RecordSourceProxy,
  connection: RecordProxy,
  edge: ?RecordProxy,
): ?RecordProxy {
  if (edge == null) {
    return edge;
  }
  const {EDGES} = RelayConnectionInterface.get();

  const edgeIndex = connection.getValue(NEXT_EDGE_INDEX);
  invariant(
    typeof edgeIndex === 'number',
    'RelayConnectionHandler: Expected %s to be a number, got `%s`.',
    NEXT_EDGE_INDEX,
    edgeIndex,
  );
  const edgeID = generateRelayClientID(
    connection.getDataID(),
    EDGES,
    edgeIndex,
  );
  const connectionEdge = store.create(edgeID, edge.getType());
  connectionEdge.copyFieldsFrom(edge);
  connection.setValue(edgeIndex + 1, NEXT_EDGE_INDEX);
  return connectionEdge;
}

/**
 * @internal
 *
 * Adds the source edges to the target edges, skipping edges with
 * duplicate node ids.
 */
function mergeEdges(
  sourceEdges: Array<?RecordProxy>,
  targetEdges: Array<?RecordProxy>,
  nodeIDs: Set<mixed>,
): void {
  const {NODE} = RelayConnectionInterface.get();

  for (let ii = 0; ii < sourceEdges.length; ii++) {
    const edge = sourceEdges[ii];
    if (!edge) {
      continue;
    }
    const node = edge.getLinkedRecord(NODE);
    const nodeID = node && node.getValue('id');
    if (nodeID) {
      if (nodeIDs.has(nodeID)) {
        continue;
      }
      nodeIDs.add(nodeID);
    }
    targetEdges.push(edge);
  }
}

module.exports = {
  buildConnectionEdge,
  createEdge,
  deleteNode,
  getConnection,
  insertEdgeAfter,
  insertEdgeBefore,
  update,
};
<commit_msg>RelayConnectionHandler.getConnection should take a ReadOnlyRecordProxy<commit_after>/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 * @format
 */

'use strict';

const RelayConnectionInterface = require('./RelayConnectionInterface');

const generateRelayClientID = require('../../store/generateRelayClientID');
const getRelayHandleKey = require('../../util/getRelayHandleKey');
const invariant = require('invariant');
const warning = require('warning');

import type {
  HandleFieldPayload,
  RecordProxy,
  ReadOnlyRecordProxy,
  RecordSourceProxy,
} from '../../store/RelayStoreTypes';
import type {DataID, Variables} from '../../util/RelayRuntimeTypes';

export type ConnectionMetadata = {
  path: ?Array<string>,
  direction: ?('forward' | 'backward' | 'bidirectional'),
  cursor: ?string,
  count: ?string,
};

const CONNECTION = 'connection';

// Per-instance incrementing index used to generate unique edge IDs
const NEXT_EDGE_INDEX = '__connection_next_edge_index';

/**
 * @public
 *
 * A default runtime handler for connection fields that appends newly fetched
 * edges onto the end of a connection, regardless of the arguments used to fetch
 * those edges.
 */
function update(store: RecordSourceProxy, payload: HandleFieldPayload): void {
  const record = store.get(payload.dataID);
  if (!record) {
    return;
  }

  const {
    EDGES,
    END_CURSOR,
    HAS_NEXT_PAGE,
    HAS_PREV_PAGE,
    PAGE_INFO,
    PAGE_INFO_TYPE,
    START_CURSOR,
  } = RelayConnectionInterface.get();

  const serverConnection = record.getLinkedRecord(payload.fieldKey);
  const serverPageInfo =
    serverConnection && serverConnection.getLinkedRecord(PAGE_INFO);
  if (!serverConnection) {
    record.setValue(null, payload.handleKey);
    return;
  }
  const clientConnection = record.getLinkedRecord(payload.handleKey);
  let clientPageInfo =
    clientConnection && clientConnection.getLinkedRecord(PAGE_INFO);
  if (!clientConnection) {
    // Initial fetch with data: copy fields from the server record
    const connection = store.create(
      generateRelayClientID(record.getDataID(), payload.handleKey),
      serverConnection.getType(),
    );
    connection.setValue(0, NEXT_EDGE_INDEX);
    connection.copyFieldsFrom(serverConnection);
    let serverEdges = serverConnection.getLinkedRecords(EDGES);
    if (serverEdges) {
      serverEdges = serverEdges.map(edge =>
        buildConnectionEdge(store, connection, edge),
      );
      connection.setLinkedRecords(serverEdges, EDGES);
    }
    record.setLinkedRecord(connection, payload.handleKey);

    clientPageInfo = store.create(
      generateRelayClientID(connection.getDataID(), PAGE_INFO),
      PAGE_INFO_TYPE,
    );
    clientPageInfo.setValue(false, HAS_NEXT_PAGE);
    clientPageInfo.setValue(false, HAS_PREV_PAGE);
    clientPageInfo.setValue(null, END_CURSOR);
    clientPageInfo.setValue(null, START_CURSOR);
    if (serverPageInfo) {
      clientPageInfo.copyFieldsFrom(serverPageInfo);
    }
    connection.setLinkedRecord(clientPageInfo, PAGE_INFO);
  } else {
    const connection = clientConnection;
    // Subsequent fetches:
    // - updated fields on the connection
    // - merge prev/next edges, de-duplicating by node id
    // - synthesize page info fields
    let serverEdges = serverConnection.getLinkedRecords(EDGES);
    if (serverEdges) {
      serverEdges = serverEdges.map(edge =>
        buildConnectionEdge(store, connection, edge),
      );
    }
    const prevEdges = connection.getLinkedRecords(EDGES);
    const prevPageInfo = connection.getLinkedRecord(PAGE_INFO);
    connection.copyFieldsFrom(serverConnection);
    // Reset EDGES and PAGE_INFO fields
    if (prevEdges) {
      connection.setLinkedRecords(prevEdges, EDGES);
    }
    if (prevPageInfo) {
      connection.setLinkedRecord(prevPageInfo, PAGE_INFO);
    }

    let nextEdges = [];
    const args = payload.args;
    if (prevEdges && serverEdges) {
      if (args.after != null) {
        // Forward pagination from the end of the connection: append edges
        if (
          clientPageInfo &&
          args.after === clientPageInfo.getValue(END_CURSOR)
        ) {
          const nodeIDs = new Set();
          mergeEdges(prevEdges, nextEdges, nodeIDs);
          mergeEdges(serverEdges, nextEdges, nodeIDs);
        } else {
          warning(
            false,
            'RelayConnectionHandler: Unexpected after cursor `%s`, edges must ' +
              'be fetched from the end of the list (`%s`).',
            args.after,
            clientPageInfo && clientPageInfo.getValue(END_CURSOR),
          );
          return;
        }
      } else if (args.before != null) {
        // Backward pagination from the start of the connection: prepend edges
        if (
          clientPageInfo &&
          args.before === clientPageInfo.getValue(START_CURSOR)
        ) {
          const nodeIDs = new Set();
          mergeEdges(serverEdges, nextEdges, nodeIDs);
          mergeEdges(prevEdges, nextEdges, nodeIDs);
        } else {
          warning(
            false,
            'RelayConnectionHandler: Unexpected before cursor `%s`, edges must ' +
              'be fetched from the beginning of the list (`%s`).',
            args.before,
            clientPageInfo && clientPageInfo.getValue(START_CURSOR),
          );
          return;
        }
      } else {
        // The connection was refetched from the beginning/end: replace edges
        nextEdges = serverEdges;
      }
    } else if (serverEdges) {
      nextEdges = serverEdges;
    } else {
      nextEdges = prevEdges;
    }
    // Update edges only if they were updated, the null check is
    // for Flow (prevEdges could be null).
    if (nextEdges != null && nextEdges !== prevEdges) {
      connection.setLinkedRecords(nextEdges, EDGES);
    }
    // Page info should be updated even if no new edge were returned.
    if (clientPageInfo && serverPageInfo) {
      if (args.after == null && args.before == null) {
        // The connection was refetched from the beginning/end: replace
        // page_info
        clientPageInfo.copyFieldsFrom(serverPageInfo);
      } else if (args.before != null || (args.after == null && args.last)) {
        clientPageInfo.setValue(
          !!serverPageInfo.getValue(HAS_PREV_PAGE),
          HAS_PREV_PAGE,
        );
        const startCursor = serverPageInfo.getValue(START_CURSOR);
        if (typeof startCursor === 'string') {
          clientPageInfo.setValue(startCursor, START_CURSOR);
        }
      } else if (args.after != null || (args.before == null && args.first)) {
        clientPageInfo.setValue(
          !!serverPageInfo.getValue(HAS_NEXT_PAGE),
          HAS_NEXT_PAGE,
        );
        const endCursor = serverPageInfo.getValue(END_CURSOR);
        if (typeof endCursor === 'string') {
          clientPageInfo.setValue(endCursor, END_CURSOR);
        }
      }
    }
  }
}

/**
 * @public
 *
 * Given a record and the name of the schema field for which a connection was
 * fetched, returns the linked connection record.
 *
 * Example:
 *
 * Given that data has already been fetched on some user `<id>` on the `friends`
 * field:
 *
 * ```
 * fragment FriendsFragment on User {
 *   friends(first: 10) @connection(key: "FriendsFragment_friends") {
 *    edges {
 *      node {
 *        id
 *        }
 *      }
 *   }
 * }
 * ```
 *
 * The `friends` connection record can be accessed with:
 *
 * ```
 * store => {
 *   const user = store.get('<id>');
 *   const friends = RelayConnectionHandler.getConnection(user, 'FriendsFragment_friends');
 *   // Access fields on the connection:
 *   const edges = friends.getLinkedRecords('edges');
 * }
 * ```
 *
 * TODO: t15733312
 * Currently we haven't run into this case yet, but we need to add a `getConnections`
 * that returns an array of the connections under the same `key` regardless of the variables.
 */
function getConnection(
  record: ReadOnlyRecordProxy,
  key: string,
  filters?: ?Variables,
): ?RecordProxy {
  const handleKey = getRelayHandleKey(CONNECTION, key, null);
  return record.getLinkedRecord(handleKey, filters);
}

/**
 * @public
 *
 * Inserts an edge after the given cursor, or at the end of the list if no
 * cursor is provided.
 *
 * Example:
 *
 * Given that data has already been fetched on some user `<id>` on the `friends`
 * field:
 *
 * ```
 * fragment FriendsFragment on User {
 *   friends(first: 10) @connection(key: "FriendsFragment_friends") {
 *    edges {
 *      node {
 *        id
 *        }
 *      }
 *   }
 * }
 * ```
 *
 * An edge can be appended with:
 *
 * ```
 * store => {
 *   const user = store.get('<id>');
 *   const friends = RelayConnectionHandler.getConnection(user, 'FriendsFragment_friends');
 *   const edge = store.create('<edge-id>', 'FriendsEdge');
 *   RelayConnectionHandler.insertEdgeAfter(friends, edge);
 * }
 * ```
 */
function insertEdgeAfter(
  record: RecordProxy,
  newEdge: RecordProxy,
  cursor?: ?string,
): void {
  const {CURSOR, EDGES} = RelayConnectionInterface.get();

  const edges = record.getLinkedRecords(EDGES);
  if (!edges) {
    record.setLinkedRecords([newEdge], EDGES);
    return;
  }
  let nextEdges;
  if (cursor == null) {
    nextEdges = edges.concat(newEdge);
  } else {
    nextEdges = [];
    let foundCursor = false;
    for (let ii = 0; ii < edges.length; ii++) {
      const edge = edges[ii];
      nextEdges.push(edge);
      if (edge == null) {
        continue;
      }
      const edgeCursor = edge.getValue(CURSOR);
      if (cursor === edgeCursor) {
        nextEdges.push(newEdge);
        foundCursor = true;
      }
    }
    if (!foundCursor) {
      nextEdges.push(newEdge);
    }
  }
  record.setLinkedRecords(nextEdges, EDGES);
}

/**
 * @public
 *
 * Creates an edge for a connection record, given a node and edge type.
 */
function createEdge(
  store: RecordSourceProxy,
  record: RecordProxy,
  node: RecordProxy,
  edgeType: string,
): RecordProxy {
  const {NODE} = RelayConnectionInterface.get();

  // An index-based client ID could easily conflict (unless it was
  // auto-incrementing, but there is nowhere to the store the id)
  // Instead, construct a client ID based on the connection ID and node ID,
  // which will only conflict if the same node is added to the same connection
  // twice. This is acceptable since the `insertEdge*` functions ignore
  // duplicates.
  const edgeID = generateRelayClientID(record.getDataID(), node.getDataID());
  let edge = store.get(edgeID);
  if (!edge) {
    edge = store.create(edgeID, edgeType);
  }
  edge.setLinkedRecord(node, NODE);
  return edge;
}

/**
 * @public
 *
 * Inserts an edge before the given cursor, or at the beginning of the list if
 * no cursor is provided.
 *
 * Example:
 *
 * Given that data has already been fetched on some user `<id>` on the `friends`
 * field:
 *
 * ```
 * fragment FriendsFragment on User {
 *   friends(first: 10) @connection(key: "FriendsFragment_friends") {
 *    edges {
 *      node {
 *        id
 *        }
 *      }
 *   }
 * }
 * ```
 *
 * An edge can be prepended with:
 *
 * ```
 * store => {
 *   const user = store.get('<id>');
 *   const friends = RelayConnectionHandler.getConnection(user, 'FriendsFragment_friends');
 *   const edge = store.create('<edge-id>', 'FriendsEdge');
 *   RelayConnectionHandler.insertEdgeBefore(friends, edge);
 * }
 * ```
 */
function insertEdgeBefore(
  record: RecordProxy,
  newEdge: RecordProxy,
  cursor?: ?string,
): void {
  const {CURSOR, EDGES} = RelayConnectionInterface.get();

  const edges = record.getLinkedRecords(EDGES);
  if (!edges) {
    record.setLinkedRecords([newEdge], EDGES);
    return;
  }
  let nextEdges;
  if (cursor == null) {
    nextEdges = [newEdge].concat(edges);
  } else {
    nextEdges = [];
    let foundCursor = false;
    for (let ii = 0; ii < edges.length; ii++) {
      const edge = edges[ii];
      if (edge != null) {
        const edgeCursor = edge.getValue(CURSOR);
        if (cursor === edgeCursor) {
          nextEdges.push(newEdge);
          foundCursor = true;
        }
      }
      nextEdges.push(edge);
    }
    if (!foundCursor) {
      nextEdges.unshift(newEdge);
    }
  }
  record.setLinkedRecords(nextEdges, EDGES);
}

/**
 * @public
 *
 * Remove any edges whose `node.id` matches the given id.
 */
function deleteNode(record: RecordProxy, nodeID: DataID): void {
  const {EDGES, NODE} = RelayConnectionInterface.get();

  const edges = record.getLinkedRecords(EDGES);
  if (!edges) {
    return;
  }
  let nextEdges;
  for (let ii = 0; ii < edges.length; ii++) {
    const edge = edges[ii];
    const node = edge && edge.getLinkedRecord(NODE);
    if (node != null && node.getDataID() === nodeID) {
      if (nextEdges === undefined) {
        nextEdges = edges.slice(0, ii);
      }
    } else if (nextEdges !== undefined) {
      nextEdges.push(edge);
    }
  }
  if (nextEdges !== undefined) {
    record.setLinkedRecords(nextEdges, EDGES);
  }
}

/**
 * @internal
 *
 * Creates a copy of an edge with a unique ID based on per-connection-instance
 * incrementing edge index. This is necessary to avoid collisions between edges,
 * which can occur because (edge) client IDs are assigned deterministically
 * based on the path from the nearest node with an id.
 *
 * Example: if the first N edges of the same connection are refetched, the edges
 * from the second fetch will be assigned the same IDs as the first fetch, even
 * though the nodes they point to may be different (or the same and in different
 * order).
 */
function buildConnectionEdge(
  store: RecordSourceProxy,
  connection: RecordProxy,
  edge: ?RecordProxy,
): ?RecordProxy {
  if (edge == null) {
    return edge;
  }
  const {EDGES} = RelayConnectionInterface.get();

  const edgeIndex = connection.getValue(NEXT_EDGE_INDEX);
  invariant(
    typeof edgeIndex === 'number',
    'RelayConnectionHandler: Expected %s to be a number, got `%s`.',
    NEXT_EDGE_INDEX,
    edgeIndex,
  );
  const edgeID = generateRelayClientID(
    connection.getDataID(),
    EDGES,
    edgeIndex,
  );
  const connectionEdge = store.create(edgeID, edge.getType());
  connectionEdge.copyFieldsFrom(edge);
  connection.setValue(edgeIndex + 1, NEXT_EDGE_INDEX);
  return connectionEdge;
}

/**
 * @internal
 *
 * Adds the source edges to the target edges, skipping edges with
 * duplicate node ids.
 */
function mergeEdges(
  sourceEdges: Array<?RecordProxy>,
  targetEdges: Array<?RecordProxy>,
  nodeIDs: Set<mixed>,
): void {
  const {NODE} = RelayConnectionInterface.get();

  for (let ii = 0; ii < sourceEdges.length; ii++) {
    const edge = sourceEdges[ii];
    if (!edge) {
      continue;
    }
    const node = edge.getLinkedRecord(NODE);
    const nodeID = node && node.getValue('id');
    if (nodeID) {
      if (nodeIDs.has(nodeID)) {
        continue;
      }
      nodeIDs.add(nodeID);
    }
    targetEdges.push(edge);
  }
}

module.exports = {
  buildConnectionEdge,
  createEdge,
  deleteNode,
  getConnection,
  insertEdgeAfter,
  insertEdgeBefore,
  update,
};
<|endoftext|>


<commit_before>/*jslint node: true */
/*jshint laxbreak: true */
'use strict';
/*
 * Licensed under the Apache 2 license.
 */
var d3 = require('d3');
var _ = require('underscore');
var BiolinksParser = require('biotea-io-parser');
var Tooltip = require('biotea-vis-tooltip');

/**
 * Private Methods
 */
var defaultOpts = {
    width: 600,
    height: 400,
    prefixID: 'PMC',
    path: undefined,
    ids: [],
    display: []
};

var displayTopics = function(viewer) {
    var labels = viewer.svg.append('g');
    labels.selectAll('.biotea_dist-topic-label')
        .data(viewer.topics)
        .enter().append('text')
        .text(function (d) { return d; })
        .attr('x', 0)
        .attr('y', function (d, i) { return i * viewer.gridSizeY; })
        .style('text-anchor', 'start')
        .attr('transform', 'translate(0,' + viewer.gridSizeY / 1.5 + ')')
        .classed('biotea_dist-topic-label', true)
        .attr('stroke', function(d) {
            return viewer.parser.getModel().model[d].color;
        })
        .on('mouseover', function(d) {
            Tooltip.create(d3.select(viewer.options.el), viewer.parser.getModel().model[d].desc, []);
        })
        .on('mouseout', function() {
            Tooltip.remove();
        });
};

var displayLegend = function(viewer) {
    var legendData = [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9];
    var legendX = Math.floor((viewer.options.width - viewer.margin.left) / legendData.length);

    var legend = viewer.svg.selectAll('.biotea_dist-legend')
        .data(legendData)
        .enter().append('g')
        .classed('biotea_dist-legend', true)
        .attr('transform', function(d, i) {
            return 'translate('
                + (viewer.margin.left + (legendX * i)) + ', '
                + (viewer.options.height - viewer.margin.bottom + 10) + ')';
        })
    legend.append('rect')
        .attr('x', 0)
        .attr('y', 0)
        .attr('width', legendX)
        .attr('height', 10)
        .attr('fill', function(d) {
            return viewer.useColorArray ? viewer.options.colorArray[Math.floor(d * 10)]
                    : viewer.colorScale(d);
        });
    legend.append('text')
        .classed('biotea_dist-legend-label', true)
        .text(function(d) { return '≥ ' + d; })
        .attr('x', 0)
        .attr('y', 22);
};

var addShadow = function(viewer) {
    viewer.selection = viewer.svg.append('g').attr('class', 'biotea_dist_shadow_group');
    viewer.selection.append('rect')
        .attr('transform', 'translate(0,0)')
        .attr('class','biotea_dist_shadow')
        .attr('x', viewer.margin.left)
        .attr('y', 0)
        .attr('width', 0)
        .attr('height', 0)
    ;
};

var createTooltip = function(viewer, dist, datum) {
    var prefixedId = viewer.options.prefixID ? viewer.options.prefixID + ':' + dist.id : dist.id;
    if (datum.score) {
        Tooltip.create(d3.select(viewer.options.el), 'Article',
            [['Id', prefixedId], ['Title', dist.display], ['Score', datum.score.toFixed(3)]]);
    } else {
        Tooltip.create(d3.select(viewer.options.el), 'Article',
            [['Id', prefixedId], ['Title', dist.display]]);
    }
};

var initMatrix = function(viewer, dist) {
    var cards = viewer.svg.append('g')
        .attr('transform', 'translate(' + viewer.margin.left + ', 0)')
        .attr('class', 'biotea_dist_group')
        .selectAll('.biotea_dist')
        .data(dist.data).enter().append('rect')
        .attr('x', function() {
            return _.indexOf(viewer.options.ids, dist.id) * viewer.gridSizeX;
        })
        .attr('y', function(d) {
            return _.indexOf(viewer.topics, d.group) * viewer.gridSizeY;
        })
        .attr('class', 'biotea_dist')
        .attr('width', viewer.gridSizeX)
        .attr('height', viewer.gridSizeY)
        .style('stroke', 'white')
        .style('fill', function(d) {
            return d.score ? viewer.useColorArray
                ? viewer.options.colorArray[Math.floor(d.score * 10)] : viewer.colorScale(d.score)
                : 'white';
        })
        .on('mouseover', function(d) {
            createTooltip(viewer, dist, d);
        })
        .on('mousemove', function(d) {
            createTooltip(viewer, dist, d);
        })
        .on('mouseout', function() {
            Tooltip.remove();
        })
        .on('click', function(d) {
            if (!viewer.selection) {
                addShadow(viewer);
            }
            d3.selectAll('.biotea_dist_shadow')
                .attr('transform', 'translate(' + _.indexOf(viewer.options.ids, dist.id) * viewer.gridSizeX + ',0)')
                .attr('width', viewer.gridSizeX)
                .attr('height', viewer.gridSizeY * viewer.topics.length);
            viewer.parser.getDispatcher().selected({
                article: dist.id,
                group: d.group,
                score: d.score ? d.score.toFixed(3) : d.score
            });
        });
    ;
};

var cleanView = function(viewer) {
    d3.select(viewer.options.el).selectAll('.biotea_dist_group').remove();
    d3.select(viewer.options.el).selectAll('.biotea_dist_shadow_group').remove();
    viewer.selection = undefined;
};

var TopicDistributionViewer = function(opts){
    var viewer = this;
    viewer.parser = new BiolinksParser();
    viewer.data = [];
    viewer.options = _.extend({}, defaultOpts, opts);
    viewer.topics = _.keys(viewer.parser.getModel().model);
    viewer.useColorArray = viewer.options.colorArray ? true : false;
    viewer.colorScale = d3.scale.linear()
        .domain([0, 1])
        .range(['#ccffcc', '#000099'])
        .clamp(true);

    viewer.margin = {left: 40, bottom: 40};
    viewer.gridSizeX = Math.floor((viewer.options.width-viewer.margin.left) / viewer.options.ids.length);
    viewer.gridSizeY = Math.floor((viewer.options.height-viewer.margin.bottom) / viewer.topics.length);
    viewer.selection = undefined;

    d3.select(viewer.options.el).selectAll('*').remove();
    viewer.svg = d3.select(viewer.options.el).append('svg')
        .attr('width', viewer.options.width)
        .attr('height', viewer.options.height);

    viewer.loadURL = function() {
        cleanView(viewer);
        _.each(viewer.options.ids, function(id, index) {
            var loader = viewer.parser.loadDistribution(viewer.options.path, id, viewer.options.display[index]);
            loader.done(function(loadedData) {
                initMatrix(viewer, loadedData.dist);
            }).fail( function(e) {
                console.log(e);
            });
        });
    };

    viewer.loadData = function(distArray) {
        cleanView(viewer);
        viewer.options.ids = _.pluck(distArray, 'id');
        viewer.gridSizeX = Math.floor((viewer.options.width-viewer.margin.left) / viewer.options.ids.length);
        _.each(distArray, function(dist) {
            initMatrix(viewer, dist);
        });
    };

    viewer.render = function() {
        var viewer = this;
        if (viewer.options.path && viewer.options.ids) {
            displayTopics(viewer);
            displayLegend(viewer);
            viewer.loadURL();
        } else if (viewer.options.data) {
            displayTopics(viewer);
            displayLegend(viewer);
            viewer.loadData(viewer.options.data);
        }
    };

    viewer.setPath = function(path) {
        this.options.path = path;
    };

    viewer.setIds = function(ids, displays) {
        this.options.ids = ids;
        this.options.display = displays ? displays : [];
        viewer.gridSizeX = Math.floor((viewer.options.width-viewer.margin.left) / viewer.options.ids.length);
        viewer.gridSizeY = Math.floor((viewer.options.height-viewer.margin.bottom) / viewer.topics.length);
    };

    viewer.setPrefix = function(prefix) {
        this.options.prefixID = prefix;
    };

    viewer.setData = function(distArray) {
        viewer.options.ids = _.pluck(distArray, 'id');
        viewer.gridSizeX = Math.floor((viewer.options.width-viewer.margin.left) / viewer.options.ids.length);
        this.options.data = distArray;
    };

    viewer.getDispatcher = function() {
        return this.parser.getDispatcher();
    };

    viewer.render();
};

module.exports = TopicDistributionViewer;<commit_msg>Add type distribution to dispatched events.<commit_after>/*jslint node: true */
/*jshint laxbreak: true */
'use strict';
/*
 * Licensed under the Apache 2 license.
 */
var d3 = require('d3');
var _ = require('underscore');
var BiolinksParser = require('biotea-io-parser');
var Tooltip = require('biotea-vis-tooltip');

/**
 * Private Methods
 */
var defaultOpts = {
    width: 600,
    height: 400,
    prefixID: 'PMC',
    path: undefined,
    ids: [],
    display: []
};

var displayTopics = function(viewer) {
    var labels = viewer.svg.append('g');
    labels.selectAll('.biotea_dist-topic-label')
        .data(viewer.topics)
        .enter().append('text')
        .text(function (d) { return d; })
        .attr('x', 0)
        .attr('y', function (d, i) { return i * viewer.gridSizeY; })
        .style('text-anchor', 'start')
        .attr('transform', 'translate(0,' + viewer.gridSizeY / 1.5 + ')')
        .classed('biotea_dist-topic-label', true)
        .attr('stroke', function(d) {
            return viewer.parser.getModel().model[d].color;
        })
        .on('mouseover', function(d) {
            Tooltip.create(d3.select(viewer.options.el), viewer.parser.getModel().model[d].desc, []);
        })
        .on('mouseout', function() {
            Tooltip.remove();
        });
};

var displayLegend = function(viewer) {
    var legendData = [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9];
    var legendX = Math.floor((viewer.options.width - viewer.margin.left) / legendData.length);

    var legend = viewer.svg.selectAll('.biotea_dist-legend')
        .data(legendData)
        .enter().append('g')
        .classed('biotea_dist-legend', true)
        .attr('transform', function(d, i) {
            return 'translate('
                + (viewer.margin.left + (legendX * i)) + ', '
                + (viewer.options.height - viewer.margin.bottom + 10) + ')';
        })
    legend.append('rect')
        .attr('x', 0)
        .attr('y', 0)
        .attr('width', legendX)
        .attr('height', 10)
        .attr('fill', function(d) {
            return viewer.useColorArray ? viewer.options.colorArray[Math.floor(d * 10)]
                    : viewer.colorScale(d);
        });
    legend.append('text')
        .classed('biotea_dist-legend-label', true)
        .text(function(d) { return '≥ ' + d; })
        .attr('x', 0)
        .attr('y', 22);
};

var addShadow = function(viewer) {
    viewer.selection = viewer.svg.append('g').attr('class', 'biotea_dist_shadow_group');
    viewer.selection.append('rect')
        .attr('transform', 'translate(0,0)')
        .attr('class','biotea_dist_shadow')
        .attr('x', viewer.margin.left)
        .attr('y', 0)
        .attr('width', 0)
        .attr('height', 0)
    ;
};

var createTooltip = function(viewer, dist, datum) {
    var prefixedId = viewer.options.prefixID ? viewer.options.prefixID + ':' + dist.id : dist.id;
    if (datum.score) {
        Tooltip.create(d3.select(viewer.options.el), 'Article',
            [['Id', prefixedId], ['Title', dist.display], ['Score', datum.score.toFixed(3)]]);
    } else {
        Tooltip.create(d3.select(viewer.options.el), 'Article',
            [['Id', prefixedId], ['Title', dist.display]]);
    }
};

var initMatrix = function(viewer, dist) {
    var cards = viewer.svg.append('g')
        .attr('transform', 'translate(' + viewer.margin.left + ', 0)')
        .attr('class', 'biotea_dist_group')
        .selectAll('.biotea_dist')
        .data(dist.data).enter().append('rect')
        .attr('x', function() {
            return _.indexOf(viewer.options.ids, dist.id) * viewer.gridSizeX;
        })
        .attr('y', function(d) {
            return _.indexOf(viewer.topics, d.group) * viewer.gridSizeY;
        })
        .attr('class', 'biotea_dist')
        .attr('width', viewer.gridSizeX)
        .attr('height', viewer.gridSizeY)
        .style('stroke', 'white')
        .style('fill', function(d) {
            return d.score ? viewer.useColorArray
                ? viewer.options.colorArray[Math.floor(d.score * 10)] : viewer.colorScale(d.score)
                : 'white';
        })
        .on('mouseover', function(d) {
            createTooltip(viewer, dist, d);
        })
        .on('mousemove', function(d) {
            createTooltip(viewer, dist, d);
        })
        .on('mouseout', function() {
            Tooltip.remove();
        })
        .on('click', function(d) {
            if (!viewer.selection) {
                addShadow(viewer);
            }
            d3.selectAll('.biotea_dist_shadow')
                .attr('transform', 'translate(' + _.indexOf(viewer.options.ids, dist.id) * viewer.gridSizeX + ',0)')
                .attr('width', viewer.gridSizeX)
                .attr('height', viewer.gridSizeY * viewer.topics.length);
            viewer.parser.getDispatcher().selected({
                type: 'distribution',
                article: dist.id,
                group: d.group,
                score: d.score ? d.score.toFixed(3) : d.score
            });
        });
    ;
};

var cleanView = function(viewer) {
    d3.select(viewer.options.el).selectAll('.biotea_dist_group').remove();
    d3.select(viewer.options.el).selectAll('.biotea_dist_shadow_group').remove();
    viewer.selection = undefined;
};

var TopicDistributionViewer = function(opts){
    var viewer = this;
    viewer.parser = new BiolinksParser();
    viewer.data = [];
    viewer.options = _.extend({}, defaultOpts, opts);
    viewer.topics = _.keys(viewer.parser.getModel().model);
    viewer.useColorArray = viewer.options.colorArray ? true : false;
    viewer.colorScale = d3.scale.linear()
        .domain([0, 1])
        .range(['#ccffcc', '#000099'])
        .clamp(true);

    viewer.margin = {left: 40, bottom: 40};
    viewer.gridSizeX = Math.floor((viewer.options.width-viewer.margin.left) / viewer.options.ids.length);
    viewer.gridSizeY = Math.floor((viewer.options.height-viewer.margin.bottom) / viewer.topics.length);
    viewer.selection = undefined;

    d3.select(viewer.options.el).selectAll('*').remove();
    viewer.svg = d3.select(viewer.options.el).append('svg')
        .attr('width', viewer.options.width)
        .attr('height', viewer.options.height);

    viewer.loadURL = function() {
        cleanView(viewer);
        _.each(viewer.options.ids, function(id, index) {
            var loader = viewer.parser.loadDistribution(viewer.options.path, id, viewer.options.display[index]);
            loader.done(function(loadedData) {
                initMatrix(viewer, loadedData.dist);
            }).fail( function(e) {
                console.log(e);
            });
        });
    };

    viewer.loadData = function(distArray) {
        cleanView(viewer);
        viewer.options.ids = _.pluck(distArray, 'id');
        viewer.gridSizeX = Math.floor((viewer.options.width-viewer.margin.left) / viewer.options.ids.length);
        _.each(distArray, function(dist) {
            initMatrix(viewer, dist);
        });
    };

    viewer.render = function() {
        var viewer = this;
        if (viewer.options.path && viewer.options.ids) {
            displayTopics(viewer);
            displayLegend(viewer);
            viewer.loadURL();
        } else if (viewer.options.data) {
            displayTopics(viewer);
            displayLegend(viewer);
            viewer.loadData(viewer.options.data);
        }
    };

    viewer.setPath = function(path) {
        this.options.path = path;
    };

    viewer.setIds = function(ids, displays) {
        this.options.ids = ids;
        this.options.display = displays ? displays : [];
        viewer.gridSizeX = Math.floor((viewer.options.width-viewer.margin.left) / viewer.options.ids.length);
        viewer.gridSizeY = Math.floor((viewer.options.height-viewer.margin.bottom) / viewer.topics.length);
    };

    viewer.setPrefix = function(prefix) {
        this.options.prefixID = prefix;
    };

    viewer.setData = function(distArray) {
        viewer.options.ids = _.pluck(distArray, 'id');
        viewer.gridSizeX = Math.floor((viewer.options.width-viewer.margin.left) / viewer.options.ids.length);
        this.options.data = distArray;
    };

    viewer.getDispatcher = function() {
        return this.parser.getDispatcher();
    };

    viewer.render();
};

module.exports = TopicDistributionViewer;<|endoftext|>