diff options
28 files changed, 2323 insertions, 1745 deletions
diff --git a/addons/web_editor/static/src/js/editor/snippets.options.js b/addons/web_editor/static/src/js/editor/snippets.options.js index 02f994fdeb2..abfcc6526e8 100644 --- a/addons/web_editor/static/src/js/editor/snippets.options.js +++ b/addons/web_editor/static/src/js/editor/snippets.options.js @@ -696,6 +696,10 @@ const SelectUserValueWidget = UserValueWidget.extend({ this.trigger_up('user_value_widget_opening'); } this.menuTogglerEl.classList.toggle('active'); + const activeButton = this._userValueWidgets.find(widget => widget.isActive()); + if (activeButton) { + this.menuEl.scrollTop = activeButton.el.offsetTop - (this.menuEl.offsetHeight / 2); + } }, }); diff --git a/addons/web_editor/static/src/scss/wysiwyg_snippets.scss b/addons/web_editor/static/src/scss/wysiwyg_snippets.scss index 5d610465f92..ed5ada70830 100644 --- a/addons/web_editor/static/src/scss/wysiwyg_snippets.scss +++ b/addons/web_editor/static/src/scss/wysiwyg_snippets.scss @@ -396,6 +396,8 @@ body.editor_enable.editor_has_snippets { z-index: $zindex-dropdown; border: 1px solid var(--o-we-color); background-color: var(--o-we-bg-color-dark); + max-height: 400px; + overflow-y: auto; } } diff --git a/addons/website_crm/static/src/js/website_crm_editor.js b/addons/website_crm/static/src/js/website_crm_editor.js index 44762e58c38..a0492d0eee5 100644 --- a/addons/website_crm/static/src/js/website_crm_editor.js +++ b/addons/website_crm/static/src/js/website_crm_editor.js @@ -7,8 +7,36 @@ var FormEditorRegistry = require('website_form.form_editor_registry'); var _t = core._t; FormEditorRegistry.add('create_lead', { - defaultTemplateName: 'website_crm.default_crm_form', - defaultTemplatePath: '/website_crm/static/src/xml/website_crm.xml', + formFields: [{ + type: 'char', + required: true, + name: 'contact_name', + string: 'Your Name', + }, { + type: 'char', + name: 'phone', + string: 'Phone Number', + }, { + type: 'email', + required: true, + name: 'email_from', + string: 'Your Email', + }, { + type: 'char', + required: true, + name: 'partner_name', + string: 'Your Company', + }, { + type: 'char', + modelRequired: true, + name: 'name', + string: 'Subject', + }, { + type: 'text', + required: true, + name: 'description', + string: 'Your Question', + }], fields: [{ name: 'team_id', type: 'many2one', diff --git a/addons/website_crm/static/src/xml/website_crm.xml b/addons/website_crm/static/src/xml/website_crm.xml deleted file mode 100644 index 71356a5176f..00000000000 --- a/addons/website_crm/static/src/xml/website_crm.xml +++ /dev/null @@ -1,55 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<templates> - - <t t-name="website_crm.default_crm_form"> - <div class="form-group row form-field o_website_form_required_custom"> - <div class="col-lg-3 col-md-4"> - <label class="col-form-label" for="contact_name">Your Name</label> - </div> - <div class="col-lg-7 col-md-8"> - <input type="text" class="form-control o_website_form_input" name="contact_name" required=""/> - </div> - </div> - <div class="form-group row form-field"> - <div class="col-lg-3 col-md-4"> - <label class="col-form-label" for="phone">Phone Number</label> - </div> - <div class="col-lg-7 col-md-8"> - <input type="text" class="form-control o_website_form_input" name="phone" /> - </div> - </div> - <div class="form-group row form-field o_website_form_required_custom"> - <div class="col-lg-3 col-md-4"> - <label class="col-form-label" for="email_from">Email</label> - </div> - <div class="col-lg-7 col-md-8"> - <input type="text" class="form-control o_website_form_input" name="email_from" required="" /> - </div> - </div> - <div class="form-group row form-field o_website_form_required_custom"> - <div class="col-lg-3 col-md-4"> - <label class="col-form-label" for="partner_name">Your Company</label> - </div> - <div class="col-lg-7 col-md-8"> - <input type="text" class="form-control o_website_form_input" name="partner_name" required="" /> - </div> - </div> - <div class="form-group row form-field o_website_form_required"> - <div class="col-lg-3 col-md-4"> - <label class="col-form-label" for="name">Subject</label> - </div> - <div class="col-lg-7 col-md-8"> - <input type="text" class="form-control o_website_form_input" name="name" required="" /> - </div> - </div> - <div class="form-group row form-field o_website_form_required_custom"> - <div class="col-lg-3 col-md-4"> - <label class="col-form-label" for="description">Your Question</label> - </div> - <div class="col-lg-7 col-md-8"> - <textarea class="form-control o_website_form_input" name="description" required=""></textarea> - </div> - </div> - </t> - -</templates> diff --git a/addons/website_crm/static/tests/tours/website_crm.js b/addons/website_crm/static/tests/tours/website_crm.js index 2923cb1ab50..9b615a56f8f 100644 --- a/addons/website_crm/static/tests/tours/website_crm.js +++ b/addons/website_crm/static/tests/tours/website_crm.js @@ -32,7 +32,7 @@ odoo.define('website_crm.tour', function(require) { run: "text ### TOUR DATA ###" }, { content: "Send the form", - trigger: ".o_website_form_send" + trigger: ".s_website_form_send" }, { content: "Check we were redirected to the success page", trigger: "#wrap:has(h1:contains('Thanks')):has(div.alert-success)" diff --git a/addons/website_crm/views/website_crm_templates.xml b/addons/website_crm/views/website_crm_templates.xml index a62eae17c89..2366a67a114 100644 --- a/addons/website_crm/views/website_crm_templates.xml +++ b/addons/website_crm/views/website_crm_templates.xml @@ -1,53 +1,82 @@ <?xml version="1.0" encoding="utf-8"?> <odoo> <template id="contactus_form" name="Contact Form (Opportunity)" inherit_id="website_form.contactus_form" customize_show="True"> - <xpath expr="//form[@id='contactus_form']" position="replace"> - <div t-ignore="true"> - <form action="/website_form/" method="post" data-model_name="crm.lead" data-success_page="/contactus-thank-you" class="s_website_form container-fluid mt32" enctype="multipart/form-data" data-editable-form="false"> - <div class="form-group row form-field o_website_form_required_custom"> - <label class="col-lg-3 col-md-4 col-form-label" for="contact_name">Your Name</label> - <div class="col-lg-7 col-md-8"> - <input type="text" class="form-control o_website_form_input" name="contact_name" t-att-value="request.params.get('contact_name', '')" required=""/> - </div> - </div> - <div class="form-group row form-field"> - <label class="col-lg-3 col-md-4 col-form-label" for="phone">Phone Number</label> - <div class="col-lg-7 col-md-8"> - <input type="text" class="form-control o_website_form_input" name="phone" t-att-value="request.params.get('phone', '')" /> - </div> - </div> - <div class="form-group row form-field o_website_form_required_custom"> - <label class="col-lg-3 col-md-4 col-form-label" for="email_from">Email</label> - <div class="col-lg-7 col-md-8"> - <input type="text" class="form-control o_website_form_input" name="email_from" required="" t-att-value="request.params.get('email_from', '')" /> - </div> - </div> - <div class="form-group row form-field"> - <label class="col-lg-3 col-md-4 col-form-label" for="partner_name">Your Company</label> - <div class="col-lg-7 col-md-8"> - <input type="text" class="form-control o_website_form_input" name="partner_name" t-att-value="request.params.get('partner_name', '')" /> - </div> - </div> - <div class="form-group row form-field o_website_form_required"> - <label class="col-lg-3 col-md-4 col-form-label" for="name">Subject</label> - <div class="col-lg-7 col-md-8"> - <input type="text" class="form-control o_website_form_input" name="name" required="" t-att-value="request.params.get('name', '')" /> - </div> - </div> - <div class="form-group row form-field o_website_form_required_custom"> - <label class="col-lg-3 col-md-4 col-form-label" for="description">Your Question</label> - <div class="col-lg-7 col-md-8"> - <textarea class="form-control o_website_form_input" name="description" required=""><t t-esc="request.params.get('description', '')"/></textarea> - </div> - </div> - <div class="form-group row"> - <div class="offset-lg-3 offset-md-4 col-md-8 col-lg-7"> - <a href="#" role="button" class="btn btn-primary btn-lg o_website_form_send">Send</a> - <span id="o_website_form_result"></span> + <xpath expr="//span[@data-for='contactus_form']" position="attributes"> + <attribute name="t-att-data-values">{ + 'contact_name': request.params.get('contact_name', ''), + 'phone': request.params.get('phone', ''), + 'email_from': request.params.get('email_from', ''), + 'partner_name': request.params.get('partner_name', ''), + 'name': request.params.get('name', ''), + 'description': request.params.get('description', ''), + }</attribute> + </xpath> + <xpath expr="//section[@id='contactus_section']" position="replace"> + <section class="s_website_form" data-vcss="001"> + <div class="container"> + <form id="contactus_form" action="/website_form/" method="post" enctype="multipart/form-data" data-model_name="crm.lead" data-success-mode="redirect" data-success-page="/contactus-thank-you" class="o_mark_required" data-mark="*" data-editable-form="false"> + <div class="s_website_form_rows row s_col_no_bgcolor"> + <div class="form-group col-12 s_website_form_field s_website_form_required" data-type="char"> + <div class="row s_col_no_resize s_col_no_bgcolor"> + <label class="col-form-label col-sm-auto s_website_form_label" style="width: 200px" for="opportunity1">Your Name<span class="s_website_form_mark"> *</span></label> + <div class="col-sm"> + <input id="opportunity1" type="text" class="form-control s_website_form_input" name="contact_name" required=""/> + </div> + </div> + </div> + <div class="form-group col-12 s_website_form_field" data-type="char"> + <div class="row s_col_no_resize s_col_no_bgcolor"> + <label class="col-form-label col-sm-auto s_website_form_label" style="width: 200px" for="opportunity2">Phone Number</label> + <div class="col-sm"> + <input id="opportunity2" type="text" class="form-control s_website_form_input" name="phone" /> + </div> + </div> + </div> + <div class="form-group col-12 s_website_form_field s_website_form_required" data-type="char"> + <div class="row s_col_no_resize s_col_no_bgcolor"> + <label class="col-form-label col-sm-auto s_website_form_label" style="width: 200px" for="opportunity3">Email<span class="s_website_form_mark"> *</span></label> + <div class="col-sm"> + <input id="opportunity3" type="text" class="form-control s_website_form_input" name="email_from" required="" /> + </div> + </div> + </div> + <div class="form-group col-12 s_website_form_field" data-type="char"> + <div class="row s_col_no_resize s_col_no_bgcolor"> + <label class="col-form-label col-sm-auto s_website_form_label" style="width: 200px" for="opportunity4">Your Company</label> + <div class="col-sm"> + <input id="opportunity4" type="text" class="form-control s_website_form_input" name="partner_name" /> + </div> + </div> + </div> + <div class="form-group col-12 s_website_form_field s_website_form_model_required" data-type="char"> + <div class="row s_col_no_resize s_col_no_bgcolor"> + <label class="col-form-label col-sm-auto s_website_form_label" style="width: 200px" for="opportunity5">Subject</label> + <div class="col-sm"> + <input id="opportunity5" type="text" class="form-control s_website_form_input" name="name" required="" /> + </div> + </div> + </div> + <div class="form-group col-12 s_website_form_field s_website_form_required" data-type="text"> + <div class="row s_col_no_resize s_col_no_bgcolor"> + <label class="col-form-label col-sm-auto s_website_form_label" style="width: 200px" for="opportunity6">Your Question<span class="s_website_form_mark"> *</span></label> + <div class="col-sm"> + <textarea id="opportunity6" class="form-control s_website_form_input" name="description" required=""></textarea> + </div> + </div> + </div> + <div class="form-group col-12 s_website_form_submit"> + <div class="row s_col_no_resize s_col_no_bgcolor"> + <div style="width: 200px" class="col-sm-auto s_website_form_label"/> + <div class="col-sm"> + <a href="#" role="button" class="btn btn-primary btn-lg s_website_form_send">Submit</a> + <span id="s_website_form_result"></span> + </div> + </div> + </div> </div> - </div> - </form> - </div> + </form> + </div> + </section> </xpath> </template> diff --git a/addons/website_form/static/src/js/website_form_editor_registry.js b/addons/website_form/static/src/js/website_form_editor_registry.js index f15cb9bd89e..93b183a9be8 100644 --- a/addons/website_form/static/src/js/website_form_editor_registry.js +++ b/addons/website_form/static/src/js/website_form_editor_registry.js @@ -16,8 +16,35 @@ var FormEditorRegistry = require('website_form.form_editor_registry'); var _t = core._t; FormEditorRegistry.add('send_mail', { - defaultTemplateName: 'website_form.default_contactus_form', - defaultTemplatePath: '/website_form/static/src/xml/website_form.xml', + formFields: [{ + type: 'char', + custom: true, + required: true, + name: 'Your Name', + }, { + type: 'char', + custom: true, + name: 'Phone Number', + }, { + type: 'email', + modelRequired: true, + name: 'email_from', + string: 'Your Email', + }, { + type: 'char', + custom: true, + name: 'Your Company', + }, { + type: 'char', + modelRequired: true, + name: 'subject', + string: 'Subject', + }, { + type: 'text', + custom: true, + required: true, + name: 'Your Question', + }], fields: [{ name: 'email_to', type: 'char', diff --git a/addons/website_form/static/src/scss/wysiwyg_snippets.scss b/addons/website_form/static/src/scss/wysiwyg_snippets.scss new file mode 100644 index 00000000000..009579f1819 --- /dev/null +++ b/addons/website_form/static/src/scss/wysiwyg_snippets.scss @@ -0,0 +1,23 @@ +#oe_snippets > .o_we_customize_panel we-customizeblock-option { + we-list { + .oe_we_table_wraper { + max-height: 200px; + overflow-y: auto; + table { + table-layout: fixed; + width: 100%; + input { + background-color: var(--o-we-bg-color-dark); + color: var(--o-we-color); + border: none; + width: 100%; + } + td { + &:first-child, &:last-child { + width: 28px; + } + } + } + } + } +} diff --git a/addons/website_form/static/src/snippets/s_website_form/000.js b/addons/website_form/static/src/snippets/s_website_form/000.js index bd97b94ddcf..7669d98b808 100644 --- a/addons/website_form/static/src/snippets/s_website_form/000.js +++ b/addons/website_form/static/src/snippets/s_website_form/000.js @@ -1,5 +1,5 @@ -odoo.define('website_form.animation', function (require) { -'use strict'; +odoo.define('website_form.s_website_form', function (require) { + 'use strict'; var core = require('web.core'); var time = require('web.time'); @@ -9,35 +9,44 @@ odoo.define('website_form.animation', function (require) { var _t = core._t; var qweb = core.qweb; - publicWidget.registry.form_builder_send = publicWidget.Widget.extend({ - selector: '.s_website_form', + publicWidget.registry.s_website_form = publicWidget.Widget.extend({ + selector: '.s_website_form form, form.s_website_form', // !compatibility + xmlDependencies: ['/website_form/static/src/xml/website_form.xml'], + events: { + 'click .s_website_form_send, .o_website_form_send': 'send', // !compatibility + }, + + /** + * @constructor + */ + init: function () { + this._super(...arguments); + this.__started = new Promise(resolve => this.__startResolve = resolve); + }, start: function () { var self = this; - this.templates_loaded = ajax.loadXML('/website_form/static/src/xml/website_form.xml', qweb); - this.$target.find('.o_website_form_send').on('click',function (e) {self.send(e);}); - // Initialize datetimepickers var datepickers_options = { - minDate: moment({ y: 1900 }), - maxDate: moment({ y: 9999, M: 11, d: 31 }), + minDate: moment({y: 1900}), + maxDate: moment({y: 9999, M: 11, d: 31}), calendarWeeks: true, - icons : { + icons: { time: 'fa fa-clock-o', date: 'fa fa-calendar', next: 'fa fa-chevron-right', previous: 'fa fa-chevron-left', up: 'fa fa-chevron-up', down: 'fa fa-chevron-down', - }, - locale : moment.locale(), - format : time.getLangDatetimeFormat(), + }, + locale: moment.locale(), + format: time.getLangDatetimeFormat(), }; - this.$target.find('.o_website_form_datetime').datetimepicker(datepickers_options); + this.$target.find('.s_website_form_datetime, .o_website_form_datetime').datetimepicker(datepickers_options); // !compatibility // Adapt options to date-only pickers datepickers_options.format = time.getLangDateFormat(); - this.$target.find('.o_website_form_date').datetimepicker(datepickers_options); + this.$target.find('.s_website_form_date, .o_website_form_date').datetimepicker(datepickers_options); // !compatibility // Display form values from tag having data-for attribute // It's necessary to handle field values generated on server-side @@ -56,21 +65,35 @@ odoo.define('website_form.animation', function (require) { }); } - return this._super.apply(this, arguments); + return this._super(...arguments).then(() => this.__startResolve()); }, destroy: function () { this._super.apply(this, arguments); this.$target.find('button').off('click'); + + // Empty imputs + this.$target.find('input[type="text"], input[type="email"], input[type="number"], textarea').val(''); + + // Remove saving of the error colors + this.$target.find('.o_has_error').removeClass('o_has_error').find('.form-control, .custom-select').removeClass('is-invalid'); + + // Remove the status message + this.$target.find('#s_website_form_result, #o_website_form_result').empty(); // !compatibility + + // Remove the success message and display the form + this.$target.removeClass('d-none'); + this.$target.parent().find('.s_website_form_end_message').addClass('d-none'); }, send: function (e) { - e.preventDefault(); // Prevent the default submit behavior - this.$target.find('.o_website_form_send').off().addClass('disabled'); // Prevent users from crazy clicking + e.preventDefault(); // Prevent the default submit behavior + // Prevent users from crazy clicking + this.$target.find('.s_website_form_send, .o_website_form_send').addClass('disabled'); // !compatibility var self = this; - self.$target.find('#o_website_form_result').empty(); + self.$target.find('#s_website_form_result, #o_website_form_result').empty(); // !compatibility if (!self.check_error_fields({})) { self.update_status('invalid'); return false; @@ -110,8 +133,10 @@ odoo.define('website_form.animation', function (require) { }); // Post form and handle result - ajax.post(this.$target.attr('action') + (this.$target.data('force_action')||this.$target.data('model_name')), form_values) + ajax.post(this.$target.attr('action') + (this.$target.data('force_action') || this.$target.data('model_name')), form_values) .then(function (result_data) { + // Restore send button behavior + self.$target.find('.s_website_form_send, .o_website_form_send').removeClass('disabled'); // !compatibility result_data = JSON.parse(result_data); if (!result_data.id) { // Failure, the server didn't return the created record ID @@ -122,19 +147,30 @@ odoo.define('website_form.animation', function (require) { } } else { // Success, redirect or update status - var success_page = self.$target.attr('data-success_page'); - if (success_page) { - $(window.location).attr('href', success_page); + let successMode = self.$target[0].dataset.successMode; + let successPage = self.$target[0].dataset.successPage; + if (!successMode) { + successPage = self.$target.attr('data-success_page'); // Compatibility + successMode = successPage ? 'redirect' : 'nothing'; } - else { - self.update_status('success'); + switch (successMode) { + case 'redirect': + $(window.location).attr('href', successPage); + break; + case 'message': + self.$target[0].classList.add('d-none'); + self.$target[0].parentElement.querySelector('.s_website_form_end_message').classList.remove('d-none'); + break; + default: + self.update_status('success'); + break; } // Reset the form self.$target[0].reset(); } }) - .guardedCatch(function (){ + .guardedCatch(function () { self.update_status('error'); }); }, @@ -143,12 +179,12 @@ odoo.define('website_form.animation', function (require) { var self = this; var form_valid = true; // Loop on all fields - this.$target.find('.form-field').each(function (k, field){ + this.$target.find('.form-field, .s_website_form_field').each(function (k, field) { // !compatibility var $field = $(field); var field_name = $field.find('.col-form-label').attr('for'); // Validate inputs for this field - var inputs = $field.find('.o_website_form_input:not(#editable_select)'); + var inputs = $field.find('.s_website_form_input, .o_website_form_input').not('#editable_select'); // !compatibility var invalid_inputs = inputs.toArray().filter(function (input, k, inputs) { // Special check for multiple required checkbox for same // field as it seems checkValidity forces every required @@ -159,17 +195,17 @@ odoo.define('website_form.animation', function (require) { // Considering we are currently processing a single // field, we can assume that all checkboxes in the // inputs variable have the same name - var checkboxes = _.filter(inputs, function (input){ + var checkboxes = _.filter(inputs, function (input) { return input.required && input.type === 'checkbox'; }); - return !_.any(checkboxes, function (checkbox) { return checkbox.checked; }); + return !_.any(checkboxes, checkbox => checkbox.checked); // Special cases for dates and datetimes - } else if ($(input).hasClass('o_website_form_date')) { + } else if ($(input).hasClass('s_website_form_date') || $(input).hasClass('o_website_form_date')) { // !compatibility if (!self.is_datetime_valid(input.value, 'date')) { return true; } - } else if ($(input).hasClass('o_website_form_datetime')) { + } else if ($(input).hasClass('s_website_form_datetime') || $(input).hasClass('o_website_form_datetime')) { // !compatibility if (!self.is_datetime_valid(input.value, 'datetime')) { return true; } @@ -179,9 +215,9 @@ odoo.define('website_form.animation', function (require) { // Update field color if invalid or erroneous $field.removeClass('o_has_error').find('.form-control, .custom-select').removeClass('is-invalid'); - if (invalid_inputs.length || error_fields[field_name]){ - $field.addClass('o_has_error').find('.form-control, .custom-select').addClass('is-invalid') - if (_.isString(error_fields[field_name])){ + if (invalid_inputs.length || error_fields[field_name]) { + $field.addClass('o_has_error').find('.form-control, .custom-select').addClass('is-invalid'); + if (_.isString(error_fields[field_name])) { $field.popover({content: error_fields[field_name], trigger: 'hover', container: 'body', placement: 'top'}); // update error message and show it. $field.data("bs.popover").options.content = error_fields[field_name]; @@ -210,32 +246,36 @@ odoo.define('website_form.animation', function (require) { parse_date: function (value, type_of_date, value_if_empty) { var date_pattern = time.getLangDateFormat(), time_pattern = time.getLangTimeFormat(); - var date_pattern_wo_zero = date_pattern.replace('MM','M').replace('DD','D'), - time_pattern_wo_zero = time_pattern.replace('HH','H').replace('mm','m').replace('ss','s'); + var date_pattern_wo_zero = date_pattern.replace('MM', 'M').replace('DD', 'D'), + time_pattern_wo_zero = time_pattern.replace('HH', 'H').replace('mm', 'm').replace('ss', 's'); switch (type_of_date) { case 'datetime': var datetime = moment(value, [date_pattern + ' ' + time_pattern, date_pattern_wo_zero + ' ' + time_pattern_wo_zero], true); - if (datetime.isValid()) + if (datetime.isValid()) { return time.datetime_to_str(datetime.toDate()); + } throw new Error(_.str.sprintf(_t("'%s' is not a correct datetime"), value)); case 'date': var date = moment(value, [date_pattern, date_pattern_wo_zero], true); - if (date.isValid()) + if (date.isValid()) { return time.date_to_str(date.toDate()); + } throw new Error(_.str.sprintf(_t("'%s' is not a correct date"), value)); } return value; }, update_status: function (status) { - var self = this; - if (status !== 'success') { // Restore send button behavior if result is an error - this.$target.find('.o_website_form_send').on('click',function (e) {self.send(e);}).removeClass('disabled'); + if (status !== 'success') { // Restore send button behavior if result is an error + this.$target.find('.s_website_form_send, .o_website_form_send').removeClass('disabled'); // !compatibility } - var $result = this.$('#o_website_form_result'); - this.templates_loaded.then(function () { - $result.replaceWith(qweb.render("website_form.status_" + status)); - }); + var $result = this.$('#s_website_form_result, #o_website_form_result'); // !compatibility + + // Note: we still need to wait that the widget is properly started + // before any qweb rendering which depends on xmlDependencies + // because the event handlers are binded before the call to + // willStart for public widgets... + this.__started.then(() => $result.replaceWith(qweb.render(`website_form.status_${status}`))); }, }); }); diff --git a/addons/website_form/static/src/snippets/s_website_form/000.scss b/addons/website_form/static/src/snippets/s_website_form/000.scss index 936edad2715..0ba43a0c6e4 100644 --- a/addons/website_form/static/src/snippets/s_website_form/000.scss +++ b/addons/website_form/static/src/snippets/s_website_form/000.scss @@ -1,57 +1,61 @@ -// Radio buttons and checkboxes flex layout -.o_website_form_flex { - display: flex; - flex-wrap: wrap; - - &.o_website_form_flex_fw > .o_website_form_flex_item { - flex-basis: 100%; +.editor_enable .s_website_form:not([data-vcss]) { + // Select inputs do not trigger the default browser behavior + // Since we use a custom editable element + .form-field select { + pointer-events: none; } - &:not(.o_website_form_flex_fw) > .o_website_form_flex_item { - // col-lg-4 - flex-basis: 33%; - - // col-md-6 - @include media-breakpoint-down(md) { - flex-basis: 50%; - } - // col-12 - @include media-breakpoint-down(sm) { - flex-basis: 100%; - } + .o_website_form_field_hidden { + display: flex; + opacity: 0.5; } -} -// Hidden field is only partially hidden in editor -.o_website_form_field_hidden { - display: none; + // Quickfix to display the editable select as a single big field + #editable_select.form-control { + height: 100%; + } } -.editor_enable .o_website_form_field_hidden { - display: flex; - opacity: 0.5; -} +.s_website_form:not([data-vcss]) { + // Radio buttons and checkboxes flex layout + .o_website_form_flex { + display: flex; + flex-wrap: wrap; -// Select inputs do not trigger the default browser behavior -// Since we use a custom editable element -.editor_enable .s_website_form .form-field select { - pointer-events: none; -} + &.o_website_form_flex_fw > .o_website_form_flex_item { + flex-basis: 100%; + } + &:not(.o_website_form_flex_fw) > .o_website_form_flex_item { + // col-lg-4 + flex-basis: 33%; + + // col-md-6 + @include media-breakpoint-down(md) { + flex-basis: 50%; + } + + // col-12 + @include media-breakpoint-down(sm) { + flex-basis: 100%; + } + } + } -// Required fields have a star which is not part of the field label -.o_website_form_required, .o_website_form_required_custom { - .col-form-label:after { - content: ' *'; + // Hidden field is only partially hidden in editor + .o_website_form_field_hidden { + display: none; } -} -// Quickfix to display the editable select as a single big field -#editable_select.form-control { - height: 100%; -} + // Required fields have a star which is not part of the field label + .o_website_form_required, .o_website_form_required_custom { + .col-form-label:after { + content: ' *'; + } + } -// Fix for firefox browse button which is too big for Bootstrap form-field -// http://stackoverflow.com/questions/22049739/fix-for-firefox-file-input-using-bootstrap-3-1 -.form-field input[type=file].form-control { - height: 100%; + // Fix for firefox browse button which is too big for Bootstrap form-field + // http://stackoverflow.com/questions/22049739/fix-for-firefox-file-input-using-bootstrap-3-1 + .form-field input[type=file].form-control { + height: 100%; + } } diff --git a/addons/website_form/static/src/snippets/s_website_form/001.scss b/addons/website_form/static/src/snippets/s_website_form/001.scss new file mode 100644 index 00000000000..561ac208543 --- /dev/null +++ b/addons/website_form/static/src/snippets/s_website_form/001.scss @@ -0,0 +1,41 @@ +.editor_enable .s_website_form[data-vcss="001"] { + // Hidden field is only partially hidden in editor + .s_website_form_field_hidden { + display: block; + opacity: 0.5; + } + // Select inputs do not trigger the default browser behavior + // Since we use a custom editable element + .s_website_form_field select { + pointer-events: none; + } + // Display the editable select as a single big field + #editable_select.form-control { + height: auto; + } +} + +.s_website_form[data-vcss="001"] { + .s_website_form_label { + @include media-breakpoint-down(xs) { + width: auto !important; + } + } + + .s_website_form_field_hidden { + display: none; + } + + span.s_website_form_mark { + font-size: 0.85em; + font-weight: 400; + } + + // The snippet editor uses padding and not margin. + // This will include bootstrap margin in the dragable y axes + .s_website_form_rows > .form-group { + margin-bottom: 0; + padding-top: 0.5rem; + padding-bottom: 0.5rem; + } +} diff --git a/addons/website_form/static/src/snippets/s_website_form/options.js b/addons/website_form/static/src/snippets/s_website_form/options.js index d659f51dd06..9a69b3efcda 100644 --- a/addons/website_form/static/src/snippets/s_website_form/options.js +++ b/addons/website_form/static/src/snippets/s_website_form/options.js @@ -1,635 +1,1269 @@ odoo.define('website_form_editor', function (require) { - 'use strict'; - - /** - * @todo this should be entirely refactored - */ - - var ajax = require('web.ajax'); - var core = require('web.core'); - var Dialog = require('web.Dialog'); - var FormEditorRegistry = require('website_form.form_editor_registry'); - var options = require('web_editor.snippets.options'); - var wUtils = require('website.utils'); - var Wysiwyg = require('web_editor.wysiwyg'); - - var qweb = core.qweb; - var _t = core._t; - - var FormEditorDialog = Dialog.extend({ - /** - * @constructor - */ - init: function (parent, options) { - this._super(parent, _.extend({ - buttons: [{ - text: _t('Save'), - classes: 'btn-primary', - close: true, - click: this._onSaveModal.bind(this), - }, { - text: _t('Cancel'), - close: true - }], - }, options)); - - this.opened(() => { - this.$modal.find('.o_website_form_input, .modal-footer .btn').first().focus(); - }); - }, - - //---------------------------------------------------------------------- - // Handlers - //---------------------------------------------------------------------- - - /** - * @private - */ - _onSaveModal: function () { - if (this.$el[0].checkValidity()) { - this.trigger_up('save'); - } else { - _.each(this.$el.find('.o_website_form_input'), function (input) { - var $field = $(input).closest('.form-field'); - $field.removeClass('o_has_error').find('.form-control, .custom-select').removeClass('is-invalid'); - if (!input.checkValidity()) { - $field.addClass('o_has_error').find('.form-control, .custom-select').addClass('is-invalid'); - } - }); - } - }, - }); - - options.registry['website_form_editor'] = options.Class.extend({ - xmlDependencies: ['/website_form/static/src/xml/website_form_editor.xml'], - - start: function () { - this.$target.addClass('o_fake_not_editable').attr('contentEditable', false); - this.$target.find('label:not(:has(span)), label span, .o_form_heading').addClass('o_fake_editable').attr('contentEditable', true); - return this._super.apply(this, arguments); - }, - - // Return the fields promise if we already issued a model - // fields fetch request, or issue said request. - fields: function () { - return this.fields_promise || this.fetch_model_fields(); - }, - - fetch_model_fields: function () { - return this._rpc({ - model: "ir.model", - method: "get_authorized_fields", - args: [this.$target.closest('form').attr('data-model_name')], - }).then(function (fields) { - // The get_fields function doesn't return the name - // in the field dict since it uses it has the key - _.each(fields, function (field, field_name) { - field.name = field_name; - }); - return fields; - }); - }, - - // Choose a model modal - website_form_model_modal: function (previewMode, widgetValue, params) { - var self = this; - this._rpc({ - model: "ir.model", - method: "search_read", +'use strict'; + +const core = require('web.core'); +const FormEditorRegistry = require('website_form.form_editor_registry'); +const options = require('web_editor.snippets.options'); + +const qweb = core.qweb; +const _t = core._t; + +const FormEditor = options.Class.extend({ + xmlDependencies: ['/website_form/static/src/xml/website_form_editor.xml'], + + //---------------------------------------------------------------------- + // Private + //---------------------------------------------------------------------- + + /** + * Returns a promise which is resolved once the records of the field + * have been retrieved. + * + * @private + * @param {Object} field + * @returns {Promise<Object>} + */ + _fetchFieldRecords: async function (field) { + // Convert the required boolean to a value directly usable + // in qweb js to avoid duplicating this in the templates + field.required = field.required ? 1 : null; + + // Fetch possible values for relation fields + if (!field.records && field.relation && field.relation !== 'ir.attachment') { + field.records = await this._rpc({ + model: field.relation, + method: 'search_read', args: [ - [['website_form_access', '=', true]], - ['id', 'model', 'name', 'website_form_label', 'website_form_key'] + field.domain, + ['display_name'] ], - }).then(function (models) { - self.models = models; - var selectedModel = self.$target.attr('data-model_name') || 'mail.mail'; - // Models selection input - var modelSelection = qweb.render("website_form.field_many2one", { - field: { - name: 'model_selection', - string: 'Action', - required: true, - records: _.map(models, function (m) { - return { - id: m.id, - display_name: m.website_form_label || m.name, - selected: (m.model === selectedModel) ? 1 : null, - }; - }), - } - }); + }); + } + return field.records; + }, + /** + * Returns a field object + * + * @private + * @param {string} type the type of the field + * @param {string} name The name of the field used also as label + * @returns {Object} + */ + _getCustomField: function (type, name) { + return { + name: name, + string: name, + custom: true, + type: type, + // Default values for x2many fields + records: [{ + id: _t('Option 1'), + display_name: _t('Option 1'), + }, { + id: _t('Option 2'), + display_name: _t('Option 2'), + }, { + id: _t('Option 3'), + display_name: _t('Option 3'), + }], + // Default values for selection fields + selection: [[ + _t('Option 1'), + _t('Option 1'), + ], [ + _t('Option 2'), + _t('Option 2'), + ], [ + _t('Option 3'), + _t('Option 3'), + ]], + }; + }, + /** + * Returns the default formatInfos of a field. + * + * @private + * @returns {Object} + */ + _getDefaultFormat: function () { + const formOptions = this.el.closest('.o_we_customize_panel'); + return { + labelWidth: formOptions && formOptions.querySelector('we-customizeblock-options [data-select-style] input').value + 'px', + labelPosition: 'left', + multiPosition: 'horizontal', + requiredMark: this._isRequiredMark(), + optionalMark: this._isOptionalMark(), + mark: this._getMark(), + }; + }, + /** + * @private + * @returns {string} + */ + _getMark: function () { + return this.$target[0].dataset.mark; + }, + /** + * @private + * @returns {boolean} + */ + _isOptionalMark: function () { + return this.$target[0].classList.contains('o_mark_optional'); + }, + /** + * @private + * @returns {boolean} + */ + _isRequiredMark: function () { + return this.$target[0].classList.contains('o_mark_required'); - // Success page input - var successPage = qweb.render("website_form.field_char", { - field: { - name: 'success_page', - string: 'Thank You Page', - value: self.$target.attr('data-success_page') - } - }); + }, + /** + * @private + * @param {Object} field + * @returns {Promise<HTMLElement>} + */ + _renderField: function (field) { + field.id = Math.random().toString(36).substring(2, 15); // Big unique ID + const template = document.createElement('template'); + template.innerHTML = qweb.render("website_form.field_" + field.type, {field: field}).trim(); + return template.content.firstElementChild; + }, +}); - var save = function () { - var successPage = this.$el.find("[name='success_page']").val(); - self.init_form(); - self.$target.attr('data-success_page', successPage); - - this.$el.find('.o_form_parameter_custom').each(function () { - var $field = $(this).find('.o_website_form_input'); - var value = $field.val(); - var fieldName = $field.attr('name'); - self.$target.find('.form-group:has("[name=' + fieldName + ']")').remove(); - if (value) { - var $hiddenField = $(qweb.render('website_form.field_char', { - field: { - name: fieldName, - value: value, - } - })).addClass('d-none'); - self.$target.find('.form-group:has(".o_website_form_send")').before($hiddenField); - } - }); - }; +const FieldEditor = FormEditor.extend({ + /** + * @override + */ + init: function () { + this._super.apply(this, arguments); + this.formEl = this.$target[0].closest('form'); + }, - var cancel = function () { - if (!self.$target.attr('data-model_name')) { - self.$target.remove(); - } - }; + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- - var $content = $('<form role="form">' + modelSelection + successPage + '</form>'); - var dialog = new FormEditorDialog(self, { - title: 'Form Parameters', - size: 'medium', - $content: $content, - }).open(); - dialog.on('closed', this, cancel); - dialog.on('save', this, ev => { - ev.stopPropagation(); - save.call(dialog); - }); + /** + * Returns the target as a field Object + * + * @private + * @returns {Object} + */ + _getActiveField: function () { + let field; + if (this._isFieldCustom()) { + const name = this.$target[0].querySelector('.s_website_form_label').firstChild.textContent.trim(); + field = this._getCustomField(this.$target[0].dataset.type, name); + } else { + field = this.fields[this._getFieldName()]; + } + this._setActiveProperties(field); + return field; + }, + /** + * Returns the format object of a field containing + * the position, labelWidth and bootstrap col class + * + * @private + * @returns {Object} + */ + _getFieldFormat: function () { + let requiredMark, optionalMark; + const mark = this.$target[0].querySelector('span.s_website_form_mark'); + if (mark) { + requiredMark = this._isFieldRequired(); + optionalMark = !requiredMark; + } + const multipleInput = this._getMultipleInputs(); + const format = { + labelPosition: this._getLabelPosition(), + labelWidth: this.$target[0].querySelector('.s_website_form_label').style.width, + multiPosition: multipleInput && multipleInput.dataset.display || 'horizontal', + col: [...this.$target[0].classList].filter(el => el.match(/^col-/g)).join(' '), + requiredMark: requiredMark, + optionalMark: optionalMark, + mark: mark && mark.textContent, + }; + return format; + }, + /** + * Returns the name of the field + * + * @private + * @returns {string} + */ + _getFieldName: function () { + const multipleName = this.$target[0].querySelector('.s_website_form_multiple'); + return multipleName ? multipleName.dataset.name : this.$target[0].querySelector('.s_website_form_input').name; + }, + /** + * Returns the type of the field, can be used for both custom and existing fields + * + * @private + * @returns {string} + */ + _getFieldType: function () { + return this.$target[0].dataset.type; + }, + /** + * @private + * @returns {string} + */ + _getLabelPosition: function () { + const label = this.$target[0].querySelector('.s_website_form_label'); + if (this.$target[0].querySelector('.row:not(.s_website_form_multiple)')) { + return label.classList.contains('text-right') ? 'right' : 'left'; + } else { + return label.classList.contains('d-none') ? 'none' : 'top'; + } + }, + /** + * Returns the multiple checkbox/radio element if it exist else null + * + * @private + * @returns {HTMLElement} + */ + _getMultipleInputs: function () { + return this.$target[0].querySelector('.s_website_form_multiple'); + }, + /** + * @private + * @returns {string} + */ + _getPlaceholder: function () { + const input = this._getPlaceholderInput(); + return input ? input.placeholder : ''; + }, + /** + * Returns the field's input if it is placeholder compatible, else null + * + * @private + * @returns {HTMLElement} + */ + _getPlaceholderInput: function () { + return this.$target[0].querySelector('input[type="text"], input[type="email"], input[type="number"] , textarea'); + }, + /** + * Returns true if the field is a custom field, false if it is an existing field + * + * @private + * @returns {boolean} + */ + _isFieldCustom: function () { + return !!this.$target[0].classList.contains('s_website_form_custom'); + }, + /** + * Returns true if the field is required by the model or by the user. + * + * @private + * @returns {boolean} + */ + _isFieldRequired: function () { + const classList = this.$target[0].classList; + return classList.contains('s_website_form_required') || classList.contains('s_website_form_model_required'); + }, + /** + * Set the active field properties on the field Object + * + * @param {Object} field Field to complete with the active field info + */ + _setActiveProperties(field) { + field.placeholder = this._getPlaceholder(); + field.required = this._isFieldRequired(); + field.hidden = this.$target[0].classList.contains('s_website_form_field_hidden'); + field.formatInfo = this._getFieldFormat(); + }, + /** + * Set the placeholder on the current field if the input allow it + * + * @private + * @param {string} value + */ + _setPlaceholder: function (value) { + const input = this._getPlaceholderInput(); + if (input) { + input.placeholder = value; + } + }, +}); + +options.registry.WebsiteFormEditor = FormEditor.extend({ + events: _.extend({}, options.Class.prototype.events || {}, { + 'click .toggle-edit-message': '_onToggleEndMessageClick', + }), + + /** + * @override + */ + willStart: async function () { + const _super = this._super.bind(this); + + // Hide change form parameters option for forms + // e.g. User should not be enable to change existing job application form + // to opportunity form in 'Apply job' page. + this.modelCantChange = this.$target.attr('hide-change-model') !== undefined; + if (this.modelCantChange) { + return _super(...arguments); + } + + // Get list of website_form compatible models. + this.models = await this._rpc({ + model: "ir.model", + method: "search_read", + args: [ + [['website_form_access', '=', true]], + ['id', 'model', 'name', 'website_form_label', 'website_form_key'] + ], + }); - wUtils.autocompleteWithPages(self, $content.find("input[name='success_page']")); - self.originSuccessPage = $content.find("input[name='success_page']").val(); - self.originFormID = $content.find("[name='model_selection']").val(); - self._renderParameterFields($content); + const targetModelName = this.$target[0].dataset.model_name || 'mail.mail'; + this.activeForm = _.findWhere(this.models, {model: targetModelName}); + // Create the Form Action select + this.selectActionEl = document.createElement('we-select'); + this.selectActionEl.setAttribute('string', 'Action'); + this.selectActionEl.dataset.noPreview = 'true'; + this.models.forEach(el => { + const option = document.createElement('we-button'); + option.textContent = el.website_form_label; + option.dataset.selectAction = el.id; + this.selectActionEl.append(option); + }); - $content.find("[name='model_selection']").on('change', function () { - self._renderParameterFields($content); + return _super(...arguments); + }, + /** + * @override + */ + start: function () { + const proms = [this._super(...arguments)]; + // Disable text edition + this.$target.attr('contentEditable', false); + // Make button editable + this.$target.find('.s_website_form_send').parent().attr('contentEditable', true); + // Get potential message + this.$message = this.$target.parent().find('.s_website_form_end_message'); + this.showEndMessage = false; + // If the form has no model it means a new snippet has been dropped. + // Apply the default model selected in willStart on it. + if (!this.$target[0].dataset.model_name) { + proms.push(this._applyFormModel()); + } + return Promise.all(proms); + }, + /** + * @override + */ + cleanForSave: function () { + const model = this.$target[0].dataset.model_name; + // because apparently this can be called on the wrong widget and + // we may not have a model, or fields... + if (model) { + // we may be re-whitelisting already whitelisted fields. Doesn't + // really matter. + const fields = [...this.$target[0].querySelectorAll('.s_website_form_field:not(.s_website_form_custom) input')].map(el => el.name); + if (fields.length) { + // ideally we'd only do this if saving the form + // succeeds... but no idea how to do that + this._rpc({ + model: 'ir.model.fields', + method: 'formbuilder_whitelist', + args: [model, _.uniq(fields)], }); - }); - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * @private - * @returns {Promise} - */ - _renderParameterFields: function ($modal) { - var self = this; - var $successPage = $modal.find("[name='success_page']"); - $modal.find('.o_form_parameter_custom').remove(); - var id = $modal.find("[name='model_selection']").val(); - this.activeForm = _.findWhere(this.models, {id: parseInt(id)}); - var formKey = this.activeForm.website_form_key; - if (!formKey) { - return Promise.resolve(); } - var proms = []; - var formInfo = FormEditorRegistry.get(formKey); + } + if (this.$message.length) { + this.$target.removeClass('d-none'); + this.$message.addClass("d-none"); + } + }, + /** + * @override + */ + updateUI: async function () { + // If we want to rerender the xml we need to avoid the updateUI + // as they are asynchronous and the ui might try to update while + // we are building the UserValueWidgets. + if (this.rerender) { + this.rerender = false; + await this._rerenderXML(); + return; + } + await this._super.apply(this, arguments); + // End Message UI + this.updateUIEndMessage(); + }, + /** + * @see this.updateUI + */ + updateUIEndMessage: function () { + this.$target.toggleClass("d-none", this.showEndMessage); + this.$message.toggleClass("d-none", !this.showEndMessage); + }, + /** + * @override + */ + notify: async function (name, data) { + await this._super(...arguments); + if (name === 'field_mark') { + this._setLabelsMark(); + } else if (name === 'add_field') { + const field = this._getCustomField('char', 'Custom Text'); + field.formatInfo = data.formatInfo; + field.formatInfo.requiredMark = this._isRequiredMark(); + field.formatInfo.optionalMark = this._isOptionalMark(); + field.formatInfo.mark = this._getMark(); + const htmlField = this._renderField(field); + data.$target.after(htmlField); + this.trigger_up('activate_snippet', { + $snippet: $(htmlField), + }); + } + }, - if (this.originFormID === id) { - $successPage.val(this.originSuccessPage || formInfo.successPage || '/contactus-thank-you'); - } else { - $successPage.val(formInfo.successPage || '/contactus-thank-you'); + //-------------------------------------------------------------------------- + // Options + //-------------------------------------------------------------------------- + + /** + * Select the value of a field (hidden) that will be used on the model as a preset. + * ie: The Job you apply for if the form is on that job's page. + */ + addActionField: function (previewMode, value, params) { + let fieldName = params.fieldName; + if (params.isSelect === 'true') { + value = parseInt(value); + } + this.$target.find(`.s_website_form_field.d-none:has(input[name="${params.fieldName}"])`).remove(); + if (value) { + const hiddenField = qweb.render('website_form.field_hidden', { + field: { + name: fieldName, + value: value, + }, + }); + this.$target.find('.s_website_form_submit').before(hiddenField); + } + }, + /** + * Changes the onSuccess event. + */ + onSuccess: function (previewMode, value, params) { + this.$target[0].dataset.successMode = value; + if (value === 'message') { + if (!this.$message.length) { + this.$message = $(qweb.render('website_form.s_website_form_end_message')); } + this.$target.after(this.$message); + } else { + this.$message.remove(); + } + }, + /** + * Select the model to create with the form. + */ + selectAction: async function (previewMode, value, params) { + if (this.modelCantChange) { + return; + } + await this._applyFormModel(parseInt(value)); + this.rerender = true; + }, + /** + * @override + */ + selectClass: function (previewMode, value, params) { + this._super(...arguments); + if (params.name === 'field_mark_select') { + this._setLabelsMark(); + } + }, + /** + * Set the mark string on the form + */ + setMark: function (previewMode, value, params) { + this.$target[0].dataset.mark = value.trim(); + this._setLabelsMark(); + }, - if (formInfo.fields && formInfo.fields.length) { - _.each(formInfo.fields, function (field) { - var value = self.$target.find('[name="' + field.name + '"]').val(); - proms.push(self.render_field(field).then(function ($field) { - $field.addClass('o_form_parameter_custom'); - // Remove content editable (Added by render_field) - $field.find('label').removeAttr('contenteditable'); - // Set tooltip on label - $field.find('label').attr('title', field.title); - // Set value - $field.find('.o_website_form_input').val(value); - $modal.append($field); - })); - }); + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + _computeWidgetState: function (methodName, params) { + switch (methodName) { + case 'selectAction': + return this.activeForm.id; + case 'addActionField': { + const value = this.$target.find(`.s_website_form_field.d-none input[name="${params.fieldName}"]`).val(); + if (value) { + return value; + } else { + return params.isSelect ? '0' : ''; + } } - return Promise.all(proms); - }, - - // Choose a field modal - website_form_field_modal: function (previewMode, widgetValue, params) { - var self = this; - - this.fields().then(function (fields) { - // Make a nice array to render the select input - var fields_array = _.map(fields, function (v, k) { return {id: k, name: v.name, display_name: v.string}; }); - // Filter the fields to remove the ones already in the form - var fields_in_form = _.map(self.$target.find('.col-form-label'), function (label) { return label.getAttribute('for'); }); - var available_fields = _.filter(fields_array, function (field) { return !_.contains(fields_in_form, field.name); }); - // Render the select input - var fieldSelection = qweb.render("website_form.field_many2one", { - field: { - name: 'field_selection', - string: 'Field', - records: _.sortBy(available_fields, 'display_name') - } - }); + case 'onSuccess': + return this.$target[0].dataset.successMode; + case 'setMark': + return this._getMark(); + } + return this._super(...arguments); + }, + /** + * @override + */ + _renderCustomXML: function (uiFragment) { + if (this.modelCantChange) { + return; + } + // Add Action select + const firstOption = uiFragment.querySelector(':first-child'); + uiFragment.insertBefore(this.selectActionEl.cloneNode(true), firstOption); - var save = function () { - var selectedFieldName = this.$el.find("[name='field_selection']").val(); - var selectedField = fields[selectedFieldName]; - self.append_field(selectedField); - }; + // Add Action related options + const formKey = this.activeForm.website_form_key; + const formInfo = FormEditorRegistry.get(formKey); + if (!formInfo || !formInfo.fields) { + return; + } + const proms = formInfo.fields.map(field => this._fetchFieldRecords(field)); + return Promise.all(proms).then(() => { + formInfo.fields.forEach(field => { + let option; + switch (field.type) { + case 'many2one': + option = this._buildSelect(field); + break; + case 'char': + option = this._buildInput(field); + break; + } + uiFragment.insertBefore(option, firstOption); + }); + }); + }, - var dialog = new FormEditorDialog(self, { - title: 'Field Parameters', - size: 'medium', - $content: '<form role="form">' + fieldSelection + '</form>', - }).open(); - dialog.on('save', this, ev => { - ev.stopPropagation(); - save.call(dialog); - }); + /** + * Returns a we-input element from the field + * + * @private + * @param {Object} field + * @returns {HTMLElement} + */ + _buildInput: function (field) { + const inputEl = document.createElement('we-input'); + inputEl.dataset.noPreview = 'true'; + inputEl.dataset.fieldName = field.name; + inputEl.dataset.addActionField = ''; + inputEl.setAttribute('string', field.string); + return inputEl; + }, + /** + * Returns a we-select element with field's records as it's options + * + * @private + * @param {Object} field + * @return {HTMLElement} + */ + _buildSelect: function (field) { + const selectEl = document.createElement('we-select'); + selectEl.dataset.noPreview = 'true'; + selectEl.dataset.fieldName = field.name; + selectEl.dataset.isSelect = 'true'; + selectEl.setAttribute('string', field.string); + const noneButton = document.createElement('we-button'); + noneButton.textContent = 'None'; + noneButton.dataset.addActionField = 0; + selectEl.append(noneButton); + field.records.forEach(el => { + const button = document.createElement('we-button'); + button.textContent = el.display_name; + button.dataset.addActionField = el.id; + selectEl.append(button); + }); + return selectEl; + }, + /** + * Apply the model on the form changing it's fields + * + * @private + * @param {Integer} modelId + */ + _applyFormModel: async function (modelId) { + let oldFormInfo; + if (modelId) { + const oldFormKey = this.activeForm.website_form_key; + if (oldFormKey) { + oldFormInfo = FormEditorRegistry.get(oldFormKey); + } + this.$target.find('.s_website_form_field').remove(); + this.activeForm = _.findWhere(this.models, {id: modelId}); + } + const formKey = this.activeForm.website_form_key; + const formInfo = FormEditorRegistry.get(formKey); + // Success page + if (!this.$target[0].dataset.successMode) { + this.$target[0].dataset.successMode = 'redirect'; + } + if (this.$target[0].dataset.successMode === 'redirect') { + const currentSuccessPage = this.$target[0].dataset.successPage; + if (formInfo && formInfo.successPage) { + this.$target[0].dataset.successPage = formInfo.successPage; + } else if (!oldFormInfo || (oldFormInfo !== formInfo && oldFormInfo.successPage && currentSuccessPage === oldFormInfo.successPage)) { + this.$target[0].dataset.successPage = '/contactus-thank-you'; + } + } + // Model name + this.$target[0].dataset.model_name = this.activeForm.model; + // Load template + if (formInfo) { + const formatInfo = this._getDefaultFormat(); + await formInfo.formFields.forEach(async field => { + field.formatInfo = formatInfo; + await this._fetchFieldRecords(field); + this.$target.find('.s_website_form_submit').before(this._renderField(field)); }); - }, - - // Create a custom field - website_form_custom_field: function (previewMode, widgetValue, params) { - var default_field_name = 'Custom ' + this.el.querySelector(`[data-website_form_custom_field="${widgetValue}"]`).textContent; // TODO improve - this.append_field({ - name: default_field_name, - string: default_field_name, - custom: true, - type: widgetValue, - // Default values for x2many fields - records: [ - { - id: 'Option 1', - display_name: _t('Option 1') - }, - { - id: 'Option 2', - display_name: _t('Option 2') - }, - { - id: 'Option 3', - display_name: _t('Option 3') - } - ], - // Default values for selection fields - selection: [ - [ - 'Option 1', - _t('Option 1') - ], - [ - 'Option 2', - _t('Option 2') - ], - [ - 'Option 3', - _t('Option 3') - ], - ] + } + }, + /** + * Set the correct mark on all fields. + * + * @private + */ + _setLabelsMark: function () { + this.$target[0].querySelectorAll('span.s_website_form_mark').forEach(el => el.remove()); + const mark = this._getMark(); + if (!mark) { + return; + } + let fieldsToMark = []; + const requiredSelector = '.s_website_form_model_required, .s_website_form_required'; + const fields = Array.from(this.$target[0].querySelectorAll('.s_website_form_field')); + if (this._isRequiredMark()) { + fieldsToMark = fields.filter(el => el.matches(requiredSelector)); + } else if (this._isOptionalMark()) { + fieldsToMark = fields.filter(el => !el.matches(requiredSelector)); + } + fieldsToMark.forEach(field => { + let span = document.createElement('span'); + span.classList.add('s_website_form_mark'); + span.textContent = ` ${mark}`; + field.querySelector('.s_website_form_label').appendChild(span); + }); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + */ + _onToggleEndMessageClick: function () { + this.showEndMessage = !this.showEndMessage; + this.$el.find(".toggle-edit-message").toggleClass('text-primary', this.showEndMessage); + this.updateUIEndMessage(); + }, +}); + +options.registry.WebsiteFieldEditor = FieldEditor.extend({ + events: _.extend({}, FieldEditor.prototype.events, { + 'click we-button.o_we_select_remove_option': '_onRemoveItemClick', + 'click we-button.o_we_list_add_optional': '_onAddCustomItemClick', + 'click we-button.o_we_list_add_existing': '_onAddExistingItemClick', + 'click we-list we-select': '_onAddItemSelectClick', + 'input we-list input': '_onListItemInput', + }), + + /** + * @override + */ + init: function () { + this._super.apply(this, arguments); + this.rerender = true; + }, + /** + * @override + */ + willStart: async function () { + const _super = this._super.bind(this); + // Get the authorized existing fields for the form model + this.existingFields = await this._rpc({ + model: "ir.model", + method: "get_authorized_fields", + args: [this.formEl.dataset.model_name], + }).then(fields => { + this.fields = _.each(fields, function (field, fieldName) { + field.name = fieldName; + field.domain = field.domain || []; }); - }, - - // Re-render the field and replace the current one - // website_form_editor_field_reset: function(previewMode, widgetValue, params) { - // var self = this; - // var target_field_name = this.$target.find('.col-form-label').attr('for'); - // this.fields().then(function(fields){ - // self.render_field(fields[target_field_name]).done(function(field){ - // self.$target.replaceWith(field); - // }) - // }); - // }, - - append_field: function (field) { - var self = this; - // Copy formatting classes of last row (field) - var $lastField = this.$target.find('.form-field.row:last'); - field.formatInfo = { - labelClass: $lastField.find('> div:first').attr('class'), - contentClass: $lastField.find('> div:last').attr('class'), - }; - this.render_field(field).then(function (field){ - self.$target.find(".form-group:has('.o_website_form_send')").before(field); + // Create the buttons for the type we-select + return Object.keys(fields).map(key => { + const field = fields[key]; + const button = document.createElement('we-button'); + button.textContent = field.string; + button.dataset.existingField = field.name; + return button; + }).sort((a, b) => (a.textContent > b.textContent) ? 1 : (a.textContent < b.textContent) ? -1 : 0); + }); + return _super(...arguments); + }, + /** + * @override + */ + cleanForSave: function () { + this.$target[0].querySelectorAll('#editable_select').forEach(el => el.remove()); + const select = this._getSelect(); + if (select && this.listTable) { + select.style.display = ''; + select.innerHTML = ''; + // Rebuild the select from the we-list + this.listTable.querySelectorAll('input').forEach(el => { + const option = document.createElement('option'); + option.textContent = el.value; + option.value = this._isFieldCustom() ? el.value : el.name; + select.appendChild(option); }); - }, - - render_field: function (field) { - // Convert the required boolean to a value directly usable - // in qweb js to avoid duplicating this in the templates - field.required = field.required ? 1 : null; - - // Fetch possible values for relation fields - var fieldRelationProm; - if (field.relation && field.relation !== 'ir.attachment') { - fieldRelationProm = this._rpc({ - model: field.relation, - method: 'search_read', - args: [ - field.domain || [], - ['display_name'] - ], - }).then(function (records) { - field.records = records; - }); + } + }, + /** + * @override + */ + updateUI: async function () { + // See Form updateUI + if (this.rerender) { + const select = this._getSelect(); + if (select && !this.$target[0].querySelector('#editable_select')) { + select.style.display = 'none'; + const editableSelect = document.createElement('div'); + editableSelect.id = 'editable_select'; + editableSelect.classList = 'form-control s_website_form_input'; + select.parentElement.appendChild(editableSelect); } + this.rerender = false; + await this._rerenderXML().then(() => this._renderList()); + return; + } + await this._super.apply(this, arguments); + }, + /** + * @override + */ + onFocus: function () { + // Other fields type might have change to an existing type. + // We need to reload the existing type list. + this.rerender = true; + }, - return Promise.resolve(fieldRelationProm).then(function () { - var $content = $(qweb.render("website_form.field_" + field.type, {field: field})); - $content.find('label:not(:has(span)), label span').addClass('o_fake_editable').attr('contentEditable', true); - return $content; - }); - }, - - onBuilt: function () { - // Open the parameters modal on snippet drop - this.website_form_model_modal('click', null, null); - }, - - /** - * Hide change form parameters option for forms - * e.g. User should not be enable to change existing job application form to opportunity form in 'Apply job' page. - * - * @override - */ - onFocus: function () { - this.$el.filter('[data-website_form_model_modal]').toggleClass('d-none', this.$target.attr('hide-change-model') !== undefined); - }, - - init_form: function () { - var self = this; - var modelName = this.activeForm.model; - var currentModel = this.$target.attr('data-model_name'); - var formKey = this.activeForm.website_form_key; - if (!currentModel) { - // Directly change the parameters if model is being set for the first time - this.changeFormParameters(modelName, formKey); - } else if (modelName !== currentModel) { - var warningMessage = _t("Are you sure you want to change the parameters of your form? All the current fields will be discarded."); - new Dialog(this, { - title: _t("Warning"), - size: 'medium', - $content: $('<div>', {text: warningMessage}), - buttons: [{ - text: _t("Ok"), - classes: 'btn-primary', - click: function () { - self.changeFormParameters(modelName, formKey); - }, - close: true, - }, { - text: _t("Discard"), - close: true, - }], - }).open({shouldFocusButtons: true}); + //---------------------------------------------------------------------- + // Options + //---------------------------------------------------------------------- + + /** + * Replace the current field with the custom field selected. + */ + customField: async function (previewMode, value, params) { + // Both custom Field and existingField are called when selecting an option + // value is '' for the method that should not be called. + if (!value) { + return; + } + const name = this.el.querySelector(`[data-custom-field="${value}"]`).textContent; + const field = this._getCustomField(value, `Custom ${name}`); + this._setActiveProperties(field); + await this._replaceField(field); + this.rerender = true; + }, + /** + * Replace the current field with the existing field selected. + */ + existingField: async function (previewMode, value, params) { + // see customField + if (!value) { + return; + } + const field = this.fields[value]; + this._setActiveProperties(field); + await this._replaceField(field); + this.rerender = true; + }, + /** + * Set the name of the field on the label + */ + setLabelText: function (previewMode, value, params) { + this.$target[0].querySelector('.s_website_form_label').firstChild.textContent = value; + if (this._isFieldCustom()) { + const multiple = this.$target[0].querySelector('.s_website_form_multiple'); + if (multiple) { + multiple.dataset.name = value; } - }, - - changeFormParameters: function (modelName, formKey) { - var self = this; - this.$target.attr('data-model_name', modelName); - this.$target.find(".form-field:not(:has('.o_website_form_send')), .o_form_heading").remove(); - if (formKey) { - var formInfo = FormEditorRegistry.get(formKey); - ajax.loadXML(formInfo.defaultTemplatePath, qweb).then(function () { - // Append form title - $('<h1>', { - class: 'o_form_heading', - text: self.activeForm.website_form_label, - contentEditable: true, - }).prependTo(self.$target.find('.container')); - self.$target.find('.form-group:has(".o_website_form_send")').before($(qweb.render(formInfo.defaultTemplateName))); - }); + this.$target[0].querySelectorAll('.s_website_form_input').forEach(el => el.name = value); + } + }, + /* + * Set the placeholder of the input + */ + setPlaceholder: function (previewMode, value, params) { + this._setPlaceholder(value); + }, + /** + * Replace the field with the same field having the label in a different position. + */ + selectLabelPosition: async function (previewMode, value, params) { + const field = this._getActiveField(); + field.formatInfo.labelPosition = value; + await this._replaceField(field); + this._renderListItems(); + }, + /** + * Select the display of the multicheckbox field (vertical & horizontal) + */ + multiCheckboxDisplay: function (previewMode, value, params) { + const target = this._getMultipleInputs(); + target.querySelectorAll('.checkbox, .radio').forEach(el => { + if (value === 'horizontal') { + el.classList.add('col-lg-4', 'col-md-6'); } else { - // Force fetch the fields of the new model - // and render all model required fields - this.fetch_model_fields().then(function (fields) { - _.each(fields, function (field, field_name) { - if (field.required) { - self.append_field(field); - } - }); - }); + el.classList.remove('col-lg-4', 'col-md-6'); } - }, - - cleanForSave: function () { - var model = this.$target.data('model_name'); - // because apparently this can be called on the wrong widget and - // we may not have a model, or fields... - if (model) { - // we may be re-whitelisting already whitelisted fields. Doesn't - // really matter. - var fields = this.$target.find('input.form-field[name=email_to], .form-field:not(.o_website_form_custom) :input').map(function (_, node) { - return node.getAttribute('name'); - }).get(); - if (fields.length) { - // ideally we'd only do this if saving the form - // succeeds... but no idea how to do that - this._rpc({ - model: 'ir.model.fields', - method: 'formbuilder_whitelist', - args: [model, _.uniq(fields)], - }); - } + }); + target.dataset.display = value; + }, + /** + * Set the field as required or not + */ + toggleRequired: function (previewMode, value, params) { + const isRequired = this.$target[0].classList.contains(params.activeValue); + this.$target[0].classList.toggle(params.activeValue, !isRequired); + this.$target[0].querySelectorAll('input, select, textarea').forEach(el => el.toggleAttribute('required', !isRequired)); + this.trigger_up('option_update', { + optionName: 'WebsiteFormEditor', + name: 'field_mark', + }); + }, + + //---------------------------------------------------------------------- + // Private + //---------------------------------------------------------------------- + + /** + * @override + */ + _computeWidgetState: function (methodName, params) { + switch (methodName) { + case 'customField': + return this._isFieldCustom() ? this._getFieldType() : ''; + case 'existingField': + return this._isFieldCustom() ? '' : this._getFieldName(); + case 'setLabelText': + return this.$target[0].querySelector('.s_website_form_label').firstChild.textContent.trim(); + case 'setPlaceholder': + return this._getPlaceholder(); + case 'selectLabelPosition': + return this._getLabelPosition(); + case 'multiCheckboxDisplay': { + const target = this._getMultipleInputs(); + return target ? target.dataset.display : ''; } + case 'toggleRequired': + return this.$target[0].classList.contains(params.activeValue) ? params.activeValue : 'false'; + } + return this._super(...arguments); + }, + /** + * @override + */ + _computeWidgetVisibility: function (widgetName, params) { + switch (widgetName) { + case 'multi_check_display_opt': + return !!this._getMultipleInputs(); + case 'placeholder_opt': + return !!this._getPlaceholderInput(); + case 'required_opt': + case 'hidden_opt': + case 'type_opt': + return !this.$target[0].classList.contains('s_website_form_model_required'); + } + return this._super(...arguments); + }, + /** + * @override + */ + _renderCustomXML: function (uiFragment) { + const selectEl = uiFragment.querySelector('we-select[data-name="type_opt"]'); + const currentFieldName = this._getFieldName(); + const fieldsInForm = Array.from(this.formEl.querySelectorAll('.s_website_form_field:not(.s_website_form_custom) .s_website_form_input')).map(el => el.name).filter(el => el !== currentFieldName); + const availableFields = this.existingFields.filter(el => !fieldsInForm.includes(el.dataset.existingField)); + if (availableFields.length) { + const title = document.createElement('we-title'); + title.textContent = 'Existing fields'; + availableFields.unshift(title); + availableFields.forEach(option => selectEl.append(option.cloneNode(true))); + } + }, + /** + * Replace the target content with the field provided + * + * @private + * @param {Object} field + * @returns {Promise} + */ + _replaceField: async function (field) { + await this._fetchFieldRecords(field); + const htmlField = this._renderField(field); + [...this.$target[0].childNodes].forEach(node => node.remove()); + [...htmlField.childNodes].forEach(node => this.$target[0].appendChild(node)); + [...htmlField.attributes].forEach(el => this.$target[0].removeAttribute(el.nodeName)); + [...htmlField.attributes].forEach(el => this.$target[0].setAttribute(el.nodeName, el.nodeValue)); + }, - // Prevent saving of the error colors // TODO: would be better on Edit - this.$target.find('.o_has_error').removeClass('o_has_error').find('.form-control, .custom-select').removeClass('is-invalid'); - - // Prevent saving of the status message // TODO: would be better on Edit - this.$target.find('#o_website_form_result').empty(); - - // Update values of custom inputs to mirror their labels - var custom_inputs = this.$target.find('.o_website_form_custom .o_website_form_input'); - _.each(custom_inputs, function (input, index) { - // Change the custom field name according to their label - var field_label = $(input).closest('.form-field').find('label:first'); - input.name = field_label.text().trim(); - field_label.attr('for', input.name); - - // Change the custom radio or checkboxes values according to their label - if (input.type === 'radio' || input.type === 'checkbox') { - var checkbox_label = $(input).closest('label').text().trim(); - if (checkbox_label) { - input.value = checkbox_label; - } - } + /** + * To do after rerenderXML to add the list to the options + * + * @private + */ + _renderList: function () { + let addItemButton, addItemTitle, listTitle; + const select = this._getSelect(); + const multipleInputs = this._getMultipleInputs(); + this.listTable = document.createElement('table'); + const isCustomField = this._isFieldCustom(); + + if (select) { + listTitle = 'Options List'; + addItemTitle = 'Add new Option'; + select.querySelectorAll('option').forEach(opt => { + this._addItemToTable(opt.value, opt.textContent.trim()); }); - } - }); - - // radio / checkboxes field option to display them horizontally or vertically - options.registry['website_form_editor_choice_field_display'] = options.Class.extend({ - xmlDependencies: ['/website_form/static/src/xml/website_form_editor.xml'], - - /** - * @see this.selectClass for parameters - */ - website_form_choice_field_display: function (previewMode, widgetValue, params) { - this.$target.toggleClass('o_website_form_flex_fw', widgetValue === 'vertical'); - this.$target[0].dataset.display = widgetValue; - }, - - //---------------------------------------------------------------------- - // Private - //---------------------------------------------------------------------- - - /** - * @override - */ - _computeWidgetState: function (methodName, params) { - if (methodName === 'website_form_choice_field_display') { - return this.$target.attr('data-display'); - } - return this._super(...arguments); - }, - }); - - // Generic custom field options - options.registry['website_form_editor_field'] = options.Class.extend({ - xmlDependencies: ['/website_form/static/src/xml/website_form_editor.xml'], - - // Option to toggle inputs required attribute - website_form_field_require: function (previewMode, widgetValue, params) { - this.$target.find('.o_website_form_input').each(function (index, input) { - input.required = !input.required; + this._renderListItems(); + } else if (multipleInputs) { + listTitle = multipleInputs.querySelector('.radio') ? 'Radio List' : 'Checkbox List'; + addItemTitle = 'Add new Checkbox'; + multipleInputs.querySelectorAll('.checkbox, .radio').forEach(opt => { + this._addItemToTable(opt.querySelector('input').value, opt.querySelector('span').textContent.trim()); }); + } else { + return; } - }); - - // Dirty hack to transform select fields into an editable construct - options.registry['website_form_editor_field_select'] = options.Class.extend({ - xmlDependencies: ['/website_form/static/src/xml/website_form_editor.xml'], - - start: function () { - if (!this.$target.find('#editable_select').length) { - var self = this; - var select = this.$target.find('select'); - select.hide(); - this.editable_select = $('<div id="editable_select" class="form-control o_website_form_input" contenteditable="true"/>'); - _.each(select.children(), function (option) { - self.editable_select.append( - $('<div id="' + $(option).attr('value') + '" class="o_website_form_select_item">' + $(option).text().trim() + '</div>') - ); - }); - select.after(this.editable_select); - } - return this._super.apply(this, arguments); - }, - - cleanForSave: function () { - if (this.$target.find('#editable_select').length) { - var self = this; - // Reconstruct the field from the select tag - var select = this.$target.find('select'); - var field = { - name: select.attr('name'), - string: this.$target.find('.col-form-label').text().trim(), - required: self.$target.hasClass('o_website_form_required'), - custom: self.$target.hasClass('o_website_form_custom'), - formatInfo: { - labelClass: this.$target.find('> div:first').attr('class'), - contentClass: this.$target.find('> div:last').attr('class'), - }, - }; - // Build the new records list from the editable select field - var records = []; - var editable_options = this.$target.find('#editable_select .o_website_form_select_item'); - _.each(editable_options, function (option) { - records.push({ - id: self.$target.hasClass('o_website_form_custom') ? $(option).text().trim() : $(option).attr('id'), - display_name: $(option).text().trim() + if (isCustomField) { + addItemButton = document.createElement('we-button'); + addItemButton.textContent = addItemTitle; + addItemButton.classList.add('o_we_list_add_optional'); + addItemButton.dataset.noPreview = 'true'; + } else { + addItemButton = document.createElement('we-select'); + addItemButton.classList.add('o_we_user_value_widget'); // Todo dont use user value widget class + const togglerEl = document.createElement('we-toggler'); + togglerEl.textContent = addItemTitle; + addItemButton.appendChild(togglerEl); + const selectMenuEl = document.createElement('we-select-menu'); + addItemButton.appendChild(selectMenuEl); + this._loadListDropdown(selectMenuEl); + } + const selectInputEl = document.createElement('we-list'); + const title = document.createElement('we-title'); + title.textContent = listTitle; + selectInputEl.appendChild(title); + const tableWrapper = document.createElement('div'); + tableWrapper.classList.add('oe_we_table_wraper'); + tableWrapper.appendChild(this.listTable); + selectInputEl.appendChild(tableWrapper); + selectInputEl.appendChild(addItemButton); + this.el.insertBefore(selectInputEl, this.el.querySelector('[data-set-placeholder]')); + this._makeListItemsSortable(); + }, + /** + * Load the dropdown of the list with the records missing from the list. + * + * @private + * @param {HTMLElement} selectMenu + */ + _loadListDropdown: function (selectMenu) { + selectMenu = selectMenu || this.el.querySelector('we-list we-select-menu'); + if (selectMenu) { + selectMenu.innerHTML = ''; + const field = this.fields[this._getFieldName()]; + this._fetchFieldRecords(field).then(() => { + let buttonItems, availableRecords; + if (field.selection) { + availableRecords = field.selection.map(el => { + return {id: el[0], display_name: el[1]}; }); + } else if (field.records) { + availableRecords = field.records; + } + const optionIds = Array.from(this.listTable.querySelectorAll('input')).map(opt => { + return field.type === 'selection' ? opt.name : parseInt(opt.name); }); - field.records = records; - - // Replace this field by the new one - var $new_select = $(qweb.render("website_form.field_many2one", {field: field})); - // Reapply the custom style classes - if (this.$target.hasClass('o_website_form_required_custom')) { - $new_select.addClass('o_website_form_required_custom'); + availableRecords = (availableRecords || []).filter(el => !optionIds.includes(el.id)); + if (availableRecords.length) { + buttonItems = availableRecords.map(el => { + const option = document.createElement('we-button'); + option.classList.add('o_we_list_add_existing'); + option.dataset.addOption = el.id; + option.dataset.noPreview = 'true'; + option.textContent = el.display_name; + return option; + }); + } else { + const title = document.createElement('we-title'); + title.textContent = 'No more records'; + buttonItems = [title]; } - if (this.$target.hasClass('o_website_form_field_hidden')) { - $new_select.addClass('o_website_form_field_hidden'); + buttonItems.forEach(button => selectMenu.appendChild(button)); + }); + } + }, + /** + * @private + */ + _makeListItemsSortable: function () { + $(this.listTable).sortable({ + axis: 'y', + handle: '.o_we_drag_handle', + items: 'tr', + cursor: 'move', + opacity: 0.6, + stop: (event, ui) => { + this._renderListItems(); + }, + }); + }, + /** + * @private + * @param {string} id + * @param {string} text + */ + _addItemToTable: function (id, text) { + const isCustomField = this._isFieldCustom(); + const draggableEl = document.createElement('we-button'); + draggableEl.classList.add('o_we_drag_handle', 'fa', 'fa-fw', 'fa-arrows'); + draggableEl.dataset.noPreview = 'true'; + const inputEl = document.createElement('input'); + inputEl.type = 'text'; + if (text) { + inputEl.value = text; + } + if (!isCustomField && id) { + inputEl.name = id; + } + inputEl.disabled = !isCustomField; + const trEl = document.createElement('tr'); + const buttonEl = document.createElement('we-button'); + buttonEl.classList.add('o_we_select_remove_option', 'fa', 'fa-fw', 'fa-minus'); + buttonEl.dataset.removeOption = id; + buttonEl.dataset.noPreview = 'true'; + const draggableTdEl = document.createElement('td'); + const inputTdEl = document.createElement('td'); + const buttonTdEl = document.createElement('td'); + draggableTdEl.appendChild(draggableEl); + trEl.appendChild(draggableTdEl); + inputTdEl.appendChild(inputEl); + trEl.appendChild(inputTdEl); + buttonTdEl.appendChild(buttonEl); + trEl.appendChild(buttonTdEl); + this.listTable.appendChild(trEl); + if (isCustomField) { + inputEl.focus(); + } + }, + /** + * Apply the we-list on the target and rebuild the input(s) + * + * @private + */ + _renderListItems: function () { + const multiInputsWrap = this._getMultipleInputs(); + const selectWrap = this.$target[0].querySelector('#editable_select'); + const isCustomField = this._isFieldCustom(); + const isRequiredField = this._isFieldRequired(); + const name = this._getFieldName(); + if (multiInputsWrap) { + const type = multiInputsWrap.querySelector('.radio') ? 'radio' : 'checkbox'; + multiInputsWrap.innerHTML = ''; + this.listTable.querySelectorAll('input').forEach(el => { + const params = { + field: { + name: name, + required: isRequiredField, + formatInfo: { + multiPosition: multiInputsWrap.dataset.display, + } + } + }; + const id = isCustomField ? el.value : el.name; + if (type === 'radio') { + params.record = [id, el.value]; + } else { + params.record = { + id: id, + display_name: el.value, + }; } - this.$target.replaceWith($new_select); - } + const template = document.createElement('template'); + template.innerHTML = qweb.render(`website_form.${type}`, params); + multiInputsWrap.appendChild(template.content.firstElementChild); + }); + } else if (selectWrap) { + selectWrap.innerHTML = ''; + this.listTable.querySelectorAll('input').forEach(el => { + const option = document.createElement('div'); + option.id = (el.name || el.value); + option.classList.add('s_website_form_select_item'); + option.textContent = el.value; + selectWrap.appendChild(option); + }); } - }); - - // allow breaking of form select items, to create new ones - Wysiwyg.include({ - /** - * @override - */ - _editorOptions: function () { - var options = this._super.apply(this, arguments); - var isUnbreakableNode = options.isUnbreakableNode; - options.isUnbreakableNode = function (node) { - var isSelItem = $(node).hasClass('o_website_form_select_item'); - return isUnbreakableNode(node) && !isSelItem; - }; - return options; - }, - }); - - // Superclass for options that need to disable a button from the snippet overlay - var disable_overlay_button_option = options.Class.extend({ - xmlDependencies: ['/website_form/static/src/xml/website_form_editor.xml'], - - // Disable a button of the snippet overlay - disable_button: function (button_name, message) { - // TODO refactor in master - var className = 'oe_snippet_' + button_name; - this.$overlay.add(this.$overlay.data('$optionsSection')).on('click', '.' + className, this.prevent_button); - var $button = this.$overlay.add(this.$overlay.data('$optionsSection')).find('.' + className); - $button.attr('title', message).tooltip({delay: 0}); - $button.removeClass(className); // Disable the functionnality - }, - - prevent_button: function (event) { - // Snippet options bind their functions before the editor, so we - // can't cleanly unbind the editor onRemove function from here - event.preventDefault(); - event.stopImmediatePropagation(); - } - }); - - // Disable duplicate button for model fields - options.registry['website_form_editor_field_model'] = disable_overlay_button_option.extend({ - start: function () { - this.disable_button('clone', _t('You can\'t duplicate a model field.')); - return this._super.apply(this, arguments); - } - }); - - // Disable delete button for model required fields - options.registry['website_form_editor_field_required'] = disable_overlay_button_option.extend({ - start: function () { - this.disable_button('remove', _t('You can\'t remove a field that is required by the model itself.')); - return this._super.apply(this, arguments); - } - }); - - // Disable duplicate button for non-custom checkboxes and radio buttons - options.registry['website_form_editor_field_x2many'] =disable_overlay_button_option.extend({ - start: function () { - this.disable_button('clone', _t('You can\'t duplicate an item which refers to an actual record.')); - return this._super.apply(this, arguments); - } - }); + }, + /** + * Returns the select element if it exist else null + * + * @private + * @returns {HTMLElement} + */ + _getSelect: function () { + return this.$target[0].querySelector('select'); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + * @param {Event} ev + */ + _onRemoveItemClick: function (ev) { + ev.target.closest('tr').remove(); + this._loadListDropdown(); + this._renderListItems(); + }, + /** + * @private + * @param {Event} ev + */ + _onAddCustomItemClick: function (ev) { + this._addItemToTable(); + this._makeListItemsSortable(); + this._renderListItems(); + }, + /** + * @private + * @param {Event} ev + */ + _onAddExistingItemClick: function (ev) { + const value = ev.currentTarget.dataset.addOption; + this._addItemToTable(value, ev.currentTarget.textContent); + this._makeListItemsSortable(); + this._loadListDropdown(); + this._renderListItems(); + }, + /** + * @private + * @param {Event} ev + */ + _onAddItemSelectClick: function (ev) { + ev.currentTarget.querySelector('we-toggler').classList.toggle('active'); + }, + /** + * @private + */ + _onListItemInput: function () { + this._renderListItems(); + }, +}); + +options.registry.AddFieldForm = FormEditor.extend({ + isTopOption: true, + + //-------------------------------------------------------------------------- + // Options + //-------------------------------------------------------------------------- + + /** + * Add a char field at the end of the form. + * New field is set as active + */ + addField: async function (previewMode, value, params) { + const field = this._getCustomField('char', 'Custom Text'); + field.formatInfo = this._getDefaultFormat(); + const htmlField = this._renderField(field); + this.$target.find('.s_website_form_submit').before(htmlField); + this.trigger_up('activate_snippet', { + $snippet: $(htmlField), + }); + }, +}); + +options.registry.AddField = FieldEditor.extend({ + isTopOption: true, + + //-------------------------------------------------------------------------- + // Options + //-------------------------------------------------------------------------- + + /** + * Add a char field with active field properties after the active field. + * New field is set as active + */ + addField: async function (previewMode, value, params) { + this.trigger_up('option_update', { + optionName: 'WebsiteFormEditor', + name: 'add_field', + data: { + formatInfo: this._getFieldFormat(), + $target: this.$target, + }, + }); + }, +}); + +// Superclass for options that need to disable a button from the snippet overlay +const DisableOverlayButtonOption = options.Class.extend({ + // Disable a button of the snippet overlay + disableButton: function (buttonName, message) { + // TODO refactor in master + const className = 'oe_snippet_' + buttonName; + this.$overlay.add(this.$overlay.data('$optionsSection')).on('click', '.' + className, this.preventButton); + const $button = this.$overlay.add(this.$overlay.data('$optionsSection')).find('.' + className); + $button.attr('title', message).tooltip({delay: 0}); + $button.removeClass(className); // Disable the functionnality + }, + + preventButton: function (event) { + // Snippet options bind their functions before the editor, so we + // can't cleanly unbind the editor onRemove function from here + event.preventDefault(); + event.stopImmediatePropagation(); + } +}); + +// Disable duplicate button for model fields +options.registry.WebsiteFormFieldModel = DisableOverlayButtonOption.extend({ + start: function () { + this.disableButton('clone', _t('You can\'t duplicate a model field.')); + return this._super.apply(this, arguments); + } +}); + +// Disable delete button for model required fields +options.registry.WebsiteFormFieldRequired = DisableOverlayButtonOption.extend({ + start: function () { + this.disableButton('remove', _t('You can\'t remove a field that is required by the model itself.')); + return this._super.apply(this, arguments); + } +}); + +// Disable delete and duplicate button for submit +options.registry.WebsiteFormSubmitRequired = DisableOverlayButtonOption.extend({ + start: function () { + this.disableButton('remove', _t('You can\'t remove the submit button of the form')); + this.disableButton('clone', _t('You can\'t duplicate the submit button of the form.')); + return this._super.apply(this, arguments); + } +}); }); diff --git a/addons/website_form/static/src/xml/website_form.xml b/addons/website_form/static/src/xml/website_form.xml index c3779f9bc01..5403f763e24 100644 --- a/addons/website_form/static/src/xml/website_form.xml +++ b/addons/website_form/static/src/xml/website_form.xml @@ -3,74 +3,22 @@ <!-- Success status --> <t t-name="website_form.status_success"> - <span id="o_website_form_result" class="text-success ml8"> + <span id="s_website_form_result" class="text-success ml8"> <i class="fa fa-check mr4" role="img" aria-label="Success" title="Success"/>The form has been sent successfully. </span> </t> <!-- Missing field status --> <t t-name="website_form.status_invalid"> - <span id="o_website_form_result" class="text-danger ml8"> + <span id="s_website_form_result" class="text-danger ml8"> <i class="fa fa-close mr4" role="img" aria-label="Error" title="Error"/>Please fill in the form correctly. </span> </t> <!-- Error status --> <t t-name="website_form.status_error"> - <span id="o_website_form_result" class="text-danger ml8"> + <span id="s_website_form_result" class="text-danger ml8"> <i class="fa fa-close mr4" role="img" aria-label="Error" title="Error"/>An error has occured, the form has not been sent. </span> </t> - - <t t-name="website_form.default_contactus_form"> - <div class="form-group row form-field o_website_form_custom o_website_form_required_custom"> - <div class="col-lg-3 col-md-4"> - <label class="col-form-label" for="Name">Your Name</label> - </div> - <div class="col-lg-7 col-md-8"> - <input type="text" class="form-control o_website_form_input" name="Name" required=""/> - </div> - </div> - <div class="form-group row form-field o_website_form_custom"> - <div class="col-lg-3 col-md-4"> - <label class="col-form-label" for="Phone">Phone Number</label> - </div> - <div class="col-lg-7 col-md-8"> - <input type="text" class="form-control o_website_form_input" name="Phone"/> - </div> - </div> - <div class="form-group row form-field o_website_form_required_custom"> - <div class="col-lg-3 col-md-4"> - <label class="col-form-label" for="email_from">Email</label> - </div> - <div class="col-lg-7 col-md-8"> - <input type="email" class="form-control o_website_form_input" name="email_from" required=""/> - </div> - </div> - <div class="form-group row form-field o_website_form_custom"> - <div class="col-lg-3 col-md-4"> - <label class="col-form-label" for="Partner Name">Your Company</label> - </div> - <div class="col-lg-7 col-md-8"> - <input type="text" class="form-control o_website_form_input" name="Partner Name"/> - </div> - </div> - <div class="form-group row form-field o_website_form_required_custom"> - <div class="col-lg-3 col-md-4"> - <label class="col-form-label" for="subject">Subject</label> - </div> - <div class="col-lg-7 col-md-8"> - <input type="text" class="form-control o_website_form_input" name="subject" required=""/> - </div> - </div> - <div class="form-group row form-field o_website_form_custom o_website_form_required_custom"> - <div class="col-lg-3 col-md-4"> - <label class="col-form-label" for="Description">Your Question</label> - </div> - <div class="col-lg-7 col-md-8"> - <textarea class="form-control o_website_form_input" name="Description" required=""></textarea> - </div> - </div> - </t> - </templates> diff --git a/addons/website_form/static/src/xml/website_form_editor.xml b/addons/website_form/static/src/xml/website_form_editor.xml index 3fb85a7dded..70c90f55593 100644 --- a/addons/website_form/static/src/xml/website_form_editor.xml +++ b/addons/website_form/static/src/xml/website_form_editor.xml @@ -1,32 +1,89 @@ <?xml version="1.0" encoding="UTF-8"?> <templates xml:space="preserve"> + <!-- End Message --> + <t t-name="website_form.s_website_form_end_message"> + <div class="s_website_form_end_message d-none"> + <div class="text-center alert alert-info css_non_editable_mode_hidden o_not_editable" t-ignore="True" role="status"> + The following message will become visible <strong>only</strong> once the form is sent. + </div> + <div class="oe_structure"> + <section class="s_text_block pt64 pb64"> + <div class="container"> + <h2 class="text-center">Thank You For Your Feedback</h2> + <p class="text-center"> + Our team will message you back as soon as possible.<br/> + In the meantime we invite you to try our awesome <a href="/">website</a>.<br/> + </p> + </div> + </section> + </div> + </div> + </t> + <!-- Generic Field Layout --> + <!-- Changes made here needs to be reflected in the different Form view (Contact Us, Jobs, ...) --> <t t-name="website_form.field"> - <div t-att-class="'row form-group form-field' + (field.custom ? ' o_website_form_custom' : '') + (field.required ? ' o_website_form_required' : '')"> - <div t-att-class="field.formatInfo and field.formatInfo.labelClass || 'col-lg-3 col-md-4'"> - <label class="col-form-label" t-att-for="field.name"> - <t t-esc="field.string"/> + <div t-attf-class="form-group s_website_form_field #{field.formatInfo.col or 'col-12'} #{field.custom and 's_website_form_custom' or ''} #{(field.required and 's_website_form_required' or '') or (field.modelRequired and 's_website_form_model_required' or '')} #{field.hidden and 's_website_form_field_hidden' or ''} #{field.dnone and 'd-none' or ''}" + t-att-data-type="field.type" + t-att-data-name="Field"> + <div t-if="field.formatInfo.labelPosition != 'none' and field.formatInfo.labelPosition != 'top'" class="row s_col_no_resize s_col_no_bgcolor"> + <label t-attf-class="col-form-label col-sm-auto s_website_form_label #{field.formatInfo.labelPosition == 'right' and 'text-right' or ''}" t-attf-style="width: #{field.formatInfo.labelWidth or '200px'}" t-att-for="field.id"> + <t t-call="website_form.label_content"/> </label> - <span t-if="field.name == 'email_cc'" title="Separate email addresses with a comma."> - <i class="fa fa-info-circle"/> - </span> + <div class="col-sm"> + <t t-raw="0"/> + </div> </div> - <div t-att-class="field.formatInfo and field.formatInfo.contentClass || 'col-lg-7 col-md-8'"> + <t t-else=""> + <label t-attf-class="s_website_form_label #{field.formatInfo.labelPosition == 'none' and 'd-none' or ''}" t-attf-style="width: #{field.formatInfo.labelWidth or '200px'}" t-att-for="field.id"> + <t t-call="website_form.label_content"/> + </label> <t t-raw="0"/> - </div> + </t> </div> </t> + <t t-name="website_form.label_content"> + <t t-if="field.custom" t-set="field.string" t-value="field.name"/> + <t t-esc="field.string"/> + <t t-if="field.required or field.modelRequired"> + <span class="s_website_form_mark" t-if="field.formatInfo.requiredMark" t-esc="' ' + field.formatInfo.mark"/> + </t> + <t t-else=""> + <span class="s_website_form_mark" t-if="field.formatInfo.optionalMark" t-esc="' ' + field.formatInfo.mark"/> + </t> + <span t-if="['email_cc', 'email_to'].includes(field.name)" title="Separate email addresses with a comma."> + <i class="fa fa-info-circle"/> + </span> + </t> + + <!-- Hidden Field --> + <t t-name="website_form.field_hidden"> + <t t-set="field.dnone" t-value="true"/> + <t t-set="field.formatInfo" t-value="{}"/> + <t t-call="website_form.field"> + <input + type="hidden" + class="form-control s_website_form_input" + t-att-name="field.name" + t-att-value="field.value" + t-att-id="field.id" + /> + </t> + </t> + <!-- Char Field --> <t t-name="website_form.field_char"> <t t-call="website_form.field"> <input type="text" - class="form-control o_website_form_input" + class="form-control s_website_form_input" t-att-name="field.name" - t-att-required="field.required" + t-att-required="field.required || field.modelRequired || None" t-att-value="field.value" + t-att-placeholder="field.placeholder" + t-att-id="field.id" /> </t> </t> @@ -35,9 +92,11 @@ <t t-name="website_form.field_text"> <t t-call="website_form.field"> <textarea - class="form-control o_website_form_input" + class="form-control s_website_form_input" t-att-name="field.name" - t-att-required="field.required" + t-att-required="field.required || field.modelRequired || None" + t-att-placeholder="field.placeholder" + t-att-id="field.id" /> </t> </t> @@ -47,9 +106,11 @@ <t t-call="website_form.field"> <input type="email" - class="form-control o_website_form_input" + class="form-control s_website_form_input" t-att-name="field.name" - t-att-required="field.required" + t-att-required="field.required || field.modelRequired || None" + t-att-placeholder="field.placeholder" + t-att-id="field.id" /> </t> </t> @@ -68,10 +129,12 @@ <t t-call="website_form.field"> <input type="number" - class="form-control o_website_form_input" + class="form-control s_website_form_input" t-att-name="field.name" step="1" - t-att-required="field.required" + t-att-required="field.required || field.modelRequired || None" + t-att-placeholder="field.placeholder" + t-att-id="field.id" /> </t> </t> @@ -81,10 +144,12 @@ <t t-call="website_form.field"> <input type="number" - class="form-control o_website_form_input" + class="form-control s_website_form_input" t-att-name="field.name" step="any" - t-att-required="field.required" + t-att-required="field.required || field.modelRequired || None" + t-att-placeholder="field.placeholder" + t-att-id="field.id" /> </t> </t> @@ -93,13 +158,15 @@ <t t-name="website_form.field_date"> <t t-call="website_form.field"> <t t-set="datepickerID" t-value="_.uniqueId('datepicker')"/> - <div class="o_website_form_date input-group date" t-att-id="datepickerID" data-target-input="nearest"> + <div class="s_website_form_date input-group date" t-att-id="datepickerID" data-target-input="nearest"> <input type="text" - class="form-control datetimepicker-input o_website_form_input" + class="form-control datetimepicker-input s_website_form_input" t-attf-data-target="##{datepickerID}" t-att-name="field.name" - t-att-required="field.required" + t-att-required="field.required || field.modelRequired || None" + t-att-placeholder="field.placeholder" + t-att-id="field.id" /> <div class="input-group-append" t-attf-data-target="##{datepickerID}" data-toggle="datetimepicker"> <div class="input-group-text"><i class="fa fa-calendar"></i></div> @@ -112,13 +179,15 @@ <t t-name="website_form.field_datetime"> <t t-call="website_form.field"> <t t-set="datetimepickerID" t-value="_.uniqueId('datetimepicker')"/> - <div class="o_website_form_datetime input-group date" t-att-id="datetimepickerID" data-target-input="nearest"> + <div class="s_website_form_datetime input-group date" t-att-id="datetimepickerID" data-target-input="nearest"> <input type="text" - class="form-control datetimepicker-input o_website_form_input" + class="form-control datetimepicker-input s_website_form_input" t-attf-data-target="##{datetimepickerID}" t-att-name="field.name" - t-att-required="field.required" + t-att-required="field.required || field.modelRequired || None" + t-att-placeholder="field.placeholder" + t-att-id="field.id" /> <div class="input-group-append" t-attf-data-target="##{datetimepickerID}" data-toggle="datetimepicker"> <div class="input-group-text"><i class="fa fa-calendar"></i></div> @@ -134,9 +203,10 @@ <input type="checkbox" value="Yes" - class="o_website_form_input" + class="s_website_form_input" t-att-name="field.name" - t-att-required="field.required" + t-att-required="field.required || field.modelRequired || None" + t-att-id="field.id" /> </div> </t> @@ -145,27 +215,41 @@ <!-- Selection Field --> <t t-name="website_form.field_selection"> <t t-call="website_form.field"> - <div class="o_website_form_flex" data-display="horizontal"> - <t t-foreach="field.selection" t-as="option"> - <div class="radio o_website_form_flex_item"> - <label class="col-form-label"> - <input - type="radio" - class="o_website_form_input" - t-att-name="field.name" - t-att-value="option[0]" - t-att-required="field.required" - /> - <span> - <t t-esc="option[1]"/> - </span> - </label> - </div> + <div class="row s_col_no_resize s_col_no_bgcolor s_website_form_multiple" t-att-data-name="field.name" t-att-data-display="field.formatInfo.multiPosition"> + <t t-if="!field.selection"> + <input + class="s_website_form_input" + t-att-name="field.name" + t-att-value="record.id" + t-att-required="field.required || field.modelRequired || None" + placeholder="No matching record !" + /> + </t> + <t t-foreach="field.selection" t-as="record"> + <t t-call="website_form.radio"/> </t> </div> </t> </t> + <!-- Radio --> + <t t-name="website_form.radio"> + <div t-attf-class="radio col-12 #{field.formatInfo.multiPosition === 'horizontal' and 'col-lg-4 col-md-6' or ''}"> + <label class="col-form-label"> + <input + type="radio" + class="s_website_form_input" + t-att-name="field.name" + t-att-value="record[0]" + t-att-required="field.required || field.modelRequired || None" + /> + <span> + <t t-esc="record[1]"/> + </span> + </label> + </div> + </t> + <!-- Many2One Field --> <t t-name="website_form.field_many2one"> <!-- Binary one2many --> @@ -175,7 +259,7 @@ <!-- Generic one2many --> <t t-if="field.relation != 'ir.attachment'"> <t t-call="website_form.field"> - <select class="form-control o_website_form_input" t-att-name="field.name" t-att-required="field.required"> + <select class="form-control s_website_form_input" t-att-name="field.name" t-att-required="field.required || field.modelRequired || None" t-att-id="field.id"> <t t-foreach="field.records" t-as="record"> <option t-att-value="record.id" t-att-selected="record.selected"> <t t-esc="record.display_name"/> @@ -197,37 +281,42 @@ <!-- Generic one2many --> <t t-if="field.relation != 'ir.attachment'"> <t t-call="website_form.field"> - <div class="o_website_form_flex" data-display="horizontal"> + <div class="row s_col_no_resize s_col_no_bgcolor s_website_form_multiple" t-att-data-name="field.name" t-att-data-display="field.formatInfo.multiPosition"> <t t-if="!field.records"> <input - class="o_website_form_input" + class="s_website_form_input" t-att-name="field.name" t-att-value="record.id" - t-att-required="field.required" + t-att-required="field.required || field.modelRequired || None" placeholder="No matching record !" /> </t> <t t-foreach="field.records" t-as="record"> - <div class="checkbox o_website_form_flex_item"> - <label class="col-form-label"> - <input - type="checkbox" - class="o_website_form_input" - t-att-name="field.name" - t-att-value="record.id" - t-att-required="field.required" - /> - <span> - <t t-esc="record.display_name"/> - </span> - </label> - </div> + <t t-call="website_form.checkbox"/> </t> </div> </t> </t> </t> + <!-- Checkbox --> + <t t-name="website_form.checkbox"> + <div t-attf-class="checkbox col-12 #{field.formatInfo.multiPosition === 'horizontal' and 'col-lg-4 col-md-6' or ''}"> + <label class="col-form-label"> + <input + type="checkbox" + class="s_website_form_input" + t-att-name="field.name" + t-att-value="record.id" + t-att-required="field.required || field.modelRequired || None" + /> + <span> + <t t-esc="record.display_name"/> + </span> + </label> + </div> + </t> + <!-- Many2Many Field --> <t t-name="website_form.field_many2many"> <t t-call="website_form.field_one2many"/> @@ -238,10 +327,11 @@ <t t-call="website_form.field"> <input type="file" - class="form-control o_website_form_input" + class="form-control-file s_website_form_input" t-att-name="field.name" - t-att-required="field.required" + t-att-required="field.required || field.modelRequired || None" t-att-multiple="multiple" + t-att-id="field.id" /> </t> </t> diff --git a/addons/website_form/static/tests/tours/website_form_editor.js b/addons/website_form/static/tests/tours/website_form_editor.js index ceb5f9a4efb..60723cb4118 100644 --- a/addons/website_form/static/tests/tours/website_form_editor.js +++ b/addons/website_form/static/tests/tours/website_form_editor.js @@ -1,459 +1,238 @@ -odoo.define('website_form_editor.tour', function(require) { +odoo.define('website_form_editor.tour', function (require) { 'use strict'; - var rpc = require('web.rpc'); - var tour = require("web_tour.tour"); + const rpc = require('web.rpc'); + const tour = require("web_tour.tour"); + + const selectButtonByText = function (text) { + return [{ + content: "Open the select", + trigger: `we-select:has(we-button:contains("${text}")) we-toggler`, + }, + { + content: "Click on the option", + trigger: `we-select we-button:contains("${text}")`, + }]; + }; + const selectButtonByData = function (data) { + return [{ + content: "Open the select", + trigger: `we-select:has(we-button[${data}]) we-toggler`, + }, { + content: "Click on the option", + trigger: `we-select we-button[${data}]`, + }]; + }; + const addField = function (data, name, type, label, required, hidden) { + const ret = [{ + content: "Select form", + extra_trigger: '.s_website_form_field', + trigger: 'section.s_website_form', + }, { + content: "Add field", + trigger: 'we-button[data-add-field]', + }, + ...selectButtonByData(data), + { + content: "Wait for field to load", + trigger: `.s_website_form_field[data-type="${name}"], .s_website_form_input[name="${name}"]`, //custom or existing field + run: function () {}, + }]; + let testText = '.s_website_form_field'; + if (required) { + testText += '.s_website_form_required'; + ret.push({ + content: "Mark the field as required", + trigger: 'we-button[data-name="required_opt"]', + }); + } + if (hidden) { + testText += '.s_website_form_field_hidden'; + ret.push({ + content: "Mark the field as hidden", + trigger: 'we-button[data-name="hidden_opt"]', + }); + } + if (label) { + testText += `:has(label:contains("${label}"))`; + ret.push({ + content: "Change the label text", + trigger: 'we-input[data-set-label-text] input', + run: `text ${label}`, + }); + } + if (type !== 'checkbox' && type !== 'radio' && type !== 'select') { + let inputType = type === 'textarea' ? type : `input[type="${type}"]`; + testText += `:has(${inputType}[name="${name}"]${required ? '[required]' : ''})`; + } + ret.push({ + content: "Check the resulting field", + trigger: testText, + run: function () {}, + }); + return ret; + }; + const addCustomField = function (name, type, label, required, hidden) { + return addField(`data-custom-field="${name}"`, name, type, label, required, hidden); + }; + const addExistingField = function (name, type, label, required, hidden) { + return addField(`data-existing-field="${name}"`, name, type, label, required, hidden); + }; tour.register("website_form_editor_tour", { test: true, }, [ // Drop a form builder snippet and configure it { - content: "Enter edit mode", - trigger: "a[data-action=edit]" - }, - { - content: "Drop the form snippet", - trigger: "#oe_snippets .oe_snippet:has(.s_website_form) .oe_snippet_thumbnail", - run: "drag_and_drop #wrap", - }, - { - content: "Check dropped snippet", - trigger: "body:has(form[action*='/website_form/'])", - }, - { - content: "Check if the snippet is dropped and if the modal is opened", - trigger: "body:has(form[action*='/website_form/'])" + - ":has(.modal-body:has(select[name='model_selection'])" + - ":has(input[name='success_page']))", - run: function () {}, - in_modal: false, - }, - { - content: "Change the action to create issues", - trigger: ".modal-body select", - run: "text project.issue" - }, - { - content: "Change the action to Send an E-mail", - trigger: ".modal-body select", - run: function (actions) { - actions.text($('select[name="model_selection"] option:contains("Send an E-mail")').val()); - } - }, - { - content: "Complete Recipient E-mail", - extra_trigger: ".modal-body input[name='email_to']", - trigger: ".modal-body input[name='email_to']", - run: "text test@test.test" - }, - { - content: "Click on Save", - trigger: ".modal-footer button:first" - }, - // Add the date field - { - content: "Click on Form snippet", - trigger: ".s_website_form[data-model_name]" - }, - { - content: "Click on Add a model field", - trigger: "[data-website_form_field_modal]" - }, - { - content: "Select the date field", - trigger: "select[name='field_selection']", - run: "text date" - }, - { - content: "Click on Save", - trigger: ".modal-footer button:first" - }, - - // Customize date field - { - content: "Change the label", - trigger: ".col-form-label[for='date']", - run: "text Test Date" - }, - { - content: "Click on Required", - trigger: "[data-website_form_field_require]" - }, - { - content: "Check the resulting field", - trigger: ".form-field.o_website_form_required_custom" + - ":has(input[type=text][name=date][required])" + - ":has(label:contains('Test Date'))", - run: function () {}, - }, - - // Add record_name field - { - content: "Click on Form snippet", - trigger: ".s_website_form[data-model_name]" - }, - { - content: "Click on Add a model field", - trigger: "[data-website_form_field_modal]" - }, - { - content: "Select the record_name field", - trigger: "select[name='field_selection']", - run: "text record_name" - }, - { - content: "Click on Save", - trigger: ".modal-footer button:first" - }, - - // Customize record_name field - { - content: "Change the label", - trigger: ".col-form-label[for='record_name']", - run: "text Awesome Label" - }, - { - content: "Click on Hidden", - trigger: "[data-select-class='o_website_form_field_hidden']" - }, - { - content: "Check the resulting field", - trigger: ".form-field.o_website_form_field_hidden" + - ":has(input:not([required])[type=text][name=record_name])" + - ":has(label:contains('Awesome Label'))", - run: function () {}, - }, + content: "Enter edit mode", + trigger: 'a[data-action=edit]', + }, { + content: "Drop the form snippet", + trigger: '#oe_snippets .oe_snippet:has(.s_website_form) .oe_snippet_thumbnail', + run: 'drag_and_drop #wrap', + }, { + content: "Check dropped snippet and select it", + extra_trigger: '.s_website_form_field', + trigger: 'section.s_website_form', + }, + ...selectButtonByText('Send an E-mail'), + { + content: "Form has a model name", + trigger: 'section.s_website_form form[data-model_name="mail.mail"]', + }, { + content: "Complete Recipient E-mail", + trigger: '[data-field-name="email_to"] input', + run: 'text_blur test@test.test', + }, + ...addExistingField('date', 'text', 'Test Date', true), + ...addExistingField('record_name', 'text', 'Awesome Label', false, true), - // Add body_html field - { - content: "Click on Form snippet", - trigger: ".s_website_form[data-model_name]" - }, - { - content: "Click on Add a model field", - trigger: "[data-website_form_field_modal]" - }, - { - content: "Select the body_html field", - trigger: "select[name='field_selection']", - run: "text body_html" - }, - { - content: "Click on Save", - trigger: ".modal-footer button:first" - }, + ...addExistingField('body_html', 'textarea', 'Your Message', true), - // Customize subject field - { - content: "Change the label", - trigger: ".col-form-label[for='body_html']", - run: "text Your Message" - }, - { - content: "Click on Required", - trigger: "[data-website_form_field_require]" - }, - { - content: "Check the resulting field", - trigger: ".form-field.o_website_form_required_custom" + - ":has(textarea[name=body_html][required])" + - ":has(label:contains('Your Message'))", - run: function () {}, - }, + ...addExistingField('recipient_ids', 'checkbox'), - // Add recipient_ids relational field - { - content: "Click on Form snippet", - trigger: ".s_website_form[data-model_name]" - }, - { - content: "Click on Add a model field", - trigger: "[data-website_form_field_modal]" - }, - { - content: "Select the recipient_ids field", - trigger: "select[name='field_selection']", - run: "text recipient_ids" - }, - { - content: "Click on Save", - trigger: ".modal-footer button:first" - }, - { - content: "Check the resulting field", - trigger: ".form-field:has(.col-form-label:contains('To (Partners)'))", - run: function () {}, - }, - - // Add a custom multiple checkboxes field - { - content: "Click on Form snippet", - trigger: ".s_website_form[data-model_name]", - }, - { - content: "Open custom fields menu", - trigger: "we-select:contains('Multiple Checkboxes')", - }, - { - content: "Click on Multiple Checkboxes in menu Add a custom field", - trigger: "we-button:contains('Multiple Checkboxes')", - }, - - // Customize custom multiple checkboxes field - { - content: "Change the label", - trigger: ".col-form-label[for='Custom Multiple Checkboxes']", - extra_trigger: ".s_website_form:not(:has(.col-form-label[for='Custom Multiple Checkboxes']:eq(1)))", - run: "text Products" - }, - { - content: "Change Option 1 label", - trigger: "label:contains('Option 1') span", - run: "text Iphone" - }, - { - content: "Change Option 2 label", - trigger: "label:contains('Option 2') span", - run: "text Galaxy S" - }, - { - content: "Click on Option 3", - trigger: "label:contains('Option 3') span", - }, - { - content: "Duplicate Option 3", - trigger: ".oe_snippet_clone:last", - }, - { - content: "Change first Option 3 label", - trigger: "label:contains('Option 3'):first span", - run: "text Xperia" - }, - { - content: "Change last Option label", - trigger: "label:contains('Option 3') span", - run: "text Wiko Stairway" - }, - { - content: "Click on field", - trigger: ".col-form-label:contains('Products')" - }, - { - content: "Click on Required", - trigger: "[data-website_form_field_require]" - }, - { - content: "Click on first Check Box", - trigger: "label:contains('Iphone') span", - }, - { - content: "Open display menu", - trigger: "we-select:contains('Vertical')", - }, - { - content: "Click on 'Vertical' in menu 'Display'", - trigger: "we-button:contains('Vertical')", - }, - { - content: "Check the resulting field", - trigger: ".form-field.o_website_form_custom.o_website_form_required_custom" + - ":has(.col-form-label:contains('Products'))" + - ":has(.o_website_form_flex[data-display='vertical'])" + - ":has(.checkbox label:contains('Iphone'):has(input[type='checkbox'][required]))" + - ":has(.checkbox label:contains('Galaxy S'):has(input[type='checkbox'][required]))" + - ":has(.checkbox label:contains('Xperia'):has(input[type='checkbox'][required]))" + - ":has(.checkbox label:contains('Wiko Stairway'):has(input[type='checkbox'][required]))", - run: function () {}, - }, - - // Add a custom radio field - { - content: "Click on Form snippet", - trigger: ".s_website_form[data-model_name]", - }, - { - content: "Open custom fields menu", - trigger: "we-select:contains('Radio Buttons')" + ...addCustomField('one2many', 'checkbox', 'Products', true), + { + content: "Change Option 1 label", + trigger: 'we-list table input:eq(0)', + run: 'text Iphone', + }, { + content: "Change Option 2 label", + trigger: 'we-list table input:eq(1)', + run: 'text Galaxy S', + }, { + content: "Change first Option 3 label", + trigger: 'we-list table input:eq(2)', + run: 'text Xperia', + }, { + content: "Click on Add new Checkbox", + trigger: 'we-list we-button.o_we_list_add_optional', + }, { + content: "Change added Option label", + trigger: 'we-list table input:eq(3)', + run: 'text Wiko Stairway', + }, { + content: "Check the resulting field", + trigger: ".s_website_form_field.s_website_form_custom.s_website_form_required" + + ":has(.s_website_form_multiple[data-display='horizontal'])" + + ":has(.checkbox label:contains('Iphone'):has(input[type='checkbox'][required]))" + + ":has(.checkbox label:contains('Galaxy S'):has(input[type='checkbox'][required]))" + + ":has(.checkbox label:contains('Xperia'):has(input[type='checkbox'][required]))" + + ":has(.checkbox label:contains('Wiko Stairway'):has(input[type='checkbox'][required]))", + run: function () {}, }, + ...selectButtonByData('data-multi-checkbox-display="vertical"'), { - content: "Click on Radio Buttons in menu Add a custom field", - trigger: "we-button:contains('Radio Buttons')" + content: "Check the resulting field", + trigger: ".s_website_form_field.s_website_form_custom.s_website_form_required" + + ":has(.s_website_form_multiple[data-display='vertical'])" + + ":has(.checkbox label:contains('Iphone'):has(input[type='checkbox'][required]))" + + ":has(.checkbox label:contains('Galaxy S'):has(input[type='checkbox'][required]))" + + ":has(.checkbox label:contains('Xperia'):has(input[type='checkbox'][required]))" + + ":has(.checkbox label:contains('Wiko Stairway'):has(input[type='checkbox'][required]))", + run: function () {}, }, - // Customize Radio Buttons field - { - content: "Change the label", - trigger: ".col-form-label[for='Custom Radio Buttons']", - run: "text Service" - }, - { - content: "Change Option 1 label", - trigger: "label:contains('Option 1') span", - run: "text After-sales Service" - }, - { - content: "Change Option 2 label", - trigger: "label:contains('Option 2') span", - run: "text Invoicing Service" - }, - { - content: "Click on Option 3", - trigger: "label:contains('Option 3') span", - }, - { - content: "Duplicate Option 3", - trigger: ".oe_snippet_clone:last", - }, - { - content: "Change first Option 3 label", - trigger: "label:contains('Option 3'):first span", - run: "text Development Service" - }, - { - content: "Change last Option label", - trigger: "label:contains('Option 3') span", - run: "text Management Service" - }, - { - content: "Click on field", - trigger: ".col-form-label:contains('Service')" - }, - { - content: "Click on Required", - trigger: "[data-website_form_field_require]" - }, - { - content: "Click on Required", - trigger: "[data-website_form_field_require]", - extra_trigger: ".form-field.o_website_form_custom.o_website_form_required_custom" + - ":has(.col-form-label:contains('Service'))", - }, - { - content: "Check the resulting field", - trigger: ".form-field.o_website_form_custom:not(.o_website_form_required_custom)" + - ":has(.col-form-label:contains('Service'))" + - ":has(.radio label:contains('After-sales Service'):has(input[type='radio']:not([required])))" + - ":has(.radio label:contains('Invoicing Service'):has(input[type='radio']:not([required])))" + - ":has(.radio label:contains('Development Service'):has(input[type='radio']:not([required])))" + - ":has(.radio label:contains('Management Service'):has(input[type='radio']:not([required])))", - run: function () {}, + ...addCustomField('selection', 'radio', 'Service', true), + { + content: "Change Option 1 label", + trigger: 'we-list table input:eq(0)', + run: 'text After-sales Service', + }, { + content: "Change Option 2 label", + trigger: 'we-list table input:eq(1)', + run: 'text Invoicing Service', + }, { + content: "Change first Option 3 label", + trigger: 'we-list table input:eq(2)', + run: 'text Development Service', + }, { + content: "Click on Add new Checkbox", + trigger: 'we-list we-button.o_we_list_add_optional', + }, { + content: "Change last Option label", + trigger: 'we-list table input:eq(3)', + run: 'text Management Service', + }, { + content: "Mark the field as not required", + trigger: 'we-button[data-name="required_opt"]', + }, { + content: "Check the resulting field", + trigger: ".s_website_form_field.s_website_form_custom:not(.s_website_form_required)" + + ":has(.radio label:contains('After-sales Service'):has(input[type='radio']:not([required])))" + + ":has(.radio label:contains('Invoicing Service'):has(input[type='radio']:not([required])))" + + ":has(.radio label:contains('Development Service'):has(input[type='radio']:not([required])))" + + ":has(.radio label:contains('Management Service'):has(input[type='radio']:not([required])))", + run: function () {}, }, - - // Add a custom selection field - { - content: "Click on Form snippet", - trigger: ".s_website_form[data-model_name]", - }, - { - content: "Open custom fields menu", - trigger: "we-select:contains('Selection')" - }, - { - content: "Click on Selection in menu Add a custom field", - trigger: "we-button:contains('Selection')", - }, + ...addCustomField('many2one', 'select', 'State', true), // Customize custom selection field { - content: "Change the label", - trigger: ".col-form-label[for='Custom Selection']", - run: "text State" - }, - { - content: "Change Option 1 Label", - trigger: ".o_website_form_select_item:contains('Option 1')", - run: "text Germany" - }, - { - content: "Change Option 2 Label", - trigger: ".o_website_form_select_item:contains('Option 2')", - run: "text Belgium" - }, - { - content: "Click on Option 3", - trigger: ".o_website_form_select_item:contains('Option 3')", - }, - { - content: "Duplicate Option 3", - trigger: ".oe_snippet_clone:last", - }, - { - content: "Change first Option 3 label", - trigger: ".o_website_form_select_item:contains('Option 3'):first", - run: "text France" - }, - { - content: "Change last Option label", - trigger: ".o_website_form_select_item:contains('Option 3')", - run: "text Canada" - }, - { - content: "Click on Germany Option", - trigger: ".o_website_form_select_item:contains('Germany')", - }, - { - content: "Remove Germany Option", - trigger: ".oe_snippet_remove:last", - }, - { - content: "Click on field", - trigger: ".col-form-label:contains('State')" - }, - { - content: "Click on Required", - trigger: "[data-website_form_field_require]" - }, - { - content: "Check the resulting snippet", - trigger: ".form-field.o_website_form_custom.o_website_form_required_custom" + - ":has(label:contains('State'))" + - ":has(select[required]:hidden)" + - ":has(.o_website_form_select_item:contains('Belgium'))" + - ":has(.o_website_form_select_item:contains('France'))" + - ":has(.o_website_form_select_item:contains('Canada'))" + - ":not(:has(.o_website_form_select_item:contains('Germany')))", - run: function () {}, + content: "Change Option 1 Label", + trigger: 'we-list table input:eq(0)', + run: 'text Germany', + }, { + content: "Change Option 2 Label", + trigger: 'we-list table input:eq(1)', + run: 'text Belgium', + }, { + content: "Change first Option 3 label", + trigger: 'we-list table input:eq(2)', + run: 'text France', + }, { + content: "Click on Add new Checkbox", + trigger: 'we-list we-button.o_we_list_add_optional', + }, { + content: "Change last Option label", + trigger: 'we-list table input:eq(3)', + run: 'text Canada', + }, { + content: "Remove Germany Option", + trigger: '.o_we_select_remove_option:eq(0)', + }, { + content: "Check the resulting snippet", + trigger: ".s_website_form_field.s_website_form_custom.s_website_form_required" + + ":has(label:contains('State'))" + + ":has(select[required]:hidden)" + + ":has(.s_website_form_select_item:contains('Belgium'))" + + ":has(.s_website_form_select_item:contains('France'))" + + ":has(.s_website_form_select_item:contains('Canada'))" + + ":not(:has(.s_website_form_select_item:contains('Germany')))", + run: function () {}, }, - // Add attachment_ids field - { - content: "Click on Form snippet", - trigger: ".s_website_form[data-model_name]" - }, - { - content: "Click on Add a model field", - trigger: "[data-website_form_field_modal]" - }, - { - content: "Select the attachment_ids field", - trigger: "select[name='field_selection']", - run: "text attachment_ids" - }, - { - content: "Click on Save", - trigger: ".modal-footer button:first" - }, + ...addExistingField('attachment_ids', 'file', 'Invoice Scan'), - // Customize attachment_ids field - { - content: "Change the label", - trigger: ".col-form-label[for='attachment_ids']", - run: "text Invoice Scan" - }, + // Save the page { - content: "Check the resulting field", - trigger: ".form-field" + - ":has(input[type=file][name=attachment_ids])" + - ":has(label:contains('Invoice Scan'))", - run: function () { + trigger: 'body', + run: function () { $('body').append('<div id="completlyloaded"></div>'); }, }, - - // Save the page { content: "Save the page", trigger: "button[data-action=save]", @@ -470,87 +249,87 @@ odoo.define('website_form_editor.tour', function(require) { { content: "Try to send empty form", extra_trigger: "form[data-model_name='mail.mail']" + - "[data-success_page='/contactus-thank-you']" + - ":has(.form-field:has(label:contains('Your Name')):has(input[type='text'][name='Your Name'][required]))" + - ":has(.form-field:has(label:contains('Email')):has(input[type='email'][name='email_from'][required]))" + - ":has(.form-field:has(label:contains('Your Question')):has(textarea[name='Your Question'][required]))" + - ":has(.form-field:has(label:contains('Subject')):has(input[type='text'][name='subject'][required]))" + - ":has(.form-field:has(label:contains('Test Date')):has(input[type='text'][name='date'][required]))" + - ":has(.form-field:has(label:contains('Awesome Label')):hidden)" + - ":has(.form-field:has(label:contains('Your Message')):has(textarea[name='body_html'][required]))" + - ":has(.form-field:has(label:contains('Products')):has(input[type='checkbox'][name='Products'][value='Iphone'][required]))" + - ":has(.form-field:has(label:contains('Products')):has(input[type='checkbox'][name='Products'][value='Galaxy S'][required]))" + - ":has(.form-field:has(label:contains('Products')):has(input[type='checkbox'][name='Products'][value='Xperia'][required]))" + - ":has(.form-field:has(label:contains('Products')):has(input[type='checkbox'][name='Products'][value='Wiko Stairway'][required]))" + - ":has(.form-field:has(label:contains('Service')):has(input[type='radio'][name='Service'][value='After-sales Service']:not([required])))" + - ":has(.form-field:has(label:contains('Service')):has(input[type='radio'][name='Service'][value='Invoicing Service']:not([required])))" + - ":has(.form-field:has(label:contains('Service')):has(input[type='radio'][name='Service'][value='Development Service']:not([required])))" + - ":has(.form-field:has(label:contains('Service')):has(input[type='radio'][name='Service'][value='Management Service']:not([required])))" + - ":has(.form-field:has(label:contains('State')):has(select[name='State'][required]:has(option[value='Belgium'])))" + - ":has(.form-field.o_website_form_required_custom:has(label:contains('State')):has(select[name='State'][required]:has(option[value='France'])))" + - ":has(.form-field:has(label:contains('State')):has(select[name='State'][required]:has(option[value='Canada'])))" + - ":has(.form-field:has(label:contains('Invoice Scan')))" + - ":has(.form-field:has(input[name='email_to'][value='test@test.test']))", - trigger: ".o_website_form_send" + "[data-success-page='/contactus-thank-you']" + + ":has(.s_website_form_field:has(label:contains('Your Name')):has(input[type='text'][name='Your Name'][required]))" + + ":has(.s_website_form_field:has(label:contains('Email')):has(input[type='email'][name='email_from'][required]))" + + ":has(.s_website_form_field:has(label:contains('Your Question')):has(textarea[name='Your Question'][required]))" + + ":has(.s_website_form_field:has(label:contains('Subject')):has(input[type='text'][name='subject'][required]))" + + ":has(.s_website_form_field:has(label:contains('Test Date')):has(input[type='text'][name='date'][required]))" + + ":has(.s_website_form_field:has(label:contains('Awesome Label')):hidden)" + + ":has(.s_website_form_field:has(label:contains('Your Message')):has(textarea[name='body_html'][required]))" + + ":has(.s_website_form_field:has(label:contains('Products')):has(input[type='checkbox'][name='Products'][value='Iphone'][required]))" + + ":has(.s_website_form_field:has(label:contains('Products')):has(input[type='checkbox'][name='Products'][value='Galaxy S'][required]))" + + ":has(.s_website_form_field:has(label:contains('Products')):has(input[type='checkbox'][name='Products'][value='Xperia'][required]))" + + ":has(.s_website_form_field:has(label:contains('Products')):has(input[type='checkbox'][name='Products'][value='Wiko Stairway'][required]))" + + ":has(.s_website_form_field:has(label:contains('Service')):has(input[type='radio'][name='Service'][value='After-sales Service']:not([required])))" + + ":has(.s_website_form_field:has(label:contains('Service')):has(input[type='radio'][name='Service'][value='Invoicing Service']:not([required])))" + + ":has(.s_website_form_field:has(label:contains('Service')):has(input[type='radio'][name='Service'][value='Development Service']:not([required])))" + + ":has(.s_website_form_field:has(label:contains('Service')):has(input[type='radio'][name='Service'][value='Management Service']:not([required])))" + + ":has(.s_website_form_field:has(label:contains('State')):has(select[name='State'][required]:has(option[value='Belgium'])))" + + ":has(.s_website_form_field.s_website_form_required:has(label:contains('State')):has(select[name='State'][required]:has(option[value='France'])))" + + ":has(.s_website_form_field:has(label:contains('State')):has(select[name='State'][required]:has(option[value='Canada'])))" + + ":has(.s_website_form_field:has(label:contains('Invoice Scan')))" + + ":has(.s_website_form_field:has(input[name='email_to'][value='test@test.test']))", + trigger: ".s_website_form_send" }, { content: "Check if required fields were detected and complete the Subject field", - extra_trigger: "form:has(#o_website_form_result.text-danger)" + - ":has(.form-field:has(label:contains('Your Name')).o_has_error)" + - ":has(.form-field:has(label:contains('Email')).o_has_error)" + - ":has(.form-field:has(label:contains('Your Question')).o_has_error)" + - ":has(.form-field:has(label:contains('Subject')).o_has_error)" + - ":has(.form-field:has(label:contains('Test Date')).o_has_error)" + - ":has(.form-field:has(label:contains('Your Message')).o_has_error)" + - ":has(.form-field:has(label:contains('Products')).o_has_error)" + - ":has(.form-field:has(label:contains('Service')):not(.o_has_error))" + - ":has(.form-field:has(label:contains('State')):not(.o_has_error))" + - ":has(.form-field:has(label:contains('Invoice Scan')):not(.o_has_error))", + extra_trigger: "form:has(#s_website_form_result.text-danger)" + + ":has(.s_website_form_field:has(label:contains('Your Name')).o_has_error)" + + ":has(.s_website_form_field:has(label:contains('Email')).o_has_error)" + + ":has(.s_website_form_field:has(label:contains('Your Question')).o_has_error)" + + ":has(.s_website_form_field:has(label:contains('Subject')).o_has_error)" + + ":has(.s_website_form_field:has(label:contains('Test Date')).o_has_error)" + + ":has(.s_website_form_field:has(label:contains('Your Message')).o_has_error)" + + ":has(.s_website_form_field:has(label:contains('Products')).o_has_error)" + + ":has(.s_website_form_field:has(label:contains('Service')):not(.o_has_error))" + + ":has(.s_website_form_field:has(label:contains('State')):not(.o_has_error))" + + ":has(.s_website_form_field:has(label:contains('Invoice Scan')):not(.o_has_error))", trigger: "input[name=subject]", run: "text Jane Smith" }, { content: "Update required field status by trying to Send again", - trigger: ".o_website_form_send" + trigger: ".s_website_form_send" }, { content: "Check if required fields were detected and complete the Message field", - extra_trigger: "form:has(#o_website_form_result.text-danger)" + - ":has(.form-field:has(label:contains('Your Name')).o_has_error)" + - ":has(.form-field:has(label:contains('Email')).o_has_error)" + - ":has(.form-field:has(label:contains('Your Question')).o_has_error)" + - ":has(.form-field:has(label:contains('Subject')):not(.o_has_error))" + - ":has(.form-field:has(label:contains('Test Date')).o_has_error)" + - ":has(.form-field:has(label:contains('Your Message')).o_has_error)" + - ":has(.form-field:has(label:contains('Products')).o_has_error)" + - ":has(.form-field:has(label:contains('Service')):not(.o_has_error))" + - ":has(.form-field:has(label:contains('State')):not(.o_has_error))" + - ":has(.form-field:has(label:contains('Invoice Scan')):not(.o_has_error))", + extra_trigger: "form:has(#s_website_form_result.text-danger)" + + ":has(.s_website_form_field:has(label:contains('Your Name')).o_has_error)" + + ":has(.s_website_form_field:has(label:contains('Email')).o_has_error)" + + ":has(.s_website_form_field:has(label:contains('Your Question')).o_has_error)" + + ":has(.s_website_form_field:has(label:contains('Subject')):not(.o_has_error))" + + ":has(.s_website_form_field:has(label:contains('Test Date')).o_has_error)" + + ":has(.s_website_form_field:has(label:contains('Your Message')).o_has_error)" + + ":has(.s_website_form_field:has(label:contains('Products')).o_has_error)" + + ":has(.s_website_form_field:has(label:contains('Service')):not(.o_has_error))" + + ":has(.s_website_form_field:has(label:contains('State')):not(.o_has_error))" + + ":has(.s_website_form_field:has(label:contains('Invoice Scan')):not(.o_has_error))", trigger: "textarea[name=body_html]", run: "text A useless message" }, { content: "Update required field status by trying to Send again", - trigger: ".o_website_form_send" + trigger: ".s_website_form_send" }, { content: "Check if required fields was detected and check a product. If this fails, you probably broke the cleanForSave.", - extra_trigger: "form:has(#o_website_form_result.text-danger)" + - ":has(.form-field:has(label:contains('Your Name')).o_has_error)" + - ":has(.form-field:has(label:contains('Email')).o_has_error)" + - ":has(.form-field:has(label:contains('Your Question')).o_has_error)" + - ":has(.form-field:has(label:contains('Subject')):not(.o_has_error))" + - ":has(.form-field:has(label:contains('Test Date')).o_has_error)" + - ":has(.form-field:has(label:contains('Your Message')):not(.o_has_error))" + - ":has(.form-field:has(label:contains('Products')).o_has_error)" + - ":has(.form-field:has(label:contains('Service')):not(.o_has_error))" + - ":has(.form-field:has(label:contains('State')):not(.o_has_error))" + - ":has(.form-field:has(label:contains('Invoice Scan')):not(.o_has_error))", + extra_trigger: "form:has(#s_website_form_result.text-danger)" + + ":has(.s_website_form_field:has(label:contains('Your Name')).o_has_error)" + + ":has(.s_website_form_field:has(label:contains('Email')).o_has_error)" + + ":has(.s_website_form_field:has(label:contains('Your Question')).o_has_error)" + + ":has(.s_website_form_field:has(label:contains('Subject')):not(.o_has_error))" + + ":has(.s_website_form_field:has(label:contains('Test Date')).o_has_error)" + + ":has(.s_website_form_field:has(label:contains('Your Message')):not(.o_has_error))" + + ":has(.s_website_form_field:has(label:contains('Products')).o_has_error)" + + ":has(.s_website_form_field:has(label:contains('Service')):not(.o_has_error))" + + ":has(.s_website_form_field:has(label:contains('State')):not(.o_has_error))" + + ":has(.s_website_form_field:has(label:contains('Invoice Scan')):not(.o_has_error))", trigger: "input[name=Products][value='Wiko Stairway']" }, { content: "Complete Date field", - trigger: ".o_website_form_datetime [data-toggle='datetimepicker']", + trigger: ".s_website_form_datetime [data-toggle='datetimepicker']", }, { content: "Check another product", @@ -571,13 +350,18 @@ odoo.define('website_form_editor.tour', function(require) { run: "text test@mail.com" }, { + content: "Complete Subject field", + trigger: 'input[name="subject"]', + run: 'text subject', + }, + { content: "Complete Your Question field", trigger: "textarea[name='Your Question']", run: "text magan" }, { content: "Send the form", - trigger: ".o_website_form_send" + trigger: ".s_website_form_send" }, { content: "Check form is submitted without errors", diff --git a/addons/website_form/views/assets.xml b/addons/website_form/views/assets.xml index 11445b89b63..b5433e0da00 100644 --- a/addons/website_form/views/assets.xml +++ b/addons/website_form/views/assets.xml @@ -1,9 +1,9 @@ <?xml version="1.0" encoding="utf-8"?> <odoo> <data> - <template id="assets_editor" name="Website Form Editor Assets Editor" inherit_id="website.assets_editor"> <xpath expr="." position="inside"> + <link rel="stylesheet" type="text/scss" href="/website_form/static/src/scss/wysiwyg_snippets.scss"/> <script type="text/javascript" src="/website_form/static/src/snippets/s_website_form/options.js"/> <script type="text/javascript" src="/website_form/static/src/js/website_form_editor_registry.js"/> </xpath> diff --git a/addons/website_form/views/snippets/s_website_form.xml b/addons/website_form/views/snippets/s_website_form.xml index 2c49bfccd62..9a4a28fe6db 100644 --- a/addons/website_form/views/snippets/s_website_form.xml +++ b/addons/website_form/views/snippets/s_website_form.xml @@ -1,86 +1,109 @@ <?xml version="1.0" encoding="utf-8"?> <odoo> -<template id="s_website_form" name="Form Builder"> - <form action="/website_form/" method="post" class="s_website_form container-fluid mt32" enctype="multipart/form-data"> +<template id="s_website_form" name="Form"> + <section class="s_website_form" data-vcss="001"> <div class="container"> - <div class="form-group row"> - <div class="offset-lg-3 offset-md-4 col-md-8 col-lg-7"> - <button class="btn btn-primary btn-lg o_website_form_send">Send</button> - <span id="o_website_form_result"></span> + <form action="/website_form/" method="post" enctype="multipart/form-data" class="o_mark_required" data-mark="*"> + <div class="s_website_form_rows row s_col_no_bgcolor"> + <div class="form-group col-12 s_website_form_submit"> + <div class="row s_col_no_resize s_col_no_bgcolor"> + <div style="width: 200px" class="col-sm-auto s_website_form_label"/> + <div class="col-sm"> + <a href="#" role="button" class="btn btn-primary btn-lg s_website_form_send">Submit</a> + <span id="s_website_form_result"></span> + </div> + </div> + </div> </div> - </div> + </form> </div> - </form> + </section> </template> -<template id="snippet_options" inherit_id="website.snippet_options"> - <xpath expr="//*[@t-set='so_snippet_addition_selector']" position="inside">, .s_website_form</xpath> +<template id="s_website_form_options" inherit_id="website.snippet_options"> <xpath expr="//div" position="after"> <!-- Form --> - <div data-js="website_form_editor" data-selector=".s_website_form"> - <we-button data-website_form_model_modal="" data-no-preview="true">Change Form Parameters</we-button> - <we-button data-website_form_field_modal="" data-no-preview="true">Add an existing field</we-button> - <we-select string="Add a custom field" data-no-preview="true"> - <we-button data-website_form_custom_field="char">Text</we-button> - <we-button data-website_form_custom_field="text">Long Text</we-button> - <we-button data-website_form_custom_field="email">Email</we-button> - <we-button data-website_form_custom_field="integer">Number</we-button> - <we-button data-website_form_custom_field="float">Decimal Number</we-button> - <we-button data-website_form_custom_field="boolean">Checkbox</we-button> - <we-button data-website_form_custom_field="selection">Radio Buttons</we-button> - <we-button data-website_form_custom_field="many2one">Selection</we-button> - <we-button data-website_form_custom_field="one2many">Multiple Checkboxes</we-button> - <we-button data-website_form_custom_field="date">Date</we-button> - <we-button data-website_form_custom_field="datetime">Date & Time</we-button> - <we-button data-website_form_custom_field="binary">File Upload</we-button> + <div data-js="WebsiteFormEditor" data-selector=".s_website_form" data-target="form"> + <we-select string="Display Fields Mark" data-name="field_mark_select" data-no-preview="true"> + <we-button data-select-class="">None</we-button> + <we-button data-select-class="o_mark_required" data-name="form_required_opt">Required</we-button> + <we-button data-select-class="o_mark_optional" data-name="form_optional_opt">Optional</we-button> </we-select> + <we-input data-set-mark="" data-dependencies="form_required_opt, form_optional_opt"/> + <we-input string="Labels Width" + data-select-style="" data-css-property="width" + data-unit="px" data-apply-to=".s_website_form_label"/> + <we-row> + <we-select string="On Success" data-no-preview="true"> + <we-button data-on-success="nothing">Nothing</we-button> + <we-button data-on-success="redirect" data-name="show_redirect_opt">Redirect</we-button> + <we-button data-on-success="message" data-name="show_message_opt">Show Message</we-button> + </we-select> + <we-button class="fa fa-fw fa-edit align-self-end toggle-edit-message" title="Edit Message" data-name="message_opt" data-dependencies="show_message_opt"/> + </we-row> + <we-urlpicker string="URL" data-select-data-attribute="/contactus-thank-you" data-attribute-name="successPage" data-name="url_opt" data-dependencies="show_redirect_opt"/> </div> - <!-- Field --> - <div data-js='website_form_editor' data-selector=".form-field" data-drop-near=".form-field"> - <we-checkbox string="Hidden" data-no-preview="true" - data-select-class="o_website_form_field_hidden"/> + <!-- Add Field Form --> + <div data-js="AddFieldForm" data-selector=".s_website_form" data-target="form"> + <we-button class="fa fa-fw fa-plus" + title="Add a new field at the end" + data-add-field="" + data-no-preview="true"/> </div> - <!-- Add an option to display radio buttons / checkboxes horizontally or vertically --> - <div data-js='website_form_editor_choice_field_display' data-selector=".o_website_form_flex"> - <we-select string="Display" data-no-preview="true"> - <we-button data-website_form_choice_field_display="horizontal">Horizontal</we-button> - <we-button data-website_form_choice_field_display="vertical">Vertical</we-button> - </we-select> + <!-- Add Field --> + <div data-js="AddField" data-selector=".s_website_form_field"> + <we-button class="fa fa-fw fa-plus" + title="Add a new field after this one" + data-add-field="" + data-no-preview="true"/> </div> - <!-- Add move, duplicate and remove controllers to checkboxes and radio buttons --> - <div data-selector=".o_website_form_flex_item" data-drop-near=".o_website_form_flex_item"/> - - <!-- Add move and remove controllers to select items --> - <div data-selector=".s_website_form .form-field.o_website_form_custom .o_website_form_select_item" data-drop-near=".o_website_form_select_item"/> - - <!-- Required option for fields that are not required fields of the model --> - <div data-js='website_form_editor_field' data-selector=".form-field:not(.o_website_form_required)"> - <we-checkbox string="Required" data-no-preview="true" - data-website_form_field_require="" - data-select-class="o_website_form_required_custom"/> + <!-- Field --> + <div data-js='WebsiteFieldEditor' data-selector=".s_website_form_field" data-drop-near=".s_website_form_field"> + <we-select data-name="type_opt" string="Type" data-no-preview="true"> + <we-title>Custom field</we-title> + <we-button data-custom-field="char">Text</we-button> + <we-button data-custom-field="text">Long Text</we-button> + <we-button data-custom-field="email">Email</we-button> + <we-button data-custom-field="integer">Number</we-button> + <we-button data-custom-field="float">Decimal Number</we-button> + <we-button data-custom-field="boolean">Checkbox</we-button> + <we-button data-custom-field="one2many">Multiple Checkboxes</we-button> + <we-button data-custom-field="selection">Radio Buttons</we-button> + <we-button data-custom-field="many2one">Selection</we-button> + <we-button data-custom-field="date">Date</we-button> + <we-button data-custom-field="datetime">Date & Time</we-button> + <we-button data-custom-field="binary">File Upload</we-button> + </we-select> + <we-select string="Display" data-name="multi_check_display_opt" data-no-preview="true"> + <we-button data-multi-checkbox-display="horizontal">Horizontal</we-button> + <we-button data-multi-checkbox-display="vertical">Vertical</we-button> + </we-select> + <we-input string="Label Name" data-set-label-text=""/> + <we-select string="Label Position" data-no-preview="true"> + <we-button data-select-label-position="none">None</we-button> + <we-button data-select-label-position="top">Top</we-button> + <we-button data-select-label-position="left">Left</we-button> + <we-button data-select-label-position="right">Right</we-button> + </we-select> + <we-input string="Input Placeholder" data-name="placeholder_opt" data-set-placeholder=""/> + <we-checkbox string="Required" data-name="required_opt" data-no-preview="true" + data-toggle-required="s_website_form_required"/> + <we-checkbox string="Hidden" data-name="hidden_opt" data-no-preview="true" + data-select-class="s_website_form_field_hidden"/> </div> - <!-- Remove the duplicate options of model fields --> - <div data-js="website_form_editor_field_model" data-selector=".s_website_form .form-field:not(.o_website_form_custom)"/> - - <!-- Remove the delete options of model required fields --> - <div data-js="website_form_editor_field_required" data-selector=".s_website_form .o_website_form_required"/> - - <!-- Remove the duplicate options of radio and checkboxes of model fields --> - <div data-js="website_form_editor_field_x2many" data-selector=".s_website_form .form-field:not(.o_website_form_custom) .o_website_form_flex_item"/> - - <!-- Transform the select inputs into editable constructs --> - <div data-js="website_form_editor_field_select" data-selector=".s_website_form .form-field:has(select)"/> + <!-- Remove the duplicate option of model fields --> + <div data-js="WebsiteFormFieldModel" data-selector=".s_website_form .s_website_form_field:not(.s_website_form_custom)"/> - <!-- Remove the duplicate option of model select items --> - <div data-js="website_form_editor_field_x2many" data-selector=".s_website_form .form-field:not(.o_website_form_custom) .o_website_form_select_item" data-drop-near=".o_website_form_select_item"/> + <!-- Remove the delete option of model required fields --> + <div data-js="WebsiteFormFieldRequired" data-selector=".s_website_form .s_website_form_model_required"/> - <!-- Remove the delete options of the Submit button --> - <div data-js="website_form_editor_field_required" data-selector=".s_website_form .o_website_form_send"/> + <!-- Remove the delete and duplicate option of the submit button --> + <div data-js="WebsiteFormSubmitRequired" data-selector=".s_website_form .s_website_form_submit"/> </xpath> </template> @@ -89,6 +112,11 @@ <link rel="stylesheet" type="text/scss" href="/website_form/static/src/snippets/s_website_form/000.scss"/> </xpath> </template> +<template id="assets_snippet_s_website_form_css_001" inherit_id="website.assets_frontend"> + <xpath expr="//link[last()]" position="after"> + <link rel="stylesheet" type="text/scss" href="/website_form/static/src/snippets/s_website_form/001.scss"/> + </xpath> +</template> <template id="assets_snippet_s_website_form_js_000" inherit_id="website.assets_frontend"> <xpath expr="//script[last()]" position="after"> diff --git a/addons/website_form/views/website_form_templates.xml b/addons/website_form/views/website_form_templates.xml index ea6e3a5a9aa..256a9ad81f7 100644 --- a/addons/website_form/views/website_form_templates.xml +++ b/addons/website_form/views/website_form_templates.xml @@ -3,70 +3,82 @@ <template id="contactus_form" name="Contact Form" inherit_id="website.contactus" customize_show="True"> <xpath expr="//div[@name='mail_button']" position="replace"> - <form id="contactus_form" t-ignore="true" action="/website_form/" method="post" data-model_name="mail.mail" class="s_website_form container-fluid mt32" enctype="multipart/form-data" data-success_page="/contactus-thank-you"> - <div class="form-group row form-field o_website_form_custom o_website_form_required_custom"> - <div class="col-lg-3 col-md-4"> - <label class="col-form-label" for="Name">Your Name</label> - </div> - <div class="col-lg-7 col-md-8"> - <input type="text" class="form-control o_website_form_input" name="Name" required=""/> - </div> - </div> - <div class="form-group row form-field o_website_form_custom"> - <div class="col-lg-3 col-md-4"> - <label class="col-form-label" for="Phone">Phone Number</label> - </div> - <div class="col-lg-7 col-md-8"> - <input type="text" class="form-control o_website_form_input" name="Phone"/> - </div> - </div> - <div class="form-group row form-field o_website_form_required_custom"> - <div class="col-lg-3 col-md-4"> - <label class="col-form-label" for="email_from">Email</label> - </div> - <div class="col-lg-7 col-md-8"> - <input type="email" class="form-control o_website_form_input" name="email_from" required=""/> - </div> - </div> - <div class="form-group row form-field o_website_form_custom"> - <div class="col-lg-3 col-md-4"> - <label class="col-form-label" for="Partner Name">Your Company</label> - </div> - <div class="col-lg-7 col-md-8"> - <input type="text" class="form-control o_website_form_input" name="Partner Name"/> - </div> - </div> - <div class="form-group row form-field o_website_form_required_custom"> - <div class="col-lg-3 col-md-4"> - <label class="col-form-label" for="subject">Subject</label> - </div> - <div class="col-lg-7 col-md-8"> - <input type="text" class="form-control o_website_form_input" name="subject" required=""/> - </div> - </div> - <div class="form-group row form-field o_website_form_custom o_website_form_required_custom"> - <div class="col-lg-3 col-md-4"> - <label class="col-form-label" for="Description">Your Question</label> - </div> - <div class="col-lg-7 col-md-8"> - <textarea class="form-control o_website_form_input" name="Description" required=""></textarea> - </div> - </div> - <div class="form-group row form-field d-none"> - <div class="col-lg-3 col-md-4"> - <label class="col-form-label" for="email_to">Email To</label> - </div> - <div class="col-lg-7 col-md-8"> - <input type="hidden" class="form-control o_website_form_input" name="email_to" t-att-value="res_company.email"/> - </div> - </div> - <div class="form-group row"> - <div class="offset-lg-3 offset-md-4 col-md-8 col-lg-7"> - <a href="#" class="btn btn-primary btn-lg o_website_form_send">Send</a> - <span id="o_website_form_result"></span> + <span class="hidden" data-for="contactus_form" t-att-data-values="{'email_to': res_company.email}" /> + <div> + <section id="contactus_section" class="s_website_form" data-vcss="001"> + <div class="container"> + <form id="contactus_form" action="/website_form/" method="post" enctype="multipart/form-data" class="o_mark_required" data-mark="*" data-model_name="mail.mail" data-success-mode="redirect" data-success-page="/contactus-thank-you"> + <div class="s_website_form_rows row s_col_no_bgcolor"> + <div class="form-group col-12 s_website_form_field s_website_form_custom s_website_form_required" data-type="char"> + <div class="row s_col_no_resize s_col_no_bgcolor"> + <label class="col-form-label col-sm-auto s_website_form_label" style="width: 200px" for="contact1">Your Name<span class="s_website_form_mark"> *</span></label> + <div class="col-sm"> + <input id="contact1" type="text" class="form-control s_website_form_input" name="Name" required=""/> + </div> + </div> + </div> + <div class="form-group col-12 s_website_form_field s_website_form_custom" data-type="char"> + <div class="row s_col_no_resize s_col_no_bgcolor"> + <label class="col-form-label col-sm-auto s_website_form_label" style="width: 200px" for="contact2">Phone Number</label> + <div class="col-sm"> + <input id="contact2" type="text" class="form-control s_website_form_input" name="Phone"/> + </div> + </div> + </div> + <div class="form-group col-12 s_website_form_field s_website_form_required" data-type="email"> + <div class="row s_col_no_resize s_col_no_bgcolor"> + <label class="col-form-label col-sm-auto s_website_form_label" style="width: 200px" for="contact3">Email<span class="s_website_form_mark"> *</span></label> + <div class="col-sm"> + <input id="contact3" type="email" class="form-control s_website_form_input" name="email_from" required=""/> + </div> + </div> + </div> + <div class="form-group col-12 s_website_form_field s_website_form_custom" data-type="char"> + <div class="row s_col_no_resize s_col_no_bgcolor"> + <label class="col-form-label col-sm-auto s_website_form_label" style="width: 200px" for="contact4">Your Company</label> + <div class="col-sm"> + <input id="contact4" type="text" class="form-control s_website_form_input" name="Partner Name"/> + </div> + </div> + </div> + <div class="form-group col-12 s_website_form_field s_website_form_required" data-type="char"> + <div class="row s_col_no_resize s_col_no_bgcolor"> + <label class="col-form-label col-sm-auto s_website_form_label" style="width: 200px" for="contact5">Subject<span class="s_website_form_mark"> *</span></label> + <div class="col-sm"> + <input id="contact5" type="text" class="form-control s_website_form_input" name="subject" required=""/> + </div> + </div> + </div> + <div class="form-group col-12 s_website_form_field s_website_form_custom s_website_form_required" data-type="text"> + <div class="row s_col_no_resize s_col_no_bgcolor"> + <label class="col-form-label col-sm-auto s_website_form_label" style="width: 200px" for="contact6">Your Question</label> + <div class="col-sm"> + <textarea id="contact6" class="form-control s_website_form_input" name="Description" required=""></textarea> + </div> + </div> + </div> + <div class="form-group col-12 s_website_form_field d-none"> + <div class="row s_col_no_resize s_col_no_bgcolor"> + <label class="col-form-label col-sm-auto s_website_form_label" style="width: 200px" for="contact7">Email To</label> + <div class="col-sm"> + <input id="contact7" type="hidden" class="form-control s_website_form_input" name="email_to"/> + </div> + </div> + </div> + <div class="form-group col-12 s_website_form_submit"> + <div class="row s_col_no_resize s_col_no_bgcolor"> + <div style="width: 200px" class="col-sm-auto s_website_form_label"/> + <div class="col-sm"> + <a href="#" role="button" class="btn btn-primary btn-lg s_website_form_send">Submit</a> + <span id="s_website_form_result"></span> + </div> + </div> + </div> + </div> + </form> </div> - </div> - </form> + </section> + </div> </xpath> </template> diff --git a/addons/website_form_project/static/src/js/website_form_project_editor.js b/addons/website_form_project/static/src/js/website_form_project_editor.js index 33f2a728ee4..0956c12475e 100644 --- a/addons/website_form_project/static/src/js/website_form_project_editor.js +++ b/addons/website_form_project/static/src/js/website_form_project_editor.js @@ -7,8 +7,21 @@ var FormEditorRegistry = require('website_form.form_editor_registry'); var _t = core._t; FormEditorRegistry.add('create_task', { - defaultTemplateName: 'website_form_project.default_task_form', - defaultTemplatePath: '/website_form_project/static/src/xml/website_form_project.xml', + formFields: [{ + type: 'char', + modelRequired: true, + name: 'name', + string: 'Task Title', + }, { + type: 'email', + modelRequired: true, + name: 'email_from', + string: 'Your Email', + }, { + type: 'char', + name: 'description', + string: 'Description', + }], fields: [{ name: 'project_id', type: 'many2one', diff --git a/addons/website_form_project/static/src/xml/website_form_project.xml b/addons/website_form_project/static/src/xml/website_form_project.xml deleted file mode 100644 index e60631feac9..00000000000 --- a/addons/website_form_project/static/src/xml/website_form_project.xml +++ /dev/null @@ -1,31 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<templates> - - <t t-name="website_form_project.default_task_form"> - <div class="form-group row form-field o_website_form_required"> - <div class="col-lg-3 col-md-4"> - <label class="col-form-label" for="name">Task Title</label> - </div> - <div class="col-lg-7 col-md-8"> - <input type="text" class="form-control o_website_form_input" name="name" required="" /> - </div> - </div> - <div class="form-group row form-field o_website_form_required"> - <div class="col-lg-3 col-md-4"> - <label class="col-form-label" for="email_from">Email</label> - </div> - <div class="col-lg-7 col-md-8"> - <input type="text" class="form-control o_website_form_input" name="email_from" required="" /> - </div> - </div> - <div class="form-group row form-field"> - <div class="col-lg-3 col-md-4"> - <label class="col-form-label" for="description">Description</label> - </div> - <div class="col-lg-7 col-md-8"> - <input type="text" class="form-control o_website_form_input" name="description" /> - </div> - </div> - </t> - -</templates> diff --git a/addons/website_hr_recruitment/static/src/js/website_hr_recruitment_editor.js b/addons/website_hr_recruitment/static/src/js/website_hr_recruitment_editor.js index bc3cef27f49..9cfd7810daf 100644 --- a/addons/website_hr_recruitment/static/src/js/website_hr_recruitment_editor.js +++ b/addons/website_hr_recruitment/static/src/js/website_hr_recruitment_editor.js @@ -7,8 +7,30 @@ var FormEditorRegistry = require('website_form.form_editor_registry'); var _t = core._t; FormEditorRegistry.add('apply_job', { - defaultTemplateName: 'website_hr_recruitment.default_job_form', - defaultTemplatePath: '/website_hr_recruitment/static/src/xml/website_hr_recruitment.xml', + formFields: [{ + type: 'char', + modelRequired: true, + name: 'partner_name', + string: 'Your Name', + }, { + type: 'email', + required: true, + name: 'email_from', + string: 'Your Email', + }, { + type: 'char', + required: true, + name: 'partner_phone', + string: 'Phone Number', + }, { + type: 'text', + name: 'description', + string: 'Short Introduction', + }, { + type: 'binary', + custom: true, + name: 'Resume', + }], fields: [{ name: 'job_id', type: 'many2one', diff --git a/addons/website_hr_recruitment/static/src/xml/website_hr_recruitment.xml b/addons/website_hr_recruitment/static/src/xml/website_hr_recruitment.xml deleted file mode 100644 index 5a63e604014..00000000000 --- a/addons/website_hr_recruitment/static/src/xml/website_hr_recruitment.xml +++ /dev/null @@ -1,47 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<templates> - - <t t-name="website_hr_recruitment.default_job_form"> - <div class="form-group row form-field o_website_form_required_custom"> - <div class="col-lg-3 col-md-4"> - <label class="col-form-label" for="partner_name">Your Name</label> - </div> - <div class="col-lg-7 col-md-8"> - <input type="text" class="form-control o_website_form_input" name="partner_name" required=""/> - </div> - </div> - <div class="form-group row form-field o_website_form_required_custom"> - <div class="col-lg-3 col-md-4"> - <label class="col-form-label" for="email_from">Your Email</label> - </div> - <div class="col-lg-7 col-md-8"> - <input type="text" class="form-control o_website_form_input" name="email_from" required="" /> - </div> - </div> - <div class="form-group row form-field o_website_form_required_custom"> - <div class="col-lg-3 col-md-4"> - <label class="col-form-label" for="partner_phone">Phone Number</label> - </div> - <div class="col-lg-7 col-md-8"> - <input type="text" class="form-control o_website_form_input" name="partner_phone" required="" /> - </div> - </div> - <div class="form-group row form-field"> - <div class="col-lg-3 col-md-4"> - <label class="col-form-label" for="description">Short Introduction</label> - </div> - <div class="col-lg-7 col-md-8"> - <textarea class="form-control o_website_form_input" name="description"></textarea> - </div> - </div> - <div class="form-group row form-field o_website_form_custom"> - <div class="col-lg-3 col-md-4"> - <label class="col-form-label" for="Resume">Resume</label> - </div> - <div class="col-lg-7 col-md-8"> - <input type="file" class="form-control o_website_form_input" name="Resume" /> - </div> - </div> - </t> - -</templates> diff --git a/addons/website_hr_recruitment/static/tests/tours/website_hr_recruitment.js b/addons/website_hr_recruitment/static/tests/tours/website_hr_recruitment.js index f75157b9d6e..c80dd1bd427 100644 --- a/addons/website_hr_recruitment/static/tests/tours/website_hr_recruitment.js +++ b/addons/website_hr_recruitment/static/tests/tours/website_hr_recruitment.js @@ -30,7 +30,7 @@ odoo.define('website_hr_recruitment.tour', function(require) { run: "text ### HR RECRUITMENT TEST DATA ###" }, { // TODO: Upload a file ? content: "Send the form", - trigger: ".o_website_form_send" + trigger: ".s_website_form_send" }, { content: "Check the form is submited without errors", trigger: ".oe_structure:has(h1:contains('Congratulations'))" diff --git a/addons/website_hr_recruitment/views/website_hr_recruitment_templates.xml b/addons/website_hr_recruitment/views/website_hr_recruitment_templates.xml index a19bc7e7e24..beba5e518e5 100644 --- a/addons/website_hr_recruitment/views/website_hr_recruitment_templates.xml +++ b/addons/website_hr_recruitment/views/website_hr_recruitment_templates.xml @@ -134,76 +134,81 @@ <h2 t-if="job" class="text-center text-muted"> <span t-field="job.name"/> </h2> - - <div class="row mt-3"> - <section id="forms" class="col"> - <span class="hidden" data-for="hr_recruitment_form" t-att-data-values="{'department_id': job and job.department_id.id or '', 'job_id': job and job.id or ''}" /> - <form id="hr_recruitment_form" action="/website_form/" method="post" class="s_website_form" enctype="multipart/form-data" data-model_name="hr.applicant" data-success_page="/job-thank-you" hide-change-model="true"> - <div class="form-group row form-field o_website_form_required_custom"> - <div class="col-lg-3 col-md-4 text-right"> - <label class="col-form-label" for="partner_name">Your Name</label> - </div> - <div class="col-lg-7 col-md-8"> - <input type="text" class="form-control o_website_form_input" name="partner_name" required=""/> - </div> - </div> - <div class="form-group row form-field o_website_form_required_custom"> - <div class="col-lg-3 col-md-4 text-right"> - <label class="col-form-label" for="email_from">Your Email</label> - </div> - <div class="col-lg-7 col-md-8"> - <input type="email" class="form-control o_website_form_input" name="email_from" required=""/> - </div> - </div> - <div class="form-group row form-field o_website_form_required_custom"> - <div class="col-lg-3 col-md-4 text-right"> - <label class="col-form-label" for="partner_phone">Your Phone Number</label> - </div> - <div class="col-lg-7 col-md-8"> - <input type="text" class="form-control o_website_form_input" name="partner_phone" required=""/> + <span class="hidden" data-for="hr_recruitment_form" t-att-data-values="{'department_id': job and job.department_id.id or '', 'job_id': job and job.id or ''}" /> + <section id="forms" class="s_website_form" data-vcss="001"> + <div class="container"> + <form id="hr_recruitment_form" action="/website_form/" method="post" enctype="multipart/form-data" class="o_mark_required" data-mark="*" data-model_name="hr.applicant" data-success-mode="redirect" data-success-page="/job-thank-you" hide-change-model="true"> + <div class="s_website_form_rows row s_col_no_bgcolor"> + <div class="form-group col-12 s_website_form_field s_website_form_required s_website_form_model_required" data-type="char"> + <div class="row s_col_no_resize s_col_no_bgcolor"> + <label class="col-form-label col-sm-auto s_website_form_label" style="width: 200px" for="recruitment1">Your Name<span class="s_website_form_mark"> *</span></label> + <div class="col-sm"> + <input id="recruitment1" type="text" class="form-control s_website_form_input" name="partner_name" required=""/> + </div> + </div> </div> - </div> - <div class="form-group row form-field"> - <div class="col-lg-3 col-md-4 text-right"> - <label class="col-form-label" for="description">Short Introduction</label> + <div class="form-group col-12 s_website_form_field s_website_form_required" data-type="email"> + <div class="row s_col_no_resize s_col_no_bgcolor"> + <label class="col-form-label col-sm-auto s_website_form_label" style="width: 200px" for="recruitment2">Your Email<span class="s_website_form_mark"> *</span></label> + <div class="col-sm"> + <input id="recruitment2" type="email" class="form-control s_website_form_input" name="email_from" required=""/> + </div> + </div> </div> - <div class="col-lg-7 col-md-8"> - <textarea class="form-control o_website_form_input" name="description"></textarea> + <div class="form-group col-12 s_website_form_field s_website_form_required" data-type="char"> + <div class="row s_col_no_resize s_col_no_bgcolor"> + <label class="col-form-label col-sm-auto s_website_form_label" style="width: 200px" for="recruitment3">Your Phone Number<span class="s_website_form_mark"> *</span></label> + <div class="col-sm"> + <input id="recruitment3" type="text" class="form-control s_website_form_input" name="partner_phone" required=""/> + </div> + </div> </div> - </div> - <div class="form-group row form-field o_website_form_custom"> - <div class="col-lg-3 col-md-4 text-right"> - <label class="col-form-label" for="Resume">Resume</label> - </div> - <div class="col-lg-7 col-md-8"> - <input type="file" class="form-control o_website_form_input" name="Resume"/> - </div> - </div> - <div class="form-group row form-field d-none"> - <div class="col-lg-3 col-md-4"> - <label class="col-form-label" for="job_id">Job</label> + <div class="form-group col-12 s_website_form_field" data-type="text"> + <div class="row s_col_no_resize s_col_no_bgcolor"> + <label class="col-form-label col-sm-auto s_website_form_label" style="width: 200px" for="recruitment4">Short Introduction</label> + <div class="col-sm"> + <textarea id="recruitment4" class="form-control s_website_form_input" name="description"></textarea> + </div> + </div> </div> - <div class="col-lg-7 col-md-8"> - <input type="hidden" class="form-control o_website_form_input" name="job_id"/> + <div class="form-group col-12 s_website_form_field s_website_form_custom" data-type="binary"> + <div class="row s_col_no_resize s_col_no_bgcolor"> + <label class="col-form-label col-sm-auto s_website_form_label" style="width: 200px" for="recruitment5">Resume</label> + <div class="col-sm"> + <input id="recruitment5" type="file" class="form-control s_website_form_input" name="Resume"/> + </div> + </div> </div> - </div> - <div class="form-group row form-field d-none"> - <div class="col-lg-3 col-md-4"> - <label class="col-form-label" for="department_id">Department</label> + <div class="form-group col-12 s_website_form_field d-none"> + <div class="row s_col_no_resize s_col_no_bgcolor"> + <label class="col-form-label col-sm-auto s_website_form_label" style="width: 200px" for="recruitment6">Job</label> + <div class="col-sm"> + <input id="recruitment6" type="hidden" class="form-control s_website_form_input" name="job_id"/> + </div> + </div> </div> - <div class="col-lg-7 col-md-8"> - <input type="hidden" class="form-control o_website_form_input" name="department_id"/> + <div class="form-group col-12 s_website_form_field d-none"> + <div class="row s_col_no_resize s_col_no_bgcolor"> + <label class="col-form-label col-sm-auto s_website_form_label" style="width: 200px" for="recruitment7">Department</label> + <div class="col-sm"> + <input id="recruitment7" type="hidden" class="form-control s_website_form_input" name="department_id"/> + </div> + </div> </div> - </div> - <div class="form-group row"> - <div class="offset-lg-3 offset-md-4 col-md-8 col-lg-7"> - <a href="#" role="button" class="btn btn-primary btn-lg o_website_form_send">Submit</a> - <span id="o_website_form_result"></span> + <div class="form-group col-12 s_website_form_submit"> + <div class="row s_col_no_resize s_col_no_bgcolor"> + <div style="width: 200px" class="col-sm-auto s_website_form_label"/> + <div class="col-sm"> + <a href="#" role="button" class="btn btn-primary btn-lg s_website_form_send">Submit</a> + <span id="s_website_form_result"></span> + </div> + </div> </div> </div> </form> - </section> - </div> + </div> + </section> + <div class="oe_structure mt-2"/> </div> </t> </template> diff --git a/addons/website_sale/data/data.xml b/addons/website_sale/data/data.xml index 64502e84ea0..430fdc87b98 100644 --- a/addons/website_sale/data/data.xml +++ b/addons/website_sale/data/data.xml @@ -41,10 +41,6 @@ </record> </data> <data> - <record id="sale.model_sale_order" model="ir.model"> - <field name="website_form_access" eval="True"/> - <field name="website_form_label">Sales Order</field> - </record> <function model="ir.model.fields" name="formbuilder_whitelist"> <value>sale.order</value> <value eval="[ diff --git a/addons/website_sale/static/src/js/website_sale_form_editor.js b/addons/website_sale/static/src/js/website_sale_form_editor.js index f9345d7f9af..8ae105f1108 100644 --- a/addons/website_sale/static/src/js/website_sale_form_editor.js +++ b/addons/website_sale/static/src/js/website_sale_form_editor.js @@ -4,8 +4,25 @@ odoo.define('website_sale.form', function (require) { var FormEditorRegistry = require('website_form.form_editor_registry'); FormEditorRegistry.add('create_customer', { - defaultTemplateName: 'website_sale.default_customer_form', - defaultTemplatePath: '/website_sale/static/src/xml/website_sale_form.xml', + formFields: [{ + type: 'char', + modelRequired: true, + name: 'name', + string: 'Your Name', + }, { + type: 'email', + required: true, + name: 'email', + string: 'Your Email', + }, { + type: 'char', + name: 'phone', + string: 'Phone Number', + }, { + type: 'char', + name: 'company_name', + string: 'Company Name', + }], }); }); diff --git a/addons/website_sale/static/src/xml/website_sale_form.xml b/addons/website_sale/static/src/xml/website_sale_form.xml deleted file mode 100644 index f2488067d00..00000000000 --- a/addons/website_sale/static/src/xml/website_sale_form.xml +++ /dev/null @@ -1,39 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<templates> - - <t t-name="website_sale.default_customer_form"> - <div class="form-group row form-field o_website_form_required"> - <div class="col-lg-3 col-md-4"> - <label class="col-form-label" for="name">Name</label> - </div> - <div class="col-lg-7 col-md-8"> - <input type="text" class="form-control o_website_form_input" name="name" required=""/> - </div> - </div> - <div class="form-group row form-field o_website_form_required_custom"> - <div class="col-lg-3 col-md-4"> - <label class="col-form-label" for="email">Email</label> - </div> - <div class="col-lg-7 col-md-8"> - <input type="text" class="form-control o_website_form_input" name="email" required="" /> - </div> - </div> - <div class="form-group row form-field"> - <div class="col-lg-3 col-md-4"> - <label class="col-form-label" for="phone">Phone</label> - </div> - <div class="col-lg-7 col-md-8"> - <input type="text" class="form-control o_website_form_input" name="phone" /> - </div> - </div> - <div class="form-group row form-field"> - <div class="col-lg-3 col-md-4"> - <label class="col-form-label" for="company_name">Company Name</label> - </div> - <div class="col-lg-7 col-md-8"> - <input type="text" class="form-control o_website_form_input" name="company_name" /> - </div> - </div> - </t> - -</templates> diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml index 41007201327..e476bfa2077 100644 --- a/addons/website_sale/views/templates.xml +++ b/addons/website_sale/views/templates.xml @@ -746,44 +746,47 @@ <t t-call="website_sale.cart_summary"/> </div> <div class="col-12 col-xl order-xl-1 oe_cart"> - <div class="row"> - <div class="col"> - <form action="/website_form/" method="post" class="s_website_form container-fluid" enctype="multipart/form-data" data-force_action="shop.sale.order" data-model_name="sale.order" data-success_page="/shop/payment"> - <div class="form-group row form-field o_website_form_custom"> - <div class="col-lg-3 col-md-4 text-md-right"> - <label class="col-form-label" for="client_order_ref">Your Reference</label> - </div> - <div class="col-lg-9 col-md-8"> - <input type="text" class="form-control o_website_form_input" name="client_order_ref"/> - </div> - </div> - <div class="form-group row form-field o_website_form_custom"> - <div class="col-lg-3 col-md-4 text-md-right"> - <label class="col-form-label" for="Give us your feedback">Give us your feedback</label> - </div> - <div class="col-lg-9 col-md-8"> - <textarea class="form-control o_website_form_input" rows="8" name="Give us your feedback" /> - </div> - </div> - <div class="form-group row form-field o_website_form_custom"> - <div class="col-lg-3 col-md-4 text-md-right"> - <label class="col-form-label" for="a_document">A document to provide</label> + <section class="s_website_form" data-vcss="001"> + <div class="container"> + <form action="/website_form/" method="post" enctype="multipart/form-data" class="o_mark_required" data-mark="*" data-force_action="shop.sale.order" data-model_name="sale.order" data-success-mode="redirect" data-success-page="/shop/payment" hide-change-model="true"> + <div class="s_website_form_rows row s_col_no_bgcolor"> + <div class="form-group col-12 s_website_form_field" data-type="char"> + <div class="row s_col_no_resize s_col_no_bgcolor"> + <label class="col-form-label col-sm-auto s_website_form_label" style="width: 200px" for="sale1">Your Reference</label> + <div class="col-sm"> + <input id="sale1" type="text" class="form-control s_website_form_input" name="client_order_ref"/> + </div> + </div> </div> - <div class="col-lg-9 col-md-8"> - <input type="file" class="form-control o_website_form_input" name="a_document" /> + <div class="form-group col-12 s_website_form_field s_website_form_custom" data-type="text"> + <div class="row s_col_no_resize s_col_no_bgcolor"> + <label class="col-form-label col-sm-auto s_website_form_label" style="width: 200px" for="sale2">Give us your feedback</label> + <div class="col-sm"> + <textarea id="sale2" class="form-control s_website_form_input" name="Give us your feedback" /> + </div> + </div> </div> - </div> - <div class="form-group row"> - <div class="col-lg-3 col-md-4"> + <div class="form-group col-12 s_website_form_field s_website_form_custom" data-type="binary"> + <div class="row s_col_no_resize s_col_no_bgcolor"> + <label class="col-form-label col-sm-auto s_website_form_label" style="width: 200px" for="sale3">A document to provide</label> + <div class="col-sm"> + <input id="sale3" type="file" class="form-control-file s_website_form_input" name="a_document" /> + </div> + </div> </div> - <div class="col-lg-9 col-md-8"> - <a role="button" href="/shop/checkout" class="btn btn-secondary mb32 float-left"><span class="fa fa-chevron-left" /> Previous</a> - <a role="button" class="btn btn-primary float-right mb32 o_website_form_send" href="/shop/confirm_order">Next <span class="fa fa-chevron-right" /></a> + <div class="form-group col-12 s_website_form_submit"> + <div class="row s_col_no_resize s_col_no_bgcolor"> + <div style="width: 200px" class="col-sm-auto s_website_form_label"/> + <div class="col-sm"> + <a role="button" href="/shop/checkout" class="btn btn-secondary float-left"><span class="fa fa-chevron-left"/> Previous</a> + <a role="button" class="btn btn-primary float-right s_website_form_send" href="/shop/confirm_order">Next <span class="fa fa-chevron-right" /></a> + </div> + </div> </div> </div> </form> </div> - </div> + </section> </div> </div> </div> |
