[IMP] formalize state transitions via an actual fsm
bzr revid: xmo@openerp.com-20121003145048-wdoo712j551jow3t
This commit is contained in:
@@ -33,6 +33,7 @@ Re-implement openerp's file import system:
|
||||
],
|
||||
'js': [
|
||||
'static/lib/select2/select2.js',
|
||||
'static/lib/javascript-state-machine/state-machine.js',
|
||||
'static/src/js/import.js',
|
||||
],
|
||||
'qweb': ['static/src/xml/import.xml'],
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
Copyright (c) 2012 Jake Gordon and contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
@@ -0,0 +1,327 @@
|
||||
Javascript Finite State Machine (v2.1.0)
|
||||
========================================
|
||||
|
||||
This standalone javascript micro-framework provides a finite state machine for your pleasure.
|
||||
|
||||
* You can find the [code here](https://github.com/jakesgordon/javascript-state-machine)
|
||||
* You can find a [description here](http://codeincomplete.com/posts/2011/8/19/javascript_state_machine_v2/)
|
||||
* You can find a [working demo here](http://codeincomplete.com/posts/2011/8/19/javascript_state_machine_v2/example/)
|
||||
|
||||
Download
|
||||
========
|
||||
|
||||
You can download [state-machine.js](https://github.com/jakesgordon/javascript-state-machine/raw/master/state-machine.js),
|
||||
or the [minified version](https://github.com/jakesgordon/javascript-state-machine/raw/master/state-machine.min.js)
|
||||
|
||||
Alternatively:
|
||||
|
||||
git clone git@github.com:jakesgordon/javascript-state-machine
|
||||
|
||||
|
||||
* All code is in state-machine.js
|
||||
* Minified version provided in state-machine.min.js
|
||||
* No 3rd party library is required
|
||||
* Demo can be found in /index.html
|
||||
* QUnit tests can be found in /test/index.html
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
Include `state-machine.min.js` in your application.
|
||||
|
||||
In its simplest form, create a standalone state machine using:
|
||||
|
||||
var fsm = StateMachine.create({
|
||||
initial: 'green',
|
||||
events: [
|
||||
{ name: 'warn', from: 'green', to: 'yellow' },
|
||||
{ name: 'panic', from: 'yellow', to: 'red' },
|
||||
{ name: 'calm', from: 'red', to: 'yellow' },
|
||||
{ name: 'clear', from: 'yellow', to: 'green' }
|
||||
]});
|
||||
|
||||
... will create an object with a method for each event:
|
||||
|
||||
* fsm.warn() - transition from 'green' to 'yellow'
|
||||
* fsm.panic() - transition from 'yellow' to 'red'
|
||||
* fsm.calm() - transition from 'red' to 'yellow'
|
||||
* fsm.clear() - transition from 'yellow' to 'green'
|
||||
|
||||
along with the following members:
|
||||
|
||||
* fsm.current - contains the current state
|
||||
* fsm.is(s) - return true if state `s` is the current state
|
||||
* fsm.can(e) - return true if event `e` can be fired in the current state
|
||||
* fsm.cannot(e) - return true if event `e` cannot be fired in the current state
|
||||
|
||||
Multiple 'from' and 'to' states for a single event
|
||||
==================================================
|
||||
|
||||
If an event is allowed **from** multiple states, and always transitions to the same
|
||||
state, then simply provide an array of states in the `from` attribute of an event. However,
|
||||
if an event is allowed from multiple states, but should transition **to** a different
|
||||
state depending on the current state, then provide multiple event entries with
|
||||
the same name:
|
||||
|
||||
var fsm = StateMachine.create({
|
||||
initial: 'hungry',
|
||||
events: [
|
||||
{ name: 'eat', from: 'hungry', to: 'satisfied' },
|
||||
{ name: 'eat', from: 'satisfied', to: 'full' },
|
||||
{ name: 'eat', from: 'full', to: 'sick' },
|
||||
{ name: 'rest', from: ['hungry', 'satisfied', 'full', 'sick'], to: 'hungry' },
|
||||
]});
|
||||
|
||||
This example will create an object with 2 event methods:
|
||||
|
||||
* fsm.eat()
|
||||
* fsm.rest()
|
||||
|
||||
The `rest` event will always transition to the `hungry` state, while the `eat` event
|
||||
will transition to a state that is dependent on the current state.
|
||||
|
||||
>> NOTE: The `rest` event could use a wildcard '*' for the 'from' state if it should be
|
||||
allowed from any current state.
|
||||
|
||||
>> NOTE: The `rest` event in the above example can also be specified as multiple events with
|
||||
the same name if you prefer the verbose approach.
|
||||
|
||||
Callbacks
|
||||
=========
|
||||
|
||||
4 callbacks are available if your state machine has methods using the following naming conventions:
|
||||
|
||||
* onbefore**event** - fired before the event
|
||||
* onleave**state** - fired when leaving the old state
|
||||
* onenter**state** - fired when entering the new state
|
||||
* onafter**event** - fired after the event
|
||||
|
||||
You can affect the event in 3 ways:
|
||||
|
||||
* return `false` from an `onbeforeevent` handler to cancel the event.
|
||||
* return `false` from an `onleavestate` handler to cancel the event.
|
||||
* return `ASYNC` from an `onleavestate` handler to perform an asynchronous state transition (see next section)
|
||||
|
||||
For convenience, the 2 most useful callbacks can be shortened:
|
||||
|
||||
* on**event** - convenience shorthand for onafter**event**
|
||||
* on**state** - convenience shorthand for onenter**state**
|
||||
|
||||
In addition, a generic `onchangestate()` callback can be used to call a single function for _all_ state changes:
|
||||
|
||||
All callbacks will be passed the same arguments:
|
||||
|
||||
* **event** name
|
||||
* **from** state
|
||||
* **to** state
|
||||
* _(followed by any arguments you passed into the original event method)_
|
||||
|
||||
Callbacks can be specified when the state machine is first created:
|
||||
|
||||
var fsm = StateMachine.create({
|
||||
initial: 'green',
|
||||
events: [
|
||||
{ name: 'warn', from: 'green', to: 'yellow' },
|
||||
{ name: 'panic', from: 'yellow', to: 'red' },
|
||||
{ name: 'calm', from: 'red', to: 'yellow' },
|
||||
{ name: 'clear', from: 'yellow', to: 'green' }
|
||||
],
|
||||
callbacks: {
|
||||
onpanic: function(event, from, to, msg) { alert('panic! ' + msg); },
|
||||
onclear: function(event, from, to, msg) { alert('thanks to ' + msg); },
|
||||
ongreen: function(event, from, to) { document.body.className = 'green'; },
|
||||
onyellow: function(event, from, to) { document.body.className = 'yellow'; },
|
||||
onred: function(event, from, to) { document.body.className = 'red'; },
|
||||
}
|
||||
});
|
||||
|
||||
fsm.panic('killer bees');
|
||||
fsm.clear('sedatives in the honey pots');
|
||||
...
|
||||
|
||||
Additionally, they can be added and removed from the state machine at any time:
|
||||
|
||||
fsm.ongreen = null;
|
||||
fsm.onyellow = null;
|
||||
fsm.onred = null;
|
||||
fsm.onchangestate = function(event, from, to) { document.body.className = to; };
|
||||
|
||||
Asynchronous State Transitions
|
||||
==============================
|
||||
|
||||
Sometimes, you need to execute some asynchronous code during a state transition and ensure the
|
||||
new state is not entered until your code has completed.
|
||||
|
||||
A good example of this is when you transition out of a `menu` state, perhaps you want to gradually
|
||||
fade the menu away, or slide it off the screen and don't want to transition to your `game` state
|
||||
until after that animation has been performed.
|
||||
|
||||
You can now return `StateMachine.ASYNC` from your `onleavestate` handler and the state machine
|
||||
will be _'put on hold'_ until you are ready to trigger the transition using the new `transition()`
|
||||
method.
|
||||
|
||||
For example, using jQuery effects:
|
||||
|
||||
var fsm = StateMachine.create({
|
||||
|
||||
initial: 'menu',
|
||||
|
||||
events: [
|
||||
{ name: 'play', from: 'menu', to: 'game' },
|
||||
{ name: 'quit', from: 'game', to: 'menu' }
|
||||
],
|
||||
|
||||
callbacks: {
|
||||
|
||||
onentermenu: function() { $('#menu').show(); },
|
||||
onentergame: function() { $('#game').show(); },
|
||||
|
||||
onleavemenu: function() {
|
||||
$('#menu').fadeOut('fast', function() {
|
||||
fsm.transition();
|
||||
});
|
||||
return StateMachine.ASYNC; // tell StateMachine to defer next state until we call transition (in fadeOut callback above)
|
||||
},
|
||||
|
||||
onleavegame: function() {
|
||||
$('#game').slideDown('slow', function() {
|
||||
fsm.transition();
|
||||
};
|
||||
return StateMachine.ASYNC; // tell StateMachine to defer next state until we call transition (in slideDown callback above)
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
State Machine Classes
|
||||
=====================
|
||||
|
||||
You can also turn all instances of a _class_ into an FSM by applying
|
||||
the state machine functionality to the prototype, including your callbacks
|
||||
in your prototype, and providing a `startup` event for use when constructing
|
||||
instances:
|
||||
|
||||
MyFSM = function() { // my constructor function
|
||||
this.startup();
|
||||
};
|
||||
|
||||
MyFSM.prototype = {
|
||||
|
||||
onpanic: function(event, from, to) { alert('panic'); },
|
||||
onclear: function(event, from, to) { alert('all is clear'); },
|
||||
|
||||
// my other prototype methods
|
||||
|
||||
};
|
||||
|
||||
StateMachine.create({
|
||||
target: MyFSM.prototype,
|
||||
events: [
|
||||
{ name: 'startup', from: 'none', to: 'green' },
|
||||
{ name: 'warn', from: 'green', to: 'yellow' },
|
||||
{ name: 'panic', from: 'yellow', to: 'red' },
|
||||
{ name: 'calm', from: 'red', to: 'yellow' },
|
||||
{ name: 'clear', from: 'yellow', to: 'green' }
|
||||
]});
|
||||
|
||||
|
||||
This should be easy to adjust to fit your appropriate mechanism for object construction.
|
||||
|
||||
Initialization Options
|
||||
======================
|
||||
|
||||
How the state machine should initialize can depend on your application requirements, so
|
||||
the library provides a number of simple options.
|
||||
|
||||
By default, if you dont specify any initial state, the state machine will be in the `'none'`
|
||||
state and you would need to provide an event to take it out of this state:
|
||||
|
||||
var fsm = StateMachine.create({
|
||||
events: [
|
||||
{ name: 'startup', from: 'none', to: 'green' },
|
||||
{ name: 'panic', from: 'green', to: 'red' },
|
||||
{ name: 'calm', from: 'red', to: 'green' },
|
||||
]});
|
||||
alert(fsm.current); // "none"
|
||||
fsm.startup();
|
||||
alert(fsm.current); // "green"
|
||||
|
||||
If you specify the name of your initial event (as in all the earlier examples), then an
|
||||
implicit `startup` event will be created for you and fired when the state machine is constructed.
|
||||
|
||||
var fsm = StateMachine.create({
|
||||
initial: 'green',
|
||||
events: [
|
||||
{ name: 'panic', from: 'green', to: 'red' },
|
||||
{ name: 'calm', from: 'red', to: 'green' },
|
||||
]});
|
||||
alert(fsm.current); // "green"
|
||||
|
||||
If your object already has a `startup` method you can use a different name for the initial event
|
||||
|
||||
var fsm = StateMachine.create({
|
||||
initial: { state: 'green', event: 'init' },
|
||||
events: [
|
||||
{ name: 'panic', from: 'green', to: 'red' },
|
||||
{ name: 'calm', from: 'red', to: 'green' },
|
||||
]});
|
||||
alert(fsm.current); // "green"
|
||||
|
||||
Finally, if you want to wait to call the initial state transition event until a later date you
|
||||
can `defer` it:
|
||||
|
||||
var fsm = StateMachine.create({
|
||||
initial: { state: 'green', event: 'init', defer: true },
|
||||
events: [
|
||||
{ name: 'panic', from: 'green', to: 'red' },
|
||||
{ name: 'calm', from: 'red', to: 'green' },
|
||||
]});
|
||||
alert(fsm.current); // "none"
|
||||
fsm.init();
|
||||
alert(fsm.current); // "green"
|
||||
|
||||
Of course, we have now come full circle, this last example is pretty much functionally the
|
||||
same as the first example in this section where you simply define your own startup event.
|
||||
|
||||
So you have a number of choices available to you when initializing your state machine.
|
||||
|
||||
Handling Failures
|
||||
======================
|
||||
|
||||
By default, if you try to call an event method that is not allowed in the current state, the
|
||||
state machine will throw an exception. If you prefer to handle the problem yourself, you can
|
||||
define a custom `error` handler:
|
||||
|
||||
var fsm = StateMachine.create({
|
||||
initial: 'green',
|
||||
error: function(eventName, from, to, args, errorCode, errorMessage) {
|
||||
return 'event ' + eventName + ' was naughty :- ' + errorMessage;
|
||||
},
|
||||
events: [
|
||||
{ name: 'panic', from: 'green', to: 'red' },
|
||||
{ name: 'calm', from: 'red', to: 'green' },
|
||||
]});
|
||||
alert(fsm.calm()); // "event calm was naughty :- event not allowed in current state green"
|
||||
|
||||
Release Notes
|
||||
=============
|
||||
|
||||
See [RELEASE NOTES](https://github.com/jakesgordon/javascript-state-machine/blob/master/RELEASE_NOTES.md) file.
|
||||
|
||||
License
|
||||
=======
|
||||
|
||||
See [LICENSE](https://github.com/jakesgordon/javascript-state-machine/blob/master/LICENSE) file.
|
||||
|
||||
Contact
|
||||
=======
|
||||
|
||||
If you have any ideas, feedback, requests or bug reports, you can reach me at
|
||||
[jake@codeincomplete.com](mailto:jake@codeincomplete.com), or via
|
||||
my website: [Code inComplete](http://codeincomplete.com/posts/2011/8/19/javascript_state_machine_v2/)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
Version 2.1.0 (January 7th 2012)
|
||||
--------------------------------
|
||||
|
||||
* Wrapped in self executing function to be more easily used with loaders like `require.js` or `curl.js` (issue #15)
|
||||
* Allow event to be cancelled by returning `false` from `onleavestate` handler (issue #13) - WARNING: this breaks backward compatibility for async transitions (you now need to return `StateMachine.ASYNC` instead of `false`)
|
||||
* Added explicit return values for event methods (issue #12)
|
||||
* Added support for wildcard events that can be fired 'from' any state (issue #11)
|
||||
* Added support for no-op events that transition 'to' the same state (issue #5)
|
||||
* extended custom error callback to handle any exceptions caused by caller provided callbacks
|
||||
* added custom error callback to override exception when an illegal state transition is attempted (thanks to cboone)
|
||||
* fixed typos (thanks to cboone)
|
||||
* fixed issue #4 - ensure before/after event hooks are called even if the event doesn't result in a state change
|
||||
|
||||
Version 2.0.0 (August 19th 2011)
|
||||
--------------------------------
|
||||
|
||||
* adding support for asynchronous state transitions (see README) - with lots of qunit tests (see test/async.js).
|
||||
* consistent arguments for ALL callbacks, first 3 args are ALWAYS event name, from state and to state, followed by whatever arguments the user passed to the original event method.
|
||||
* added a generic `onchangestate(event,from,to)` callback to detect all state changes with a single function.
|
||||
* allow callbacks to be declared at creation time (instead of having to attach them afterwards)
|
||||
* renamed 'hooks' => 'callbacks'
|
||||
* [read more...](http://codeincomplete.com/posts/2011/8/19/javascript_state_machine_v2/)
|
||||
|
||||
Version 1.2.0 (June 21st 2011)
|
||||
------------------------------
|
||||
* allows the same event to transition to different states, depending on the current state (see 'Multiple...' section in README.md)
|
||||
* [read more...](http://codeincomplete.com/posts/2011/6/21/javascript_state_machine_v1_2_0/)
|
||||
|
||||
Version 1.0.0 (June 1st 2011)
|
||||
-----------------------------
|
||||
* initial version
|
||||
* [read more...](http://codeincomplete.com/posts/2011/6/1/javascript_state_machine/)
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
desc "create minified version of state-machine.js"
|
||||
task :minify do
|
||||
require File.expand_path(File.join(File.dirname(__FILE__), 'minifier/minifier'))
|
||||
Minifier.enabled = true
|
||||
Minifier.minify('state-machine.js')
|
||||
end
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Javascript Finite State Machine</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
||||
<link href="demo/demo.css" media="screen, print" rel="stylesheet" type="text/css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div id="demo" class='green'>
|
||||
|
||||
<h1> Finite State Machine </h1>
|
||||
|
||||
<div id="controls">
|
||||
<button id="clear" onclick="Demo.clear();">clear</button>
|
||||
<button id="calm" onclick="Demo.calm();">calm</button>
|
||||
<button id="warn" onclick="Demo.warn();">warn</button>
|
||||
<button id="panic" onclick="Demo.panic();">panic!</button>
|
||||
</div>
|
||||
|
||||
<div id="diagram">
|
||||
</div>
|
||||
|
||||
<div id="notes">
|
||||
<i>dashed lines are asynchronous state transitions (3 seconds)</i>
|
||||
</div>
|
||||
|
||||
<textarea id="output">
|
||||
</textarea>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<script src="state-machine.js"></script>
|
||||
<script src="demo/demo.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,155 @@
|
||||
(function (window) {
|
||||
|
||||
StateMachine = {
|
||||
|
||||
//---------------------------------------------------------------------------
|
||||
|
||||
VERSION: "2.1.0",
|
||||
|
||||
//---------------------------------------------------------------------------
|
||||
|
||||
Result: {
|
||||
SUCCEEDED: 1, // the event transitioned successfully from one state to another
|
||||
NOTRANSITION: 2, // the event was successfull but no state transition was necessary
|
||||
CANCELLED: 3, // the event was cancelled by the caller in a beforeEvent callback
|
||||
ASYNC: 4, // the event is asynchronous and the caller is in control of when the transition occurs
|
||||
},
|
||||
|
||||
Error: {
|
||||
INVALID_TRANSITION: 100, // caller tried to fire an event that was innapropriate in the current state
|
||||
PENDING_TRANSITION: 200, // caller tried to fire an event while an async transition was still pending
|
||||
INVALID_CALLBACK: 300, // caller provided callback function threw an exception
|
||||
},
|
||||
|
||||
WILDCARD: '*',
|
||||
ASYNC: 'async',
|
||||
|
||||
//---------------------------------------------------------------------------
|
||||
|
||||
create: function(cfg, target) {
|
||||
|
||||
var initial = (typeof cfg.initial == 'string') ? { state: cfg.initial } : cfg.initial; // allow for a simple string, or an object with { state: 'foo', event: 'setup', defer: true|false }
|
||||
var fsm = target || cfg.target || {};
|
||||
var events = cfg.events || [];
|
||||
var callbacks = cfg.callbacks || {};
|
||||
var map = {};
|
||||
|
||||
var add = function(e) {
|
||||
var from = (e.from instanceof Array) ? e.from : (e.from ? [e.from] : [StateMachine.WILDCARD]); // allow 'wildcard' transition if 'from' is not specified
|
||||
map[e.name] = map[e.name] || {};
|
||||
for (var n = 0 ; n < from.length ; n++)
|
||||
map[e.name][from[n]] = e.to || from[n]; // allow no-op transition if 'to' is not specified
|
||||
};
|
||||
|
||||
if (initial) {
|
||||
initial.event = initial.event || 'startup';
|
||||
add({ name: initial.event, from: 'none', to: initial.state });
|
||||
}
|
||||
|
||||
for(var n = 0 ; n < events.length ; n++)
|
||||
add(events[n]);
|
||||
|
||||
for(var name in map) {
|
||||
if (map.hasOwnProperty(name))
|
||||
fsm[name] = StateMachine.buildEvent(name, map[name]);
|
||||
}
|
||||
|
||||
for(var name in callbacks) {
|
||||
if (callbacks.hasOwnProperty(name))
|
||||
fsm[name] = callbacks[name]
|
||||
}
|
||||
|
||||
fsm.current = 'none';
|
||||
fsm.is = function(state) { return this.current == state; };
|
||||
fsm.can = function(event) { return !this.transition && (map[event].hasOwnProperty(this.current) || map[event].hasOwnProperty(StateMachine.WILDCARD)); }
|
||||
fsm.cannot = function(event) { return !this.can(event); };
|
||||
fsm.error = cfg.error || function(name, from, to, args, error, msg) { throw msg; }; // default behavior when something unexpected happens is to throw an exception, but caller can override this behavior if desired (see github issue #3)
|
||||
|
||||
if (initial && !initial.defer)
|
||||
fsm[initial.event]();
|
||||
|
||||
return fsm;
|
||||
|
||||
},
|
||||
|
||||
//===========================================================================
|
||||
|
||||
doCallback: function(fsm, func, name, from, to, args) {
|
||||
if (func) {
|
||||
try {
|
||||
return func.apply(fsm, [name, from, to].concat(args));
|
||||
}
|
||||
catch(e) {
|
||||
return fsm.error(name, from, to, args, StateMachine.Error.INVALID_CALLBACK, "an exception occurred in a caller-provided callback function");
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
beforeEvent: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onbefore' + name], name, from, to, args); },
|
||||
afterEvent: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onafter' + name] || fsm['on' + name], name, from, to, args); },
|
||||
leaveState: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onleave' + from], name, from, to, args); },
|
||||
enterState: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onenter' + to] || fsm['on' + to], name, from, to, args); },
|
||||
changeState: function(fsm, name, from, to, args) { return StateMachine.doCallback(fsm, fsm['onchangestate'], name, from, to, args); },
|
||||
|
||||
|
||||
buildEvent: function(name, map) {
|
||||
return function() {
|
||||
|
||||
var from = this.current;
|
||||
var to = map[from] || map[StateMachine.WILDCARD] || from;
|
||||
var args = Array.prototype.slice.call(arguments); // turn arguments into pure array
|
||||
|
||||
if (this.transition)
|
||||
return this.error(name, from, to, args, StateMachine.Error.PENDING_TRANSITION, "event " + name + " inappropriate because previous transition did not complete");
|
||||
|
||||
if (this.cannot(name))
|
||||
return this.error(name, from, to, args, StateMachine.Error.INVALID_TRANSITION, "event " + name + " inappropriate in current state " + this.current);
|
||||
|
||||
if (false === StateMachine.beforeEvent(this, name, from, to, args))
|
||||
return StateMachine.CANCELLED;
|
||||
|
||||
if (from === to) {
|
||||
StateMachine.afterEvent(this, name, from, to, args);
|
||||
return StateMachine.NOTRANSITION;
|
||||
}
|
||||
|
||||
// prepare a transition method for use EITHER lower down, or by caller if they want an async transition (indicated by an ASYNC return value from leaveState)
|
||||
var fsm = this;
|
||||
this.transition = function() {
|
||||
fsm.transition = null; // this method should only ever be called once
|
||||
fsm.current = to;
|
||||
StateMachine.enterState( fsm, name, from, to, args);
|
||||
StateMachine.changeState(fsm, name, from, to, args);
|
||||
StateMachine.afterEvent( fsm, name, from, to, args);
|
||||
};
|
||||
|
||||
var leave = StateMachine.leaveState(this, name, from, to, args);
|
||||
if (false === leave) {
|
||||
this.transition = null;
|
||||
return StateMachine.CANCELLED;
|
||||
}
|
||||
else if ("async" === leave) {
|
||||
return StateMachine.ASYNC;
|
||||
}
|
||||
else {
|
||||
if (this.transition)
|
||||
this.transition(); // in case user manually called transition() but forgot to return ASYNC
|
||||
return StateMachine.SUCCEEDED;
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
}; // StateMachine
|
||||
|
||||
//===========================================================================
|
||||
|
||||
if ("function" === typeof define) {
|
||||
define("statemachine", [], function() { return StateMachine; });
|
||||
}
|
||||
else {
|
||||
window.StateMachine = StateMachine;
|
||||
}
|
||||
|
||||
}(this));
|
||||
|
||||
@@ -71,14 +71,8 @@ openerp.base_import = function (instance) {
|
||||
],
|
||||
events: {
|
||||
// 'change .oe_import_grid input': 'import_dryrun',
|
||||
'change input.oe_import_file': 'file_update',
|
||||
'change input.oe_import_has_header, .oe_import_options input': 'settings_updated',
|
||||
'click a.oe_import_csv': function (e) {
|
||||
e.preventDefault();
|
||||
},
|
||||
'click a.oe_import_export': function (e) {
|
||||
e.preventDefault();
|
||||
},
|
||||
'change input.oe_import_file': 'loaded_file',
|
||||
'change input.oe_import_has_header, .oe_import_options input': 'settings_changed',
|
||||
'click a.oe_import_toggle': function (e) {
|
||||
e.preventDefault();
|
||||
var $el = $(e.target);
|
||||
@@ -109,14 +103,15 @@ openerp.base_import = function (instance) {
|
||||
this.do_action(_.extend(action, {target: 'new'}));
|
||||
},
|
||||
// buttons
|
||||
'click .oe_import_validate': 'import_dryrun',
|
||||
'click .oe_import_import': 'do_import',
|
||||
'click .oe_import_validate': 'validate',
|
||||
'click .oe_import_import': 'import',
|
||||
'click .oe_import_cancel': function (e) {
|
||||
e.preventDefault();
|
||||
this.exit();
|
||||
}
|
||||
},
|
||||
init: function (parent, params) {
|
||||
var self = this;
|
||||
this._super.apply(this, arguments);
|
||||
this.res_model = params.model;
|
||||
// import object id
|
||||
@@ -150,42 +145,48 @@ openerp.base_import = function (instance) {
|
||||
},
|
||||
|
||||
//- File & settings change section
|
||||
file_update: function (e) {
|
||||
onfile_loaded: function () {
|
||||
this.$('.oe_import_button').prop('disabled', true);
|
||||
if (!this.$('input.oe_import_file').val()) { return; }
|
||||
|
||||
this.$el.removeClass('oe_import_preview oe_import_error');
|
||||
jsonp(this.$el, {
|
||||
url: '/base_import/set_file'
|
||||
}, this.proxy('settings_updated'));
|
||||
}, this.proxy('settings_changed'));
|
||||
},
|
||||
settings_updated: function () {
|
||||
onpreviewing: function () {
|
||||
var self = this;
|
||||
this.$('.oe_import_button').prop('disabled', true);
|
||||
this.$el.addClass('oe_import_with_file');
|
||||
// TODO: test that write // succeeded?
|
||||
this.Import.call(
|
||||
'parse_preview', [this.id, this.import_options()])
|
||||
.then(this.proxy('preview'));
|
||||
},
|
||||
preview: function (result) {
|
||||
this.$el.removeClass('oe_import_preview_error oe_import_error');
|
||||
this.$el.toggleClass(
|
||||
'oe_import_noheaders',
|
||||
!this.$('input.oe_import_has_header').prop('checked'));
|
||||
if (result.error) {
|
||||
this.$('.oe_import_options').show();
|
||||
this.$el.addClass('oe_import_preview_error oe_import_error');
|
||||
this.$('.oe_import_error_report').html(
|
||||
QWeb.render('ImportView.preview.error', result))
|
||||
.get(0).scrollIntoView();
|
||||
return;
|
||||
}
|
||||
this.Import.call(
|
||||
'parse_preview', [this.id, this.import_options()])
|
||||
.then(function (result) {
|
||||
var signal = result.error ? 'preview_failed' : 'preview_succeeded';
|
||||
self[signal](result);
|
||||
});
|
||||
},
|
||||
onpreview_error: function (event, from, to, result) {
|
||||
this.$('.oe_import_options').show();
|
||||
this.$el.addClass('oe_import_preview_error oe_import_error');
|
||||
this.$('.oe_import_error_report').html(
|
||||
QWeb.render('ImportView.preview.error', result))
|
||||
.get(0).scrollIntoView();
|
||||
},
|
||||
onpreview_success: function (event, from, to, result) {
|
||||
this.$('.oe_import_import').removeClass('oe_highlight');
|
||||
this.$('.oe_import_validate').addClass('oe_highlight');
|
||||
this.$('.oe_import_button').prop('disabled', false);
|
||||
this.$el.addClass('oe_import_preview');
|
||||
this.$('table').html(QWeb.render('ImportView.preview', result));
|
||||
|
||||
if (result.headers.length === 1) {
|
||||
this.$('.oe_import_options').show();
|
||||
this.render_import_result([{
|
||||
this.onresults(null, null, null, [{
|
||||
type: 'warning',
|
||||
message: _t("A single column was found in the file, this often means the file separator is incorrect")
|
||||
}]);
|
||||
@@ -225,7 +226,6 @@ openerp.base_import = function (instance) {
|
||||
width: 'resolve',
|
||||
dropdownCssClass: 'oe_import_selector'
|
||||
});
|
||||
//this.import_dryrun();
|
||||
},
|
||||
generate_fields_completion: function (root) {
|
||||
var basic = [];
|
||||
@@ -303,28 +303,31 @@ openerp.base_import = function (instance) {
|
||||
return this.Import.call(
|
||||
'do', [this.id, fields, this.import_options()], options);
|
||||
},
|
||||
import_dryrun: function () {
|
||||
onvalidate: function () {
|
||||
return this.call_import({ dryrun: true })
|
||||
.then(this.proxy('render_import_result'));
|
||||
.then(this.proxy('validated'));
|
||||
},
|
||||
do_import: function () {
|
||||
onimport: function () {
|
||||
var self = this;
|
||||
return this.call_import({ dryrun: false }).then(function (message) {
|
||||
if (!_.any(message, function (message) {
|
||||
return message.type === 'error' })) {
|
||||
self.exit();
|
||||
self['import_succeeded']();
|
||||
return;
|
||||
}
|
||||
self.render_import_result(message);
|
||||
self['import_failed'](message);
|
||||
});
|
||||
},
|
||||
onimported: function () {
|
||||
this.exit();
|
||||
},
|
||||
exit: function () {
|
||||
this.do_action({
|
||||
type: 'ir.actions.client',
|
||||
tag: 'history_back'
|
||||
});
|
||||
},
|
||||
render_import_result: function (message) {
|
||||
onresults: function (event, from, to, message) {
|
||||
var no_messages = _.isEmpty(message);
|
||||
this.$('.oe_import_import').toggleClass('oe_highlight', no_messages);
|
||||
this.$('.oe_import_validate').toggleClass('oe_highlight', !no_messages);
|
||||
@@ -386,4 +389,23 @@ openerp.base_import = function (instance) {
|
||||
})).get(0).scrollIntoView();
|
||||
},
|
||||
});
|
||||
// FSM-ize DataImport
|
||||
StateMachine.create({
|
||||
target: instance.web.DataImport.prototype,
|
||||
events: [
|
||||
{ name: 'loaded_file',
|
||||
from: ['none', 'file_loaded', 'preview_error', 'preview_success', 'results'],
|
||||
to: 'file_loaded' },
|
||||
{ name: 'settings_changed',
|
||||
from: ['file_loaded', 'preview_error', 'preview_success', 'results'],
|
||||
to: 'previewing' },
|
||||
{ name: 'preview_failed', from: 'previewing', to: 'preview_error' },
|
||||
{ name: 'preview_succeeded', from: 'previewing', to: 'preview_success' },
|
||||
{ name: 'validate', from: 'preview_success', to: 'validating' },
|
||||
{ name: 'validated', from: 'validating', to: 'results' },
|
||||
{ name: 'import', from: ['preview_success', 'results'], to: 'importing' },
|
||||
{ name: 'import_succeeded', from: 'importing', to: 'imported'},
|
||||
{ name: 'import_failed', from: 'importing', to: 'results' }
|
||||
]
|
||||
})
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user