Open a Content Editor modal from a property editor (editorService.open alternative)

Hi there,

I’m migrating a custom property editor, from Umbraco 12 (AngularJS) to Umbraco 15+.

Background info: wrapping AngularJS (please, don’t shoot me)
I’ve created a wrapper, which loads AngularJS inside of the web component. That actually works pretty well. Saved me a lot of time, too.

To access Umbraco functions, I bubble events back to the WebComponent, which calls imported Umbraco components (see an example below).

Legacy solution
In AngularJS (U12), the editorService can be used to open a sidebar / modal to edit a particular node. I’m looking for an alternative for Umbraco 15+.

Content picker (but not editor!)
In my property editor script file, I can open a Content Picker with this function:

import { UMB_MODAL_MANAGER_CONTEXT as mm } from '@umbraco-cms/backoffice/modal';
import { UMB_DOCUMENT_PICKER_MODAL } from '@umbraco-cms/backoffice/document';

async openContentPicker(options) {
    // get context and open modal
    const modalContext = await this.getContext(mm);
    const modalHandler = modalContext?.open(this, UMB_DOCUMENT_PICKER_MODAL, {
        data: {
            modal: {
                type: "sidebar",
                size: "small"
            },
            hideTreeRoot: true,
            multiple: options.multiPicker,
            filter: options.filter,
            filterExclude: false,
            startNodeId: options.startNodeId
        }
    });

    // modal event handlers (submit / cancel)
    modalHandler.onSubmit().then((selection) => {
        if (options.submitCallback) {
            options.submitCallback?.(selection);
        }
    }).catch(() => {
        if (options.closeCallback) {
            options.closeCallback?.();
        }
    });
}

Is there a way to open a ‘content editor’ in a similar way? Or do I have to navigate to a particular route by using UMB_ROUTE_CONTEXT?

Example
An example of what I’m trying to accomplish, can be found in the Media-section:

  • Open a media item
  • Click the ‘Info’ tab
  • Find the ’ Referenced by the following items’ panel
  • Click one of the references, so a sidebar / modal opens where you can edit the node

It’s a built in function of Umbraco, so I assume it’s possible…

Please help!
I’ve spent quite some time sniffing the network tab in Chrome and scrolling through the documentation and source code, but can’t find a solution so far.

Would be nice if somebody could help!

Best wishes,
Koopa

Hey @itsmekoopa, without seeing the rest of the code, I’m not 100% sure, but have you have you had a look at the consumeContext functionality? Consume a Context | Umbraco CMS

This might be what you’re looking for in terms of navigate the context to get what you need.

Hi Mike, thank you. Yes, I hope there’s a context, similar to UMB_DOCUMENT_PICKER_MODAL, which lets me open a ‘edit document’ modal.

I couldn’t find one so far, unfortunately…

Could it be a workspace context that you’re looking for? Workspace Context | Umbraco CMS

Apologies if this isn’t much help by the way, it’s still a little tricky finding the right info isn’t it!

When you click a “referenced content item” link on a media item, it appears to open a workspace (it’s displayed in the url bar). However, I couldn’t find the right files to reference, so I can establish such a link myself.

Yes, pretty tough making progress this way.

It’s all trial & error, although the question is pretty straightforward: how can I open a content editor (and pass a NodeId to it) and edit a node, from a property editor.

Best guess so far is that I have to open a (routed) modal, which contains a workspace for the entity type ‘document’.

My product (and thus my property editor) is relying heavily on editing nodes in a modal / infinite editor, so it’s a “must fix” before I can upgrade to U15/16!

Any MVP or expert willing to help me out? :folded_hands:

(on the bright side… figuring out how to refresh the content tree was done in 10 mins) :slight_smile:

Anyone able to help me out, please? Still looking for an answer, but unable to find one so far. :folded_hands:

Should be pretty easy to fix for a core dev or experienced package developer, I think.

Additionally, these examples might help some users in the future:

Refresh the content tree

import { UMB_ACTION_EVENT_CONTEXT } from '@umbraco-cms/backoffice/action';

async refreshContentTree(options) {
        // get the context to refresh
        const refreshContext = await this.getContext(UMB_ACTION_EVENT_CONTEXT);

        // compose an event
        const refreshEvent = new UmbRequestReloadChildrenOfEntityEvent({
            unique: options.id,
            entityType: 'document',
        });

        // dispatch the event
        refreshContext.dispatchEvent(refreshEvent);
    }

Show a notification

import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification';

async showNotification(type, headline, message) {
        // get notification context
        const notificationContext = await this.getContext(UMB_NOTIFICATION_CONTEXT);

        // send notification
        notificationContext.peek((type === 'success' ? 'positive' : type === 'error' ? 'danger' : 'warning'), {
            data: { headline: headline, message: message }
        });
    }

Open a content picker (modal)

import { UMB_MODAL_MANAGER_CONTEXT as mm } from '@umbraco-cms/backoffice/modal';
import { UMB_DOCUMENT_PICKER_MODAL, UMB_CREATE_DOCUMENT_WORKSPACE_PATH_PATTERN } from '@umbraco-cms/backoffice/document';

async openContentPicker(options) {
    // get modal context
    const modalContext = await this.getContext(mm);

    // open document picker modal
    const modalHandler = modalContext?.open(this, UMB_DOCUMENT_PICKER_MODAL, {
        data: {
            hideTreeRoot: true,
            multiple: options.multiPicker,
            filter: options.filter,
            filterExclude: false,
            startNodeId: options.startNodeId
        },
        modal: {
            type: 'sidebar',
            size: 'small',
        }
    });

    // modal event handlers (submit / cancel)
    modalHandler.onSubmit().then((selection) => {
        if (options.submitCallback) {
            options.submitCallback?.(selection);
        }
    }).catch(() => {
        if (options.closeCallback) {
            options.closeCallback?.();
        }
    });
}

Open a content sorter (modal)

import { UMB_MODAL_MANAGER_CONTEXT as mm } from '@umbraco-cms/backoffice/modal';
import { UMB_SORT_CHILDREN_OF_MODAL } from '@umbraco-cms/backoffice/tree';

async openContentSorter(options) {
    // get modal context
    const modalContext = await this.getContext(mm);
    
    // open sorter modal
    const modalHandler = modalContext?.open(this, UMB_SORT_CHILDREN_OF_MODAL, {
        data: {
            unique: options.id,
            entityType: 'document',
            treeRepositoryAlias: 'Umb.Repository.Document.Tree',
            sortChildrenOfRepositoryAlias: 'Umb.Repository.Document.SortChildrenOf'
        },
        modal: {
            type: 'sidebar',
            size: 'small',
        }
    });

    // modal event handlers (submit / cancel)
    modalHandler.onSubmit().then((selection) => {
        if (options.submitCallback) {
            options.submitCallback?.(selection);
        }
    }).catch(() => {
        if (options.closeCallback) {
            options.closeCallback?.();
        }
    });
}

Hello, I was wondering if there’s somebody willing to work this issue out with me. Maybe an Umbraco dev?

I’ve upgrade my project to the latest v16, but still no progress…

Hi @itsmekoopa

Without diving into your specific case, I do feel quite sure that the problem is that Workspaces(content editor) are using routes as well. A Modal that needs to utilize Routes needs to be opened as a Routable Modal.

Which requires the Modal to be setup initialy, and instead opened via a Path.
You can dive into our code to see various ways of how this can be done.

But I will scrap together a snippet for you:

In your constructor, set up the Routable Modal:

new UmbModalRouteRegistrationController(this, UMB_WORKSPACE_MODAL)
				.onSetup(async () => {
					return { data: { entityType: UMB_DOCUMENT_TYPE_ENTITY_TYPE, preset: {} } };
				})
				.observeRouteBuilder((routeBuilder) => {
					this._editContentTypePath = routeBuilder({});
				});

But the path(_editContentTypePath) to the modal is not enough, so we need to implement a method to ensure the route includes the parts for editing a specific Document Type.

#getWorkspaceHref(docTypeUnique) {
		if (!docTypeUnique) return;
		const path = UMB_EDIT_DOCUMENT_TYPE_WORKSPACE_PATH_PATTERN.generateLocal({ unique: docTypeUnique });
		return `${this._editContentTypePath}/${path}`;
	}

And then you can use this method to get the path as part of your rendering:

return html`
			<a href=${ifDefined(this.#getWorkspaceHref('insert-guid-for-the-documenttype-to-edit'))}>Open workspace</a>
		`;

I hope that will help you move forward.

2 Likes

Awesome, thank you @nielslyngsoe! With the help of your code, I’m able to open a document type edit modal.

Next challenge is to open a document edit modal. Seems to be matter of using the right imports.

I’ll post a working example when I’m able to figure that out. :slight_smile:

1 Like

Well, finally figured it out! :partying_face:

Thanks for helping me out, @nielslyngsoe !

Video of the plugin

For those interested, here’s a video of the plugin I’ve upgraded (running Umbraco 12, but you’ll get the idea):

Working example

Here’s a working example how to open a node editor from a plugin, quite similar to the ancient AngularJS ‘editorService’ from Umbraco 7-12.

Place necessary imports

import { UmbModalRouteRegistrationController } from ‘@umbraco-cms/backoffice/router’;
import { UMB_WORKSPACE_MODAL } from ‘@umbraco-cms/backoffice/workspace’;
import { UMB_DOCUMENT_ENTITY_TYPE } from ‘@umbraco-cms/backoffice/document’;
import { UMB_EDIT_DOCUMENT_WORKSPACE_PATH_PATTERN } from ‘@umbraco-cms/backoffice/document’;

Open a workspace modal from a function (or event)

async openContentEditor(options) {
    // create a new modal route registration controller
    this._contentEditorWorkspaceModal = new UmbModalRouteRegistrationController(this, UMB_WORKSPACE_MODAL)
        .onSetup(async () => {
            return {
                data: {
                    entityType: UMB_DOCUMENT_ENTITY_TYPE,
                    preset: { }
                }
            };
        })
        .onSubmit(async () => {
            if (options.submitCallback) {
                options.submitCallback?.();
            }
        })
        .onReject(async () => {
            if (options.closeCallback) {
                options.closeCallback?.();
            }
        })
        .observeRouteBuilder((routeBuilder) => {
            // compose the workspace modal href
            this._editContentPath = routeBuilder({ });
            const hrefPath = UMB_EDIT_DOCUMENT_WORKSPACE_PATH_PATTERN.generateLocal({ unique: options.id });

            let href = `${this._editContentPath}/${hrefPath}`;
            if (href) {
                // navigate to the workspace modal href, will be picked up by the controller
                history.pushState(null, '', href);
                window.dispatchEvent(new PopStateEvent('popstate'));
            }
        });
}

Hookup to an event

onOpenEditorClick() {
let cmsId = ‘node-guid’;
this.openContentEditor({
id: cmsId,
submitCallback: function(result) { //logic when submitted
},
closeCallback: function() { //logic when closed
}
});
}

1 Like

Additionally, here’s an extended example of how to open a create OR edit content modal:

// IMPORTS
import { UmbModalRouteRegistrationController } from ‘@umbraco-cms/backoffice/router’;
import { UMB_WORKSPACE_MODAL } from ‘@umbraco-cms/backoffice/workspace’;
import { UMB_DOCUMENT_ENTITY_TYPE } from ‘@umbraco-cms/backoffice/document’;
import { UMB_EDIT_DOCUMENT_WORKSPACE_PATH_PATTERN, UMB_CREATE_DOCUMENT_WORKSPACE_PATH_PATTERN } from ‘@umbraco-cms/backoffice/document’;

// METHOD TO OPEN WORKSPACE MODAL
async openContentEditor(options) {
    if (options.create) {
        // create a new modal route registration controller
        this._contentEditorWorkspaceModal = new UmbModalRouteRegistrationController(this, UMB_WORKSPACE_MODAL)
            .onSetup(async () => {
                return {
                    data: {
                        entityType: UMB_DOCUMENT_ENTITY_TYPE,
                        preset: {}
                    }
                };
            })
            .onSubmit(async () => {
                if (options.submitCallback) {
                    options.submitCallback?.();
                }
            })
            .onReject(async () => {
                if (options.closeCallback) {
                    options.closeCallback?.();
                }
            })
            .observeRouteBuilder((routeBuilder) => {
                // compose the workspace modal href
                this._createContentPath = routeBuilder({});
                const hrefPath = UMB_CREATE_DOCUMENT_WORKSPACE_PATH_PATTERN.generateLocal({
                    parentEntityType: 'document',
                    parentUnique: options.parentId,
                    documentTypeUnique: options.documentTypeId
                });
                
                let href = `${this._createContentPath}/${hrefPath}`;
                if (href) {
                    // navigate to the workspace modal href, will be picked up by the controller
                    history.pushState(null, '', href);
                    window.dispatchEvent(new PopStateEvent('popstate'));
                }
            });
    }
    else {
        // create a new modal route registration controller
        this._contentEditorWorkspaceModal = new UmbModalRouteRegistrationController(this, UMB_WORKSPACE_MODAL)
            .onSetup(async () => {
                return {
                    data: {
                        entityType: UMB_DOCUMENT_ENTITY_TYPE,
                        preset: {}
                    }
                };
            })
            .onSubmit(async () => {
                if (options.submitCallback) {
                    options.submitCallback?.();
                }
            })
            .onReject(async () => {
                if (options.closeCallback) {
                    options.closeCallback?.();
                }
            })
            .observeRouteBuilder((routeBuilder) => {
                // compose the workspace modal href
                this._editContentPath = routeBuilder({});
                const hrefPath = UMB_EDIT_DOCUMENT_WORKSPACE_PATH_PATTERN.generateLocal({ unique: options.id });

                let href = `${this._editContentPath}/${hrefPath}`;
                if (href) {
                    // navigate to the workspace modal href, will be picked up by the controller
                    history.pushState(null, '', href);
                    window.dispatchEvent(new PopStateEvent('popstate'));
                }
            });
    }
}

let cmsId = 'guid-of-node-to-edit-or-create-content-under';

// Opening a CREATE CONTENT modal
let modelCreateContentModal = {
    create: true,
    documentTypeAlias: cmsContentTypeAlias,
    documentTypeId: cmsContentTypeId,
    parentId: cmsId,
    submitCallback: function (result) {
    },
    closeCallback: function () {
    }
};
this.openContentEditor(modelCreateContentModal);

//...OR...

// Opening a EDIT CONTENT modal
let modelEditContentModal = {
    id: cmsId,
    submitCallback: function (result) {
    },
    closeCallback: function () {
    }
};
this.openContentEditor(modelEditContentModal);

Hope this might help somebody in the future!

3 Likes

Hi @itsmekoopa

Thanks for the extensive insight.

There is one detail I think is important not to miss, which is the ability to link into such a modal without interaction. Like when sending the URL to a colleague, or just returning to a browser that remembers your last visit.

In your code, you are first creating the Routable Modal when the user interacts to open the modal, instead, the Routable Modal should be created up front. Like part of the constructor.
This will enable the user via URL to get into the modal — no clicking on buttons.
This technically also means you should only generate the _editContentPath as part of the observeRouteBuilder-callback.

The rest of that callback should be part of the rendering logic of the specific a-tag(button), cause here you know the specific guid/unique to open. Use this logic to generate a href and insert that into the HTML.

const hrefPath = UMB_EDIT_DOCUMENT_WORKSPACE_PATH_PATTERN.generateLocal({ unique: id});
 let href = `${this._editContentPath}/${hrefPath}`;

In this way, the user is also able to CTRL/CMS+click the link to open that editor in a new tab. Or just right click and copy the link to send it to a colleague.

I hope that makes sense, and you can verify everything works by experiencing that you get to the Content Editor by refreshing the browser.

Good luck

1 Like

Thank you, @nielslyngsoe . Will look into that when I’m back at this project!

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.