<commit_before>'use strict';

(function() {
  angular.module('archivesSpaceController', ['alertService', 'sipArrangeService', 'transferService', 'ui.bootstrap']).

  controller('ArchivesSpaceController', ['$scope', '$modal', 'Alert', 'ArchivesSpace', 'SipArrange', 'Transfer', function($scope, $modal, Alert, ArchivesSpace, SipArrange, Transfer) {
    var levels_of_description = ArchivesSpace.get_levels_of_description().$object;
      $scope.edit = function(node) {
        var form = $modal.open({
          templateUrl: 'archivesspace/form.html',
          controller: 'ArchivesSpaceEditController',
          controllerAs: 'form',
          resolve: {
            levels: function() {
              return levels_of_description;
            },
            title: function() {
              return node.title;
            },
            level: function() {
              return node.levelOfDescription;
            },
          },
        });
        form.result.then(function(result) {
          // Assign these to variables so we can restore them if the PUT fails
          var original_title = node.title;
          var original_level = node.levelOfDescription;

          node.title = result.title;
          node.levelOfDescription = result.level;
          // Any node with pending requests will be marked as non-editable
          node.request_pending = true;

          var on_success = function(response) {
            node.request_pending = false;
          };

          var on_failure = function(error) {
            // Restore the original title/level since the request failed
            node.title = original_title;
            node.levelOfDescription = original_level;
            node.request_pending = false;

            var title = node.title;
            if (node.identifier) {
              title = title + ' (' + node.identifier + ')';
            }

            Alert.alerts.push({
              type: 'danger',
              message: 'Unable to submit edits to record "' + title + '"; check dashboard logs.',
            });
          };

          ArchivesSpace.edit(node.id, node).then(on_success, on_failure);
        });
      };

      $scope.add_child = function(node) {
        var form = $modal.open({
          templateUrl: 'archivesspace/form.html',
          controller: 'ArchivesSpaceEditController',
          controllerAs: 'form',
          resolve: {
            levels: function() {
              return ArchivesSpace.get_levels_of_description().$object;
            },
            title: function() {
              return '';
            },
            level: function() {
              return '';
            },
          },
        });
        form.result.then(function(result) {
          result.levelOfDescription = result.level;

          var on_success = function(response) {
            result.id = response.id;
            result.parent = node;
            if (!node.has_children) {
              node.has_children = true;
              node.children = [];
              node.children_fetched = false;
            }
            node.children.push(result);
          };

          var on_failure = function(error) {
            Alert.alerts.append({
              type: 'danger',
              message: 'Unable to add new child record to record ' + node.id,
            });
          };

          ArchivesSpace.add_child(node.id, result).then(on_success, on_failure);
        });
      };

      $scope.options = {
        dirSelectable: true,
        equality: function(a, b) {
          if (a === undefined || b === undefined) {
            return false;
          } else if (a.id && b.id) {
            return a.id === b.id;
          } else {
            return a.path === b.path;
          }
        },
        isLeaf: function(node) {
          return !node.has_children;
        },
      };

      // Loads the children of the specified element,
      // along with arranged SIP contents that might exist
      var load_element_children = function(node) {
        $scope.loading = true;

        var on_failure_aspace = function(error) {
          Alert.alerts.push({
            type: 'danger',
            message: 'Unable to fetch record ' + node.id + ' from ArchivesSpace!',
          });
          $scope.loading = false;
        };

        var on_failure_arrange = function(error) {
          Alert.alerts.push({
            type: 'danger',
            message: 'Unable to fetch record ' + node.path + ' from Arrangement!',
          });
          $scope.loading = false;
        };

        node.children = [];

        if (node.id) {  // ArchivesSpace node
          ArchivesSpace.get_children(node.id).then(function(children) {
            children.map(function(element) { element.parent = node; });
            node.children = node.children.concat(children);
            node.children_fetched = true;
            $scope.loading = false;
          }, on_failure_aspace);

          // Also call into SIP arrange to see if there are any contents at this
          // level of description; if so, render them like any other ASpace objects
          ArchivesSpace.list_arrange_contents(node.id, node).then(function(entries) {
            node.children = node.children.concat(entries);
          });
        } else {  // SipArrange node
          SipArrange.list_contents(node.path, node).then(function(entries) {
            node.children = entries;
            node.children_fetched = true;
            $scope.loading = false;
          }, on_failure_arrange);
        }
      };

      $scope.refresh = function(node) {
        if (node) {
          load_element_children(node);
        } else {
          load_data();
        }
      };

      $scope.on_toggle = function(node, expanded) {
        if (!expanded || !node.has_children || node.children_fetched) {
          return;
        }

        load_element_children(node);
      };

      var load_data = function() {
        // TODO: handle failure to contact ArchivesSpace here;
        //       probably want to scope this to only happen if ASpace pane is opened.
        //       (The controller is always instantiated before the pane opens,
        //       so adding an alert here would always render even if ArchivesSpace
        //       wasn't clicked.)
        ArchivesSpace.all().then(function(data) {
          $scope.data = data;
        });
      };
      load_data();

      // Prevent a given file or its descendants from being dragged more than once
      var dragged_ids = [];
      var log_ids = function(file) {
        dragged_ids.push(file.id);
        if (file.children) {
          for (var i = 0; i < file.children.length; i++) {
            log_ids(file.children[i]);
          }
        }
      }

      // Filter the list of dragged files to contain only files with the "display"
      // parameter, so that only visibly selected files are dragged over
      var filter_files = function(file) {
        if (!file.display) {
          return {};
        }

        // Filter children recursively
        if (file.children) {
          var children = file.children;
          file.children = [];
          angular.forEach(children, function(child) {
            child = filter_files(child);
            // Omit empty objects, or directories whose children have all been filtered out
            if (child.id && child.type === 'file' || (child.children && child.children.length > 0)) {
              file.children.push(child);
            }
          });
        }

        return file;
      };

      $scope.drop = function(unused, ui) {
        if ($scope.loading) {
          return;
        }

        var file_uuid = ui.draggable.attr('uuid');
        var file = Transfer.id_map[file_uuid];
        if (dragged_ids.indexOf(file_uuid) !== -1) {
          alert('File "' + file.title + '" already added.');
          return;
        }

        if (this.id) {  // ArchivesSpace information object
          return drop_onto_aspace_record.apply(this, [file]);
        } else {  // file within arrange
          return drop_onto_arrange_directory.apply(this, [file]);
        }
      };

      var drop_onto_aspace_record = function(file) {
        var self = this;

        // create a deep copy of the file and its children so we don't mutate
        // the copies used in the backlog
        file = filter_files(_.extend({}, file));
        if (!file.id) {
          return;
        }

        var on_directory_creation = function(response) {
          // Add path to parent
          self.path = response.path;
          ArchivesSpace.copy_to_arrange(self.id, '/originals/' + source_path).then(on_copy);
        };

        var on_copy = function() {
          if ($scope.expanded_nodes.indexOf(self) === -1) {
            $scope.expanded_nodes.push(self);
            // expanded event will not fire if the node was programmatically expanded - this loads children
            $scope.on_toggle(self, true);
          } else {
            // Reload the directory to reflect new contents
            load_element_children(self);
          }
        };

        var source_path;
        if (file.type === 'file') {
          source_path = file.relative_path;
        } else {
          source_path = file.relative_path + '/';
        }

        $scope.$apply(function() {
          $scope.loading = true;

          ArchivesSpace.create_directory(self.id).then(on_directory_creation);
        });
      };

      var drop_onto_arrange_directory = function(file) {
        var self = this;

        // Reload the directory to reflect new contents
        var on_success = function(entries) {
          self.parent.children = entries;
        };

        $scope.$apply(function() {
          SipArrange.list_contents('/arrange/' + self.parent.path, self.parent.parent).then(on_success);
        });
      };

      $scope.remove = function(node) {
        if ($scope.loading) {
          return;
        }

        var on_delete = function(element) {
          // `node.parent` is undefined if this is a root-level directory
          var siblings = node.parent ? node.parent.children : $scope.data.children;
          var idx = siblings.indexOf(node);
          if (idx !== -1){
            siblings.splice(idx, 1);
          }
          $scope.selected = undefined;
        };

        // TODO is this a reliable way to tell nodes apart?
        if (node.id) { // ArchivesSpace node
          ArchivesSpace.remove(node.id).then(on_delete);
        } else {  // SipArrange node
          SipArrange.remove(node.path).then(on_delete);
        }
      };

      $scope.finalize_arrangement = function(node) {
        var on_success = function() {
          Alert.alerts.push({
            type: 'success',
            message: 'Successfully started SIP from record "' + node.title + '"',
          });
        };
        var on_failure = function(error) {
          var message;
          // error.message won't be defined if this returned an HTML 500
          if (error.message && error.message.startsWith('No SIP Arrange mapping')) {
            message = 'Unable to start SIP; no files arranged into record "' + node.title + '".';
          } else {
            message = 'Unable to start SIP; check dashboard logs.';
          }
          Alert.alerts.push({
            type: 'danger',
            message: message,
          });
        };

        ArchivesSpace.start_sip(node.id).then(on_success, on_failure);
      };
    }]).

  controller('ArchivesSpaceEditController', ['$modalInstance', 'levels', 'level', 'title', function($modalInstance, levels, level, title) {
    var vm = this;

    vm.levels = levels;
    vm.level = level;
    vm.title = title;

    vm.ok = function() {
      $modalInstance.close(vm);
    };
    vm.cancel = function() {
      $modalInstance.dismiss('cancel');
    };
  }]);
})();
<commit_msg>AS: fix drop onto record with no children<commit_after>'use strict';

(function() {
  angular.module('archivesSpaceController', ['alertService', 'sipArrangeService', 'transferService', 'ui.bootstrap']).

  controller('ArchivesSpaceController', ['$scope', '$modal', 'Alert', 'ArchivesSpace', 'SipArrange', 'Transfer', function($scope, $modal, Alert, ArchivesSpace, SipArrange, Transfer) {
    var levels_of_description = ArchivesSpace.get_levels_of_description().$object;
      $scope.edit = function(node) {
        var form = $modal.open({
          templateUrl: 'archivesspace/form.html',
          controller: 'ArchivesSpaceEditController',
          controllerAs: 'form',
          resolve: {
            levels: function() {
              return levels_of_description;
            },
            title: function() {
              return node.title;
            },
            level: function() {
              return node.levelOfDescription;
            },
          },
        });
        form.result.then(function(result) {
          // Assign these to variables so we can restore them if the PUT fails
          var original_title = node.title;
          var original_level = node.levelOfDescription;

          node.title = result.title;
          node.levelOfDescription = result.level;
          // Any node with pending requests will be marked as non-editable
          node.request_pending = true;

          var on_success = function(response) {
            node.request_pending = false;
          };

          var on_failure = function(error) {
            // Restore the original title/level since the request failed
            node.title = original_title;
            node.levelOfDescription = original_level;
            node.request_pending = false;

            var title = node.title;
            if (node.identifier) {
              title = title + ' (' + node.identifier + ')';
            }

            Alert.alerts.push({
              type: 'danger',
              message: 'Unable to submit edits to record "' + title + '"; check dashboard logs.',
            });
          };

          ArchivesSpace.edit(node.id, node).then(on_success, on_failure);
        });
      };

      $scope.add_child = function(node) {
        var form = $modal.open({
          templateUrl: 'archivesspace/form.html',
          controller: 'ArchivesSpaceEditController',
          controllerAs: 'form',
          resolve: {
            levels: function() {
              return ArchivesSpace.get_levels_of_description().$object;
            },
            title: function() {
              return '';
            },
            level: function() {
              return '';
            },
          },
        });
        form.result.then(function(result) {
          result.levelOfDescription = result.level;

          var on_success = function(response) {
            result.id = response.id;
            result.parent = node;
            if (!node.has_children) {
              node.has_children = true;
              node.children = [];
              node.children_fetched = false;
            }
            node.children.push(result);
          };

          var on_failure = function(error) {
            Alert.alerts.append({
              type: 'danger',
              message: 'Unable to add new child record to record ' + node.id,
            });
          };

          ArchivesSpace.add_child(node.id, result).then(on_success, on_failure);
        });
      };

      $scope.options = {
        dirSelectable: true,
        equality: function(a, b) {
          if (a === undefined || b === undefined) {
            return false;
          } else if (a.id && b.id) {
            return a.id === b.id;
          } else {
            return a.path === b.path;
          }
        },
        isLeaf: function(node) {
          return !node.has_children;
        },
      };

      // Loads the children of the specified element,
      // along with arranged SIP contents that might exist
      var load_element_children = function(node) {
        $scope.loading = true;

        var on_failure_aspace = function(error) {
          Alert.alerts.push({
            type: 'danger',
            message: 'Unable to fetch record ' + node.id + ' from ArchivesSpace!',
          });
          $scope.loading = false;
        };

        var on_failure_arrange = function(error) {
          Alert.alerts.push({
            type: 'danger',
            message: 'Unable to fetch record ' + node.path + ' from Arrangement!',
          });
          $scope.loading = false;
        };

        node.children = [];

        if (node.id) {  // ArchivesSpace node
          ArchivesSpace.get_children(node.id).then(function(children) {
            children.map(function(element) { element.parent = node; });
            node.children = node.children.concat(children);
            node.children_fetched = true;
            $scope.loading = false;
          }, on_failure_aspace);

          // Also call into SIP arrange to see if there are any contents at this
          // level of description; if so, render them like any other ASpace objects
          ArchivesSpace.list_arrange_contents(node.id, node).then(function(entries) {
            node.children = node.children.concat(entries);
          });
        } else {  // SipArrange node
          SipArrange.list_contents(node.path, node).then(function(entries) {
            node.children = entries;
            node.children_fetched = true;
            $scope.loading = false;
          }, on_failure_arrange);
        }
      };

      $scope.refresh = function(node) {
        if (node) {
          load_element_children(node);
        } else {
          load_data();
        }
      };

      $scope.on_toggle = function(node, expanded) {
        if (!expanded || !node.has_children || node.children_fetched) {
          return;
        }

        load_element_children(node);
      };

      var load_data = function() {
        // TODO: handle failure to contact ArchivesSpace here;
        //       probably want to scope this to only happen if ASpace pane is opened.
        //       (The controller is always instantiated before the pane opens,
        //       so adding an alert here would always render even if ArchivesSpace
        //       wasn't clicked.)
        ArchivesSpace.all().then(function(data) {
          $scope.data = data;
        });
      };
      load_data();

      // Prevent a given file or its descendants from being dragged more than once
      var dragged_ids = [];
      var log_ids = function(file) {
        dragged_ids.push(file.id);
        if (file.children) {
          for (var i = 0; i < file.children.length; i++) {
            log_ids(file.children[i]);
          }
        }
      }

      // Filter the list of dragged files to contain only files with the "display"
      // parameter, so that only visibly selected files are dragged over
      var filter_files = function(file) {
        if (!file.display) {
          return {};
        }

        // Filter children recursively
        if (file.children) {
          var children = file.children;
          file.children = [];
          angular.forEach(children, function(child) {
            child = filter_files(child);
            // Omit empty objects, or directories whose children have all been filtered out
            if (child.id && child.type === 'file' || (child.children && child.children.length > 0)) {
              file.children.push(child);
            }
          });
        }

        return file;
      };

      $scope.drop = function(unused, ui) {
        if ($scope.loading) {
          return;
        }

        var file_uuid = ui.draggable.attr('uuid');
        var file = Transfer.id_map[file_uuid];
        if (dragged_ids.indexOf(file_uuid) !== -1) {
          alert('File "' + file.title + '" already added.');
          return;
        }

        if (this.id) {  // ArchivesSpace information object
          return drop_onto_aspace_record.apply(this, [file]);
        } else {  // file within arrange
          return drop_onto_arrange_directory.apply(this, [file]);
        }
      };

      var drop_onto_aspace_record = function(file) {
        var self = this;

        // create a deep copy of the file and its children so we don't mutate
        // the copies used in the backlog
        file = filter_files(_.extend({}, file));
        if (!file.id) {
          return;
        }

        var on_directory_creation = function(response) {
          // Add path to parent
          self.path = response.path;
          self.has_children = true;
          self.children = [];
          self.children_fetched = false;
          ArchivesSpace.copy_to_arrange(self.id, '/originals/' + source_path).then(on_copy);
        };

        var on_copy = function() {
          if ($scope.expanded_nodes.indexOf(self) === -1) {
            $scope.expanded_nodes.push(self);
            // expanded event will not fire if the node was programmatically expanded - this loads children
            $scope.on_toggle(self, true);
          } else {
            // Reload the directory to reflect new contents
            load_element_children(self);
          }
        };

        var source_path;
        if (file.type === 'file') {
          source_path = file.relative_path;
        } else {
          source_path = file.relative_path + '/';
        }

        $scope.$apply(function() {
          $scope.loading = true;

          ArchivesSpace.create_directory(self.id).then(on_directory_creation);
        });
      };

      var drop_onto_arrange_directory = function(file) {
        var self = this;

        // Reload the directory to reflect new contents
        var on_success = function(entries) {
          self.parent.children = entries;
        };

        $scope.$apply(function() {
          SipArrange.list_contents('/arrange/' + self.parent.path, self.parent.parent).then(on_success);
        });
      };

      $scope.remove = function(node) {
        if ($scope.loading) {
          return;
        }

        var on_delete = function(element) {
          // `node.parent` is undefined if this is a root-level directory
          var siblings = node.parent ? node.parent.children : $scope.data.children;
          var idx = siblings.indexOf(node);
          if (idx !== -1){
            siblings.splice(idx, 1);
          }
          $scope.selected = undefined;
        };

        // TODO is this a reliable way to tell nodes apart?
        if (node.id) { // ArchivesSpace node
          ArchivesSpace.remove(node.id).then(on_delete);
        } else {  // SipArrange node
          SipArrange.remove(node.path).then(on_delete);
        }
      };

      $scope.finalize_arrangement = function(node) {
        var on_success = function() {
          Alert.alerts.push({
            type: 'success',
            message: 'Successfully started SIP from record "' + node.title + '"',
          });
        };
        var on_failure = function(error) {
          var message;
          // error.message won't be defined if this returned an HTML 500
          if (error.message && error.message.startsWith('No SIP Arrange mapping')) {
            message = 'Unable to start SIP; no files arranged into record "' + node.title + '".';
          } else {
            message = 'Unable to start SIP; check dashboard logs.';
          }
          Alert.alerts.push({
            type: 'danger',
            message: message,
          });
        };

        ArchivesSpace.start_sip(node.id).then(on_success, on_failure);
      };
    }]).

  controller('ArchivesSpaceEditController', ['$modalInstance', 'levels', 'level', 'title', function($modalInstance, levels, level, title) {
    var vm = this;

    vm.levels = levels;
    vm.level = level;
    vm.title = title;

    vm.ok = function() {
      $modalInstance.close(vm);
    };
    vm.cancel = function() {
      $modalInstance.dismiss('cancel');
    };
  }]);
})();
<|endoftext|>


<commit_before>import Ember from 'ember';
import ClickOutside from '../mixins/click-outside';
const { on } = Ember;
const { next } = Ember.run;

export default Ember.Component.extend(ClickOutside, {
    tagName: 'div',
    classNames: ['ember-select-it'],

    content: [],
    value: null,
    required: false,
    placeHolder: null,
    optionLabelPath: 'name',
    optionIdPath: null,
    disabled: false,

    _isOpen: false,
    _searchResults: null,

    _valueObj: Ember.computed('value', 'optionIdPath', 'content', function() {
        if(this.get('optionIdPath')) {
            return this.get('content').findBy(this.get('optionIdPath'), this.get('value'));
        }

        return this.get('value');
    }),

    displayValue: Ember.computed('value', function() {
        return this.get('_valueObj') ? this.get('_valueObj')[this.get('optionLabelPath')] : "";
    }),

    searchValueChanged: Ember.observer('searchValue', function() {
        this.searchAction();
    }),

    showPlaceHolder: Ember.computed('value', 'placeHolder', function() {
        if(this.get('placeHolder') && !this.get('value')) {
            return true;
        }

        return false;
    }),

    searchAction() {
        var searchValue = this.get('searchValue');
        var optionLabelPath = this.get('optionLabelPath');
        var content = this.get('content').filter(function(obj) {
            var re = new RegExp(searchValue, 'gi');
            return obj[optionLabelPath].match(re);
        });

        var value = this.get('value');

        content = content.map(function(obj) {
            return Ember.Object.create({
                value: obj[optionLabelPath],
                isHighlighted: false,
                isSelected: (value && value.id === obj.id),
                obj: obj
            });
        });

        if(!this.get('required') && !searchValue) {
            content.unshiftObject(Ember.Object.create({
                value: this.get('placeHolder'),
                isHighlighted: false,
                isSelected: false,
                obj: null
            }));
        }

        if(content.objectAt(0)) {
            content.objectAt(0).set('isHighlighted', true);
        }

        this.set('_searchResults', content);
    },

    onKeydown: function(e) {
        var _searchResults = this.get('_searchResults');
        var next, current;

        // arrow down
        if(e.which === 40) {
            if(!this.get('_isOpen')) {
                this.openDropDown();
            }
            if(!_searchResults) {
                return;
            }

            current = _searchResults.findBy('isHighlighted', true);
            next = null;

            if(current) {
                current.set('isHighlighted', false);
                next = _searchResults.objectAt(_searchResults.indexOf(current) + 1);
                if(!next) {
                    next = _searchResults.objectAt(0);
                }
            } else {
                next = _searchResults.objectAt(0);
            }

            next.set('isHighlighted', true);

        // arrow up
        } else if(e.which === 38) {
            if(!this.get('_isOpen')) {
                this.openDropDown();
            }
            if(!_searchResults) {
                return;
            }

            current = _searchResults.findBy('isHighlighted', true);
            next = null;

            if(current) {
                current.set('isHighlighted', false);
                next = _searchResults.objectAt(_searchResults.indexOf(current) - 1);
                if(!next) {
                    next = _searchResults.objectAt(_searchResults.length - 1);
                }
            } else {
                next = _searchResults.objectAt(_searchResults.length - 1);
            }

            next.set('isHighlighted', true);

        // enter
        } else if(e.which === 13) {
            if(this.get('_isOpen')) {
                current = _searchResults.findBy('isHighlighted', true);
                this.send('itemSelected', current);
            } else {
                this.openDropDown();
            }
        // esc or tap
        } else if(e.which === 27 || e.which === 9) {
            if(this.get('_isOpen')) {
                this.closeDropDown();
            }
        }
    }.on('keyDown'),

    closeDropDown() {
        this.set('_isOpen', false);
        this.set('_searchResults', null);
        var self = this;
        Ember.run.next(function() {
            self.$('.select-it-display').focus();
        });

    },

    openDropDown() {
        if(this.get('disabled')) {
            return;
        }

        this.set('_isOpen', true);
        this.set('searchValue', this.get('displayValue'));
        this.searchAction();
        var self = this;
        Ember.run.next(function() {
            self.$('input.select-it-search').focus().select();
        });
    },

    clickOutside(e) {
        const exceptSelector = '.select-it-ignore';
        if (exceptSelector && Ember.$(e.target).closest(exceptSelector).length > 0) {
            return;
        }

        if(this.get('_isOpen')) {
            this.closeDropDown();
        }
    },

    _attachClickOutsideHandler: on('didInsertElement', function() {
        next(this, this.addClickOutsideListener);
    }),

    _removeClickOutsideHandler: on('willDestroyElement', function() {
        this.removeClickOutsideListener();
    }),

    actions: {
        openSearch() {
            this.openDropDown();
        },
        closeSearch() {
            this.closeDropDown();
        },
        itemHighlighted(result) {
            var current = this.get('_searchResults').findBy('isHighlighted', true);
            current.set('isHighlighted', false);

            result.toggleProperty('isHighlighted');
        },
        itemSelected(result) {
            if(this.get('optionIdPath') && result.obj) {
                this.set('value', result.obj[this.get('optionIdPath')]);
            } else {
                this.set('value', result.obj);
            }
            this.closeDropDown();
        }
    }
});
<commit_msg>Renamed option<commit_after>import Ember from 'ember';
import ClickOutside from '../mixins/click-outside';
const { on } = Ember;
const { next } = Ember.run;

export default Ember.Component.extend(ClickOutside, {
    tagName: 'div',
    classNames: ['ember-select-it'],

    content: [],
    value: null,
    required: false,
    placeHolder: null,
    optionLabelPath: 'name',
    optionValuePath: null,
    disabled: false,

    _isOpen: false,
    _searchResults: null,

    _valueObj: Ember.computed('value', 'optionValuePath', 'content', function() {
        if(this.get('optionValuePath')) {
            return this.get('content').findBy(this.get('optionValuePath'), this.get('value'));
        }

        return this.get('value');
    }),

    displayValue: Ember.computed('value', function() {
        return this.get('_valueObj') ? this.get('_valueObj')[this.get('optionLabelPath')] : "";
    }),

    searchValueChanged: Ember.observer('searchValue', function() {
        this.searchAction();
    }),

    showPlaceHolder: Ember.computed('value', 'placeHolder', function() {
        if(this.get('placeHolder') && !this.get('value')) {
            return true;
        }

        return false;
    }),

    searchAction() {
        var searchValue = this.get('searchValue');
        var optionLabelPath = this.get('optionLabelPath');
        var content = this.get('content').filter(function(obj) {
            var re = new RegExp(searchValue, 'gi');
            return obj[optionLabelPath].match(re);
        });

        var value = this.get('value');

        content = content.map(function(obj) {
            return Ember.Object.create({
                value: obj[optionLabelPath],
                isHighlighted: false,
                isSelected: (value && value.id === obj.id),
                obj: obj
            });
        });

        if(!this.get('required') && !searchValue) {
            content.unshiftObject(Ember.Object.create({
                value: this.get('placeHolder'),
                isHighlighted: false,
                isSelected: false,
                obj: null
            }));
        }

        if(content.objectAt(0)) {
            content.objectAt(0).set('isHighlighted', true);
        }

        this.set('_searchResults', content);
    },

    onKeydown: function(e) {
        var _searchResults = this.get('_searchResults');
        var next, current;

        // arrow down
        if(e.which === 40) {
            if(!this.get('_isOpen')) {
                this.openDropDown();
            }
            if(!_searchResults) {
                return;
            }

            current = _searchResults.findBy('isHighlighted', true);
            next = null;

            if(current) {
                current.set('isHighlighted', false);
                next = _searchResults.objectAt(_searchResults.indexOf(current) + 1);
                if(!next) {
                    next = _searchResults.objectAt(0);
                }
            } else {
                next = _searchResults.objectAt(0);
            }

            next.set('isHighlighted', true);

        // arrow up
        } else if(e.which === 38) {
            if(!this.get('_isOpen')) {
                this.openDropDown();
            }
            if(!_searchResults) {
                return;
            }

            current = _searchResults.findBy('isHighlighted', true);
            next = null;

            if(current) {
                current.set('isHighlighted', false);
                next = _searchResults.objectAt(_searchResults.indexOf(current) - 1);
                if(!next) {
                    next = _searchResults.objectAt(_searchResults.length - 1);
                }
            } else {
                next = _searchResults.objectAt(_searchResults.length - 1);
            }

            next.set('isHighlighted', true);

        // enter
        } else if(e.which === 13) {
            if(this.get('_isOpen')) {
                current = _searchResults.findBy('isHighlighted', true);
                this.send('itemSelected', current);
            } else {
                this.openDropDown();
            }
        // esc or tap
        } else if(e.which === 27 || e.which === 9) {
            if(this.get('_isOpen')) {
                this.closeDropDown();
            }
        }
    }.on('keyDown'),

    closeDropDown() {
        this.set('_isOpen', false);
        this.set('_searchResults', null);
        var self = this;
        Ember.run.next(function() {
            self.$('.select-it-display').focus();
        });

    },

    openDropDown() {
        if(this.get('disabled')) {
            return;
        }

        this.set('_isOpen', true);
        this.set('searchValue', this.get('displayValue'));
        this.searchAction();
        var self = this;
        Ember.run.next(function() {
            self.$('input.select-it-search').focus().select();
        });
    },

    clickOutside(e) {
        const exceptSelector = '.select-it-ignore';
        if (exceptSelector && Ember.$(e.target).closest(exceptSelector).length > 0) {
            return;
        }

        if(this.get('_isOpen')) {
            this.closeDropDown();
        }
    },

    _attachClickOutsideHandler: on('didInsertElement', function() {
        next(this, this.addClickOutsideListener);
    }),

    _removeClickOutsideHandler: on('willDestroyElement', function() {
        this.removeClickOutsideListener();
    }),

    actions: {
        openSearch() {
            this.openDropDown();
        },
        closeSearch() {
            this.closeDropDown();
        },
        itemHighlighted(result) {
            var current = this.get('_searchResults').findBy('isHighlighted', true);
            current.set('isHighlighted', false);

            result.toggleProperty('isHighlighted');
        },
        itemSelected(result) {
            if(this.get('optionValuePath') && result.obj) {
                this.set('value', result.obj[this.get('optionValuePath')]);
            } else {
                this.set('value', result.obj);
            }
            this.closeDropDown();
        }
    }
});
<|endoftext|>


<commit_before>// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

import { action, computed, observable, observe, extendObservable } from 'mobx'
import storage from '../../IO/storage'
import SuiteState from './SuiteState'
import TestState from './TestState'
import PlaybackState from './PlaybackState'
import ModalState from './ModalState'
import Command from '../../models/Command'
import Manager from '../../../plugin/manager'
import WindowSession from '../../IO/window-session'
import BackgroundRecorder from '../../IO/SideeX/recorder'

class UiState {
  views = ['Tests', 'Test suites', 'Executing']
  @observable
  lastViewSelection = new Map()
  @observable
  selectedView = 'Tests'
  @observable
  selectedTest = {}
  @observable
  selectedCommand = null
  @observable
  filterTerm = ''
  @observable
  clipboard = null
  @observable
  isRecording = false
  @observable
  lastRecordedCommand = null
  @observable
  isSelectingTarget = false
  @observable
  windowHeight = window.innerHeight
  @observable
  consoleHeight = 200
  @observable
  minConsoleHeight = 30
  @observable
  minContentHeight = 460
  @observable
  minNavigationWidth = 180
  @observable
  maxNavigationWidth = 250
  @observable
  navigationWidth = 180
  @observable
  pristineCommand = new Command()
  @observable
  lastFocus = {}
  @observable
  options = {}
  @observable
  pauseNotificationSent = false

  constructor() {
    this.suiteStates = {}
    this.testStates = {}
    this.filterFunction = this.filterFunction.bind(this)
    this.observePristine()
    storage.get().then(data => {
      if (
        data.consoleSize !== undefined &&
        data.consoleSize >= this.minConsoleHeight
      ) {
        this.storedConsoleHeight =
          data.consoleSize > this.minConsoleHeight
            ? data.consoleSize
            : this.windowHeight - this.minContentHeight
        this.resizeConsole(data.consoleSize)
      }
      if (
        data.navigationSize !== undefined &&
        data.navigationSize >= this.minNavigationWidth
      ) {
        this.resizeNavigation(data.navigationSize)
      }
      if (data.options) {
        this.setOptions(data.options)
      }
    })
    this.recorder = new BackgroundRecorder(WindowSession)
    this.windowSession = WindowSession
  }

  @action.bound
  setProject(project) {
    this._project = project
  }

  @computed
  get project() {
    return this._project
  }

  @computed
  get filteredTests() {
    return this._project.tests.filter(this.filterFunction)
  }

  @computed
  get baseUrl() {
    return this._project.url
  }

  @computed
  get maxContentHeight() {
    return this.windowHeight - this.minConsoleHeight
  }

  @computed
  get gaugeSpeed() {
    const value = PlaybackState.maxDelay - PlaybackState.delay
    const speed = Math.ceil((value / PlaybackState.maxDelay) * 5)
    return speed ? speed : 1
  }

  @action.bound
  _changeView(view, ignoreCache) {
    this.lastViewSelection.set(this.selectedView, this.selectedTest)
    const lastSelection = this.lastViewSelection.get(view)
    if (!ignoreCache && lastSelection) {
      this.selectTest(
        lastSelection.test,
        lastSelection.suite,
        lastSelection.stack
      )
    }
    this.selectedView = view
  }

  @action.bound
  async changeView(view, ignoreCache) {
    if (this.isRecording && view !== this.selectedView) {
      const choseChange = await ModalState.showAlert({
        title: 'Stop recording',
        description:
          'Are you sure you would like to stop recording, and change views?',
        confirmLabel: 'stop recording',
        cancelLabel: 'cancel',
      })
      if (choseChange) {
        await this.stopRecording()
        this._changeView(view, ignoreCache)
      }
    } else {
      this._changeView(view, ignoreCache)
    }
  }

  @action.bound
  clearViewCache() {
    this.lastViewSelection.clear()
  }

  @action.bound
  copyToClipboard(item) {
    this.clipboard = item
  }

  @action.bound
  pasteFromClipboard(index) {
    if (this.clipboard && this.displayedTest) {
      const newCommand = this.clipboard.clone()
      this.displayedTest.insertCommandAt(newCommand, index)
    }
  }

  @computed
  get displayedTest() {
    return this.selectedTest.stack !== undefined &&
      this.selectedTest.stack >= 0 &&
      this.selectedTest.stack < PlaybackState.callstack.length
      ? PlaybackState.callstack[this.selectedTest.stack].callee
      : this.selectedTest.test
  }

  @action.bound
  _selectTest(test, suite, stack, override) {
    if (!PlaybackState.isPlaying || PlaybackState.paused || override) {
      const _test =
        stack !== undefined && stack >= 0
          ? PlaybackState.callstack[stack].callee
          : test
      if (_test && _test !== this.displayedTest) {
        this.selectedTest = {
          test,
          suite,
          stack: stack >= 0 ? stack : undefined,
          state: this.getTestState(test),
        }
        if (PlaybackState.isPlaying && !PlaybackState.paused) {
          this.selectCommand(undefined)
        } else if (_test && _test.commands.length) {
          let command = this.selectedTest.state.selectedCommand
          command = command ? command : _test.commands[0]
          this.selectCommand(command)
        } else if (_test && !_test.commands.length) {
          this.selectCommand(this.pristineCommand)
        } else {
          this.selectCommand(undefined)
        }
      }
    }
  }

  @action.bound
  async selectTest(test, suite, stack, override) {
    if (this.isRecording && test !== this.selectedTest.test) {
      const choseSelect = await ModalState.showAlert({
        title: 'Stop recording',
        description:
          'Are you sure you would like to stop recording, and select a different test?',
        confirmLabel: 'stop recording',
        cancelLabel: 'cancel',
      })
      if (choseSelect) {
        await this.stopRecording()
        this._selectTest(test, suite, stack, override)
      }
    } else {
      this._selectTest(test, suite, stack, override)
    }
  }

  @action.bound
  selectTestByIndex(index, suite) {
    const selectTestInArray = (index, tests) =>
      index >= 0 && index < tests.length ? tests[index] : undefined
    if (this.selectedView === 'Tests') {
      const test = selectTestInArray(index, this.filteredTests)
      if (test) this.selectTest(test)
    } else if (this.selectedView === 'Test suites') {
      const suiteState = this.getSuiteState(suite)
      const tests = suiteState.filteredTests.get()
      const test = selectTestInArray(index, tests)
      const suiteIndex = this._project.suites.indexOf(suite)
      if (test) {
        suiteState.setOpen(true)
        this.selectTest(test, suite)
      } else if (suiteIndex > 0 && index < 0) {
        const previousSuite = this._project.suites[suiteIndex - 1]
        this.selectTestByIndex(
          this.getSuiteState(previousSuite).filteredTests.get().length - 1,
          previousSuite
        )
      } else if (
        suiteIndex + 1 < this._project.suites.length &&
        index >= tests.length
      ) {
        const nextSuite = this._project.suites[suiteIndex + 1]
        this.selectTestByIndex(0, nextSuite)
      }
    } else if (this.selectedView === 'Executing') {
      const test = selectTestInArray(index, PlaybackState.testsToRun)
      if (test) {
        let stack = undefined
        if (
          PlaybackState.callstack.length &&
          PlaybackState.stackCaller === test
        ) {
          stack = PlaybackState.callstack.length - 1
        }
        this.selectTest(test, suite, stack)
      }
    }
  }

  @action.bound
  selectCommand(command, opts = { isCommandTarget: false }) {
    if (
      !PlaybackState.isPlaying ||
      PlaybackState.paused ||
      opts.isCommandTarget
    ) {
      this.selectedTest.state.selectedCommand = command
      this.selectedCommand = command
    }
  }

  @action.bound
  selectCommandByIndex(index, opts) {
    const test = this.displayedTest
    if (index >= 0 && index < test.commands.length) {
      this.selectCommand(test.commands[index], opts)
    } else if (index === test.commands.length) {
      this.selectCommand(this.pristineCommand, opts)
    }
  }

  @action.bound
  selectNextCommand(opts = { from: undefined, isCommandTarget: false }) {
    this.selectCommandByIndex(this.nextCommandIndex(opts.from), opts)
  }

  nextCommandIndex(targetCommand) {
    const commands = this.displayedTest.commands
    if (targetCommand) return commands.indexOf(targetCommand) + 1
    else return commands.indexOf(this.selectedCommand) + 1
  }

  @action.bound
  changeFilter(term) {
    this.filterTerm = term
  }

  @action.bound
  async toggleRecord(isInvalid) {
    await (this.isRecording
      ? this.stopRecording()
      : this.startRecording(isInvalid))
  }

  @action.bound
  beforeRecording() {}

  @action.bound
  async startRecording(isInvalid) {
    let startingUrl = this.baseUrl
    if (!startingUrl) {
      startingUrl = await ModalState.selectBaseUrl({
        isInvalid,
        confirmLabel: 'Start recording',
      })
    }
    try {
      await this.recorder.attach(startingUrl)
      this._setRecordingState(true)
      this.lastRecordedCommand = null
      await this.emitRecordingState()
    } catch (err) {
      ModalState.showAlert({
        title: 'Could not start recording',
        description: err ? err.message : undefined,
      })
    }
  }

  nameNewTest(isEnabled = true) {
    const test = this.selectedTest.test
    if (isEnabled && test.name === 'Untitled' && !test.nameDialogShown) {
      ModalState.renameTest(test.name, { isNewTest: true }).then(name => {
        test.setName(name)
      })
      test.nameDialogShown = true
    }
  }

  @action.bound
  async stopRecording(opts = { nameNewTest: true }) {
    await this.recorder.detach()
    this._setRecordingState(false)
    await this.emitRecordingState()
    await this.nameNewTest(opts.nameNewTest)
  }

  // Do not call this method directly, use start and stop
  @action.bound
  _setRecordingState(isRecording) {
    this.isRecording = isRecording
  }

  @action.bound
  emitRecordingState() {
    Manager.emitMessage({
      action: 'event',
      event: this.isRecording ? 'recordingStarted' : 'recordingStopped',
      options: {
        testName: this.selectedTest.test
          ? this.selectedTest.test.name
          : undefined,
      },
    })
  }

  @action.bound
  setSelectingTarget(isSelecting) {
    this.isSelectingTarget = isSelecting
  }

  @action.bound
  resizeConsole(height) {
    const maxConsoleHeight = this.windowHeight - this.minContentHeight
    const tmpHeight = height > maxConsoleHeight ? maxConsoleHeight : height

    this.storedConsoleHeight =
      height > this.minConsoleHeight + 20 ? height : this.storedConsoleHeight
    this.consoleHeight =
      height > this.minConsoleHeight ? tmpHeight : this.minConsoleHeight

    storage.set({
      consoleSize: this.consoleHeight,
    })
  }

  @action.bound
  maximizeConsole() {
    this.resizeConsole(this.windowHeight - this.minContentHeight)
  }

  @action.bound
  minimizeConsole() {
    this.resizeConsole(this.minConsoleHeight)
  }

  @action.bound
  restoreConsoleSize() {
    this.resizeConsole(this.storedConsoleHeight)
  }

  @action.bound
  toggleConsole() {
    if (this.consoleHeight === this.minConsoleHeight) {
      this.restoreConsoleSize()
    } else {
      this.minimizeConsole()
    }
  }

  @action.bound
  setWindowHeight(height) {
    this.windowHeight = height
    if (this.windowHeight - this.consoleHeight < this.minContentHeight) {
      this.resizeConsole(this.windowHeight - this.minContentHeight)
    }
  }

  @action.bound
  resizeNavigation(width) {
    this.navigationWidth = width
    storage.set({
      navigationSize: this.navigationWidth,
    })
  }

  @action.bound
  setOptions(options) {
    extendObservable(this.options, options)
    storage.set({
      options: this.options,
    })
  }

  @action.bound
  observePristine() {
    this.pristineDisposer = observe(this.pristineCommand, () => {
      this.pristineDisposer()
      this.displayedTest.addCommand(this.pristineCommand)
      this.pristineCommand = new Command()
      this.observePristine()
    })
  }

  @action.bound
  focusNavigation() {
    if (this.lastFocus.navigation) {
      this.lastFocus.navigation()
    }
  }

  @action.bound
  focusEditor() {
    if (this.lastFocus.editor) {
      this.lastFocus.editor()
    }
  }

  @action.bound
  setSectionFocus(section, cb) {
    this.lastFocus[section] = cb
  }

  getSuiteState(suite) {
    if (!this.suiteStates[suite.id]) {
      this.suiteStates[suite.id] = new SuiteState(this, suite)
    }

    return this.suiteStates[suite.id]
  }

  getTestState(test) {
    if (!this.testStates[test.id]) {
      this.testStates[test.id] = new TestState(test)
    }

    return this.testStates[test.id]
  }

  filterFunction({ name }) {
    return name.toLowerCase().indexOf(this.filterTerm.toLowerCase()) !== -1
  }

  setUrl(url, addToCache) {
    this._project.setUrl(url)
    if (addToCache) this._project.addUrl(url)
  }

  @action.bound
  projectChanged() {
    this.selectedTest = {}
    this.selectedCommand = null
    this.filterTerm = ''
    this.clipboard = null
    this.isRecording = false
    this.suiteStates = {}
    this.clearTestStates()
    this.selectTest(this._project.tests[0])
    WindowSession.closeAllOpenedWindows()
  }

  @action.bound
  clearTestStates() {
    Object.values(this.testStates).forEach(state => state.dispose())
    this.testStates = {}
  }

  isSaved() {
    return this._project.modified === false
  }

  @action.bound
  saved() {
    Object.values(this.testStates).forEach(state => {
      state.modified = false
    })
    this._project.setModified(false)
  }
}

if (!window._state) window._state = new UiState()

export default window._state
<commit_msg>increase max navigation width<commit_after>// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

import { action, computed, observable, observe, extendObservable } from 'mobx'
import storage from '../../IO/storage'
import SuiteState from './SuiteState'
import TestState from './TestState'
import PlaybackState from './PlaybackState'
import ModalState from './ModalState'
import Command from '../../models/Command'
import Manager from '../../../plugin/manager'
import WindowSession from '../../IO/window-session'
import BackgroundRecorder from '../../IO/SideeX/recorder'

class UiState {
  views = ['Tests', 'Test suites', 'Executing']
  @observable
  lastViewSelection = new Map()
  @observable
  selectedView = 'Tests'
  @observable
  selectedTest = {}
  @observable
  selectedCommand = null
  @observable
  filterTerm = ''
  @observable
  clipboard = null
  @observable
  isRecording = false
  @observable
  lastRecordedCommand = null
  @observable
  isSelectingTarget = false
  @observable
  windowHeight = window.innerHeight
  @observable
  consoleHeight = 200
  @observable
  minConsoleHeight = 30
  @observable
  minContentHeight = 460
  @observable
  minNavigationWidth = 180
  @observable
  maxNavigationWidth = 350
  @observable
  navigationWidth = 180
  @observable
  pristineCommand = new Command()
  @observable
  lastFocus = {}
  @observable
  options = {}
  @observable
  pauseNotificationSent = false

  constructor() {
    this.suiteStates = {}
    this.testStates = {}
    this.filterFunction = this.filterFunction.bind(this)
    this.observePristine()
    storage.get().then(data => {
      if (
        data.consoleSize !== undefined &&
        data.consoleSize >= this.minConsoleHeight
      ) {
        this.storedConsoleHeight =
          data.consoleSize > this.minConsoleHeight
            ? data.consoleSize
            : this.windowHeight - this.minContentHeight
        this.resizeConsole(data.consoleSize)
      }
      if (
        data.navigationSize !== undefined &&
        data.navigationSize >= this.minNavigationWidth
      ) {
        this.resizeNavigation(data.navigationSize)
      }
      if (data.options) {
        this.setOptions(data.options)
      }
    })
    this.recorder = new BackgroundRecorder(WindowSession)
    this.windowSession = WindowSession
  }

  @action.bound
  setProject(project) {
    this._project = project
  }

  @computed
  get project() {
    return this._project
  }

  @computed
  get filteredTests() {
    return this._project.tests.filter(this.filterFunction)
  }

  @computed
  get baseUrl() {
    return this._project.url
  }

  @computed
  get maxContentHeight() {
    return this.windowHeight - this.minConsoleHeight
  }

  @computed
  get gaugeSpeed() {
    const value = PlaybackState.maxDelay - PlaybackState.delay
    const speed = Math.ceil((value / PlaybackState.maxDelay) * 5)
    return speed ? speed : 1
  }

  @action.bound
  _changeView(view, ignoreCache) {
    this.lastViewSelection.set(this.selectedView, this.selectedTest)
    const lastSelection = this.lastViewSelection.get(view)
    if (!ignoreCache && lastSelection) {
      this.selectTest(
        lastSelection.test,
        lastSelection.suite,
        lastSelection.stack
      )
    }
    this.selectedView = view
  }

  @action.bound
  async changeView(view, ignoreCache) {
    if (this.isRecording && view !== this.selectedView) {
      const choseChange = await ModalState.showAlert({
        title: 'Stop recording',
        description:
          'Are you sure you would like to stop recording, and change views?',
        confirmLabel: 'stop recording',
        cancelLabel: 'cancel',
      })
      if (choseChange) {
        await this.stopRecording()
        this._changeView(view, ignoreCache)
      }
    } else {
      this._changeView(view, ignoreCache)
    }
  }

  @action.bound
  clearViewCache() {
    this.lastViewSelection.clear()
  }

  @action.bound
  copyToClipboard(item) {
    this.clipboard = item
  }

  @action.bound
  pasteFromClipboard(index) {
    if (this.clipboard && this.displayedTest) {
      const newCommand = this.clipboard.clone()
      this.displayedTest.insertCommandAt(newCommand, index)
    }
  }

  @computed
  get displayedTest() {
    return this.selectedTest.stack !== undefined &&
      this.selectedTest.stack >= 0 &&
      this.selectedTest.stack < PlaybackState.callstack.length
      ? PlaybackState.callstack[this.selectedTest.stack].callee
      : this.selectedTest.test
  }

  @action.bound
  _selectTest(test, suite, stack, override) {
    if (!PlaybackState.isPlaying || PlaybackState.paused || override) {
      const _test =
        stack !== undefined && stack >= 0
          ? PlaybackState.callstack[stack].callee
          : test
      if (_test && _test !== this.displayedTest) {
        this.selectedTest = {
          test,
          suite,
          stack: stack >= 0 ? stack : undefined,
          state: this.getTestState(test),
        }
        if (PlaybackState.isPlaying && !PlaybackState.paused) {
          this.selectCommand(undefined)
        } else if (_test && _test.commands.length) {
          let command = this.selectedTest.state.selectedCommand
          command = command ? command : _test.commands[0]
          this.selectCommand(command)
        } else if (_test && !_test.commands.length) {
          this.selectCommand(this.pristineCommand)
        } else {
          this.selectCommand(undefined)
        }
      }
    }
  }

  @action.bound
  async selectTest(test, suite, stack, override) {
    if (this.isRecording && test !== this.selectedTest.test) {
      const choseSelect = await ModalState.showAlert({
        title: 'Stop recording',
        description:
          'Are you sure you would like to stop recording, and select a different test?',
        confirmLabel: 'stop recording',
        cancelLabel: 'cancel',
      })
      if (choseSelect) {
        await this.stopRecording()
        this._selectTest(test, suite, stack, override)
      }
    } else {
      this._selectTest(test, suite, stack, override)
    }
  }

  @action.bound
  selectTestByIndex(index, suite) {
    const selectTestInArray = (index, tests) =>
      index >= 0 && index < tests.length ? tests[index] : undefined
    if (this.selectedView === 'Tests') {
      const test = selectTestInArray(index, this.filteredTests)
      if (test) this.selectTest(test)
    } else if (this.selectedView === 'Test suites') {
      const suiteState = this.getSuiteState(suite)
      const tests = suiteState.filteredTests.get()
      const test = selectTestInArray(index, tests)
      const suiteIndex = this._project.suites.indexOf(suite)
      if (test) {
        suiteState.setOpen(true)
        this.selectTest(test, suite)
      } else if (suiteIndex > 0 && index < 0) {
        const previousSuite = this._project.suites[suiteIndex - 1]
        this.selectTestByIndex(
          this.getSuiteState(previousSuite).filteredTests.get().length - 1,
          previousSuite
        )
      } else if (
        suiteIndex + 1 < this._project.suites.length &&
        index >= tests.length
      ) {
        const nextSuite = this._project.suites[suiteIndex + 1]
        this.selectTestByIndex(0, nextSuite)
      }
    } else if (this.selectedView === 'Executing') {
      const test = selectTestInArray(index, PlaybackState.testsToRun)
      if (test) {
        let stack = undefined
        if (
          PlaybackState.callstack.length &&
          PlaybackState.stackCaller === test
        ) {
          stack = PlaybackState.callstack.length - 1
        }
        this.selectTest(test, suite, stack)
      }
    }
  }

  @action.bound
  selectCommand(command, opts = { isCommandTarget: false }) {
    if (
      !PlaybackState.isPlaying ||
      PlaybackState.paused ||
      opts.isCommandTarget
    ) {
      this.selectedTest.state.selectedCommand = command
      this.selectedCommand = command
    }
  }

  @action.bound
  selectCommandByIndex(index, opts) {
    const test = this.displayedTest
    if (index >= 0 && index < test.commands.length) {
      this.selectCommand(test.commands[index], opts)
    } else if (index === test.commands.length) {
      this.selectCommand(this.pristineCommand, opts)
    }
  }

  @action.bound
  selectNextCommand(opts = { from: undefined, isCommandTarget: false }) {
    this.selectCommandByIndex(this.nextCommandIndex(opts.from), opts)
  }

  nextCommandIndex(targetCommand) {
    const commands = this.displayedTest.commands
    if (targetCommand) return commands.indexOf(targetCommand) + 1
    else return commands.indexOf(this.selectedCommand) + 1
  }

  @action.bound
  changeFilter(term) {
    this.filterTerm = term
  }

  @action.bound
  async toggleRecord(isInvalid) {
    await (this.isRecording
      ? this.stopRecording()
      : this.startRecording(isInvalid))
  }

  @action.bound
  beforeRecording() {}

  @action.bound
  async startRecording(isInvalid) {
    let startingUrl = this.baseUrl
    if (!startingUrl) {
      startingUrl = await ModalState.selectBaseUrl({
        isInvalid,
        confirmLabel: 'Start recording',
      })
    }
    try {
      await this.recorder.attach(startingUrl)
      this._setRecordingState(true)
      this.lastRecordedCommand = null
      await this.emitRecordingState()
    } catch (err) {
      ModalState.showAlert({
        title: 'Could not start recording',
        description: err ? err.message : undefined,
      })
    }
  }

  nameNewTest(isEnabled = true) {
    const test = this.selectedTest.test
    if (isEnabled && test.name === 'Untitled' && !test.nameDialogShown) {
      ModalState.renameTest(test.name, { isNewTest: true }).then(name => {
        test.setName(name)
      })
      test.nameDialogShown = true
    }
  }

  @action.bound
  async stopRecording(opts = { nameNewTest: true }) {
    await this.recorder.detach()
    this._setRecordingState(false)
    await this.emitRecordingState()
    await this.nameNewTest(opts.nameNewTest)
  }

  // Do not call this method directly, use start and stop
  @action.bound
  _setRecordingState(isRecording) {
    this.isRecording = isRecording
  }

  @action.bound
  emitRecordingState() {
    Manager.emitMessage({
      action: 'event',
      event: this.isRecording ? 'recordingStarted' : 'recordingStopped',
      options: {
        testName: this.selectedTest.test
          ? this.selectedTest.test.name
          : undefined,
      },
    })
  }

  @action.bound
  setSelectingTarget(isSelecting) {
    this.isSelectingTarget = isSelecting
  }

  @action.bound
  resizeConsole(height) {
    const maxConsoleHeight = this.windowHeight - this.minContentHeight
    const tmpHeight = height > maxConsoleHeight ? maxConsoleHeight : height

    this.storedConsoleHeight =
      height > this.minConsoleHeight + 20 ? height : this.storedConsoleHeight
    this.consoleHeight =
      height > this.minConsoleHeight ? tmpHeight : this.minConsoleHeight

    storage.set({
      consoleSize: this.consoleHeight,
    })
  }

  @action.bound
  maximizeConsole() {
    this.resizeConsole(this.windowHeight - this.minContentHeight)
  }

  @action.bound
  minimizeConsole() {
    this.resizeConsole(this.minConsoleHeight)
  }

  @action.bound
  restoreConsoleSize() {
    this.resizeConsole(this.storedConsoleHeight)
  }

  @action.bound
  toggleConsole() {
    if (this.consoleHeight === this.minConsoleHeight) {
      this.restoreConsoleSize()
    } else {
      this.minimizeConsole()
    }
  }

  @action.bound
  setWindowHeight(height) {
    this.windowHeight = height
    if (this.windowHeight - this.consoleHeight < this.minContentHeight) {
      this.resizeConsole(this.windowHeight - this.minContentHeight)
    }
  }

  @action.bound
  resizeNavigation(width) {
    this.navigationWidth = width
    storage.set({
      navigationSize: this.navigationWidth,
    })
  }

  @action.bound
  setOptions(options) {
    extendObservable(this.options, options)
    storage.set({
      options: this.options,
    })
  }

  @action.bound
  observePristine() {
    this.pristineDisposer = observe(this.pristineCommand, () => {
      this.pristineDisposer()
      this.displayedTest.addCommand(this.pristineCommand)
      this.pristineCommand = new Command()
      this.observePristine()
    })
  }

  @action.bound
  focusNavigation() {
    if (this.lastFocus.navigation) {
      this.lastFocus.navigation()
    }
  }

  @action.bound
  focusEditor() {
    if (this.lastFocus.editor) {
      this.lastFocus.editor()
    }
  }

  @action.bound
  setSectionFocus(section, cb) {
    this.lastFocus[section] = cb
  }

  getSuiteState(suite) {
    if (!this.suiteStates[suite.id]) {
      this.suiteStates[suite.id] = new SuiteState(this, suite)
    }

    return this.suiteStates[suite.id]
  }

  getTestState(test) {
    if (!this.testStates[test.id]) {
      this.testStates[test.id] = new TestState(test)
    }

    return this.testStates[test.id]
  }

  filterFunction({ name }) {
    return name.toLowerCase().indexOf(this.filterTerm.toLowerCase()) !== -1
  }

  setUrl(url, addToCache) {
    this._project.setUrl(url)
    if (addToCache) this._project.addUrl(url)
  }

  @action.bound
  projectChanged() {
    this.selectedTest = {}
    this.selectedCommand = null
    this.filterTerm = ''
    this.clipboard = null
    this.isRecording = false
    this.suiteStates = {}
    this.clearTestStates()
    this.selectTest(this._project.tests[0])
    WindowSession.closeAllOpenedWindows()
  }

  @action.bound
  clearTestStates() {
    Object.values(this.testStates).forEach(state => state.dispose())
    this.testStates = {}
  }

  isSaved() {
    return this._project.modified === false
  }

  @action.bound
  saved() {
    Object.values(this.testStates).forEach(state => {
      state.modified = false
    })
    this._project.setModified(false)
  }
}

if (!window._state) window._state = new UiState()

export default window._state
<|endoftext|>


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

'use strict';

/*
* When adding modules to the namespace, ensure that they are added in alphabetical order according to module name.
*/

// MODULES //

var setReadOnly = require( '@stdlib/utils/define-read-only-property' );

// MAIN //

/**
* Namespace.
*
* @namespace ns
*/
var ns = {};

/**
* @name arraylike2object
* @memberof ns
* @readonly
* @type {Function}
* @see {@link module:@stdlib/array/base/arraylike2object}
*/
setReadOnly( ns, 'arraylike2object', require( '@stdlib/array/base/arraylike2object' ) );

/**
* @name filled
* @memberof ns
* @readonly
* @type {Function}
* @see {@link module:@stdlib/array/base/filled}
*/
setReadOnly( ns, 'filled', require( '@stdlib/array/base/filled' ) );

/**
* @name incrspace
* @memberof ns
* @readonly
* @type {Function}
* @see {@link module:@stdlib/array/base/incrspace}
*/
setReadOnly( ns, 'incrspace', require( '@stdlib/array/base/incrspace' ) );

/**
* @name linspace
* @memberof ns
* @readonly
* @type {Function}
* @see {@link module:@stdlib/array/base/linspace}
*/
setReadOnly( ns, 'linspace', require( '@stdlib/array/base/linspace' ) );

/**
* @name logspace
* @memberof ns
* @readonly
* @type {Function}
* @see {@link module:@stdlib/array/base/logspace}
*/
setReadOnly( ns, 'logspace', require( '@stdlib/array/base/logspace' ) );

/**
* @name ones
* @memberof ns
* @readonly
* @type {Function}
* @see {@link module:@stdlib/array/base/ones}
*/
setReadOnly( ns, 'ones', require( '@stdlib/array/base/ones' ) );

/**
* @name zeros
* @memberof ns
* @readonly
* @type {Function}
* @see {@link module:@stdlib/array/base/zeros}
*/
setReadOnly( ns, 'zeros', require( '@stdlib/array/base/zeros' ) );

// EXPORTS //

module.exports = ns;
<commit_msg>Update namespace<commit_after>/**
* @license Apache-2.0
*
* Copyright (c) 2021 The Stdlib Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*    http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

'use strict';

/*
* When adding modules to the namespace, ensure that they are added in alphabetical order according to module name.
*/

// MODULES //

var setReadOnly = require( '@stdlib/utils/define-read-only-property' );

// MAIN //

/**
* Namespace.
*
* @namespace ns
*/
var ns = {};

/**
* @name arraylike2object
* @memberof ns
* @readonly
* @type {Function}
* @see {@link module:@stdlib/array/base/arraylike2object}
*/
setReadOnly( ns, 'arraylike2object', require( '@stdlib/array/base/arraylike2object' ) );

/**
* @name filled
* @memberof ns
* @readonly
* @type {Function}
* @see {@link module:@stdlib/array/base/filled}
*/
setReadOnly( ns, 'filled', require( '@stdlib/array/base/filled' ) );

/**
* @name filledBy
* @memberof ns
* @readonly
* @type {Function}
* @see {@link module:@stdlib/array/base/filled-by}
*/
setReadOnly( ns, 'filledBy', require( '@stdlib/array/base/filled-by' ) );

/**
* @name incrspace
* @memberof ns
* @readonly
* @type {Function}
* @see {@link module:@stdlib/array/base/incrspace}
*/
setReadOnly( ns, 'incrspace', require( '@stdlib/array/base/incrspace' ) );

/**
* @name linspace
* @memberof ns
* @readonly
* @type {Function}
* @see {@link module:@stdlib/array/base/linspace}
*/
setReadOnly( ns, 'linspace', require( '@stdlib/array/base/linspace' ) );

/**
* @name logspace
* @memberof ns
* @readonly
* @type {Function}
* @see {@link module:@stdlib/array/base/logspace}
*/
setReadOnly( ns, 'logspace', require( '@stdlib/array/base/logspace' ) );

/**
* @name ones
* @memberof ns
* @readonly
* @type {Function}
* @see {@link module:@stdlib/array/base/ones}
*/
setReadOnly( ns, 'ones', require( '@stdlib/array/base/ones' ) );

/**
* @name zeros
* @memberof ns
* @readonly
* @type {Function}
* @see {@link module:@stdlib/array/base/zeros}
*/
setReadOnly( ns, 'zeros', require( '@stdlib/array/base/zeros' ) );

// EXPORTS //

module.exports = ns;
<|endoftext|>