Files
tryton/sao/src/wizard.js
T
Cédric Krier bf5328229e Process response to wizard button only if the state is still the same
The response is asynchronous so when the event is processed the state may have
changed.

Closes #14847
(grafted from 1aad63cbbba72af487eddba703c136c9b8e83060)

--HG--
branch : 8.0
2026-05-19 17:18:34 +02:00

410 lines
15 KiB
JavaScript

/* This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. */
(function() {
'use strict';
Sao.Wizard = Sao.class_(Object, {
init: function(name) {
this.widget = jQuery('<div/>', {
'class': 'wizard'
});
this.name = name || Sao.i18n.gettext('Wizard');
this.action_id = null;
this.id = null;
this.ids = null;
this.action = null;
this.context = null;
this.states = {};
this.session_id = null;
this.start_state = null;
this.end_state = null;
this.screen = null;
this.screen_state = null;
this.state = null;
this.session = Sao.Session.current_session;
this.__prm = jQuery.Deferred();
this.__processing = false;
this.__waiting_response = false;
this.info_bar = new Sao.Window.InfoBar();
},
run: function(attributes) {
this.action = attributes.action;
this.action_id = attributes.data.action_id;
this.id = attributes.data.id;
this.ids = attributes.data.ids;
this.model = attributes.data.model;
this.context = jQuery.extend({}, attributes.context);
this.context.active_id = this.id;
this.context.active_ids = this.ids;
this.context.active_model = this.model;
this.context.action_id = this.action_id;
Sao.rpc({
'method': 'wizard.' + this.action + '.create',
'params': [this.session.context]
}, this.session).then(result => {
this.session_id = result[0];
this.start_state = this.state = result[1];
this.end_state = result[2];
return this.process();
}, () => {
this.destroy();
});
return this.__prm.promise();
},
process: function() {
if (this.__processing || this.__waiting_response) {
return jQuery.when();
}
this.__processing = true;
var process = function() {
if (this.state == this.end_state) {
return this.end();
}
var ctx = jQuery.extend({}, this.context);
var data = {};
if (this.screen) {
data[this.screen_state] = this.screen.get_on_change_value();
}
return Sao.rpc({
'method': 'wizard.' + this.action + '.execute',
'params': [this.session_id, data, this.state, ctx]
}, this.session).then(result => {
var prms = [];
if (result.view) {
this.clean();
var view = result.view;
this.update(view.fields_view, view.buttons);
prms.push(this.screen.new_(false).then(() => {
this.screen.current_record.set_default(
view.defaults || {})
.then(() => {
this.screen.current_record.set(
view.values || {});
this.update_buttons();
this.screen.set_cursor();
});
}));
this.screen_state = view.state;
this.__waiting_response = true;
} else {
this.state = this.end_state;
}
const execute_actions = () => {
var prms = [];
if (result.actions) {
for (const action of result.actions) {
var context = jQuery.extend({}, this.context);
// Remove wizard keys added by run
delete context.active_id;
delete context.active_ids;
delete context.active_model;
delete context.action_id;
prms.push(Sao.Action.execute(
action[0], action[1], context));
}
}
return jQuery.when.apply(jQuery, prms);
};
if (this.state == this.end_state) {
prms.push(this.end().then(execute_actions));
} else {
prms.push(execute_actions());
}
return jQuery.when.apply(jQuery, prms).then(
() => this.__processing = false);
}, result => {
if (!result || !this.screen) {
this.state = this.end_state;
this.end();
}
this.__processing = false;
});
};
return process.call(this);
},
destroy: function(action) {
// TODO
},
end: function() {
return Sao.rpc({
'method': 'wizard.' + this.action + '.delete',
'params': [this.session_id, this.session.context]
}, this.session).then(action => {
this.destroy(action);
this.__prm.resolve();
})
.fail(() => {
Sao.Logger.warn(
"Unable to delete session %s of wizard %s",
this.session_id, this.action);
this.__prm.reject();
});
},
clean: function() {
this.widget.empty();
this.states = {};
},
response: function(definition) {
this.__waiting_response = false;
this.screen.current_view.set_value();
if (definition.validate && !this.screen.current_record.validate(
null, null, null)) {
this.screen.display(true);
this.info_bar.add(this.screen.invalid_message(), 'danger');
return;
}
this.info_bar.clear();
this.state = definition.state;
this.process();
},
_get_button: function(definition) {
var style = 'btn-default';
if (definition.default) {
style = 'btn-primary';
} else if (definition.state == this.end_state) {
style = 'btn-link';
}
var button = new Sao.common.Button(
definition, undefined, undefined, style);
this.states[definition.state] = button;
return button;
},
record_modified: function() {
this.update_buttons();
this.info_bar.refresh();
},
update_buttons: function() {
var record = this.screen.current_record;
for (var state in this.states) {
var button = this.states[state];
button.set_state(record);
}
},
update: function(view, buttons) {
for (const button of buttons) {
this._get_button(button);
}
if (this.screen) {
this.screen.windows.splice(
this.screen.windows.indexOf(this), 1);
}
this.screen = new Sao.Screen(view.model,
{mode: [], context: this.context});
this.screen.add_view(view);
this.screen.switch_view();
this.screen.windows.push(this);
this.header.append(jQuery('<h4/>', {
'class': 'model-title',
'title': this.name,
}).text(Sao.common.ellipsize(this.name, 80)));
this.widget.append(this.screen.screen_container.el);
}
});
Sao.Wizard.create = function(attributes) {
var win;
if (attributes.window) {
win = new Sao.Wizard.Form(attributes.name);
var tab = new Sao.Tab.Wizard(win);
Sao.Tab.add(tab);
} else {
win = new Sao.Wizard.Dialog(attributes.name);
}
return win.run(attributes);
};
Sao.Wizard.Form = Sao.class_(Sao.Wizard, {
init: function(name) {
Sao.Wizard.Form._super.init.call(this, name);
this.tab = null; // Filled by Sao.Tab.Wizard
this.header = jQuery('<div/>', {
'class': 'modal-header',
});
this.form = jQuery('<div/>', {
'class': 'wizard-form',
}).append(this.header).append(this.widget);
this.footer = jQuery('<div/>', {
'class': 'modal-footer'
}).appendTo(this.form);
},
clean: function() {
Sao.Wizard.Form._super.clean.call(this);
this.header.empty();
this.footer.empty();
},
_get_button: function(definition) {
let state = this.state;
var button = Sao.Wizard.Form._super._get_button.call(this,
definition);
this.footer.append(button.el);
button.el.click(() => {
if (this.state === state) {
this.response(definition);
}
});
return button;
},
destroy: function(action) {
Sao.Wizard.Form._super.destroy.call(this, action);
switch (action) {
case 'reload menu':
Sao.Session.current_session.reload_context()
.then(function() {
Sao.menu();
});
break;
case 'reload context':
Sao.Session.current_session.reload_context();
break;
}
},
end: function() {
return Sao.Wizard.Form._super.end.call(this).always(
() => this.tab.close());
}
});
Sao.Wizard.Dialog = Sao.class_(Sao.Wizard, { // TODO nomodal
init: function(name) {
Sao.Wizard.Dialog._super.init.call(this, name);
var dialog = new Sao.Dialog(name, 'wizard-dialog', 'md', false);
this.dialog = dialog.modal;
this.header = dialog.header;
this.content = dialog.content;
this.footer = dialog.footer;
this.dialog.on('keydown', e => {
if (e.which == Sao.common.ESC_KEYCODE) {
e.preventDefault();
if (this.end_state in this.states) {
this.response(this.states[this.end_state].attributes);
}
}
});
dialog.body.append(this.widget).append(this.info_bar.el);
},
clean: function() {
Sao.Wizard.Dialog._super.clean.call(this);
this.header.empty();
this.footer.empty();
},
_get_button: function(definition) {
let state = this.state;
var button = Sao.Wizard.Dialog._super._get_button.call(this,
definition);
this.footer.append(button.el);
if (definition['default']) {
this.content.unbind('submit');
this.content.submit(e => {
e.preventDefault();
if (this.state === state) {
this.response(definition);
}
});
button.el.attr('type', 'submit');
} else {
button.el.click(() => {
if (this.state === state) {
this.response(definition);
}
});
}
return button;
},
update: function(view, buttons) {
this.content.unbind('submit');
Sao.Wizard.Dialog._super.update.call(this, view, buttons);
this.show();
},
destroy: function(action) {
Sao.Wizard.Dialog._super.destroy.call(this, action);
const destroy = () => {
this.dialog.remove();
var dialog = jQuery('.wizard-dialog').filter(':visible')[0];
var is_menu = false;
var screen;
if (!dialog) {
dialog = Sao.Tab.tabs.get_current();
}
if (!dialog ||
!this.model ||
(Sao.main_menu_screen &&
(Sao.main_menu_screen.model_name == this.model))) {
is_menu = true;
screen = Sao.main_menu_screen;
} else {
screen = dialog.screen;
}
if (screen) {
var prm = jQuery.when();
if (screen.current_record && !is_menu) {
var ids;
if (screen.model_name == this.model) {
ids = this.ids;
} else {
// Wizard run form a children record so reload
// parent record
ids = [screen.current_record.id];
}
prm = screen.reload(ids, true);
}
if (action) {
prm.then(function() {
screen.client_action(action);
});
}
}
};
if ((this.dialog.data('bs.modal') || {}).isShown) {
this.dialog.on('hidden.bs.modal', destroy);
this.dialog.modal('hide');
} else {
destroy();
}
},
show: function() {
var view = this.screen.current_view;
var expand;
if (view.view_type == 'form') {
expand = false;
var fields = view.get_fields();
for (const name of fields) {
var widgets = view.widgets[name];
for (const widget of widgets) {
if (widget.expand) {
expand = true;
break;
}
}
if (expand) {
break;
}
}
} else {
expand = true;
}
if (expand) {
this.dialog.find('.modal-dialog')
.removeClass('modal-md modal-sm')
.addClass('modal-lg');
} else {
this.dialog.find('.modal-dialog')
.removeClass('modal-lg modal-sm')
.addClass('modal-md');
}
this.dialog.modal('show');
},
hide: function() {
this.dialog.modal('hide');
},
state_changed: function() {
this.process();
}
});
}());