verbb / formie

The most user-friendly forms plugin for Craft CMS.
Other
93 stars 69 forks source link

Unable to populate repeater entries field on secondary site #1667

Closed jessedobbelaere closed 6 months ago

jessedobbelaere commented 6 months ago

Describe the bug

I'm having trouble getting an entries field to prepopulate, inside a repeater field, on a secondary (multi-site) website 😅 I first thought that it was related to #1190, but it seems to be more complex than that.

I have a basic multi-site setup, with a primary and secondary website. I have a section products that is only enabled on the secondary site. I have a form with one repeater field with one nested entries field inside. And a standalone entries field below the repeater field (for debugging). I observed that:

Screenshot 2023-12-22 at 01 58 42@2x

Steps to reproduce

Using these steps, I can reproduce the bug in a new project:

  1. Install a fresh craft cms

    cd craftcmsprimary
    ddev config --project-type=craftcms --docroot=web --create-docroot
    ddev composer create -y --no-scripts craftcms/craft
    ddev craft install
  2. Add another hostname for the secondary site: modify .ddev/config.yaml and specify:

    additional_hostnames:
    - "craftcmssecondary"

    And restart ddev: ddev restart

  3. Visit the admin panel: https://craftcmsprimary.ddev.site/admin and go to settings -> sites and create a secondary site. Fill in base url: https://craftcmssecondary.ddev.site.

  4. Create a section Pages (enabled for both sites) and create some dummy entries

  5. Create a section Products (enabled only for secondary site) and create two entries

Screenshot 2023-12-22 at 02 04 01@2x

  1. Install Formie: ddev composer require verbb/formie -w && ddev exec php craft plugin/install formie
  2. Create a new form. Add a repeater with one entries field entriesField (keep all defaults, all sections are checked) and a number field quantityField. Add a standalone entries field below the repeater (keep all defaults): entriesFieldTwo.
  3. Create a test page in templates/test.twig
<html>
<body>
    <h1>Test</h1>

    <div class="container">
        {% set form = craft.formie.forms({ handle: 'testForm' }).one() %}
        {% do craft.formie.populateFormValues(form, {
            repeaterField: [
                {
                    entriesField: [6],
                    quantityField: 5,
                },
            ],
            entriesFieldTwo: [6],
        }) %}

        {{ craft.formie.renderForm(form) }}
    </div>
</body>
</html>

We populate using entries ID = 6 which the first product entry, in both entries fields.

  1. Visit the secondary site: https://craftcmssecondary.ddev.site/test

We can see that only the standalone entries field gets set with a default, but not the nested entries field.

Screenshot 2023-12-22 at 01 58 42@2x

I assume that the repeater field population has some difficulty with entries that are exclusive to a secondary site? The entry with ID=6 is part of the dropdown values, but setting it as a default does not work?

Screenshot 2023-12-22 at 02 01 22@2x

Form settings

Craft CMS version

4.5.13

Plugin version

2.0.44.1

Multi-site?

Yes

Additional context

Section with entries is only available to secondary site

jessedobbelaere commented 6 months ago

Did some debugging 🕵️

The issue

To repeat the problem: when I load the Formie form on my secondary site, it does not set a default value (which I populated using craft.formie.populateFormValues) in my nested entries field in a Repeater. 🤔

In the query that fetches the default value (prepopulated), it filters incorrectly by siteId=1, because we're browsing the secondary website (siteId should be 2).

SELECT `elements`.`id`, `elements`.`canonicalId`, `elements`.`fieldLayoutId`, `elements`.`uid`, `elements`.`enabled`, `elements`.`archived`, `elements`.`dateLastMerged`, `elements`.`dateCreated`, `elements`.`dateUpdated`, `elements_sites`.`id` AS `siteSettingsId`, `elements_sites`.`slug`, `elements_sites`.`siteId`, `elements_sites`.`uri`, `elements_sites`.`enabled` AS `enabledForSite`, `entries`.`sectionId`, `entries`.`typeId`, `entries`.`authorId`, `entries`.`postDate`, `entries`.`expiryDate`, `content`.`id` AS `contentId`, `content`.`title`, `structureelements`.`root`, `structureelements`.`lft`, `structureelements`.`rgt`, `structureelements`.`level`, `structureelements`.`structureId`
FROM (SELECT `elements`.`id` AS `elementsId`, `elements_sites`.`id` AS `elementsSitesId`, `content`.`id` AS `contentId`, `structureelements`.`structureId`
FROM `elements` `elements`
INNER JOIN `entries` `entries` ON `entries`.`id` = `elements`.`id`
INNER JOIN `elements_sites` `elements_sites` ON `elements_sites`.`elementId` = `elements`.`id`
INNER JOIN `content` `content` ON (`content`.`elementId` = `elements`.`id`) AND (`content`.`siteId` = `elements_sites`.`siteId`)
LEFT JOIN `structureelements` `structureelements` ON (`structureelements`.`elementId` = `elements`.`id`) AND (EXISTS (SELECT *
FROM `structures` use index(primary)
WHERE (`id` = `structureelements`.`structureId`) AND (`dateDeleted` IS NULL)))
WHERE (`elements_sites`.`siteId`=1) AND (`elements`.`id`=6) AND (((`elements`.`enabled`=TRUE) AND (`elements_sites`.`enabled`=TRUE)) AND (`entries`.`postDate` <= '2023-12-23 01:01:59') AND ((`entries`.`expiryDate` IS NULL) OR (`entries`.`expiryDate` > '2023-12-23 01:01:59'))) AND (`elements`.`archived`=FALSE) AND (`elements`.`dateDeleted` IS NULL) AND (`elements`.`draftId` IS NULL) AND (`elements`.`revisionId` IS NULL)
ORDER BY FIELD(`elements`.`id`,2372)
LIMIT 1) `subquery`
INNER JOIN `elements` `elements` ON `elements`.`id` = `subquery`.`elementsId`
INNER JOIN `elements_sites` `elements_sites` ON `elements_sites`.`id` = `subquery`.`elementsSitesId`
INNER JOIN `entries` `entries` ON `entries`.`id` = `subquery`.`elementsId`
INNER JOIN `content` `content` ON `content`.`id` = `subquery`.`contentId`
LEFT JOIN `structureelements` `structureelements` ON (`structureelements`.`elementId` = `subquery`.`elementsId`) AND (`structureelements`.`structureId` = `subquery`.`structureId`)
ORDER BY FIELD(`elements`.`id`,2372)

However, the dropdown field shows all options correctly in the dropdown ✅ The query that fetches the options, correctly uses siteId=2

SELECT `elements`.`id`, `elements`.`canonicalId`, `elements`.`fieldLayoutId`, `elements`.`uid`, `elements`.`enabled`, `elements`.`archived`, `elements`.`dateLastMerged`, `elements`.`dateCreated`, `elements`.`dateUpdated`, `elements_sites`.`id` AS `siteSettingsId`, `elements_sites`.`slug`, `elements_sites`.`siteId`, `elements_sites`.`uri`, `elements_sites`.`enabled` AS `enabledForSite`, `entries`.`sectionId`, `entries`.`typeId`, `entries`.`authorId`, `entries`.`postDate`, `entries`.`expiryDate`, `content`.`id` AS `contentId`, `content`.`title`, `structureelements`.`root`, `structureelements`.`lft`, `structureelements`.`rgt`, `structureelements`.`level`, `structureelements`.`structureId`
FROM (SELECT `elements`.`id` AS `elementsId`, `elements_sites`.`id` AS `elementsSitesId`, `content`.`id` AS `contentId`, `structureelements`.`structureId`
FROM `elements` `elements`
INNER JOIN `entries` `entries` ON `entries`.`id` = `elements`.`id`
INNER JOIN `elements_sites` `elements_sites` ON `elements_sites`.`elementId` = `elements`.`id`
INNER JOIN `content` `content` ON (`content`.`elementId` = `elements`.`id`) AND (`content`.`siteId` = `elements_sites`.`siteId`)
LEFT JOIN `structureelements` `structureelements` ON (`structureelements`.`elementId` = `elements`.`id`) AND (EXISTS (SELECT *
FROM `structures` use index(primary)
WHERE (`id` = `structureelements`.`structureId`) AND (`dateDeleted` IS NULL)))
WHERE (`elements_sites`.`siteId`=2) AND (((`elements`.`enabled`=TRUE) AND (`elements_sites`.`enabled`=TRUE)) AND (`entries`.`postDate` <= '2023-12-23 01:01:59') AND ((`entries`.`expiryDate` IS NULL) OR (`entries`.`expiryDate` > '2023-12-23 01:01:59'))) AND (`elements`.`archived`=FALSE) AND (`elements`.`dateDeleted` IS NULL) AND (`elements`.`draftId` IS NULL) AND (`elements`.`revisionId` IS NULL)
ORDER BY `content`.`title`) `subquery`
INNER JOIN `elements` `elements` ON `elements`.`id` = `subquery`.`elementsId`
INNER JOIN `elements_sites` `elements_sites` ON `elements_sites`.`id` = `subquery`.`elementsSitesId`
INNER JOIN `entries` `entries` ON `entries`.`id` = `subquery`.`elementsId`
INNER JOIN `content` `content` ON `content`.`id` = `subquery`.`contentId`
LEFT JOIN `structureelements` `structureelements` ON (`structureelements`.`elementId` = `subquery`.`elementsId`) AND (`structureelements`.`structureId` = `subquery`.`structureId`)
ORDER BY `content`.`title`

Debugging

Digging in the Formie code, I could see that:

Code that sets the Default value: https://github.com/verbb/formie/blob/2.0.44.1/src/fields/formfields/Repeater.php#L182 Here, inside the populateValue function, an array of $blocks is created. Each block is a new NestedFieldRow object, which always has siteId = 1 set 🔴. This is the main problem. It should be siteId=2 because that's the website we're currently browsing...

https://github.com/craftcms/cms/blob/4.5.13/src/base/Element.php#L2241-L2247 The value of siteId=1 gets set by Craft CMS. NestedFieldRow extends Element, and in the Element.php class, it always takes the primary site ID.

Code that retrieves the Dropdown options https://github.com/verbb/formie/blob/2.0.44.1/src/fields/formfields/Entries.php#L216-L219 The options of a dropdown field are populated by getElementsQuery which Formie explicitly restricts to the current site ID 👍

Solution?

❓ Should Formie manually override the NestedFieldRow->siteId just like it does in the dropdown getElementsQuery? It feels wrong that the Repeater has NestedFieldRow with siteId=1 on them, when I'm browsing site 2.

I have no prior experience with the Formie core, but I suppose something like this could do the job:

// src/fields/formfields/Repeater.php
    public function populateValue($value): void
    {
        if (!is_array($value) || !isset($value[0])) {
            return;
        }

        $blocks = [];

        foreach ($value as $i => $fieldContent) {
            try {
                $row = new NestedFieldRow();
                $row->fieldId = $this->id;
                $row->setFieldValues($fieldContent);

+               if (Craft::$app->getIsMultiSite()) {
+                   $row->siteId = Craft::$app->getSites()->getCurrentSite()->id;
+               }

                $blocks[] = $row;
            } catch (Throwable $e) {
                continue;
            }
        }

        if ($blocks) {
            $this->defaultValue = new NestedFieldRowQuery(NestedFieldRow::class);
            $this->defaultValue->setBlocks($blocks);
        }
    }

Adding this in my local setup seems to fix the issue. The value prepopulates nicely.

engram-design commented 6 months ago

Great investigation here, and that's a very good point - something I hadn't considered. Appreciate your debugging this one!

Fixed for the next release. To get this early, run composer require verbb/formie:"dev-craft-4 as 2.0.44.1".

engram-design commented 6 months ago

Fixed in 2.0.45