Support for Drupal 7 is ending on 5 January 2025—it’s time to migrate to Drupal 10! Learn about the many benefits of Drupal 10 and find migration tools in our resource center.
Problem/Motivation
It would be nice if users could visually select media when editing and creating content, instead of using the Entity Reference field widget.
Proposed resolution
We should create a new field widget that allows users to select media from the Media library view, and ensure that the widget is designed in a way that other selection interfaces (like upload, oembed, etc.) will also work in the future.
Remaining tasks
Write/review patches, wait for #2962110: Add the Media Library module to Drupal core to land.
User interface changes
A new field widget will be available to users who are already using the Media library.
API changes
TBD.
Data model changes
None.
Comment | File | Size | Author |
---|---|---|---|
#99 | interdiff-2962525-91-99.txt | 10.42 KB | samuel.mortenson |
#99 | 2962525-99.patch | 81.79 KB | samuel.mortenson |
#70 | library-review-4.png | 546.89 KB | seanB |
#70 | library-review-3.png | 1.11 MB | seanB |
#70 | library-review-2.png | 1.11 MB | seanB |
Comments
Comment #2
samuel.mortensonHere's an initial patch. The combined one can be applied and tested on its own, the other patch is an interdiff with #2962110: Add the Media Library module to Drupal core and that should be where review goes. Do not review the combined patch!
@todos I know about:
Comment #3
samuel.mortensonHere's a re-roll for the styling changes from the main issue.
Comment #4
samuel.mortenson#2.3 is fixed by the latest patch from #2861860: View inside core modal doesn't refresh correctly when reopened.
Comment #5
phenaproxima@samuel.mortenson demoed this patch in the UX meeting today. Here's the feedback we got:
<label>Weight<span class="visually-hidden"> for $title_of_item</span></label>
/li>Comment #6
phenaproximaComment #7
andrewmacpherson CreditAttribution: andrewmacpherson as a volunteer commentedWe reviewed this in the UX meeting today. The weight field just has a label which says "weight" and it's not clear which media item it refers to. So we'd want either:
<label>Weight<span class="visually-hidden"> for blue bunny</span></label>
. This option is the more robust, because assistive tech will always have it as part of the "accessible name" of the input.Don't try to convey this with a title attribute, that won't work.
We didn't look at the remove button in detail, but the same point applies; the button label needs to make it clear which media item will be removed.
The multi-value widgets currently used in core have table rows wrapping the weight and remove button, so their context comes from that semantic table row wrapper.
Leaving the needs accessibility review tag on this, I'm sure I'll want to have another look.
Comment #8
samuel.mortensonThere's another bug unrelated to #4 - if you open the library for the first time, then use the exposed filters, then select media, that doesn't work. It looks like the relationship between the dialog button bar and the actual views buttons gets broken or something, since clicking "Select media" ends up clicking the exposed filter submit button.
Comment #9
samuel.mortensonI'm pretty sure the bug in #8 is related to #2504115: AJAX forms should submit to $form['#action'] instead of <current>, which has no available patch. 😢
Comment #10
samuel.mortensonThis updated patch works around the problem from #2504115: AJAX forms should submit to $form['#action'] instead of <current>, and improves accessibility for the remove button. Users are also now warned when clicking an outgoing Media item link, which may otherwise cause data loss.
None of the UX feedback from the meeting yesterday has been addressed, yet.
I also found that #2823541: Table clicksort is lost when using views exposed filter is half of the problem described in #8, as query parameters passed when building the View are lost when using the exposed form. This causes the media widget ID to become lost, which means that the select form cannot properly update the widget in its AJAX callback.
So to resolve all Views bugs, you need the latest patch from #2823541: Table clicksort is lost when using views exposed filter as well as #2861860: View inside core modal doesn't refresh correctly when reopened. 😢
Comment #11
samuel.mortensonComment #12
samuel.mortensonAfter a lot of review, I determined that #2823541: Table clicksort is lost when using views exposed filter is related to the bug in #8, but is not exactly the same. I've added a fix for persisting the widget ID to our post render, which is working nicely for now.
Comment #13
phenaproximaLooking pretty good :)
Should we create a new display plugin so that we don't have to expose a full path to this display? After all, it's never meant to be used for anything but the media library widget. I'm not sure if that's useful, feasible, or how much work it would take.
jquery.ui.sortable already depends on jquery, so we can remove the redundant dependency.
Can we add a comment linking to the related issues/blockers?
We'll need JavaScript review on this file, and it couldn't hurt to run it through eslint as well.
I'm not sure we need this command -- we could just use a pair of InvokeCommands to call .val() and .trigger() on the relevant elements.
Since this functionality will hopefully be ported into Views at some point in the future, should we rename this to something more generic, like entity_selection_form?
We can remove this entire class if we use InvokeCommand as suggested previously.
Nit: This can be "media entities".
Nit: Let's just return parent::form().
Can we say "No media items selected"?
This will make the JS a little more complex, but can we say "Show" and "Hide" instead of "Toggle"? Also, are there any relevant aria-* accessibility attributes that might be useful here?
Could we extract this delta from the triggering element's #parents? If not, no big deal; just curious.
If there is already an issue, let's link to it here.
Why do we need a container here? I feel like we could use #theme_wrappers on the button itself for that.
I don't think we need to expose a UI for it right now, but let's go ahead and make it a setting now, and open a follow-up to expose it as an option.
We should use \Drupal\Component\Serialization\Json::encode().
We can phrase this more lucidly, I think, using the plural: "This field cannot accept more than @count item(s)."
This points to a UX hole; the view should not even *show* any media items that are of an unacceptable type. We can open a follow-up for that, but it may well be a stability blocker. =\
To mitigate this for now, though, let's add a helpful description to the widget which says something like "You may select media items of the Foo, Bar, and Baz types."
Does this need to be public? We should avoid adding API surface if possible.
Same here -- not sure this needs to be public.
Here too.
Lets just return early if $this->view->result is empty, so we don't have to indent the entire function.
For accessibility, can we improve the label here? Something like 'Select "$item_label"' would do the trick.
Can there be a comment explaining this?
Comment #14
phenaproximaTagging for JavaScript review.
Comment #15
phenaproximaTagging for a follow-up issue per #13.15.
Comment #16
samuel.mortensonAddressing #13:
This patch includes all the recent changes from the parent issue - which has added a "Select all items" checkbox to the widget modal. I understand that this does not make sense, but we can't fix it in this issue since the code was added in #2962110: Add the Media Library module to Drupal core. I will make it so this checkbox only shows up on the page view in the parent issue, please don't review it here! ;-)
Comment #17
samuel.mortensonHere's a new patch with these changes:
Comment #18
samuel.mortensonHere's a new patch with the following changes:
I also discovered an issue we need to address - the Media library widget display currently requires the "Access media overview" permission to use. This was inherited by the default display (Page), and we can override it so that the "View media" permission is used instead which makes sense to me.
But here's an interesting case - if a user with permission to use the widget does not have the "view the administration theme", the Media library would render using the frontend theme, which would make styling super inconsistent. Is there a way for us to always use the admin theme for the widget path, even if a user does not have the "view the administration theme" permission?
Comment #19
phenaproximaThis is a question that should be addressed by the UX team, IMHO.
And this should be answered by a front-end framework manager.
Comment #20
lauriiiTheme authors should provide styles for this part of the system just like for any other part. We already have this problem for example with node forms that by a good chance they might be rendered using either, the admin or the frontend theme.
I'm leaving the tag since I didn't have a chance to review the frontend parts of the patch yet.
Comment #21
samuel.mortensonThanks @lauriii - I changed the permission for the widget view display to "View media", and will leave the admin-theme stuff aside per your review.
With my security hat on, I did still worry about providing a new core view on an admin path with AJAX and exposed filters to anonymous users. The view shouldn't disclose any information, but it was awkward that anonymous users could hit "/admin/content/media-widget" on any Drupal site using this. To avoid this problem I added the _csrf_token requirement to the route generated by views for the widget, which ensures that users cannot hit the path directly, but can still use it in the field widget.
Comment #22
samuel.mortensonRe-roll for the CSS file split from the main issue.
Comment #24
xjmWe discussed that we can make adding oEmbed media in the widget something that we discuss in a followup.
Comment #25
phenaproximaThe media library has been committed, so let's put booster rockets on this thing.
Comment #26
phenaproximaMarking for a re-roll.
Comment #27
phenaproximaIt'd be nice to not expose this display as a page, but we can deal with that in a follow-up.
Is there an existing class name we could re-use?
I wonder if this might raise some eyebrows. Even if it does, I'm not sure there's much we can do about it until we are using a different display plugin.
I'm not sure if this would annoy the UX team, but why shouldn't we expose the ability to select all items?
What does this do?
Not a CSS expert, but .media-library-item-remove seems like it should be renamed to media-library-item__remove, for BEM conformance.
Nit: Could use #fff.
Let's group this one with the :hover and :focus pseudo-selectors.
I'm not a huge fan of the fact that this is hard-coded, but I think we could address that later. Maybe a follow-up?
This could use a comment or two.
Can we expand on what "can be selected" means? By what criteria are we determining selectability?
The first condition needs ===.
This code is repeated later in the module, so can we make a helper function? Also, we should ensure that $types is non-empty and consists entirely of strings.
Is there any way we can avoid hard-coding the name of the table?
Should 'in' be all-caps?
Not sure that "not" should be here.
We don't need to keep $field_type_manager in its own variable.
drupaL! Sounds like a fork =P
Let's keep $session in its own variable to avoid calling getSession() repeatedly.
Comment #28
GrandmaGlassesRopeManMissing trailing comma.
These properties don't need to be quoted. Additionally, probably don't need to be in an Object since they are used infrequently.
Both `.parent()` and `.toggle()` should be on their own line.
`$view` is already declared in the previous scope.
Use triple-equals.
Comment #29
phenaproximaLet's mark this explicitly @internal, as all plugin classes are.
I think a lot of the logic in here can potentially be encapsulated as its own form element class -- essentially it is "select some things from a view". What do you think -- could we implement that semi-generically? I don't know if we necessarily should bother with that now, if it will be very complicated, but it might be worth exploring in a follow-up.
#limit_validation_errors is a tricky little devil. If we can avoid using it, that'd be nice. Otherwise, let's add a comment explaining what this is for.
Should this element's #type be 'number', since it must be a number?
I love this.
We should use the CARDINALITY_UNLIMITED constant, rather than hard-code -1.
A comment would be good here, since #limit_validation_errors is so opaque.
A comment would be useful here.
What does this do? A comment would be useful here as well.
Can we get a comment here?
I don't know if this makes sense, but I almost feel like the "current selection" should be encapsulated in some kind of value object or something, just to make it easier to understand and manipulate. But, that's not a priority right now unless it's easy.
Why self instead of static here?
I think we'll need a PluralMarkup here.
"Not of an accepted type" sounds developer-ey to me. Not quite sure how to rephrase it; maybe this is one for the UX team.
Should be "...and flags the form to be rebuilt."
Should self be static?
Why is the ->access() check being done here? Needs a comment, I think.
Nit: 'ids' --> 'IDs'
Let's mark this guy explicitly @internal too.
Comment #30
samuel.mortensonNow that's what I call a re-roll!
Addressed a lot of review:
#27:
1. It would be nice, although I'm not sure how exposed filters work with other displays. May be worth looking into it in this issue.
2. This is an existing class.
3. I felt like this would raise eyebrows as well, so I added a CSRF token requirement to the view which prevents direct access to the widget outside of a field widget context. :)
4. I don't think we should allow selecting all items in the context of a widget. You would have to have filtered the view to be seeing exactly what you want to select, which is extremely rare (I've never been in that situation).
5. I don't know! Removed.
6. Reworked to be more BEM-like.
7. Done.
8. Done, this was verbose but it works.
9. I think when we make the view is configurable we should address this.
10. Done.
11. Done.
12. Done.
13. Done.
14. Yes, done.
15. In Views, "in" is lowercase. It may make it uppercase when forming the query, I'm not sure.
16. Fixed.
17. Fixed.
18. Fixed, hah.
19. Done.
#28:
1. Fixed.
2. Fixed - they're only in an object because "show" is used twice. This is pattern in other libraries like tableselect.
3. Fixed.
4. Fixed.
5. Fixed.
#29:
1. Done.
2. This will have to be addressed in a follow up - making this generic is a ton of work.
3. I commented all instances of limit_validation_errors.
4. Other "Weight" elements in core use a textfield, so I'm just following that standard.
5. Thanks! It should look familiar :-)
6. Fixed.
7. See #3
8. Commented.
9. Commented - there was a block above this that briefly explained it but I added more information.
10. Commented.
11. I don't think moving to a value object is easy - this use of field state is kind of normal in core but if you want to move it I can look into it more.
12. No idea, changed to static.
13. Done.
14. The errors here should never be shown to end users, they're just a fail-safe in case something goes wrong with the View. The widget's JS and our View query alter already restrict invalid selections. I'm OK re-wording it if we want to though.
15. Done.
16. Done.
17. Commented.
18. Done.
---------------
I also saw this notice when testing:
From debugging, I can see that a render array like [#markup => Media] is being passed as a dialog title here, but we don't use that as the modal title in the patch so I have no clue where it's coming from. Any help is welcome.
For those testing, remember that #2861860: View inside core modal doesn't refresh correctly when reopened is required if you're going to open/close the modal a lot and use exposed filters.
Comment #31
samuel.mortensonComment #32
phenaproximaGood stuff. This is only a partial review, but I think this patch is shaping up. We can get away with a lot since Media Library is experimental.
Is there any more specific CSS class/selector we can use to target the inner elements, not just div?
Correct me if I'm wrong, but can't this be rephrased to just
$(target).find('.media-library-item-weight').each((weight, element) => { element.value = weight; })
?Can we set this attribute on the server side, rather than in JavaScript?
I'm not sure we need this if block. We could just do the .find().on('change') stuff, and if nothing is found...it'll have no effect.
===
Let's improve this -- how about "Allows you to select items from the media library"?
Comment #33
phenaproximaAnother partial review.
Will this query string be attached to *all* AJAX rebuilds on the page? If so, it's probably not a problem because all our query string parameters are prefixed with media_library...but we should mention that in a comment, and probably have a test scenario with the media library co-existing with another AJAX-enabled view on the same page. I don't mind deferring that to a follow-up, though.
Couldn't we implement hook_form_BASE_FORM_ID_alter() for this?
Nit: Why not add this to the big
$element +=
operation above?What is the 'media-library-item-in-widget' class used for?
Can we rename this element to rendered_entity, to prevent confusion with Views?
Maybe this should be media-library-item__weight for BEM compliance.
We should only be calculating this if !$cardinality_unlimited.
Let's say "media item" or "media items" everywhere, for consistency.
Why not use a hidden field?
I'm not sure this should be visually hidden; it seems like something we *always* want to hide, even from screen readers. Maybe we should just hard-code a
display: none
there?No need for the
else {}
boilerplate. We can just return [].Am I overly paranoid for thinking this is potentially brittle?
This is confusing. Can we get a comment explaining what we expect these arrays to look like?
These can be combined into a single if statement.
isset($values['selection'][$delta])
This could use some comments as well.
Comment #34
samuel.mortensonAddressed review:
#32:
1. Yes, we should add one. Fixed.
2. We need the index of the thing that is draggable (the parent), not the index of the weight item, so this cannot be rephrased.
3. Yes! Fixed.
4. Fixed, I think that's right.
5. Fixed! For real this time. :-)
6. Changed, thanks.
#33:
1. Yes, it will be - I've updated the comment to reflect this. There are Drupal settings per View but none of these let you modify the /views/ajax path, we need to add query parameters here to ensure that the view is given the correct contextual information (specifically the allowed types and field widget identifier). I looked into other ways to do this but I couldn't find anything better.
2. No - the ViewsForm "getBaseFormId" method is not actually the base form ID, just to make things confusing. It's the base of the full form ID. The base ID is "views_form", I think.
3. We could do this, but since there are so many $element[...] blocks later on I preferred to keep each one in its own section. This makes thing a bit more readable, I think.
4. No idea! Removed.
5. Done.
6. Changed, thanks.
7. We could do this, but $remaining is used later on in the method and we can't easily inclose all of its uses in conditional statements. It's easier to leave it as is, I think.
8. Fixed.
9. I don't know! Looks like hidden works fine.
10. Good catch! I went with a new CSS rule to hide this.
11. Fixed.
12. This is a weird part of form API - because we control what the render array structure is, these depths should never change. Basically all field widgets that use AJAX to rebuild have to do this. So I'd say yes, you're overly paranoid, but it's still funky.
13. Commented.
14. Fixed.
15. Commented.
Comment #35
phenaproximaPro tip: isset() is variadic. This could be
isset($values['selection'], $values['selection'][$delta])
, and it will only be TRUE if both are set. However, I don't think we need to explicitly check for $values['selection'], so justisset($values['selection'][$delta])
should work fine here.Nit: This can go inside the if block, since that's the only place we're actually changing $field_state.
Supernit: 'view' should be lowercase.
Correct me if I'm wrong, but isn't formatPlural() part of StringTranslationTrait, which I believe we're using in this class? Also, the plural string should end with "items", not "item".
Is #target_bundles a set of machine names? If so, we should put the human-readable labels of the media types in this message, rather than the machine names.
Why not have have getWidgetState() always return an empty array of items, if one is not already set?
We can remove the first condition -- if $media_item is falsy, it means there is an error condition and we should allow it to fail hard so we can easily trace and debug the problem.
Do we need an explicit weight? The items are numerically indexed, which implicitly serves as a weight.
We don't need the 'array' type hint; the second one is more useful and specific.
I don't know if this needs to be public.
We should probably validate
that these are all integers.
It's always an array, so I'm not sure we need the mixed type hint. It would also be nice to document what is found in the array. Also, does the method need to be public?
Is there any reason for this to be public?
updateWidget() expects the entity type ID to be 'media' -- hard-coded -- so we should hard-code 'media' here too.
I didn't know you could do this! Neat stuff.
Let's throw a BadRequestHttpException if $widget_id is invalid.
If my understanding of Drupal's CSRF system is correct -- and it's a very sketchy understanding indeed -- this will prevent the media library from being usable by anonymous users. Which might be an edge case, but are we sure we want to cut off this feature for anonymous users? Seems like it might be better to expose a new permission or something. However, I don't mind moving that to a follow-up.
Comment #36
samuel.mortensonHere's a demo video comparing the current entity reference widget with the widget provided in this patch: https://www.youtube.com/watch?v=LwGuf17Oz40
Comment #37
samuel.mortensonIt's going to take me a bit to get #35.17 working but we should fix it here, not in a follow up. We can do something similar to file fields are validate access to an entity field in a custom access check.
Comment #38
Gábor Hojtsy1. Remove "You can", we don't speak to our users in this form in our text.
2. The first one should also be *up to* one media item no? Or it depends on whether its a required field? Do we express the requiredness in some form?
3. If we can avoid the "@remaining left" if the maximum equals remaining, that would be nicer.
Comment #39
dqd#38 was faster ... The "You can" immediately catched my eye. Agreed on all points of Gábor. Plus, I would love to know if this (modal popup) has been tested mobile and if it breaks if not Seven is admin theme?
EDIT: Let me rephrase the last question. I would like to prevent that the modal polish is "hard" connected with Seven and want to make sure that it is following general standarts for other themes to adapt, since otherwise this would cause a lot of trouble and work arounds in contrib again. (Or) we should add the option to switch between modal and a collapsible form or something similar. We all remember the issues raising regarding modals in admin area in D7 times? But its just a thought which came in mind...
Comment #40
webchickThanks so much for recording this video! The widget looks + works AMAZINGLY! :D Brilliant touch with "greying out" media when it can no longer be selected.
Two points of feedback:
1) I think the "add" help text should remain as part of this patch, and can be removed if/when #2938116: Allow media to be uploaded with the Media Library field widget makes it in. We want to always keep core in a "shippable state" and at the moment if we remove that text, we are regressing from the current level of functionality (clunky as it may be).
2) I noticed that even though you've attached Newman to the node, you can go back and re-attach Newman again the second time. I guess it's possible there's a use case for attaching the same image twice, but it seems like undesirable behaviour that's easy for someone to unintentionally do. Is it possible/easy to filter out from the Media Library selection media that are already attached to the node via the same field?
Comment #41
phenaproximaIt should be possible, but I don't know if that would be desirable. Entity reference fields by their nature allow one to reference the same item multiple times if desired, so I'm not sure if we should prevent that here. If anything, I'd prefer to open a follow-up to explore that possibility, rather than disable it in the initial patch.
Comment #42
webchickOh, and I guess a third point, though unrelated to this patch... is there an issue somewhere for making the "Add media" flow vastly simpler? I know we inherit all those bogus fields (URL alias, Revisions, etc.) from other parts of core, but here especially it is cringe-worthy how bad of a UX this creates. :\ Literally 3/4 of the screen is dedicated to completely irrelevant stuff to the behaviour you're trying to do. It might almost be worth one-offing with some form alters here to create a much slicker experience, as long as we're still in experimental mode. (Would need framework manager approval.)
OTOH, if the mocks at #2938116: Allow media to be uploaded with the Media Library field widget seem doable for 8.6, that probably negates this feedback.
Comment #43
webchickCross-post. A follow-up to discuss about re-attaching the same media to the same field sounds good!
Comment #44
phenaproximaFollow-up filed: #2981895: Prevent already-selected media items from being chosen twice
Comment #45
phenaproximaThis was demoed in the UX call today -- everyone agrees that it's lightyears better than the current widget in core. @yoroy was not in the call, so we don't have official UX sign-off yet, but everyone in the call seemed very happy with it. @ckrina had some design feedback, but she didn't express any major concerns (potentially doable in a follow-up). She promised to post her feedback here too.
Comment #46
ckrinaThe issue about the design was that the first design styles are the ones implemented while the final version proposed has some modifications:
- Removes the white padding around the image
- Reduces the space for the title/link to the detail
My other concern was the dimensions of the cross icon, because I think it's too big for the space around it. Anyway it's a really minor issue and I'll check it with some more time because I'm not sure.
Comment #47
samuel.mortensonAddressed review:
#35:
1. Didn't know this, thanks!
2. Fixed.
3. Fixed.
4. We are using StringTranslationTrait, but this is a static method and cannot use $this.
5. It is a set of machine names, but like the other label you found this text shouldn't be visible unless something went wrong in the modal.
6. Why not? Done.
7. Done.
8. We do need to explicitly set weight, if you re-order items with drag+drop the numeric index will match weight, but if you set the weight with the textfield it will not. This is how weight is stored in tabledrag as well, iirc.
9. Fixed.
10. Static methods must be public.
11. Done.
12. Done.
13. Static methods must be public.
14. Done.
15. It's pretty crazy! I made sure this was documented in the API before using it.
16. Done.
17. I need to think about this more - it could be that having the View be accessible by a URL is a mistake to begin with, so I'm going to look into using the "Embed" display plugin instead of "Page". Wish me luck!
#38 - Good feedback, I moved things around and simplified this a little bit. Now the prompts are "One media item remaining", "@count media items remaining.", and "The maximum number of media items have been selected.".
#39 - The widget is using the standard core modal and Views to render itself, and does not have anything Seven specific, in terms of markup. I also checked the widget out on mobile and it's usable, although we could probably find areas to improve. Switching between the modal and a collapsable form is possible but I'm not sure we want to introduce that pattern. Best for someone from UX to answer this.
#40 - After some refactoring I got the "add" help text back in.
#46 - I can't believe I missed the padding! Sorry about that, I've tweaked the CSS a bit to make it look like the real final mockups. See attached screenshot.
Comment #48
phenaproximaEvery time I review this patch, I find less and less to complain about. The field widget makes a lot of sense to me. It's dense, but also deceptively simple. Really nice work here.
Supernit: url --> URL
Same here. Also, what is meant by "help URL"?
Let's mark this function explicitly deprecated and internal, and link to the issue where we intend to remove it.
This seems like a good candidate for array_map() and array_filter().
I don't think we need the !empty() check, and we can simply do early returns in each of the inner conditions. Then the whole function can simply end with
return FALSE
. I think that'll be a little more readable.Do we have test coverage to assert that this checkbox is never visible in the widget?
Let's use UrlHelper::buildQuery() instead of http_build_query().
Supernit: 'views' should be 'Views', since it's the proper name of a module.
I think static::getWidgetState() should always return an array with an 'items' key, which defaults to an empty array.
Sigh...oh, for the day when we can remove this.
Rather than use #access, let's just generate this
if (empty($referenced_entities))
. There's no other conceivable reason why this element should exist at all.As with the previous thing, let's only create this element if $referenced_entities is not empty to begin with.
Although it breaks precedent, let's have #type => 'number' here. Weights cannot be anything except integers, so we should let the generated HTML reflect that.
Supernit: 'View' should be lowercase.
Today I learned that the use-ajax class will have no effect if the core/drupal.ajax library is not loaded. So let's explicitly attach it to this button. :)
Superdupermeganit: There's an extra space after "...based on the".
Let's use the js-hide class to keep this button hidden, rather than a custom class.
Should we also apply this validation to the other AJAX-ey buttons exposed by this widget? Probably couldn't hurt.
Let's generate this array once at the top of the method and re-use it as needed. For readability.
We can condense these into one line:
$parents = array_slice($triggering_element['#array_parents'], 0, $length)
.We should probably check that $parents is of the correct length before the second array_slice() call, and throw LogicException if it's not.
We can remove the first argument to isset().
Out of curiosity, why do we need $field_state['items_count']? Can't we just call
count($field_state['items'])
as needed?I know this error message is not ever supposed to appear to the user, but I'm not certain we can count on that. Therefore, I'm not sure about the use of the phrase "This field", since we can't be entirely sure where the message may be printed. How about injecting the field label into the message?
We probably only need to update the field state if $media is not empty.
Let's just return this directly. Then we can remove the
$media = []
line, and return an empty array as the final line of the method.We don't need to keep $field_name and $parents in their own variables.
Can we get a little explanation about what is expected to be in $field_state?
Same here.
Comment #49
samuel.mortensonI spent the better part of today trying to use the "Embed" Views display plugin to avoid the issue with this widget view being accessible at a path, but ran into a ton of issues. Apparently there were good reasons I did the query-parameter-juggling in this patch, although it does look strange.
Going to push forward with the view as-is and move the CSRF token to a custom hash and see how that works.
Comment #50
samuel.mortensonAddressed #48:
1. Done.
2. Done - that text was leftover from the last one.
3. Done - made the decision to just add a todo to remove it, and marked is as internal.
4. Done - also fixed a runtime error when using the entity reference widget.
5. Done.
6. Added test coverage.
7. Done.
8. Done.
9. We don't enforce what this method returns, maybe you're thinking of getFieldState?
10. Yep!
11. Done.
12. Done.
13. Done, seems to work fine.
14. Done.
15. Interesting. Added.
16. Fixed.
17. Done.
18. This validation isn't applicable to the entire widget's selection, just the new selection from the library. It's worth noting that our validation in this widget is mostly done for UX reasons, WidgetBase will also do a lot of validation on form submit.
19. Done.
20. Done.
21. Done - I added a similar check to another method as well.
22. Done.
23. We don't, looks like! I only had this because WidgetBase has it, but we don't really need it for what we're doing since we handle multiple values.
24. I see what you're saying - I disagree that the field label needs to be added since the error is already shown on the field, but I improved the text and added the bundle labels to the other error per your earlier recommendation.
25. Done.
26. Done.
27. Done.
28. Added lots of documentation, in a format that I found in
\Drupal\Core\Field\FormatterPluginManager::getInstance
29. Done.
Still working on replacing the CSRF token checking.
Comment #51
samuel.mortensonSo I spent a lot of time thinking about access checking for the widget view and don't have any ideas I feel really good about. I'd love to hear input from other people about how (and how much) we should restrict access.
Here's my notes so far:
Can anyone think of a good way to only allow users access to the widget view if they have access to use the widget, in a way that would support anonymous users? Alternatively, could we just allow all users with the "View media" permission access to the view?
Comment #52
webchickWhat are the concerns around just using the "view media" permission? This seems consistent with how nodes, for example, are handled (anon users see all posts that are listed on the front page view, for example). As long as the media library view respects any access control modules that might be in play, I'm not sure why this would be a problem?
Comment #53
phenaproximaThere are no immediate concerns, so let's not bother with anything more advanced than that. I suggest we open a follow-up to investigate a more robust access check. Depending on how much concern there is about that, it could potentially be a stable blocker, but by no means should it block progress in this issue and getting something usable into 8.6. So, full steam ahead!
Comment #54
samuel.mortensonThat makes things easier! This patch removes the CSRF token check and adds an explicit test for anonymous users. I also tested using the widget as an anonymous user in Bartik and it worked fine, which is re-assuring.
I also created a follow up per #53: #2983179: [META] Implement stricter access checking for the media library
Comment #55
GrandmaGlassesRopeManSome minor points here,
Missing a trailing comma.
Missing a trailing comma.
Could we use
.closest()
?Lets be consistent and put each method on a new line.
Lets be consistent and put each method on a new line.
Comment #56
GrandmaGlassesRopeManFor #55.1 🤦♀️
Comment #57
samuel.mortensonAddressed #55
1. Fixed (and thanks for context).
2. Fixed.
3. No, not with the element structure the way it is.
4. Fixed.
5. Fixed.
Comment #58
phenaproximaWell, folks, after this review I'm pretty much out of things to nitpick.
Under the @internal, should we explicitly mention that this function may be removed in a minor release?
We can condense this to one line:
$create_bundles = array_filter($allowed_bundles, [$access_handler, 'createAccess'])
.We can remove the
if (!empty($create_bundles))
check, as it doesn't really add anything to this logic.Let's open a follow-up, and add a todo here, to look into not exposing this display as a page (possibly with a custom display plugin).
Can the selector be input.media-library-item__weight, just to be sure we're targeting the right kind of thing?
Same here.
And here.
This can be $checkboxes.not(':checked').
I wonder if we should open a follow-up against Views to allow each view to have its own AJAX path. Might be more trouble than it's worth, but at least we can raise the issue.
Let's wrap this in an isset($form['header']) check.
Let's open a follow-up to implement a custom Views filter plugin which can do this in a nicer way (i.e., expose an API to limit exposed filters in a contextual way).
Can we use the ::class format for EntityReferenceItem?
Nit: Should have a trailing comma.
Nit: We can pass these directly to getWidgetState(), we don't need to keep them in their own local variables.
We can lose the first condition; isset() will check it implicitly.
We don't need to call getFieldSetting(); this widget is hard-coded to work only with media items.
Can we use $this->t()?
Same here.
We should probably only return this if $ids is not empty, now that I think of it.
Should be array[], since it's an array of arrays.
Same here.
This logic could allow external forces to inject the value of the FormBuilderInterface::AJAX_FORM_REQUEST query parameter. I doubt it's a security hazard, but let's overwrite the value fully instead.
Nit: We can make this a beautiful chain:
return (new AjaxResponse())->addCommand()->addCommand()->addCommand()
.Can we do this without executing JavaScript? Won't
$assert_session->buttonExists('Apply Filters')->press()
do the same thing?Same here -- any way we can accomplish this without JavaScript?
Nit: "...for anonymous users".
I'd like to get official sign-off from the UX team in this issue, as well as official accessibility sign-off. Follow-ups should be opened for anything stable-blocking. Otherwise, I'm pretty close to being ready to RTBC this sucker.
Comment #59
samuel.mortensonAddressing #58:
1. Fixed.
2. Smart! Fixed.
3. Fixed.
4. Added a todo linking to #2983179: [META] Implement stricter access checking for the media library.
5. Done.
6. Done.
7. Done.
8. Done.
9. Opened #2983451: Query string that was used to build View not included in AJAX path, which accurately describes the problem we're working around here.
10. Done.
11. Opened #2983454: Extend the "bundle" Views filter plugin to be contextually aware of allowed bundles.
12. Done.
13. Done, although worth noting core doesn't always do this.
14. Done.
15. Fixed.
16. Done.
17. Yes, done.
18. Copy+paste! Fixed.
19. Done, although I don't think it will make much of a difference.
20. Done.
21. Done.
22. Fixed.
23. Huh, never thought of that. Changed.
24. Yes - this was pretty hard but I think I got everything working without jQuery.
25. See 24.
26. Fixed.
I also got tests passing on our only blocker, #2861860: View inside core modal doesn't refresh correctly when reopened, so a review and RTBC would be super helpful there, if anyone has time.
Comment #60
samuel.mortensonComment #61
chr.fritschResponding to #30.4
While testing this patch I also had the feeling that the "select all" would be nice. For example, if you extend the view with filtering for tags, then selecting all items could be useful. So I think it doesn't hurt if we display the checkbox.
Could we get some default height for the library? It looks a bit strange if it's empty.
Comment #62
phenaproximaI think this is a decision that should be deferred to the UX team.
Comment #63
samuel.mortensonRe-rolled #59 as it was no longer applying. Updated tests to work with the new test base.
Comment #64
samuel.mortensonThis patch tweaks CSS a bit to better support Firefox's handling of flex box, and adds a minimum height to address #61. I chose a minimum height based on what the exposed form and one row of items would be.
Comment #65
samuel.mortensonI recorded a new video since not a lot of people made the UX meeting on Tuesday: https://www.youtube.com/watch?v=ZCvW5BYlExg
Comment #66
samuel.mortensonI found an issue for the notice I was seeing locally: #2663316: Broken title in modal dialog when title is a render array. The latest patch seems to work as expected.
Comment #67
samuel.mortensonWhoops.
Comment #68
samuel.mortensonRe-roll, again!
Comment #69
phenaproximaI consider the code for this widget RTBC. Now we just need review and sign-off from everyone mentioned in the tags. I'll start pinging folks.
Comment #70
seanBJust started testing this. First off, this is looking really nice! Thanks samuel.mortenson. I only have some small usability related comments:
Comment #71
beautifulmindAbout #1 in above comment, how about using 'Browse media' or 'Browse Media'?
Comment #72
beautifulmindAbout #2 -- Apply Filters, how about adding a button in line of actual filter that reads 'Apply'?
About #3 -- Let there be only media, nothing else, no links to edit or actual media file
About #4 -- Remove the link
About #5 -- For videos, how about displaying a 'play' icon somewhere on the image, without allowing it to play actually?
Comment #73
samuel.mortensonThanks for the review @seanb!
Responding to #70:
1. When we add upload support to the library would "Select media" or "Browse media" still make sense?
2. This has been brought up in UX review but I'm not sure how to accomplish this - all buttons within the
.form-actions
element get moved to the button pane byDrupal.dialog.prepareDialogButtons
. We would need to add new functionality to the dialog system, or our own aggressive CSS/JS to make this work. If we want to fix this in core (add an "ignore" feature?) we should open a follow up.3. I think there is a use case for wanting to edit media referenced by a node, without having to browse to the library in another tab, find it, and click the name there. I'd defer this to the UX team.
4. Moving the edit link to a button that edits in a modal would replicate Entity Browser's current functionality, but it seems like a lot for the widget to handle. I'd again ask the UX team to see what behavior they expect.
5. This will need to be discussed in another issue, but the UX team decided that including the type made the library look cluttered. The items in the library use a new view mode and template, so a theme could implement some custom styling to make types look distinct without just including the type name above the media item name.
Comment #74
seanB1. Maybe not, but if you are able to add media we can change the name of the button back later. The widget would change a bit anyway when we add that functionality? Might be good to also ask the UX team about this.
2. Having a follow up to fix the core functionality seems to make sense. I don't think this is a blocker.
3. I will ask the UX team what they think about removing the link.
4. Same as 3.
5. Let's have a follow up to talk about this separately.
Comment #75
seanBWe just talked about this issue in the UX meeting. We decided on the following regarding my feedback in #70:
To sum it up for this patch:
I think we can remove some of the issue tags as well, but leaving those for now.
Comment #76
jrockowitz CreditAttribution: jrockowitz as a volunteer and at The Big Blue House commentedI feel that #70.2 is going to confuse a lot of users. Applying a temporary and quick fix/workaround might be worth considering.
The 'Filter' button is being moved into the dialog's buttons via dialog.ajax.js
The attached patch removes the
.form-actions
class from the views exposed filter form and replaces it with.media-library-view--form-actions
class. This patch fixes the immediate issue with the media library but we should open up a dedicated ticket to figure out a better long-term solution.Comment #77
samuel.mortensonI changed the button label to "Browse media", and disabled the links within the widget temporarily until we resolve the new follow up: #2985168: [PP-1] Allow media items to be edited in a modal when using the field widget
I didn't open a follow up for showing the Media type in the view, I think it's less work to have the UX team on a call to decide this and then open an issue if they think it's needed. I'll bring this up next Tuesday.
Comment #78
samuel.mortensonComment #81
samuel.mortensonDidn't know tests were failing as of #76, fixed.
Comment #82
lauriiiReviewed the JS and the CSS. I would like to review the ajax code before removing the tag which I didn't have a chance to do yet.
Should we just use
.media-library-item--disabled
since this is not proper usage of SMACSS state classes?Should we rename this to
.media-library-widget__toggle-weight
since this actually depends being child of.media-library-widget
?We should use
.js-
prefixed class names when attaching JS to HTML elements to make sure that the frontend developers are aware of this. While doing this, we should also remove the HTML elements from the selectors.Comment #83
lauriiiThanks @samuel.mortenson for pointing out the conflicting feedback from @phenaproxima on #58.5. Generally we don't add the element type on the selectors because it is hard to predict selectors like that when making changes to markup. For the same reason we use
.js-
prefixed classes for targeting elements so that it's clear there's some logic attached to an element, and that it has to be taken into account when making modifications to the markup.Comment #84
samuel.mortensonAddressed #82 - ready for review again.
Comment #85
lauriiiThis one is still missing a
.js
prefix.The
js-
prefixed classes doesn't have to follow BEM because generally there shouldn't be CSS attached to them.Comment #86
samuel.mortensonAddressed #85 - we were using BEM style classes for the click to select logic so I switched that over to, so we stay consistent before alpha.
Comment #87
phenaproximaWe are 24 hours from 8.6.0 alpha. It is the 11th hour. And it's time for this to go into Drupal. I am removing the remaining accessibility and usability review tags.
Why?
Speaking as a Media subsystem maintainer: it's magic time. Let's do this. RTBC once green on all backends.
Comment #88
phenaproximaComment #89
plachWe are now in pre-alpha commit freeze, so I think it's unlikely that this will be part of 8.6 alpha, however, according to https://www.drupal.org/core/d8-allowed-changes#alpha, exceptions are still possible after alpha is tagged:
I would be surprised if this weren't considered a priority.
Comment #90
phenaproximaUh-oh...looks like we have a failure on Postgres. Kicking back for that.
Comment #91
samuel.mortensonTested locally and fixed the failures - we weren't explicitly setting created times which led to media having the same timestamp, which caused inconsistent sorting in the view.
Comment #92
phenaproximaNice detective work! Thanks, @samuel.mortenson. Restoring RTBC once green on all backends.
Comment #95
phenaproximaD'oh!
Comment #96
phenaproximaAh, doesn't actually need a reroll; just need to change all testing to 8.7.x since the Media Library module has been (temporarily) removed from 8.6.x.
Comment #97
phenaproximaRestoring RTBC once all backends are green on 8.7.x.
Comment #98
plachOverall this is looking very good and a much needed improvement, great work!
I found a few issues while reviewing and manually testing this, however I don't think any of these needs to be a blocker, as this is experimental code and the functionality is complete and working very well for the common use cases, so it should serve well for evaluation and beta testing.
Testing issues:
Code review:
Are we sure this URL will never contain a query string already? I guess it would be safer to parse it and add the ML parameters rather than just appending them.
Isn't this is missing a
[]
operator?These comments do not wrap at column 80.
Can we move this line inside the if branch? It's used only there.
Can we use
$query->addWhere()
? It should make the code simpler and easier to read.This is the main issue that caught my eye while reviewing the code: we are filtering the view via user input, but this can be easily altered to produce unwanted results: I confirmed this is the case while testing with JS disabled. It would feel way less fragile if we passed the field name and retrieved the allowed bundles from that.
I think it's ok for now to mark this as @internal, but in the future it would be nice to allow contrib to subclass the widget.
Isn't there any reusable code for the weight toggle?
The exception message could contain the actual/wrong data to make the issue easier to troubleshoot.
Can we inject these services?
At first sight I was a bit surprised we are checking "view" access, OTOH we don't have a permission for selection AFAIK and I guess view is the closest operation I can think of, so this should be ok.
It would be nice to add a comment explaining why we need the
media:
prefix, given that we are discarding it later.Can we inject this?
Same as above: the exception message could contain the actual/wrong data to make the issue easier to troubleshoot.
Why do we need a dedicated method for this?
Comment #99
samuel.mortensonThanks for the review @plach! I rolled a new patch to fix the easy-to-solve problems you pointed out.
Addressing #98:
I fixed the testing issue where you ran into a notice, and opened #2987675: The media library field widget should work without Javascript to work on progressive enhancement support.
1. To be safe I now parse the URL and merge in query parameters instead of doing an append.
2. Fixed.
3. Fixed. Also ran PHPCBF to fix some standards problems with the test class.
4. Done.
5. Done.
6. I think we can address this in #2983179: [META] Implement stricter access checking for the media library. Added a comment there.
7. Agreed.
8. The markup for the weight toggle is tabledrag-specific and is generated in Javascript.
9. Fixed.
10. We can't this is a static method.
11. Since any media ID can be given to the field widget, I wanted to be extra-sure that we don't bypass access.
12. I went ahead and removed the "media:" prefix for now since this code isn't meant to be re-used (yet).
13. Same as 10 - this is a static method.
14. Fixed.
15. We don't - I was following a pattern in the bulk form but we don't need to do that here. Moved.
Leaving in RTBC since no logic has changed in the new patch.
Comment #100
plachLooks good, thanks!
(sorry for missing the static methods :)
Comment #101
plachSaving credits
Comment #103
plachCommitted de52834 and pushed to 8.7.x. Awesome work, guys!
I (auto)fixed a few issues
stylelint
was complaining about on commit:Here's the diff:
Comment #104
plachDiscussed this with @webchick, @catch and @Gábor Hojtsy: we are fine with backporting this to 8.6.x, since it's experimental code, involves no data changes, and this commit will allow to mark "media_library" as beta.
Given the huge value this would provide, we hope the Media team will be able to have #2938116: Allow media to be uploaded with the Media Library field widget RTBC before beta. Also that change would still be considered allowed per our policy.
Comment #105
plachComment #107
plachAnd this is backported now!
Comment #108
catchUntagging for release notes since this doesn't have upgrade path implications and fits more into highlights.
Comment #109
geek-merlin