Problem/Motivation
There are a number of bugs with the preview form and specifically fields with unlimited values.
The basic problem is that we only restore the field values but nothing else from the form state storage but field widgets rely on that. All fields rely on the item count, e.g. the file widget stores all data in there.
Steps to reproduce:
- Go to admin/structure/types/manage/article/fields to manage content type fields.
- Click on 'Add field' button.
- Add new field of 'Text(plain) field type.
- Make sure field's cardinality is set to unlimited.
- Go to http://local.d8/node/add/article to create a new article
- Fill out the required fields, add one value to the multi-value field, save
- Edit again
- Add at least two values additional values to the multi-value field
- Click 'Preview' button
- All values are displayed.
- On the new page click on 'Back to content editing button'
- All values are still displayed.
- press Save
- Only the first additional value (in total 2) was saved.
Same happens when editing content, already having e.g. one value and adding two more.
The reason is a mix of two things, first is the problem that we do not restore the form storage but only the values. But the other part is we immediately throw away the form state from key value for existing articles, so when the form is rebuild again for saving, we do not have the data to build enough form elements and throw away the additional ones.
Additionally, non-field values are also completely broken.
Proposed resolution
We restore the complete form state storage, not just the values. The second problem is solved by always relying on the UUID and only deleting the stored preview form_state after saving. So that it can be reliably restored for both form builds.
This is possibly a regression caused by the change to no longer cache forms with ajax elements on the first request.
Non-field values are restored by making sure to take over the user input.
Remaining tasks
User interface changes
The changed edit preview has one side effect. Previously, you could go to edit, then preview, then click away, then go back to the edit form and you you see the changes that you previewed again. The easiest way to see this is through another bug: click on edit on admin/content, make a change, then click on preview. You are redirect back to admin/content, when you click on edit again it looks as if your values were saved but it's actually just the preview.
I'd say that's actually an unexpected and confusing behaviour and not restoring them makes more sense.
API changes
Data model changes
| Comment | File | Size | Author |
|---|---|---|---|
| #76 | cannot_add_more_items-2548713-76-interdiff.txt | 649 bytes | berdir |
| #76 | cannot_add_more_items-2548713-76.patch | 11.83 KB | berdir |
| #72 | cannot_add_more_items-2548713-72-interdiff.txt | 9.1 KB | berdir |
| #72 | cannot_add_more_items-2548713-72.patch | 11.75 KB | berdir |
| #53 | cannot_add_more_items-2548713-52-test-only.patch | 5.66 KB | johnchque |
Comments
Comment #2
larowlanComment #3
larowlanhere's a failing test
Comment #5
larowlanThe issue here is $preview = $form_state loses the $form_state->getStorage('field_storage') when the NodeForm::form method returns.
No idea why
Comment #6
larowlanSo $form_state = $preview is invalid as it doesn't persist outside NodeForm::form (not passed by reference).
So we need to copy the values over.
Comment #7
larowlanThis fixes the issue *but* breaks the reload feature (seen as test fails)
But I don't think we can have both, as allowing reload means we keep loading the stale form state from the tempstore using the UUID.
I think the fix is to remove those three asserts that check reloading retains the data.
@swentel - is there a reason we support reloading for the unsaved node (but not for saved nodes)?
Comment #8
swentel commentedHmm, the only reason I can think of (right now) is that if you reload node/add/{x}, you'd lose all entered data. However, that's only the case if you have gone to the preview at least once since we have an entry in the tempstore at that point - or at least in current HEAD.
I'm fine with removing that 'feature'. Reloading is a deliberate action by either clicking on the reload button in your browser or using a keyboard shortcut. Also, in case you'd click away and hit the back button, you're also still fine.
Comment #9
larowlanOk removes tests for that feature
Comment #10
swentel commentedCode looks fine to me - added manual testing just for a double confirmation, didn't get to that yet.
Extreme nitpick - spaces :)
Comment #11
swentel commentedRemoved the newline
Comment #12
swentel commentedDid some manual testing on the node article which I made unlimited.
It has some weird side effects like losing the alternate text, or even a second image when coming back to the node form - unless that is already in HEAD, ran out of time to test that.
Comment #13
kattekrab commentedConfirming that alt text getting lost after previewing is in head.
But if you add the alt text again, preview for a 2nd time, it is still there when going back.
Quick chat with @larowlan - going to send that to a new issue.
Comment #14
kattekrab commentedAdded #2551217: Data loss using Preview on content types with image field.
Comment #15
webchickLee asked me about this in IRC. I had no idea that you could hit reload in your browser and get your old form contents back. I would never in my life expect that to happen, since it doesn't happen literally anywhere else on the entire internet. :) Reload = destructive. (Honestly, by design. It's the "oh crap, abort abort!" button.) So I'm fine to kill that feature, as long as we don't screw Views UI in the process, in case people were counting on that for that UI. (I don't see any hunks outside of Node so I think we're fine there.)
Comment #16
swentel commentedTested it again - looks good now. #2551217: Data loss using Preview on content types with image field. fixes the actual data loss then.
Comment #20
subhojit777Comment #21
subhojit777Comment #23
subhojit777By mistake I had removed the code changes that are already committed. Still the tests will fail (tested on local). Will look into them later.
Comment #24
larowlanlooking
Comment #25
larowlanComment #26
larowlanSo the issue here is that FileWidget::submit stores the uploaded items in storage.
Which takes precedence over $items taken from
FormStateInterface::getValues()in\Drupal\file\Plugin\Field\FieldWidget\FileWidget::formMultipleElementsand in
\Drupal\file\Plugin\Field\FieldWidget\FileWidget::submitSo let's see what if this breaks anything.
Comment #28
larowlanComment #29
larowlanOk, instead of clearing it, we should update it.
Comment #30
larowlanIn my books this is ready now
Comment #31
swentel commentedWill look and do manual test today
Comment #32
swentel commentedSo there's still a problem here - unless we have a different issue for that ?
1) Set image to unlimited
2) Add an image
3) Click preview
4) Go back to node edit
5) Add another image
6) it won't work
7) Try to add again, now it will work
8) Click preview and you'll see the second image
9) Go back to node edit
10) Hit preview again, second image will be gone.
Tricky one :/
Comment #33
swentel commentedWill try to check later today to write a test for #32
Comment #34
swentel commentedPatch that mimics the scenario from #32 until step 6 proving the fail to upload the second image after coming back from preview.
Comment #37
mgiffordComment #39
johnchqueExtended tests. Added a new approach, it seems the node is not saved with the change. Let's see with testbot.
Comment #40
berdirnot on preview, on form submission.
Comment #42
berdirAdd that, then adding content works again.
Comment #43
johnchqueTrue, test passes locally with it, let's see about test bot. :)
Comment #44
berdirTurns out my idea with post_$uuid doesn't work, it would still be used if you refresh the page and then submit.
Discussed with @swentel and I have a different idea:
So.... in the back to content link, always append the uuid argument. And in the code, only do it if that argument is present. And then remove the delete completely, then a refresh will keep the data, but given that we have that special argument, that makes sense to me.
Comment #45
johnchqueThank you for the feedback! :D
Seems to be working, let's see about testbot.
Comment #46
johnchqueDiscussed with @Berdir this will help with consistency.
Comment #47
swentel commentedLove the cleanup, makes the code more consistent in general.
We'll need a test for this line. Talked with berdir in IRC, and probably a simple form alter on the node form which puts something in the storage should do the trick for testing this.
Comment #49
johnchqueAdded code of a previous patch. Could not manage to fix the tests already written.
Comment #51
berdirOk, the problem with the previous patch was that we always took the storage now and replaced it if it was updated already. Now we skip that if we are rebuilding and have our own storage already.
Also, as far as I can see, setting the form values is not necessary, the test passes without that and manual testing is fine too.
We still need test coverage that form storage works in the first place.
Comment #52
johnchqueThank you so much @Berdir for helping with the patch! Added test coverage. If it passes should be Ok now. :)
Comment #53
johnchqueSorry, here we go with the test-only patch.
Comment #55
berdirTest fail was just the test-only patch.
Comment #57
larowlanNit: We can inject the request stack although I note that this is existing in HEAD
For other reviewers note that this is cleaned up when the form is submitted and the node is saved (see
\Drupal\node\NodeForm::save)Comment #58
alexpottIs it possible some contrib widgets will need similar fixes?
Comment #59
alexpottSetting back to needs review to get an answer to #58
Comment #60
berdirYes, I guess it is possible.
But I don't see an alternative. We have to restore form storage. Other things are broken right now because that's missing, we're just not seeing it.
To be honest, I don't really understand what the code in FileWidget is even doing. Why is it the only widget affected by this. How is it not a problem for all other widgets? Why is that even needed?
The only other two modules that I have locally that use those widget state methods are Paragraphs and Inline Entity Form. I've just run all paragraphs tests and they are passing with this patch. We do have preview tests, quite a few actually. In fact, we even have a commented out assertion that is blocked on this issue because things are pretty broken without this fix.
And IEF doesn't support previews at all right now.
Comment #61
xjmThe core committers and Entity and Field maintainers agreed that this issue was a major bug. See #60 for some of the impacts of this; the issue is apparently quite disruptive when combined with a workflow that involves a lot of previewing (as is common with paragraph).
Comment #62
bojanz commentedberdir pinged me to look at this issue. The patch looks sane, I agree with the general approach.
It makes sense for a widget that uses widget state to keep that state up to date with changes to $items, but I'm unsure why the logic in FileWidget::submit() isn't enough (perhaps submit() doesn't trigger when needed?)
Comment #63
swentel commentedI've also had a quick look on the widget state code that this patch adds. As @bojanz said, the same routine happens in ::submit, but it isn't triggered, but I have no clue why yet either. Makes me wonder whether we can remove that code from the submit method if this now would live in massageFormValues() too ?
Comment #65
berdirRe #58.
I think it is pretty unlikely that another widget needs this. File upload is very special, it is the only one in core that uses widget state for this, everything else just works. It has to do with the very strange #type file_managed and how it is processing values.
The reason we do need it now is that there can be case where a file is uploaded and ::submit() is not called, for example when having JS disabled and directly clicking on preview.
IMHO, even if we would break some widget for preview, this is fixing worse bugs in core that affect pretty much all field types than breaking a widget. So setting back to RTBC, lets discuss if you're not convinced.
Comment #66
effulgentsia commentedManually testing HEAD (8.3.x) with the steps in the current IS, I'm not experiencing the bug described.
Looking at the test in the patch, I'm not seeing those steps either. Instead I'm seeing this:
Which is testing what happens when you go to preview and back multiple times. Which is a great test, and is uncovering a real bug in HEAD, but it's different than this issue's title and summary.
I'm not sure if this needs to be set to "Needs work" and "Needs issue summary update", or if the issue title and summary are correct, and there's something I'm doing wrong in testing this. Therefore, setting to "Needs review" for feedback on that.
Comment #67
berdirSorry about the old isse summary. I updated it and also slightly changed the title to better explain the problem.
Comment #68
berdirComment #69
berdirThis breaks values from non-fields, e.g. menu_ui. We apparently have no test coverage for that, so we need to add that as well.
Comment #70
effulgentsia commentedThis is likely the reason for #69.
massageFormValues() shouldn't have a side-effect like this. I think this makes more sense in extractFormValues(), where WidgetBase already does similar state manipulation. I wonder if we should split this whole bit out into a separate issue? Is it possible to trigger the problem this is solving independently of what the rest of this patch is doing? Seems to me like it should be, but I haven't delved into trying.
Related: it bothers me that with this patch, we would have this code duplicated between FileWidget::extractFormValues() and FileWidget::submit(). In fact, looks like FileWidget::submit() already duplicates some of WidgetBase::extractFormValues(). I wonder if we could/should somehow make FileWidget::submit() invoke extractFormValues() rather than duplicate it? Possibly tricky due to the former being a static function, and maybe a bad idea for other reasons, just throwing it out there as a first impression.
Comment #71
effulgentsia commentedAlso, thank you for the updated IS, but:
The test in the patch is not testing this. It's testing something similar, via multiple successive previews, but I think it's worth having an explicit test for the steps in the issue summary, since that appears a more direct pathway to data loss.
Comment #72
berdirYeah, it is a bit more complicated than just restoring that code, because that actually don't really do anything. We restore form values from user input, not values. And restoring that makes it want to cache, so I have to trick it into believing it is a post request. Not sure if that has side effects.
One side effect of this is that the exact submitted values are restored and we don't rebuild based on the built entity. That means it requires a small change in the existing test coverage, because the term field is exactly as it was entered, without the (1) part.
I'm also sure that this is *not* a regression from HEAD, as setting form state values doesn't do anything.
I also added test coverage for actually saving and ensuring the value is there. And I moved the widget state stuff.
Comment #73
berdirStill looking for reviews here :)
Also, I'm still surprised myself, but I actually *understand* FileWidget now I think. See #2811841-32: Add test coverage ensuring user input is mapped on the correct form elements when elements are reordered and the issue in general for a lot of fun facts about entity forms, widgets, widget state and more.
Comment #74
swentel commentedNit: {@inheritdoc}
I didn't know about getUserInput() this far, so would be nice if someone else could confirm that this is ok (although tests prove if of course).
Comment #75
tim.plunkettThis will be the first usage of setUserInput() outside of Views code :)
It also happens to be correct usage here.
Looking at \Drupal\Core\Form\FormState::getCacheableArray(), pretty sure that storage is the only part we care about, so this looks correct.
Comment #76
berdirFixed the nitpick. This should be ready then?
Comment #77
swentel commentedYes!
Comment #78
alexpottI've manually tested this and it fixes the bug. There are no changes that contradict our BC policy.
Committed and pushed f403ccb to 8.3.x and 5d0814d to 8.2.x. Thanks!
Fixed on commit.
Comment #81
berdirArgh, I did find one regression here, actually identifed by a paragraph test that I didn't run when working on this (ParagraphsAccessTest) but took me a while to track it down.
Given the following:
* An Image field (probably file as well), does not need to be on paragraph/nested form, the one on article works to reproduce this
* JS disabled
* alt/title not required
When you select a file, and then directly go to Preview, without clicking "Upload" first, and then go back to content edit then the image field is in a weird, messed up state. Somehow I think we get a mess between user input and stored widget state so that the fids input overrides the actual data.
Saving works fine without clicking upload, it's really just Preview.
I *think* this is rare enough to not require that this is reverted but can be looked into in a follow-up, thoughts?
Comment #82
swentel commentedI'm fine with follow up
Comment #83
berdirOk, created #2836455: Image/file lost after node preview without JS