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.

CommentFileSizeAuthor
#99 interdiff-2962525-91-99.txt10.42 KBsamuel.mortenson
#99 2962525-99.patch81.79 KBsamuel.mortenson
#98 Media_settings_for_Article___Drupal_8_x.png597.42 KBplach
#91 interdiff-2962525-86-91.txt1.68 KBsamuel.mortenson
#91 2962525-91.patch81.72 KBsamuel.mortenson
#86 2962525-86.patch81.66 KBsamuel.mortenson
#86 interdiff-2962525-84-86.txt15.35 KBsamuel.mortenson
#84 interdiff-2962525-81-84.txt12.16 KBsamuel.mortenson
#84 2962525-84.patch74.6 KBsamuel.mortenson
#81 interdiff-2962525-78-81.txt1.2 KBsamuel.mortenson
#81 2962525-81.patch73.76 KBsamuel.mortenson
#78 interdiff-2962525-76-77.txt1.95 KBsamuel.mortenson
#78 2962525-77.patch73.84 KBsamuel.mortenson
#76 2962525-76.patch73.38 KBjrockowitz
#76 interdiff-2962525-68-76.txt2.42 KBjrockowitz
#76 media-library-filter.png86.81 KBjrockowitz
#70 library-review-4.png546.89 KBseanB
#70 library-review-3.png1.11 MBseanB
#70 library-review-2.png1.11 MBseanB
#70 library-review-1.png53.1 KBseanB
#68 2962525-68.patch72.14 KBsamuel.mortenson
#64 interdiff-2962525-63-64.txt853 bytessamuel.mortenson
#64 2962525-64.patch72.14 KBsamuel.mortenson
#63 2962525-63.patch71.99 KBsamuel.mortenson
#61 empty_library.png138.1 KBchr.fritsch
#59 interdiff-2962525-57-59.txt19.42 KBsamuel.mortenson
#59 2962525-59.patch71.05 KBsamuel.mortenson
#57 interdiff-2962525-54-57.txt1.58 KBsamuel.mortenson
#57 2962525-57.patch70.39 KBsamuel.mortenson
#56 here.png160.88 KBGrandmaGlassesRopeMan
#54 interdiff-2962525-50-54.txt3.8 KBsamuel.mortenson
#54 2962525-54.patch70.36 KBsamuel.mortenson
#50 interdiff-2962525-47-50.txt17.6 KBsamuel.mortenson
#50 2962525-50.patch69.47 KBsamuel.mortenson
#47 interdiff-2962525-34-47.txt13.32 KBsamuel.mortenson
#47 2962525-47.patch68 KBsamuel.mortenson
#47 media_library_widget_47.png261.81 KBsamuel.mortenson
#46 media-card.png81.41 KBckrina
#34 2962525-34.patch63.1 KBsamuel.mortenson
#34 interdiff-2962525-30-34.txt13.61 KBsamuel.mortenson
#30 2962525-30.patch63.04 KBsamuel.mortenson
#22 interdiff-2962525-21-22.txt4.32 KBsamuel.mortenson
#22 2962525-22.patch56.62 KBsamuel.mortenson
#22 2962525-combined-22.patch102.93 KBsamuel.mortenson
#21 interdiff-2962525-18-21.txt1.98 KBsamuel.mortenson
#21 2962525-21.patch56.29 KBsamuel.mortenson
#21 2962525-combined-21.patch102.38 KBsamuel.mortenson
#18 interdiff-2962525-17-18.txt27.41 KBsamuel.mortenson
#18 2962525-18.patch55.29 KBsamuel.mortenson
#18 2962525-combined-18.patch101.08 KBsamuel.mortenson
#17 media-library-widget-ux-work.mp41.69 MBsamuel.mortenson
#17 interdiff-2962525-16-17.txt19.3 KBsamuel.mortenson
#17 2962525-17.patch40.48 KBsamuel.mortenson
#17 2962525-combined-17.patch88.74 KBsamuel.mortenson
#16 interdiff-2962525-12-16.txt12.13 KBsamuel.mortenson
#16 2962525-16.patch36.15 KBsamuel.mortenson
#16 2962525-combined-16.patch85.04 KBsamuel.mortenson
#12 interdiff-2962525-10-12.txt1.07 KBsamuel.mortenson
#12 2962525-12.patch35.47 KBsamuel.mortenson
#12 2962525-combined-12.patch80.9 KBsamuel.mortenson
#11 2962525-combined-10.patch80.11 KBsamuel.mortenson
#11 2962525-10.patch35.09 KBsamuel.mortenson
#3 2962525-3.patch33.36 KBsamuel.mortenson
#3 2962525-combined-3.patch79.23 KBsamuel.mortenson
#2 2962525-2.patch31.81 KBsamuel.mortenson
#2 2962525-combined-2.patch80.54 KBsamuel.mortenson
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

samuel.mortenson created an issue. See original summary.

samuel.mortenson’s picture

Status: Active » Postponed
Issue tags: +Needs tests, +Needs accessibility review
FileSize
80.54 KB
31.81 KB

Here'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:

  1. Needs test coverage.
  2. The View allows you to select media that aren't of an acceptable type (based on handler settings), and lets you go over the cardinality limit. Can we do some JS or Views magic here to make the UX better?
  3. If you open the library twice in the same page load, contextual filters do not work. This should be a separate core issue, but I would like some help figuring out why it's happening.
  4. If you click outgoing links (namely the link to the media item) in the library or in the widget, you could lose edits on the form. We should either prevent this completely, or show a dialog message.
  5. We should make the View configurable - users may want to provide their own or extend this widget.
  6. The remove button and weight are accessible, which is good, but more review would be nice.
samuel.mortenson’s picture

FileSize
79.23 KB
33.36 KB

Here's a re-roll for the styling changes from the main issue.

samuel.mortenson’s picture

phenaproxima’s picture

@samuel.mortenson demoed this patch in the UX meeting today. Here's the feedback we got:

  • For the most part, it works very well ("it does what you expect it to do") and satisfied accessibility requirements. Everyone really liked it. Great work!
  • The weight fields are not labeled specifically enough for accessibility purposes. Their labels should include a visually hidden span tag with the name of the item being weighted. For example: <label>Weight<span class="visually-hidden"> for $title_of_item</span></label>/li>
  • The media library, when opened in the modal dialog, is affected by #2861860: View inside core modal doesn't refresh correctly when reopened. I don't think that issue needs to block this one, but it's likely a blocker for the media library module (as a whole) to be considered stable.
  • In the dialog, the "Apply filters" button is...techy. Could we use better wording? I'm not sure what would be better; this likely needs UX input and can be tackled in a follow-up.
  • Is it possible to move the "Apply filters" button to be inline with the filters? It is jarring to put it at the bottom of the dialog box, since those actions are placed in a way which communicates that clicking any of them will finish the dialog. If this is easy, let's do it now; otherwise, we can open a follow-up.
phenaproxima’s picture

andrewmacpherson’s picture

The remove button and weight are accessible, which is good, but more review would be nice.

We 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:

  1. An invisible span inside the label to clarify what media item the label was for, e.g. <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.
  2. Use Form API #description so the input gets an aria-describedby relationship. You could put a .visually-hidden class on the description. This option would put the information in the "accessible description" of the input, which isn't guaranteed to be announced by all screen readers, say.

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.

samuel.mortenson’s picture

There'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.

samuel.mortenson’s picture

samuel.mortenson’s picture

This 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. 😢

samuel.mortenson’s picture

FileSize
35.09 KB
80.11 KB
samuel.mortenson’s picture

FileSize
80.9 KB
35.47 KB
1.07 KB

After 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.

phenaproxima’s picture

Status: Postponed » Needs work

Looking pretty good :)

  1. +++ b/core/modules/media_library/config/install/views.view.media_library.yml
    @@ -416,6 +417,166 @@
    +      path: admin/content/media-widget
    

    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.

  2. +++ b/core/modules/media_library/media_library.libraries.yml
    @@ -23,0 +24,10 @@
    +  dependencies:
    +    - core/drupal
    +    - core/jquery
    +    - core/jquery.ui.sortable
    

    jquery.ui.sortable already depends on jquery, so we can remove the redundant dependency.

  3. +++ b/core/modules/media_library/media_library.module
    @@ -41,6 +41,11 @@
    +    // Ensure that the widget ID passed to the modal persists across rebuilds.
    +    $request = \Drupal::request();
    +    if ($request->query->has('media_library_widget_id')) {
    +      $output['#attached']['drupalSettings']['views']['ajax_path'] .= '?media_library_widget_id=' . $request->query->get('media_library_widget_id');
    +    }
    

    Can we add a comment linking to the related issues/blockers?

  4. +++ b/core/modules/media_library/js/media_library.widget.es6.js
    @@ -0,0 +1,62 @@
    + * @file media_library.widget.js
    

    We'll need JavaScript review on this file, and it couldn't hurt to run it through eslint as well.

  5. +++ b/core/modules/media_library/js/media_library.widget.es6.js
    @@ -0,0 +1,62 @@
    +  Drupal.AjaxCommands.prototype.addMediaToWidget = function (ajax, response, status) {
    

    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.

  6. +++ b/core/modules/media_library/media_library.views.inc
    @@ -0,0 +1,22 @@
    +  $data['media']['media_library_select_form'] = [
    +    'title' => t('Select media'),
    +    'help' => t('Provides a field for selecting media entities in our media library view'),
    +    'real field' => 'mid',
    +    'field' => [
    +      'id' => 'media_library_select_form',
    +    ],
    

    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?

  7. +++ b/core/modules/media_library/src/Ajax/AddMediaToWidget.php
    @@ -0,0 +1,61 @@
    +class AddMediaToWidget implements CommandInterface {
    

    We can remove this entire class if we use InvokeCommand as suggested previously.

  8. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,443 @@
    + *   description = @Translation("Allows you to re-use Media Entities."),
    

    Nit: This can be "media entities".

  9. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,443 @@
    +    $build = parent::form($items, $form, $form_state, $get_delta);
    +
    +    return $build;
    

    Nit: Let's just return parent::form().

  10. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,443 @@
    +        '#markup' => $this->t('<p>No media selected.</p>'),
    

    Can we say "No media items selected"?

  11. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,443 @@
    +        '#value' => $this->t('Toggle media item weights'),
    

    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?

  12. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,443 @@
    +              '#media_library_remove_delta' => $delta,
    

    Could we extract this delta from the triggering element's #parents? If not, no big deal; just curious.

  13. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,443 @@
    +    // @todo Contextual filters only work the first time the modal is loaded.
    +    // Investigate and file an issue for Views if possible.
    

    If there is already an issue, let's link to it here.

  14. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,443 @@
    +    $element['media_library_open_container'] = [
    

    Why do we need a container here? I feel like we could use #theme_wrappers on the button itself for that.

  15. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,443 @@
    +        // @todo Make the View configurable.
    +        '#url' => Url::fromRoute('view.media_library.widget', [], [
    +          'query' => ['media_library_widget_id' => $field_name . $id_suffix],
    +        ]),
    

    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.

  16. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,443 @@
    +          'data-dialog-options' => json_encode([
    +            'dialogClass' => 'media-library-widget-modal',
    +            'height' => '75%',
    +            'width' => '75%',
    +            'title' => $this->t('Media library'),
    +          ]),
    

    We should use \Drupal\Component\Serialization\Json::encode().

  17. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,443 @@
    +        $form_state->setError($element, t('More items have been selected than this field allows (max @count).', [
    +          '@count' => $element['#cardinality'],
    +        ]));
    

    We can phrase this more lucidly, I think, using the plural: "This field cannot accept more than @count item(s)."

  18. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,443 @@
    +        if ($element['#target_bundles'] && !in_array($media_item->bundle(), $element['#target_bundles'], TRUE)) {
    +          $form_state->setError($element, t('The media item "@label" is not of an accepted type.', [
    +            '@label' => $media_item->label(),
    +          ]));
    +        }
    

    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."

  19. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,443 @@
    +  public static function getNewMediaItems(array $element, FormStateInterface $form_state) {
    

    Does this need to be public? We should avoid adding API surface if possible.

  20. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,443 @@
    +  public static function getFieldState(array $element, FormStateInterface $form_state) {
    

    Same here -- not sure this needs to be public.

  21. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,443 @@
    +  public static function setFieldState(array $element, FormStateInterface $form_state, array $field_state) {
    

    Here too.

  22. +++ b/core/modules/media_library/src/Plugin/views/field/MediaLibrarySelectForm.php
    @@ -0,0 +1,129 @@
    +    // Only add the bulk form options and buttons if there are results.
    +    if (!empty($this->view->result)) {
    

    Lets just return early if $this->view->result is empty, so we don't have to indent the entire function.

  23. +++ b/core/modules/media_library/src/Plugin/views/field/MediaLibrarySelectForm.php
    @@ -0,0 +1,129 @@
    +        $form[$this->options['id']][$row_index] = [
    +          '#type' => 'checkbox',
    +          '#title' => $this->t('Select this item'),
    

    For accessibility, can we improve the label here? Something like 'Select "$item_label"' would do the trick.

  24. +++ b/core/modules/media_library/src/Plugin/views/field/MediaLibrarySelectForm.php
    @@ -0,0 +1,129 @@
    +      $form['actions']['submit']['#ajax'] = [
    +        'url' => Url::fromUserInput($url),
    +        'options' => [
    +          'query' => \Drupal::request()->query->all() + [
    +            FormBuilderInterface::AJAX_FORM_REQUEST => TRUE,
    +          ],
    +        ],
    +        'callback' => [static::class, 'updateWidget'],
    +      ];
    

    Can there be a comment explaining this?

phenaproxima’s picture

Tagging for JavaScript review.

phenaproxima’s picture

Issue tags: +Needs followup

Tagging for a follow-up issue per #13.15.

samuel.mortenson’s picture

Status: Needs work » Postponed
Related issues: +#2971209: Allow the MediaLibraryUiBuilder service to use an alternative view display
FileSize
85.04 KB
36.15 KB
12.13 KB

Addressing #13:

  1. We have to expose a path so that an AJAX request can be made to that path. An alternative would be to use the "Embed" display and write our own controller that returns the widget, but this seemed much easier and both expose a path.
  2. Done.
  3. There are no related issues or blockers for this... I know it's weird but we need a specific functionality to persist a query parameter that was originally passed when building the view for all subsequent view rebuilds.
  4. Will wait for review, and will look into core eslint.
  5. Having a new command seems verbose for this issue - but we plan to add additional selection methods (bulk upload, individual add form, maybe a quick OEmbed form), and the command means we won't have to copy+paste code.
  6. I'm not sure about renaming just the machine name for this, since it has specific Media library code in it. Maybe we need one big follow up that says "Make the selection method that Media library uses re-usable for other widgets"? There are a lot of co-related features here.
  7. See above comment about the command.
  8. Done.
  9. Done.
  10. Done.
  11. This was a little complex, but I did it. Thanks for the tip. Will wait for accessibility review to determine needed aria attributes.
  12. Done! Thanks for the tip.
  13. Linked the issue.
  14. I dunno why there's a container here, hah! I removed it.
  15. I opted to open a follow up and add a @todo instead - may as well have the form and configuration in one go to reduce patch complexity.
  16. Done.
  17. Done, good call.
  18. There is a UX problem here with allowing invalid selections - and I think it should block commit of this patch. I mention this in comment #2. We can do something with JS in the view that makes selecting invalid media impossible.
  19. It's a static method, and must be public.
  20. It's a static method, and must be public.
  21. Done.
  22. I improved the label per your suggestion.
  23. Added a verbose comment explaining what in Sam Hill is going on here.

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! ;-)

samuel.mortenson’s picture

FileSize
88.74 KB
40.48 KB
19.3 KB
1.69 MB

Here's a new patch with these changes:

  1. The AJAX command has been removed per @phenaproxima's request. We'll add it back when we have new selection methods for the widget.
  2. The Javascript has been updated to match the changes @drpal made in the main issue (destructuring, arrow functions, etc.)
  3. The View query is now altered based on what types are selectable (!!). This means that users only see media items that are allowed by the field in the view. You can see me demo this in the attached video - I allow Audio and Image media types, and those are the only ones that show up in the view. This is going to prevent a ton of user headaches, because previously you would have to select something invalid, then see an error, then try again.
  4. When selecting items in the View, Javascript now prevents you from selecting more items that the field allows. In the demo you'll see that the cardinality of the field is two, so when two items are selected other items are disabled. Disabling the media item is done with CSS, and the input also has the disabled property added for accessibility. This is going to make selection a lot more straightforward for users, in my opinion.
  5. The description text of the field is now extended with information about the cardinality. This is important for knowing why more items cannot be added, and how many items are left. You can see this in the demo video as well.
samuel.mortenson’s picture

Issue tags: -Needs tests
FileSize
101.08 KB
55.29 KB
27.41 KB

Here's a new patch with the following changes:

  1. Test coverage added for everything in the issue! Woo!
  2. Invalid Views configuration (added in a bad patch re-roll I think) remove
  3. Fixed a bug where cardinality settings would leak from one widget to another if two are on the same page
  4. Made it so that the Media library widget is the default for all new Media fields, which makes testing easier
  5. Cleaned up JS
  6. Reduced conditional nesting levels in the widget by using #access

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?

phenaproxima’s picture

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.

This is a question that should be addressed by the UX team, IMHO.

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?

And this should be answered by a front-end framework manager.

lauriii’s picture

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?

Theme 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.

samuel.mortenson’s picture

FileSize
102.38 KB
56.29 KB
1.98 KB

Thanks @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.

samuel.mortenson’s picture

Status: Postponed » Needs review
FileSize
102.93 KB
56.62 KB
4.32 KB

Re-roll for the CSS file split from the main issue.

Status: Needs review » Needs work

The last submitted patch, 22: 2962525-22.patch, failed testing. View results

xjm’s picture

We discussed that we can make adding oEmbed media in the widget something that we discuss in a followup.

phenaproxima’s picture

Title: [PP-1] Create a field widget for the Media library module » Create a field widget for the Media library module
Priority: Normal » Major

The media library has been committed, so let's put booster rockets on this thing.

phenaproxima’s picture

Issue tags: +Needs reroll

Marking for a re-roll.

phenaproxima’s picture

  1. +++ b/core/modules/media_library/config/install/views.view.media_library.yml
    @@ -498,0 +458,127 @@
    +      path: admin/content/media-widget
    

    It'd be nice to not expose this display as a page, but we can deal with that in a follow-up.

  2. +++ b/core/modules/media_library/config/install/views.view.media_library.yml
    @@ -498,0 +458,127 @@
    +          element_class: media-library-item__content
    

    Is there an existing class name we could re-use?

  3. +++ b/core/modules/media_library/config/install/views.view.media_library.yml
    @@ -498,0 +458,127 @@
    +      access:
    +        type: perm
    +        options:
    +          perm: 'view media'
    

    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.

  4. +++ b/core/modules/media_library/css/media_library.module.css
    @@ -47,6 +48,22 @@
    +.media-library-view.view-display-id-widget .media-library-select-all {
    +  display: none;
    +}
    

    I'm not sure if this would annoy the UX team, but why shouldn't we expose the ability to select all items?

  5. +++ b/core/modules/media_library/css/media_library.module.css
    @@ -47,6 +48,22 @@
    +.media-library-selection .media-library-item::after {
    +  display: none;
    +}
    

    What does this do?

  6. +++ b/core/modules/media_library/css/media_library.theme.css
    @@ -19,7 +19,53 @@
    +.media-library-item .media-library-item-remove:hover,
    +.media-library-item .media-library-item-remove:focus,
    +.media-library-item .media-library-item-remove.button:disabled {
    

    Not a CSS expert, but .media-library-item-remove seems like it should be renamed to media-library-item__remove, for BEM conformance.

  7. +++ b/core/modules/media_library/css/media_library.theme.css
    @@ -19,7 +19,53 @@
    +  background: url('../../../misc/icons/787878/ex.svg') #ffffff center no-repeat;
    

    Nit: Could use #fff.

  8. +++ b/core/modules/media_library/css/media_library.theme.css
    @@ -19,7 +19,53 @@
    +.media-library-item .media-library-item-remove {
    +  position: absolute;
    +  z-index: 1;
    +  top: 0;
    +  right: 0;
    +  width: 24px;
    +  height: 24px;
    +  margin: 5px;
    +  padding: 0;
    +  background: url('../../../misc/icons/787878/ex.svg') #ffffff center no-repeat;
    +  background-size: 16px 16px;
    +  border: 2px solid #ccc;
    +  border-radius: 20px;
    +  color: transparent;
    +  text-shadow: none;
    +  transition: .2s border-color;
    

    Let's group this one with the :hover and :focus pseudo-selectors.

  9. +++ b/core/modules/media_library/media_library.module
    @@ -43,6 +44,21 @@
    +    if ($view->current_display === 'widget') {
    

    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?

  10. +++ b/core/modules/media_library/media_library.module
    @@ -43,6 +44,21 @@
    +      if (!empty($query)) {
    +        $output['#attached']['drupalSettings']['views']['ajax_path'] .= '?' . http_build_query($query);
    +        if (isset($query['media_library_remaining'])) {
    +          $output['#attached']['drupalSettings']['media_library']['selection_remaining'] = $query['media_library_remaining'];
    +        }
    +      }
    

    This could use a comment or two.

  11. +++ b/core/modules/media_library/media_library.module
    @@ -90,0 +107,62 @@
    + * Alters the widget view's query to only show media that can be selected.
    

    Can we expand on what "can be selected" means? By what criteria are we determining selectability?

  12. +++ b/core/modules/media_library/media_library.module
    @@ -90,0 +107,62 @@
    +  if ($view->id() == 'media_library' && $view->current_display === 'widget') {
    

    The first condition needs ===.

  13. +++ b/core/modules/media_library/media_library.module
    @@ -90,0 +107,62 @@
    +    $types = \Drupal::request()->query->get('media_library_allowed_types');
    +    if ($types && is_array($types)) {
    

    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.

  14. +++ b/core/modules/media_library/media_library.module
    @@ -90,0 +107,62 @@
    +          'field' => 'media_field_data.bundle',
    

    Is there any way we can avoid hard-coding the name of the table?

  15. +++ b/core/modules/media_library/media_library.module
    @@ -90,0 +107,62 @@
    +          'operator' => 'in',
    

    Should 'in' be all-caps?

  16. +++ b/core/modules/media_library/media_library.module
    @@ -90,0 +107,62 @@
    + * filter by a type that is not un-selectable.
    

    Not sure that "not" should be here.

  17. +++ b/core/modules/media_library/media_library.module
    @@ -90,0 +107,62 @@
    +  /** @var \Drupal\Core\Field\FieldTypePluginManager $field_type_manager */
    +  $field_type_manager = \Drupal::service('plugin.manager.field.field_type');
    +  $class = $field_type_manager->getPluginClass($field_type);
    

    We don't need to keep $field_type_manager in its own variable.

  18. +++ b/core/modules/media_library/tests/modules/media_library_test/media_library_test.info.yml
    @@ -9,2 +9,3 @@
    +  - drupaL:node
    

    drupaL! Sounds like a fork =P

  19. +++ b/core/modules/media_library/tests/src/FunctionalJavascript/MediaLibraryTest.php
    @@ -92,11 +99,104 @@
    +    $this->getSession()->executeScript("jQuery('$checkbox_selector:eq(3)').click()");
    +    $this->getSession()->executeScript("jQuery('$checkbox_selector:eq(4)').click()");
    +    $this->getSession()->executeScript("jQuery('$checkbox_selector:eq(5)').click()");
    +    $this->getSession()->executeScript('jQuery(\'button:contains("Select media")\').click()');
    

    Let's keep $session in its own variable to avoid calling getSession() repeatedly.

GrandmaGlassesRopeMan’s picture

  1. +++ b/core/modules/media_library/js/media_library.widget.es6.js
    @@ -0,0 +1,93 @@
    +        }
    

    Missing trailing comma.

  2. +++ b/core/modules/media_library/js/media_library.widget.es6.js
    @@ -0,0 +1,93 @@
    +        'show': Drupal.t('Show media item weights'),
    +        'hide': Drupal.t('Hide media item weights')
    

    These properties don't need to be quoted. Additionally, probably don't need to be in an Object since they are used infrequently.

  3. +++ b/core/modules/media_library/js/media_library.widget.es6.js
    @@ -0,0 +1,93 @@
    +            .find('.media-library-item-weight').parent().toggle();
    

    Both `.parent()` and `.toggle()` should be on their own line.

  4. +++ b/core/modules/media_library/js/media_library.widget.es6.js
    @@ -0,0 +1,93 @@
    +              const $view = $(currentTarget).closest('.media-library-view');
    

    `$view` is already declared in the previous scope.

  5. +++ b/core/modules/media_library/js/media_library.widget.es6.js
    @@ -0,0 +1,93 @@
    +              if ($checkboxes.filter(':checked').length == settings.media_library.selection_remaining) {
    

    Use triple-equals.

phenaproxima’s picture

  1. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,470 @@
    +class MediaLibraryWidget extends WidgetBase implements ContainerFactoryPluginInterface {
    

    Let's mark this explicitly @internal, as all plugin classes are.

  2. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,470 @@
    +  public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
    

    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.

  3. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,470 @@
    +            '#limit_validation_errors' => [array_merge($form['#parents'], [$field_name])],
    

    #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.

  4. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,470 @@
    +        'weight' => [
    +          '#type' => 'textfield',
    +          '#title' => $this->t('Weight'),
    +          '#default_value' => $delta,
    +          '#attributes' => [
    +            'class' => ['media-library-item-weight'],
    +          ],
    +        ],
    

    Should this element's #type be 'number', since it must be a number?

  5. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,470 @@
    +    // Inform the user of how many items are remaining.
    +    if (!$cardinality_unlimited) {
    +      if ($element['#cardinality'] === 1 && $remaining === 1) {
    +        $cardinality_message = 'You can select one media item.';
    +      }
    +      elseif ($remaining) {
    +        $cardinality_message = 'You can select up to @maximum media (@remaining left).';
    +      }
    +      else {
    +        $cardinality_message = 'The maximum number of media has been selected.';
    +      }
    +      $element['#description'] .= '<br />' . $this->t($cardinality_message, [
    +        '@maximum' => $element['#cardinality'],
    +        '@remaining' => $remaining,
    +      ]);
    +    }
    

    I love this.

  6. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,470 @@
    +          'media_library_remaining' => $cardinality_unlimited ? -1 : $remaining,
    

    We should use the CARDINALITY_UNLIMITED constant, rather than hard-code -1.

  7. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,470 @@
    +      '#limit_validation_errors' => [array_merge($parents, [$field_name])],
    

    A comment would be good here, since #limit_validation_errors is so opaque.

  8. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,470 @@
    +        'data-media-library-widget-value' => $field_name . $id_suffix,
    

    A comment would be useful here.

  9. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,470 @@
    +    $element['media_library_update_widget'] = [
    +      '#type' => 'submit',
    +      '#value' => $this->t('Update widget'),
    +      '#name' => $field_name . '-media-library-update' . $id_suffix,
    +      '#ajax' => [
    +        'callback' => [static::class, 'updateWidget'],
    +        'wrapper' => $wrapper_id,
    +      ],
    +      '#attributes' => [
    +        'data-media-library-widget-update' => $field_name . $id_suffix,
    +        'class' => ['visually-hidden', 'media-library-update'],
    +      ],
    +      '#validate' => [[static::class, 'validateItems']],
    +      '#submit' => [[static::class, 'updateItems']],
    +      '#limit_validation_errors' => [array_merge($parents, [$field_name])],
    +    ];
    

    What does this do? A comment would be useful here as well.

  10. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,470 @@
    +    $length = end($triggering_element['#parents']) === 'remove_button' ? -4 : -1;
    

    Can we get a comment here?

  11. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,470 @@
    +    if (isset($values['selection'])) {
    +      if (isset($values['selection'][$delta])) {
    +        array_splice($values['selection'], $delta, 1);
    +        $field_state['items'] = $values['selection'];
    +        $field_state['items_count'] = count($field_state['items']);
    +      }
    +    }
    

    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.

  12. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,470 @@
    +    $field_state = self::getFieldState($element, $form_state);
    +    $media = self::getNewMediaItems($element, $form_state);
    

    Why self instead of static here?

  13. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,470 @@
    +      $form_state->setError($element, t('This field cannot accept more than @count item(s).', [
    +        '@count' => $element['#cardinality'],
    +      ]));
    

    I think we'll need a PluralMarkup here.

  14. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,470 @@
    +        $form_state->setError($element, t('The media item "@label" is not of an accepted type. Allowed types: @types', [
    +          '@label' => $media_item->label(),
    +          '@types' => implode(', ', $element['#target_bundles']),
    +        ]));
    

    "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.

  15. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,470 @@
    +   * Updates the field state based on the form state and sets the form rebuild.
    

    Should be "...and flags the form to be rebuilt."

  16. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,470 @@
    +    $field_state = self::getFieldState($element, $form_state);
    +
    +    $media = self::getNewMediaItems($element, $form_state);
    

    Should self be static?

  17. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,470 @@
    +      if ($media && $media_item->access('view')) {
    

    Why is the ->access() check being done here? Needs a comment, I think.

  18. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,470 @@
    +    // Get the new media ids passed to our hidden button.
    

    Nit: 'ids' --> 'IDs'

  19. +++ b/core/modules/media_library/src/Plugin/views/field/MediaLibrarySelectForm.php
    @@ -0,0 +1,137 @@
    +class MediaLibrarySelectForm extends FieldPluginBase {
    

    Let's mark this guy explicitly @internal too.

samuel.mortenson’s picture

Now 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:

Notice: Array to string conversion in Drupal\Component\Render\PlainTextOutput::renderFromHtml() (line 22 of /xxxx/drupal/core/lib/Drupal/Component/Render/PlainTextOutput.php)

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.

samuel.mortenson’s picture

Issue tags: -Needs reroll
phenaproxima’s picture

Good 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.

  1. +++ b/core/modules/media_library/css/media_library.module.css
    @@ -2,22 +2,19 @@
    +.media-library-views-form__header > div,
    

    Is there any more specific CSS class/selector we can use to target the inner elements, not just div?

  2. +++ b/core/modules/media_library/js/media_library.widget.es6.js
    @@ -0,0 +1,94 @@
    +          $(target).children().each((index, child) => {
    +            $(child).find('.media-library-item-weight').val(index);
    +          });
    

    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; })?

  3. +++ b/core/modules/media_library/js/media_library.widget.es6.js
    @@ -0,0 +1,94 @@
    +        .attr('title', Drupal.t('Re-order media by numerical weight instead of dragging.'))
    

    Can we set this attribute on the server side, rather than in JavaScript?

  4. +++ b/core/modules/media_library/js/media_library.widget.es6.js
    @@ -0,0 +1,94 @@
    +      if ($view.length && $view.find('.media-library-item').length) {
    

    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.

  5. +++ b/core/modules/media_library/js/media_library.widget.es6.js
    @@ -0,0 +1,94 @@
    +              if ($checkboxes.filter(':checked').length == settings.media_library.selection_remaining) {
    

    ===

  6. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,479 @@
    + *   description = @Translation("Allows you to re-use media entities."),
    

    Let's improve this -- how about "Allows you to select items from the media library"?

phenaproxima’s picture

Status: Needs review » Needs work

Another partial review.

  1. +++ b/core/modules/media_library/media_library.module
    @@ -43,6 +45,22 @@ function media_library_theme() {
    +        $output['#attached']['drupalSettings']['views']['ajax_path'] .= '?' . http_build_query($query);
    

    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.

  2. +++ b/core/modules/media_library/media_library.module
    @@ -93,6 +109,81 @@ function media_library_form_views_form_media_library_page_alter(array &$form, Fo
    +  if ($form_object instanceof ViewsForm && strpos($form_object->getBaseFormId(), 'views_form_media_library') === 0) {
    

    Couldn't we implement hook_form_BASE_FORM_ID_alter() for this?

  3. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,479 @@
    +    $element['empty_selection'] = [
    +      '#markup' => $this->t('<p>No media items are selected.</p>'),
    +      '#access' => empty($referenced_entities),
    +    ];
    +
    +    $element['weight_toggle'] = [
    +      '#type' => 'html_tag',
    +      '#tag' => 'button',
    +      '#value' => $this->t('Show media item weights'),
    +      '#attributes' => [
    +        'class' => ['link', 'media-library-toggle-weight'],
    +      ],
    +      '#access' => !empty($referenced_entities),
    +    ];
    

    Nit: Why not add this to the big $element += operation above?

  4. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,479 @@
    +          'class' => ['media-library-item', 'media-library-item-in-widget'],
    

    What is the 'media-library-item-in-widget' class used for?

  5. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,479 @@
    +          'view' => $view_builder->view($media_item, 'media_library'),
    

    Can we rename this element to rendered_entity, to prevent confusion with Views?

  6. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,479 @@
    +            'class' => ['media-library-item-weight'],
    

    Maybe this should be media-library-item__weight for BEM compliance.

  7. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,479 @@
    +    $remaining = $element['#cardinality'] - count($referenced_entities);
    

    We should only be calculating this if !$cardinality_unlimited.

  8. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,479 @@
    +        $cardinality_message = $this->t('You can select one media item.');
    +      }
    +      elseif ($remaining) {
    +        $cardinality_message = $this->t('You can select up to @maximum media (@remaining left).', [
    +          '@maximum' => $element['#cardinality'],
    +          '@remaining' => $remaining,
    +        ]);
    +      }
    +      else {
    +        $cardinality_message = $this->t('The maximum number of media has been selected.');
    

    Let's say "media item" or "media items" everywhere, for consistency.

  9. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,479 @@
    +    $element['media_library_selection'] = [
    +      '#type' => 'textfield',
    +      '#attributes' => [
    +        // This is used to pass the selection from the modal to the widget.
    +        'data-media-library-widget-value' => $field_name . $id_suffix,
    +        'class' => ['visually-hidden'],
    +      ],
    +    ];
    

    Why not use a hidden field?

  10. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,479 @@
    +      '#attributes' => [
    +        'data-media-library-widget-update' => $field_name . $id_suffix,
    +        'class' => ['visually-hidden', 'media-library-update'],
    +      ],
    

    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?

  11. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,479 @@
    +    else {
    +      return [];
    +    }
    

    No need for the else {} boilerplate. We can just return [].

  12. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,479 @@
    +    $length = end($triggering_element['#parents']) === 'remove_button' ? -4 : -1;
    

    Am I overly paranoid for thinking this is potentially brittle?

  13. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,479 @@
    +    $parents = array_slice($triggering_element['#array_parents'], 0, -4);
    +    $delta = array_slice($triggering_element['#array_parents'], -3, 1)[0];
    

    This is confusing. Can we get a comment explaining what we expect these arrays to look like?

  14. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,479 @@
    +    if (isset($values['selection'])) {
    +      if (isset($values['selection'][$delta])) {
    

    These can be combined into a single if statement. isset($values['selection'][$delta])

  15. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,479 @@
    +        array_splice($values['selection'], $delta, 1);
    +        $field_state['items'] = $values['selection'];
    +        $field_state['items_count'] = count($field_state['items']);
    

    This could use some comments as well.

samuel.mortenson’s picture

Status: Needs work » Needs review
FileSize
13.61 KB
63.1 KB

Addressed 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.

phenaproxima’s picture

Status: Needs review » Needs work
  1. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,482 @@
    +    if (isset($values['selection']) && isset($values['selection'][$delta])) {
    

    Pro 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 just isset($values['selection'][$delta]) should work fine here.

  2. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,482 @@
    +    static::setFieldState($element, $form_state, $field_state);
    

    Nit: This can go inside the if block, since that's the only place we're actually changing $field_state.

  3. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,482 @@
    +   * Making an invalid selection from the View should not be possible, but we
    

    Supernit: 'view' should be lowercase.

  4. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,482 @@
    +      $form_state->setError($element, \Drupal::translation()->formatPlural($element['#cardinality'], 'This field only accepts one item.', 'This field cannot accept more than @count item.'));
    

    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".

  5. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,482 @@
    +      if ($element['#target_bundles'] && !in_array($media_item->bundle(), $element['#target_bundles'], TRUE)) {
    +        $form_state->setError($element, t('The media item "@label" is not of an accepted type. Allowed types: @types', [
    +          '@label' => $media_item->label(),
    +          '@types' => implode(', ', $element['#target_bundles']),
    +        ]));
    

    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.

  6. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,482 @@
    +    $field_state['items'] = isset($field_state['items']) ? $field_state['items'] : [];
    

    Why not have have getWidgetState() always return an empty array of items, if one is not already set?

  7. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,482 @@
    +      if ($media_item && $media_item->access('view')) {
    

    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.

  8. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,482 @@
    +          'weight' => $weight++,
    

    Do we need an explicit weight? The items are numerically indexed, which implicitly serves as a weight.

  9. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,482 @@
    +   * @return array|\Drupal\media\MediaInterface[]
    

    We don't need the 'array' type hint; the second one is more useful and specific.

  10. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,482 @@
    +  public static function getNewMediaItems(array $element, FormStateInterface $form_state) {
    

    I don't know if this needs to be public.

  11. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,482 @@
    +      $ids = explode(',', $value['media_library_selection']);
    

    We should probably validate
    that these are all integers.

  12. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,482 @@
    +   * @return array|mixed
    +   *   The field state.
    +   */
    +  public static function getFieldState(array $element, FormStateInterface $form_state) {
    

    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?

  13. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,482 @@
    +  public static function setFieldState(array $element, FormStateInterface $form_state, array $field_state) {
    

    Is there any reason for this to be public?

  14. +++ b/core/modules/media_library/src/Plugin/views/field/MediaLibrarySelectForm.php
    @@ -0,0 +1,138 @@
    +        '#return_value' => $entity->getEntityTypeId() . ':' . $entity->id(),
    

    updateWidget() expects the entity type ID to be 'media' -- hard-coded -- so we should hard-code 'media' here too.

  15. +++ b/core/modules/media_library/src/Plugin/views/field/MediaLibrarySelectForm.php
    @@ -0,0 +1,138 @@
    +    $form['actions']['submit']['#ajax'] = [
    +      'url' => Url::fromUserInput($url),
    +      'options' => [
    +        'query' => \Drupal::request()->query->all() + [
    +          FormBuilderInterface::AJAX_FORM_REQUEST => TRUE,
    +        ],
    +      ],
    

    I didn't know you could do this! Neat stuff.

  16. +++ b/core/modules/media_library/src/Plugin/views/field/MediaLibrarySelectForm.php
    @@ -0,0 +1,138 @@
    +    $widget_id = \Drupal::request()->query->get('media_library_widget_id');
    

    Let's throw a BadRequestHttpException if $widget_id is invalid.

  17. +++ b/core/modules/media_library/src/Routing/RouteSubscriber.php
    @@ -0,0 +1,22 @@
    +    if ($route = $collection->get('view.media_library.widget')) {
    +      $route->addRequirements(['_csrf_token' => 'TRUE']);
    +    }
    

    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.

samuel.mortenson’s picture

Here's a demo video comparing the current entity reference widget with the widget provided in this patch: https://www.youtube.com/watch?v=LwGuf17Oz40

samuel.mortenson’s picture

It'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.

Gábor Hojtsy’s picture

        $cardinality_message = $this->t('You can select one media item.');
      }
      elseif ($remaining) {
        $cardinality_message = $this->t('You can select up to @maximum media items (@remaining left).', [

1. 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.

dqd’s picture

#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...

webchick’s picture

Thanks 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?

phenaproxima’s picture

Is it possible/easy to filter out from the Media Library selection media that are already attached to the node via the same field?

It 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.

webchick’s picture

Oh, 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.

webchick’s picture

Cross-post. A follow-up to discuss about re-attaching the same media to the same field sounds good!

phenaproxima’s picture

phenaproxima’s picture

This 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.

ckrina’s picture

Issue summary: View changes
FileSize
81.41 KB

The 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.

samuel.mortenson’s picture

Status: Needs work » Needs review
FileSize
261.81 KB
68 KB
13.32 KB

Addressed 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.

phenaproxima’s picture

Status: Needs review » Needs work

Every 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.

  1. +++ b/core/modules/media/media.module
    @@ -336,3 +314,42 @@ function media_preprocess_media_reference_help(&$variables) {
    + * Returns the appropriate url to add media for the current user.
    

    Supernit: url --> URL

  2. +++ b/core/modules/media/media.module
    @@ -336,3 +314,42 @@ function media_preprocess_media_reference_help(&$variables) {
    + *   The help url, or FALSE if the user cannot create any media.
    

    Same here. Also, what is meant by "help URL"?

  3. +++ b/core/modules/media/media.module
    @@ -336,3 +314,42 @@ function media_preprocess_media_reference_help(&$variables) {
    +function _media_get_add_url($allowed_bundles) {
    

    Let's mark this function explicitly deprecated and internal, and link to the issue where we intend to remove it.

  4. +++ b/core/modules/media/media.module
    @@ -336,3 +314,42 @@ function media_preprocess_media_reference_help(&$variables) {
    +  $bundle_labels = [];
    +  $create_bundles = [];
    +  foreach ($allowed_bundles as $bundle) {
    +    $bundle_labels[] = $all_bundles[$bundle]['label'];
    +    if ($access_handler->createAccess($bundle)) {
    +      $create_bundles[] = $bundle;
    +      if (count($create_bundles) > 1) {
    +        // If the user has access to create more than 1 bundle then the
    +        // individual media type form can not be used.
    +        break;
    +      }
    +    }
    +  }
    

    This seems like a good candidate for array_map() and array_filter().

  5. +++ b/core/modules/media/media.module
    @@ -336,3 +314,42 @@ function media_preprocess_media_reference_help(&$variables) {
    +  // Add a section about how to create media if the user has access to do so.
    +  if (!empty($create_bundles)) {
    +    if (count($create_bundles) === 1) {
    +      $add_url = Url::fromRoute('entity.media.add_form', ['media_type' => $create_bundles[0]])->toString();
    +    }
    +    elseif (count($create_bundles) > 1) {
    +      $add_url = Url::fromRoute('entity.media.add_page')->toString();
    +    }
    +  }
    

    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.

  6. +++ b/core/modules/media_library/css/media_library.module.css
    @@ -53,6 +54,18 @@
    +.media-library-view.view-display-id-widget .media-library-select-all {
    +  display: none;
    +}
    

    Do we have test coverage to assert that this checkbox is never visible in the widget?

  7. +++ b/core/modules/media_library/media_library.module
    @@ -43,6 +45,24 @@ function media_library_theme() {
    +        $output['#attached']['drupalSettings']['views']['ajax_path'] .= '?' . http_build_query($query);
    

    Let's use UrlHelper::buildQuery() instead of http_build_query().

  8. +++ b/core/modules/media_library/media_library.views.inc
    @@ -0,0 +1,22 @@
    + * Contains views integration for the media_library module.
    

    Supernit: 'views' should be 'Views', since it's the proper name of a module.

  9. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,503 @@
    +    $field_state = static::getWidgetState($parents, $field_name, $form_state);
    +    if ($field_state && isset($field_state['items'])) {
    

    I think static::getWidgetState() should always return an array with an 'items' key, which defaults to an empty array.

  10. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,503 @@
    +      $element['create_help']['description'] = [
    +        '#type' => 'html_tag',
    +        '#tag' => 'div',
    +        '#attributes' => [
    +          'class' => ['description'],
    +        ],
    +        '#value' => t('Create your media on the <a href=":add_page" target="_blank">media add page</a> (opens a new window), then select it in the library.', [':add_page' => $add_url]),
    +      ];
    

    Sigh...oh, for the day when we can remove this.

  11. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,503 @@
    +    $element['empty_selection'] = [
    +      '#markup' => $this->t('<p>No media items are selected.</p>'),
    +      '#access' => empty($referenced_entities),
    +    ];
    

    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.

  12. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,503 @@
    +    $element['weight_toggle'] = [
    +      '#type' => 'html_tag',
    +      '#tag' => 'button',
    +      '#value' => $this->t('Show media item weights'),
    +      '#attributes' => [
    +        'class' => [
    +          'link',
    +          'media-library-toggle-weight',
    +        ],
    +        'title' => $this->t('Re-order media by numerical weight instead of dragging'),
    +      ],
    +      '#access' => !empty($referenced_entities),
    +    ];
    

    As with the previous thing, let's only create this element if $referenced_entities is not empty to begin with.

  13. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,503 @@
    +        'weight' => [
    +          '#type' => 'textfield',
    +          '#title' => $this->t('Weight'),
    +          '#default_value' => $delta,
    +          '#attributes' => [
    +            'class' => ['media-library-item__weight'],
    +          ],
    +        ],
    

    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.

  14. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,503 @@
    +      // @todo Make the View configurable in https://www.drupal.org/project/drupal/issues/2971209
    

    Supernit: 'View' should be lowercase.

  15. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,503 @@
    +      '#attributes' => [
    +        'class' => ['button', 'use-ajax', 'media-library-open-button'],
    +        'data-dialog-type' => 'modal',
    +        'data-dialog-options' => Json::encode([
    +          'dialogClass' => 'media-library-widget-modal',
    +          'height' => '75%',
    +          'width' => '75%',
    +          'title' => $this->t('Media library'),
    +        ]),
    +      ],
    

    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. :)

  16. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,503 @@
    +    // items based on the  "media_library_selection" value.
    

    Superdupermeganit: There's an extra space after "...based on the".

  17. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,503 @@
    +      '#attributes' => [
    +        'data-media-library-widget-update' => $field_name . $id_suffix,
    +        'class' => ['media-library-update'],
    +      ],
    

    Let's use the js-hide class to keep this button hidden, rather than a custom class.

  18. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,503 @@
    +      '#validate' => [[static::class, 'validateItems']],
    

    Should we also apply this validation to the other AJAX-ey buttons exposed by this widget? Probably couldn't hurt.

  19. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,503 @@
    +      '#limit_validation_errors' => [array_merge($parents, [$field_name])],
    

    Let's generate this array once at the top of the method and re-use it as needed. For readability.

  20. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,503 @@
    +    $parents = $triggering_element['#array_parents'];
    +    $parents = array_slice($parents, 0, $length);
    

    We can condense these into one line: $parents = array_slice($triggering_element['#array_parents'], 0, $length).

  21. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,503 @@
    +    // Get the parents required to find the top-level widget element.
    +    $parents = array_slice($triggering_element['#array_parents'], 0, -4);
    +    // Get the delta of the item being removed.
    +    $delta = array_slice($triggering_element['#array_parents'], -3, 1)[0];
    

    We should probably check that $parents is of the correct length before the second array_slice() call, and throw LogicException if it's not.

  22. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,503 @@
    +    if (isset($values['selection'], $values['selection'][$delta])) {
    

    We can remove the first argument to isset().

  23. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,503 @@
    +      $field_state['items_count'] = count($field_state['items']);
    

    Out of curiosity, why do we need $field_state['items_count']? Can't we just call count($field_state['items']) as needed?

  24. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,503 @@
    +      $form_state->setError($element, \Drupal::translation()->formatPlural($element['#cardinality'], 'This field only accepts one item.', 'This field cannot accept more than @count items.'));
    

    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?

  25. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,503 @@
    +    static::setFieldState($element, $form_state, $field_state);
    

    We probably only need to update the field state if $media is not empty.

  26. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,503 @@
    +      $media = Media::loadMultiple($ids);
    

    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.

  27. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,503 @@
    +    $field_name = $element['#field_name'];
    +    $parents = $element['#field_parents'];
    +    $widget_state = static::getWidgetState($parents, $field_name, $form_state);
    

    We don't need to keep $field_name and $parents in their own variables.

  28. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,503 @@
    +   * @param array $field_state
    +   *   The field state to set.
    

    Can we get a little explanation about what is expected to be in $field_state?

  29. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,503 @@
    +    $field_name = $element['#field_name'];
    +    $parents = $element['#field_parents'];
    

    Same here.

samuel.mortenson’s picture

I 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.

samuel.mortenson’s picture

Status: Needs work » Needs review
FileSize
69.47 KB
17.6 KB

Addressed #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.

samuel.mortenson’s picture

So 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:

  1. There have been a few reviewers that were worried about giving all users with the "View media" permission access to the widget view (the thing that opens in the modal).
  2. To address this, I added a CSRF token check to that route. This does not restrict access for anonymous users, so it is not a viable solution.
  3. Locally I've tried to implement my own hash token, something like "entity_type:entity_id:uid:field_name", but in the end realized that wasn't secure enough because anonymous users would all share the same token, and if I added token expiry it would be way too complicated.
  4. I also tried passing an un-hashed token in the format "entity_type:entity_id:uid:field_name", where you would check create/update access to "entity_type:entity_id" for "uid", and that "field_name" is a media field. This could work but the access checking seemed complicated.

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?

webchick’s picture

What 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?

phenaproxima’s picture

There 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!

samuel.mortenson’s picture

FileSize
70.36 KB
3.8 KB

That 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

GrandmaGlassesRopeMan’s picture

Status: Needs review » Needs work

Some minor points here,

  1. +++ b/core/modules/media_library/js/media_library.widget.es6.js
    @@ -0,0 +1,91 @@
    +        }
    

    Missing a trailing comma.

  2. +++ b/core/modules/media_library/js/media_library.widget.es6.js
    @@ -0,0 +1,91 @@
    +        hide: Drupal.t('Hide media item weights')
    

    Missing a trailing comma.

  3. +++ b/core/modules/media_library/js/media_library.widget.es6.js
    @@ -0,0 +1,91 @@
    +            .parent()
    +            .find('.media-library-item__weight')
    +            .parent()
    

    Could we use .closest()?

  4. +++ b/core/modules/media_library/js/media_library.widget.es6.js
    @@ -0,0 +1,91 @@
    +            if ($checkboxes.filter(':checked').length === settings.media_library.selection_remaining) {
    +              $checkboxes.filter(':not(:checked)')
    +                .prop('disabled', true)
    +                .closest('.media-library-item')
    +                .addClass('disabled');
    

    Lets be consistent and put each method on a new line.

  5. +++ b/core/modules/media_library/js/media_library.widget.es6.js
    @@ -0,0 +1,91 @@
    +              $checkboxes.prop('disabled', false)
    +                .closest('.media-library-item')
    +                .removeClass('disabled');
    

    Lets be consistent and put each method on a new line.

GrandmaGlassesRopeMan’s picture

FileSize
160.88 KB

For #55.1 🤦‍♀️

samuel.mortenson’s picture

Status: Needs work » Needs review
FileSize
70.39 KB
1.58 KB

Addressed #55

1. Fixed (and thanks for context).
2. Fixed.
3. No, not with the element structure the way it is.
4. Fixed.
5. Fixed.

phenaproxima’s picture

Status: Needs review » Needs work

Well, folks, after this review I'm pretty much out of things to nitpick.

  1. +++ b/core/modules/media/media.module
    @@ -336,3 +318,35 @@ function media_preprocess_media_reference_help(&$variables) {
    + * @internal
    + */
    +function _media_get_add_url($allowed_bundles) {
    

    Under the @internal, should we explicitly mention that this function may be removed in a minor release?

  2. +++ b/core/modules/media/media.module
    @@ -336,3 +318,35 @@ function media_preprocess_media_reference_help(&$variables) {
    +  $create_bundles = array_filter($allowed_bundles, function ($bundle) use ($access_handler) {
    +    return $access_handler->createAccess($bundle);
    +  });
    

    We can condense this to one line: $create_bundles = array_filter($allowed_bundles, [$access_handler, 'createAccess']).

  3. +++ b/core/modules/media/media.module
    @@ -336,3 +318,35 @@ function media_preprocess_media_reference_help(&$variables) {
    +  // Add a section about how to create media if the user has access to do so.
    +  if (!empty($create_bundles)) {
    +    if (count($create_bundles) === 1) {
    +      return Url::fromRoute('entity.media.add_form', ['media_type' => reset($create_bundles)])->toString();
    +    }
    +    elseif (count($create_bundles) > 1) {
    +      return Url::fromRoute('entity.media.add_page')->toString();
    +    }
    +  }
    

    We can remove the if (!empty($create_bundles)) check, as it doesn't really add anything to this logic.

  4. +++ b/core/modules/media_library/config/install/views.view.media_library.yml
    @@ -445,3 +404,130 @@ display:
    +  widget:
    +    display_plugin: page
    

    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).

  5. +++ b/core/modules/media_library/js/media_library.widget.es6.js
    @@ -0,0 +1,91 @@
    +            $(child).find('.media-library-item__weight').val(index);
    

    Can the selector be input.media-library-item__weight, just to be sure we're targeting the right kind of thing?

  6. +++ b/core/modules/media_library/js/media_library.widget.es6.js
    @@ -0,0 +1,91 @@
    +            .parent()
    +            .find('.media-library-item__weight')
    +            .parent()
    

    Same here.

  7. +++ b/core/modules/media_library/js/media_library.widget.es6.js
    @@ -0,0 +1,91 @@
    +      $('.media-library-item__weight', context).once('media-library-toggle').parent().hide();
    

    And here.

  8. +++ b/core/modules/media_library/js/media_library.widget.es6.js
    @@ -0,0 +1,91 @@
    +              $checkboxes.filter(':not(:checked)')
    

    This can be $checkboxes.not(':checked').

  9. +++ b/core/modules/media_library/media_library.module
    @@ -43,6 +46,23 @@ function media_library_theme() {
    +      if (!empty($query)) {
    +        $output['#attached']['drupalSettings']['views']['ajax_path'] .= '?' . UrlHelper::buildQuery($query);
    +        if (isset($query['media_library_remaining'])) {
    +          $output['#attached']['drupalSettings']['media_library']['selection_remaining'] = (int) $query['media_library_remaining'];
    +        }
    +      }
    

    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.

  10. +++ b/core/modules/media_library/media_library.module
    @@ -93,6 +111,82 @@ function media_library_form_views_form_media_library_page_alter(array &$form, Fo
    +    $form['header']['#attributes']['class'][] = 'media-library-views-form__header';
    +    $form['header']['media_bulk_form']['#attributes']['class'] = 'media-library-views-form__bulk_form';
    

    Let's wrap this in an isset($form['header']) check.

  11. +++ b/core/modules/media_library/media_library.module
    @@ -93,6 +111,82 @@ function media_library_form_views_form_media_library_page_alter(array &$form, Fo
    +function media_library_form_views_exposed_form_alter(array &$form, FormStateInterface $form_state) {
    +  if (isset($form['#id']) && $form['#id'] === 'views-exposed-form-media-library-widget') {
    +    $types = _media_library_get_allowed_types();
    +    if ($types && isset($form['type']['#options'])) {
    +      $keys = array_flip($types);
    +      // Ensure that the default value (by default "All") persists.
    +      if (isset($form['type']['#default_value'])) {
    +        $keys[$form['type']['#default_value']] = TRUE;
    +      }
    +      $form['type']['#options'] = array_intersect_key($form['type']['#options'], $keys);
    +    }
    +  }
    +}
    

    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).

  12. +++ b/core/modules/media_library/media_library.module
    @@ -93,6 +111,82 @@ function media_library_form_views_form_media_library_page_alter(array &$form, Fo
    +  if (!is_a($class, 'Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem', TRUE)) {
    

    Can we use the ::class format for EntityReferenceItem?

  13. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,514 @@
    + *   multiple_values = TRUE
    

    Nit: Should have a trailing comma.

  14. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,514 @@
    +    $field_name = $this->fieldDefinition->getName();
    +    $parents = $form['#parents'];
    

    Nit: We can pass these directly to getWidgetState(), we don't need to keep them in their own local variables.

  15. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,514 @@
    +    if ($field_state && isset($field_state['items'])) {
    

    We can lose the first condition; isset() will check it implicitly.

  16. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,514 @@
    +    $view_builder = $this->entityTypeManager->getViewBuilder($this->getFieldSetting('target_type'));
    

    We don't need to call getFieldSetting(); this widget is hard-coded to work only with media items.

  17. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,514 @@
    +        '#value' => t('Create new media'),
    

    Can we use $this->t()?

  18. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,514 @@
    +        '#value' => t('Create your media on the <a href=":add_page" target="_blank">media add page</a> (opens a new window), then select it in the library.', [':add_page' => $add_url]),
    

    Same here.

  19. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,514 @@
    +      return Media::loadMultiple($ids);
    

    We should probably only return this if $ids is not empty, now that I think of it.

  20. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,514 @@
    +   * @return array
    

    Should be array[], since it's an array of arrays.

  21. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,514 @@
    +   * @param array $field_state
    

    Same here.

  22. +++ b/core/modules/media_library/src/Plugin/views/field/MediaLibrarySelectForm.php
    @@ -0,0 +1,140 @@
    +        'query' => \Drupal::request()->query->all() + [
    +          FormBuilderInterface::AJAX_FORM_REQUEST => TRUE,
    +        ],
    

    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.

  23. +++ b/core/modules/media_library/src/Plugin/views/field/MediaLibrarySelectForm.php
    @@ -0,0 +1,140 @@
    +    // Pass the selection to the field widget based on the current widget ID.
    +    $response->addCommand(new InvokeCommand("[data-media-library-widget-value=\"$widget_id\"]", 'val', [implode(',', $mids)]));
    +    $response->addCommand(new InvokeCommand("[data-media-library-widget-update=\"$widget_id\"]", 'trigger', ['mousedown']));
    +    $response->addCommand(new CloseDialogCommand());
    +    return $response;
    

    Nit: We can make this a beautiful chain: return (new AjaxResponse())->addCommand()->addCommand()->addCommand().

  24. +++ b/core/modules/media_library/tests/src/FunctionalJavascript/MediaLibraryTest.php
    @@ -92,11 +101,158 @@ public function testAdministrationPage() {
    +    $session->executeScript('jQuery(\'button:contains("Apply Filters")\').click()');
    

    Can we do this without executing JavaScript? Won't $assert_session->buttonExists('Apply Filters')->press() do the same thing?

  25. +++ b/core/modules/media_library/tests/src/FunctionalJavascript/MediaLibraryTest.php
    @@ -92,11 +101,158 @@ public function testAdministrationPage() {
    +    $session->executeScript("jQuery('$checkbox_selector:eq(0)').click()");
    +    $session->executeScript("jQuery('$checkbox_selector:eq(1)').click()");
    +    $session->executeScript("jQuery('$checkbox_selector:eq(2)').click()");
    

    Same here -- any way we can accomplish this without JavaScript?

  26. +++ b/core/modules/media_library/tests/src/FunctionalJavascript/MediaLibraryTest.php
    @@ -92,11 +101,158 @@ public function testAdministrationPage() {
    +   * Tests that the Media library's widget works as expected for anonymous.
    

    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.

samuel.mortenson’s picture

FileSize
71.05 KB
19.42 KB

Addressing #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.

samuel.mortenson’s picture

Status: Needs work » Needs review
chr.fritsch’s picture

Issue summary: View changes
FileSize
138.1 KB

Responding to #30.4

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).

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.

phenaproxima’s picture

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.

I think this is a decision that should be deferred to the UX team.

samuel.mortenson’s picture

FileSize
71.99 KB

Re-rolled #59 as it was no longer applying. Updated tests to work with the new test base.

samuel.mortenson’s picture

FileSize
72.14 KB
853 bytes

This 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.

samuel.mortenson’s picture

I recorded a new video since not a lot of people made the UX meeting on Tuesday: https://www.youtube.com/watch?v=ZCvW5BYlExg

samuel.mortenson’s picture

I 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.

samuel.mortenson’s picture

samuel.mortenson’s picture

FileSize
72.14 KB

Re-roll, again!

phenaproxima’s picture

I 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.

seanB’s picture

Just started testing this. First off, this is looking really nice! Thanks samuel.mortenson. I only have some small usability related comments:

  1. The button in the widget currently says 'Add Media'. I think this is confusing since the button on /admin/content/media also says 'Add media' but does something different. Maybe we should use 'Select media' to avoid confusion? Also, the word 'media' should be lowercase here.

    Media library 'Add Media' button

  2. When opening the modal the 'Apply filters' button is not shown near the actual filter form. Can we show this next to the form? This was already mentioned in #5 but I can't find if there is a good reason not to do this.

    Media library 'Apply filters' button

  3. In the modal, the media library shows edit links for media below the thumbnail. I don't think we currently have a good reason to show these links. Even though there is an alert to warn you, when you leave the page you lose all the information. This is probably going to frustrate users.

    Media library title link

  4. When media is selected, the title of the media item again links to the edit page. I think removing the link is best for now. We could find a solution to edit media in a follow-up (like showing an edit button to load the media form in a modal).

    Media widget title link

  5. Last thing I noticed is we are not showing the media type in the library. This is probably a separate issue, but it could be hard to see the difference between a remote video and an image at first glance. They both show an image thumbnail. This might be an issue when users enable multiple types for a field (like for a slideshow or something).
beautifulmind’s picture

About #1 in above comment, how about using 'Browse media' or 'Browse Media'?

beautifulmind’s picture

About #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?

samuel.mortenson’s picture

Thanks 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 by Drupal.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.

seanB’s picture

1. 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.

seanB’s picture

Status: Needs review » Needs work

We just talked about this issue in the UX meeting. We decided on the following regarding my feedback in #70:

  1. The button should say 'Browse media' for now. When we add the possibility to create new media, we can change the button again. The widget would change a bit anyway.
  2. This is not a blocker for this issue. We should make it a follow up with a high priority.
  3. Since the link could lead to data loss, we should remove it in this issue. We do recognize information and options are missing (view media link, edit link and the media type are the main things). There should be a follow up to improve the grid view.
  4. Same as 3.
  5. Same as 3.

To sum it up for this patch:

  • Change the Add media button to Browse media.
  • Remove the edit link from the grid display for the widget and modal.
  • Create the follow ups.

I think we can remove some of the issue tags as well, but leaving those for now.

jrockowitz’s picture

I feel that #70.2 is going to confuse a lot of users. Applying a temporary and quick fix/workaround might be worth considering.

When opening the modal the 'Apply filters' button is not shown near the actual filter form.

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.

Media library browse dialog

samuel.mortenson’s picture

I 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.

samuel.mortenson’s picture

The last submitted patch, 76: 2962525-76.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

Status: Needs review » Needs work

The last submitted patch, 78: 2962525-77.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

samuel.mortenson’s picture

Status: Needs work » Needs review
FileSize
73.76 KB
1.2 KB

Didn't know tests were failing as of #76, fixed.

lauriii’s picture

Status: Needs review » Needs work

Reviewed 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.

  1. +++ b/core/modules/media_library/css/media_library.module.css
    @@ -51,6 +48,25 @@
    +.media-library-item.disabled {
    
    +++ b/core/modules/media_library/css/media_library.theme.css
    @@ -135,6 +143,68 @@
    +.media-library-item.disabled {
    

    Should we just use .media-library-item--disabled since this is not proper usage of SMACSS state classes?

  2. +++ b/core/modules/media_library/css/media_library.theme.css
    @@ -135,6 +143,68 @@
    +.media-library-toggle-weight {
    

    Should we rename this to .media-library-widget__toggle-weight since this actually depends being child of .media-library-widget?

  3. +++ b/core/modules/media_library/js/media_library.widget.es6.js
    @@ -0,0 +1,93 @@
    +      $('.media-library-selection', context).once('media-library-sortable').sortable({
    ...
    +        handle: '.media-library-item__preview',
    ...
    +            $(child).find('input.media-library-item__weight').val(index);
    ...
    +      $('.media-library-toggle-weight', context).once('media-library-toggle')
    ...
    +            .find('input.media-library-item__weight')
    ...
    +      $('input.media-library-item__weight', context).once('media-library-toggle').parent().hide();
    ...
    +      $('.media-library-item a[href]', context).once('media-library-warn-link')
    ...
    +      const $view = $('.media-library-view', context).once('media-library-remaining');
    +      $view.find('.media-library-item input[type="checkbox"]')
    ...
    +            const $checkboxes = $view.find('.media-library-item input[type="checkbox"]');
    

    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.

lauriii’s picture

Thanks @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.

samuel.mortenson’s picture

Status: Needs work » Needs review
FileSize
74.6 KB
12.16 KB

Addressed #82 - ready for review again.

lauriii’s picture

Status: Needs review » Needs work
Issue tags: -Needs frontend framework manager review
  1. +++ b/core/modules/media_library/js/media_library.widget.es6.js
    @@ -0,0 +1,93 @@
    +        handle: '.media-library-item__preview',
    

    This one is still missing a .js prefix.

  2. +++ b/core/modules/media_library/js/media_library.widget.es6.js
    @@ -0,0 +1,93 @@
    +            $(child).find('.js-media-library-item__weight').val(index);
    ...
    +      $('.js-media-library-widget__toggle-weight', context).once('media-library-toggle')
    ...
    +            .find('.js-media-library-item__weight')
    ...
    +      $('.js-media-library-item__weight', context).once('media-library-toggle').parent().hide();
    

    The js- prefixed classes doesn't have to follow BEM because generally there shouldn't be CSS attached to them.

samuel.mortenson’s picture

Addressed #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.

phenaproxima’s picture

Status: Needs review » Reviewed & tested by the community
Issue tags: -Needs accessibility review, -Needs JavaScript review, -Needs followup, -Needs usability review

We 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?

  • Code-wise, this patch has been ready for a fair two weeks now. I base this on the fact that I have read it through several times, understand it thoroughly, and can no longer find anything to complain about.
  • Our only hard blocker, #2861860: View inside core modal doesn't refresh correctly when reopened, has been committed.
  • This patch has been demoed to the UX team (including such luminaries as @ckrina, @webchick, and @Gábor Hojtsy) repeatedly in the last several UX calls...and no serious problems have been identified. Anything that was identified as commit-blocking has been addressed in this patch, and everything else has been moved to into follow-up issues (some of which must be fixed before the media library module can be marked stable).
  • We have tried repeatedly to get accessibility review on this patch, but have not been able to get any time from the accessibility team. In the last UX call, @webchick felt (and I agree) that, at this point, we have done our due diligence and any accessibility issues that do turn up will be turned up as bugs, which can be stable blockers if needed.
  • @samuel.mortenson has made tremendous progress on #2938116: Allow media to be uploaded with the Media Library field widget, which is the only thing needed to take the media library from "useful" (once this patch is committed) to "truly freaking awesome". It is blocked only on this issue.
  • We have completed reviews from a front-end framework manager and a JavaScript maintainer.
  • This is an experimental module which exposes no APIs, and this patch contains the absolute minimum functionality we must have in Drupal in order for the media library not to get reverted. If the media library is removed from core until 8.7, it will be a tragedy for the Media Initiative, which has made incredible progress in this release cycle. We are more than happy to deal with any stable blockers that arise once this is in.

Speaking as a Media subsystem maintainer: it's magic time. Let's do this. RTBC once green on all backends.

phenaproxima’s picture

plach’s picture

We 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:

There are no specific restrictions on what changes are allowed in alpha releases beyond the allowed changes for minors, but additions and disruptive changes are backported to the alpha only at committer discretion, with criticals, strategic initiatives, and product management priorities as the first focus.

I would be surprised if this weren't considered a priority.

phenaproxima’s picture

Status: Reviewed & tested by the community » Needs work

Uh-oh...looks like we have a failure on Postgres. Kicking back for that.

samuel.mortenson’s picture

Tested 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.

phenaproxima’s picture

Status: Needs review » Reviewed & tested by the community

Nice detective work! Thanks, @samuel.mortenson. Restoring RTBC once green on all backends.

Version: 8.6.x-dev » 8.7.x-dev

Drupal 8.6.0-alpha1 will be released the week of July 16, 2018, which means new developments and disruptive changes should now be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 91: 2962525-91.patch, failed testing. View results

phenaproxima’s picture

Issue tags: +Needs reroll

D'oh!

phenaproxima’s picture

Issue tags: -Needs reroll

Ah, 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.

phenaproxima’s picture

Status: Needs work » Reviewed & tested by the community

Restoring RTBC once all backends are green on 8.7.x.

plach’s picture

Overall 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:

  • I got a notice while adding a media reference field (see attached screenshot).
  • I was not able to make the widget work without Javascript: after submitting the media library form, I wasn't brought back to the parent node form.

Code review:

  1. +++ b/core/modules/media_library/media_library.module
    @@ -43,6 +47,24 @@ function media_library_theme() {
    +        $output['#attached']['drupalSettings']['views']['ajax_path'] .= '?' . UrlHelper::buildQuery($query);
    

    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.

  2. +++ b/core/modules/media_library/media_library.module
    @@ -93,6 +113,112 @@ function media_library_form_views_form_media_library_page_alter(array &$form, Fo
    +      $form['header']['media_bulk_form']['#attributes']['class'] = 'media-library-views-form__bulk_form';
    

    Isn't this is missing a [] operator?

  3. +++ b/core/modules/media_library/media_library.module
    @@ -93,6 +113,112 @@ function media_library_form_views_form_media_library_page_alter(array &$form, Fo
    +  // Add after build to fix media library views exposed filter's
    +  // 'Apply filter' submit button.
    ...
    +  // This prevents the views exposed filter's 'Apply filter' submit button
    +  // from being moved into the dialog's buttons.
    ...
    + * Alters the widget view's query to only show media that can be selected,
    + * based on what types are allowed in the field settings.
    

    These comments do not wrap at column 80.

  4. +++ b/core/modules/media_library/media_library.module
    @@ -93,6 +113,112 @@ function media_library_form_views_form_media_library_page_alter(array &$form, Fo
    +    $entity_type = \Drupal::entityTypeManager()->getDefinition('media');
    

    Can we move this line inside the if branch? It's used only there.

  5. +++ b/core/modules/media_library/media_library.module
    @@ -93,6 +113,112 @@ function media_library_form_views_form_media_library_page_alter(array &$form, Fo
    +      $query->where[] = [
    

    Can we use $query->addWhere()? It should make the code simpler and easier to read.

  6. +++ b/core/modules/media_library/media_library.module
    @@ -107,3 +233,17 @@ function media_library_local_tasks_alter(&$local_tasks) {
    +  $types = \Drupal::request()->query->get('media_library_allowed_types');
    

    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.

  7. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,524 @@
    + * @internal
    

    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.

  8. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,524 @@
    +        '#type' => 'html_tag',
    

    Isn't there any reusable code for the weight toggle?

  9. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,524 @@
    +      throw new \LogicException('The element that triggered the widget update was at an unexpected depth.');
    

    The exception message could contain the actual/wrong data to make the issue easier to troubleshoot.

  10. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,524 @@
    +      $form_state->setError($element, \Drupal::translation()->formatPlural($element['#cardinality'], 'Only one item can be selected.', 'Only @count items can be selected.'));
    ...
    +    $all_bundles = \Drupal::service('entity_type.bundle.info')->getBundleInfo('media');
    

    Can we inject these services?

  11. +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
    @@ -0,0 +1,524 @@
    +        if ($media_item->access('view')) {
    

    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.

  12. +++ b/core/modules/media_library/src/Plugin/views/field/MediaLibrarySelectForm.php
    @@ -0,0 +1,139 @@
    +        '#return_value' => 'media:' . $entity->id(),
    

    It would be nice to add a comment explaining why we need the media: prefix, given that we are discarding it later.

  13. +++ b/core/modules/media_library/src/Plugin/views/field/MediaLibrarySelectForm.php
    @@ -0,0 +1,139 @@
    +    $widget_id = \Drupal::request()->query->get('media_library_widget_id');
    

    Can we inject this?

  14. +++ b/core/modules/media_library/src/Plugin/views/field/MediaLibrarySelectForm.php
    @@ -0,0 +1,139 @@
    +      throw new BadRequestHttpException('The "media_library_widget_id" query parameter is required and must be a string.');
    

    Same as above: the exception message could contain the actual/wrong data to make the issue easier to troubleshoot.

  15. +++ b/core/modules/media_library/src/Plugin/views/field/MediaLibrarySelectForm.php
    @@ -0,0 +1,139 @@
    +  protected function emptySelectedMessage() {
    

    Why do we need a dedicated method for this?

samuel.mortenson’s picture

Thanks 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.

plach’s picture

Looks good, thanks!
(sorry for missing the static methods :)

plach’s picture

Saving credits

  • plach committed de52834 on 8.7.x
    Issue #2962525 by samuel.mortenson, jrockowitz, seanB, drpal, chr....
plach’s picture

Version: 8.7.x-dev » 8.6.x-dev

Committed de52834 and pushed to 8.7.x. Awesome work, guys!

I (auto)fixed a few issues stylelint was complaining about on commit:

core/modules/media_library/css/media_library.theme.css
 147:12  ✖  Expected a leading zero   number-leading-zero
 165:11  ✖  Expected a leading zero   number-leading-zero
 190:15  ✖  Expected a leading zero   number-leading-zero

Here's the diff:

diff --git a/core/modules/media_library/css/media_library.theme.css b/core/modules/media_library/css/media_library.theme.css
index 004f37c4d3..5aab3c5bbd 100644
--- a/core/modules/media_library/css/media_library.theme.css
+++ b/core/modules/media_library/css/media_library.theme.css
@@ -144,7 +144,7 @@
 }
 
 .media-library-item--disabled {
-  opacity: .5;
+  opacity: 0.5;
 }
 
 .media-library-selection {
@@ -162,7 +162,7 @@
 }
 
 .media-library-item .form-item {
-  margin: .75em;
+  margin: 0.75em;
 }
 
 .media-library-item__remove,
@@ -187,7 +187,7 @@
   border-radius: 20px;
   color: transparent;
   text-shadow: none;
-  transition: .2s border-color;
+  transition: 0.2s border-color;
 }
 
 .media-library-item__remove:hover,
plach’s picture

Discussed 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.

plach’s picture

  • plach committed cf1e3ee on 8.6.x
    Issue #2962525 by samuel.mortenson, jrockowitz, seanB, drpal, chr....
plach’s picture

Status: Reviewed & tested by the community » Fixed

And this is backported now!

catch’s picture

Issue tags: -8.6.0 release notes

Untagging for release notes since this doesn't have upgrade path implications and fits more into highlights.

geek-merlin’s picture

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.