From ad67d744249b8f78f7a8ef1ab11354d53a19836d Mon Sep 17 00:00:00 2001
From: "Robin Lejeune (role)" <role@odoo.com>
Date: Tue, 23 May 2023 08:49:10 +0000
Subject: [PATCH] [FIX] website: trigger visibility with prefilled form input

Form fields can be prefilled since [1] and their visibility
conditionally linked to other fields since [2].
1. This commit ensures prepopulated fields trigger the conditional
visibility mechanism (A), including when said field is first hidden,
then conditionally displayed (B).
2. Until [3], with a chain of visibility conditions, when one of them
was "contains", "does not contain" or a date-related condition, a
traceback was raised (C). This commit corrects the fix from [3] to
prevent this behavior now that the reason has been identified.
3. Tests are also set up to enforce these fixes.

Steps to reproduce (A):
- Drop a form
- Make field A depend on field B being set
- Have field B be autopopulated from user mail
- Save and check the form: A won't be shown despite B being set
- Removing one letter or typing anything in the input B will trigger
the visibility of A.
- Expected result: B should immediately appear.

Steps to reproduce (B):
- Drop a form
- Make field A depend on field B being set
- Have field B be autopopulated and depend on field C being set
- Field C is empty by default
- Save: only field C is shown. Type a letter: field B appears, but not
field A.
- Expected result: both should appear.
Note: this bug only occurs when field A is before field B.

Steps to reproduce (C) (until [3]):
- Drop a form
- Field A depends on B containing "abc"
- Field B depends on C being set
- Save and type something in C: there is a traceback.

task-3335536

[1]: https://github.com/odoo/odoo/commit/54873d2
[2]: https://github.com/odoo/odoo/commit/2dcbfec
[3]: https://github.com/odoo/odoo/commit/2714469

closes odoo/odoo#122044

Signed-off-by: Benoit Socias (bso) <bso@odoo.com>
---
 .../static/src/snippets/s_website_form/000.js |  42 +++----
 .../static/tests/tours/website_form_editor.js | 112 +++++++++++++++++-
 2 files changed, 128 insertions(+), 26 deletions(-)

diff --git a/addons/website/static/src/snippets/s_website_form/000.js b/addons/website/static/src/snippets/s_website_form/000.js
index d1f92d26e719..aa3c40da0e2f 100644
--- a/addons/website/static/src/snippets/s_website_form/000.js
+++ b/addons/website/static/src/snippets/s_website_form/000.js
@@ -91,7 +91,6 @@ odoo.define('website.s_website_form', function (require) {
             for (const [name, funcs] of visibilityFunctionsByFieldName.entries()) {
                 this._visibilityFunctionByFieldName.set(name, () => funcs.some(func => func()));
             }
-            this._updateFieldsVisibility();
 
             this._onFieldInputDebounced = _.debounce(this._onFieldInput.bind(this), 400);
             this.$el.on('input.s_website_form', '.s_website_form_field', this._onFieldInputDebounced);
@@ -177,6 +176,7 @@ odoo.define('website.s_website_form', function (require) {
                     }
                 }
             }
+            this._updateFieldsVisibility();
 
             // Check disabled states
             this.inputEls = this.$target[0].querySelectorAll('.s_website_form_field.s_website_form_field_hidden_if .s_website_form_input');
@@ -582,32 +582,17 @@ odoo.define('website.s_website_form', function (require) {
          * @returns {boolean}
          */
         _compareTo(comparator, value = '', comparable, between) {
-            const valueWasNull = (value === null);
-            if (valueWasNull) {
-                // TODO One customer apparently reached that case of receiving
-                // a `null` value (actually possible when retrieving a form
-                // data which is an unchecked checkbox for example) but combined
-                // with an operator which really requires the received value to
-                // be a string... but not sure how this is possible. In any
-                // case, it should not make the form crash: it's just a
-                // problematic *visibility* dependency, it should not prevent
-                // using the form. So if the caller sends a `null` value, treat
-                // it as an empty string. For now, let's also add an error in
-                // the console so we may investigate the issue if it ever shows
-                // up in a test (grep: COMPARE_TO_INVALID_OPERATOR)
+            // Value can be null when the compared field is supposed to be
+            // visible, but is not yet retrievable from the FormData() because
+            // the field was conditionally hidden. It can be considered empty.
+            if (value === null) {
                 value = '';
             }
 
             switch (comparator) {
                 case 'contains':
-                    if (valueWasNull) { // grep: COMPARE_TO_INVALID_OPERATOR
-                        console.error("This field value is null but also uses an invalid operator");
-                    }
                     return value.includes(comparable);
                 case '!contains':
-                    if (valueWasNull) { // grep: COMPARE_TO_INVALID_OPERATOR
-                        console.error("This field value is null but also uses an invalid operator");
-                    }
                     return !value.includes(comparable);
                 case 'equal':
                 case 'selected':
@@ -633,9 +618,6 @@ odoo.define('website.s_website_form', function (require) {
                     return value.name === '';
             }
             // Date & Date Time comparison requires formatting the value
-            if (valueWasNull) { // grep: COMPARE_TO_INVALID_OPERATOR
-                console.error("This field value is null but also uses an invalid operator");
-            }
             if (value.includes(':')) {
                 const datetimeFormat = time.getLangDatetimeFormat();
                 value = moment(value, datetimeFormat)._d.getTime() / 1000;
@@ -693,8 +675,20 @@ odoo.define('website.s_website_form', function (require) {
          * Calculates the visibility for each field with conditional visibility
          */
         _updateFieldsVisibility() {
+            let anyFieldVisibilityUpdated = false;
             for (const [fieldEl, visibilityFunction] of this._visibilityFunctionByFieldEl.entries()) {
-                this._updateFieldVisibility(fieldEl, visibilityFunction());
+                const wasVisible = !fieldEl.closest(".s_website_form_field")
+                    .classList.contains("d-none");
+                const isVisible = !!visibilityFunction();
+                this._updateFieldVisibility(fieldEl, isVisible);
+                anyFieldVisibilityUpdated |= wasVisible !== isVisible;
+            }
+            // Recursive check needed in case of a field (C) that
+            // conditionally displays a prefilled field (B), which in turn
+            // triggers a conditional visibility on another field (A),
+            // registered before B.
+            if (anyFieldVisibilityUpdated) {
+                this._updateFieldsVisibility();
             }
         },
         /**
diff --git a/addons/website/static/tests/tours/website_form_editor.js b/addons/website/static/tests/tours/website_form_editor.js
index 077ed2fa7a36..544934dae88e 100644
--- a/addons/website/static/tests/tours/website_form_editor.js
+++ b/addons/website/static/tests/tours/website_form_editor.js
@@ -37,6 +37,15 @@ odoo.define('website.tour.form_editor', function (require) {
                        .replaceAll(/`/g, character => `&lsquo;`);
     };
 
+    const getFieldByLabel = (label) => {
+        return `.s_website_form_field:has(label:contains("${label}"))`;
+    };
+    const selectFieldByLabel = (label) => {
+        return [{
+            content: `Select field "${label}"`,
+            trigger: getFieldByLabel(label),
+        }];
+    };
     const selectButtonByText = function (text) {
         return [{
             content: "Open the select",
@@ -359,6 +368,21 @@ odoo.define('website.tour.form_editor', function (require) {
             trigger: 'we-input[data-attribute-name="value"] input',
             run: 'text John Smith',
         },
+
+        // Add two fields: the 1st one's visibility is tied to the 2nd one
+        // being set, and the 2nd one is autopopulated. As a result, both
+        // should be visible by default.
+        ...addCustomField("char", "text", "field A", false, {visibility: CONDITIONALVISIBILITY}),
+        ...addCustomField("char", "text", "field B", false),
+        ...selectFieldByLabel("field A"),
+        ...selectButtonByData('data-set-visibility-dependency="field B"'),
+        ...selectButtonByData('data-select-data-attribute="set"'),
+        ...selectFieldByLabel("field B"),
+        {
+            content: "Insert default value",
+            trigger: 'we-input[data-attribute-name="value"] input',
+            run: "text prefilled",
+        },
         {
             content:  "Save the page",
             trigger:  "button[data-action=save]",
@@ -371,7 +395,21 @@ odoo.define('website.tour.form_editor', function (require) {
             content: 'Verify that phone field is still auto-fillable',
             trigger: '.s_website_form_field input[data-fill-with="phone"]:propValue("+1 555-555-5555")',
         },
-        // Check that if we edit again and save again the default value is not deleted.
+        // Check that the resulting form behavior is correct.
+        {
+            content: "Check that field B prefill text is set",
+            trigger: `${getFieldByLabel("field B")}:has(input[value="prefilled"])`,
+            run: () => null, // it's a check
+        }, {
+            content: "Check that field A is visible",
+            trigger: `.s_website_form:has(${getFieldByLabel("field A")}:visible)`,
+            run: () => null, // it's a check
+        },
+        // A) Check that if we edit again and save again the default value is
+        // not deleted.
+        // B) Add a 3rd field. Field A's visibility is tied to field B being set,
+        // field B is autopopulated and its visibility is tied to field C being
+        // set, and field C is empty.
         {
             content: 'Enter in edit mode again',
             trigger: 'a[data-action="edit"]',
@@ -383,15 +421,85 @@ odoo.define('website.tour.form_editor', function (require) {
             extra_trigger: 'button[data-action="save"]',
             run: 'click',
         },
-        ...addCustomField('many2one', 'select', 'Select Field', true),
+        ...addCustomField("char", "text", "field C", false),
+        ...selectFieldByLabel("field B"),
+        ...selectButtonByText(CONDITIONALVISIBILITY),
+        ...selectButtonByData('data-set-visibility-dependency="field C"'),
+        ...selectButtonByData('data-select-data-attribute="set"'),
         {
             content: 'Save the page',
             trigger: 'button[data-action=save]',
             run: 'click',
         },
+
+        // Check that the resulting form behavior is correct.
         {
             content: 'Verify that the value has not been deleted',
             trigger: '.s_website_form_field:eq(0) input[value="John Smith"]',
+        }, {
+            content: "Check that fields A and B are not visible and that field B's prefill text is still set",
+            trigger: `.s_website_form:has(${getFieldByLabel("field A")}:not(:visible))` +
+                `:has(${getFieldByLabel("field B")}:has(input[value="prefilled"]):not(:visible))`,
+            run: () => null, // it's a check
+        }, {
+            content: "Type something in field C",
+            trigger: `${getFieldByLabel("field C")} input`,
+            run: "text Sesame",
+        }, {
+            content: "Check that fields A and B are visible",
+            trigger: `.s_website_form:has(${getFieldByLabel("field B")}:visible)` +
+                `:has(${getFieldByLabel("field A")}:visible)`,
+            run: () => null, // it's a check
+        },
+
+        // Have field A's visibility tied to field B containing something,
+        // while field B's visibility is also tied to another field.
+        {
+            content: "Enter edit mode",
+            trigger: "a[data-action=edit]",
+        }, {
+            content: "Check that we are in edit mode",
+            trigger: "#oe_snippets.o_loaded",
+            run: () => null,
+        },
+        ...selectFieldByLabel("field A"),
+        {
+            content: "Verify that the form editor appeared",
+            trigger: ".o_we_customize_panel .snippet-option-WebsiteFormEditor",
+            run: () => null,
+        },
+        ...selectButtonByData('data-select-data-attribute="contains"'),
+        {
+            content: "Tie the visibility of field A to field B containing 'peek-a-boo'",
+            trigger: "we-input[data-name=hidden_condition_additional_text] input",
+            run: "text peek-a-boo",
+        }, {
+            content: "Save the page",
+            trigger: "button[data-action=save]",
+        },
+
+        // Check that the resulting form works and does not raise an error.
+        {
+            content: "Wait for page reload",
+            trigger: 'body:not(.editor_enable) [data-snippet="s_website_form"]',
+            run: () => null,
+        }, {
+            content: "Write anything in C",
+            trigger: `${getFieldByLabel("field C")} input`,
+            run: "text Mellon",
+        }, {
+            content: "Check that field B is visible, but field A is not",
+            trigger: `.s_website_form:has(${getFieldByLabel("field B")}:visible)` +
+                `:has(${getFieldByLabel("field A")}:not(:visible))`,
+            run: () => null, // it's a check
+        }, {
+            content: "Insert 'peek-a-boo' in field B",
+            trigger: `${getFieldByLabel("field B")} input`,
+            run: "text peek-a-boo",
+        }, {
+            content: "Check that field A is visible",
+            trigger: `.s_website_form:has(${getFieldByLabel("field A")}:visible)`,
+            run: () => null, // it's a check
         },
         {
             content: 'Enter in edit mode again',
-- 
GitLab