mirror of
https://github.com/nasa/openmct.git
synced 2025-03-13 07:54:13 +00:00
Merge pull request #1131 from nasa/open1094
Resolve synchronization issues with MutableObject
This commit is contained in:
commit
b4dc50295c
@ -34,9 +34,9 @@
|
||||
'./tutorials/todo/todo',
|
||||
'./example/imagery/bundle',
|
||||
'./example/eventGenerator/bundle',
|
||||
'./example/generator/bundle',
|
||||
], function (todo) {
|
||||
todo(mct);
|
||||
'./example/generator/bundle'
|
||||
], function (todoPlugin) {
|
||||
mct.install(todoPlugin);
|
||||
mct.run();
|
||||
})
|
||||
});
|
||||
|
@ -8,7 +8,7 @@ define(['EventEmitter'], function (EventEmitter) {
|
||||
|
||||
Selection.prototype.select = function (value) {
|
||||
this.selectedValues.push(value);
|
||||
this.emit('change');
|
||||
this.emit('change', this.selectedValues);
|
||||
return this.deselect.bind(this, value);
|
||||
};
|
||||
|
||||
@ -16,7 +16,7 @@ define(['EventEmitter'], function (EventEmitter) {
|
||||
this.selectedValues = this.selectedValues.filter(function (v) {
|
||||
return v !== value;
|
||||
});
|
||||
this.emit('change');
|
||||
this.emit('change', this.selectedValues);
|
||||
};
|
||||
|
||||
Selection.prototype.selected = function () {
|
||||
@ -25,7 +25,7 @@ define(['EventEmitter'], function (EventEmitter) {
|
||||
|
||||
Selection.prototype.clear = function () {
|
||||
this.selectedValues = [];
|
||||
this.emit('change');
|
||||
this.emit('change', this.selectedValues);
|
||||
};
|
||||
|
||||
return Selection;
|
||||
|
@ -1,15 +1,54 @@
|
||||
define([
|
||||
'./object-utils',
|
||||
'./ObjectAPI'
|
||||
'./ObjectAPI',
|
||||
'./objectEventEmitter'
|
||||
], function (
|
||||
utils,
|
||||
ObjectAPI
|
||||
ObjectAPI,
|
||||
objectEventEmitter
|
||||
) {
|
||||
function ObjectServiceProvider(objectService, instantiate) {
|
||||
function ObjectServiceProvider(objectService, instantiate, topic) {
|
||||
this.objectService = objectService;
|
||||
this.instantiate = instantiate;
|
||||
|
||||
this.generalTopic = topic('mutation');
|
||||
this.bridgeEventBuses();
|
||||
}
|
||||
|
||||
/**
|
||||
* Bridges old and new style mutation events to provide compatibility between the two APIs
|
||||
* @private
|
||||
*/
|
||||
ObjectServiceProvider.prototype.bridgeEventBuses = function () {
|
||||
var removeGeneralTopicListener;
|
||||
|
||||
var handleMutation = function (newStyleObject) {
|
||||
var keyString = utils.makeKeyString(newStyleObject.key);
|
||||
var oldStyleObject = this.instantiate(utils.toOldFormat(newStyleObject), keyString);
|
||||
|
||||
// Don't trigger self
|
||||
removeGeneralTopicListener();
|
||||
|
||||
oldStyleObject.getCapability('mutation').mutate(function () {
|
||||
return utils.toOldFormat(newStyleObject);
|
||||
});
|
||||
|
||||
removeGeneralTopicListener = this.generalTopic.listen(handleLegacyMutation);
|
||||
}.bind(this);
|
||||
|
||||
var handleLegacyMutation = function (legacyObject){
|
||||
var newStyleObject = utils.toNewFormat(legacyObject.getModel(), legacyObject.getId());
|
||||
|
||||
//Don't trigger self
|
||||
objectEventEmitter.off('mutation', handleMutation);
|
||||
objectEventEmitter.emit(newStyleObject.key.identifier + ":*", newStyleObject);
|
||||
objectEventEmitter.on('mutation', handleMutation);
|
||||
}.bind(this);
|
||||
|
||||
objectEventEmitter.on('mutation', handleMutation);
|
||||
removeGeneralTopicListener = this.generalTopic.listen(handleLegacyMutation);
|
||||
};
|
||||
|
||||
ObjectServiceProvider.prototype.save = function (object) {
|
||||
var key = object.key,
|
||||
keyString = utils.makeKeyString(key),
|
||||
@ -37,7 +76,7 @@ define([
|
||||
|
||||
// Injects new object API as a decorator so that it hijacks all requests.
|
||||
// Object providers implemented on new API should just work, old API should just work, many things may break.
|
||||
function LegacyObjectAPIInterceptor(ROOTS, instantiate, objectService) {
|
||||
function LegacyObjectAPIInterceptor(ROOTS, instantiate, topic, objectService) {
|
||||
this.getObjects = function (keys) {
|
||||
var results = {},
|
||||
promises = keys.map(function (keyString) {
|
||||
@ -56,7 +95,7 @@ define([
|
||||
};
|
||||
|
||||
ObjectAPI._supersecretSetFallbackProvider(
|
||||
new ObjectServiceProvider(objectService, instantiate)
|
||||
new ObjectServiceProvider(objectService, instantiate, topic)
|
||||
);
|
||||
|
||||
ROOTS.forEach(function (r) {
|
||||
|
@ -1,10 +1,21 @@
|
||||
define([
|
||||
'lodash'
|
||||
'lodash',
|
||||
'./objectEventEmitter'
|
||||
], function (
|
||||
_
|
||||
_,
|
||||
objectEventEmitter
|
||||
) {
|
||||
function MutableObject(eventEmitter, object) {
|
||||
this.eventEmitter = eventEmitter;
|
||||
|
||||
var ANY_OBJECT_EVENT = "mutation";
|
||||
|
||||
/**
|
||||
* The MutableObject wraps a DomainObject and provides getters and
|
||||
* setters for
|
||||
* @param eventEmitter
|
||||
* @param object
|
||||
* @constructor
|
||||
*/
|
||||
function MutableObject(object) {
|
||||
this.object = object;
|
||||
this.unlisteners = [];
|
||||
}
|
||||
@ -21,22 +32,22 @@ define([
|
||||
|
||||
MutableObject.prototype.on = function(path, callback) {
|
||||
var fullPath = qualifiedEventName(this.object, path);
|
||||
this.eventEmitter.on(fullPath, callback);
|
||||
this.unlisteners.push(this.eventEmitter.off.bind(this.eventEmitter, fullPath, callback));
|
||||
objectEventEmitter.on(fullPath, callback);
|
||||
this.unlisteners.push(objectEventEmitter.off.bind(objectEventEmitter, fullPath, callback));
|
||||
};
|
||||
|
||||
MutableObject.prototype.set = function (path, value) {
|
||||
|
||||
_.set(this.object, path, value);
|
||||
_.set(this.object, 'modified', Date.now());
|
||||
|
||||
//Emit event specific to property
|
||||
this.eventEmitter.emit(qualifiedEventName(this.object, path), value);
|
||||
objectEventEmitter.emit(qualifiedEventName(this.object, path), value);
|
||||
//Emit wildcare event
|
||||
this.eventEmitter.emit(qualifiedEventName(this.object, '*'), this.object);
|
||||
};
|
||||
objectEventEmitter.emit(qualifiedEventName(this.object, '*'), this.object);
|
||||
|
||||
MutableObject.prototype.get = function (path) {
|
||||
return _.get(this.object, path);
|
||||
//Emit a general "any object" event
|
||||
objectEventEmitter.emit(ANY_OBJECT_EVENT, this.object);
|
||||
};
|
||||
|
||||
return MutableObject;
|
||||
|
@ -60,25 +60,25 @@ define(['./MutableObject'], function (MutableObject) {
|
||||
});
|
||||
|
||||
it('Supports getting and setting of object properties', function () {
|
||||
expect(mutableObject.get('stringProperty')).toEqual('stringValue');
|
||||
expect(domainObject.stringProperty).toEqual('stringValue');
|
||||
mutableObject.set('stringProperty', 'updated');
|
||||
expect(mutableObject.get('stringProperty')).toEqual('updated');
|
||||
expect(domainObject.stringProperty).toEqual('updated');
|
||||
|
||||
var newArrayProperty = [];
|
||||
expect(mutableObject.get('arrayProperty')).toEqual(arrayProperty);
|
||||
expect(domainObject.arrayProperty).toEqual(arrayProperty);
|
||||
mutableObject.set('arrayProperty', newArrayProperty);
|
||||
expect(mutableObject.get('arrayProperty')).toEqual(newArrayProperty);
|
||||
expect(domainObject.arrayProperty).toEqual(newArrayProperty);
|
||||
|
||||
var newObjectProperty = [];
|
||||
expect(mutableObject.get('objectProperty')).toEqual(objectProperty);
|
||||
expect(domainObject.objectProperty).toEqual(objectProperty);
|
||||
mutableObject.set('objectProperty', newObjectProperty);
|
||||
expect(mutableObject.get('objectProperty')).toEqual(newObjectProperty);
|
||||
expect(domainObject.objectProperty).toEqual(newObjectProperty);
|
||||
});
|
||||
|
||||
it('Supports getting and setting of nested properties', function () {
|
||||
expect(mutableObject.get('objectProperty')).toEqual(objectProperty);
|
||||
expect(mutableObject.get('objectProperty.prop1')).toEqual(objectProperty.prop1);
|
||||
expect(mutableObject.get('objectProperty.prop3.propA')).toEqual(objectProperty.prop3.propA);
|
||||
expect(domainObject.objectProperty).toEqual(objectProperty);
|
||||
expect(domainObject.objectProperty.prop1).toEqual(objectProperty.prop1);
|
||||
expect(domainObject.objectProperty.prop3.propA).toEqual(objectProperty.prop3.propA);
|
||||
|
||||
mutableObject.set('objectProperty.prop1', 'new-prop-1');
|
||||
expect(domainObject.objectProperty.prop1).toEqual('new-prop-1');
|
||||
|
@ -1,11 +1,9 @@
|
||||
define([
|
||||
'lodash',
|
||||
'EventEmitter',
|
||||
'./object-utils',
|
||||
'./MutableObject'
|
||||
], function (
|
||||
_,
|
||||
EventEmitter,
|
||||
utils,
|
||||
MutableObject
|
||||
) {
|
||||
@ -18,8 +16,7 @@ define([
|
||||
var Objects = {},
|
||||
ROOT_REGISTRY = [],
|
||||
PROVIDER_REGISTRY = {},
|
||||
FALLBACK_PROVIDER,
|
||||
eventEmitter = new EventEmitter();
|
||||
FALLBACK_PROVIDER;
|
||||
|
||||
Objects._supersecretSetFallbackProvider = function (p) {
|
||||
FALLBACK_PROVIDER = p;
|
||||
@ -83,7 +80,7 @@ define([
|
||||
};
|
||||
|
||||
Objects.getMutable = function (object) {
|
||||
return new MutableObject(eventEmitter, object);
|
||||
return new MutableObject(object);
|
||||
};
|
||||
|
||||
return Objects;
|
||||
|
@ -40,7 +40,8 @@ define([
|
||||
implementation: LegacyObjectAPIInterceptor,
|
||||
depends: [
|
||||
"roots[]",
|
||||
"instantiate"
|
||||
"instantiate",
|
||||
"topic"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
10
src/api/objects/objectEventEmitter.js
Normal file
10
src/api/objects/objectEventEmitter.js
Normal file
@ -0,0 +1,10 @@
|
||||
define([
|
||||
"EventEmitter"
|
||||
], function (
|
||||
EventEmitter
|
||||
) {
|
||||
/**
|
||||
* Provides a singleton event bus for sharing between objects.
|
||||
*/
|
||||
return new EventEmitter();
|
||||
});
|
@ -1,14 +1,14 @@
|
||||
<div class="example-todo">
|
||||
<div class="example-button-group">
|
||||
<a class="example-todo-button-all">All</a>
|
||||
<a class="example-todo-button-incomplete">Incomplete</a>
|
||||
<a class="example-todo-button-complete">Complete</a>
|
||||
<a class="example-todo-button" data-filter="all">All</a>
|
||||
<a class="example-todo-button" data-filter="incomplete">Incomplete</a>
|
||||
<a class="example-todo-button" data-filter="complete">Complete</a>
|
||||
</div>
|
||||
|
||||
<ul class="example-todo-task-list">
|
||||
</ul>
|
||||
|
||||
<div class="example-message">
|
||||
<div class="example-no-tasks">
|
||||
There are no tasks to show.
|
||||
</div>
|
||||
</div>
|
||||
|
@ -25,21 +25,16 @@ define([
|
||||
});
|
||||
|
||||
function TodoView(domainObject) {
|
||||
this.domainObject = domainObject;
|
||||
this.mutableObject = undefined;
|
||||
|
||||
this.tasks = domainObject.tasks;
|
||||
this.filterValue = "all";
|
||||
this.render = this.render.bind(this);
|
||||
this.objectChanged = this.objectChanged.bind(this);
|
||||
}
|
||||
|
||||
TodoView.prototype.objectChanged = function (object) {
|
||||
if (this.mutableObject) {
|
||||
this.mutableObject.stopListening();
|
||||
}
|
||||
this.mutableObject = mct.Objects.getMutable(object);
|
||||
this.render();
|
||||
this.setTaskStatus = this.setTaskStatus.bind(this);
|
||||
this.selectTask = this.selectTask.bind(this);
|
||||
|
||||
<<<<<<< HEAD
|
||||
this.mutableObject = mct.Objects.getMutable(domainObject);
|
||||
this.mutableObject.on('tasks', this.updateTasks.bind(this));
|
||||
=======
|
||||
//If anything on object changes, re-render view
|
||||
this.mutableObject.on("*", this.objectChanged);
|
||||
};
|
||||
@ -56,40 +51,71 @@ define([
|
||||
};
|
||||
|
||||
$(container).empty().append(self.$els);
|
||||
>>>>>>> origin/api-tutorials
|
||||
|
||||
this.$el = $(todoTemplate);
|
||||
this.$emptyMessage = this.$el.find('.example-no-tasks');
|
||||
this.$taskList = this.$el.find('.example-todo-task-list');
|
||||
this.$el.on('click', '[data-filter]', this.updateFilter.bind(this));
|
||||
this.$el.on('change', 'li', this.setTaskStatus.bind(this));
|
||||
this.$el.on('click', '.example-task-description', this.selectTask.bind(this));
|
||||
|
||||
<<<<<<< HEAD
|
||||
this.updateSelection = this.updateSelection.bind(this);
|
||||
mct.selection.on('change', this.updateSelection);
|
||||
}
|
||||
|
||||
TodoView.prototype.show = function (container) {
|
||||
$(container).empty().append(this.$el);
|
||||
this.render();
|
||||
=======
|
||||
self.initialize();
|
||||
self.objectChanged(this.domainObject);
|
||||
|
||||
mct.selection.on('change', self.render);
|
||||
>>>>>>> origin/api-tutorials
|
||||
};
|
||||
|
||||
TodoView.prototype.destroy = function () {
|
||||
if (this.mutableObject) {
|
||||
this.mutableObject.stopListening();
|
||||
}
|
||||
mct.selection.off('change', this.render);
|
||||
this.mutableObject.stopListening();
|
||||
mct.selection.off('change', this.updateSelection);
|
||||
};
|
||||
|
||||
TodoView.prototype.setFilter = function (value) {
|
||||
this.filterValue = value;
|
||||
TodoView.prototype.updateSelection = function (selection) {
|
||||
if (selection && selection.length) {
|
||||
this.selection = selection[0].index;
|
||||
} else {
|
||||
this.selection = -1;
|
||||
}
|
||||
this.render();
|
||||
};
|
||||
|
||||
TodoView.prototype.initialize = function () {
|
||||
Object.keys(this.$buttons).forEach(function (k) {
|
||||
this.$buttons[k].on('click', this.setFilter.bind(this, k));
|
||||
}, this);
|
||||
TodoView.prototype.updateTasks = function (tasks) {
|
||||
this.tasks = tasks;
|
||||
this.render();
|
||||
};
|
||||
|
||||
TodoView.prototype.render = function () {
|
||||
var $els = this.$els;
|
||||
var mutableObject = this.mutableObject;
|
||||
var tasks = mutableObject.get('tasks');
|
||||
var $message = $els.find('.example-message');
|
||||
var $list = $els.find('.example-todo-task-list');
|
||||
var $buttons = this.$buttons;
|
||||
var filters = {
|
||||
TodoView.prototype.updateFilter = function (e) {
|
||||
this.filterValue = $(e.target).data('filter');
|
||||
this.render();
|
||||
};
|
||||
|
||||
TodoView.prototype.setTaskStatus = function (e) {
|
||||
var $checkbox = $(e.target);
|
||||
var taskIndex = $checkbox.data('taskIndex');
|
||||
var completed = !!$checkbox.prop('checked');
|
||||
this.tasks[taskIndex].completed = completed;
|
||||
this.mutableObject.set('tasks[' + taskIndex + '].checked', completed);
|
||||
};
|
||||
|
||||
TodoView.prototype.selectTask = function (e) {
|
||||
var taskIndex = $(e.target).data('taskIndex');
|
||||
mct.selection.clear();
|
||||
mct.selection.select({index: taskIndex});
|
||||
};
|
||||
|
||||
TodoView.prototype.getFilteredTasks = function () {
|
||||
return this.tasks.filter({
|
||||
all: function () {
|
||||
return true;
|
||||
},
|
||||
@ -99,61 +125,89 @@ define([
|
||||
complete: function (task) {
|
||||
return task.completed;
|
||||
}
|
||||
};
|
||||
var filterValue = this.filterValue;
|
||||
var selected = mct.selection.selected();
|
||||
}[this.filterValue]);
|
||||
};
|
||||
|
||||
Object.keys($buttons).forEach(function (k) {
|
||||
$buttons[k].toggleClass('selected', filterValue === k);
|
||||
});
|
||||
tasks = tasks.filter(filters[filterValue]);
|
||||
TodoView.prototype.render = function () {
|
||||
var filteredTasks = this.getFilteredTasks();
|
||||
this.$emptyMessage.toggle(filteredTasks.length === 0);
|
||||
this.$taskList.empty();
|
||||
filteredTasks
|
||||
.forEach(function (task) {
|
||||
var $taskEl = $(taskTemplate),
|
||||
taskIndex = this.tasks.indexOf(task);
|
||||
$taskEl.find('.example-task-checked')
|
||||
.prop('checked', task.completed)
|
||||
.attr('data-task-index', taskIndex);
|
||||
$taskEl.find('.example-task-description')
|
||||
.text(task.description)
|
||||
.toggleClass('selected', taskIndex === this.selection)
|
||||
.attr('data-task-index', taskIndex);
|
||||
|
||||
$list.empty();
|
||||
tasks.forEach(function (task, index) {
|
||||
var $taskEls = $(taskTemplate);
|
||||
var $checkbox = $taskEls.find('.example-task-checked');
|
||||
var $desc = $taskEls.find('.example-task-description');
|
||||
$checkbox.prop('checked', task.completed);
|
||||
$desc.text(task.description);
|
||||
|
||||
$checkbox.on('change', function () {
|
||||
var checked = !!$checkbox.prop('checked');
|
||||
mutableObject.set("tasks." + index + ".completed", checked);
|
||||
});
|
||||
|
||||
$desc.on('click', function () {
|
||||
mct.selection.clear();
|
||||
mct.selection.select({ index: index });
|
||||
});
|
||||
|
||||
if (selected.length > 0 && selected[0].index === index) {
|
||||
$desc.addClass('selected');
|
||||
}
|
||||
|
||||
$list.append($taskEls);
|
||||
});
|
||||
|
||||
$message.toggle(tasks.length < 1);
|
||||
this.$taskList.append($taskEl);
|
||||
}, this);
|
||||
};
|
||||
|
||||
function TodoToolbarView(domainObject) {
|
||||
this.domainObject = domainObject;
|
||||
this.mutableObject = undefined;
|
||||
this.tasks = domainObject.tasks;
|
||||
this.mutableObject = mct.Objects.getMutable(domainObject);
|
||||
|
||||
this.handleSelectionChange = this.handleSelectionChange.bind(this);
|
||||
|
||||
this.mutableObject.on('tasks', this.updateTasks.bind(this));
|
||||
mct.selection.on('change', this.handleSelectionChange);
|
||||
this.$el = $(toolbarTemplate);
|
||||
this.$remove = this.$el.find('.example-remove');
|
||||
this.$el.on('click', '.example-add', this.addTask.bind(this));
|
||||
this.$el.on('click', '.example-remove', this.removeTask.bind(this));
|
||||
}
|
||||
|
||||
TodoToolbarView.prototype.updateTasks = function (tasks) {
|
||||
this.tasks = tasks;
|
||||
};
|
||||
|
||||
TodoToolbarView.prototype.addTask = function () {
|
||||
var $dialog = $(dialogTemplate),
|
||||
view = {
|
||||
show: function (container) {
|
||||
$(container).append($dialog);
|
||||
},
|
||||
destroy: function () {}
|
||||
};
|
||||
|
||||
mct.dialog(view, "Add a Task").then(function () {
|
||||
var description = $dialog.find('input').val();
|
||||
this.tasks.push({description: description});
|
||||
this.mutableObject.set('tasks', this.tasks);
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
TodoToolbarView.prototype.removeTask = function () {
|
||||
if (this.selection >= 0) {
|
||||
this.tasks.splice(this.selection, 1);
|
||||
this.mutableObject.set('tasks', this.tasks);
|
||||
mct.selection.clear();
|
||||
this.render();
|
||||
}
|
||||
};
|
||||
|
||||
TodoToolbarView.prototype.show = function (container) {
|
||||
var self = this;
|
||||
this.destroy();
|
||||
this.$els = $(toolbarTemplate);
|
||||
this.render();
|
||||
$(container).append(this.$els);
|
||||
};
|
||||
|
||||
TodoToolbarView.prototype.render = function () {
|
||||
var self = this;
|
||||
var $els = this.$els;
|
||||
|
||||
self.mutableObject = mct.Objects.getMutable(this.domainObject);
|
||||
|
||||
var $els = $(toolbarTemplate);
|
||||
var $add = $els.find('a.example-add');
|
||||
var $remove = $els.find('a.example-remove');
|
||||
|
||||
$(container).append($els);
|
||||
|
||||
$add.on('click', function () {
|
||||
var $dialog = $(dialogTemplate),
|
||||
view = {
|
||||
@ -191,14 +245,12 @@ define([
|
||||
if (this.$remove) {
|
||||
this.$remove.toggle(selected.length > 0);
|
||||
}
|
||||
this.render();
|
||||
};
|
||||
|
||||
TodoToolbarView.prototype.destroy = function () {
|
||||
mct.selection.off('change', this.handleSelectionChange);
|
||||
this.$remove = undefined;
|
||||
if (this.mutableObject) {
|
||||
this.mutableObject.stopListening();
|
||||
}
|
||||
this.mutableObject.stopListening();
|
||||
};
|
||||
|
||||
mct.type('example.todo', todoType);
|
||||
|
Loading…
x
Reference in New Issue
Block a user