<commit_before>import Ember from 'ember';
import layout from './template';

import loadAll from 'ember-osf/utils/load-relationship';
import { authenticatedAJAX } from 'ember-osf/utils/ajax-helpers';

/**
 * File browser widget
 *
 * Sample usage:
 * ```handlebars
 * {{file-browser}}
 * ```
 * @class file-browser
 */
export default Ember.Component.extend({
    // TODO: Improve documentation in the future
    layout,
    //Can be overwritten to have a trimmed down display, these are all the options available to be displayed
    display: Ember.A(['header', 'share-link-column', 'size-column', 'version-column', 'downloads-column', 'modified-column', 'delete-button', 'rename-button', 'download-button', 'view-button', 'info-button', 'upload-button']),
    store: Ember.inject.service(),
    growler: Ember.inject.service(),
    toast: Ember.inject.service(),
    classNames: ['file-browser'],
    dropzoneOptions: {
        createImageThumbnails: false,
        method: 'PUT',
        withCredentials: true,
    },
    init() {
        this.set('_items', Ember.A());
        this._super(...arguments);
    },
    currentUser: Ember.inject.service(),
    edit: Ember.computed('user', function() {
        return this.get('user.id') === this.get('currentUser.currentUserId');
    }),
    _loadFiles(user) {
        //pagination? when?
        loadAll(user, 'quickfiles', this.get('_items')).then(() => this.set('loaded', true));
    },
    _loadUser:  Ember.on('init', Ember.observer('user', function() {
        let user = this.get('user');
        if (!user || this.get('loaded')) {
            return;
        }
        //items need to be reloaded when attrs are received
        let _load = user_ => {
            Ember.run(() => {
                this.set('_items', Ember.A());
                Ember.run.next(() => {
                    this._loadFiles(user_);
                });
            });
        };
        if (user.then) {
            user.then(user_ => {
                _load(user_);
            })
        } else {
            _load(user);
        }
    })),
    uploadUrl: Ember.computed('user', function() {
        return this.get('user.links.relationships.quickfiles.links.upload.href');
    }),
    downloadUrl: Ember.computed('user', function() {
        return this.get('user.links.relationships.quickfiles.links.download.href');
    }),
    loaded: false,
    filtering: false,
    renaming: false,
    uploading: Ember.A(),
    filter: null,
    modalOpen: false,
    itemsLoaded: true,
    selectedItems: Ember.computed.filterBy('items', 'isSelected', true),
    loadedChanged: Ember.observer('itemsLoaded', function() {
        let containerWidth = this.$().width();
        this.set('itemWidth', containerWidth);
    }),
    items: Ember.computed('_items', 'textValue', 'filtering', function() {
        //look at ways to use the api to filter
        return this.get('textValue') && this.get('filtering') ? this.get('_items').filter(i => i.get('name').indexOf(this.get('textValue')) !== -1) : this.get('_items');
    }),
    textFieldOpen: Ember.computed('filtering', 'renaming', function() {
        return this.get('filtering') ? 'filtering' : (this.get('renaming') ? 'renaming' : false);
    }),
    nameColumnWidth: Ember.computed('display', function() {
        let display = this.get('display');
        let width = 6 + !display.includes('share-link-column') + !display.includes('size-column') + !display.includes('version-column') + !display.includes('downloads-column') + 2 * !display.includes('modified-column');
        return width;
    }),
    browserState: Ember.computed('loaded', '_items', function() {
        return this.get('loaded') ? (this.get('_items').length ? (this.get('items').length ? 'show' : 'filtered') : 'empty') : 'loading';
    }),
    actions: {
        //dropzone listeners
        addedFile(_, __, file) {
            this.get('uploading').pushObject(file);
        },
        uploadProgress(_, __, file, progress) {
            Ember.$('#uploading-' + file.size).css('width', progress + '%');
        },
        dragStart() {
            this.set('dropping', true);
        },
        dragEnd() {
            this.set('dropping', false);
        },
        error(_, __, file, response) {
            this.get('uploading').removeObject(file);
            // this.get('growler').growl({
                // message: response
            // });
            this.get('toast').error(response);
        },
        success(_, __, file, response) {
            this.get('uploading').removeObject(file);
            let data = response.data.attributes;
            //OPTIONS (some not researched)
            //Ember store method for passing updated attributes (either with a query for the object, or iterating to find matching)
            //Manually updating the object based on new attrs
            //Making an additional request anytime success is done, finding the file detail page based on path
            let path = data.path; //THERE SHOULD BE A BETTER WAY OF DOING THIS
            let conflictingItem = false;
            for (let file of this.get('_items')) {
                if (path === file.get('path')) {
                    conflictingItem = file;
                    break;
                }
            }
            if (conflictingItem) {
                conflictingItem.setProperties({
                    size: data.size,
                    currentVersion: data.extra.version,
                    dateModified: data.modified_utc
                })
                return;
            }
            response.data.type = 'file'; //
            response.data.attributes.currentVersion = '1';
            let item = this.get('store').push(response);
            item.set('links', response.data.links); //Push doesnt pass it links
            this.get('_items').pushObject(item);
            this.notifyPropertyChange('_items');
        },
        buildUrl(files) {
            let name = files[0].name;
            let conflictingItem = false;
            for (let file of this.get('_items')) {
                if (name === file.get('itemName')) {
                    conflictingItem = file;
                    break;
                }
            }
            if (conflictingItem) {
                return conflictingItem.get('links.upload') + '?kind=file';
            }
            return this.get('uploadUrl') + '?' + Ember.$.param({
                name: files[0].name
            });
        },
        selectItem(item) {
            if (this.get('selectedItems.length')) {
                for (var item_ of this.get('selectedItems')) {
                    item_.set('isSelected', item_ === item);
                }
            }
            item.set('isSelected', true);
            this.set('shiftAnchor', item);
        },
        selectMultiple(item, toggle) {
            if (toggle) {
                item.toggleProperty('isSelected');
            } else {
                let items = this.get('items');
                let anchor = this.get('shiftAnchor');
                if (anchor) {
                    let max = Math.max(items.indexOf(anchor), items.indexOf(item));
                    let min = Math.min(items.indexOf(anchor), items.indexOf(item));
                    for (var item_ of this.get('items')) {
                        item_.set('isSelected', item_ === item || item_ === anchor || (items.indexOf(item_) > min && items.indexOf(item_) < max));
                    }
                }
                item.set('isSelected', true);
            }
            Ember.run.next(this, function(){
                if (this.get('selectedItems.length') === 1) {
                    this.set('shiftAnchor', item)
                }
            });
        },
        viewItem() {
            let item = this.get('selectedItems.firstObject');
            this.sendAction('openFile', item);
        },
        openItem(item) {
            this.sendAction('openFile', item);
        },
        downloadItem() {
            let downloadLink = this.get('selectedItems.firstObject.links.download');
            window.location = downloadLink;
        },
        _deleteItem(item, url) {
            authenticatedAJAX({
                url: url,
                type: 'DELETE',
                xhrFields: {withCredentials: true}
            })
            .done(() => {
                //TODO rethink the flash system, this seems gross nooddly
                item.set('flash', {
                    message: 'This file has been deleted',
                    type: 'danger'
                });
                setTimeout(() => {
                    this.get('_items').removeObject(item);
                    this.notifyPropertyChange('_items');
                }, 1800);
            })
            .fail(() => {
                item.set('flash', {
                    message: 'Delete failed',
                    type: 'danger'
                });
            });
        },
        deleteItem(){
            let item = this.get('selectedItems.firstObject');
            let url = item.get('links.download');
            this.send('_deleteItem', item, url);
            this.set('modalOpen', false);
        },
        deleteItems() {
            for (let item_ of this.get('selectedItems')) {
                let url = item_.get('links.download');
                this.send('_deleteItem', item_, url);
            }
            this.set('modalOpen', false);
        },
        _rename(conflict) {
            let item = this.get('selectedItems.firstObject');
            this.set('modalOpen', false);
            authenticatedAJAX({
                url: item.get('links.upload'),
                type: 'POST',
                xhrFields: {withCredentials: true},
                headers: {
                    'Content-Type': 'Application/json'
                },
                data: JSON.stringify({
                    action: 'rename',
                    rename: this.get('textValue'),
                    conflict: conflict || 'replace'
                })
            })
            .done(response => {
                item.set('itemName', response.data.attributes.name);
                item.set('flash', {
                    message: 'Successfully renamed',
                    type: 'success'
                });
            })
            .fail(() => {
                item.set('flash', {
                    message: 'Failed to rename item',
                    type: 'danger'
                })
            });
            this.set('textValue', null);
            this.toggleProperty('renaming');
        },
        rename() {
            let rename = this.get('textValue');
            let conflict = false;
            for (let item of this.get('_items')) {
                if (item.get('itemName') === rename) {
                    if (item === this.get('selectedItems.firstObject')) {
                        this.set('textValue', null);
                        this.toggleProperty('renaming');
                        return;
                    }
                    conflict = true;
                    break;
                }
            }
            if (conflict) {
                this.set('modalOpen', 'renameConflict');
            } else {
                this.send('_rename', 'replace');
            }
        },
        cancelRename() {
            this.set('textValue', null);
            this.toggleProperty('renaming')
            this.set('modalOpen', false);
        },
        sort(by, order) {
            let sorted = this.get('_items').sortBy(by);
            this.set('_items', order === 'asc' ? sorted : sorted.reverse());
        },
        toggleText(which) {
            this.set('textValue', null);
            this.toggleProperty(which);
        },
        openModal(modalType) {
            this.set('modalOpen', modalType);
        },
        closeModal() {
            this.set('modalOpen', false);
        },
        textValueKeypress() {
            if (this.get('renaming')) {
                this.send('rename');
            }
        },
        dismissOtherPops(item) {
            for (var item_ of this.get('items')) {
                if (item_.get('path') !== item.get('path')) {
                    item_.set('visiblePopup', false);
                }
            }
        }
    }
});
<commit_msg>Allow deselecting an item by clicking it again<commit_after>import Ember from 'ember';
import layout from './template';

import loadAll from 'ember-osf/utils/load-relationship';
import { authenticatedAJAX } from 'ember-osf/utils/ajax-helpers';

/**
 * File browser widget
 *
 * Sample usage:
 * ```handlebars
 * {{file-browser}}
 * ```
 * @class file-browser
 */
export default Ember.Component.extend({
    // TODO: Improve documentation in the future
    layout,
    //Can be overwritten to have a trimmed down display, these are all the options available to be displayed
    display: Ember.A(['header', 'share-link-column', 'size-column', 'version-column', 'downloads-column', 'modified-column', 'delete-button', 'rename-button', 'download-button', 'view-button', 'info-button', 'upload-button']),
    store: Ember.inject.service(),
    growler: Ember.inject.service(),
    toast: Ember.inject.service(),
    classNames: ['file-browser'],
    dropzoneOptions: {
        createImageThumbnails: false,
        method: 'PUT',
        withCredentials: true,
    },
    init() {
        this.set('_items', Ember.A());
        this._super(...arguments);
    },
    currentUser: Ember.inject.service(),
    edit: Ember.computed('user', function() {
        return this.get('user.id') === this.get('currentUser.currentUserId');
    }),
    _loadFiles(user) {
        //pagination? when?
        loadAll(user, 'quickfiles', this.get('_items')).then(() => this.set('loaded', true));
    },
    _loadUser:  Ember.on('init', Ember.observer('user', function() {
        let user = this.get('user');
        if (!user || this.get('loaded')) {
            return;
        }
        //items need to be reloaded when attrs are received
        let _load = user_ => {
            Ember.run(() => {
                this.set('_items', Ember.A());
                Ember.run.next(() => {
                    this._loadFiles(user_);
                });
            });
        };
        if (user.then) {
            user.then(user_ => {
                _load(user_);
            })
        } else {
            _load(user);
        }
    })),
    uploadUrl: Ember.computed('user', function() {
        return this.get('user.links.relationships.quickfiles.links.upload.href');
    }),
    downloadUrl: Ember.computed('user', function() {
        return this.get('user.links.relationships.quickfiles.links.download.href');
    }),
    loaded: false,
    filtering: false,
    renaming: false,
    uploading: Ember.A(),
    filter: null,
    modalOpen: false,
    itemsLoaded: true,
    selectedItems: Ember.computed.filterBy('items', 'isSelected', true),
    loadedChanged: Ember.observer('itemsLoaded', function() {
        let containerWidth = this.$().width();
        this.set('itemWidth', containerWidth);
    }),
    items: Ember.computed('_items', 'textValue', 'filtering', function() {
        //look at ways to use the api to filter
        return this.get('textValue') && this.get('filtering') ? this.get('_items').filter(i => i.get('name').indexOf(this.get('textValue')) !== -1) : this.get('_items');
    }),
    textFieldOpen: Ember.computed('filtering', 'renaming', function() {
        return this.get('filtering') ? 'filtering' : (this.get('renaming') ? 'renaming' : false);
    }),
    nameColumnWidth: Ember.computed('display', function() {
        let display = this.get('display');
        let width = 6 + !display.includes('share-link-column') + !display.includes('size-column') + !display.includes('version-column') + !display.includes('downloads-column') + 2 * !display.includes('modified-column');
        return width;
    }),
    browserState: Ember.computed('loaded', '_items', function() {
        return this.get('loaded') ? (this.get('_items').length ? (this.get('items').length ? 'show' : 'filtered') : 'empty') : 'loading';
    }),
    actions: {
        //dropzone listeners
        addedFile(_, __, file) {
            this.get('uploading').pushObject(file);
        },
        uploadProgress(_, __, file, progress) {
            Ember.$('#uploading-' + file.size).css('width', progress + '%');
        },
        dragStart() {
            this.set('dropping', true);
        },
        dragEnd() {
            this.set('dropping', false);
        },
        error(_, __, file, response) {
            this.get('uploading').removeObject(file);
            // this.get('growler').growl({
                // message: response
            // });
            this.get('toast').error(response);
        },
        success(_, __, file, response) {
            this.get('uploading').removeObject(file);
            let data = response.data.attributes;
            //OPTIONS (some not researched)
            //Ember store method for passing updated attributes (either with a query for the object, or iterating to find matching)
            //Manually updating the object based on new attrs
            //Making an additional request anytime success is done, finding the file detail page based on path
            let path = data.path; //THERE SHOULD BE A BETTER WAY OF DOING THIS
            let conflictingItem = false;
            for (let file of this.get('_items')) {
                if (path === file.get('path')) {
                    conflictingItem = file;
                    break;
                }
            }
            if (conflictingItem) {
                conflictingItem.setProperties({
                    size: data.size,
                    currentVersion: data.extra.version,
                    dateModified: data.modified_utc
                })
                return;
            }
            response.data.type = 'file'; //
            response.data.attributes.currentVersion = '1';
            let item = this.get('store').push(response);
            item.set('links', response.data.links); //Push doesnt pass it links
            this.get('_items').pushObject(item);
            this.notifyPropertyChange('_items');
        },
        buildUrl(files) {
            let name = files[0].name;
            let conflictingItem = false;
            for (let file of this.get('_items')) {
                if (name === file.get('itemName')) {
                    conflictingItem = file;
                    break;
                }
            }
            if (conflictingItem) {
                return conflictingItem.get('links.upload') + '?kind=file';
            }
            return this.get('uploadUrl') + '?' + Ember.$.param({
                name: files[0].name
            });
        },
        selectItem(item) {
            if (this.get('selectedItems.length') > 1) {
                for (var item_ of this.get('selectedItems')) {
                    item_.set('isSelected', item_ === item);
                }
            } else if (this.get('selectedItems.length') ===  1) {
                if (item.get('isSelected')) {
                    item.set('isSelected', false);
                    return;
                }
                let otherItem = this.get('selectedItems.firstObject');
                otherItem.set('isSelected', false);
            }
            item.set('isSelected', true);
            this.set('shiftAnchor', item);
        },
        selectMultiple(item, toggle) {
            if (toggle) {
                item.toggleProperty('isSelected');
            } else {
                let items = this.get('items');
                let anchor = this.get('shiftAnchor');
                if (anchor) {
                    let max = Math.max(items.indexOf(anchor), items.indexOf(item));
                    let min = Math.min(items.indexOf(anchor), items.indexOf(item));
                    for (var item_ of this.get('items')) {
                        item_.set('isSelected', item_ === item || item_ === anchor || (items.indexOf(item_) > min && items.indexOf(item_) < max));
                    }
                }
                item.set('isSelected', true);
            }
            Ember.run.next(this, function(){
                if (this.get('selectedItems.length') === 1) {
                    this.set('shiftAnchor', item)
                }
            });
        },
        viewItem() {
            let item = this.get('selectedItems.firstObject');
            this.sendAction('openFile', item);
        },
        openItem(item) {
            this.sendAction('openFile', item);
        },
        downloadItem() {
            let downloadLink = this.get('selectedItems.firstObject.links.download');
            window.location = downloadLink;
        },
        _deleteItem(item, url) {
            authenticatedAJAX({
                url: url,
                type: 'DELETE',
                xhrFields: {withCredentials: true}
            })
            .done(() => {
                //TODO rethink the flash system, this seems gross nooddly
                item.set('flash', {
                    message: 'This file has been deleted',
                    type: 'danger'
                });
                setTimeout(() => {
                    this.get('_items').removeObject(item);
                    this.notifyPropertyChange('_items');
                }, 1800);
            })
            .fail(() => {
                item.set('flash', {
                    message: 'Delete failed',
                    type: 'danger'
                });
            });
        },
        deleteItem(){
            let item = this.get('selectedItems.firstObject');
            let url = item.get('links.download');
            this.send('_deleteItem', item, url);
            this.set('modalOpen', false);
        },
        deleteItems() {
            for (let item_ of this.get('selectedItems')) {
                let url = item_.get('links.download');
                this.send('_deleteItem', item_, url);
            }
            this.set('modalOpen', false);
        },
        _rename(conflict) {
            let item = this.get('selectedItems.firstObject');
            this.set('modalOpen', false);
            authenticatedAJAX({
                url: item.get('links.upload'),
                type: 'POST',
                xhrFields: {withCredentials: true},
                headers: {
                    'Content-Type': 'Application/json'
                },
                data: JSON.stringify({
                    action: 'rename',
                    rename: this.get('textValue'),
                    conflict: conflict || 'replace'
                })
            })
            .done(response => {
                item.set('itemName', response.data.attributes.name);
                item.set('flash', {
                    message: 'Successfully renamed',
                    type: 'success'
                });
            })
            .fail(() => {
                item.set('flash', {
                    message: 'Failed to rename item',
                    type: 'danger'
                })
            });
            this.set('textValue', null);
            this.toggleProperty('renaming');
        },
        rename() {
            let rename = this.get('textValue');
            let conflict = false;
            for (let item of this.get('_items')) {
                if (item.get('itemName') === rename) {
                    if (item === this.get('selectedItems.firstObject')) {
                        this.set('textValue', null);
                        this.toggleProperty('renaming');
                        return;
                    }
                    conflict = true;
                    break;
                }
            }
            if (conflict) {
                this.set('modalOpen', 'renameConflict');
            } else {
                this.send('_rename', 'replace');
            }
        },
        cancelRename() {
            this.set('textValue', null);
            this.toggleProperty('renaming')
            this.set('modalOpen', false);
        },
        sort(by, order) {
            let sorted = this.get('_items').sortBy(by);
            this.set('_items', order === 'asc' ? sorted : sorted.reverse());
        },
        toggleText(which) {
            this.set('textValue', null);
            this.toggleProperty(which);
        },
        openModal(modalType) {
            this.set('modalOpen', modalType);
        },
        closeModal() {
            this.set('modalOpen', false);
        },
        textValueKeypress() {
            if (this.get('renaming')) {
                this.send('rename');
            }
        },
        dismissOtherPops(item) {
            for (var item_ of this.get('items')) {
                if (item_.get('path') !== item.get('path')) {
                    item_.set('visiblePopup', false);
                }
            }
        }
    }
});
<|endoftext|>


<commit_before>import Ember from 'ember';

const { Mixin, isBlank } = Ember;

export default Mixin.create({
  init: function() {
    this._super(...arguments);
    var model = this.get('model');
    var field = this.get('field');

    model.addObserver(field, this, this.valueObserver);
  },

  willDestroy: function() {
    this._super(...arguments);
    var model = this.get('model');
    var field = this.get('field');

    if(!isBlank(model)){
      model.removeObserver(field, this, this.valueObserver);
    }
  },

  valueObserver: function(){
    // this function observes changes in the model of the field, as we can't put dynamic computed properties
    this.notifyPropertyChange('model');
  }
});
<commit_msg>small refactor<commit_after>import Ember from 'ember';

const { Mixin, isBlank } = Ember;

export default Mixin.create({
  init() {
    this._super(...arguments);
    const model = this.get('model');
    const field = this.get('field');

    model.addObserver(field, this, this.valueObserver);
  },

  willDestroy() {
    this._super(...arguments);
    const model = this.get('model');
    const field = this.get('field');

    if(!isBlank(model)){
      model.removeObserver(field, this, this.valueObserver);
    }
  },

  valueObserver(){
    // this function observes changes in the model of the field, as we can't put dynamic computed properties
    this.notifyPropertyChange('model');
  }
});
<|endoftext|>


<commit_before>/* globals analytics, browser, communication, makeUuid, Raven, catcher, auth */

window.errorpopup = (function () {
  let exports = {};

  let messages = {
    REQUEST_ERROR: {
      title: browser.i18n.getMessage("requestErrorTitle"),
      info: browser.i18n.getMessage("requestErrorDetails")
    },
    CONNECTION_ERROR: {
      title: browser.i18n.getMessage("connectionErrorTitle"),
      info: browser.i18n.getMessage("connectionErrorDetails")
    },
    LOGIN_ERROR: {
      title: browser.i18n.getMessage("requestErrorTitle"),
      info: browser.i18n.getMessage("loginErrorDetails")
    },
    LOGIN_CONNECTION_ERROR: {
      title: browser.i18n.getMessage("connectionErrorTitle"),
      info: browser.i18n.getMessage("connectionErrorDetails")
    },
    UNSHOOTABLE_PAGE: {
      title: browser.i18n.getMessage("unshootablePageErrorTitle"),
      info: browser.i18n.getMessage("unshootablePageErrorDetails")
    },
    SHOT_PAGE: {
      title: browser.i18n.getMessage("selfScreenshotErrorTitle")
    },
    MY_SHOTS: {
      title: browser.i18n.getMessage("selfScreenshotErrorTitle")
    },
    generic: {
      title: browser.i18n.getMessage("genericErrorTitle"),
      info: browser.i18n.getMessage("genericErrorDetails"),
      showMessage: true
    }
  };

  communication.register("reportError", (sender, error) => {
    catcher.unhandled(error);
  });

  exports.showError = function (error) {
    let id = makeUuid();
    let popupMessage = error.popupMessage || "generic";
    if (! messages[popupMessage]) {
      popupMessage = "generic";
    }
    let title = messages[popupMessage].title;
    let message = messages[popupMessage].info || '';
    let showMessage = messages[popupMessage].showMessage;
    if (error.message && showMessage) {
      if (message) {
        message += "\n" + error.message;
      } else {
        message = error.message;
      }
    }
    browser.notifications.create(id, {
      type: "basic",
      // FIXME: need iconUrl for an image, see #2239
      title,
      message
    });
  };

  exports.reportError = function (e) {
    if (!analytics.getTelemetryPrefSync()) {
      console.error("Telemetry disabled. Not sending critical error:", e);
      return;
    }
    let dsn = auth.getSentryPublicDSN();
    if (! dsn) {
      console.warn("Error:", e);
      return;
    }
    if (! Raven.isSetup()) {
      Raven.config(dsn).install();
    }
    let exception = new Error(e.message);
    exception.stack = e.multilineStack || e.stack || undefined;
    let rest = {};
    for (let attr in e) {
      if (! ["name", "message", "stack", "multilineStack", "popupMessage", "version", "sentryPublicDSN", "help"].includes(attr)) {
        rest[attr] = e[attr];
      }
    }
    rest.stack = e.multilineStack || e.stack;
    Raven.captureException(exception, {
      logger: 'addon',
      tags: {version: e.version, category: e.popupMessage},
      message: exception.message,
      extra: rest
    });
  };

  catcher.registerHandler((errorObj) => {
    exports.showError(errorObj);
    if (! errorObj.noPopup) {
      exports.reportError(errorObj);
    }
  });

  return exports;
})();
<commit_msg>Fix #2353, put 3 second limit on error notifications<commit_after>/* globals analytics, browser, communication, makeUuid, Raven, catcher, auth */

window.errorpopup = (function () {
  let exports = {};

  // Do not show an error more than every ERROR_TIME_LIMIT milliseconds:
  const ERROR_TIME_LIMIT = 3000;

  let messages = {
    REQUEST_ERROR: {
      title: browser.i18n.getMessage("requestErrorTitle"),
      info: browser.i18n.getMessage("requestErrorDetails")
    },
    CONNECTION_ERROR: {
      title: browser.i18n.getMessage("connectionErrorTitle"),
      info: browser.i18n.getMessage("connectionErrorDetails")
    },
    LOGIN_ERROR: {
      title: browser.i18n.getMessage("requestErrorTitle"),
      info: browser.i18n.getMessage("loginErrorDetails")
    },
    LOGIN_CONNECTION_ERROR: {
      title: browser.i18n.getMessage("connectionErrorTitle"),
      info: browser.i18n.getMessage("connectionErrorDetails")
    },
    UNSHOOTABLE_PAGE: {
      title: browser.i18n.getMessage("unshootablePageErrorTitle"),
      info: browser.i18n.getMessage("unshootablePageErrorDetails")
    },
    SHOT_PAGE: {
      title: browser.i18n.getMessage("selfScreenshotErrorTitle")
    },
    MY_SHOTS: {
      title: browser.i18n.getMessage("selfScreenshotErrorTitle")
    },
    generic: {
      title: browser.i18n.getMessage("genericErrorTitle"),
      info: browser.i18n.getMessage("genericErrorDetails"),
      showMessage: true
    }
  };

  communication.register("reportError", (sender, error) => {
    catcher.unhandled(error);
  });

  let lastErrorTime;

  exports.showError = function (error) {
    if (lastErrorTime && (Date.now() - lastErrorTime) < ERROR_TIME_LIMIT) {
      return;
    }
    lastErrorTime = Date.now();
    let id = makeUuid();
    let popupMessage = error.popupMessage || "generic";
    if (! messages[popupMessage]) {
      popupMessage = "generic";
    }
    let title = messages[popupMessage].title;
    let message = messages[popupMessage].info || '';
    let showMessage = messages[popupMessage].showMessage;
    if (error.message && showMessage) {
      if (message) {
        message += "\n" + error.message;
      } else {
        message = error.message;
      }
    }
    browser.notifications.create(id, {
      type: "basic",
      // FIXME: need iconUrl for an image, see #2239
      title,
      message
    });
  };

  exports.reportError = function (e) {
    if (!analytics.getTelemetryPrefSync()) {
      console.error("Telemetry disabled. Not sending critical error:", e);
      return;
    }
    let dsn = auth.getSentryPublicDSN();
    if (! dsn) {
      console.warn("Error:", e);
      return;
    }
    if (! Raven.isSetup()) {
      Raven.config(dsn).install();
    }
    let exception = new Error(e.message);
    exception.stack = e.multilineStack || e.stack || undefined;
    let rest = {};
    for (let attr in e) {
      if (! ["name", "message", "stack", "multilineStack", "popupMessage", "version", "sentryPublicDSN", "help"].includes(attr)) {
        rest[attr] = e[attr];
      }
    }
    rest.stack = e.multilineStack || e.stack;
    Raven.captureException(exception, {
      logger: 'addon',
      tags: {version: e.version, category: e.popupMessage},
      message: exception.message,
      extra: rest
    });
  };

  catcher.registerHandler((errorObj) => {
    exports.showError(errorObj);
    if (! errorObj.noPopup) {
      exports.reportError(errorObj);
    }
  });

  return exports;
})();
<|endoftext|>


<commit_before>var TreeObservable = require("FuseJS/TreeObservable")

require("Polyfills/Window");
require("./ZonePatches");

var rootZone = Zone.current;

function shouldEmitProperty(key) {
	return !key.startsWith("_");
}

function isThenable(thing) {
	return thing instanceof Object
		&& typeof thing.then === "function";
}

function Model(initialState, stateInitializer)
{
	var stateToMeta = new Map();
	var idToMeta = new Map();
	var evaluatingDerivedProps = 0;
	var idEnumerator = 0;
	var store = this;

	instrument(null, this, initialState, stateInitializer)

	function instrument(parentMeta, node, state, stateInitializer)
	{
		if (stateInitializer instanceof Function) {
			runInZone(stateInitializer);
		}

		var meta = stateToMeta.get(state);

		if (meta instanceof Object) {
			if (parentMeta != null) {
				attachChild(parentMeta, meta);
			}
			return meta.node;
		}

		meta = {
			parents: [parentMeta],
			id: idEnumerator++,
			node: node,
			state: state,
			promises: {},
			isClass: false
		}

		idToMeta.set(meta.id, meta);
		stateToMeta.set(state, meta);
		node.__fuse_id = meta.id;
		node.__fuse_raw = state;

		//this is an internal variable but needs to be emitted for use in Uno, the primary use-case is for
		//the default $path for Navigation. Ideally this wouldn't be part of the IObject keys, but it works for now.
		if (state instanceof Object && !('$__fuse_classname' in state)) {
			node.$__fuse_classname = state.constructor.name;
		}

		// create zone lazily to avoid overhead when not needed
		var nodeZone = undefined;
		function prepareZone() {
			if (nodeZone != undefined) { return; }
			nodeZone = rootZone.fork({
				name: (parentMeta != null ? parentMeta.key : '(root)'),
				onInvokeTask: function(parentZoneDelegate, currentZone, targetZone, task, applyThis, applyArgs) {
					dirty();
					parentZoneDelegate.invokeTask(targetZone, task, applyThis, applyArgs);
				},
				onHandleError: function(error) {
					throw error;
				}
			})
		}

		meta.isClass = false;
		for (var k in state) {
			if (!shouldEmitProperty(k)) continue;
			var v = state[k];
			if (v instanceof Function) {
				node[k] = wrapFunction(v);
				state[k] = node[k];
				meta.isClass = true;
			}
			else if (isThenable(v)) {
				node[k] = null;
				dealWithPromise(k, v);
			}
			else if (v instanceof Array) {
				node[k] = instrument({meta: meta, key: k}, [], v);
			}
			else if (v instanceof Object) {
				node[k] = instrument({meta: meta, key: k}, {}, v);
			}
			else
			{
				node[k] = v;
			}
		}

		var propGetters = {}

		if (!(state instanceof Array)) {
			registerProps(state);
		}

		function dealWithPromise(key, prom) {
			if (meta.promises[key] !== prom) {
				meta.promises[key] = prom;
				prom.then(function(result) {
					if (meta.promises[key] === prom) {
						removeAsParentFrom(key, node[key])
						set(key, wrap(key, result));
					}
				})
			}
		}

		function registerProps(obj) {

			var keys = Object.getOwnPropertyNames(obj);
			for (var i in keys) {
				var p = keys[i];
				if (p === "constructor") { continue; }
				var value = state[p];

				if (value instanceof Function) {
					node[p] = wrapFunction(value);
					state[p] = node[p];
				}
				else {
					var descriptor = Object.getOwnPropertyDescriptor(obj, p);
					if (descriptor.get instanceof Function)
					{
						if (isThenable(value)) { node[p] = null; dealWithPromise(p, value); }
						else { node[p] = wrap(p, value); }
						propGetters[p] = descriptor.get;
					}
				}
			}

			// Include members from object's prototype chain (to allow ES6 classes)
			var proto = Object.getPrototypeOf(obj);
			if (proto && proto !== Object.prototype) { registerProps(proto); }
		}

		function hasParent() {
			return meta.parents.length > 0;
		}

		meta.evaluateDerivedProps = function(visited)
		{
			if (!hasParent()) return;

			isDerivedPropsDirty = false;

			if (visited.indexOf(node) !== -1) { return; }
			visited.push(node);

			for (var p in propGetters) {
				evaluatingDerivedProps++;
				try
				{
					var v = propGetters[p].call(state);
					if (isThenable(v)) {
						dealWithPromise(p, v);
					}
					else {
						set(p, wrap(p, v), true); // don't count this as a state change
					}
				}
				finally
				{
					evaluatingDerivedProps--;
				}
			}

			for (var parent of meta.parents) {
				if (parent !== null) {
					parent.meta.evaluateDerivedProps(visited);
				}
			}
		}

		function runInZone(func) {
			prepareZone();
			return nodeZone.run(func);
		}

		function wrapFunction(func) {
			var f = function() {
				var args = arguments;
				return runInZone(function() {
					dirty();
					return func.apply(state, args);
				});
			}
			f.__fuse_isWrapped = true;
			return f;
		}

		var isDirty = false;

		function dirty() {
			if (evaluatingDerivedProps !== 0) { return; }
			if (isDirty) { return; }
			isDirty = true;
			rootZone.run(function() {
				setTimeout(function() {
					meta.diff(new Set())
				}, 0)
			});
		}

		var changesDetected = 0;

		meta.diff = function(visited) {
			if (!(visited instanceof Set)) {
				throw new Error("Needs set of visited nodes");
			}
			if (visited.has(state)) {
				return;
			}
			visited.add(state);

			isDirty = false;

			if (!hasParent()) {
				// This object is no longer attached to the model tree,
				// we got this callback as an async remnant
				return;
			}

			if (state instanceof Array) {
				for (var i = 0; i < Math.min(state.length, node.length); i++) {
					if (isThenable(state[i])) { dealWithPromise(i, state[i]); }
					if (oldValueEquals(i, state[i])) continue;

					if (state.length > node.length) {
						insertAt(i, state[i]);
						i++;
					}
					else if (state.length < node.length) {
						removeRange(i, 1)
						i--;
					}
					else {
						removeAsParentFrom(i, node[i]);
						set(i, wrap(i, state[i]))
					}
				}

				if (state.length > node.length) {
					addRange(state.slice(node.length, state.length))
				}
				else if (state.length < node.length) {
					removeRange(i, node.length-state.length)
				}
			}
			else {
				for (var k in state) {
					if (!shouldEmitProperty(k)) continue;
					var v = state[k];
					update(k, v, visited);
				}
			}

			if (changesDetected > 0) {
				dirtyDerivedProps()
				changesDetected = 0
			}
		}

		var isDerivedPropsDirty = false;
		function dirtyDerivedProps() {
			if (isDerivedPropsDirty) { return; }
			isDerivedPropsDirty = true;
			rootZone.run(function() {
				setTimeout(function() { meta.evaluateDerivedProps([]) }, 0)
			});
		}

		function oldValueEquals(key, newValue) {
			if (newValue instanceof Object) {
				var keyMeta = stateToMeta.get(newValue);
				return keyMeta instanceof Object && meta.node[key].__fuse_id === keyMeta.id;
			}
			else {
				return node[key] === newValue;
			}
		}

		function isSameParentMeta(a, b) {
			return a && b
				&& a.meta === b.meta
				&& (""+a.key) === (""+b.key);
				// All object properties are strings (even array indices), however,
				// array indices will masquerade as numbers in some cases (but not all cases).
				// Therefore, we compare the keys as strings.
		}

		function attachChild(parentMeta, childMeta) {
			if (childMeta.parents.some(function(x) { return isSameParentMeta(x, parentMeta) })) {
				// Already a parent
				return;
			}

			childMeta.parents.push(parentMeta);
		}

		function removeAsParentFrom(key, node) {
			if (!(node instanceof Object)) { return; }
			var oldMeta = idToMeta.get(node.__fuse_id);
			if (oldMeta instanceof Object) {
				var parentMetaToDelete = {key:key, meta:meta};
				var thisIndex = oldMeta.parents.findIndex(function(x) { return isSameParentMeta(x, parentMetaToDelete) });
				if (thisIndex == -1) {
					throw new Error("Internal error: Attempted to detach child that is not attached to us");
				}
				oldMeta.parents.splice(thisIndex, 1);
				oldMeta.invalidatePath();

				if (oldMeta.parents.length === 0) {
					idToMeta.delete(node.__fuse_id);
					stateToMeta.delete(oldMeta.state);
				}
			}
		}

		if (node instanceof Array) {
			node.__fuse_replaceAll = function(values) {
				replaceAllInternal(state, values);
				replaceAllInternal(node, values);
				dirty();
			}
		}

		function replaceAllInternal(subject, values) {
			Array.prototype.splice.call(subject, 0);
			Array.prototype.push.apply(subject, values);
		}

		node.__fuse_requestChange = function(key, value) {
			var changeAccepted = true;
			if ('$requestChange' in state) {
				changeAccepted = state.$requestChange(key, value);
			}

			if (changeAccepted) {
				state[key] = value;
				setInternal(meta.getPath(), key, value);
			}

			meta.diff(new Set());
		}

		function update(key, value, visited)
		{
			if (value instanceof Function) {
				if (!value.__fuse_isWrapped) {
					state[key] = wrapFunction(value)
					set(key, state[key]);
				}
			}
			else if (isThenable(value)) {
				dealWithPromise(key, value);
			}
			else if (value instanceof Object) {
				var keyMeta = stateToMeta.get(value);
				var oldValue = meta.node[key];

				var newValue;
				if (keyMeta instanceof Object) {
					// Value is already instrumented, but re-instrument to add as parent
					newValue = instrument({meta: meta, key: key}, keyMeta, value);
				}
				else if (value instanceof Object) {
					newValue = instrument({meta: meta, key: key}, (value instanceof Array) ? [] : {}, value);
				}
				else {
					newValue = value;
				}

				if (oldValue instanceof Object &&
					keyMeta instanceof Object &&
					oldValue.__fuse_id === keyMeta.id
				)
				{
					if (!keyMeta.isClass) {
						keyMeta.diff(visited);
					}
				}
				else {
					removeAsParentFrom(key, oldValue);
					set(key, newValue);
				}
			}
			else if (value !== node[key])
			{
				set(key, value);
			}
		}

		var cachedPath = null;
		function getPath(visited) {
			if (cachedPath === null) { cachedPath = computePath(visited); }
			return cachedPath;
		}

		meta.getPath = getPath;
		meta.invalidatePath = function() { cachedPath = null; }

		// Finds a valid path to the root TreeObservable, if any
		function computePath(visited)
		{
			visited = visited || new Set();

			if (meta.parents.indexOf(null) >= 0) {
				// We are the root node
				return [];
			}

			for (var i = 0; i < meta.parents.length; i++) {
				var parent = meta.parents[i];

				if(visited.has(parent.meta))
					continue;

				visited.add(parent.meta);

				var arr = parent.meta.getPath(visited);
				if (arr instanceof Array) {
					return arr.concat(parent.key);
				}
			}
		}

		function set(key, value, omitStateChange)
		{
			var path = getPath();
			if (!setInternal(path, key, value, omitStateChange)) { return; }

			var argPath = path.concat(key, value instanceof Array ? [value] : value);
			TreeObservable.set.apply(store, argPath);
		}

		function removeRange(index, count) {
			for (var i = 0; i < count; i++) {
				removeAsParentFrom(index+i, node[index+i]);
			}
			node.splice(index, count);
			var removePath = getPath().concat(index);
			for (var i = 0; i < count; i++) {
				TreeObservable.removeAt.apply(store, removePath);
			}
			changesDetected++;
		}

		function wrap(key, item) {
			if (isThenable(item)) {
				dealWithPromise(key, item);
			}
			else if (item instanceof Array) {
				return instrument({meta: meta, key: key}, [], item)
			}
			else if (item instanceof Object) {
				return instrument({meta: meta, key: key}, {}, item)
			}
			else {
				return item
			}
		}

		function insertAt(index, item) {
			node.splice(index, 0, null);
			node[index] = item = wrap(index, item)

			TreeObservable.insertAt.apply(store, getPath().concat(index, item));
			changesDetected++;
		}

		function addRange(items) {
			for (var item of items) {
				var index = node.length;
				node.push(null);
				node[index] = item = wrap(index, item);
				TreeObservable.add.apply(store, getPath().concat(item));
			}

			changesDetected++;
		}

		function pathString(key) {
			var path = getPath();
			if (path.length === 0) { return key }
			if (path.length === 1) { return path[0] + "." + key; }
			return path.concat(key).join(".");
		}

		function setInternal(path, key, value, omitStateChange) {
			if (node[key] === value) { return false; }
			node[key] = value;

			if (!omitStateChange) { changesDetected++; }

			return true;
		}

		return node;
	}
}

Model.prototype = Object.create(TreeObservable.prototype);

module.exports = Model;
<commit_msg>Model: Fix dangling parent bug<commit_after>var TreeObservable = require("FuseJS/TreeObservable")

require("Polyfills/Window");
require("./ZonePatches");

var rootZone = Zone.current;

function shouldEmitProperty(key) {
	return !key.startsWith("_");
}

function isThenable(thing) {
	return thing instanceof Object
		&& typeof thing.then === "function";
}

function Model(initialState, stateInitializer)
{
	var stateToMeta = new Map();
	var idToMeta = new Map();
	var evaluatingDerivedProps = 0;
	var idEnumerator = 0;
	var store = this;

	instrument(null, this, initialState, stateInitializer)

	function instrument(parentMeta, node, state, stateInitializer)
	{
		if (stateInitializer instanceof Function) {
			runInZone(stateInitializer);
		}

		var meta = stateToMeta.get(state);

		if (meta instanceof Object) {
			if (parentMeta != null) {
				attachChild(parentMeta, meta);
			}
			return meta.node;
		}

		meta = {
			parents: [parentMeta],
			id: idEnumerator++,
			node: node,
			state: state,
			promises: {},
			isClass: false
		}

		idToMeta.set(meta.id, meta);
		stateToMeta.set(state, meta);
		node.__fuse_id = meta.id;
		node.__fuse_raw = state;

		//this is an internal variable but needs to be emitted for use in Uno, the primary use-case is for
		//the default $path for Navigation. Ideally this wouldn't be part of the IObject keys, but it works for now.
		if (state instanceof Object && !('$__fuse_classname' in state)) {
			node.$__fuse_classname = state.constructor.name;
		}

		// create zone lazily to avoid overhead when not needed
		var nodeZone = undefined;
		function prepareZone() {
			if (nodeZone != undefined) { return; }
			nodeZone = rootZone.fork({
				name: (parentMeta != null ? parentMeta.key : '(root)'),
				onInvokeTask: function(parentZoneDelegate, currentZone, targetZone, task, applyThis, applyArgs) {
					dirty();
					parentZoneDelegate.invokeTask(targetZone, task, applyThis, applyArgs);
				},
				onHandleError: function(error) {
					throw error;
				}
			})
		}

		meta.isClass = false;
		for (var k in state) {
			if (!shouldEmitProperty(k)) continue;
			var v = state[k];
			if (v instanceof Function) {
				node[k] = wrapFunction(v);
				state[k] = node[k];
				meta.isClass = true;
			}
			else if (isThenable(v)) {
				node[k] = null;
				dealWithPromise(k, v);
			}
			else if (v instanceof Array) {
				node[k] = instrument({meta: meta, key: k}, [], v);
			}
			else if (v instanceof Object) {
				node[k] = instrument({meta: meta, key: k}, {}, v);
			}
			else
			{
				node[k] = v;
			}
		}

		var propGetters = {}

		if (!(state instanceof Array)) {
			registerProps(state);
		}

		function dealWithPromise(key, prom) {
			if (meta.promises[key] !== prom) {
				meta.promises[key] = prom;
				prom.then(function(result) {
					if (meta.promises[key] === prom) {
						removeAsParentFrom(key, node[key])
						set(key, wrap(key, result));
					}
				})
			}
		}

		function registerProps(obj) {

			var keys = Object.getOwnPropertyNames(obj);
			for (var i in keys) {
				var p = keys[i];
				if (p === "constructor") { continue; }
				var value = state[p];

				if (value instanceof Function) {
					node[p] = wrapFunction(value);
					state[p] = node[p];
				}
				else {
					var descriptor = Object.getOwnPropertyDescriptor(obj, p);
					if (descriptor.get instanceof Function)
					{
						if (isThenable(value)) { node[p] = null; dealWithPromise(p, value); }
						else { node[p] = wrap(p, value); }
						propGetters[p] = descriptor.get;
					}
				}
			}

			// Include members from object's prototype chain (to allow ES6 classes)
			var proto = Object.getPrototypeOf(obj);
			if (proto && proto !== Object.prototype) { registerProps(proto); }
		}

		function hasParent() {
			return meta.parents.length > 0;
		}

		meta.evaluateDerivedProps = function(visited)
		{
			if (!hasParent()) return;

			isDerivedPropsDirty = false;

			if (visited.indexOf(node) !== -1) { return; }
			visited.push(node);

			for (var p in propGetters) {
				evaluatingDerivedProps++;
				try
				{
					var v = propGetters[p].call(state);
					if (isThenable(v)) {
						dealWithPromise(p, v);
					}
					else {
						set(p, wrap(p, v), true); // don't count this as a state change
					}
				}
				finally
				{
					evaluatingDerivedProps--;
				}
			}

			for (var parent of meta.parents) {
				if (parent !== null) {
					parent.meta.evaluateDerivedProps(visited);
				}
			}
		}

		function runInZone(func) {
			prepareZone();
			return nodeZone.run(func);
		}

		function wrapFunction(func) {
			var f = function() {
				var args = arguments;
				return runInZone(function() {
					dirty();
					return func.apply(state, args);
				});
			}
			f.__fuse_isWrapped = true;
			return f;
		}

		var isDirty = false;

		function dirty() {
			if (evaluatingDerivedProps !== 0) { return; }
			if (isDirty) { return; }
			isDirty = true;
			rootZone.run(function() {
				setTimeout(function() {
					meta.diff(new Set())
				}, 0)
			});
		}

		var changesDetected = 0;

		meta.diff = function(visited) {
			if (!(visited instanceof Set)) {
				throw new Error("Needs set of visited nodes");
			}
			if (visited.has(state)) {
				return;
			}
			visited.add(state);

			isDirty = false;

			if (!hasParent()) {
				// This object is no longer attached to the model tree,
				// we got this callback as an async remnant
				return;
			}

			if (state instanceof Array) {
				for (var i = 0; i < Math.min(state.length, node.length); i++) {
					if (isThenable(state[i])) { dealWithPromise(i, state[i]); }
					if (oldValueEquals(i, state[i])) continue;

					if (state.length > node.length) {
						insertAt(i, state[i]);
						i++;
					}
					else if (state.length < node.length) {
						removeRange(i, 1)
						i--;
					}
					else {
						removeAsParentFrom(i, node[i]);
						set(i, wrap(i, state[i]))
					}
				}

				if (state.length > node.length) {
					addRange(state.slice(node.length, state.length))
				}
				else if (state.length < node.length) {
					removeRange(i, node.length-state.length)
				}
			}
			else {
				for (var k in state) {
					if (!shouldEmitProperty(k)) continue;
					var v = state[k];
					update(k, v, visited);
				}
			}

			if (changesDetected > 0) {
				dirtyDerivedProps()
				changesDetected = 0
			}
		}

		var isDerivedPropsDirty = false;
		function dirtyDerivedProps() {
			if (isDerivedPropsDirty) { return; }
			isDerivedPropsDirty = true;
			rootZone.run(function() {
				setTimeout(function() { meta.evaluateDerivedProps([]) }, 0)
			});
		}

		function oldValueEquals(key, newValue) {
			if (newValue instanceof Object) {
				var keyMeta = stateToMeta.get(newValue);
				return keyMeta instanceof Object && meta.node[key].__fuse_id === keyMeta.id;
			}
			else {
				return node[key] === newValue;
			}
		}

		function isSameParentMeta(a, b) {
			return a && b
				&& a.meta === b.meta
				&& (""+a.key) === (""+b.key);
				// All object properties are strings (even array indices), however,
				// array indices will masquerade as numbers in some cases (but not all cases).
				// Therefore, we compare the keys as strings.
		}

		function attachChild(parentMeta, childMeta) {
			if (childMeta.parents.some(function(x) { return isSameParentMeta(x, parentMeta) })) {
				// Already a parent
				return;
			}

			childMeta.parents.push(parentMeta);
		}

		function removeAsParentFrom(key, node) {
			if (!(node instanceof Object)) { return; }
			var oldMeta = idToMeta.get(node.__fuse_id);
			if (oldMeta instanceof Object) {
				var parentMetaToDelete = {key:key, meta:meta};
				var thisIndex = oldMeta.parents.findIndex(function(x) { return isSameParentMeta(x, parentMetaToDelete) });
				if (thisIndex == -1) {
					throw new Error("Internal error: Attempted to detach child that is not attached to us");
				}
				oldMeta.parents.splice(thisIndex, 1);
				oldMeta.invalidatePath();

				if (oldMeta.parents.length === 0) {
					idToMeta.delete(node.__fuse_id);
					stateToMeta.delete(oldMeta.state);
				}
			}
		}

		if (node instanceof Array) {
			node.__fuse_replaceAll = function(values) {
				replaceAllInternal(state, values);
				replaceAllInternal(node, values);
				dirty();
			}
		}

		function replaceAllInternal(subject, values) {
			Array.prototype.splice.call(subject, 0);
			Array.prototype.push.apply(subject, values);
		}

		node.__fuse_requestChange = function(key, value) {
			var changeAccepted = true;
			if ('$requestChange' in state) {
				changeAccepted = state.$requestChange(key, value);
			}

			if (changeAccepted) {
				state[key] = value;
				setInternal(meta.getPath(), key, value);
			}

			meta.diff(new Set());
		}

		function update(key, value, visited)
		{
			if (value instanceof Function) {
				if (!value.__fuse_isWrapped) {
					state[key] = wrapFunction(value)
					set(key, state[key]);
				}
				return;
			}

			if (isThenable(value)) {
				dealWithPromise(key, value);
				return;
			}

			var oldNode = meta.node[key];

			if(!(value instanceof Object))
			{
				// We're dealing with a primitive value

				if (oldNode instanceof Object)
					removeAsParentFrom(key, oldNode);

				if (value !== oldNode)
					set(key, value);

				return;
			}

			var newNode;
			var newValueMeta = stateToMeta.get(value);
			if (newValueMeta instanceof Object)
			{
				if (oldNode instanceof Object && newValueMeta.id === oldNode.__fuse_id)
				{
					// The object has not changed since the last diff
					if (!newValueMeta.isClass) {
						// If it's a plain object (no methods that could trigger change detection),
						// perform change detection on its behalf
						newValueMeta.diff(visited);
					}
					return;
				}

				// Value is already instrumented, but re-instrument to add as parent
				newNode = instrument({meta: meta, key: key}, newValueMeta.node, value);
			}
			else {
				// This is an object we haven't seen before, so instrument it
				newNode = instrument({meta: meta, key: key}, (value instanceof Array) ? [] : {}, value);
			}

			if (oldNode instanceof Object)
				removeAsParentFrom(key, oldNode);

			set(key, newNode);
		}

		var cachedPath = null;
		function getPath(visited) {
			if (cachedPath === null) { cachedPath = computePath(visited); }
			return cachedPath;
		}

		meta.getPath = getPath;
		meta.invalidatePath = function() { cachedPath = null; }

		// Finds a valid path to the root TreeObservable, if any
		function computePath(visited)
		{
			visited = visited || new Set();

			if (meta.parents.indexOf(null) >= 0) {
				// We are the root node
				return [];
			}

			for (var i = 0; i < meta.parents.length; i++) {
				var parent = meta.parents[i];

				if(visited.has(parent.meta))
					continue;

				visited.add(parent.meta);

				var arr = parent.meta.getPath(visited);
				if (arr instanceof Array) {
					return arr.concat(parent.key);
				}
			}
		}

		function set(key, value, omitStateChange)
		{
			var path = getPath();
			if (!setInternal(path, key, value, omitStateChange)) { return; }

			var argPath = path.concat(key, value instanceof Array ? [value] : value);
			TreeObservable.set.apply(store, argPath);
		}

		function removeRange(index, count) {
			for (var i = 0; i < count; i++) {
				removeAsParentFrom(index+i, node[index+i]);
			}
			node.splice(index, count);
			var removePath = getPath().concat(index);
			for (var i = 0; i < count; i++) {
				TreeObservable.removeAt.apply(store, removePath);
			}
			changesDetected++;
		}

		function wrap(key, item) {
			if (isThenable(item)) {
				dealWithPromise(key, item);
			}
			else if (item instanceof Array) {
				return instrument({meta: meta, key: key}, [], item)
			}
			else if (item instanceof Object) {
				return instrument({meta: meta, key: key}, {}, item)
			}
			else {
				return item
			}
		}

		function insertAt(index, item) {
			node.splice(index, 0, null);
			node[index] = item = wrap(index, item)

			TreeObservable.insertAt.apply(store, getPath().concat(index, item));
			changesDetected++;
		}

		function addRange(items) {
			for (var item of items) {
				var index = node.length;
				node.push(null);
				node[index] = item = wrap(index, item);
				TreeObservable.add.apply(store, getPath().concat(item));
			}

			changesDetected++;
		}

		function pathString(key) {
			var path = getPath();
			if (path.length === 0) { return key }
			if (path.length === 1) { return path[0] + "." + key; }
			return path.concat(key).join(".");
		}

		function setInternal(path, key, value, omitStateChange) {
			if (node[key] === value) { return false; }
			node[key] = value;

			if (!omitStateChange) { changesDetected++; }

			return true;
		}

		return node;
	}
}

Model.prototype = Object.create(TreeObservable.prototype);

module.exports = Model;
<|endoftext|>


<commit_before>/*
The latest version of this package is available at:
<http://github.com/jantman/biweeklybudget>

################################################################################
Copyright 2016 Jason Antman <jason@jasonantman.com> <http://www.jasonantman.com>

    This file is part of biweeklybudget, also known as biweeklybudget.

    biweeklybudget is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    biweeklybudget is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with biweeklybudget.  If not, see <http://www.gnu.org/licenses/>.

The Copyright and Authors attributions contained herein may not be removed or
otherwise altered, except to add the Author attribution of a contributor to
this work. (Additional Terms pursuant to Section 7b of the AGPL v3)
################################################################################
While not legally required, I sincerely request that anyone who finds
bugs please submit them at <https://github.com/jantman/biweeklybudget> or
to me via email, and that you send any contributions or improvements
either as a pull request on GitHub, or to me via email.
################################################################################

AUTHORS:
Jason Antman <jason@jasonantman.com> <http://www.jasonantman.com>
################################################################################
*/

/**
 * Generic function to handle form submission with server-side validation.
 *
 * See the Python server-side code for further information.
 *
 * @param {string} container_id - The ID of the container element (div) that is
 *  the visual parent of the form. On successful submission, this element will
 *  be emptied and replaced with a success message.
 * @param {string} form_id - The ID of the form itself.
 * @param {string} post_url - Relative URL to post form data to.
 * @param {Object} dataTableObj - passed on to ``handleFormSubmitted()``
 */
function handleForm(container_id, form_id, post_url, dataTableObj) {
    var data = serializeForm(form_id);
    $('.formfeedback').remove();
    $('.has-error').each(function(index) { $(this).removeClass('has-error'); });
    $.ajax({
        type: "POST",
        url: post_url,
        data: data,
        success: function(data) {
            handleFormSubmitted(data, container_id, form_id, dataTableObj);
        },
        error: function(jqXHR, textStatus, errorThrown) {
            handleFormError(jqXHR, textStatus, errorThrown, container_id, form_id);
        }
    });
}

/**
 * Return True if ``functionToCheck`` is a function, False otherwise.
 *
 * From: http://stackoverflow.com/a/7356528/211734
 *
 * @param {Object} functionToCheck - The object to test.
 */
function isFunction(functionToCheck) {
 var getType = {};
 return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]';
}

/**
 * Handle the response from the API URL that the form data is POSTed to.
 *
 * This should either display a success message, or one or more error messages.
 *
 * @param {Object} data - response data
 * @param {string} container_id - the ID of the modal container on the page
 * @param {string} form_id - the ID of the form on the page
 * @param {Object} dataTableObj - A reference to the DataTable on the page, that
 *   needs to be refreshed. If null, reload the whole page. If a function, call
 *   that function. If false, do nothing.
 */
function handleFormSubmitted(data, container_id, form_id, dataTableObj) {
    if(data.hasOwnProperty('error_message')) {
        $('#' + container_id).prepend(
            '
' +
            'Server Error:  ' + data.error_message + '
');
    } else if (data.hasOwnProperty('errors')) {
        var form = $('#' + form_id);
        Object.keys(data.errors).forEach(function (key) {
            var elem = form.find('[name=' + key + ']');
            data.errors[key].forEach( function(msg) {
                elem.parent().append('
' + msg + '
');
                elem.parent().addClass('has-error');
            });
        });
    } else {
        $('#' + container_id).empty();
        $('#' + container_id).append('
' + data.success_message + '
');
        $('#modalSaveButton').hide();
        $('[data-dismiss="modal"]').click(function() {
            var oneitem_re = /\/\d+$/;
            if (dataTableObj === null || typeof dataTableObj == 'undefined') {
                // dataTableObj is null, refresh the page
                if (oneitem_re.test(window.location.href)) {
                    // don't reload if it will just show the modal again
                    var url = window.location.href.replace(/\/\d+$/, "");
                    window.location = url;
                } else {
                    location.reload();
                }
            } else if (isFunction(dataTableObj)) {
                // if it's a function, call it.
                dataTableObj();
            } else if (dataTableObj === false) {
                // do nothing
            } else if (dataTableObj !== null) {
                dataTableObj.api().ajax.reload();
            } else {
                console.log("ERROR: dataTableObj unknown type");
                console.log(dataTableObj);
            }
        });
    }
}

/**
 * Handle an error in the HTTP request to submit the form.
 */
function handleFormError(jqXHR, textStatus, errorThrown, container_id, form_id) {
    console.log("Form submission error: %s (%s)", textStatus, errorThrown);
    if($('#formStatus').length == 0) { $('#' + container_id).prepend('

'); }
    $('#formStatus').html(
        '
Error submitting ' +
        'form: ' + textStatus + ': ' + jqXHR.status + ' ' + errorThrown + '
'
    );
}

/**
 * Given the ID of a form, return an Object (hash/dict) of all data from it,
 * to POST to the server.
 *
 * @param {string} form_id - The ID of the form itself.
 */
function serializeForm(form_id) {
    var form = $('#' + form_id);
    var data = {};
    form.find('input').each(function(index) {
        var type = $(this).attr('type');
        if (type == 'text' || type == 'hidden') {
            data[$(this).attr('name')] = $(this).val();
        } else if (type == 'checkbox') {
            data[$(this).attr('name')] = $(this).prop('checked');
        } else if (type == 'radio') {
            if ($(this).prop('checked') === true) {
                if ($(this).prop('value') == 'true') {
                    data[$(this).attr('name')] = true;
                } else if ($(this).prop('value') == 'false') {
                    data[$(this).attr('name')] = false;
                } else {
                    data[$(this).attr('name')] = $(this).prop('value');
                }
            }
        } else {
            console.error(
                "Found form input with unknown type: %s (name=%s)",
                type, $(this).attr('name')
            );
        }
    });
    form.find('select').each(function(index) {
        data[$(this).attr('name')] = $(this).find(':selected').val();
    });
    return data;
}
<commit_msg>issue #43 - js handleFormSubmitted() - if dataTableObj is a function, pass response data to it<commit_after>/*
The latest version of this package is available at:
<http://github.com/jantman/biweeklybudget>

################################################################################
Copyright 2016 Jason Antman <jason@jasonantman.com> <http://www.jasonantman.com>

    This file is part of biweeklybudget, also known as biweeklybudget.

    biweeklybudget is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    biweeklybudget is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with biweeklybudget.  If not, see <http://www.gnu.org/licenses/>.

The Copyright and Authors attributions contained herein may not be removed or
otherwise altered, except to add the Author attribution of a contributor to
this work. (Additional Terms pursuant to Section 7b of the AGPL v3)
################################################################################
While not legally required, I sincerely request that anyone who finds
bugs please submit them at <https://github.com/jantman/biweeklybudget> or
to me via email, and that you send any contributions or improvements
either as a pull request on GitHub, or to me via email.
################################################################################

AUTHORS:
Jason Antman <jason@jasonantman.com> <http://www.jasonantman.com>
################################################################################
*/

/**
 * Generic function to handle form submission with server-side validation.
 *
 * See the Python server-side code for further information.
 *
 * @param {string} container_id - The ID of the container element (div) that is
 *  the visual parent of the form. On successful submission, this element will
 *  be emptied and replaced with a success message.
 * @param {string} form_id - The ID of the form itself.
 * @param {string} post_url - Relative URL to post form data to.
 * @param {Object} dataTableObj - passed on to ``handleFormSubmitted()``
 */
function handleForm(container_id, form_id, post_url, dataTableObj) {
    var data = serializeForm(form_id);
    $('.formfeedback').remove();
    $('.has-error').each(function(index) { $(this).removeClass('has-error'); });
    $.ajax({
        type: "POST",
        url: post_url,
        data: data,
        success: function(data) {
            handleFormSubmitted(data, container_id, form_id, dataTableObj);
        },
        error: function(jqXHR, textStatus, errorThrown) {
            handleFormError(jqXHR, textStatus, errorThrown, container_id, form_id);
        }
    });
}

/**
 * Return True if ``functionToCheck`` is a function, False otherwise.
 *
 * From: http://stackoverflow.com/a/7356528/211734
 *
 * @param {Object} functionToCheck - The object to test.
 */
function isFunction(functionToCheck) {
 var getType = {};
 return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]';
}

/**
 * Handle the response from the API URL that the form data is POSTed to.
 *
 * This should either display a success message, or one or more error messages.
 *
 * @param {Object} data - response data
 * @param {string} container_id - the ID of the modal container on the page
 * @param {string} form_id - the ID of the form on the page
 * @param {Object} dataTableObj - A reference to the DataTable on the page, that
 *   needs to be refreshed. If null, reload the whole page. If a function, call
 *   that function. If false, do nothing.
 */
function handleFormSubmitted(data, container_id, form_id, dataTableObj) {
    if(data.hasOwnProperty('error_message')) {
        $('#' + container_id).prepend(
            '
' +
            'Server Error:  ' + data.error_message + '
');
    } else if (data.hasOwnProperty('errors')) {
        var form = $('#' + form_id);
        Object.keys(data.errors).forEach(function (key) {
            var elem = form.find('[name=' + key + ']');
            data.errors[key].forEach( function(msg) {
                elem.parent().append('
' + msg + '
');
                elem.parent().addClass('has-error');
            });
        });
    } else {
        $('#' + container_id).empty();
        $('#' + container_id).append('
' + data.success_message + '
');
        $('#modalSaveButton').hide();
        $('[data-dismiss="modal"]').click(function() {
            var oneitem_re = /\/\d+$/;
            if (dataTableObj === null || typeof dataTableObj == 'undefined') {
                // dataTableObj is null, refresh the page
                if (oneitem_re.test(window.location.href)) {
                    // don't reload if it will just show the modal again
                    var url = window.location.href.replace(/\/\d+$/, "");
                    window.location = url;
                } else {
                    location.reload();
                }
            } else if (isFunction(dataTableObj)) {
                // if it's a function, call it.
                dataTableObj(data);
            } else if (dataTableObj === false) {
                // do nothing
            } else if (dataTableObj !== null) {
                dataTableObj.api().ajax.reload();
            } else {
                console.log("ERROR: dataTableObj unknown type");
                console.log(dataTableObj);
            }
        });
    }
}

/**
 * Handle an error in the HTTP request to submit the form.
 */
function handleFormError(jqXHR, textStatus, errorThrown, container_id, form_id) {
    console.log("Form submission error: %s (%s)", textStatus, errorThrown);
    if($('#formStatus').length == 0) { $('#' + container_id).prepend('

'); }
    $('#formStatus').html(
        '
Error submitting ' +
        'form: ' + textStatus + ': ' + jqXHR.status + ' ' + errorThrown + '
'
    );
}

/**
 * Given the ID of a form, return an Object (hash/dict) of all data from it,
 * to POST to the server.
 *
 * @param {string} form_id - The ID of the form itself.
 */
function serializeForm(form_id) {
    var form = $('#' + form_id);
    var data = {};
    form.find('input').each(function(index) {
        var type = $(this).attr('type');
        if (type == 'text' || type == 'hidden') {
            data[$(this).attr('name')] = $(this).val();
        } else if (type == 'checkbox') {
            data[$(this).attr('name')] = $(this).prop('checked');
        } else if (type == 'radio') {
            if ($(this).prop('checked') === true) {
                if ($(this).prop('value') == 'true') {
                    data[$(this).attr('name')] = true;
                } else if ($(this).prop('value') == 'false') {
                    data[$(this).attr('name')] = false;
                } else {
                    data[$(this).attr('name')] = $(this).prop('value');
                }
            }
        } else {
            console.error(
                "Found form input with unknown type: %s (name=%s)",
                type, $(this).attr('name')
            );
        }
    });
    form.find('select').each(function(index) {
        data[$(this).attr('name')] = $(this).find(':selected').val();
    });
    return data;
}
<|endoftext|>


<commit_before>function <%= componentName %>Controller( view, model ){

  // Controller constructor is called when an instance
  // of the html element is created

  // Controllers are stateless, the model is used to
  // store state data instead.

  // This class extends component/view/AbstractController
  // so we need to call the super contructor
  this.super( view );

  // Fires when an instance was inserted into the document
  this.onAttached = function(){
    // TODO: this is a stub
  };

  // Fires when an instance was inserted into the document,
  // after onAttached is called
  this.updateView = function(){
    // TODO: this is a stub
  };

  // Fires when an instance was removed from the document
  this.onAttributeChanged = function( attr, oldVal, newVal ){
    // TODO: this is a stub
    this.updateView();
  };

  // Fires when an attribute was added, removed, or updated
  this.onDetached = function(){
    // TODO: this is a stub
  };

}

module.exports = <%= componentName %>Controller;
<commit_msg>Added model parameter to super class call in component subgenerator<commit_after>function <%= componentName %>Controller( view, model ){

  // Controller constructor is called when an instance
  // of the html element is created

  // Controllers are stateless, the model is used to
  // store state data instead.

  // This class extends component/view/AbstractController
  // so we need to call the super contructor
  this.super( view, model );

  // Fires when an instance was inserted into the document
  this.onAttached = function(){
    // TODO: this is a stub
  };

  // Fires when an instance was inserted into the document,
  // after onAttached is called
  this.updateView = function(){
    // TODO: this is a stub
  };

  // Fires when an instance was removed from the document
  this.onAttributeChanged = function( attr, oldVal, newVal ){
    // TODO: this is a stub
    this.updateView();
  };

  // Fires when an attribute was added, removed, or updated
  this.onDetached = function(){
    // TODO: this is a stub
  };

}

module.exports = <%= componentName %>Controller;
<|endoftext|>