Seattle
Account created on 11 June 2008, almost 16 years ago
#

Merge Requests

Recent comments

🇺🇸United States freelock Seattle

freelock made their first commit to this issue’s fork.

🇺🇸United States freelock Seattle

My comment in #15 turned out to actually be a bug in Smart Date module, which is fixed in their version 4.1.0.

🇺🇸United States freelock Seattle

I think what you're looking for is not block "access", but rather a "condition" plugin. This section of the manage block display shows core condition plugins that control block visibility.

As I understand it, we could implement entity access for "content_block" entities that might allow you to show/hide blocks anywhere they appear -- but this would only apply to content_block entities, not any other type of block, or custom block plugins, or anything else.

To control whether or not a block appears on a particular page, there are several different ways of showing blocks -- the core block system (which uses condition plugins), Layout Builder (which currently doesn't have any way to add conditions), Context, Display Suite, Panels, Twig Tweak, Ctools... Each of these has its own way of controlling whether or not to show a block.

🇺🇸United States freelock Seattle

So maybe I'm conflating things here, but to me having a module enabled/disabled is not a feature flag.

When I'm thinking of feature flags, I'm thinking of this: https://launchdarkly.com/blog/what-are-feature-flags/ ... specifically NOT config. Disabling a module is a config change, it's not a feature flag that can be done for some users and not others.

I think for 📌 Replace "Expose all fields as blocks to Layout Builder" configuration with feature flag Active having a module to enable/disable functionality seems like a reasonable approach -- but can we please not call it a feature flag? If we do, we're going to confuse a whole lotta devops folks that have an entirely different definition, when they need to work with Drupal.

🇺🇸United States freelock Seattle

Great to hear this is working!

I subscribed to this case looking for more modern CSS support than was available in wkhtmltopdf, and thought WeasyPrint might be a good option. However, since then we've started using headless Chrome, which has fairly decent PDF support.

And there is now an entity_print_chrome module , which has basic functionality working, with the chrome_pdf library.

So now I'm wondering: Should WeasyPrint go into another module, and this module just needs some pointers to the other entity print plugins available?

Or should entity_print_chrome get pulled into this module as a submodule?

I think the big issue here is discoverability -- it took quite some digging to find entity_print_chrome, and I think it should have more visibility, along with WeasyPrint.

If Weasyprint is added to the existing module, will anyone notice and discover it? I'm thinking it will be more noticeable on the project page for entity print...

🇺🇸United States freelock Seattle

I did find myself needing a new feature flag just this week -- for a decoupled website that is getting a design refresh on the front end. I implemented it as a custom condition that I applied to two different blocks, each associated with a different library getting sent to the browser.

The new front end in this case depends upon new fields we needed to deploy so the site owners could populate content and preview it before going live.

So it does seem like there are 3 parts to making a feature flag work:

1. A service that indicates whether a particular feature flag is active or not
2. Code that is controlled by a specific feature flag -- a condition plugin makes this easy ,to show or hide a block, to enable/disable something, etc
3. Mechanisms for enabling/disabling a feature flag.

For #3, having something that can be enabled/disabled as an anonymous user is crucial for the scenarios I've been doing so far. I ended up using a query argument to toggle a feature on or off, and then set a cookie to persist this status across requests.

I think this is an important scenario, to allow testing in production.

Also need to have ways to toggle feature flags per user, per role, site-wide, and perhaps to log places in code where each flag is used, so the code can be cleaned up when the feature is live for everyone if desired...

🇺🇸United States freelock Seattle

The patch is currently working for many tokens, but I have one scenario we need still broken -- trying to get a particular date format on a Smart Date. Haven't had a chance to circle back to fix that.

Otherwise I think it still needs help for non-string props -- I'm wondering if we should attempt to automatically cast whatever is in Fixed Values to a numeric or boolean type? This might be a different issue...

Cheers,
John

🇺🇸United States freelock Seattle

Hi,

I have a different scenario we've started implementing just in the browser -- feature-flags that can be enabled in the browser with a GET param.

We've done this so far for two features:

- Enabling a "control" for toggling dark mode and growing/shrinking text
- Disabling a headless widget so we can more easily test a fallback experience

To do this, I just added some JS at the top of the page to look for a particular query param, and if found/set, store in the browser LocalStorage. Also a remove value. Then, if the localstorage value is enabled, apply a class or do something different in the JS bootstrapping of the widget.

So I was thinking of generalizing this, and found this issue.

Main point: I think there are different scopes to consider for feature flags, and obviously these have different implementations for the developers -- and I wanted to add this browser scope to the list. I'm thinking the base scopes here should include:

- Site-wide -- config and/or state to enable site-wide
- User -- user can enable a feature individually
- Some sort of plugin along the lines of @jurgenhaas mentions, that might be activated through ECA or similar -- which could be extended to support individual groups, roles, etc
- Browser - sessionless -- this can only work client-side, because otherwise it would have an impact on caching
- Browser - cookie/session-based

... a "feature flag" at its base would be one of these types, with a service making it easy to check if a feature is active, and perhaps a lightweight Javascript version for client-side flags.

Then, the feature flag system could provide plugins for reacting to flags -- condition plugins for use in blocks, something to wrap the Javascript attach() behavior, a default CSS class to apply to the body if a feature is active, an ECA condition (not in core, obviously, but if there's a plugin that could be provided)...

Just spit-balling here, expanding the scope slightly, perhaps in a direction that could go in contrib for part of...

Is this relevant?

🇺🇸United States freelock Seattle

We also see this on a site with > 50K links. Sometimes it generates 3 sitemap pages with exactly 10K links each, sometimes 2, sometimes none. When run manually from drush, it generates 6 pages, the first 5 10K links and the remainder on the 6th. But doing this today took 1 hour 24 minutes to complete...

We might try the workaround suggested -- disabling the cron generation and adding our own drush cron job... or give simple_sitemap a try.

🇺🇸United States freelock Seattle

I've spent about 10 hours on this in the past couple days, and I don't think it's possible with the current entity types.

I got quite far down this path -- I did manage to get the field ui showing up on individual group menus, content saving, and more -- which took patches all over the place, all in menu_item_extras.

However, the final thing needed involves loading the bundle from the menu_link_content entity -- and I'm hitting a hard blocker here. Each entity type can define a single "Bundle Entity Type." This gets stored in the entity type definitions, and looked up directly in \Drupal\Core\Entity\EntityType->getBundleEntityType() -- and I don't see any way to override this to allow either "menu" or "group_content_menu" (or anything else) -- menu_item_extras sets this to "menu" in menu_item_extras_entity_type_build(), which stores it in the entity type definition -- and there's nowhere to change it before it gets loaded.

If I remove the bundle_entity_type and add a provider instead, it doesn't load the bundle-specific field configs at all.

So this looks like a hard blocker to making this work at all -- if the menu_link_content could set a bundle_entity_type to one of the entities in this module, I think the rest would fall into place -- but you can't get there from here! At least not with that entity type.

I'm thinking the "correct" solution is to have group_content_menu module define its own menu_link_content entity -- can probably extend the original from core, and set the bundle entity type to the group_content_menu_type -- which would be far better than what I have, trying to jam it onto individual group_content_menus -- I don't see a reason the group_content_menu should be fieldable, I think it makes a lot more sense for the existing field_ui on the group_content_menu_type to apply to its menu_links, not the menu itself.

🇺🇸United States freelock Seattle

Fix pushed.

The original code was checking for an empty value, and replacing with the token name if it is empty. There are clearly values that evaluate to empty that should be posted.

After that, it assumes the value can be evaluated as a string. I'm not sure what other types are being checked here -- if it wasn't a string, then this code wouldn't function at all, unless the substituted value was null, an empty array, or an object that somehow evaluates to empty.

I changed this check to simply check for null. Is there another scenario that needs to be tested here?

🇺🇸United States freelock Seattle

Took the code from @e0ipso's link, for recursive token replacement.

I have this working with fixed_value props -- I did have an issue destructuring the output of the token replacement where my scenario used it, so I think there might be another spot or two where this needs to get fixed as well (the token replacement was wrapped with an extra array).

Still in progress, but partially working...

🇺🇸United States freelock Seattle

I don't understand at this point what the data types for each SDC parameter types are allowed to be -- I patched this as an experiment to see if I could get it to work -- and it does work fine for strings.

I've had a tough time finding any documentation for the SDC interface -- but got errors when declaring a prop as a number so I just changed everything to be a string (aside from the slots).

I've gone through the documentation in this section: https://www.drupal.org/docs/develop/theming-drupal/using-single-director... ... but there's nothing there describing how to use the various prop types, or examples of how to use these effectively...

Is there a good resource describing these types?

`drush generate sdc` prompts for properties and lets you pick among these types:

  1. String
  2. Number
  3. Boolean
  4. Array
  5. Object
  6. Always Null

... I'm thinking if we can detect the prop type here, we can then do some casting (for numeric types) or wrapping (e.g. for array, if the resolved token is a string)

(not sure why there would be an "Always Null" prop -- should this ignore anything assigned, or throw an exception?)

🇺🇸United States freelock Seattle

Merging in for further review and testing, thanks for your work on this!

🇺🇸United States freelock Seattle

Creating a new major version, since Drupal 9 is no longer supported with this change.

🇺🇸United States freelock Seattle

Hi,

Does this change require Drupal 10 only? I see you've removed the 8 and 9 compatibility from the info file...

Cheers,
John

🇺🇸United States freelock Seattle

Hi,

I solved this problem a few years ago using an arbitrary field to hold the absolute stock level (synchronized from another system using an update migration we run every 15 minutes). I added a presave hook to add a stock level adjustment based on the difference between the stock level and my custom field:

/**
 * Implements hook_ENTITY_TYPE_update().
 */
function mymodule_commerce_product_variation_presave(EntityInterface $entity) {
  // Sync up stock level with last imported MAS stock level
  /** @var \Drupal\commerce_stock\StockServiceManagerInterface $stockManager */
  $stockManager = \Drupal::service('commerce_stock.service_manager');
  $currentLevel = $stockManager->getStockLevel($entity);
  if (round($currentLevel, 3) != round($entity->field_absolute_stock_level->value, 3)) {
    $diff = $entity->field_absolute_stock_level->value - $currentLevel;
    $entity->field_stock_level->setValue($diff);
  }
}

Not sure whether this makes sense to turn into an ECA action...

🇺🇸United States freelock Seattle

Added the current entity type to the token browser, and expanded the token replacement to cover view modes -- previously it only applied on field formatters.

🇺🇸United States freelock Seattle

freelock made their first commit to this issue’s fork.

🇺🇸United States freelock Seattle

Super stoked to see this committed!

@quietone @jwilson3 this issue is not strictly related to Views, though views is the easiest place to reproduce the issue.

It requires a query (or view) with a relationship to another table/entity where entity access of some kind is used to filter out rows. I've hit it most often with OG or Group module. In this scenario, the query filters out the wrong items -- usually hiding rows the user should be able to see.

Has anyone actually benchmarked performance regressions with this change? I'm not sure why that belongs in the change record -- using a node access module implies slower performance, but we use this on sites with tens of thousands of affected entities without anything being noticeably slow... I think the change record should focus on it returning correct results.

I've needed to add this patch to dozens of sites over the years...

🇺🇸United States freelock Seattle

@mmatsoo the previous patch may apply, but I don't think it actually works.

We have entirely moved over to Layout Builder Component Attributes for block classes within layouts (we still use Block Class with core block system placements).

On #3395068: Migration from block_class , I've posted code we successfully used to update block_class usage inside all node and taxonomy term layout overrides. This doesn't update block_class usage on "manage display" pages (the templates) -- for those we just searched our config for usage of block_class, moved the classes from the block_class field to the manage attributes field, and then we could export the config. The update function is for content with layout overrides.

Hope that helps anyone else stuck on this!

🇺🇸United States freelock Seattle

Circling back to this, here's code we've used to successfully move block_class classes to layout_builder_component_attributes, for anyone who might need it, wired into hook_update_N() in a custom module install file:

/**
 * Move block_class in custom layouts to layout_builder_custom_attributes.
 */
function mymodule_update_9035(&$sandbox) {
  // we have both taxonomy term and node updates to do:
  $query = \Drupal::entityTypeManager()->getStorage('taxonomy_term')->getQuery();
  $query->condition('layout_builder__layout', '%block_class%', 'LIKE')
    ->accessCheck(FALSE);
  $results = $query->execute();

  foreach ($results as $tid) {
    $term = \Drupal\taxonomy\Entity\Term::load($tid);
    _cgrace_site_bc_to_lbca($term);
  }
  // Now nodes
  $query = \Drupal::entityTypeManager()->getStorage('node')->getQuery();
  $query->condition('layout_builder__layout', '%block_class%', 'LIKE')
    ->accessCheck(FALSE);
  $results = $query->execute();

  foreach ($results as $nid) {
    $node = \Drupal\node\Entity\Node::load($nid);
    _mymodule_bc_to_lbca($node);
  }
}

function _mymodule_bc_to_lbca(\Drupal\Core\Entity\ContentEntityInterface $entity) {
  $changed = FALSE;
  foreach($entity->layout_builder__layout as $delta => $item) {
    /** @var \Drupal\layout_builder\Section $section */
    $section = $item->section;
    $array = $section->toArray();
    foreach ($section->getComponents() as $uuid => $component) {
      $classes = $component->getThirdPartySetting('block_class', 'classes');
      if ($classes) {
        $additional_settings = [
          'block_attributes' => [
            'id' => '',
            'class' => $classes,
            'style' => '',
            'data' => '',
          ],
          'block_title_attributes' => [
            'id' => '',
            'class' => '',
            'style' => '',
            'data' => '',
          ],
          'block_content_attributes' => [
            'id' => '',
            'class' => '',
            'style' => '',
            'data' => '',
          ],
        ];
        $component->set('component_attributes', $additional_settings);
        $component->unsetThirdPartySetting('block_class', 'classes');
        $changed = TRUE;
      }
    }
  }
  if ($changed) {
    $entity->save();
    $message = t('Migrated block class on %type - %id',[
      '%type' => $entity->getEntityTypeId(),
      '%bundle' => $entity->bundle(),
      '%id' => $entity->id(),
    ]);
    \Drupal::messenger()->addMessage($message);
    \Drupal::logger('cgrace')->notice($message);
  }
}

🇺🇸United States freelock Seattle

I was looking into this for a few annoying instances in our sites, and am not finding PHP differences. I'm thinking perhaps database differences? SELECTs with no ORDER BY...

We most often hit this issue on text format filters and static menu overrides. Today I'm seeing it on an entity_view_display layout builder settings.

🇺🇸United States freelock Seattle

> FYI: Herodevs is offering "never ending support" as a vendor . They will be forking the Drupal 7 codebase, providing security vulnerability coverage , compatibility support and have a reporting system.

Huh. Isn't this exactly what Backdrop is?

@klausi

> How do I turn this issue around? I could use some support and positive interaction now, anyone still listening that is supportive of the initiative and wants to leave a comment?

... I'm entirely onboard with your initiative. I'm not a huge fan of D7, but we have a dozen D7 sites we are still supporting, and may get more clients going forward with some of our marketing initiatives -- we're committed to supporting the actual modules our customers are using and still need, until we've moved them off to another platform -- Backdrop, WordPress, or hopefully Drupal 10/11.

🇺🇸United States freelock Seattle

I would be interested in participating/helping out with this initiative, as long as we continue to have Drupal 7 clients. We're down to 11 at the moment, but may pick up some new ones.

🇺🇸United States freelock Seattle

Two core action plugins and one ECA condition plugin are pushed to this issue's fork:

- Action: Add Item to subqueue
- Action: Remove item from subqueue
- ECA Condition: Entity is in subqueue

... I implemented them for a customer's site, and they are working in ECA just fine for us now!

Note that you do need to know the subqueue ID to make use of them (I found it in the database, in entity_subqueue_field_data.queue)

Cheers,
John

🇺🇸United States freelock Seattle

Hi,

Came here looking for ECA plugins to add/remove items from a subqueue... hopefully nobody minds me hijacking the issue....

🇺🇸United States freelock Seattle

freelock made their first commit to this issue’s fork.

🇺🇸United States freelock Seattle

Removing assigned.

🇺🇸United States freelock Seattle

Fixes committed, is working for us now in Drupal 10.1.

🇺🇸United States freelock Seattle

Still throwing an entityQuery missing accessCheck exception. Fix to come shortly.

🇺🇸United States freelock Seattle

Several places missing -- added to the issue fork fixes for the taxonomy_place_example submodule, a removed core function in the SettingsForm, and the composer.json core requirement.

🇺🇸United States freelock Seattle

freelock made their first commit to this issue’s fork.

🇺🇸United States freelock Seattle

I would think it's fine to close this, I'm not hearing any substantive disagreements here...

To clarify, the one thing I would not want to see is adopting some big heavy framework that forces us all into a particular way of doing things -- e.g. React.

I'm all for webcomponents, single-directory components, Svelte and Vue and whatever other components people would like to build in -- I think most of these can coexist and/or complement each other. Let each module choose what's best for their needs -- generally more of this can be done using vanilla js and native browser functionality, keeping the JS layer lean and mean.

I do think there are many admin user stories that would benefit from a rich client layer, though, and I personally greatly prefer JSON:API or a simple JSON endpoint over Drupal's Ajax layer...

🇺🇸United States freelock Seattle

Ha, I've been promoting and using Vue.js for years as a much better, lighter, faster, easier alternative to React. But if there's one JS framework that I see as possibly better than Vue, it's Svelte.

I think React would be a mistake to put in core. If you want reasons, follow Alex Russell -- here's a representative post outlining why the proliferation of front-end frameworks is bad: https://infrequently.org/2023/02/the-market-for-lemons/ -- and see he lists both Svelte and Vue as having the right motivations of putting the end user's experience first, ahead of the developer experience.

Some clearer posts about why: https://thenewstack.io/too-much-javascript-why-the-frontend-needs-to-bui... , https://www.spicyweb.dev/the-great-gaslighting-of-the-js-age/ .

So that said, I would hope that SDCs would support using any of these libraries, and that modules can choose whatever front-end framework makes the most sense to them. That's how this all works, anyway -- the people doing the work make the choices. I'm all in favor of making it easy to make good choices here, and if there is support added for particular frameworks, that choice needs to support goals we all have -- making a great user experience, keep the performance high, stick as close to standards as practical, get wide testing and review before anything happens that might make it exclude other approaches...

🇺🇸United States freelock Seattle

Yes, it can be confusing, which syntax to use where. Mine would look like this:

Method: set only when empty
Strip tags: no
Trim: no
Field name: field_volledig_adres
Save entity: yes
Field value: [node:field_straatnaam:value] [node:field_huisnummer:value]
Entity: [node:content-type:locatie]

... if you had another token set to the correct entity you want to set field_volledig_adres on, you would use that without square brackets for Entity.

The token browser lets you see what tokens are available on each entity type -- but the first part of that token needs to be replaced with the actual token you want to operate on. Many events do create a "node" token for you -- almost all create an "entity" token plus a token with the entity type id.

I highly recommend reading https://ecaguide.org/eca/concepts/tokens/ , and the other docs there, it will help you come up to speed quickly...

🇺🇸United States freelock Seattle

@SirClickalot is the result you expect in the "main" views table, or in the "relationship" table?

From your results, I'm guessing that argument you're passing in is for the "main" table, and you're using the views relationship to get the related nodes.

If so, you should be able to flip this around -- make the main content type the ones you want to return, make the relationship go the other direction, and then in the argument handler, tell it to use the relationship. This should give ECA the related node instead of the original node...

🇺🇸United States freelock Seattle

What is the entity you want to set the field on?

field name: name of the field
entity: entity with that field
value: value to set

🇺🇸United States freelock Seattle

Thanks for the feedback, I'm quite familiar with Commerce.

I set up Commerce Registration as suggested, but I'm thinking this is entirely too much for this site -- they want as simple as possible of a signup -- a simple "select how many people", collect an email address and name for the registration, and send them a confirmation mail.

With Commerce Registration, it looks like you need to supply an email for each attendee, and it's a bunch more steps to first add one seating type and then the next -- and then go through a full commerce checkout to get confirmed.

I did start on this -- adding a config option to work like a "feature flag" to enable multiple registration fields on an entity. I think it would not be too hard to adjust for the scenario we are trying to build -- much of it related to the HostEntity becoming aware of multiple registration fields, and showing the unique info for each of them (we would only want to change the capacity, all other fields would be the same).

It looks like the registration table stores enough info to get the context, as long as each registration field is associated with a unique registration bundle.

That said, after talking with our client, they are going to make a decision on whether or not to move forward to this -- they might opt to keep things simple and just do a single registration type. So we're paused on this at the moment...

🇺🇸United States freelock Seattle

I'm looking at this now -- we have a project for reserving up to 4 slots per person, for a museum. They have 2 accessible seats (wheelchair) per showing, and 25 regular seats.

I'm thinking that most of the work involved looks like changes to the HostEntity wrapper -- creating the ability to manage multiple registration fields, each tied to a registration bundle. For this particular scenario, we want to put a limit on the combined total registered on a single registration, but have distinct capacity, etc.

Anybody have a good alternative to doing this?

🇺🇸United States freelock Seattle

freelock created an issue.

🇺🇸United States freelock Seattle

Moving back to needs review...

🇺🇸United States freelock Seattle

On most of our newer sites, we've switched over to using Layout Builder Component Attributes instead of applying this patch, and that works fine without requiring a core patch -- and does more than this, allowing you to specify id, data- attributes, inline styles on not just the block but also the wrapper and the title (if your theme injects the attributes correctly).

So we're now planning to convert our older sites that used this patch to use that module instead. I've opened an issue #3395068: Migration from block_class there to provide an upgrade path using a custom Drush command, if anyone else is interested.

🇺🇸United States freelock Seattle

Any reason not to bring this into the simple_oauth module?

🇺🇸United States freelock Seattle

Looks like there's a module that provides this for the OAuth2 server module: https://www.drupal.org/project/openid_connect_autodiscovery

🇺🇸United States freelock Seattle

Hi,

This looks like a good fix for the issue, allows you to support this need.

I'm wondering about consistency and user expectations, though, particularly around how these are chained together.

We know that if we're inside a token, you walk these relationships with : (colon) characters. However, if you're not inside a token -- if you're in a Twig context, or Javascript, or other places in ECA that refer to sub-fields, the separator is a . (period).

order_item:purchased_entity:entity:weight:number

vs

order_item.purchased_entity.entity.weight.number

And of course PHP is entirely different, depending on whether you're looking at an array or object, and declaring or assigning...

How is a non-developer supposed to know what to use?

I'm not sure I can justify this, but at least the way I think about it currently, I think we should use : as the separator ONLY INSIDE tokens with square brackets, and if there's no brackets, use dots.

That would just mean adding a string replacement inside your MR to replace colons with dots... This wouldn't actually break the colon syntax, but I think it's a pattern that might be more familiar?

🇺🇸United States freelock Seattle

I have this warning on several sites, and so far have ignored it without any apparent consequences...

🇺🇸United States freelock Seattle

We're hitting this too -- however, in our case, you can't actually set the link attribute to a field -- it only affected layouts with that already set. In our case, this was not saved on the node, but in the State field for layout builder.

Deleting the layout override and avoiding the use of the setting to get the link from a different field allowed us to continue editing...

🇺🇸United States freelock Seattle

I'm hitting this as well -- I've debugged to find that the node lookup is successful, and it returns a double-array as the transformed value after finding the result. However, this does not successfully save in the entity reference.

I tried expanding the definition using a sub_process to set the target_id directly, and didn't get it working at first -- but that's because I didn't map the source field in the sub_process. Once I did that, it's working fine! So this is how you do it -- map the target_id using a subprocess, along these lines:

  field_library:
    -
      plugin: sub_process
      source: field_library_name
      process:
        target_id:
          plugin: entity_generate
          source: value
          value_key: title
          default_values:
            status: 1
          values:
            uid: node_uid
            body: field_library_description
            field_library_type: field_library_type
            field_city: field_city
            field_state: field_state
🇺🇸United States freelock Seattle

It looks like it's just the singe file, src/Plugin/WebformHandler/RestWebformHandler.php, that has CRLF line endings. The rest are all already LF.

🇺🇸United States freelock Seattle

As far as I can tell, it's a very similar situation to CKEditor Accordion, except they chose to make an inline template instead of a modal. I am seeing other examples of modals, though.

🇺🇸United States freelock Seattle

Hmm not sure I have time to take this on, maybe there will be some somewhere.

I wonder if it helps to look at how other modules did it? E.g. I see CKEditor Accordion has a version working for both CKEditor 4 and 5...

🇺🇸United States freelock Seattle

Hi,

The token code we created is necessary to get access to the various things like the dates registration is open, the close date, etc.

We're using it with ECA to make it so a user can edit or delete their registration up to the close time -- and after the close time, ECA prevents them from making any changes to their registration. Our client had had some instances of people deleting their registrations after an event to avoid payment (this client is not using commerce...)

For most of the things we want to do, access to the host entity is necessary, so that's what the token code adds... Otherwise ECA works with registrations already, since it has full support for entities, fields, forms, views, etc.

Cheers,
John

🇺🇸United States freelock Seattle

Hi,

Not an answer to this request, but a suggestion for an alternative: ECA module. ECA does everything Rules does, and the BPMN modeler even lets you do it visually as a flow chart.

We're using ECA with registration for a lot of tasks like the OP. There is one missing bit: ECA relies upon tokens to make entities available. We needed tokens for the host_entity and the registration settings entity, so we added to a custom module:

function mymodule_token_info_alter(&$data) {
  $data['tokens']['registration']['host_entity'] = [
    'name' => t('Host Entity'),
    'description'=> t('Entity that this is attached to (usually node'),
    'module' => 'mymodule',
    'type' => 'node',
  ];
  $data['tokens']['registration']['settings'] = [
    'name' => t('Registration Settings'),
    'description'=> t('The Registration Settings for the entity this registration is attached to'),
    'module' => 'mymodule',
    'type' => 'registration_settings',
  ];
}

function mymodule_tokens_alter(&$replacements, $context, \Drupal\Core\Render\BubbleableMetadata $bubbleable_metadata ){
  if ($context['type'] == 'registration'){
    $tokens = $context['tokens'];
    foreach($tokens as $name => $original){
      if(strpos($name, 'host_entity') === 0){
        $field_tokens = \Drupal::token()->findWithPrefix($tokens, 'node');
        $node = $context['data']['registration']->getHostEntity()->getEntity();
        if ($name =='host_entity'){
          $replacements[$original]= $node -> id();
        } else {
          $replacements += \Drupal::token()->generate('node', $field_tokens, ['node'=> $node ], $context['options'], $bubbleable_metadata);
        }
      }
      if(strpos($name, 'settings') === 0){
        $field_tokens = \Drupal::token()->findWithPrefix($tokens, 'settings');
        $registration_settings = $context['data']['registration']->getHostEntity()->getSettings();
        if($name =='settings'){
          $replacements[$original]= $registration_settings -> id();
        } else {
          $replacements += Drupal::token()->generate('registration_settings', $field_tokens,['registration_settings'=> $registration_settings], $context['options'], $bubbleable_metadata);
        }
      }
      $item = $context;
    }
  }
}
🇺🇸United States freelock Seattle

Hmm not sure whether this should be here or a separate feature request... we're using Easy Email to send various automated mails to users. We've been using views to create a list of recipients, and feeding them into Entity Aware Custom Events, triggering a notification event. This all works great -- however, for each different email template we want to send, we need to create a different custom event which then creates the appropriate email from the easy email template.

Easy Email uses the core entity system, and each template is a new bundle. We could eliminate a lot of extra custom events if we could pass the bundle in as a token.

I like the current ability to select a specific entity type and bundle, but if this is being refactored, it seems like having them split into entity type and bundle fields, with an option to allow the bundle to be specified by a token, would be a nice improvement!

🇺🇸United States freelock Seattle

Ok! Confirmed that the patch works with 1.1.

With the patch, no error, data loads successfully, and access is denied to nodes that the model sends to the failed state.

I removed the patch, and the error reappeared, with no other differences.

🇺🇸United States freelock Seattle

Hmm for some reason I'm not hitting the error anymore -- but the ECA model isn't working, either. I updated my composer.json to skip the core patch and apply this one, and it didn't apply on 1.1, and after upgrading to 1.2.x-dev, the ECA is not taking effect -- I'm seeing rules getting evaluated but it's never executing the final step that sets the AccessResult to forbidden.

Rolling back to 1.1 makes it work again.

Is there some change I would need to make in my model, between 1.1 and 1.2?

🇺🇸United States freelock Seattle

Bumping the priority -- this breaks "composer outdated" entirely, for any site that has Mailchimp installed... even after installing the updated version.

This makes it impossible to discover available releases using composer on any affected site!

🇺🇸United States freelock Seattle

That's definitely the issue - the patch on core there does fix the problem. Really not sure whether there is a better solution -- I'm not seeing anywhere I can wrap or update the context so it doesn't end up as "early rendering".

🇺🇸United States freelock Seattle

...Nudge...

🇺🇸United States freelock Seattle

... and you know, as I think about it, the initial question about what role do you have on the website makes me wonder if it would make sense to actually split the menu into different role-based menus?

Content -- show each entity type as primary menu items. No structure or admin info at all.

Builder -- switch the menu to have all the site-builder stuff -- bundle configs, many module configs, all the stuff on structure and configuration menus

Admin -- Reports, module/theme management, user management

... then have some switcher for users that have multiple roles...

🇺🇸United States freelock Seattle

Done.

I actually really like the current admin menu organization, but when faced with a blank slate, thought it might be good to re-orient for a site user around the main content entities they would expect to see, and nest the corresponding structure across those -- e.g.

Content (nodes, blocks)
Media
Structure (menus, menu items, taxonomies)
Appearance (theme settings, block layouts)

🇺🇸United States freelock Seattle

Maybe Drupal\Component\Utility\DiffArray::diffAssocRecursive() is sorting out these issues internally? Or maybe the $entity->toArray() calls are putting them in the same format?

🇺🇸United States freelock Seattle

Well, our custom plugin does seem to be working fine for us --

We're using it like this:

  1. Update Content Entity event
  2. Entity: Load - token: originalentity, Current scope, latest revision no, unchanged values yes, entity: entity
  3. Load Diffs - token: diffs, return values no, entity: entity, compare: originalentity, exclude fields: changed, a couple others
  4. Compare number of list items - token: diffs, operator: greater than, second value: 0
  5. Tamper: implode - data: [diffs], glue: %n, result_token: diff_text

We're then dropping that in an email notification. It provides a list of fields that have actual changes between the unsaved and saved version of the entity.

We're using it for both user entities and nodes -- looks like one of our node ones is using a list of include fields instead of exclude.

I have not tried to use the values (the return values option), just the keys.

🇺🇸United States freelock Seattle

We've done a dozen Vue projects, mostly using Drupal as a back end. I'm a big fan of Vue. But I'm struggling to see what benefit this would have getting added to core, and if it was added, how it might be used by contrib modules...

In our most recent projects, we're building Vue with our code using Vite. This creates a directory of assets that browsers load directly. We treat these apps as a complete standalone module built all at once, and bootstrapped into the browser all at once.

So if this is for a decoupled admin UI, I would think this would be built for, and within, a single admin theme, or encapsulated by a single module -- making this a module- or theme- specific library, not something directly in core.

If this were added to core, how could contrib benefit from it? What does the build process look like? What if a contrib module is depending on Vue functionality from an older, or newer, version than is in core?

🇺🇸United States freelock Seattle

Ah, cool, thanks!

🇺🇸United States freelock Seattle

@boinkster

1. Yes, your ECA model first needs to create a new entity. Your easy email templates should show up in the create entity dropdown for type/bundle.
2. You can't pass the tokens directly -- what I do is add fields to the email template, which you can then set in ECA before triggering the message send.

🇺🇸United States freelock Seattle

This is a huge help on a Calendar View calendar. On a site we're trying to launch, it was taking 45-60 seconds to render the next month of the calendar.

This is with around 4,000 events in the database, many of them recurring.

Using a profiler, I found the Drupal\smart_date\Plugin\Field\FieldFormatter\SmartDateFormatterBase::formatSmartDate method was called nearly 1/2 million times!

After this patch, it's now getting called ~17K times, and it's down to ~8 seconds to move a month. Still could use improvement, but this was about an 80% improvement!

🇺🇸United States freelock Seattle

Hi,

Just saw this issue referenced on an old Slack thread -- I did end up creating a custom action plugin for our clients, so this can be the start of a more generic one.

With this implementation, you can get either a list of fields that differ, or a list of the different values with the field names as keys. You can pass in two entirely different entities, or an entity and its :original.

We are only using this to get a list of field names, I have not tested the value option.


namespace Drupal\my_module\Plugin\Action;

use Drupal\Component\Utility\DiffArray;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\eca\Plugin\Action\ConfigurableActionBase;

/**
 * Provides a Load Diffs action.
 *
 * @Action(
 *   id = "my_module_load_diffs",
 *   label = @Translation("Load Diffs"),
 *   description = @Translation("Compare 2 entities and return a list of fields that differ"),
 *   type = "entity"
 * )
 *
 */
class LoadDiffs extends ConfigurableActionBase {

  /**
   * {@inheritDoc}
   */
  public function defaultConfiguration(): array
  {
    return [
      'token_name' => '',
      'compare' => null,
      'return_values' => FALSE,
      'exclude_fields' => [],
      'include_fields' => [],
    ] + parent::defaultConfiguration();
  }

  /**
   * {@inheritdoc}
   */
  public function buildConfigurationForm(array $form, FormStateInterface $form_state): array {
    $form['token_name'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Name of token'),
      '#default_value' => $this->configuration['token_name'],
      '#description' => $this->t('Provide the name of a token that holds the new list.'),
      '#weight' => -60,
    ];
    $form['compare'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Compare'),
      '#description' => $this->t('Provide the name of a token that holds the original entity.'),
      '#default_value' => $this->configuration['compare'],
      '#weight' => 30,
    ];
    $form['return_values'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Return values'),
      '#default_value' => $this->configuration['return_values'],
      '#description' => $this->t('If checked, the list will return values. If unchecked, it will only return the different machine names of changed fields.'),
    ];
    $form['exclude_fields'] = [
      '#type' => 'textarea',
      '#title' => 'Exclude fields',
      '#description' => $this->t('List field machine names to remove from difference/ignore'),
      '#default_value' => implode("\n", $this->configuration['exclude_fields']),
      '#weight' => 40,
    ];
    $form['include_fields'] = [
      '#type' => 'textarea',
      '#title' => 'Include fields',
      '#description' => $this->t('List field machine names to include in difference -- all others will be ignored.'),
      '#default_value' => implode("\n", $this->configuration['include_fields']),
      '#weight' => 40,
    ];
    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function submitConfigurationForm(array &$form, FormStateInterface $form_state): void
  {
    $this->configuration['token_name'] = $form_state->getValue('token_name');
    $this->configuration['compare'] = $form_state->getValue('compare');
    $this->configuration['return_values'] = $form_state->getValue('return_values');
    $this->configuration['exclude_fields'] = explode("\n", $form_state->getValue('exclude_fields'));
    $this->configuration['include_fields'] = explode("\n", $form_state->getValue('include_fields'));
    parent::submitConfigurationForm($form, $form_state);
  }

  /**
   * {@inheritdoc}
   */
  public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) {
    /** @var ContentEntityInterface $object */
    $access = $object->access('view', $account, TRUE);
    return $return_as_object ? $access : $access->isAllowed();
  }

  /**
   * {@inheritdoc}
   */
  public function execute($entity = NULL) {
    $compare = $this->tokenServices->getTokenData($this->configuration['compare']);
    $diff = DiffArray::diffAssocRecursive($entity->toArray(), $compare->toArray());
    $exclude_fields = $this->configuration['exclude_fields'];
    if (count($exclude_fields)) {
      foreach ($exclude_fields as $field) {
        unset($diff[$field]);
      }
    }
    $include_fields = $this->configuration['include_fields'];
    if (count($include_fields)) {
      $included = [];
      foreach ($include_fields as $field) {
        if (isset ($diff[$field])) {
          $included[$field] = $diff[$field];
        }
      }
      $diff = $included;
    }
    if (!$this->configuration['return_values']) {
      $diff = array_keys($diff);
    }
    $this->tokenServices->addTokenData($this->configuration['token_name'], $diff);
  }

}
🇺🇸United States freelock Seattle

With ECA you can switch users, to execute any action as user 1 - and this is pretty routine. You can capture the previous user in a token, too. I do think a specific permission to send an email makes sense, but ECA can work around it either way...

🇺🇸United States freelock Seattle

No, I would recommend moving it to a core action. There's nothing needed from the ECA base class...

🇺🇸United States freelock Seattle

Here's an example plugin I have working in another project. This works fine in custom code, but if it's added here as is, it would require adding a dependency on ECA, because without it you will get a whitescreen -- this is because ECA Actions share the same namespace as core actions, so the plugin does get loaded on cache rebuilds, and if the base Action plugin from ECA isn't found, Drupal is not happy.

So this either needs to be in a submodule with an ECA dependency, or should get adjusted to be a native Drupal action...


namespace Drupal\my_module\Plugin\Action;

use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\easy_email\Service\EmailHandlerInterface;
use Drupal\eca\EcaState;
use Drupal\eca\Plugin\Action\ActionBase;
use Drupal\eca\Token\TokenInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Provides a Send Easy Email action.
 *
 * @Action(
 *   id = "my_module_send_easy_email",
 *   label = @Translation("Send Easy Email"),
 *   type = "easy_email",
 *   category = @Translation("Custom")
 * )
 *
 */
class SendEasyEmail extends ActionBase implements ContainerFactoryPluginInterface {

  /**
   * @var EmailHandlerInterface
   */
  protected EmailHandlerInterface $emailHandler;

  public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, TokenInterface $token_services, AccountProxyInterface $current_user, TimeInterface $time, EcaState $state, EmailHandlerInterface $emailHandler) {
    parent::__construct($configuration, $plugin_id, $plugin_definition, $entity_type_manager, $token_services, $current_user, $time, $state);
    $this->emailHandler = $emailHandler;
  }

  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): ActionBase {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('entity_type.manager'),
      $container->get('eca.token_services'),
      $container->get('current_user'),
      $container->get('datetime.time'),
      $container->get('eca.state'),
      $container->get('easy_email.handler')
    );
  }

  /**
   * {@inheritdoc}
   */
  public function access($email, AccountInterface $account = NULL, $return_as_object = FALSE) {
    /** @var \Drupal\node\NodeInterface $node */
    if ($return_as_object) {
      return AccessResult::allowed();
    }
    return TRUE;
  }

  /**
   * {@inheritdoc}
   */
  public function execute($entity = null) {
    $this->emailHandler->sendEmail($entity);
  }

}
🇺🇸United States freelock Seattle

@Kaushik1216 it's failing on the version constraint in composer.json:

"drupal/core": "^9.2",

... that needs to get changed to match the value in easy_email.info.yml,

"drupal/core": "^9.2|^10",

🇺🇸United States freelock Seattle

Hi,

I'm not sure I was hitting the same issue, but I found something that makes me wonder how people could use this for megamenus at all. After spending hours in a debugger on a site with thousands of menu links, I got to the bottom of our problem at least.

On the site in question, we had been using layout builder to make a megamenu with a bunch of menu blocks, loading from another menu. This worked fine -- until the client went to add some inline blocks. This also worked fine -- for admins -- nobody else could see the blocks at all.

It all boils down to this code in \Drupal\menu_link_content\MenuLinkAccessControlHandler::checkAccess():

  protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
    switch ($operation) {
      case 'view':
        // There is no direct viewing of a menu link, but still for purposes of
        // content_translation we need a generic way to check access.
        return AccessResult::allowedIfHasPermission($account, 'administer menu');

"administer menu" permission is needed to view menu_link_content entities, as well as MenuItemExtrasMenuLinkContent, which extends them...

And Layout Builder's inline block entity checks for dependent permissions before rendering -- e.g. to render an inline block, the user needs a positive permission to view the block itself AND to view the entity containing the layout. (I guess menu_blocks don't enforce this...).

Because only users with "administer menus" had a view permission on the menu link content entity, that made it so no normal user can see regular inline blocks added to a menu link using Layout Builder.

The fix was easy -- implement a hook_ENTITY_TYPE_access hook to grant this view access. Here's sample code:

/**
 * Implements hook_ENTITY_TYPE_access().
 */
function my_module_menu_link_content_access(\Drupal\Core\Entity\EntityInterface $entity, $operation, \Drupal\Core\Session\AccountInterface $account) {
  if ($operation == 'view') {
    return \Drupal\Core\Access\AccessResult::allowed();
  }
  return \Drupal\Core\Access\AccessResult::neutral();
}

... since doing this is one of the main goals for this module, would it make sense to add?

Production build 0.69.0 2024