Only allow deleting Code Components if there's 0 usages

Created on 13 June 2025, 3 months ago

Overview

Quoting @catch and I from #3528723-10: Component::onDependencyRemoval() should be an uninstall validator if the dependency being removed is a module or theme โ†’ :

Reviewed this together with @catch.

The behavior in HEAD is that a code component (JavaScriptComponent) that is in use in e.g. 1 article and 1 page can still be deleted. It requires a little bit of shenanigans though: you have to open a content entity in XB that does not contain an instance of that code component!

If such an instance is present, you do get a nice error message/confirmation dialog:

On all other content entities (i.e. any with an XB component tree but without this particular code component), the deletion will proceed unceremoniously ๐Ÿ˜… โ‡’ It's far too easy to shoot yourself in the foot right now!

The changes in this MR remove the "shoot yourself in the foot" case for:

  • uninstalling modules providing block plugins
  • uninstalling themes/modules providing SDCs

๐Ÿ‘

BUT concerns remain regarding code components โ€” because this MR doesn't change anything (intentionally!) for those.

@catch and I agreed that:

  1. A confirmation dialog like the above should ALWAYS appear
  2. It should inform the user of how many entities/revisions would be impacted
  3. It should inform the user that if they proceed all existing content will look broken.

IOW: allow it only for developers who really know what they're doing, but present the consequences as explicitly as possible.

This strikes the middle ground of desire for reliability+consistency (@catch and I) vs pragmaticness when developing code components (@lauriii's concern).

In the future, if Drupal core gets a "developer mode", we could adjust the behavior based on that โ€” see โœจ Add a Production/Development Toggle To Core Needs work .

+

After some more brainstorming with @catch, we think we thought of an alternative approach that actually is better still:

  1. add a soft_deleted: true|false config entity property to JavaScriptComponent, which defaults to false
  2. make the delete operation either:
    1. : actually delete (as is the case today)
    2. : set soft_deleted: true (๐Ÿ†•)
  3. make XB in all its API responses pretend this config entity does not exist
  4. โ€ฆ then just respect all config dependency management as per usual

This approach would mean that the associated Component config entity would still exist, the JS code-on-disk powering it would also still exist, and hence all existing usages of this code component would continue to work exactly as before.

P.S.: yet another alternative approach could be that we make JavaScriptComponent implement VersionedConfigEntityInterface (new since ~2 weeks) to keep the underlying config entity around, and only allow 2 versions: whichever one is the active one, plus a special deleted/trashed version that gets treated everywhere in XB as if it doesn't exist. That way, we would not have to delete

Proposed resolution

Discuss the nuances, but prevent the deleting JavaScriptComponent config entities to comply with config management best practices. We now have usage data, versioned config entity types, etc. โ€” multiple additional pieces of infrastructure that should enable a better UX.

User interface changes

๐Ÿ“Œ Task
Status

Active

Version

0.0

Component

Theme builder

Created by

๐Ÿ‡ง๐Ÿ‡ชBelgium wim leers Ghent ๐Ÿ‡ง๐Ÿ‡ช๐Ÿ‡ช๐Ÿ‡บ

Live updates comments and jobs are added and updated live.
  • Usability

    Makes Drupal easier to use. Preferred over UX, D7UX, etc.

Sign in to follow issues

Merge Requests

Comments & Activities

  • Issue created by @wim leers
  • ๐Ÿ‡ฌ๐Ÿ‡งUnited Kingdom catch
  • ๐Ÿ‡ง๐Ÿ‡ชBelgium wim leers Ghent ๐Ÿ‡ง๐Ÿ‡ช๐Ÿ‡ช๐Ÿ‡บ
  • ๐Ÿ‡ง๐Ÿ‡ชBelgium wim leers Ghent ๐Ÿ‡ง๐Ÿ‡ช๐Ÿ‡ช๐Ÿ‡บ
  • ๐Ÿ‡ง๐Ÿ‡ชBelgium wim leers Ghent ๐Ÿ‡ง๐Ÿ‡ช๐Ÿ‡ช๐Ÿ‡บ
  • ๐Ÿ‡ง๐Ÿ‡ชBelgium wim leers Ghent ๐Ÿ‡ง๐Ÿ‡ช๐Ÿ‡ช๐Ÿ‡บ
  • Scenario that needs to be addressed with this fix :

    • Create code component named {code01}
    • Create code component named {code02}
    • Import code01 to code02
    • Delete code01 able to delete code01
    • Code02 gives error as below

  • ๐Ÿ‡ซ๐Ÿ‡ฎFinland lauriii Finland

    Could we make the soft delete as an explicit feature? I.e. hiding modules from the UI. I think this would be really useful in the beginning until we can associate which users can use which components. Tagging with needs follow-up for that.

    We could still offer the delete as an option because there are scenarios where it's desired to delete a component and all of its uses altogether. However, there's a difference between content vs components referring to components. I'd say that uses of a component in another component should prevent deleting, but deleting a component used in content should not prevent the deletion (especially because even past revisions would prevent deletion).

    What would be extremely useful is having a list of usages of the component. This is not only useful for the delete use case but also when you want to assess how components are being adopted.

  • ๐Ÿ‡ช๐Ÿ‡ธSpain penyaskito Seville ๐Ÿ’ƒ, Spain ๐Ÿ‡ช๐Ÿ‡ธ, UTC+2 ๐Ÿ‡ช๐Ÿ‡บ

    I tested #7.

    To clarify, this is not an issue when the components are stored (e.g. I publish them, even without "adding to components" aka status=1), only on the transient time when they are in auto-save, not published, not status=1, as we do this validation on config entity dependencies. That part IMHO is Works as designed, as the preview message is informative enough to fix the situation.

    The original purpose of this issue still remains though.

  • ๐Ÿ‡ง๐Ÿ‡ชBelgium wim leers Ghent ๐Ÿ‡ง๐Ÿ‡ช๐Ÿ‡ช๐Ÿ‡บ

    It seems this is kind of a duplicate of โœจ Usage info for code components with unpublished changes Postponed (because it's hard to find).

    @lauriii in #8:

    What would be extremely useful is having a list of usages of the component.

    We already have that infrastructure and information, since ๐Ÿ“Œ Calculate field and component dependencies on save and store them in an easy to retrieve format Active . We just don't have it in the modal dialog, which is what I suspect you're hinting at?

    If so, that makes this even more closely related to โœจ Usage info for code components with unpublished changes Postponed !

    Could we make the soft delete as an explicit feature?

    Yes, but what does "explicit feature" mean? ๐Ÿ‘ˆ confused me even more, perhaps you meant "components"?

  • ๐Ÿ‡ซ๐Ÿ‡ฎFinland lauriii Finland

    Moving to critical due to this causing data integrity issues.

  • ๐Ÿ‡ณ๐Ÿ‡ฑNetherlands balintbrews Amsterdam, NL

    Because of the data integrity issues, I wonder if we could prioritize the simplest possible change before we settle on the final solution, including soft-deletion etc.

    I think it would be the following:

    When users try to delete or make a component internal (i.e. remove from components), we currently do that check to see if the component is being used in the current page's component tree. We can now do that properly thanks to ๐Ÿ“Œ Calculate field and component dependencies on save and store them in an easy to retrieve format Active . I don't see an HTTP API for the audit info, so unless I'm mistaken, that would need to happen before we update the check on the UI.

    Do we already account for first-party code component imports (JavaScriptComponent dependencies) in the audit logic?

  • ๐Ÿ‡บ๐Ÿ‡ธUnited States mglaman WI, USA

    I think component imports should be handled via \Drupal\experience_builder\Audit\ComponentAudit::getConfigEntityDependenciesUsingComponent in the audit logic.

  • ๐Ÿ‡ฌ๐Ÿ‡งUnited Kingdom thoward216
  • ๐Ÿ‡ง๐Ÿ‡ชBelgium wim leers Ghent ๐Ÿ‡ง๐Ÿ‡ช๐Ÿ‡ช๐Ÿ‡บ

    I'm sorry, but I can't help but question the current MR direction (which I've not been involved in). ๐Ÿ™ˆ

    Questions

    I don't quite understand 2 fundamentals:

    1. Why we need a dry-run operation. Because that would then imply we would want to do this for many (config) entities Canvas provides! Do we really want to build such infrastructure multiple times over?
    2. How would such a dry-run operation allow the "delete" operation to be available or not in the UI? Is the UI going to do a "Delete (checkingโ€ฆ)" disabled contextual menu first, fire a request to the server, wait for the response, and then either change it to "Delete" or "Delete (not possible, see usage)"? ๐Ÿค”

    Potential alternative

    Why not:

    1. Update access check logic at \Drupal\canvas\EntityHandlers\CanvasConfigEntityAccessControlHandler::checkAccess() to prevent deletion if there's >0 usages returned by \Drupal\canvas\Audit\ComponentAudit::hasUsages()
    2. ๐Ÿ‘ˆ presumably too slow?
    3. Provide 3 internal HTTP APIs:
      1. /canvas/api/v0/usage/{canvas_config_entity_type_id}/{canvas_config_entity} API, which provides a simple boolean true/false response
      2. /canvas/api/v0/usage/{canvas_config_entity_type_id}/{canvas_config_entity}/details API, which provides not a simple boolean true/false response, but the full breadth and depth of information that \Drupal\canvas\Audit\ComponentAudit can
      3. a sibling /canvas/api/v0/usage/{canvas_config_entity_type_id}/{canvas_config_entity_ids} which accepts a comma-separated list of config entity IDs (say, up to 50), to bulk-check?
      4. OR a sibling /canvas/api/v0/usage/{canvas_config_entity_type_id}?page=N which allows iterating over all is paginated-per-50 and lists an object like {id1: true, id2: false, โ€ฆ, id50: true} aka booleans-per-known config entity of this type, to bulk-check?

      Either of the 2 bulk-check ones could then be used by the UI to solve problem 2 above, and the detailed one could be used to (in the future) provide detailed information to the person trying to delete it.

      This could then be generalized to work for all config entity types automatically: JavaScriptComponent, ContentTemplate, etc.

      This proposed alternative is AFAICT what @balintbrews proposed in #12.

      The MR does not tackle the issue summary, which is about soft-deleting, so we need the issue summary to be overhauled to reflect the updated direction โ€” also to ensure we don't forget about the original scope.

  • ๐Ÿ‡ฌ๐Ÿ‡งUnited Kingdom thoward216

    Thanks @wimleers, its good to question and after reviewing #16, this seems clearer to me. I've started implementing as per #16.
    My only thought is that we currently only have audit data for components - but is there a view to extend that in the future and the reason that we should keep these endpoints more general?

  • ๐Ÿ‡บ๐Ÿ‡ธUnited States effulgentsia

    Not every stable blocker needs to block an RC, but this one does.

  • ๐Ÿ‡ฌ๐Ÿ‡งUnited Kingdom thoward216

    I have implemented the following endpoints as per #16:

    1. /canvas/api/v0/usage/{canvas_config_entity_type_id}/{canvas_config_entity} API, which provides a simple boolean true/false response
    2. /canvas/api/v0/usage/{canvas_config_entity_type_id}/{canvas_config_entity}/details API, which provides detailed of information that \Drupal\canvas\Audit\ComponentAudit can
    3. /canvas/api/v0/usage/{canvas_config_entity_type_id}?page=N which allows iterating over all is paginated-per-50 and lists an object like {id1: true, id2: false, โ€ฆ, id50: true}

    I've updated the access check in VisibleWhenDisabledCanvasConfigEntityAccessControlHandler rather than in CanvasConfigEntityAccessControlHandler which may not be correct, but we'd have to inject component audit into the other handlers that extend this which I'm not sure makes sense as its quite specific at the moment.

    The above update in the access check resolves the scenario where you have a Code component added to your library that is in use on a canvas page for example and attempt to delete it, previously this would delete the code component.

    I've tested #7 and this scenario looks to have been accounted for before this MR.

  • ๐Ÿ‡ฌ๐Ÿ‡งUnited Kingdom thoward216
  • ๐Ÿ‡ฌ๐Ÿ‡งUnited Kingdom thoward216

    Marking as needs review for an initial review and a few questions on the MR.

Production build 0.71.5 2024