Able to duplicate single instance child XBlocks within a Nested XBlock in Studio

Hello, we have created a Nested XBlock and configured some of the available child XBlock types with single_instance=True as shown below.

class CaseReportXBlock(StudioContainerWithNestedXBlocksMixin, XBlock):
    display_name = String(
        display_name="Display Name",
        help="The display name for this component.",
        scope=Scope.content,
        default="Case Report",
    )

    completion_mode = XBlockCompletionMode.AGGREGATOR
    show_in_read_only_mode = True
    icon_class = "other"

    @property
    def allowed_nested_blocks(self):
        from ..case_file import AmbraCaseFileXBlock, CaseFileXBlock, CaseHistoryXBlock
        from ..case_report import CaseReportSubmissionXBlock

        return [
            NestedXBlockSpec(
                CaseHistoryXBlock,
                label="Case History",
                category="case_history",
                single_instance=True,
            ),
            NestedXBlockSpec(
                CaseFileXBlock,
                label="Case File (URL)",
                category="case_file",
            ),
            NestedXBlockSpec(
                AmbraCaseFileXBlock,
                label="Case File (Ambra)",
                category="ambra",
            ),
            NestedXBlockSpec(
                CaseReportSubmissionXBlock,
                label="Case Report Submission",
                category="case_report_submission",
                single_instance=True,
            ),
        ]

Using single_instance=True, we are able to successfully disable the buttons for the XBlock types in the “Add New Component” section, however this does not disable the ability to duplicate already existing XBlocks providing a way around the “single instance” configuration as shown in the screenshot below.

Based on my initial investigation, whether or not an XBlock can be duplicated is controlled by the can_add context in this template edx-platform/cms/templates/studio_xblock_wrapper.html at 5071f28e20055f7039326cd0d2e2f8ca933217ed · openedx/edx-platform · GitHub, which is appears to be added to the context here edx-platform/cms/djangoapps/contentstore/views/preview.py at 5071f28e20055f7039326cd0d2e2f8ca933217ed · openedx/edx-platform · GitHub.

Setting can_add to false does successfully remove the duplicate option, but it also removes the delete option from the menu (this also comes from studio_xblock_wrapper.html). While not ideal, this is something we could likely find a work around for. However, it is not clear how to set this can_add context for a given XBlock within the nested spec since depending on the XBlock type we may want to allow for multiple/duplication if it’s a direct child of a Vertical XBlock for instance.

I had some luck overriding the render method of a child XBlock to update the context, however that unexpectedly prevented duplication for all children within the nested XBlock and the parent nested XBlock as well instead of just the XBlock type I modified.

It’s worth noting that we are on the Redwood 3 release and are still using old Studio, we have not made the move to the Content Authoring MFE.

If anyone has experience with nested XBlocks and has suggestions, they would be greatly appreciated.

We made StudioContainerWithNestedXBlocksMixin 10 years ago for the “Problem Builder” block, and it received some minor maintenance 6 years ago during a Django upgrade, but otherwise nobody has really touched that code in a long time and I didn’t realize it was even being used by any other XBlocks. That’s also why the UI looks terrible. What’s more, I’m sorry to be the bearer of bad news, but “nested XBlocks” have a number of performance problems so our plan for some time has been to eventually remove support for nested XBlocks completely. This post made me realize that we haven’t yet clearly indicated that in the code/documentation, so we should do that ASAP (CC @kmccormick).

The issue you’re having is simply a bug, and your initial investigation is roughly on track. I don’t think there’s any way to work around it without actually modifying the platform code to fix the bug, by introducing some more detailed “can duplicate” context that’s specific to each block or something along those lines. However, given the plans to deprecate all this functionality entirely, it may not be worth investing in that.

You can of course continue along this path and find a way to either fix the bug or hack around it, and you should have something that works for now - but realize that it will most likely need a significant refactor in a year or two. So if you have the capacity, I’d recommend finding a way to achieve the same thing without using nested XBlocks, e.g. by storing the “case history”, “case files”, and “case reports” as XBlock fields on the main XBlock or using a Django model if necessary.

Some people in the community are also working on react-based XBlock UIs and an overhaul of the XBlock API to address some of the longstanding pain points with XBlock development such as this.

@braden Thank you for your reply, I’ll follow up with my team to find a path forward for our use cases. It’s good to have the heads up on the planned deprecation.

@braden Follow up question from my team: will the deprecation plan include a migration strategy?

Well, we thought problem builder was the only XBlock using this capability, and we have started the process of planning a migration strategy for it: Future Directions for Problem Builder

We can certainly make recommendations but they would likely be what I just said above: moving from multiple child blocks to one XBlock with a more complex data structure to represent it. We also hope to have an improved XBlock v2 API that offers more simplicity and flexibility to XBlock authors, but haven’t made much progress on that yet.

There are also some built-in XBlocks like A/B testing, randomization, etc. that use XBlock children but aren’t using StudioContainerWithNestedXBlocksMixin and there is a migration plan being developed for them - moving their logic into a new “selector” API at the unit level.